diff --git a/docs/api/renewer.rst b/docs/api/renewer.rst deleted file mode 100644 index cc42c6eab..000000000 --- a/docs/api/renewer.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.renewer` --------------------------- - -.. automodule:: letsencrypt.renewer - :members: diff --git a/docs/conf.py b/docs/conf.py index 21bcc6817..739d6ee43 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -281,8 +281,6 @@ man_pages = [ [project], 7), ('man/letsencrypt', 'letsencrypt', u'letsencrypt script documentation', [project], 1), - ('man/letsencrypt-renewer', 'letsencrypt-renewer', - u'letsencrypt-renewer script documentation', [project], 1), ] # If true, show URL addresses after external links. diff --git a/docs/man/letsencrypt-renewer.rst b/docs/man/letsencrypt-renewer.rst deleted file mode 100644 index 8fd232fa8..000000000 --- a/docs/man/letsencrypt-renewer.rst +++ /dev/null @@ -1 +0,0 @@ -.. program-output:: letsencrypt-renewer --help diff --git a/letsencrypt-auto b/letsencrypt-auto index 20465dbb1..2b956aaf5 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -101,9 +101,7 @@ DeterminePythonVersion() { fi PYVER=`$LE_PYTHON --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` - if [ $PYVER -eq 26 ] ; then - ExperimentalBootstrap "Python 2.6" - elif [ $PYVER -lt 26 ] ; then + if [ $PYVER -lt 26 ] ; then echo "You have an ancient version of Python entombed in your operating system..." echo "This isn't going to work; you'll need at least version 2.6." exit 1 diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 0755081c8..64b06048b 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -105,9 +105,7 @@ DeterminePythonVersion() { fi PYVER=`"$LE_PYTHON" --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` - if [ $PYVER -eq 26 ]; then - ExperimentalBootstrap "Python 2.6" - elif [ $PYVER -lt 26 ]; then + if [ $PYVER -lt 26 ]; then echo "You have an ancient version of Python entombed in your operating system..." echo "This isn't going to work; you'll need at least version 2.6." exit 1 diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index ba17d7a99..de4844c9e 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -105,9 +105,7 @@ DeterminePythonVersion() { fi PYVER=`"$LE_PYTHON" --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` - if [ $PYVER -eq 26 ]; then - ExperimentalBootstrap "Python 2.6" - elif [ $PYVER -lt 26 ]; then + if [ $PYVER -lt 26 ]; then echo "You have an ancient version of Python entombed in your operating system..." echo "This isn't going to work; you'll need at least version 2.6." exit 1 diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 597fb3731..78e908f98 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -8,6 +8,7 @@ import argparse import atexit import copy import functools +import glob import json import logging import logging.handlers @@ -53,7 +54,7 @@ _parser = None # file's renewalparams and actually used in the client configuration # during the renewal process. We have to record their types here because # the renewal configuration process loses this information. -STR_CONFIG_ITEMS = ["config_dir", "log_dir", "work_dir", "user_agent", +STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent", "server", "account", "authenticator", "installer", "standalone_supported_challenges"] INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"] @@ -174,7 +175,7 @@ def _determine_account(config): acc = accounts[0] else: # no account registered yet if config.email is None and not config.register_unsafely_without_email: - config.email = display_ops.get_email() + config.namespace.email = display_ops.get_email() def _tos_cb(regr): if config.tos: @@ -196,7 +197,7 @@ def _determine_account(config): raise errors.Error( "Unable to register an account with ACME server") - config.account = acc.id + config.namespace.account = acc.id return acc, acme @@ -223,15 +224,12 @@ def _find_duplicative_certs(config, domains): # Verify the directory is there le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) - for renewal_file in os.listdir(configs_dir): - if not renewal_file.endswith(".conf"): - continue + for renewal_file in _renewal_conf_files(cli_config): try: - full_path = os.path.join(configs_dir, renewal_file) - candidate_lineage = storage.RenewableCert(full_path, cli_config) + candidate_lineage = storage.RenewableCert(renewal_file, cli_config) except (errors.CertStorageError, IOError): - logger.warning("Renewal configuration file %s is broken. " - "Skipping.", full_path) + logger.warning("Renewal conf file %s is broken. Skipping.", renewal_file) + logger.debug("Traceback was:\n%s", traceback.format_exc()) continue # TODO: Handle these differently depending on whether they are # expired or still valid? @@ -250,8 +248,10 @@ def _find_duplicative_certs(config, domains): def _treat_as_renewal(config, domains): - """Determine whether there are duplicated names and how to handle them - (renew, reinstall, newcert, or no action). + """Determine whether there are duplicated names and how to handle + them (renew, reinstall, newcert, or raising an error to stop + the client run if the user chooses to cancel the operation when + prompted). :returns: Two-element tuple containing desired new-certificate behavior as a string token ("reinstall", "renew", or "newcert"), plus either @@ -463,8 +463,8 @@ def _auth_from_domains(le_client, config, domains, lineage=None): lineage.save_successor( lineage.latest_common_version(), OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, new_certr.body.wrapped), - new_key.pem, crypto_util.dump_pyopenssl_chain(new_chain)) - + new_key.pem, crypto_util.dump_pyopenssl_chain(new_chain), + configuration.RenewerConfiguration(config.namespace)) lineage.update_all_links_to(lineage.latest_common_version()) # TODO: Check return value of save_successor # TODO: Also update lineage renewal config with any relevant @@ -709,7 +709,13 @@ def obtain_cert(config, plugins, lineage=None): if config.dry_run: _report_successful_dry_run() - _suggest_donation_if_appropriate(config, action) + elif config.verb == "renew" and installer is not None: + # In case of a renewal, reload server to pick up new certificate. + # In principle we could have a configuration option to inhibit this + # from happening. + installer.restart() + print("reloaded") + _suggest_donation_if_appropriate(config) def install(config, plugins): @@ -732,7 +738,15 @@ def install(config, plugins): le_client.enhance_config(domains, config) -def _restore_required_config_elements(full_path, config, renewalparams): +def _restore_required_config_elements(config, renewalparams): + """Sets non-plugin specific values in config from renewalparams + + :param configuration.NamespaceConfig config: configuration for the + current lineage + :param configobj.Section renewalparams: parameters from the renewal + configuration file that defines this lineage + + """ # string-valued items to add if they're present for config_item in STR_CONFIG_ITEMS: if config_item in renewalparams: @@ -741,50 +755,59 @@ def _restore_required_config_elements(full_path, config, renewalparams): # so we don't know if the original was NoneType or str! if value == "None": value = None - config.__setattr__(config_item, value) + setattr(config.namespace, config_item, value) # int-valued items to add if they're present for config_item in INT_CONFIG_ITEMS: if config_item in renewalparams: try: value = int(renewalparams[config_item]) - config.__setattr__(config_item, value) + setattr(config.namespace, config_item, value) except ValueError: - logger.warning("Renewal configuration file %s specifies " - "a non-numeric value for %s. Skipping.", - full_path, config_item) - raise + raise errors.Error( + "Expected a numeric value for {0}".format(config_item)) + +def _restore_plugin_configs(config, renewalparams): + """Sets plugin specific values in config from renewalparams + + :param configuration.NamespaceConfig config: configuration for the + current lineage + :param configobj.Section renewalparams: Parameters from the renewal + configuration file that defines this lineage + + """ # Now use parser to get plugin-prefixed items with correct types # XXX: the current approach of extracting only prefixed items # related to the actually-used installer and authenticator # works as long as plugins don't need to read plugin-specific # variables set by someone else (e.g., assuming Apache # configurator doesn't need to read webroot_ variables). - # XXX: is it true that an item will end up in _parser._actions even - # when no action was explicitly specified? + # Note: if a parameter that used to be defined in the parser is no + # longer defined, stored copies of that parameter will be + # deserialized as strings by this logic even if they were + # originally meant to be some other type. plugin_prefixes = [renewalparams["authenticator"]] - if "installer" in renewalparams and renewalparams["installer"] != None: + if renewalparams.get("installer", None) is not None: plugin_prefixes.append(renewalparams["installer"]) - for plugin_prefix in set(renewalparams): - for config_item in renewalparams.keys(): - if renewalparams[config_item] == "None": + for plugin_prefix in set(plugin_prefixes): + for config_item, config_value in renewalparams.iteritems(): + if config_item.startswith(plugin_prefix + "_"): # Avoid confusion when, for example, "csr = None" (avoid # trying to read the file called "None") # Should we omit the item entirely rather than setting # its value to None? - config.__setattr__(config_item, None) - continue - if config_item.startswith(plugin_prefix + "_"): - for action in _parser.parser._actions: - if action.dest == config_item: - if action.type is not None: - config.__setattr__(config_item, action.type(renewalparams[config_item])) - break + if config_value == "None": + setattr(config.namespace, config_item, None) + continue + for action in _parser.parser._actions: # pylint: disable=protected-access + if action.type is not None and action.dest == config_item: + setattr(config.namespace, config_item, + action.type(config_value)) + break else: - config.__setattr__(config_item, str(renewalparams[config_item])) - return True + setattr(config.namespace, config_item, str(config_value)) -def _reconstitute(full_path, config): +def _reconstitute(config, full_path): """Try to instantiate a RenewableCert, updating config with relevant items. This is specifically for use in renewal and enforces several checks @@ -792,15 +815,21 @@ def _reconstitute(full_path, config): request. The config argument is modified by including relevant options read from the renewal configuration file. + :param configuration.NamespaceConfig config: configuration for the + current lineage + :param str full_path: Absolute path to the configuration file that + defines this lineage + :returns: the RenewableCert object or None if a fatal error occurred :rtype: `storage.RenewableCert` or NoneType - """ + """ try: - renewal_candidate = storage.RenewableCert(full_path, config) + renewal_candidate = storage.RenewableCert( + full_path, configuration.RenewerConfiguration(config)) except (errors.CertStorageError, IOError): - logger.warning("Renewal configuration file %s is broken. " - "Skipping.", full_path) + logger.warning("Renewal configuration file %s is broken. Skipping.", full_path) + logger.debug("Traceback was:\n%s", traceback.format_exc()) return None if "renewalparams" not in renewal_candidate.configuration: logger.warning("Renewal configuration file %s lacks " @@ -814,17 +843,20 @@ def _reconstitute(full_path, config): # Now restore specific values along with their data types, if # those elements are present. try: - _restore_required_config_elements(full_path, config, renewalparams) - except ValueError: - # There was a data type error which has already been - # logged. + _restore_required_config_elements(config, renewalparams) + _restore_plugin_configs(config, renewalparams) + except (ValueError, errors.Error) as error: + logger.warning( + "An error occured while parsing %s. The error was %s. " + "Skipping the file.", full_path, error.message) + logger.debug("Traceback was:\n%s", traceback.format_exc()) return None # webroot_map is, uniquely, a dict, and the general-purpose # configuration restoring logic is not able to correctly parse it # from the serialized form. if "webroot_map" in renewalparams: - config.__setattr__("webroot_map", renewalparams["webroot_map"]) + setattr(config.namespace, "webroot_map", renewalparams["webroot_map"]) try: domains = [le_util.enforce_domain_sanity(x) for x in @@ -835,14 +867,18 @@ def _reconstitute(full_path, config): "invalid. Skipping.", full_path) return None - config.__setattr__("domains", domains) + setattr(config.namespace, "domains", domains) return renewal_candidate -def renew(cli_config, plugins): +def _renewal_conf_files(config): + """Return /path/to/*.conf in the renewal conf directory""" + return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) + + +def renew(config, unused_plugins): """Renew previously-obtained certificates.""" - cli_config = configuration.RenewerConfiguration(cli_config) - if cli_config.domains != []: + if config.domains != []: raise errors.Error("Currently, the renew verb is only capable of " "renewing all installed certificates that are due " "to be renewed; individual domains cannot be " @@ -850,40 +886,48 @@ def renew(cli_config, plugins): "renew specific certificates, use the certonly " "command. The renew verb may provide other options " "for selecting certificates to renew in the future.") - configs_dir = cli_config.renewal_configs_dir - for renewal_file in os.listdir(configs_dir): - if not renewal_file.endswith(".conf"): - continue + if config.csr is not None: + raise errors.Error("Currently, the renew verb cannot be used when " + "specifying a CSR file. Please try the certonly " + "command instead.") + renewer_config = configuration.RenewerConfiguration(config) + for renewal_file in _renewal_conf_files(renewer_config): print("Processing " + renewal_file) # XXX: does this succeed in making a fully independent config object # each time? - config = configuration.RenewerConfiguration(copy.deepcopy(cli_config)) - config.noninteractive_mode = True - full_path = os.path.join(configs_dir, renewal_file) + lineage_config = copy.deepcopy(config) # Note that this modifies config (to add back the configuration # elements from within the renewal configuration file). try: - renewal_candidate = _reconstitute(full_path, config) - except Exception as e: + renewal_candidate = _reconstitute(lineage_config, renewal_file) + except Exception as e: # pylint: disable=broad-except # reconstitute encountered an unanticipated problem. logger.warning("Renewal configuration file %s produced an " - "unexpected error: %s. Skipping.", full_path, e) + "unexpected error: %s. Skipping.", renewal_file, e) + logger.debug("Traceback was:\n%s", traceback.format_exc()) continue - if renewal_candidate is None: - # reconstitute indicated an error or problem which has - # already been logged. Go on to the next config. - continue - # XXX: ensure that each call here replaces the previous one - zope.component.provideUtility(config) + try: + if renewal_candidate is not None: + # _reconstitute succeeded in producing a RenewableCert, so we + # have something to work with from this particular config file. - print("Trying...") - # Because obtain_cert itself indirectly decides whether to renew - # or not, we couldn't currently make a UI/logging distinction at - # this stage to indicate whether renewal was actually attempted - # (or successful). - obtain_cert(config, plugins, renewal_candidate) + # XXX: ensure that each call here replaces the previous one + zope.component.provideUtility(lineage_config) + print("Trying...") + # Because obtain_cert itself indirectly decides whether to renew + # or not, we couldn't currently make a UI/logging distinction at + # this stage to indicate whether renewal was actually attempted + # (or successful). + obtain_cert(lineage_config, + plugins_disco.PluginsRegistry.find_all(), + renewal_candidate) + except Exception as e: # pylint: disable=broad-except + # obtain_cert (presumably) encountered an unanticipated problem. + logger.warning("Attempting to renew cert from %s produced an " + "unexpected error: %s. Skipping.", renewal_file, e) + logger.debug("Traceback was:\n%s", traceback.format_exc()) def revoke(config, unused_plugins): # TODO: coop with renewal config @@ -1068,9 +1112,9 @@ class HelpfulArgumentParser(object): parsed_args.server = constants.STAGING_URI if parsed_args.dry_run: - if self.verb != "certonly": + if self.verb not in ["certonly", "renew"]: raise errors.Error("--dry-run currently only works with the " - "'certonly' subcommand") + "'certonly' or 'renew' subcommands") parsed_args.break_my_certs = parsed_args.staging = True return parsed_args @@ -1359,7 +1403,7 @@ def prepare_and_parse_args(plugins, args): # parser (--help should display plugin-specific options last) _plugins_parsing(helpful, plugins) - global _parser + global _parser # pylint: disable=global-statement _parser = helpful return helpful.parse_args() @@ -1582,7 +1626,7 @@ def setup_log_file_handler(config, logfile, fmt): def _cli_log_handler(config, level, fmt): - if config.text_mode: + if config.text_mode or config.noninteractive_mode or config.verb == "renew": handler = colored_logging.StreamHandler() handler.setFormatter(logging.Formatter(fmt)) else: @@ -1699,6 +1743,9 @@ def main(cli_args=sys.argv[1:]): displayer = display_util.NoninteractiveDisplay(sys.stdout) elif config.text_mode: displayer = display_util.FileDisplay(sys.stdout) + elif config.verb == "renew": + config.noninteractive_mode = True + displayer = display_util.NoninteractiveDisplay(sys.stdout) else: displayer = display_util.NcursesDisplay() zope.component.provideUtility(displayer) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index b7d486ba1..57b21a55f 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -249,7 +249,7 @@ class Client(object): `.register` must be called before `.obtain_certificate` - :param set domains: domains to get a certificate + :param list domains: domains to get a certificate :returns: `.CertificateResource`, certificate chain (as returned by `.fetch_chain`), and newly generated private key @@ -282,23 +282,13 @@ class Client(object): """ certr, chain, key, _ = self.obtain_certificate(domains) - # XXX: We clearly need a more general and correct way of getting - # options into the configobj for the RenewableCert instance. - # This is a quick-and-dirty way to do it to allow integration - # testing to start. (Note that the config parameter to new_lineage - # ideally should be a ConfigObj, but in this case a dict will be - # accepted in practice.) - params = vars(self.config.namespace) - config = {} - cli_config = configuration.RenewerConfiguration(self.config.namespace) - - if (cli_config.config_dir != constants.CLI_DEFAULTS["config_dir"] or - cli_config.work_dir != constants.CLI_DEFAULTS["work_dir"]): + if (self.config.config_dir != constants.CLI_DEFAULTS["config_dir"] or + self.config.work_dir != constants.CLI_DEFAULTS["work_dir"]): logger.warning( "Non-standard path(s), might not work with crontab installed " "by your operating system package manager") - if cli_config.dry_run: + if self.config.dry_run: logger.info("Dry run: Skipping creating new lineage for %s", domains[0]) return None @@ -307,7 +297,7 @@ class Client(object): domains[0], OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped), key.pem, crypto_util.dump_pyopenssl_chain(chain), - params, config, cli_config) + configuration.RenewerConfiguration(self.config.namespace)) def save_certificate(self, certr, chain_cert, cert_path, chain_path, fullchain_path): diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 72aabe548..2bbf1b019 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -1,4 +1,5 @@ """Let's Encrypt user-supplied configuration.""" +import copy import os import urlparse @@ -43,12 +44,6 @@ class NamespaceConfig(object): # Check command line parameters sanity, and error out in case of problem. check_config_sanity(self) - # We're done setting up the attic. Now pull up the ladder after ourselves... - self.__setattr__ = self.__setattr_implementation__ - - def __setattr_implementation__(self, var, value): - return self.namespace.__setattr__(var, value) - def __getattr__(self, name): return getattr(self.namespace, name) @@ -84,21 +79,22 @@ class NamespaceConfig(object): return os.path.join( self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) + def __deepcopy__(self, _memo): + # Work around https://bugs.python.org/issue1515 for py26 tests :( :( + # https://travis-ci.org/letsencrypt/letsencrypt/jobs/106900743#L3276 + new_ns = copy.deepcopy(self.namespace) + return type(self)(new_ns) + class RenewerConfiguration(object): """Configuration wrapper for renewer.""" def __init__(self, namespace): self.namespace = namespace - # We're done setting up the attic. Now pull up the ladder after ourselves... - self.__setattr__ = self.__setattr_implementation__ def __getattr__(self, name): return getattr(self.namespace, name) - def __setattr_implementation__(self, var, value): - return self.namespace.__setattr__(var, value) - @property def archive_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 29f4639fe..54244db2a 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -93,7 +93,6 @@ s.serve_forever()" """ def prepare(self): # pylint: disable=missing-docstring,no-self-use if self.config.noninteractive_mode: raise errors.PluginError("Running manual mode non-interactively is not supported") - pass # pragma: no cover def more_info(self): # pylint: disable=missing-docstring,no-self-use return ("This plugin requires user's manual intervention in setting " diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py deleted file mode 100644 index 83c6106c0..000000000 --- a/letsencrypt/renewer.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Renewer tool. - -Renewer tool handles autorenewal and autodeployment of renewed certs -within lineages of successor certificates, according to configuration. - -.. todo:: Sanity checking consistency, validity, freshness? -.. todo:: Call new installer API to restart servers after deployment - -""" -from __future__ import print_function - -import argparse -import logging -import os -import sys - -import OpenSSL -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 - -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. - - A trick to allow accessing dictionary keys as object attributes. - - """ - def __init__(self, *args, **kwargs): - super(_AttrDict, self).__init__(*args, **kwargs) - self.__dict__ = self - - -def renew(cert, old_version): - """Perform automated renewal of the referenced cert, if possible. - - :param letsencrypt.storage.RenewableCert cert: The certificate - lineage to attempt to renew. - :param int old_version: The version of the certificate lineage - relative to which the renewal should be attempted. - - :returns: A number referring to newly created version of this cert - lineage, or ``False`` if renewal was not successful. - :rtype: `int` or `bool` - - """ - # TODO: handle partial success (some names can be renewed but not - # others) - # TODO: handle obligatory key rotation vs. optional key rotation vs. - # requested key rotation - if "renewalparams" not in cert.configfile: - # TODO: notify user? - return False - renewalparams = cert.configfile["renewalparams"] - if "authenticator" not in renewalparams: - # TODO: notify user? - return False - # Instantiate the appropriate authenticator - plugins = plugins_disco.PluginsRegistry.find_all() - config = configuration.NamespaceConfig(_AttrDict(renewalparams)) - # XXX: this loses type data (for example, the fact that key_size - # was an int, not a str) - config.rsa_key_size = int(config.rsa_key_size) - config.tls_sni_01_port = int(config.tls_sni_01_port) - config.namespace.http01_port = int(config.namespace.http01_port) - zope.component.provideUtility(config) - try: - authenticator = plugins[renewalparams["authenticator"]] - except KeyError: - # TODO: Notify user? (authenticator could not be found) - return False - authenticator = authenticator.init(config) - - authenticator.prepare() - acc = account.AccountFileStorage(config).load( - account_id=renewalparams["account"]) - - le_client = client.Client(config, acc, authenticator, None) - with open(cert.version("cert", old_version)) as f: - sans = crypto_util.get_sans_from_cert(f.read()) - new_certr, new_chain, new_key, _ = le_client.obtain_certificate(sans) - if new_chain: - # XXX: Assumes that there was a key change. We need logic - # for figuring out whether there was or not. Probably - # best is to have obtain_certificate return None for - # new_key if the old key is to be used (since save_successor - # already understands this distinction!) - return cert.save_successor( - old_version, OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, new_certr.body.wrapped), - new_key.pem, crypto_util.dump_pyopenssl_chain(new_chain)) - # TODO: Notify results - else: - # TODO: Notify negative results - return False - # TODO: Consider the case where the renewal was partially successful - # (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)) - handler.setLevel(level) - return handler - - -def _paths_parser(parser): - add = parser.add_argument_group("paths").add_argument - add("--config-dir", default=cli.flag_default("config_dir"), - help=cli.config_help("config_dir")) - add("--work-dir", default=cli.flag_default("work_dir"), - help=cli.config_help("work_dir")) - add("--logs-dir", default=cli.flag_default("logs_dir"), - help="Path to a directory where logs are stored.") - - return parser - - -def _create_parser(): - parser = argparse.ArgumentParser() - #parser.add_argument("--cron", action="store_true", help="Run as cronjob.") - 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(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 - # invocations if /etc/letsencrypt/renewal.conf defaults have - # turned it off. (The boolean parameter should probably be - # called renewer_enabled.) - - # TODO: When we have a more elaborate renewer command line, we will - # presumably also be able to specify a config file on the - # command line, which, if provided, should take precedence over - # te default config files - - zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) - - 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) - - # 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 renewal_file in os.listdir(cli_config.renewal_configs_dir): - if not renewal_file.endswith(".conf"): - continue - print("Processing " + renewal_file) - try: - # TODO: Before trying to initialize the RenewableCert object, - # we could check here whether the combination of the config - # and the rc_config together disables all autorenewal and - # autodeployment applicable to this cert. In that case, we - # can simply continue and don't need to instantiate a - # RenewableCert object for this cert at all, which could - # dramatically improve performance for large deployments - # where autorenewal is widely turned off. - cert = storage.RenewableCert( - os.path.join(cli_config.renewal_configs_dir, renewal_file), - cli_config) - except errors.CertStorageError: - # This indicates an invalid renewal configuration file, such - # as one missing a required parameter (in the future, perhaps - # also one that is internally inconsistent or is missing a - # required parameter). As a TODO, maybe we should warn the - # user about the existence of an invalid or corrupt renewal - # config rather than simply ignoring it. - continue - if cert.should_autorenew(): - # Note: not cert.current_version() because the basis for - # the renewal is the latest version, even if it hasn't been - # deployed yet! - old_version = cert.latest_common_version() - renew(cert, old_version) - notify.notify("Autorenewed a cert!!!", "root", "It worked!") - # TODO: explain what happened - if cert.should_autodeploy(): - cert.update_all_links_to(cert.latest_common_version()) - # TODO: restart web server (invoke IInstaller.restart() method) - notify.notify("Autodeployed a cert!!!", "root", "It worked!") - # TODO: explain what happened diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index e41805459..6f7f54646 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -50,6 +50,68 @@ def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()): return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0] +def write_renewal_config(filename, target, cli_config): + """Writes a renewal config file with the specified name and values. + + :param str filename: Absolute path to the config file + :param dict target: Maps ALL_FOUR to their symlink paths + :param .RenewerConfiguration cli_config: parsed command line + arguments + + :returns: Configuration object for the new config file + :rtype: configobj.ConfigObj + + """ + # create_empty creates a new config file if filename does not exist + config = configobj.ConfigObj(filename, create_empty=True) + for kind in ALL_FOUR: + config[kind] = target[kind] + + # XXX: We clearly need a more general and correct way of getting + # options into the configobj for the RenewableCert instance. + # This is a quick-and-dirty way to do it to allow integration + # testing to start. (Note that the config parameter to new_lineage + # ideally should be a ConfigObj, but in this case a dict will be + # accepted in practice.) + renewalparams = vars(cli_config.namespace) + if renewalparams: + config["renewalparams"] = renewalparams + config.comments["renewalparams"] = ["", + "Options and defaults used" + " in the renewal process"] + + # TODO: add human-readable comments explaining other available + # parameters + logger.debug("Writing new config %s.", filename) + config.write() + return config + + +def update_configuration(lineagename, target, cli_config): + """Modifies lineagename's config to contain the specified values. + + :param str lineagename: Name of the lineage being modified + :param dict target: Maps ALL_FOUR to their symlink paths + :param .RenewerConfiguration cli_config: parsed command line + arguments + + :returns: Configuration object for the updated config file + :rtype: configobj.ConfigObj + + """ + config_filename = os.path.join( + cli_config.renewal_configs_dir, lineagename) + ".conf" + temp_filename = config_filename + ".new" + + # If an existing tempfile exists, delete it + if os.path.exists(temp_filename): + os.unlink(temp_filename) + write_renewal_config(temp_filename, target, cli_config) + os.rename(temp_filename, config_filename) + + return configobj.ConfigObj(config_filename) + + class RenewableCert(object): # pylint: disable=too-many-instance-attributes """Renewable certificate. @@ -589,9 +651,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes return False @classmethod - def new_lineage(cls, lineagename, cert, privkey, chain, - renewalparams=None, config=None, cli_config=None): - # pylint: disable=too-many-locals,too-many-arguments + def new_lineage(cls, lineagename, cert, privkey, chain, cli_config): + # pylint: disable=too-many-locals """Create a new certificate lineage. Attempts to create a certificate lineage -- enrolled for @@ -611,26 +672,13 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :param str cert: the initial certificate version in PEM format :param str privkey: the private key in PEM format :param str chain: the certificate chain in PEM format - :param configobj.ConfigObj renewalparams: parameters that - should be used when instantiating authenticator and installer - objects in the future to attempt to renew this cert or deploy - new versions of it - :param configobj.ConfigObj config: renewal configuration - defaults, affecting, for example, the locations of the - directories where the associated files will be saved :param .RenewerConfiguration cli_config: parsed command line arguments :returns: the newly-created RenewalCert object - :rtype: :class:`storage.renewableCert`""" - - config = config_with_defaults(config) - # This attempts to read the renewer config file and augment or replace - # the renewer defaults with any options contained in that file. If - # renewer_config_file is undefined or if the file is nonexistent or - # empty, this .merge() will have no effect. - config.merge(configobj.ConfigObj(cli_config.renewer_config_file)) + :rtype: :class:`storage.renewableCert` + """ # Examine the configuration and find the new lineage's name for i in (cli_config.renewal_configs_dir, cli_config.archive_dir, cli_config.live_dir): @@ -685,21 +733,11 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # Document what we've done in a new renewal config file config_file.close() - new_config = configobj.ConfigObj(config_filename, create_empty=True) - for kind in ALL_FOUR: - new_config[kind] = target[kind] - if renewalparams: - new_config["renewalparams"] = renewalparams - new_config.comments["renewalparams"] = ["", - "Options and defaults used" - " in the renewal process"] - # TODO: add human-readable comments explaining other available - # parameters - logger.debug("Writing new config %s.", config_filename) - new_config.write() + new_config = write_renewal_config(config_filename, target, cli_config) return cls(new_config.filename, cli_config) - def save_successor(self, prior_version, new_cert, new_privkey, new_chain): + def save_successor(self, prior_version, new_cert, + new_privkey, new_chain, cli_config): """Save new cert and chain as a successor of a prior version. Returns the new version number that was created. @@ -715,6 +753,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes :param str new_privkey: the new private key, in PEM format, or ``None``, if the private key has not changed :param str new_chain: the new chain, in PEM format + :param .RenewerConfiguration cli_config: parsed command line + arguments :returns: the new version number that was created :rtype: int @@ -726,8 +766,12 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # if needed (ensuring their permissions are correct) # Figure out what the new version is and hence where to save things + self.cli_config = cli_config target_version = self.next_free_version() archive = self.cli_config.archive_dir + # XXX if anyone ever moves a renewal configuration file, this will + # break... perhaps prefix should be the dirname of the previous + # cert.pem? prefix = os.path.join(archive, self.lineagename) target = dict( [(kind, @@ -763,4 +807,11 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes with open(target["fullchain"], "w") as f: logger.debug("Writing full chain to %s.", target["fullchain"]) f.write(new_cert + new_chain) + + symlinks = dict((kind, self.configuration[kind]) for kind in ALL_FOUR) + # Update renewal config file + self.configfile = update_configuration( + self.lineagename, symlinks, cli_config) + self.configuration = config_with_defaults(self.configfile) + return target_version diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index c414d9054..053bdb113 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -24,7 +24,7 @@ from letsencrypt import le_util from letsencrypt.plugins import disco from letsencrypt.plugins import manual -from letsencrypt.tests import renewer_test +from letsencrypt.tests import storage_test from letsencrypt.tests import test_util @@ -531,7 +531,9 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertRaises(errors.Error, self._certonly_new_request_common, mock_client) - def _test_renewal_common(self, due_for_renewal, extra_args, outstring, renew=True): + def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None, + args=None, renew=True): + # pylint: disable=too-many-locals cert_path = 'letsencrypt/tests/testdata/cert.pem' chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path) @@ -547,27 +549,34 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mock_init.return_value = mock_client get_utility_path = 'letsencrypt.cli.zope.component.getUtility' with mock.patch(get_utility_path) as mock_get_utility: - with mock.patch('letsencrypt.cli.OpenSSL'): + with mock.patch('letsencrypt.cli.OpenSSL') as mock_ssl: + mock_latest = mock.MagicMock() + mock_latest.get_issuer.return_value = "Fake fake" + mock_ssl.crypto.load_certificate.return_value = mock_latest with mock.patch('letsencrypt.cli.crypto_util'): - args = ['-d', 'foo.bar', '-a', - 'standalone', 'certonly'] + if not args: + args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly'] if extra_args: args += extra_args self._call(args) - if outstring: - with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: - self.assertTrue(outstring in lf.read()) + try: + if log_out: + with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: + self.assertTrue(log_out in lf.read()) - if renew: - mock_client.obtain_certificate.assert_called_once_with(['foo.bar']) - else: - self.assertEqual(mock_client.obtain_certificate.call_count, 0) + if renew: + mock_client.obtain_certificate.assert_called_once_with(['isnot.org']) + else: + self.assertEqual(mock_client.obtain_certificate.call_count, 0) + except: + self._dump_log() + raise return mock_lineage, mock_get_utility def test_certonly_renewal(self): - lineage, get_utility = self._test_renewal_common(True, [], None) + lineage, get_utility = self._test_renewal_common(True, []) self.assertEqual(lineage.save_successor.call_count, 1) lineage.update_all_links_to.assert_called_once_with( lineage.latest_common_version()) @@ -578,17 +587,34 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods def test_certonly_renewal_triggers(self): # --dry-run should force renewal _, get_utility = self._test_renewal_common(False, ['--dry-run', '--keep'], - "simulating renewal") + log_out="simulating renewal") self.assertEqual(get_utility().add_message.call_count, 1) self.assertTrue('dry run' in get_utility().add_message.call_args[0][0]) _, _ = self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'], - "Auto-renewal forced") + log_out="Auto-renewal forced") self.assertEqual(get_utility().add_message.call_count, 1) _, _ = self._test_renewal_common(False, ['-tvv', '--debug', '--keep'], - "not yet due", renew=False) + log_out="not yet due", renew=False) + def _dump_log(self): + with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: + print "Logs:" + print lf.read() + + + def test_renewal_verb(self): + with open(test_util.vector_path('sample-renewal.conf')) as src: + # put the correct path for cert.pem, chain.pem etc in the renewal conf + renewal_conf = src.read().replace("MAGICDIR", test_util.vector_path()) + rd = os.path.join(self.config_dir, "renewal") + os.makedirs(rd) + rc = os.path.join(rd, "sample-renewal.conf") + with open(rc, "w") as dest: + dest.write(renewal_conf) + args = ["renew", "--dry-run", "-tvv"] + self._test_renewal_common(True, [], args=args, renew=True) @mock.patch('letsencrypt.cli.zope.component.getUtility') @mock.patch('letsencrypt.cli._treat_as_renewal') @@ -800,7 +826,7 @@ class DetermineAccountTest(unittest.TestCase): self.assertEqual('other email', self.config.email) -class DuplicativeCertsTest(renewer_test.BaseRenewableCertTest): +class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): """Test to avoid duplicate lineages.""" def setUp(self): diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/storage_test.py similarity index 79% rename from letsencrypt/tests/renewer_test.py rename to letsencrypt/tests/storage_test.py index 3c8e3cb95..9d402089c 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/storage_test.py @@ -1,4 +1,4 @@ -"""Tests for letsencrypt.renewer.""" +"""Tests for letsencrypt.storage.""" import datetime import pytz import os @@ -9,8 +9,6 @@ import unittest import configobj import mock -from acme import jose - from letsencrypt import configuration from letsencrypt import errors from letsencrypt.storage import ALL_FOUR @@ -100,7 +98,7 @@ class BaseRenewableCertTest(unittest.TestCase): class RenewableCertTests(BaseRenewableCertTest): # pylint: disable=too-many-public-methods - """Tests for letsencrypt.renewer.*.""" + """Tests for letsencrypt.storage.""" def test_initialization(self): self.assertEqual(self.test_rc.lineagename, "example.org") @@ -506,8 +504,9 @@ class RenewableCertTests(BaseRenewableCertTest): with open(where, "w") as f: f.write(kind) self.test_rc.update_all_links_to(3) - self.assertEqual(6, self.test_rc.save_successor(3, "new cert", None, - "new chain")) + self.assertEqual( + 6, self.test_rc.save_successor(3, "new cert", None, + "new chain", self.cli_config)) with open(self.test_rc.version("cert", 6)) as f: self.assertEqual(f.read(), "new cert") with open(self.test_rc.version("chain", 6)) as f: @@ -518,10 +517,12 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertFalse(os.path.islink(self.test_rc.version("privkey", 3))) self.assertTrue(os.path.islink(self.test_rc.version("privkey", 6))) # Let's try two more updates - self.assertEqual(7, self.test_rc.save_successor(6, "again", None, - "newer chain")) - self.assertEqual(8, self.test_rc.save_successor(7, "hello", None, - "other chain")) + self.assertEqual( + 7, self.test_rc.save_successor(6, "again", None, + "newer chain", self.cli_config)) + self.assertEqual( + 8, self.test_rc.save_successor(7, "hello", None, + "other chain", self.cli_config)) # All of the subsequent versions should link directly to the original # privkey. for i in (6, 7, 8): @@ -534,27 +535,33 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(self.test_rc.current_version(kind), 3) # Test updating from latest version rather than old version self.test_rc.update_all_links_to(8) - self.assertEqual(9, self.test_rc.save_successor(8, "last", None, - "attempt")) + self.assertEqual( + 9, self.test_rc.save_successor(8, "last", None, + "attempt", self.cli_config)) for kind in ALL_FOUR: self.assertEqual(self.test_rc.available_versions(kind), range(1, 10)) self.assertEqual(self.test_rc.current_version(kind), 8) with open(self.test_rc.version("fullchain", 9)) as f: self.assertEqual(f.read(), "last" + "attempt") + temp_config_file = os.path.join(self.cli_config.renewal_configs_dir, + self.test_rc.lineagename) + ".conf.new" + with open(temp_config_file, "w") as f: + f.write("We previously crashed while writing me :(") # Test updating when providing a new privkey. The key should # be saved in a new file rather than creating a new symlink. - self.assertEqual(10, self.test_rc.save_successor(9, "with", "a", - "key")) + self.assertEqual( + 10, self.test_rc.save_successor(9, "with", "a", + "key", self.cli_config)) self.assertTrue(os.path.exists(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) + self.assertFalse(os.path.exists(temp_config_file)) def test_new_lineage(self): """Test for new_lineage() class method.""" from letsencrypt import storage result = storage.RenewableCert.new_lineage( - "the-lineage.com", "cert", "privkey", "chain", None, - self.defaults, self.cli_config) + "the-lineage.com", "cert", "privkey", "chain", self.cli_config) # This consistency check tests most relevant properties about the # newly created cert lineage. # pylint: disable=protected-access @@ -565,27 +572,23 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(f.read(), "cert" + "chain") # Let's do it again and make sure it makes a different lineage result = storage.RenewableCert.new_lineage( - "the-lineage.com", "cert2", "privkey2", "chain2", None, - self.defaults, self.cli_config) + "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) self.assertTrue(os.path.exists(os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf"))) # Now trigger the detection of already existing files os.mkdir(os.path.join( self.cli_config.live_dir, "the-lineage.com-0002")) self.assertRaises(errors.CertStorageError, - storage.RenewableCert.new_lineage, - "the-lineage.com", "cert3", "privkey3", "chain3", - None, self.defaults, self.cli_config) + storage.RenewableCert.new_lineage, "the-lineage.com", + "cert3", "privkey3", "chain3", self.cli_config) os.mkdir(os.path.join(self.cli_config.archive_dir, "other-example.com")) self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, - "other-example.com", "cert4", "privkey4", "chain4", - None, self.defaults, self.cli_config) + "other-example.com", "cert4", + "privkey4", "chain4", self.cli_config) # Make sure it can accept renewal parameters - params = {"stuff": "properties of stuff", "great": "awesome"} result = storage.RenewableCert.new_lineage( - "the-lineage.com", "cert2", "privkey2", "chain2", - params, self.defaults, self.cli_config) + "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) # TODO: Conceivably we could test that the renewal parameters actually # got saved @@ -597,8 +600,7 @@ class RenewableCertTests(BaseRenewableCertTest): shutil.rmtree(self.cli_config.live_dir) storage.RenewableCert.new_lineage( - "the-lineage.com", "cert2", "privkey2", "chain2", - None, self.defaults, self.cli_config) + "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) self.assertTrue(os.path.exists( os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) @@ -612,9 +614,8 @@ class RenewableCertTests(BaseRenewableCertTest): from letsencrypt import storage mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes" self.assertRaises(errors.CertStorageError, - storage.RenewableCert.new_lineage, - "example.com", "cert", "privkey", "chain", - None, self.defaults, self.cli_config) + storage.RenewableCert.new_lineage, "example.com", + "cert", "privkey", "chain", self.cli_config) def test_bad_kind(self): self.assertRaises( @@ -681,114 +682,6 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(storage.add_time_interval(base_time, interval), excepted) - @mock.patch("letsencrypt.renewer.plugins_disco") - @mock.patch("letsencrypt.account.AccountFileStorage") - @mock.patch("letsencrypt.client.Client") - def test_renew(self, mock_c, mock_acc_storage, mock_pd): - from letsencrypt import renewer - - test_cert = test_util.load_vector("cert-san.pem") - for kind in ALL_FOUR: - os.symlink(os.path.join("..", "..", "archive", "example.org", - kind + "1.pem"), - getattr(self.test_rc, kind)) - fill_with_sample_data(self.test_rc) - with open(self.test_rc.cert, "w") as f: - f.write(test_cert) - - # Fails because renewalparams are missing - self.assertFalse(renewer.renew(self.test_rc, 1)) - self.test_rc.configfile["renewalparams"] = {"some": "stuff"} - # Fails because there's no authenticator specified - self.assertFalse(renewer.renew(self.test_rc, 1)) - self.test_rc.configfile["renewalparams"]["rsa_key_size"] = "2048" - self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com" - self.test_rc.configfile["renewalparams"]["authenticator"] = "fake" - self.test_rc.configfile["renewalparams"]["tls_sni_01_port"] = "4430" - self.test_rc.configfile["renewalparams"]["http01_port"] = "1234" - self.test_rc.configfile["renewalparams"]["account"] = "abcde" - self.test_rc.configfile["renewalparams"]["domains"] = ["example.com"] - self.test_rc.configfile["renewalparams"]["config_dir"] = "config" - self.test_rc.configfile["renewalparams"]["work_dir"] = "work" - self.test_rc.configfile["renewalparams"]["logs_dir"] = "logs" - mock_auth = mock.MagicMock() - mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth} - # Fails because "fake" != "apache" - self.assertFalse(renewer.renew(self.test_rc, 1)) - self.test_rc.configfile["renewalparams"]["authenticator"] = "apache" - mock_client = mock.MagicMock() - # pylint: disable=star-args - comparable_cert = jose.ComparableX509(CERT) - mock_client.obtain_certificate.return_value = ( - mock.MagicMock(body=comparable_cert), [comparable_cert], - mock.Mock(pem="key"), mock.sentinel.csr) - mock_c.return_value = mock_client - self.assertEqual(2, renewer.renew(self.test_rc, 1)) - # TODO: We could also make several assertions about calls that should - # have been made to the mock functions here. - mock_acc_storage().load.assert_called_once_with(account_id="abcde") - mock_client.obtain_certificate.return_value = ( - mock.sentinel.certr, [], mock.sentinel.key, mock.sentinel.csr) - # 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): - from letsencrypt import renewer - mock_rc_instance = mock.MagicMock() - mock_rc_instance.should_autodeploy.return_value = True - mock_rc_instance.should_autorenew.return_value = True - mock_rc_instance.latest_common_version.return_value = 10 - mock_rc.return_value = mock_rc_instance - with open(os.path.join(self.cli_config.renewal_configs_dir, - "example.org.conf"), "w") as f: - # This isn't actually parsed in this test; we have a separate - # test_initialization that tests the initialization, assuming - # that configobj can correctly parse the config file. - f.write("cert = cert.pem\nprivkey = privkey.pem\n") - f.write("chain = chain.pem\nfullchain = fullchain.pem\n") - with open(os.path.join(self.cli_config.renewal_configs_dir, - "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(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) - self.assertEqual(mock_renew.call_count, 2) - # If we have instances that don't need any work done, no work should - # be done (call counts associated with processing deployments or - # renewals should not increase). - mock_happy_instance = mock.MagicMock() - mock_happy_instance.should_autodeploy.return_value = False - 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(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) - self.assertEqual(mock_renew.call_count, 2) - - def test_bad_config_file(self): - from letsencrypt import renewer - os.unlink(os.path.join(self.cli_config.renewal_configs_dir, - "example.org.conf")) - with open(os.path.join(self.cli_config.renewal_configs_dir, - "bad.conf"), "w") as f: - f.write("incomplete = configfile\n") - renewer.main(cli_args=self._common_cli_args()) - # The errors.CertStorageError is caught inside and nothing happens. - def test_missing_cert(self): from letsencrypt import storage self.assertRaises(errors.CertStorageError, diff --git a/letsencrypt/tests/testdata/archive/sample-renewal/cert1.pem b/letsencrypt/tests/testdata/archive/sample-renewal/cert1.pem new file mode 100644 index 000000000..4010000ef --- /dev/null +++ b/letsencrypt/tests/testdata/archive/sample-renewal/cert1.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF +ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5 +MDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+ +slm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN +NYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h +A5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx +UpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP +r4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC +AhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8 +L54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE +bDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy +eXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0 +c2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB +8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw +Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl +cnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy +dGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl +IFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0 +b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy +KzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve +jzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2 +Ese3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU ++ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf +rAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ== +-----END CERTIFICATE----- diff --git a/letsencrypt/tests/testdata/archive/sample-renewal/chain1.pem b/letsencrypt/tests/testdata/archive/sample-renewal/chain1.pem new file mode 100644 index 000000000..760417fe9 --- /dev/null +++ b/letsencrypt/tests/testdata/archive/sample-renewal/chain1.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV +BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw +NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i +8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8 +tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj +7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8 +BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD +HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj +UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7 +eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA +A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB +vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl +zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo +vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L +oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW +rFo4Uv1EnkKJm3vJFe50eJGhEKlx +-----END CERTIFICATE----- diff --git a/letsencrypt/tests/testdata/archive/sample-renewal/fullchain1.pem b/letsencrypt/tests/testdata/archive/sample-renewal/fullchain1.pem new file mode 100644 index 000000000..6e24d6038 --- /dev/null +++ b/letsencrypt/tests/testdata/archive/sample-renewal/fullchain1.pem @@ -0,0 +1,47 @@ +-----BEGIN CERTIFICATE----- +MIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF +ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5 +MDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+ +slm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN +NYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h +A5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx +UpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP +r4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC +AhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8 +L54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE +bDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy +eXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0 +c2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB +8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw +Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl +cnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy +dGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl +IFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0 +b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy +KzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve +jzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2 +Ese3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU ++ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf +rAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV +BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw +NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i +8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8 +tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj +7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8 +BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD +HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj +UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7 +eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA +A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB +vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl +zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo +vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L +oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW +rFo4Uv1EnkKJm3vJFe50eJGhEKlx +-----END CERTIFICATE----- diff --git a/letsencrypt/tests/testdata/archive/sample-renewal/privkey1.pem b/letsencrypt/tests/testdata/archive/sample-renewal/privkey1.pem new file mode 100644 index 000000000..f03fdd0a3 --- /dev/null +++ b/letsencrypt/tests/testdata/archive/sample-renewal/privkey1.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8rnaiynCHVmeV +WitX7oZRA22bb7NhPrJZuHZkktZxb3uzHZWevStIHbZ4ZIj0NMBkE5YoHg2fH2PI +i/OxmTtLaeX+0vkCTTWAomMOo+KqeSQcv8xttpCc9ULru/71zGC8y7yusemIsYlg +Fsqqr5VFQS0ta+Ft4QOWfhdRCLF3ss+0yXt5QMyFgAMaXx/Tr4iM9qxA6LbgnVO+ +9VEiyzUsOKiaIzZU8VKWRupRhbPChm/LakTl5J2jChOD7zd0iE4sUlExW5LbdVs+ +tJqFmzXSzCr4JM7lD6+L4jtj+EjubJTQGNGXhLLD6VQHpTdxL6wHBGbaCphuX50A +1aPLlWoNAgMBAAECggEAfKKWFWS6PnwSAnNErFoQeZVVItb/XB5JO8EA2+CvLNFi +mefR/MCixYlzDkYCvaXW7ISPrMJlZxYaGNBx0oAQzfkPB2wfNqj/zY/29SXGxast +8puzk0mEb1oHsaZGfeFaiXvfkFpPlI8J2uJTT7qaVNv/1sArciSv9QonpsyiRhlB +yqT49juNVoR1tJHyXzkkRfHKTG8OlJd4kuFOl3fM9dTFPQ/ft0kTNAQ/B4SFvSwF +RJsbLbsbFGsUdV9ekE6UX6oWD/Ah707rvgtCyS0Bc+0O3t2EKwmm3RXPRUMHCVxE +bKdTxRB4etbjMVXMuVhB8Y4GbfrtMCy+qxZQ6znCAQKBgQDr7bcYAZVZp/nBMVB+ +lBO9w73J6lnEWm6bZ9728KlGAKETaRhxZQSi6TN6MWwNwnk6rinyz4uVwVr9ZRCs +WkB1TbvW0JNcWdr3YClwsKXAt8X22bjGe0LagDJHG6r1TPS+MdovOS2M6IMaxlbT +rzFhSJ8ojLX3tqnOsmc7YAFLjQKBgQDMu8E9hoJt82lQzOGrjHmGzGEu2GLx9WKO +e4nkj335kX6fIhMMqSXBFbTJZwXoYvk5J8ZnaARbYG0m5nxDCwRjX5HWa8q0B2Po +ta53w01sKKznzlPjUhsdhEthun7MCFfLZpgvcZ9xVzOXo3/Zfn2+RrsPSjrVDqBy +hj+k5mW4gQKBgHFWKf3LTO7cBdvsD8ou4mjn7nVgMi1kb/wR4wdnxzmMtdR4STi4 +GYkVVBhgQ5M8mDY7UoWFdH3FfCt8cI0Lcimn5ROl8RSNSeZKeL3c7lNtNRmHr/8R +WaVTrlOAlBjxFiWEF1dWNW6ah9jF7RIV+DfOxj6ZkhTk2CAmjfb1AMpFAoGABf96 +KdNG/vGipDtcYSo8ZTaXoke0nmISARqdb5TEnAsnKoJVDInoEUARi9T411YO9x2z +MlRZzFOG3xzhhxVLi53BKAcAaUXOJ4MrGVcfbYvDhQcGbiJ5qOO3UaWlEVUtPUhE +LR+nDCsB1+9yT2zlQi3QTSJflt5W1QQZ2TrmwAECgYEAvQ7+sTcHs1K9yKj7koEu +A19FbMA0IwvrVRcV/VqmlsoW6e6wW2YND+GtaDbKdD0aBPivqLJwpNFrsRA+W0iB +vzmML6sKhhL+j7tjSgq+iQdBkKz0j9PyReuhe9CRnljMmyun+4qKEk0KUvxBrjPY +Skn+ML18qyUoEPnmbpfHxCs= +-----END PRIVATE KEY----- diff --git a/letsencrypt/tests/testdata/live/sample-renewal/cert.pem b/letsencrypt/tests/testdata/live/sample-renewal/cert.pem new file mode 120000 index 000000000..e06effe40 --- /dev/null +++ b/letsencrypt/tests/testdata/live/sample-renewal/cert.pem @@ -0,0 +1 @@ +../../archive/sample-renewal/cert1.pem \ No newline at end of file diff --git a/letsencrypt/tests/testdata/live/sample-renewal/chain.pem b/letsencrypt/tests/testdata/live/sample-renewal/chain.pem new file mode 120000 index 000000000..71f665f29 --- /dev/null +++ b/letsencrypt/tests/testdata/live/sample-renewal/chain.pem @@ -0,0 +1 @@ +../../archive/sample-renewal/chain1.pem \ No newline at end of file diff --git a/letsencrypt/tests/testdata/live/sample-renewal/fullchain.pem b/letsencrypt/tests/testdata/live/sample-renewal/fullchain.pem new file mode 120000 index 000000000..0f06f077d --- /dev/null +++ b/letsencrypt/tests/testdata/live/sample-renewal/fullchain.pem @@ -0,0 +1 @@ +../../archive/sample-renewal/fullchain1.pem \ No newline at end of file diff --git a/letsencrypt/tests/testdata/live/sample-renewal/privkey.pem b/letsencrypt/tests/testdata/live/sample-renewal/privkey.pem new file mode 120000 index 000000000..5187eda6b --- /dev/null +++ b/letsencrypt/tests/testdata/live/sample-renewal/privkey.pem @@ -0,0 +1 @@ +../../archive/sample-renewal/privkey1.pem \ No newline at end of file diff --git a/letsencrypt/tests/testdata/sample-renewal.conf b/letsencrypt/tests/testdata/sample-renewal.conf new file mode 100755 index 000000000..16778303a --- /dev/null +++ b/letsencrypt/tests/testdata/sample-renewal.conf @@ -0,0 +1,76 @@ +cert = MAGICDIR/live/sample-renewal/cert.pem +privkey = MAGICDIR/live/sample-renewal/privkey.pem +chain = MAGICDIR/live/sample-renewal/chain.pem +fullchain = MAGICDIR/live/sample-renewal/fullchain.pem +renew_before_expiry = 1 year + +# Options and defaults used in the renewal process +[renewalparams] +no_self_upgrade = False +apache_enmod = a2enmod +no_verify_ssl = False +ifaces = None +apache_dismod = a2dismod +register_unsafely_without_email = False +apache_handle_modules = True +uir = None +installer = none +nginx_ctl = nginx +config_dir = MAGICDIR +text_mode = False +func = +staging = True +prepare = False +work_dir = /var/lib/letsencrypt +tos = False +init = False +http01_port = 80 +duplicate = False +noninteractive_mode = True +key_path = None +nginx = False +nginx_server_root = /etc/nginx +fullchain_path = /home/ubuntu/letsencrypt/chain.pem +email = None +csr = None +agree_dev_preview = None +redirect = None +verb = certonly +verbose_count = -3 +config_file = None +renew_by_default = False +hsts = False +apache_handle_sites = True +authenticator = standalone +domains = isnot.org, +rsa_key_size = 2048 +apache_challenge_location = /etc/apache2 +checkpoints = 1 +manual_test_mode = False +apache = False +cert_path = /home/ubuntu/letsencrypt/cert.pem +webroot_path = None +reinstall = False +expand = False +strict_permissions = False +apache_server_root = /etc/apache2 +account = None +dry_run = False +manual_public_ip_logging_ok = False +chain_path = /home/ubuntu/letsencrypt/chain.pem +break_my_certs = False +standalone = True +manual = False +server = https://acme-staging.api.letsencrypt.org/directory +standalone_supported_challenges = "tls-sni-01,http-01" +webroot = False +os_packages_only = False +apache_init_script = None +user_agent = None +apache_le_vhost_ext = -le-ssl.conf +debug = False +tls_sni_01_port = 443 +logs_dir = /var/log/letsencrypt +apache_vhost_root = /etc/apache2/sites-available +configurator = None +[[webroot_map]] diff --git a/setup.py b/setup.py index b51b53b18..b094e2d04 100644 --- a/setup.py +++ b/setup.py @@ -130,7 +130,6 @@ setup( entry_points={ 'console_scripts': [ 'letsencrypt = letsencrypt.cli:main', - 'letsencrypt-renewer = letsencrypt.renewer:main', ], 'letsencrypt.plugins': [ 'manual = letsencrypt.plugins.manual:Authenticator', diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 53996cd20..b6c76ee22 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -27,6 +27,13 @@ common() { "$@" } +common_no_force_renew() { + letsencrypt_test_no_force_renew \ + --authenticator standalone \ + --installer null \ + "$@" +} + common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth common --domains le2.wtf --standalone-supported-challenges http-01 run common -a manual -d le.wtf auth @@ -44,20 +51,21 @@ common --domains le3.wtf install \ --cert-path "${root}/csr/cert.pem" \ --key-path "${root}/csr/key.pem" -# the following assumes that Boulder issues certificates for less than -# 10 years, otherwise renewal will not take place -cat < "$root/conf/renewer.conf" -renew_before_expiry = 10 years -deploy_before_expiry = 10 years -EOF -letsencrypt-renewer $store_flags -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")" - [ "${dir}/${latest}" = "$live" ] # renewer fails this test -done +# This won't renew (because it's not time yet) +letsencrypt_test_no_force_renew --authenticator standalone --installer null renew + +# This will renew because the expiry is less than 10 years from now +sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le1.wtf.conf" +letsencrypt_test_no_force_renew --authenticator standalone --installer null renew + +ls "$root/conf/archive/le1.wtf" +# 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")" +# [ "${dir}/${latest}" = "$live" ] # renewer fails this test +# done # revoke by account key common revoke --cert-path "$root/conf/live/le.wtf/cert.pem" diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 4572b0fb3..9230cc682 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -28,3 +28,20 @@ letsencrypt_test () { -vvvvvvv \ "$@" } + +letsencrypt_test_no_force_renew () { + letsencrypt \ + --server "${SERVER:-http://localhost:4000/directory}" \ + --no-verify-ssl \ + --tls-sni-01-port 5001 \ + --http-01-port 5002 \ + --manual-test-mode \ + $store_flags \ + --text \ + --no-redirect \ + --agree-tos \ + --register-unsafely-without-email \ + --debug \ + -vvvvvvv \ + "$@" +} diff --git a/tests/letstest/scripts/test_renew_standalone.sh b/tests/letstest/scripts/test_renew_standalone.sh new file mode 100755 index 000000000..d90ae9ab6 --- /dev/null +++ b/tests/letstest/scripts/test_renew_standalone.sh @@ -0,0 +1,55 @@ +#!/bin/bash -x + +# $OS_TYPE $PUBLIC_IP $PRIVATE_IP $PUBLIC_HOSTNAME $BOULDER_URL +# are dynamically set at execution + +# run letsencrypt-apache2 via letsencrypt-auto +cd letsencrypt + +export SUDO=sudo +if [ -f /etc/debian_version ] ; then + echo "Bootstrapping dependencies for Debian-based OSes..." + $SUDO bootstrap/_deb_common.sh +elif [ -f /etc/redhat-release ] ; then + echo "Bootstrapping dependencies for RedHat-based OSes..." + $SUDO bootstrap/_rpm_common.sh +else + echo "Dont have bootstrapping for this OS!" + exit 1 +fi + +bootstrap/dev/venv.sh +sudo venv/bin/letsencrypt certonly --debug --standalone -t --agree-dev-preview --agree-tos \ + --renew-by-default --redirect --register-unsafely-without-email \ + --domain $PUBLIC_HOSTNAME --server $BOULDER_URL -v +if [ $? -ne 0 ] ; then + FAIL=1 +fi + +if [ "$OS_TYPE" = "ubuntu" ] ; then + venv/bin/tox -e apacheconftest +else + echo Not running hackish apache tests on $OS_TYPE +fi + +if [ $? -ne 0 ] ; then + FAIL=1 +fi + +sudo venv/bin/letsencrypt renew --renew-by-default + +if [ $? -ne 0 ] ; then + FAIL=1 +fi + + +ls /etc/letsencrypt/archive/$PUBLIC_HOSTNAME | grep -q 2.pem + +if [ $? -ne 0 ] ; then + FAIL=1 +fi + +# return error if any of the subtests failed +if [ "$FAIL" = 1 ] ; then + exit 1 +fi