Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/certbot/certbot.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'letsencrypt-auto-source/rebuild_dependencies.py')
-rwxr-xr-xletsencrypt-auto-source/rebuild_dependencies.py283
1 files changed, 283 insertions, 0 deletions
diff --git a/letsencrypt-auto-source/rebuild_dependencies.py b/letsencrypt-auto-source/rebuild_dependencies.py
new file mode 100755
index 000000000..6d1ec15ff
--- /dev/null
+++ b/letsencrypt-auto-source/rebuild_dependencies.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python
+"""
+Gather and consolidate the up-to-date dependencies available and required to install certbot
+on various Linux distributions. It generates a requirements file contained the pinned and hashed
+versions, ready to be used by pip to install the certbot dependencies.
+
+This script is typically used to update the certbot-requirements.txt file of certbot-auto.
+
+To achieve its purpose, this script will start a certbot installation with unpinned dependencies,
+then gather them, on various distributions started as Docker containers.
+
+Usage: letsencrypt-auto-source/rebuild_dependencies new_requirements.txt
+
+NB1: Docker must be installed on the machine running this script.
+NB2: Python library 'hashin' must be installed on the machine running this script.
+"""
+from __future__ import print_function
+import re
+import shutil
+import subprocess
+import tempfile
+import os
+from os.path import dirname, abspath, join
+import sys
+import argparse
+
+# The list of docker distributions to test dependencies against with.
+DISTRIBUTION_LIST = [
+ 'ubuntu:18.04', 'ubuntu:16.04',
+ 'debian:stretch', 'debian:jessie',
+ 'centos:7', 'centos:6',
+ 'opensuse/leap:15',
+ 'fedora:29',
+]
+
+# These constraints will be added while gathering dependencies on each distribution.
+# It can be used because a particular version for a package is required for any reason,
+# or to solve a version conflict between two distributions requirements.
+AUTHORITATIVE_CONSTRAINTS = {
+ # Using an older version of mock here prevents regressions of #5276.
+ 'mock': '1.3.0',
+ # Too touchy to move to a new version. And will be removed soon
+ # in favor of pure python parser for Apache.
+ 'python-augeas': '0.5.0',
+ # Package enum34 needs to be explicitly limited to Python2.x, in order to avoid
+ # certbot-auto failures on Python 3.6+ which enum34 doesn't support. See #5456.
+ # TODO: hashin seems to overwrite environment markers in dependencies. This needs to be fixed.
+ 'enum34': '1.1.6 ; python_version < \'3.4\'',
+}
+
+
+# ./certbot/letsencrypt-auto-source/rebuild_dependencies.py (2 levels from certbot root path)
+CERTBOT_REPO_PATH = dirname(dirname(abspath(__file__)))
+
+# The script will be used to gather dependencies for a given distribution.
+# - certbot-auto is used to install relevant OS packages, and set up an initial venv
+# - then this venv is used to consistently construct an empty new venv
+# - once pipstraped, this new venv pip-installs certbot runtime (including apache/nginx),
+# without pinned dependencies, and respecting input authoritative requirements
+# - `certbot plugins` is called to check we have a healthy environment
+# - finally current set of dependencies is extracted out of the docker using pip freeze
+SCRIPT = r"""#!/bin/sh
+set -e
+
+cd /tmp/certbot
+letsencrypt-auto-source/letsencrypt-auto --install-only -n
+PYVER=`/opt/eff.org/certbot/venv/bin/python --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'`
+
+/opt/eff.org/certbot/venv/bin/python letsencrypt-auto-source/pieces/create_venv.py /tmp/venv "$PYVER" 1
+
+/tmp/venv/bin/python letsencrypt-auto-source/pieces/pipstrap.py
+/tmp/venv/bin/pip install -e acme -e certbot -e certbot-apache -e certbot-nginx -c /tmp/constraints.txt
+/tmp/venv/bin/certbot plugins
+/tmp/venv/bin/pip freeze >> /tmp/workspace/requirements.txt
+"""
+
+
+def _read_from(file):
+ """Read all content of the file, and return it as a string."""
+ with open(file, 'r') as file_h:
+ return file_h.read()
+
+
+def _write_to(file, content):
+ """Write given string content to the file, overwriting its initial content."""
+ with open(file, 'w') as file_h:
+ file_h.write(content)
+
+
+def _requirements_from_one_distribution(distribution, verbose):
+ """
+ Calculate the Certbot dependencies expressed for the given distribution, using the official
+ Docker for this distribution, and return the lines of the generated requirements file.
+ """
+ print('===> Gathering dependencies for {0}.'.format(distribution))
+ workspace = tempfile.mkdtemp()
+ script = join(workspace, 'script.sh')
+ authoritative_constraints = join(workspace, 'constraints.txt')
+ cid_file = join(workspace, 'cid')
+
+ try:
+ _write_to(script, SCRIPT)
+ os.chmod(script, 0o755)
+
+ _write_to(authoritative_constraints, '\n'.join(
+ ['{0}=={1}'.format(package, version) for package, version in AUTHORITATIVE_CONSTRAINTS.items()]))
+
+ command = ['docker', 'run', '--rm', '--cidfile', cid_file,
+ '-v', '{0}:/tmp/certbot'.format(CERTBOT_REPO_PATH),
+ '-v', '{0}:/tmp/workspace'.format(workspace),
+ '-v', '{0}:/tmp/constraints.txt'.format(authoritative_constraints),
+ distribution, '/tmp/workspace/script.sh']
+ sub_stdout = sys.stdout if verbose else subprocess.PIPE
+ sub_stderr = sys.stderr if verbose else subprocess.STDOUT
+ process = subprocess.Popen(command, stdout=sub_stdout, stderr=sub_stderr, universal_newlines=True)
+ stdoutdata, _ = process.communicate()
+
+ if process.returncode:
+ if stdoutdata:
+ sys.stderr.write('Output was:\n{0}'.format(stdoutdata))
+ raise RuntimeError('Error while gathering dependencies for {0}.'.format(distribution))
+
+ with open(join(workspace, 'requirements.txt'), 'r') as file_h:
+ return file_h.readlines()
+ finally:
+ if os.path.isfile(cid_file):
+ cid = _read_from(cid_file)
+ try:
+ subprocess.check_output(['docker', 'kill', cid], stderr=subprocess.PIPE)
+ except subprocess.CalledProcessError:
+ pass
+ shutil.rmtree(workspace)
+
+
+def _parse_and_merge_requirements(dependencies_map, requirements_file_lines, distribution):
+ """
+ Extract every requirement from the given requirements file, and merge it in the dependency map.
+ Merging here means that the map contain every encountered dependency, and the version used in
+ each distribution.
+
+ Example:
+ # dependencies_map = {
+ # }
+ _parse_and_merge_requirements(['cryptography=='1.2','requests=='2.1.0'], dependencies_map, 'debian:stretch')
+ # dependencies_map = {
+ # 'cryptography': [('1.2', 'debian:stretch)],
+ # 'requests': [('2.1.0', 'debian:stretch')]
+ # }
+ _parse_and_merge_requirements(['requests=='2.4.0', 'mock==1.3'], dependencies_map, 'centos:7')
+ # dependencies_map = {
+ # 'cryptography': [('1.2', 'debian:stretch)],
+ # 'requests': [('2.1.0', 'debian:stretch'), ('2.4.0', 'centos:7')],
+ # 'mock': [('2.4.0', 'centos:7')]
+ # }
+ """
+ for line in requirements_file_lines:
+ match = re.match(r'([^=]+)==([^=]+)', line.strip())
+ if not line.startswith('-e') and match:
+ package, version = match.groups()
+ if package not in ['acme', 'certbot', 'certbot-apache', 'certbot-nginx', 'pkg-resources']:
+ dependencies_map.setdefault(package, []).append((version, distribution))
+
+
+def _consolidate_and_validate_dependencies(dependency_map):
+ """
+ Given the dependency map of all requirements found in all distributions for Certbot,
+ construct an array containing the unit requirements for Certbot to be used by pip,
+ and the version conflicts, if any, between several distributions for a package.
+ Return requirements and conflicts as a tuple.
+ """
+ print('===> Consolidate and validate the dependency map.')
+ requirements = []
+ conflicts = []
+ for package, versions in dependency_map.items():
+ reduced_versions = _reduce_versions(versions)
+
+ if len(reduced_versions) > 1:
+ version_list = ['{0} ({1})'.format(version, ','.join(distributions))
+ for version, distributions in reduced_versions.items()]
+ conflict = ('package {0} is declared with several versions: {1}'
+ .format(package, ', '.join(version_list)))
+ conflicts.append(conflict)
+ sys.stderr.write('ERROR: {0}\n'.format(conflict))
+ else:
+ requirements.append((package, list(reduced_versions)[0]))
+
+ requirements.sort(key=lambda x: x[0])
+ return requirements, conflicts
+
+
+def _reduce_versions(version_dist_tuples):
+ """
+ Get an array of version/distribution tuples,
+ and reduce it to a map based on the version values.
+
+ Example: [('1.2.0', 'debian:stretch'), ('1.4.0', 'ubuntu:18.04'), ('1.2.0', 'centos:6')]
+ => {'1.2.0': ['debiqn:stretch', 'centos:6'], '1.4.0': ['ubuntu:18.04']}
+ """
+ version_dist_map = {}
+ for version, distribution in version_dist_tuples:
+ version_dist_map.setdefault(version, []).append(distribution)
+
+ return version_dist_map
+
+
+def _write_requirements(dest_file, requirements, conflicts):
+ """
+ Given the list of requirements and conflicts, write a well-formatted requirements file,
+ whose requirements are hashed signed using hashin library. Conflicts are written at the end
+ of the generated file.
+ """
+ print('===> Calculating hashes for the requirement file.')
+
+ _write_to(dest_file, '''\
+# This is the flattened list of packages certbot-auto installs.
+# To generate this, do (with docker and package hashin installed):
+# ```
+# letsencrypt-auto-source/rebuild_dependencies.py \\
+# letsencrypt-auto-source/pieces/dependency-requirements.txt
+# ```
+# If you want to update a single dependency, run commands similar to these:
+# ```
+# pip install hashin
+# hashin -r dependency-requirements.txt cryptography==1.5.2
+# ```
+''')
+
+ for req in requirements:
+ subprocess.check_call(['hashin', '{0}=={1}'.format(req[0], req[1]),
+ '--requirements-file', dest_file])
+
+ if conflicts:
+ with open(dest_file, 'a') as file_h:
+ file_h.write('\n## ! SOME ERRORS OCCURRED ! ##\n')
+ file_h.write('\n'.join('# {0}'.format(conflict) for conflict in conflicts))
+ file_h.write('\n')
+
+ return _read_from(dest_file)
+
+
+def _gather_dependencies(dest_file, verbose):
+ """
+ Main method of this script. Given a destination file path, will write the file
+ containing the consolidated and hashed requirements for Certbot, validated
+ against several Linux distributions.
+ """
+ dependencies_map = {}
+
+ for distribution in DISTRIBUTION_LIST:
+ requirements_file_lines = _requirements_from_one_distribution(distribution, verbose)
+ _parse_and_merge_requirements(dependencies_map, requirements_file_lines, distribution)
+
+ requirements, conflicts = _consolidate_and_validate_dependencies(dependencies_map)
+
+ return _write_requirements(dest_file, requirements, conflicts)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(
+ description=('Build a sanitized, pinned and hashed requirements file for certbot-auto, '
+ 'validated against several OS distributions using Docker.'))
+ parser.add_argument('requirements_path',
+ help='path for the generated requirements file')
+ parser.add_argument('--verbose', '-v', action='store_true',
+ help='verbose will display all output during docker execution')
+
+ namespace = parser.parse_args()
+
+ try:
+ subprocess.check_output(['hashin', '--version'])
+ except subprocess.CalledProcessError:
+ raise RuntimeError('Python library hashin is not installed in the current environment.')
+
+ try:
+ subprocess.check_output(['docker', '--version'], stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError:
+ raise RuntimeError('Docker is not installed or accessible to current user.')
+
+ file_content = _gather_dependencies(namespace.requirements_path, namespace.verbose)
+
+ print(file_content)
+ print('===> Rebuilt requirement file is available on path {0}'
+ .format(abspath(namespace.requirements_path)))