Merge branch 'master' into donate-spamfilter

Conflicts:
	letsencrypt/cli.py
This commit is contained in:
Brad Warren
2016-02-05 18:29:14 -08:00
27 changed files with 593 additions and 534 deletions
-5
View File
@@ -1,5 +0,0 @@
:mod:`letsencrypt.renewer`
--------------------------
.. automodule:: letsencrypt.renewer
:members:
-2
View File
@@ -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.
-1
View File
@@ -1 +0,0 @@
.. program-output:: letsencrypt-renewer --help
+1 -3
View File
@@ -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
+1 -3
View File
@@ -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
@@ -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
+125 -78
View File
@@ -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)
+5 -15
View File
@@ -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):
+7 -11
View File
@@ -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)
-1
View File
@@ -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 "
-210
View File
@@ -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
+82 -31
View File
@@ -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
+43 -17
View File
@@ -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):
@@ -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,
@@ -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-----
@@ -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-----
@@ -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-----
@@ -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-----
+1
View File
@@ -0,0 +1 @@
../../archive/sample-renewal/cert1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/sample-renewal/chain1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/sample-renewal/fullchain1.pem
+1
View File
@@ -0,0 +1 @@
../../archive/sample-renewal/privkey1.pem
+76
View File
@@ -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 = <function obtain_cert at 0x7f093a163c08>
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]]
-1
View File
@@ -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',
+22 -14
View File
@@ -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 <<EOF > "$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"
+17
View File
@@ -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 \
"$@"
}
+55
View File
@@ -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