diff --git a/appveyor.yml b/appveyor.yml index 33f522df1..53f29a5e6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,6 +4,7 @@ environment: matrix: - TOXENV: py35 - TOXENV: py37-cover + - TOXENV: integration-certbot branches: only: @@ -24,14 +25,16 @@ init: install: # Use Python 3.7 by default - - "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%" + - SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH% + # Using 4 processes is proven to be the most efficient integration tests config for AppVeyor + - IF %TOXENV%==integration-certbot SET PYTEST_ADDOPTS=--numprocesses=4 # Check env - - "python --version" + - python --version # Upgrade pip to avoid warnings - - "python -m pip install --upgrade pip" + - python -m pip install --upgrade pip # Ready to install tox and coverage # tools/pip_install.py is used to pin packages to a known working version. - - "python tools\\pip_install.py tox codecov" + - python tools\\pip_install.py tox codecov build: off diff --git a/certbot-ci/certbot_integration_tests/assets/hook.py b/certbot-ci/certbot_integration_tests/assets/hook.py new file mode 100755 index 000000000..ff735a216 --- /dev/null +++ b/certbot-ci/certbot_integration_tests/assets/hook.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +import sys +import os + +hook_script_type = os.path.basename(os.path.dirname(sys.argv[1])) +if hook_script_type == 'deploy' and ('RENEWED_DOMAINS' not in os.environ or 'RENEWED_LINEAGE' not in os.environ): + sys.stderr.write('Environment variables not properly set!\n') + sys.exit(1) + +with open(sys.argv[2], 'a') as file_h: + file_h.write(hook_script_type + '\n') diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py index b82c0b5f0..5177ffbd2 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py @@ -1,6 +1,17 @@ """This module contains advanced assertions for the certbot integration tests.""" import os -import grp +try: + import grp + POSIX_MODE = True +except ImportError: + import win32api + import win32security + import ntsecuritycon + POSIX_MODE = False + +EVERYBODY_SID = 'S-1-1-0' +SYSTEM_SID = 'S-1-5-18' +ADMINS_SID = 'S-1-5-32-544' def assert_hook_execution(probe_path, probe_content): @@ -10,9 +21,10 @@ def assert_hook_execution(probe_path, probe_content): :param probe_content: content expected when the hook is executed """ with open(probe_path, 'r') as file: - lines = file.readlines() + data = file.read() - assert '{0}{1}'.format(probe_content, os.linesep) in lines + lines = [line.strip() for line in data.splitlines()] + assert probe_content in lines def assert_saved_renew_hook(config_dir, lineage): @@ -38,16 +50,51 @@ def assert_cert_count_for_lineage(config_dir, lineage, count): assert len(certs) == count -def assert_equals_permissions(file1, file2, mask): +def assert_equals_group_permissions(file1, file2): """ - Assert that permissions on two files are identical in respect to a given umask. + Assert that two files have the same permissions for group owner. :param file1: first file path to compare :param file2: second file path to compare - :param mask: 3-octal representation of a POSIX umask under which the two files mode - should match (eg. 0o074 will test RWX on group and R on world) """ - mode_file1 = os.stat(file1).st_mode & mask - mode_file2 = os.stat(file2).st_mode & mask + # On Windows there is no group, so this assertion does nothing on this platform + if POSIX_MODE: + mode_file1 = os.stat(file1).st_mode & 0o070 + mode_file2 = os.stat(file2).st_mode & 0o070 + + assert mode_file1 == mode_file2 + + +def assert_equals_world_read_permissions(file1, file2): + """ + Assert that two files have the same read permissions for everyone. + :param file1: first file path to compare + :param file2: second file path to compare + """ + if POSIX_MODE: + mode_file1 = os.stat(file1).st_mode & 0o004 + mode_file2 = os.stat(file2).st_mode & 0o004 + else: + everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) + + security1 = win32security.GetFileSecurity(file1, win32security.DACL_SECURITY_INFORMATION) + dacl1 = security1.GetSecurityDescriptorDacl() + + mode_file1 = dacl1.GetEffectiveRightsFromAcl({ + 'TrusteeForm': win32security.TRUSTEE_IS_SID, + 'TrusteeType': win32security.TRUSTEE_IS_USER, + 'Identifier': everybody, + }) + mode_file1 = mode_file1 & ntsecuritycon.FILE_GENERIC_READ + + security2 = win32security.GetFileSecurity(file2, win32security.DACL_SECURITY_INFORMATION) + dacl2 = security2.GetSecurityDescriptorDacl() + + mode_file2 = dacl2.GetEffectiveRightsFromAcl({ + 'TrusteeForm': win32security.TRUSTEE_IS_SID, + 'TrusteeType': win32security.TRUSTEE_IS_USER, + 'Identifier': everybody, + }) + mode_file2 = mode_file2 & ntsecuritycon.FILE_GENERIC_READ assert mode_file1 == mode_file2 @@ -57,20 +104,57 @@ def assert_equals_group_owner(file1, file2): Assert that two files have the same group owner. :param file1: first file path to compare :param file2: second file path to compare - :return: """ - group_owner_file1 = grp.getgrgid(os.stat(file1).st_gid)[0] - group_owner_file2 = grp.getgrgid(os.stat(file2).st_gid)[0] + # On Windows there is no group, so this assertion does nothing on this platform + if POSIX_MODE: + group_owner_file1 = grp.getgrgid(os.stat(file1).st_gid)[0] + group_owner_file2 = grp.getgrgid(os.stat(file2).st_gid)[0] - assert group_owner_file1 == group_owner_file2 + assert group_owner_file1 == group_owner_file2 -def assert_world_permissions(file, mode): +def assert_world_no_permissions(file): """ - Assert that a file has the expected world permission. - :param file: file path to check - :param mode: world permissions mode expected + Assert that the given file is not world-readable. + :param file: path of the file to check """ - mode_file_all = os.stat(file).st_mode & 0o007 + if POSIX_MODE: + mode_file_all = os.stat(file).st_mode & 0o007 + assert mode_file_all == 0 + else: + security = win32security.GetFileSecurity(file, win32security.DACL_SECURITY_INFORMATION) + dacl = security.GetSecurityDescriptorDacl() + mode = dacl.GetEffectiveRightsFromAcl({ + 'TrusteeForm': win32security.TRUSTEE_IS_SID, + 'TrusteeType': win32security.TRUSTEE_IS_USER, + 'Identifier': win32security.ConvertStringSidToSid(EVERYBODY_SID), + }) - assert mode_file_all == mode + assert not mode + + +def assert_world_read_permissions(file): + """ + Assert that the given file is world-readable, but not world-writable or world-executable. + :param file: path of the file to check + """ + if POSIX_MODE: + mode_file_all = os.stat(file).st_mode & 0o007 + assert mode_file_all == 4 + else: + security = win32security.GetFileSecurity(file, win32security.DACL_SECURITY_INFORMATION) + dacl = security.GetSecurityDescriptorDacl() + mode = dacl.GetEffectiveRightsFromAcl({ + 'TrusteeForm': win32security.TRUSTEE_IS_SID, + 'TrusteeType': win32security.TRUSTEE_IS_USER, + 'Identifier': win32security.ConvertStringSidToSid(EVERYBODY_SID), + }) + + assert not mode & ntsecuritycon.FILE_GENERIC_WRITE + assert not mode & ntsecuritycon.FILE_GENERIC_EXECUTE + assert mode & ntsecuritycon.FILE_GENERIC_READ == ntsecuritycon.FILE_GENERIC_READ + + +def _get_current_user(): + account_name = win32api.GetUserNameEx(win32api.NameSamCompatible) + return win32security.LookupAccountName(None, account_name)[0] diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/context.py b/certbot-ci/certbot_integration_tests/certbot_tests/context.py index 562748fb6..6f8670000 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/context.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/context.py @@ -1,4 +1,5 @@ """Module to handle the context of integration tests.""" +import logging import os import shutil import sys @@ -30,7 +31,10 @@ class IntegrationTestsContext(object): self.workspace = tempfile.mkdtemp() self.config_dir = os.path.join(self.workspace, 'conf') - self.hook_probe = tempfile.mkstemp(dir=self.workspace)[1] + + probe = tempfile.mkstemp(dir=self.workspace) + os.close(probe[0]) + self.hook_probe = probe[1] self.manual_dns_auth_hook = ( '{0} -c "import os; import requests; import json; ' diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py index a2d1e7123..80fa15584 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py @@ -11,8 +11,11 @@ from os.path import join, exists import pytest from certbot_integration_tests.certbot_tests import context as certbot_context from certbot_integration_tests.certbot_tests.assertions import ( - assert_hook_execution, assert_saved_renew_hook, assert_cert_count_for_lineage, - assert_world_permissions, assert_equals_group_owner, assert_equals_permissions, + assert_hook_execution, assert_saved_renew_hook, + assert_cert_count_for_lineage, + assert_world_no_permissions, assert_world_read_permissions, + assert_equals_group_owner, assert_equals_group_permissions, assert_equals_world_read_permissions, + EVERYBODY_SID ) from certbot_integration_tests.utils import misc @@ -84,9 +87,9 @@ def test_http_01(context): context.certbot([ '--domains', certname, '--preferred-challenges', 'http-01', 'run', '--cert-name', certname, - '--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe), - '--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe), - '--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe) + '--pre-hook', misc.echo('wtf_pre', context.hook_probe), + '--post-hook', misc.echo('wtf_post', context.hook_probe), + '--deploy-hook', misc.echo('deploy', context.hook_probe), ]) assert_hook_execution(context.hook_probe, 'deploy') @@ -104,9 +107,9 @@ def test_manual_http_auth(context): '--cert-name', certname, '--manual-auth-hook', scripts[0], '--manual-cleanup-hook', scripts[1], - '--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe), - '--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe), - '--renew-hook', 'echo renew >> "{0}"'.format(context.hook_probe) + '--pre-hook', misc.echo('wtf_pre', context.hook_probe), + '--post-hook', misc.echo('wtf_post', context.hook_probe), + '--renew-hook', misc.echo('renew', context.hook_probe), ]) with pytest.raises(AssertionError): @@ -122,9 +125,9 @@ def test_manual_dns_auth(context): 'run', '--cert-name', certname, '--manual-auth-hook', context.manual_dns_auth_hook, '--manual-cleanup-hook', context.manual_dns_cleanup_hook, - '--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe), - '--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe), - '--renew-hook', 'echo renew >> "{0}"'.format(context.hook_probe) + '--pre-hook', misc.echo('wtf_pre', context.hook_probe), + '--post-hook', misc.echo('wtf_post', context.hook_probe), + '--renew-hook', misc.echo('renew', context.hook_probe), ]) with pytest.raises(AssertionError): @@ -173,21 +176,19 @@ def test_renew_files_permissions(context): certname = context.get_domain('renew') context.certbot(['-d', certname]) + privkey1 = join(context.config_dir, 'archive', certname, 'privkey1.pem') + privkey2 = join(context.config_dir, 'archive', certname, 'privkey2.pem') + assert_cert_count_for_lineage(context.config_dir, certname, 1) - assert_world_permissions( - join(context.config_dir, 'archive', certname, 'privkey1.pem'), 0) + assert_world_no_permissions(privkey1) context.certbot(['renew']) assert_cert_count_for_lineage(context.config_dir, certname, 2) - assert_world_permissions( - join(context.config_dir, 'archive', certname, 'privkey2.pem'), 0) - assert_equals_group_owner( - join(context.config_dir, 'archive', certname, 'privkey1.pem'), - join(context.config_dir, 'archive', certname, 'privkey2.pem')) - assert_equals_permissions( - join(context.config_dir, 'archive', certname, 'privkey1.pem'), - join(context.config_dir, 'archive', certname, 'privkey2.pem'), 0o074) + assert_world_no_permissions(privkey2) + assert_equals_group_owner(privkey1, privkey2) + assert_equals_world_read_permissions(privkey1, privkey2) + assert_equals_group_permissions(privkey1, privkey2) def test_renew_with_hook_scripts(context): @@ -211,15 +212,35 @@ def test_renew_files_propagate_permissions(context): assert_cert_count_for_lineage(context.config_dir, certname, 1) - os.chmod(join(context.config_dir, 'archive', certname, 'privkey1.pem'), 0o444) + privkey1 = join(context.config_dir, 'archive', certname, 'privkey1.pem') + privkey2 = join(context.config_dir, 'archive', certname, 'privkey2.pem') + + if os.name != 'nt': + os.chmod(privkey1, 0o444) + else: + import win32security + import ntsecuritycon + # Get the current DACL of the private key + security = win32security.GetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION) + dacl = security.GetSecurityDescriptorDacl() + # Create a read permission for Everybody group + everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) + dacl.AddAccessAllowedAce(win32security.ACL_REVISION, ntsecuritycon.FILE_GENERIC_READ, everybody) + # Apply the updated DACL to the private key + security.SetSecurityDescriptorDacl(1, dacl, 0) + win32security.SetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION, security) + context.certbot(['renew']) assert_cert_count_for_lineage(context.config_dir, certname, 2) - assert_world_permissions( - join(context.config_dir, 'archive', certname, 'privkey2.pem'), 4) - assert_equals_permissions( - join(context.config_dir, 'archive', certname, 'privkey1.pem'), - join(context.config_dir, 'archive', certname, 'privkey2.pem'), 0o074) + if os.name != 'nt': + # On Linux, read world permissions + all group permissions will be copied from the previous private key + assert_world_read_permissions(privkey2) + assert_equals_world_read_permissions(privkey1, privkey2) + assert_equals_group_permissions(privkey1, privkey2) + else: + # On Windows, world will never have any permissions, and group permission is irrelevant for this platform + assert_world_no_permissions(privkey2) def test_graceful_renew_it_is_not_time(context): @@ -229,7 +250,7 @@ def test_graceful_renew_it_is_not_time(context): assert_cert_count_for_lineage(context.config_dir, certname, 1) - context.certbot(['renew', '--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe)], + context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)], force_renew=False) assert_cert_count_for_lineage(context.config_dir, certname, 1) @@ -250,7 +271,7 @@ def test_graceful_renew_it_is_time(context): with open(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)), 'w') as file: file.writelines(lines) - context.certbot(['renew', '--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe)], + context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)], force_renew=False) assert_cert_count_for_lineage(context.config_dir, certname, 2) @@ -317,9 +338,9 @@ def test_renew_hook_override(context): context.certbot([ 'certonly', '-d', certname, '--preferred-challenges', 'http-01', - '--pre-hook', 'echo pre >> "{0}"'.format(context.hook_probe), - '--post-hook', 'echo post >> "{0}"'.format(context.hook_probe), - '--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe) + '--pre-hook', misc.echo('pre', context.hook_probe), + '--post-hook', misc.echo('post', context.hook_probe), + '--deploy-hook', misc.echo('deploy', context.hook_probe), ]) assert_hook_execution(context.hook_probe, 'pre') @@ -330,14 +351,14 @@ def test_renew_hook_override(context): open(context.hook_probe, 'w').close() context.certbot([ 'renew', '--cert-name', certname, - '--pre-hook', 'echo pre-override >> "{0}"'.format(context.hook_probe), - '--post-hook', 'echo post-override >> "{0}"'.format(context.hook_probe), - '--deploy-hook', 'echo deploy-override >> "{0}"'.format(context.hook_probe) + '--pre-hook', misc.echo('pre_override', context.hook_probe), + '--post-hook', misc.echo('post_override', context.hook_probe), + '--deploy-hook', misc.echo('deploy_override', context.hook_probe), ]) - assert_hook_execution(context.hook_probe, 'pre-override') - assert_hook_execution(context.hook_probe, 'post-override') - assert_hook_execution(context.hook_probe, 'deploy-override') + assert_hook_execution(context.hook_probe, 'pre_override') + assert_hook_execution(context.hook_probe, 'post_override') + assert_hook_execution(context.hook_probe, 'deploy_override') with pytest.raises(AssertionError): assert_hook_execution(context.hook_probe, 'pre') with pytest.raises(AssertionError): @@ -349,11 +370,11 @@ def test_renew_hook_override(context): open(context.hook_probe, 'w').close() context.certbot(['renew', '--cert-name', certname]) - assert_hook_execution(context.hook_probe, 'pre-override') - assert_hook_execution(context.hook_probe, 'post-override') - assert_hook_execution(context.hook_probe, 'deploy-override') + assert_hook_execution(context.hook_probe, 'pre_override') + assert_hook_execution(context.hook_probe, 'post_override') + assert_hook_execution(context.hook_probe, 'deploy_override') + - def test_invalid_domain_with_dns_challenge(context): """Test certificate issuance failure with DNS-01 challenge.""" # Manual dns auth hooks from misc are designed to fail if the domain contains 'fail-*'. @@ -512,7 +533,7 @@ def test_revoke_multiple_lineages(context): data = file.read() data = re.sub('archive_dir = .*\n', - 'archive_dir = {0}\n'.format(join(context.config_dir, 'archive', cert1)), + 'archive_dir = {0}\n'.format(join(context.config_dir, 'archive', cert1).replace('\\', '\\\\')), data) with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'w') as file: diff --git a/certbot-ci/certbot_integration_tests/utils/misc.py b/certbot-ci/certbot_integration_tests/utils/misc.py index 14e2b232e..c7d92a4e6 100644 --- a/certbot-ci/certbot_integration_tests/utils/misc.py +++ b/certbot-ci/certbot_integration_tests/utils/misc.py @@ -3,9 +3,11 @@ Misc module contains stateless functions that could be used during pytest execut or outside during setup/teardown of the integration tests environment. """ import contextlib +import logging import errno import multiprocessing import os +import re import shutil import stat import subprocess @@ -62,6 +64,10 @@ class GracefulTCPServer(socketserver.TCPServer): allow_reuse_address = True +def _run_server(port): + GracefulTCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler).serve_forever() + + @contextlib.contextmanager def create_http_server(port): """ @@ -74,10 +80,7 @@ def create_http_server(port): current_cwd = os.getcwd() webroot = tempfile.mkdtemp() - def run(): - GracefulTCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler).serve_forever() - - process = multiprocessing.Process(target=run) + process = multiprocessing.Process(target=_run_server, args=(port,)) try: # SimpleHTTPServer is designed to serve files from the current working directory at the @@ -119,15 +122,9 @@ def generate_test_file_hooks(config_dir, hook_probe): :param str config_dir: current certbot config directory :param hook_probe: path to the hook probe to test hook scripts execution """ - if sys.platform == 'win32': - extension = 'bat' - else: - extension = 'sh' + hook_path = pkg_resources.resource_filename('certbot_integration_tests', 'assets/hook.py') - renewal_hooks_dirs = list_renewal_hooks_dirs(config_dir) - renewal_deploy_hook_path = os.path.join(renewal_hooks_dirs[1], 'hook.sh') - - for hook_dir in renewal_hooks_dirs: + for hook_dir in list_renewal_hooks_dirs(config_dir): # We want an equivalent of bash `chmod -p $HOOK_DIR, that does not fail if one folder of # the hierarchy already exists. It is not the case of os.makedirs. Python 3 has an # optional parameter `exists_ok` to not fail on existing dir, but Python 2.7 does not. @@ -137,26 +134,25 @@ def generate_test_file_hooks(config_dir, hook_probe): except OSError as error: if error.errno != errno.EEXIST: raise - hook_path = os.path.join(hook_dir, 'hook.{0}'.format(extension)) - if extension == 'sh': - data = '''\ -#!/bin/bash -xe -if [ "$0" = "{0}" ]; then - if [ -z "$RENEWED_DOMAINS" -o -z "$RENEWED_LINEAGE" ]; then - echo "Environment variables not properly set!" >&2 - exit 1 - fi -fi -echo $(basename $(dirname "$0")) >> "{1}"\ -'''.format(renewal_deploy_hook_path, hook_probe) - else: - # TODO: Write the equivalent bat file for Windows - data = '''\ -''' - with open(hook_path, 'w') as file: - file.write(data) - os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IEXEC) + if os.name != 'nt': + entrypoint_script_path = os.path.join(hook_dir, 'entrypoint.sh') + entrypoint_script = '''\ +#!/usr/bin/env bash +set -e +"{0}" "{1}" "{2}" "{3}" +'''.format(sys.executable, hook_path, entrypoint_script_path, hook_probe) + else: + entrypoint_script_path = os.path.join(hook_dir, 'entrypoint.bat') + entrypoint_script = '''\ +@echo off +"{0}" "{1}" "{2}" "{3}" + '''.format(sys.executable, hook_path, entrypoint_script_path, hook_probe) + + with open(entrypoint_script_path, 'w') as file_h: + file_h.write(entrypoint_script) + + os.chmod(entrypoint_script_path, os.stat(entrypoint_script_path).st_mode | stat.S_IEXEC) @contextlib.contextmanager @@ -193,7 +189,7 @@ for _ in range(0, 10): except requests.exceptions.ConnectionError: pass raise ValueError('Error, url did not respond after 10 attempts: {{0}}'.format(url)) -'''.format(http_server_root, http_port)) +'''.format(http_server_root.replace('\\', '\\\\'), http_port)) os.chmod(auth_script_path, 0o755) cleanup_script_path = os.path.join(tempdir, 'cleanup.py') @@ -204,7 +200,7 @@ import os import shutil well_known = os.path.join('{0}', '.well-known') shutil.rmtree(well_known) -'''.format(http_server_root)) +'''.format(http_server_root.replace('\\', '\\\\'))) os.chmod(cleanup_script_path, 0o755) yield ('{0} {1}'.format(sys.executable, auth_script_path), @@ -287,4 +283,32 @@ def load_sample_data_path(workspace): original = pkg_resources.resource_filename('certbot_integration_tests', 'assets/sample-config') copied = os.path.join(workspace, 'sample-config') shutil.copytree(original, copied, symlinks=True) + + if os.name == 'nt': + # Fix the symlinks on Windows since GIT is not creating them upon checkout + for lineage in ['a.encryption-example.com', 'b.encryption-example.com']: + current_live = os.path.join(copied, 'live', lineage) + for name in os.listdir(current_live): + if name != 'README': + current_file = os.path.join(current_live, name) + with open(current_file) as file_h: + src = file_h.read() + os.unlink(current_file) + os.symlink(os.path.join(current_live, src), current_file) + return copied + + +def echo(keyword, path=None): + """ + Generate a platform independent executable command + that echoes the given keyword into the given file. + :param keyword: the keyword to echo (must be a single keyword) + :param path: path to the file were keyword is echoed + :return: the executable command + """ + if not re.match(r'^\w+$', keyword): + raise ValueError('Error, keyword `{0}` is not a single keyword.' + .format(keyword)) + return '{0} -c "from __future__ import print_function; print(\'{1}\')"{2}'.format( + os.path.basename(sys.executable), keyword, ' >> "{0}"'.format(path) if path else '') diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py index fcb393f1e..2b1557928 100644 --- a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py +++ b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py @@ -1,5 +1,4 @@ import json -import platform import os import stat @@ -13,9 +12,7 @@ ASSETS_PATH = pkg_resources.resource_filename('certbot_integration_tests', 'asse def fetch(workspace): - suffix = '{0}-{1}{2}'.format(platform.system().lower(), - platform.machine().lower().replace('x86_64', 'amd64'), - '.exe' if platform.system() == 'Windows' else '') + suffix = 'linux-amd64' if os.name != 'nt' else 'windows-amd64.exe' pebble_path = _fetch_asset('pebble', suffix) challtestsrv_path = _fetch_asset('pebble-challtestsrv', suffix) diff --git a/certbot-ci/setup.py b/certbot-ci/setup.py index 91ee1403b..8ab9b9659 100644 --- a/certbot-ci/setup.py +++ b/certbot-ci/setup.py @@ -1,5 +1,7 @@ -from setuptools import setup -from setuptools import find_packages +import sys + +from distutils.version import StrictVersion +from setuptools import setup, find_packages, __version__ as setuptools_version version = '0.32.0.dev0' @@ -17,6 +19,17 @@ install_requires = [ 'six', ] +# Add pywin32 on Windows platforms to handle low-level system calls. +# This dependency needs to be added using environment markers to avoid its installation on Linux. +# However environment markers are supported only with setuptools >= 36.2. +# So this dependency is not added for old Linux distributions with old setuptools, +# in order to allow these systems to build certbot from sources. +if StrictVersion(setuptools_version) >= StrictVersion('36.2'): + install_requires.append("pywin32>=224 ; sys_platform == 'win32'") +elif 'bdist_wheel' in sys.argv[1:]: + raise RuntimeError('Error, you are trying to build certbot wheels using an old version ' + 'of setuptools. Version 36.2+ of setuptools is required.') + setup( name='certbot-ci', version=version, diff --git a/setup.py b/setup.py index 84c27fce5..017b66619 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ install_requires = [ # So this dependency is not added for old Linux distributions with old setuptools, # in order to allow these systems to build certbot from sources. if StrictVersion(setuptools_version) >= StrictVersion('36.2'): - install_requires.append("pywin32 ; sys_platform == 'win32'") + install_requires.append("pywin32>=224 ; sys_platform == 'win32'") elif 'bdist_wheel' in sys.argv[1:]: raise RuntimeError('Error, you are trying to build certbot wheels using an old version ' 'of setuptools. Version 36.2+ of setuptools is required.') diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt index 660986da9..0db06a1f1 100644 --- a/tools/dev_constraints.txt +++ b/tools/dev_constraints.txt @@ -61,6 +61,7 @@ pytest-sugar==0.9.2 pytest-rerunfailures==4.2 python-dateutil==2.6.1 python-digitalocean==1.11 +pywin32==224 PyYAML==3.13 repoze.sphinx.autointerface==0.8 requests-file==1.4.2 diff --git a/tox.ini b/tox.ini index bbe3c7a05..a4f4bd3e3 100644 --- a/tox.ini +++ b/tox.ini @@ -232,6 +232,15 @@ commands = coverage report --include 'certbot-nginx/*' --show-missing --fail-under=74 passenv = DOCKER_* +[testenv:integration-certbot] +commands = + {[base]pip_install} acme . certbot-ci + pytest certbot-ci/certbot_integration_tests/certbot_tests \ + --acme-server={env:ACME_SERVER:pebble} \ + --cov=acme --cov=certbot --cov-report= \ + --cov-config=certbot-ci/certbot_integration_tests/.coveragerc + coverage report --include 'certbot/*' --show-missing --fail-under=62 + [testenv:integration-certbot-oldest] commands = {[base]pip_install} .