mirror of
https://github.com/certbot/certbot.git
synced 2026-05-28 04:34:11 -04:00
Merge pull request #2359 from letsencrypt/old-renewer
Remove renewer.py
This commit is contained in:
commit
d92b424597
8 changed files with 4 additions and 348 deletions
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt.renewer`
|
||||
--------------------------
|
||||
|
||||
.. automodule:: letsencrypt.renewer
|
||||
:members:
|
||||
|
|
@ -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 +0,0 @@
|
|||
.. program-output:: letsencrypt-renewer --help
|
||||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
1
setup.py
1
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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue