diff --git a/.travis.yml b/.travis.yml index 46b14fe63..3041fdd82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,15 @@ env: - TOXENV=lint - TOXENV=cover + +# Only build pushes to the master branch, PRs, and branches beginning with +# `test-`. This reduces the number of simultaneous Travis runs, which speeds +# turnaround time on review since there is a cap of 5 simultaneous runs. +branches: + only: + - master + - /^test-.*$/ + sudo: false # containers addons: # make sure simplehttp simple verification works (custom /etc/hosts) diff --git a/MANIFEST.in b/MANIFEST.in index 80fd8777e..e421e0cd7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,4 +5,5 @@ include CONTRIBUTING.md include LICENSE.txt include linter_plugin.py include letsencrypt/EULA +recursive-include docs * recursive-include letsencrypt/tests/testdata * diff --git a/acme/acme/jose/interfaces.py b/acme/acme/jose/interfaces.py index f841848b3..f85777a30 100644 --- a/acme/acme/jose/interfaces.py +++ b/acme/acme/jose/interfaces.py @@ -194,7 +194,7 @@ class JSONDeSerializable(object): :rtype: str """ - return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': ')) + return self.json_dumps(sort_keys=True, indent=4) @classmethod def json_dump_default(cls, python_object): diff --git a/acme/acme/jose/interfaces_test.py b/acme/acme/jose/interfaces_test.py index 380c3a2a5..84dc2a1be 100644 --- a/acme/acme/jose/interfaces_test.py +++ b/acme/acme/jose/interfaces_test.py @@ -1,6 +1,8 @@ """Tests for acme.jose.interfaces.""" import unittest +import six + class JSONDeSerializableTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes @@ -90,8 +92,9 @@ class JSONDeSerializableTest(unittest.TestCase): self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps()) def test_json_dumps_pretty(self): - self.assertEqual( - self.seq.json_dumps_pretty(), '[\n "foo1",\n "foo2"\n]') + filler = ' ' if six.PY2 else '' + self.assertEqual(self.seq.json_dumps_pretty(), + '[\n "foo1",{0}\n "foo2"\n]'.format(filler)) def test_json_dump_default(self): from acme.jose.interfaces import JSONDeSerializable diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index 7a976f189..74fa72319 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -1,10 +1,12 @@ """JSON Web Key.""" import abc import binascii +import json import logging import cryptography.exceptions from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import rsa @@ -27,6 +29,32 @@ class JWK(json_util.TypedJSONObjectWithFields): cryptography_key_types = () """Subclasses should override.""" + required = NotImplemented + """Required members of public key's representation as defined by JWK/JWA.""" + + _thumbprint_json_dumps_params = { + # "no whitespace or line breaks before or after any syntactic + # elements" + 'indent': 0, + 'separators': (',', ':'), + # "members ordered lexicographically by the Unicode [UNICODE] + # code points of the member names" + 'sort_keys': True, + } + + def thumbprint(self, hash_function=hashes.SHA256): + """Compute JWK Thumbprint. + + https://tools.ietf.org/html/rfc7638 + + """ + digest = hashes.Hash(hash_function(), backend=default_backend()) + digest.update(json.dumps( + dict((k, v) for k, v in six.iteritems(self.to_json()) + if k in self.required), + **self._thumbprint_json_dumps_params).encode()) + return digest.finalize() + @abc.abstractmethod def public_key(self): # pragma: no cover """Generate JWK with public key. @@ -60,7 +88,7 @@ class JWK(json_util.TypedJSONObjectWithFields): exceptions[loader] = error # no luck - raise errors.Error("Unable to deserialize key: {0}".format(exceptions)) + raise errors.Error('Unable to deserialize key: {0}'.format(exceptions)) @classmethod def load(cls, data, password=None, backend=None): @@ -81,17 +109,17 @@ class JWK(json_util.TypedJSONObjectWithFields): try: key = cls._load_cryptography_key(data, password, backend) except errors.Error as error: - logger.debug("Loading symmetric key, assymentric failed: %s", error) + logger.debug('Loading symmetric key, assymentric failed: %s', error) return JWKOct(key=data) if cls.typ is not NotImplemented and not isinstance( key, cls.cryptography_key_types): - raise errors.Error("Unable to deserialize {0} into {1}".format( + raise errors.Error('Unable to deserialize {0} into {1}'.format( key.__class__, cls.__class__)) for jwk_cls in six.itervalues(cls.TYPES): if isinstance(key, jwk_cls.cryptography_key_types): return jwk_cls(key=key) - raise errors.Error("Unsupported algorithm: {0}".format(key.__class__)) + raise errors.Error('Unsupported algorithm: {0}'.format(key.__class__)) @JWK.register @@ -105,6 +133,7 @@ class JWKES(JWK): # pragma: no cover typ = 'ES' cryptography_key_types = ( ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) + required = ('crv', JWK.type_field_name, 'x', 'y') def fields_to_partial_json(self): raise NotImplementedError() @@ -122,6 +151,7 @@ class JWKOct(JWK): """Symmetric JWK.""" typ = 'oct' __slots__ = ('key',) + required = ('k', JWK.type_field_name) def fields_to_partial_json(self): # TODO: An "alg" member SHOULD also be present to identify the @@ -150,6 +180,7 @@ class JWKRSA(JWK): typ = 'RSA' cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey) __slots__ = ('key',) + required = ('e', JWK.type_field_name, 'n') def __init__(self, *args, **kwargs): if 'key' in kwargs and not isinstance( @@ -204,7 +235,7 @@ class JWKRSA(JWK): jobj.get(x) for x in ('p', 'q', 'dp', 'dq', 'qi')) if tuple(param for param in all_params if param is None): raise errors.Error( - "Some private parameters are missing: {0}".format( + 'Some private parameters are missing: {0}'.format( all_params)) p, q, dp, dq, qi = tuple( cls._decode_param(x) for x in all_params) diff --git a/acme/acme/jose/jwk_test.py b/acme/acme/jose/jwk_test.py index 5462af6b0..d8a7410e8 100644 --- a/acme/acme/jose/jwk_test.py +++ b/acme/acme/jose/jwk_test.py @@ -25,9 +25,24 @@ class JWKTest(unittest.TestCase): self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM) -class JWKOctTest(unittest.TestCase): +class JWKTestBaseMixin(object): + """Mixin test for JWK subclass tests.""" + + thumbprint = NotImplemented + + def test_thumbprint_private(self): + self.assertEqual(self.thumbprint, self.jwk.thumbprint()) + + def test_thumbprint_public(self): + self.assertEqual(self.thumbprint, self.jwk.public_key().thumbprint()) + + +class JWKOctTest(unittest.TestCase, JWKTestBaseMixin): """Tests for acme.jose.jwk.JWKOct.""" + thumbprint = (b"=,\xdd;I\x1a+i\x02x\x8a\x12?06IM\xc2\x80" + b"\xe4\xc3\x1a\xfc\x89\xf3)'\xce\xccm\xfd5") + def setUp(self): from acme.jose.jwk import JWKOct self.jwk = JWKOct(key=b'foo') @@ -52,10 +67,13 @@ class JWKOctTest(unittest.TestCase): self.assertTrue(self.jwk.public_key() is self.jwk) -class JWKRSATest(unittest.TestCase): +class JWKRSATest(unittest.TestCase, JWKTestBaseMixin): """Tests for acme.jose.jwk.JWKRSA.""" # pylint: disable=too-many-instance-attributes + thumbprint = (b'\x08\xfa1\x87\x1d\x9b6H/*\x1eW\xc2\xe3\xf6P' + b'\xefs\x0cKB\x87\xcf\x85yO\x045\x0e\x91\x80\x0b') + def setUp(self): from acme.jose.jwk import JWKRSA self.jwk256 = JWKRSA(key=RSA256_KEY.public_key()) @@ -87,6 +105,7 @@ class JWKRSATest(unittest.TestCase): 'dq': 'bHh2u7etM8LKKCF2pY2UdQ', 'qi': 'oi45cEkbVoJjAbnQpFY87Q', }) + self.jwk = self.private def test_init_auto_comparable(self): self.assertTrue(isinstance( diff --git a/docs/contributing.rst b/docs/contributing.rst index 3277d321a..614f6f2aa 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -267,6 +267,22 @@ Please: .. _PEP 8 - Style Guide for Python Code: https://www.python.org/dev/peps/pep-0008 +Submitting a pull request +========================= + +Steps: + +1. Write your code! +2. Make sure your environment is set up properly and that you're in your + virtualenv. You can do this by running ``./bootstrap/dev/venv.sh``. + (this is a **very important** step) +3. Run ``./pep8.travis.sh`` to do a cursory check of your code style. + Fix any errors. +4. Run ``tox -e lint`` to check for pylint errors. Fix any errors. +5. Run ``tox`` to run the entire test suite including coverage. Fix any errors. +6. If your code touches communication with an ACME server/Boulder, you + should run the integration tests, see `integration`_. +7. Submit the PR. Updating the documentation ========================== diff --git a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/configurators/apache/a2enmod.sh b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/configurators/apache/a2enmod.sh index f822a1f7b..4da9288a2 100755 --- a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/configurators/apache/a2enmod.sh +++ b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/configurators/apache/a2enmod.sh @@ -1,33 +1,18 @@ #!/bin/bash # An extremely simplified version of `a2enmod` for enabling modules in the -# httpd docker image. First argument is the server_root and the second is the -# module to be enabled. +# httpd docker image. First argument is the Apache ServerRoot which should be +# an absolute path. The second is the module to be enabled, such as `ssl`. -APACHE_CONFDIR=$1 +confdir=$1 +module=$2 -enable () { - echo "LoadModule "$1"_module /usr/local/apache2/modules/mod_"$1".so" >> \ - $APACHE_CONFDIR"/test.conf" - available_base="/mods-available/"$1".conf" - available_conf=$APACHE_CONFDIR$available_base - enabled_dir=$APACHE_CONFDIR"/mods-enabled" - enabled_conf=$enabled_dir"/"$1".conf" - if [ -e "$available_conf" -a -d "$enabled_dir" -a ! -e "$enabled_conf" ] - then - ln -s "..$available_base" $enabled_conf - fi -} - -if [ $2 == "ssl" ] +echo "LoadModule ${module}_module " \ + "/usr/local/apache2/modules/mod_${module}.so" >> "${confdir}/test.conf" +availbase="/mods-available/${module}.conf" +availconf=$confdir$availbase +enabldir="$confdir/mods-enabled" +enablconf="$enabldir/${module}.conf" +if [ -e $availconf -a -d $enabldir -a ! -e $enablconf ] then - # Enables ssl and all its dependencies - enable "setenvif" - enable "mime" - enable "socache_shmcb" - enable "ssl" -elif [ $2 == "rewrite" ] -then - enable "rewrite" -else - exit 1 + ln -s "..$availbase" $enablconf fi diff --git a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py index 6181da16b..43070cf03 100644 --- a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py +++ b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/util.py @@ -39,7 +39,7 @@ def create_le_config(parent_dir): def extract_configs(configs, parent_dir): """Extracts configs to a new dir under parent_dir and returns it""" - config_dir = os.path.join(parent_dir, "renewal") + config_dir = os.path.join(parent_dir, "configs") if os.path.isdir(configs): shutil.copytree(configs, config_dir, symlinks=True) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 64cba508d..4d109a9c1 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -839,9 +839,23 @@ def _plugins_parsing(helpful, plugins): helpful.add_plugin_args(plugins) -def _setup_logging(args): - level = -args.verbose_count * 10 - fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" +def setup_log_file_handler(args, logfile, fmt): + """Setup file debug logging.""" + log_file_path = os.path.join(args.logs_dir, logfile) + handler = logging.handlers.RotatingFileHandler( + log_file_path, maxBytes=2 ** 20, backupCount=10) + # rotate on each invocation, rollover only possible when maxBytes + # is nonzero and backupCount is nonzero, so we set maxBytes as big + # as possible not to overrun in single CLI invocation (1MB). + handler.doRollover() # TODO: creates empty letsencrypt.log.1 file + handler.setLevel(logging.DEBUG) + handler_formatter = logging.Formatter(fmt=fmt) + handler_formatter.converter = time.gmtime # don't use localtime + handler.setFormatter(handler_formatter) + return handler, log_file_path + + +def _cli_log_handler(args, level, fmt): if args.text_mode: handler = colored_logging.StreamHandler() handler.setFormatter(logging.Formatter(fmt)) @@ -850,30 +864,26 @@ def _setup_logging(args): # dialog box is small, display as less as possible handler.setFormatter(logging.Formatter("%(message)s")) handler.setLevel(level) + return handler + + +def setup_logging(args, cli_handler_factory, logfile): + """Setup logging.""" + fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" + level = -args.verbose_count * 10 + file_handler, log_file_path = setup_log_file_handler( + args, logfile=logfile, fmt=fmt) + cli_handler = cli_handler_factory(args, level, fmt) # TODO: use fileConfig? - # unconditionally log to file for debugging purposes - # TODO: change before release? - log_file_name = os.path.join(args.logs_dir, 'letsencrypt.log') - file_handler = logging.handlers.RotatingFileHandler( - log_file_name, maxBytes=2 ** 20, backupCount=10) - # rotate on each invocation, rollover only possible when maxBytes - # is nonzero and backupCount is nonzero, so we set maxBytes as big - # as possible not to overrun in single CLI invocation (1MB). - file_handler.doRollover() # TODO: creates empty letsencrypt.log.1 file - file_handler.setLevel(logging.DEBUG) - file_handler_formatter = logging.Formatter(fmt=fmt) - file_handler_formatter.converter = time.gmtime # don't use localtime - file_handler.setFormatter(file_handler_formatter) - root_logger = logging.getLogger() root_logger.setLevel(logging.DEBUG) # send all records to handlers - root_logger.addHandler(handler) + root_logger.addHandler(cli_handler) root_logger.addHandler(file_handler) logger.debug("Root logging level set at %d", level) - logger.info("Saving debug log to %s", log_file_name) + logger.info("Saving debug log to %s", log_file_path) def _handle_exception(exc_type, exc_value, trace, args): @@ -942,7 +952,7 @@ def main(cli_args=sys.argv[1:]): # private key! #525 le_util.make_or_verify_dir( args.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args) - _setup_logging(args) + setup_logging(args, _cli_log_handler, logfile='letsencrypt.log') # do not log `args`, as it contains sensitive data (e.g. revoke --key)! logger.debug("Arguments: %r", cli_args) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 762409d25..362009ec6 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -27,6 +27,7 @@ CLI_DEFAULTS = dict( auth_cert_path="./cert.pem", auth_chain_path="./chain.pem", + strict_permissions=False, ) """Defaults for CLI flags and `.IConfig` attributes.""" diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 9d5ef87e9..99463c362 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -129,12 +129,18 @@ binary for temporary key/certificate generation.""".replace("\n", "") ct=response.CONTENT_TYPE, port=port) if self.conf("test-mode"): logger.debug("Test mode. Executing the manual command: %s", command) + # sh shipped with OS X does't support echo -n + if sys.platform == "darwin": + executable = "/bin/bash" + else: + executable = None try: self._httpd = subprocess.Popen( command, # don't care about setting stdout and stderr, # we're in test mode anyway shell=True, + executable=executable, # "preexec_fn" is UNIX specific, but so is "command" preexec_fn=os.setsid) except OSError as error: # ValueError should not happen! diff --git a/letsencrypt/plugins/util.py b/letsencrypt/plugins/util.py index 42c64b052..d50c7d61c 100644 --- a/letsencrypt/plugins/util.py +++ b/letsencrypt/plugins/util.py @@ -1,4 +1,5 @@ """Plugin utilities.""" +import logging import socket import psutil @@ -7,6 +8,9 @@ import zope.component from letsencrypt import interfaces +logger = logging.getLogger(__name__) + + def already_listening(port): """Check if a process is already listening on the port. @@ -17,9 +21,20 @@ def already_listening(port): run as root. :param int port: The TCP port in question. - :returns: True or False.""" + :returns: True or False. - listeners = [conn.pid for conn in psutil.net_connections() + """ + try: + net_connections = psutil.net_connections() + except psutil.AccessDenied as error: + logger.info("Access denied when trying to list network " + "connections: %s. Are you root?", error) + # this function is just a pre-check that often causes false + # positives and problems in testing (c.f. #680 on Mac, #255 + # generally); we will fail later in bind() anyway + return False + + listeners = [conn.pid for conn in net_connections if conn.status == 'LISTEN' and conn.type == socket.SOCK_STREAM and conn.laddr[1] == port] diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index e00fe5532..72c8244ea 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -8,6 +8,7 @@ within lineages of successor certificates, according to configuration. """ import argparse +import logging import os import sys @@ -17,10 +18,13 @@ import zope.component from letsencrypt import account from letsencrypt import configuration +from letsencrypt import constants +from letsencrypt import colored_logging from letsencrypt import cli from letsencrypt import client from letsencrypt import crypto_util from letsencrypt import errors +from letsencrypt import le_util from letsencrypt import notify from letsencrypt import storage @@ -28,6 +32,9 @@ from letsencrypt.display import util as display_util from letsencrypt.plugins import disco as plugins_disco +logger = logging.getLogger(__name__) + + class _AttrDict(dict): """Attribute dictionary. @@ -105,6 +112,12 @@ def renew(cert, old_version): # (where fewer than all names were renewed) +def _cli_log_handler(args, level, fmt): # pylint: disable=unused-argument + handler = colored_logging.StreamHandler() + handler.setFormatter(logging.Formatter(fmt)) + return handler + + def _paths_parser(parser): add = parser.add_argument_group("paths").add_argument add("--config-dir", default=cli.flag_default("config_dir"), @@ -120,11 +133,16 @@ def _paths_parser(parser): def _create_parser(): parser = argparse.ArgumentParser() #parser.add_argument("--cron", action="store_true", help="Run as cronjob.") - # pylint: disable=protected-access + parser.add_argument( + "-v", "--verbose", dest="verbose_count", action="count", + default=cli.flag_default("verbose_count"), help="This flag can be used " + "multiple times to incrementally increase the verbosity of output, " + "e.g. -vvv.") + return _paths_parser(parser) -def main(config=None, args=sys.argv[1:]): +def main(config=None, cli_args=sys.argv[1:]): """Main function for autorenewer script.""" # TODO: Distinguish automated invocation from manual invocation, # perhaps by looking at sys.argv[0] and inhibiting automated @@ -134,8 +152,13 @@ def main(config=None, args=sys.argv[1:]): zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) - cli_config = configuration.RenewerConfiguration( - _create_parser().parse_args(args)) + args = _create_parser().parse_args(cli_args) + + uid = os.geteuid() + le_util.make_or_verify_dir(args.logs_dir, 0o700, uid) + cli.setup_logging(args, _cli_log_handler, logfile='renewer.log') + + cli_config = configuration.RenewerConfiguration(args) config = storage.config_with_defaults(config) # Now attempt to read the renewer config file and augment or replace @@ -146,6 +169,9 @@ def main(config=None, args=sys.argv[1:]): # specify a config file on the command line, which, if provided, should # take precedence over this one. config.merge(configobj.ConfigObj(cli_config.renewer_config_file)) + # Ensure that all of the needed folders have been created before continuing + le_util.make_or_verify_dir( + cli_config.work_dir, constants.CONFIG_DIRS_MODE, uid) for i in os.listdir(cli_config.renewal_configs_dir): print "Processing", i diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 5101b4777..1e434b79e 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -45,8 +45,15 @@ class BaseRenewableCertTest(unittest.TestCase): self.cli_config = configuration.RenewerConfiguration( namespace=mock.MagicMock( - config_dir=self.tempdir, no_simple_http_tls=False)) + config_dir=self.tempdir, + work_dir=self.tempdir, + logs_dir=self.tempdir, + no_simple_http_tls=False, + ) + ) + # TODO: maybe provide RenewerConfiguration.make_dirs? + # TODO: main() should create those dirs, c.f. #902 os.makedirs(os.path.join(self.tempdir, "live", "example.org")) os.makedirs(os.path.join(self.tempdir, "archive", "example.org")) os.makedirs(os.path.join(self.tempdir, "renewal")) @@ -63,6 +70,9 @@ class BaseRenewableCertTest(unittest.TestCase): self.test_rc = storage.RenewableCert( self.config, self.defaults, self.cli_config) + def tearDown(self): + shutil.rmtree(self.tempdir) + def _write_out_ex_kinds(self): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) @@ -80,11 +90,6 @@ class BaseRenewableCertTest(unittest.TestCase): class RenewableCertTests(BaseRenewableCertTest): # pylint: disable=too-many-public-methods """Tests for letsencrypt.renewer.*.""" - def setUp(self): - super(RenewableCertTests, self).setUp() - - def tearDown(self): - shutil.rmtree(self.tempdir) def test_initialization(self): self.assertEqual(self.test_rc.lineagename, "example.org") @@ -667,11 +672,17 @@ class RenewableCertTests(BaseRenewableCertTest): # This should fail because the renewal itself appears to fail self.assertFalse(renewer.renew(self.test_rc, 1)) + def _common_cli_args(self): + return [ + "--config-dir", self.cli_config.config_dir, + "--work-dir", self.cli_config.work_dir, + "--logs-dir", self.cli_config.logs_dir, + ] + @mock.patch("letsencrypt.renewer.notify") @mock.patch("letsencrypt.storage.RenewableCert") @mock.patch("letsencrypt.renewer.renew") def test_main(self, mock_renew, mock_rc, mock_notify): - """Test for main() function.""" from letsencrypt import renewer mock_rc_instance = mock.MagicMock() mock_rc_instance.should_autodeploy.return_value = True @@ -693,8 +704,7 @@ class RenewableCertTests(BaseRenewableCertTest): "example.com.conf"), "w") as f: f.write("cert = cert.pem\nprivkey = privkey.pem\n") f.write("chain = chain.pem\nfullchain = fullchain.pem\n") - renewer.main(self.defaults, args=[ - '--config-dir', self.cli_config.config_dir]) + renewer.main(self.defaults, cli_args=self._common_cli_args()) self.assertEqual(mock_rc.call_count, 2) self.assertEqual(mock_rc_instance.update_all_links_to.call_count, 2) self.assertEqual(mock_notify.notify.call_count, 4) @@ -707,8 +717,7 @@ class RenewableCertTests(BaseRenewableCertTest): mock_happy_instance.should_autorenew.return_value = False mock_happy_instance.latest_common_version.return_value = 10 mock_rc.return_value = mock_happy_instance - renewer.main(self.defaults, args=[ - '--config-dir', self.cli_config.config_dir]) + renewer.main(self.defaults, cli_args=self._common_cli_args()) self.assertEqual(mock_rc.call_count, 4) self.assertEqual(mock_happy_instance.update_all_links_to.call_count, 0) self.assertEqual(mock_notify.notify.call_count, 4) @@ -719,8 +728,7 @@ class RenewableCertTests(BaseRenewableCertTest): with open(os.path.join(self.cli_config.renewal_configs_dir, "bad.conf"), "w") as f: f.write("incomplete = configfile\n") - renewer.main(self.defaults, args=[ - '--config-dir', self.cli_config.config_dir]) + renewer.main(self.defaults, cli_args=self._common_cli_args()) # The errors.CertStorageError is caught inside and nothing happens. diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 4ae01317a..1de46eda1 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -14,6 +14,12 @@ export PATH="/usr/sbin:$PATH" # /usr/sbin/nginx export GOPATH="${GOPATH:-/tmp/go}" export PATH="$GOPATH/bin:$PATH" +if [ `uname` == 'Darwin' ]; then + readlink="greadlink" +else + readlink="readlink" +fi + common() { letsencrypt_test \ --authenticator standalone \ @@ -49,7 +55,7 @@ dir="$root/conf/archive/le1.wtf" for x in cert chain fullchain privkey; do latest="$(ls -1t $dir/ | grep -e "^${x}" | head -n1)" - live="$(readlink -f "$root/conf/live/le1.wtf/${x}.pem")" + live="$($readlink -f "$root/conf/live/le1.wtf/${x}.pem")" [ "${dir}/${latest}" = "$live" ] # renewer fails this test done diff --git a/tools/dev-release.sh b/tools/dev-release.sh index 06f49f0a5..d93a6d21f 100755 --- a/tools/dev-release.sh +++ b/tools/dev-release.sh @@ -70,7 +70,7 @@ echo "Testing packages" cd "dist.$version" # start local PyPI python -m SimpleHTTPServer $PORT & -# cd .. is NOT done on purpose: we make sure that all subpacakges are +# cd .. is NOT done on purpose: we make sure that all subpackages are # installed from local PyPI rather than current directory (repo root) virtualenv --no-site-packages ../venv . ../venv/bin/activate @@ -82,15 +82,16 @@ pip install \ # stop local PyPI kill $! -# freeze before installing anythin else, so that we know end-user KGS -mkdir kgs -kgs="kgs/$version" +# freeze before installing anything else, so that we know end-user KGS +# make sure "twine upload" doesn't catch "kgs" +mkdir ../kgs +kgs="../kgs/$version" pip freeze | tee $kgs pip install nose # TODO: letsencrypt_apache fails due to symlink, c.f. #838 nosetests letsencrypt $SUBPKGS || true echo "New root: $root" -echo "KGS is at $root/$kgs" +echo "KGS is at $root/kgs" echo "In order to upload packages run the following command:" echo twine upload "$root/dist.$version/*/*"