mirror of
https://github.com/certbot/certbot.git
synced 2026-06-08 00:02:14 -04:00
Add list-certs command (#3669)
* Switch to using absolute path in symlink * save archive_dir to config and read it back * cli_config.archive_dir --> cli_config.default_archive_dir * Use archive_dir specified in renewal config file * add helpful broken symlink info * add docstring to method * Add tests * remove extraneous test imports * fix tests * py2.6 syntax fix * git problems * Add list-certs command * no dict comprehension in python2.6 * add test coverage * More py26 wrangling * update tests for py3 and lint * remove extra dep from test * test coverage * test shouldn't be based on dict representation order * Redo report UX and add tests to cover * remove storage str test * lint and use mock properly * mock properly * address code review comments * lineage --> certificate name and print fullchain and privkey paths * make py26 happy * actually make py26 happy * don't wrap text
This commit is contained in:
parent
a7bfefc6d0
commit
af46f644a7
8 changed files with 197 additions and 13 deletions
|
|
@ -1,8 +1,17 @@
|
|||
"""Tools for managing certificates."""
|
||||
import datetime
|
||||
import logging
|
||||
import pytz
|
||||
import traceback
|
||||
import zope.component
|
||||
|
||||
from certbot import configuration
|
||||
from certbot import interfaces
|
||||
from certbot import renewal
|
||||
from certbot import storage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def update_live_symlinks(config):
|
||||
"""Update the certificate file family symlinks to use archive_dir.
|
||||
|
||||
|
|
@ -20,3 +29,78 @@ def update_live_symlinks(config):
|
|||
storage.RenewableCert(renewal_file,
|
||||
configuration.RenewerConfiguration(renewer_config),
|
||||
update_symlinks=True)
|
||||
|
||||
def _report_lines(msgs):
|
||||
"""Format a results report for a category of single-line renewal outcomes"""
|
||||
return " " + "\n ".join(str(msg) for msg in msgs)
|
||||
|
||||
def _report_human_readable(parsed_certs):
|
||||
"""Format a results report for a parsed cert"""
|
||||
certinfo = []
|
||||
for cert in parsed_certs:
|
||||
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
|
||||
if cert.target_expiry <= now:
|
||||
expiration_text = "EXPIRED"
|
||||
else:
|
||||
diff = cert.target_expiry - now
|
||||
if diff.days == 1:
|
||||
expiration_text = "1 day"
|
||||
elif diff.days < 1:
|
||||
expiration_text = "under 1 day"
|
||||
else:
|
||||
expiration_text = "{0} days".format(diff.days)
|
||||
valid_string = "{0} ({1})".format(cert.target_expiry, expiration_text)
|
||||
certinfo.append(" Certificate Name: {0}\n"
|
||||
" Domains: {1}\n"
|
||||
" Valid Until: {2}\n"
|
||||
" Certificate Path: {3}\n"
|
||||
" Private Key Path: {4}".format(
|
||||
cert.lineagename,
|
||||
" ".join(cert.names()),
|
||||
valid_string,
|
||||
cert.fullchain,
|
||||
cert.privkey))
|
||||
return "\n".join(certinfo)
|
||||
|
||||
def _describe_certs(parsed_certs, parse_failures):
|
||||
"""Print information about the certs we know about"""
|
||||
out = []
|
||||
|
||||
notify = out.append
|
||||
|
||||
if not parsed_certs and not parse_failures:
|
||||
notify("No certs found.")
|
||||
else:
|
||||
if parsed_certs:
|
||||
notify("Found the following certs:")
|
||||
notify(_report_human_readable(parsed_certs))
|
||||
if parse_failures:
|
||||
notify("\nThe following renewal configuration files "
|
||||
"were invalid:")
|
||||
notify(_report_lines(parse_failures))
|
||||
|
||||
disp = zope.component.getUtility(interfaces.IDisplay)
|
||||
disp.notification("\n".join(out), pause=False, wrap=False)
|
||||
|
||||
def certificates(config):
|
||||
"""Display information about certs configured with Certbot
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`certbot.interfaces.IConfig`
|
||||
"""
|
||||
renewer_config = configuration.RenewerConfiguration(config)
|
||||
parsed_certs = []
|
||||
parse_failures = []
|
||||
for renewal_file in renewal.renewal_conf_files(renewer_config):
|
||||
try:
|
||||
renewal_candidate = storage.RenewableCert(renewal_file,
|
||||
configuration.RenewerConfiguration(config))
|
||||
parsed_certs.append(renewal_candidate)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.warning("Renewal configuration file %s produced an "
|
||||
"unexpected error: %s. Skipping.", renewal_file, e)
|
||||
logger.debug("Traceback was:\n%s", traceback.format_exc())
|
||||
parse_failures.append(renewal_file)
|
||||
|
||||
# Describe all the certs
|
||||
_describe_certs(parsed_certs, parse_failures)
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ cert. Major SUBCOMMANDS are:
|
|||
config_changes Show changes made to server config during installation
|
||||
update_symlinks Update cert symlinks based on renewal config file
|
||||
plugins Display information about installed plugins
|
||||
certificates Display information about certs configured with Certbot
|
||||
|
||||
""".format(cli_command)
|
||||
|
||||
|
|
@ -324,7 +325,8 @@ class HelpfulArgumentParser(object):
|
|||
"install": main.install, "plugins": main.plugins_cmd,
|
||||
"register": main.register, "renew": main.renew,
|
||||
"revoke": main.revoke, "rollback": main.rollback,
|
||||
"everything": main.run, "update_symlinks": main.update_symlinks}
|
||||
"everything": main.run, "update_symlinks": main.update_symlinks,
|
||||
"certificates": main.certificates}
|
||||
|
||||
# List of topics for which additional help can be provided
|
||||
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS)
|
||||
|
|
|
|||
|
|
@ -484,6 +484,11 @@ def update_symlinks(config, unused_plugins):
|
|||
"""
|
||||
cert_manager.update_live_symlinks(config)
|
||||
|
||||
def certificates(config, unused_plugins):
|
||||
"""Display information about certs configured with Certbot
|
||||
"""
|
||||
cert_manager.certificates(config)
|
||||
|
||||
def revoke(config, unused_plugins): # TODO: coop with renewal config
|
||||
"""Revoke a previously obtained certificate."""
|
||||
# For user-agent construction
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ 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
|
||||
and policies to ensure that we can try to proceed with the renwal
|
||||
and policies to ensure that we can try to proceed with the renewal
|
||||
request. The config argument is modified by including relevant options
|
||||
read from the renewal configuration file.
|
||||
|
||||
|
|
|
|||
|
|
@ -263,6 +263,15 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
self._update_symlinks()
|
||||
self._check_symlinks()
|
||||
|
||||
@property
|
||||
def target_expiry(self):
|
||||
"""The current target certificate's expiration datetime
|
||||
|
||||
:returns: Expiration datetime of the current target certificate
|
||||
:rtype: :class:`datetime.datetime`
|
||||
"""
|
||||
return crypto_util.notAfter(self.current_target("cert"))
|
||||
|
||||
@property
|
||||
def archive_dir(self):
|
||||
"""Returns the default or specified archive directory"""
|
||||
|
|
@ -671,9 +680,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
if self.has_pending_deployment():
|
||||
interval = self.configuration.get("deploy_before_expiry",
|
||||
"5 days")
|
||||
expiry = crypto_util.notAfter(self.current_target("cert"))
|
||||
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
|
||||
if expiry < add_time_interval(now, interval):
|
||||
if self.target_expiry < add_time_interval(now, interval):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -8,25 +8,21 @@ import unittest
|
|||
import configobj
|
||||
import mock
|
||||
|
||||
from certbot import configuration
|
||||
from certbot.storage import ALL_FOUR
|
||||
|
||||
class CertManagerTest(unittest.TestCase):
|
||||
"""Tests for certbot.cert_manager
|
||||
class BaseCertManagerTest(unittest.TestCase):
|
||||
"""Base class for setting up Cert Manager tests.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
|
||||
os.makedirs(os.path.join(self.tempdir, "renewal"))
|
||||
|
||||
mock_namespace = mock.MagicMock(
|
||||
self.cli_config = mock.MagicMock(
|
||||
config_dir=self.tempdir,
|
||||
work_dir=self.tempdir,
|
||||
logs_dir=self.tempdir,
|
||||
)
|
||||
|
||||
self.cli_config = configuration.RenewerConfiguration(
|
||||
namespace=mock_namespace
|
||||
quiet=False,
|
||||
)
|
||||
|
||||
self.domains = {
|
||||
|
|
@ -67,6 +63,9 @@ class CertManagerTest(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
shutil.rmtree(self.tempdir)
|
||||
|
||||
class UpdateLiveSymlinksTest(BaseCertManagerTest):
|
||||
"""Tests for certbot.cert_manager.update_live_symlinks
|
||||
"""
|
||||
def test_update_live_symlinks(self):
|
||||
"""Test update_live_symlinks"""
|
||||
# pylint: disable=too-many-statements
|
||||
|
|
@ -97,5 +96,87 @@ class CertManagerTest(unittest.TestCase):
|
|||
self.assertEqual(os.readlink(self.configs[domain][kind]),
|
||||
archive_paths[domain][kind])
|
||||
|
||||
class CertificatesTest(BaseCertManagerTest):
|
||||
"""Tests for certbot.cert_manager.certificates
|
||||
"""
|
||||
def _certificates(self, *args, **kwargs):
|
||||
from certbot.cert_manager import certificates
|
||||
return certificates(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@mock.patch('zope.component.getUtility')
|
||||
def test_certificates_parse_fail(self, mock_utility, mock_logger):
|
||||
self._certificates(self.cli_config)
|
||||
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
|
||||
self.assertTrue(mock_utility.called)
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@mock.patch('zope.component.getUtility')
|
||||
def test_certificates_quiet(self, mock_utility, mock_logger):
|
||||
self.cli_config.quiet = True
|
||||
self._certificates(self.cli_config)
|
||||
self.assertFalse(mock_utility.notification.called)
|
||||
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@mock.patch('zope.component.getUtility')
|
||||
@mock.patch("certbot.storage.RenewableCert")
|
||||
@mock.patch('certbot.cert_manager._report_human_readable')
|
||||
def test_certificates_parse_success(self, mock_report, mock_renewable_cert,
|
||||
mock_utility, mock_logger):
|
||||
mock_report.return_value = ""
|
||||
self._certificates(self.cli_config)
|
||||
self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member
|
||||
self.assertTrue(mock_report.called)
|
||||
self.assertTrue(mock_utility.called)
|
||||
self.assertTrue(mock_renewable_cert.called)
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@mock.patch('zope.component.getUtility')
|
||||
def test_certificates_no_files(self, mock_utility, mock_logger):
|
||||
tempdir = tempfile.mkdtemp()
|
||||
|
||||
cli_config = mock.MagicMock(
|
||||
config_dir=tempdir,
|
||||
work_dir=tempdir,
|
||||
logs_dir=tempdir,
|
||||
quiet=False,
|
||||
)
|
||||
|
||||
os.makedirs(os.path.join(tempdir, "renewal"))
|
||||
self._certificates(cli_config)
|
||||
self.assertFalse(mock_logger.warning.called) #pylint: disable=no-member
|
||||
self.assertTrue(mock_utility.called)
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
def test_report_human_readable(self):
|
||||
from certbot import cert_manager
|
||||
import datetime, pytz
|
||||
expiry = pytz.UTC.fromutc(datetime.datetime.utcnow())
|
||||
|
||||
cert = mock.MagicMock(lineagename="nameone")
|
||||
cert.target_expiry = expiry
|
||||
cert.names.return_value = ["nameone", "nametwo"]
|
||||
parsed_certs = [cert]
|
||||
# pylint: disable=protected-access
|
||||
out = cert_manager._report_human_readable(parsed_certs)
|
||||
self.assertTrue('EXPIRED' in out)
|
||||
|
||||
cert.target_expiry += datetime.timedelta(hours=2)
|
||||
# pylint: disable=protected-access
|
||||
out = cert_manager._report_human_readable(parsed_certs)
|
||||
self.assertTrue('under 1 day' in out)
|
||||
|
||||
cert.target_expiry += datetime.timedelta(days=1)
|
||||
# pylint: disable=protected-access
|
||||
out = cert_manager._report_human_readable(parsed_certs)
|
||||
self.assertTrue('1 day' in out)
|
||||
self.assertFalse('under' in out)
|
||||
|
||||
cert.target_expiry += datetime.timedelta(days=2)
|
||||
# pylint: disable=protected-access
|
||||
out = cert_manager._report_human_readable(parsed_certs)
|
||||
self.assertTrue('3 days' in out)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -277,6 +277,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
self._call_no_clientmock(['update_symlinks'])
|
||||
self.assertEqual(1, mock_cert_manager.call_count)
|
||||
|
||||
@mock.patch('certbot.cert_manager.certificates')
|
||||
def test_certificates(self, mock_cert_manager):
|
||||
self._call_no_clientmock(['certificates'])
|
||||
self.assertEqual(1, mock_cert_manager.call_count)
|
||||
|
||||
def test_plugins(self):
|
||||
flags = ['--init', '--prepare', '--authenticators', '--installers']
|
||||
for args in itertools.chain(
|
||||
|
|
|
|||
|
|
@ -751,6 +751,5 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
storage.RenewableCert(self.config.filename, self.cli_config,
|
||||
update_symlinks=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
Loading…
Reference in a new issue