Merge pull request #2359 from letsencrypt/old-renewer

Remove renewer.py
This commit is contained in:
Peter Eckersley 2016-02-03 19:10:30 -08:00
commit d92b424597
8 changed files with 4 additions and 348 deletions

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.renewer`
--------------------------
.. automodule:: letsencrypt.renewer
:members:

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.

View file

@ -1 +0,0 @@
.. program-output:: letsencrypt-renewer --help

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

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
@ -782,7 +782,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):

View file

@ -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")
@ -681,114 +679,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,

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',

View file

@ -44,21 +44,6 @@ 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
# revoke by account key
common revoke --cert-path "$root/conf/live/le.wtf/cert.pem"
# revoke renewed