From 13a5faeda7728e12869860f94dc0ac9c9df46824 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 1 Apr 2016 18:14:50 -0700 Subject: [PATCH 01/15] An attempt at --quiet --- letsencrypt/cli.py | 17 ++++++++----- letsencrypt/main.py | 15 ++++++----- letsencrypt/renewal.py | 58 +++++++++++++++++++++++++----------------- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 256e0c801..7ff47990f 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -600,6 +600,13 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "regardless of whether it is near expiry. (Often " "--keep-until-expiring is more appropriate). Also implies " "--expand.") + helpful.add( + "automation", "--allow-subset-of-names", action="store_true", + help="When performing domain validation, do not consider it a failure " + "if authorizations can not be obtained for a strict subset of " + "the requested domains. This may be useful for allowing renewals for " + "multiple domains to succeed even if some domains no longer point " + "at this system. This option cannot be used with --csr.") helpful.add( "automation", "--agree-tos", dest="tos", action="store_true", help="Agree to the Let's Encrypt Subscriber Agreement") @@ -617,6 +624,10 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "automation", "--no-self-upgrade", action="store_true", help="(letsencrypt-auto only) prevent the letsencrypt-auto script from" " upgrading itself to newer released versions") + helpful.add( + "automation", "-q", "--quiet", dest="quiet", action="store_true", + help="Silence all output except errors. Useful for automation via cron." + "Implies --non-interactive.") helpful.add_group( "testing", description="The following flags are meant for " @@ -677,12 +688,6 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): "security", "--strict-permissions", action="store_true", help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") - helpful.add( - "automation", "--allow-subset-of-names", - action="store_true", - help="When performing domain validation, do not consider it a failure " - "if authorizations can not be obtained for a strict subset of " - "the requested domains. This option cannot be used with --csr.") helpful.add_group( "renew", description="The 'renew' subcommand will attempt to renew all" diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 0afccc85e..1998e09ea 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -34,7 +34,6 @@ from letsencrypt.display import util as display_util, ops as display_ops from letsencrypt.plugins import disco as plugins_disco from letsencrypt.plugins import selection as plug_sel - logger = logging.getLogger(__name__) @@ -537,20 +536,21 @@ def obtain_cert(config, plugins, lineage=None): domains = _find_domains(config, installer) _, action = _auth_from_domains(le_client, config, domains, lineage) + notify = zope.component.getUtility(interfaces.IDisplay).notification if config.dry_run: _report_successful_dry_run(config) elif config.verb == "renew": if installer is None: # Tell the user that the server was not restarted. - print("new certificate deployed without reload, fullchain is", - lineage.fullchain) + notify("new certificate deployed without reload, fullchain is {0}".format( + lineage.fullchain), pause=False) else: # 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("new certificate deployed with reload of", - config.installer, "server; fullchain is", lineage.fullchain) + notify("new certificate deployed with reload of {0} server; fullchain is {1}".format( + config.installer, lineage.fullchain), pause=False) _suggest_donation_if_appropriate(config, action) def renew(config, unused_plugins): @@ -689,7 +689,10 @@ def main(cli_args=sys.argv[1:]): sys.excepthook = functools.partial(_handle_exception, config=config) # Displayer - if config.noninteractive_mode: + if config.quiet: + config.noninteractive_mode = True + displayer = display_util.NoninteractiveDisplay(open("/dev/null", "w")) + elif config.noninteractive_mode: displayer = display_util.NoninteractiveDisplay(sys.stdout) elif config.text_mode: displayer = display_util.FileDisplay(sys.stdout) diff --git a/letsencrypt/renewal.py b/letsencrypt/renewal.py index 27546bec9..f0edd55fe 100644 --- a/letsencrypt/renewal.py +++ b/letsencrypt/renewal.py @@ -12,6 +12,7 @@ import zope.component from letsencrypt import configuration from letsencrypt import cli from letsencrypt import errors +from letsencrypt import interfaces from letsencrypt import storage from letsencrypt.plugins import disco as plugins_disco @@ -201,44 +202,53 @@ def should_renew(config, lineage): def _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures): - def _status(msgs, category): - return " " + "\n ".join("%s (%s)" % (m, category) for m in msgs) + def notify(msg): + "Our version of print()" + zope.component.getUtility(interfaces.IDisplay).notification(msg, pause=False) + + def report(msgs, category): + "Report results for a category of renewal outcomes" + notify(" " + "\n ".join("%s (%s)" % (m, category) for m in msgs)) + if config.dry_run: - print("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") - print("** (The test certificates below have not been saved.)") - print() + notify("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") + notify("** (The test certificates below have not been saved.)") + notify("") if renew_skipped: - print("The following certs are not due for renewal yet:") - print(_status(renew_skipped, "skipped")) + notify("The following certs are not due for renewal yet:") + report(renew_skipped, "skipped") if not renew_successes and not renew_failures: - print("No renewals were attempted.") + notify("No renewals were attempted.") elif renew_successes and not renew_failures: - print("Congratulations, all renewals succeeded. The following certs " - "have been renewed:") - print(_status(renew_successes, "success")) + notify("Congratulations, all renewals succeeded. The following certs " + "have been renewed:") + report(renew_successes, "success") elif renew_failures and not renew_successes: - print("All renewal attempts failed. The following certs could not be " - "renewed:") - print(_status(renew_failures, "failure")) + notify("All renewal attempts failed. The following certs could not be " + "renewed:") + report(renew_failures, "failure") elif renew_failures and renew_successes: - print("The following certs were successfully renewed:") - print(_status(renew_successes, "success")) - print("\nThe following certs could not be renewed:") - print(_status(renew_failures, "failure")) + notify("The following certs were successfully renewed:") + report(renew_successes, "success") + notify("\nThe following certs could not be renewed:") + report(renew_failures, "failure") if parse_failures: - print("\nAdditionally, the following renewal configuration files " - "were invalid: ") - print(_status(parse_failures, "parsefail")) + notify("\nAdditionally, the following renewal configuration files " + "were invalid: ") + report(parse_failures, "parsefail") if config.dry_run: - print("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") - print("** (The test certificates above have not been saved.)") + notify("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") + notify("** (The test certificates above have not been saved.)") def renew_all_lineages(config): """Examine each lineage; renew if due and report results""" + def _notify(msg): + zope.component.getUtility(interfaces.IDisplay).notification(msg, pause=False) + if config.domains != []: raise errors.Error("Currently, the renew verb is only capable of " "renewing all installed certificates that are due " @@ -253,7 +263,7 @@ def renew_all_lineages(config): renew_skipped = [] parse_failures = [] for renewal_file in renewal_conf_files(renewer_config): - print("Processing " + renewal_file) + _notify("Processing " + renewal_file) lineage_config = copy.deepcopy(config) # Note that this modifies config (to add back the configuration From 1f4f1fdf39e83757d53577e3203f478446fac76d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 1 Apr 2016 18:22:07 -0700 Subject: [PATCH 02/15] Verbosity tuning --- letsencrypt/renewal.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/letsencrypt/renewal.py b/letsencrypt/renewal.py index f0edd55fe..af4bbca1e 100644 --- a/letsencrypt/renewal.py +++ b/letsencrypt/renewal.py @@ -203,12 +203,12 @@ def should_renew(config, lineage): def _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures): def notify(msg): - "Our version of print()" + "A variant of print() that is silenced by -q" zope.component.getUtility(interfaces.IDisplay).notification(msg, pause=False) def report(msgs, category): "Report results for a category of renewal outcomes" - notify(" " + "\n ".join("%s (%s)" % (m, category) for m in msgs)) + return " " + "\n ".join("%s (%s)" % (m, category) for m in msgs) if config.dry_run: notify("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") @@ -216,27 +216,27 @@ def _renew_describe_results(config, renew_successes, renew_failures, notify("") if renew_skipped: notify("The following certs are not due for renewal yet:") - report(renew_skipped, "skipped") + notify(report(renew_skipped, "skipped")) if not renew_successes and not renew_failures: notify("No renewals were attempted.") elif renew_successes and not renew_failures: notify("Congratulations, all renewals succeeded. The following certs " "have been renewed:") - report(renew_successes, "success") + notify(report(renew_successes, "success")) elif renew_failures and not renew_successes: - notify("All renewal attempts failed. The following certs could not be " + logger.error("All renewal attempts failed. The following certs could not be " "renewed:") - report(renew_failures, "failure") + logger.error(report(renew_failures, "failure")) elif renew_failures and renew_successes: - notify("The following certs were successfully renewed:") - report(renew_successes, "success") - notify("\nThe following certs could not be renewed:") - report(renew_failures, "failure") + logger.error("The following certs were successfully renewed:") + logger.error(report(renew_successes, "success")) + logger.error("\nThe following certs could not be renewed:") + logger.error(renew_failures, "failure") if parse_failures: - notify("\nAdditionally, the following renewal configuration files " - "were invalid: ") - report(parse_failures, "parsefail") + logger.error("\nAdditionally, the following renewal configuration files " + "were invalid: ") + logger.error(parse_failures, "parsefail") if config.dry_run: notify("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") From 10214763b0f2a4573e07a21d0321472593a61552 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 1 Apr 2016 18:23:38 -0700 Subject: [PATCH 03/15] bugfix --- letsencrypt/renewal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/renewal.py b/letsencrypt/renewal.py index af4bbca1e..afca174a7 100644 --- a/letsencrypt/renewal.py +++ b/letsencrypt/renewal.py @@ -231,7 +231,7 @@ def _renew_describe_results(config, renew_successes, renew_failures, logger.error("The following certs were successfully renewed:") logger.error(report(renew_successes, "success")) logger.error("\nThe following certs could not be renewed:") - logger.error(renew_failures, "failure") + logger.error(report(renew_failures, "failure")) if parse_failures: logger.error("\nAdditionally, the following renewal configuration files " From e2340a5da91036bbc68eccfec46b7d1d574e20d2 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 1 Apr 2016 18:40:21 -0700 Subject: [PATCH 04/15] Make all of _renew_describe_results print or not print depending on whether there have been any errors --- letsencrypt/renewal.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/letsencrypt/renewal.py b/letsencrypt/renewal.py index afca174a7..672106e7b 100644 --- a/letsencrypt/renewal.py +++ b/letsencrypt/renewal.py @@ -4,6 +4,7 @@ import copy import glob import logging import os +import sys import traceback import six @@ -14,6 +15,7 @@ from letsencrypt import cli from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import storage +from letsencrypt.display import util as display_util from letsencrypt.plugins import disco as plugins_disco logger = logging.getLogger(__name__) @@ -200,15 +202,20 @@ def should_renew(config, lineage): return False +def report(msgs, category): + "Report results for a category of renewal outcomes" + lines = ("%s (%s)" % (m, category) for m in msgs) + return " " + "\n ".join(lines) + def _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures): - def notify(msg): - "A variant of print() that is silenced by -q" - zope.component.getUtility(interfaces.IDisplay).notification(msg, pause=False) + if config.quiet and (renew_failures or parse_failures): + # In case of errors, spin up a new non-quiet output display + dest = display_util.NoninteractiveDisplay(sys.stdout).log() + else: + dest = zope.component.getUtility(interfaces.IDisplay) - def report(msgs, category): - "Report results for a category of renewal outcomes" - return " " + "\n ".join("%s (%s)" % (m, category) for m in msgs) + notify = lambda msg: dest.notification(msg, pause=False) if config.dry_run: notify("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") @@ -224,19 +231,19 @@ def _renew_describe_results(config, renew_successes, renew_failures, "have been renewed:") notify(report(renew_successes, "success")) elif renew_failures and not renew_successes: - logger.error("All renewal attempts failed. The following certs could not be " + notify("All renewal attempts failed. The following certs could not be " "renewed:") - logger.error(report(renew_failures, "failure")) + notify(report(renew_failures, "failure")) elif renew_failures and renew_successes: - logger.error("The following certs were successfully renewed:") - logger.error(report(renew_successes, "success")) - logger.error("\nThe following certs could not be renewed:") - logger.error(report(renew_failures, "failure")) + notify("The following certs were successfully renewed:") + notify(report(renew_successes, "success")) + notify("\nThe following certs could not be renewed:") + notify(report(renew_failures, "failure")) if parse_failures: - logger.error("\nAdditionally, the following renewal configuration files " - "were invalid: ") - logger.error(parse_failures, "parsefail") + notify("\nAdditionally, the following renewal configuration files " + "were invalid: ") + notify(parse_failures, "parsefail") if config.dry_run: notify("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") From 8f454e0666ff763cee5ed9d336ea45555c18deb4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 1 Apr 2016 18:43:18 -0700 Subject: [PATCH 05/15] But let's not get ahead of ourselves --- letsencrypt/renewal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/renewal.py b/letsencrypt/renewal.py index 672106e7b..ec8aa25ef 100644 --- a/letsencrypt/renewal.py +++ b/letsencrypt/renewal.py @@ -211,7 +211,7 @@ def _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures): if config.quiet and (renew_failures or parse_failures): # In case of errors, spin up a new non-quiet output display - dest = display_util.NoninteractiveDisplay(sys.stdout).log() + dest = display_util.NoninteractiveDisplay(sys.stdout) else: dest = zope.component.getUtility(interfaces.IDisplay) From 6c13616b1563fbbfc6467efe531d285e1cfed028 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 1 Apr 2016 18:47:30 -0700 Subject: [PATCH 06/15] Less delimitation --- letsencrypt/renewal.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/letsencrypt/renewal.py b/letsencrypt/renewal.py index ec8aa25ef..bcea6c127 100644 --- a/letsencrypt/renewal.py +++ b/letsencrypt/renewal.py @@ -203,19 +203,15 @@ def should_renew(config, lineage): def report(msgs, category): - "Report results for a category of renewal outcomes" + "Format a results report for a category of renewal outcomes" lines = ("%s (%s)" % (m, category) for m in msgs) return " " + "\n ".join(lines) def _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures): - if config.quiet and (renew_failures or parse_failures): - # In case of errors, spin up a new non-quiet output display - dest = display_util.NoninteractiveDisplay(sys.stdout) - else: - dest = zope.component.getUtility(interfaces.IDisplay) - notify = lambda msg: dest.notification(msg, pause=False) + out = [] + notify = out.append if config.dry_run: notify("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") @@ -249,6 +245,14 @@ def _renew_describe_results(config, renew_successes, renew_failures, notify("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") notify("** (The test certificates above have not been saved.)") + if config.quiet and (renew_failures or parse_failures): + # In case of errors, spin up a new non-quiet output display + dest = display_util.NoninteractiveDisplay(sys.stdout) + else: + dest = zope.component.getUtility(interfaces.IDisplay) + + dest.notification("\n".join(out), pause=False) + def renew_all_lineages(config): """Examine each lineage; renew if due and report results""" From 9201f2691f55d946924c2458f74f0d85013f92c8 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 1 Apr 2016 20:14:06 -0700 Subject: [PATCH 07/15] Wrangle the Reporter to also be --quiet --- letsencrypt/account.py | 2 +- letsencrypt/hooks.py | 2 +- letsencrypt/main.py | 2 +- letsencrypt/renewal.py | 8 +++----- letsencrypt/reporter.py | 15 +++++++++++---- letsencrypt/tests/cli_test.py | 2 +- letsencrypt/tests/hook_test.py | 24 ++++++++++-------------- letsencrypt/tests/reporter_test.py | 3 ++- 8 files changed, 30 insertions(+), 28 deletions(-) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index c41b10c4a..464d07b18 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -98,7 +98,7 @@ def report_new_account(acc, config): recovery_msg = ("If you lose your account credentials, you can " "recover through e-mails sent to {0}.".format( ", ".join(acc.regr.body.emails))) - reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY) + reporter.add_message(recovery_msg, reporter.MEDIUM_PRIORITY) class AccountMemoryStorage(interfaces.AccountStorage): diff --git a/letsencrypt/hooks.py b/letsencrypt/hooks.py index 6a0997708..dce17713d 100644 --- a/letsencrypt/hooks.py +++ b/letsencrypt/hooks.py @@ -62,7 +62,7 @@ def renew_hook(config, domains, lineage_path): os.environ["RENEWED_LINEAGE"] = lineage_path _run_hook(config.renew_hook) else: - print("Dry run: skipping renewal hook command: {0}".format(config.renew_hook)) + logger.warning("Dry run: skipping renewal hook command: %s", config.renew_hook) def _run_hook(shell_cmd): """Run a hook command. diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 8e2d77b3a..059b2a36d 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -687,7 +687,7 @@ def main(cli_args=sys.argv[1:]): zope.component.provideUtility(displayer) # Reporter - report = reporter.Reporter() + report = reporter.Reporter(config) zope.component.provideUtility(report) atexit.register(report.atexit_print_messages) diff --git a/letsencrypt/renewal.py b/letsencrypt/renewal.py index 00cb46de7..b3999b7ba 100644 --- a/letsencrypt/renewal.py +++ b/letsencrypt/renewal.py @@ -288,7 +288,7 @@ def _renew_describe_results(config, renew_successes, renew_failures, if parse_failures: notify("\nAdditionally, the following renewal configuration files " - "were invalid: ") + "were invalid: ") notify(parse_failures, "parsefail") if config.dry_run: @@ -307,9 +307,6 @@ def _renew_describe_results(config, renew_successes, renew_failures, def renew_all_lineages(config): """Examine each lineage; renew if due and report results""" - def _notify(msg): - zope.component.getUtility(interfaces.IDisplay).notification(msg, pause=False) - if config.domains != []: raise errors.Error("Currently, the renew verb is only capable of " "renewing all installed certificates that are due " @@ -324,7 +321,8 @@ def renew_all_lineages(config): renew_skipped = [] parse_failures = [] for renewal_file in renewal_conf_files(renewer_config): - _notify("Processing " + renewal_file) + disp = zope.component.getUtility(interfaces.IDisplay) + disp.notification("Processing " + renewal_file, pause=False) lineage_config = copy.deepcopy(config) # Note that this modifies config (to add back the configuration diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 147928e3c..178747121 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -35,8 +35,9 @@ class Reporter(object): _msg_type = collections.namedtuple('ReporterMsg', 'priority text on_crash') - def __init__(self): + def __init__(self, config): self.messages = queue.PriorityQueue() + self.config = config def add_message(self, msg, priority, on_crash=True): """Adds msg to the list of messages to be printed. @@ -76,9 +77,10 @@ class Reporter(object): if not self.messages.empty(): no_exception = sys.exc_info()[0] is None bold_on = sys.stdout.isatty() - if bold_on: - print(le_util.ANSI_SGR_BOLD) - print('IMPORTANT NOTES:') + if not self.config.quiet: + if bold_on: + print(le_util.ANSI_SGR_BOLD) + print('IMPORTANT NOTES:') first_wrapper = textwrap.TextWrapper( initial_indent=' - ', subsequent_indent=(' ' * 3)) next_wrapper = textwrap.TextWrapper( @@ -86,6 +88,11 @@ class Reporter(object): subsequent_indent=first_wrapper.subsequent_indent) while not self.messages.empty(): msg = self.messages.get() + if self.config.quiet: + # In --quiet mode, we only print high priority messages that + # are flagged for crash cases + if not (msg.priority == self.HIGH_PRIORITY and msg.on_crash): + continue if no_exception or msg.on_crash: if bold_on and msg.priority > self.HIGH_PRIORITY: sys.stdout.write(le_util.ANSI_SGR_RESET) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index de8b0c7e8..7819eff66 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -373,7 +373,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods try: self._call(['--csr', CSR]) except errors.Error as e: - assert "Please try the certonly" in e.message + assert "Please try the certonly" in repr(e) return assert False, "Expected supplying --csr to fail with default verb" diff --git a/letsencrypt/tests/hook_test.py b/letsencrypt/tests/hook_test.py index 6506eea2c..62d3d5986 100644 --- a/letsencrypt/tests/hook_test.py +++ b/letsencrypt/tests/hook_test.py @@ -3,7 +3,6 @@ import os import unittest -import sys import mock @@ -47,12 +46,14 @@ class HookTest(unittest.TestCase): mockwhich.return_value = None self.assertEqual(hooks._prog("funky"), None) - def _test_a_hook(self, config, hook_function, calls_expected): - with mock.patch('letsencrypt.hooks.logger'): - with mock.patch('letsencrypt.hooks._run_hook') as mock_run_hook: - hook_function(config) - hook_function(config) - self.assertEqual(mock_run_hook.call_count, calls_expected) + @mock.patch('letsencrypt.hooks.logger') + def _test_a_hook(self, config, hook_function, calls_expected, mock_logger): + mock_logger.warning = mock.MagicMock() + with mock.patch('letsencrypt.hooks._run_hook') as mock_run_hook: + hook_function(config) + hook_function(config) + self.assertEqual(mock_run_hook.call_count, calls_expected) + return mock_logger.warning def test_pre_hook(self): config = mock.MagicMock(pre_hook="true") @@ -78,13 +79,8 @@ class HookTest(unittest.TestCase): self.assertEqual(os.environ["RENEWED_LINEAGE"], "thing") config = mock.MagicMock(renew_hook="true", dry_run=True) - if sys.version_info < (2, 7): - # the print() function is not mockable in py26 - self._test_a_hook(config, rhook, 0) - else: - with mock.patch("letsencrypt.hooks.print") as mock_print: - self._test_a_hook(config, rhook, 0) - self.assertEqual(mock_print.call_count, 2) + mock_warn = self._test_a_hook(config, rhook, 0) + self.assertEqual(mock_warn.call_count, 2) @mock.patch('letsencrypt.hooks.Popen') def test_run_hook(self, mock_popen): diff --git a/letsencrypt/tests/reporter_test.py b/letsencrypt/tests/reporter_test.py index 26a1105c8..191c1b933 100644 --- a/letsencrypt/tests/reporter_test.py +++ b/letsencrypt/tests/reporter_test.py @@ -1,4 +1,5 @@ """Tests for letsencrypt.reporter.""" +import mock import sys import unittest @@ -10,7 +11,7 @@ class ReporterTest(unittest.TestCase): def setUp(self): from letsencrypt import reporter - self.reporter = reporter.Reporter() + self.reporter = reporter.Reporter(mock.MagicMock(quiet=False)) self.old_stdout = sys.stdout sys.stdout = six.StringIO() From 7fbd800ddd96e37d6293afc6b9580c8d4cb040b6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 1 Apr 2016 20:17:06 -0700 Subject: [PATCH 08/15] lint --- letsencrypt/tests/hook_test.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt/tests/hook_test.py b/letsencrypt/tests/hook_test.py index 62d3d5986..3751133cf 100644 --- a/letsencrypt/tests/hook_test.py +++ b/letsencrypt/tests/hook_test.py @@ -46,14 +46,14 @@ class HookTest(unittest.TestCase): mockwhich.return_value = None self.assertEqual(hooks._prog("funky"), None) - @mock.patch('letsencrypt.hooks.logger') - def _test_a_hook(self, config, hook_function, calls_expected, mock_logger): - mock_logger.warning = mock.MagicMock() - with mock.patch('letsencrypt.hooks._run_hook') as mock_run_hook: - hook_function(config) - hook_function(config) - self.assertEqual(mock_run_hook.call_count, calls_expected) - return mock_logger.warning + def _test_a_hook(self, config, hook_function, calls_expected): + with mock.patch('letsencrypt.hooks.logger') as mock_logger: + mock_logger.warning = mock.MagicMock() + with mock.patch('letsencrypt.hooks._run_hook') as mock_run_hook: + hook_function(config) + hook_function(config) + self.assertEqual(mock_run_hook.call_count, calls_expected) + return mock_logger.warning def test_pre_hook(self): config = mock.MagicMock(pre_hook="true") From f5e0aca89129918bc488a97452b7714274ebe956 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 2 Apr 2016 12:25:10 -0700 Subject: [PATCH 09/15] Try things with just print() --- letsencrypt/main.py | 2 +- letsencrypt/renewal.py | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/letsencrypt/main.py b/letsencrypt/main.py index 059b2a36d..ac0ec69e8 100644 --- a/letsencrypt/main.py +++ b/letsencrypt/main.py @@ -674,7 +674,7 @@ def main(cli_args=sys.argv[1:]): # Displayer if config.quiet: config.noninteractive_mode = True - displayer = display_util.NoninteractiveDisplay(open("/dev/null", "w")) + displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w")) elif config.noninteractive_mode: displayer = display_util.NoninteractiveDisplay(sys.stdout) elif config.text_mode: diff --git a/letsencrypt/renewal.py b/letsencrypt/renewal.py index b3999b7ba..55a7d8262 100644 --- a/letsencrypt/renewal.py +++ b/letsencrypt/renewal.py @@ -295,13 +295,9 @@ def _renew_describe_results(config, renew_successes, renew_failures, notify("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") notify("** (The test certificates above have not been saved.)") - if config.quiet and (renew_failures or parse_failures): - # In case of errors, spin up a new non-quiet output display - dest = display_util.NoninteractiveDisplay(sys.stdout) - else: - dest = zope.component.getUtility(interfaces.IDisplay) - - dest.notification("\n".join(out), pause=False) + if config.quiet and not (renew_failures or parse_failures): + return + print("\n".join(out)) def renew_all_lineages(config): From 6a11e171e8539aaec8abfb5926c8156c2cc35348 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 2 Apr 2016 12:48:55 -0700 Subject: [PATCH 10/15] lintmonster --- letsencrypt/renewal.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/letsencrypt/renewal.py b/letsencrypt/renewal.py index 55a7d8262..c65c59822 100644 --- a/letsencrypt/renewal.py +++ b/letsencrypt/renewal.py @@ -4,7 +4,6 @@ import copy import glob import logging import os -import sys import traceback import six @@ -21,7 +20,6 @@ from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import hooks from letsencrypt import storage -from letsencrypt.display import util as display_util from letsencrypt.plugins import disco as plugins_disco logger = logging.getLogger(__name__) From 59d7c44bd6f419afaeb83a16341b027913e68fbf Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 4 Apr 2016 10:38:49 -0700 Subject: [PATCH 11/15] Avoid all terminal codes in --quiet mode --- letsencrypt/reporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 178747121..03f5ca200 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -102,5 +102,5 @@ class Reporter(object): if len(lines) > 1: print("\n".join( next_wrapper.fill(line) for line in lines[1:])) - if bold_on: + if bold_on and not self.config.quiet: sys.stdout.write(le_util.ANSI_SGR_RESET) From 12006fb5f51d37124fea101670f95dfe2969296e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 4 Apr 2016 10:53:11 -0700 Subject: [PATCH 12/15] cli_test: _call() now allows stdout content inspection --- letsencrypt/tests/cli_test.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 7819eff66..413e0965c 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -65,10 +65,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods def _call_no_clientmock(self, args): "Run the client with output streams mocked out" args = self.standard_args + args - with mock.patch('letsencrypt.main.sys.stdout') as stdout: + + toy_stdout = six.StringIO() + with mock.patch('letsencrypt.main.sys.stdout', new=toy_stdout): with mock.patch('letsencrypt.main.sys.stderr') as stderr: ret = main.main(args[:]) # NOTE: parser can alter its args! - return ret, stdout, stderr + return ret, toy_stdout, stderr def _call_stdout(self, args): """ @@ -283,7 +285,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods plugins.visible.assert_called_once_with() plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() - stdout.write.called_once_with(str(filtered)) + #stdout.write.called_once_with(str(filtered)) + self.assertEqual(stdout.getvalue().strip(), str(filtered)) @mock.patch('letsencrypt.main.plugins_disco') @mock.patch('letsencrypt.main.cli.HelpfulArgumentParser.determine_help_topics') @@ -298,7 +301,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertEqual(filtered.init.call_count, 1) filtered.verify.assert_called_once_with(ifaces) verified = filtered.verify() - stdout.write.called_once_with(str(verified)) + self.assertEqual(stdout.getvalue().strip(), str(verified)) + #stdout.write.called_once_with(str(verified)) @mock.patch('letsencrypt.main.plugins_disco') @mock.patch('letsencrypt.main.cli.HelpfulArgumentParser.determine_help_topics') @@ -315,7 +319,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods verified.prepare.assert_called_once_with() verified.available.assert_called_once_with() available = verified.available() - stdout.write.called_once_with(str(available)) + self.assertEqual(stdout.getvalue().strip(), str(available)) + #stdout.write.called_once_with(str(available)) def test_certonly_abspath(self): cert = 'cert' From 13e0fbb1f1b2281e0fa91dfc06788b372977765f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 4 Apr 2016 11:06:37 -0700 Subject: [PATCH 13/15] _call can now handle the _call_stdout case --- letsencrypt/tests/cli_test.py | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 413e0965c..5cb6c909c 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -56,33 +56,22 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods # pylint: disable=protected-access cli._parser = cli.set_by_cli.detector = None - def _call(self, args): + def _call(self, args, stdout=None): "Run the cli with output streams and actual client mocked out" with mock.patch('letsencrypt.main.client') as client: - ret, stdout, stderr = self._call_no_clientmock(args) + ret, stdout, stderr = self._call_no_clientmock(args, stdout) return ret, stdout, stderr, client - def _call_no_clientmock(self, args): + def _call_no_clientmock(self, args, stdout=None): "Run the client with output streams mocked out" args = self.standard_args + args - toy_stdout = six.StringIO() + toy_stdout = stdout if stdout else six.StringIO() with mock.patch('letsencrypt.main.sys.stdout', new=toy_stdout): with mock.patch('letsencrypt.main.sys.stderr') as stderr: ret = main.main(args[:]) # NOTE: parser can alter its args! return ret, toy_stdout, stderr - def _call_stdout(self, args): - """ - Variant of _call that preserves stdout so that it can be mocked by the - caller. - """ - args = self.standard_args + args - with mock.patch('letsencrypt.main.sys.stderr') as stderr: - with mock.patch('letsencrypt.main.client') as client: - ret = main.main(args[:]) # NOTE: parser can alter its args! - return ret, None, stderr, client - def test_no_flags(self): with mock.patch('letsencrypt.main.run') as mock_run: self._call([]) @@ -92,10 +81,9 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods "Run a command, and return the ouput string for scrutiny" output = six.StringIO() - with mock.patch('letsencrypt.main.sys.stdout', new=output): - self.assertRaises(SystemExit, self._call_stdout, args) - out = output.getvalue() - return out + self.assertRaises(SystemExit, self._call, args, output) + out = output.getvalue() + return out def test_help(self): self.assertRaises(SystemExit, self._call, ['--help']) @@ -285,7 +273,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods plugins.visible.assert_called_once_with() plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() - #stdout.write.called_once_with(str(filtered)) self.assertEqual(stdout.getvalue().strip(), str(filtered)) @mock.patch('letsencrypt.main.plugins_disco') @@ -302,7 +289,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods filtered.verify.assert_called_once_with(ifaces) verified = filtered.verify() self.assertEqual(stdout.getvalue().strip(), str(verified)) - #stdout.write.called_once_with(str(verified)) @mock.patch('letsencrypt.main.plugins_disco') @mock.patch('letsencrypt.main.cli.HelpfulArgumentParser.determine_help_topics') @@ -320,7 +306,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods verified.available.assert_called_once_with() available = verified.available() self.assertEqual(stdout.getvalue().strip(), str(available)) - #stdout.write.called_once_with(str(available)) def test_certonly_abspath(self): cert = 'cert' From f8867ab357666a8b6ba486198d3ead862fabab69 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 4 Apr 2016 11:14:04 -0700 Subject: [PATCH 14/15] Test for --quiet renew --- letsencrypt/tests/cli_test.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 5cb6c909c..7f88db3bd 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -560,6 +560,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods mock_certr = mock.MagicMock() mock_key = mock.MagicMock(pem='pem_key') mock_client = mock.MagicMock() + stdout = None mock_client.obtain_certificate.return_value = (mock_certr, 'chain', mock_key, 'csr') try: @@ -579,7 +580,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods if extra_args: args += extra_args try: - ret, _, _, _ = self._call(args) + ret, stdout, _, _ = self._call(args) if ret: print("Returned", ret) raise AssertionError(ret) @@ -602,10 +603,10 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: self.assertTrue(log_out in lf.read()) - return mock_lineage, mock_get_utility + return mock_lineage, mock_get_utility, stdout def test_certonly_renewal(self): - lineage, get_utility = self._test_renewal_common(True, []) + 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()) @@ -615,17 +616,18 @@ 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'], - log_out="simulating renewal") + _, get_utility, _ = self._test_renewal_common(False, ['--dry-run', '--keep'], + 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'], - log_out="Auto-renewal forced") + self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'], + log_out="Auto-renewal forced") self.assertEqual(get_utility().add_message.call_count, 1) - _, _ = self._test_renewal_common(False, ['-tvv', '--debug', '--keep'], - log_out="not yet due", should_renew=False) + self._test_renewal_common(False, ['-tvv', '--debug', '--keep'], + log_out="not yet due", should_renew=False) + def _dump_log(self): with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: @@ -650,6 +652,19 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(True, [], args=args, should_renew=True) + def test_quiet_renew(self): + self._make_test_renewal_conf('sample-renewal.conf') + args = ["renew", "--dry-run"] + _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True) + out = stdout.getvalue() + self.assertTrue("renew" in out) + + args = ["renew", "--dry-run", "-q"] + _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True) + out = stdout.getvalue() + self.assertEqual("", out) + + @mock.patch("letsencrypt.cli.set_by_cli") def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): mock_set_by_cli.return_value = False From b567a542064d5036f437f1a65b6402a334bc306a Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 4 Apr 2016 11:16:37 -0700 Subject: [PATCH 15/15] Really never echo terminal codes in quiet mode --- letsencrypt/reporter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 03f5ca200..f3ab93763 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -95,8 +95,9 @@ class Reporter(object): continue if no_exception or msg.on_crash: if bold_on and msg.priority > self.HIGH_PRIORITY: - sys.stdout.write(le_util.ANSI_SGR_RESET) - bold_on = False + if not self.config.quiet: + sys.stdout.write(le_util.ANSI_SGR_RESET) + bold_on = False lines = msg.text.splitlines() print(first_wrapper.fill(lines[0])) if len(lines) > 1: