mirror of
https://github.com/certbot/certbot.git
synced 2026-03-23 10:53:11 -04:00
Merge pull request #2741 from letsencrypt/reset_by_cli
Fix plugin default detection
This commit is contained in:
commit
b6a4eec54f
4 changed files with 191 additions and 90 deletions
|
|
@ -87,6 +87,48 @@ More detailed help:
|
|||
"""
|
||||
|
||||
|
||||
# These argparse parameters should be removed when detecting defaults.
|
||||
ARGPARSE_PARAMS_TO_REMOVE = ("const", "nargs", "type",)
|
||||
|
||||
|
||||
# These sets are used when to help detect options set by the user.
|
||||
EXIT_ACTIONS = set(("help", "version",))
|
||||
|
||||
|
||||
ZERO_ARG_ACTIONS = set(("store_const", "store_true",
|
||||
"store_false", "append_const", "count",))
|
||||
|
||||
|
||||
# Maps a config option to a set of config options that may have modified it.
|
||||
# This dictionary is used recursively, so if A modifies B and B modifies C,
|
||||
# it is determined that C was modified by the user if A was modified.
|
||||
VAR_MODIFIERS = {"account": set(("server",)),
|
||||
"server": set(("dry_run", "staging",)),
|
||||
"webroot_map": set(("webroot_path",))}
|
||||
|
||||
|
||||
def report_config_interaction(modified, modifiers):
|
||||
"""Registers config option interaction to be checked by set_by_cli.
|
||||
|
||||
This function can be called by during the __init__ or
|
||||
add_parser_arguments methods of plugins to register interactions
|
||||
between config options.
|
||||
|
||||
:param modified: config options that can be modified by modifiers
|
||||
:type modified: iterable or str
|
||||
:param modifiers: config options that modify modified
|
||||
:type modifiers: iterable or str
|
||||
|
||||
"""
|
||||
if isinstance(modified, str):
|
||||
modified = (modified,)
|
||||
if isinstance(modifiers, str):
|
||||
modifiers = (modifiers,)
|
||||
|
||||
for var in modified:
|
||||
VAR_MODIFIERS.setdefault(var, set()).update(modifiers)
|
||||
|
||||
|
||||
def usage_strings(plugins):
|
||||
"""Make usage strings late so that plugins can be initialised late"""
|
||||
if "nginx" in plugins:
|
||||
|
|
@ -100,6 +142,22 @@ def usage_strings(plugins):
|
|||
return USAGE % (apache_doc, nginx_doc), SHORT_USAGE
|
||||
|
||||
|
||||
class _Default(object):
|
||||
"""A class to use as a default to detect if a value is set by a user"""
|
||||
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, _Default)
|
||||
|
||||
def __hash__(self):
|
||||
return id(_Default)
|
||||
|
||||
def __nonzero__(self):
|
||||
return self.__bool__()
|
||||
|
||||
|
||||
def set_by_cli(var):
|
||||
"""
|
||||
Return True if a particular config variable has been set by the user
|
||||
|
|
@ -116,30 +174,18 @@ def set_by_cli(var):
|
|||
detector = set_by_cli.detector = prepare_and_parse_args(
|
||||
plugins, reconstructed_args, detect_defaults=True)
|
||||
# propagate plugin requests: eg --standalone modifies config.authenticator
|
||||
auth, inst = plugin_selection.cli_plugin_requests(detector)
|
||||
detector.authenticator = auth if auth else ""
|
||||
detector.installer = inst if inst else ""
|
||||
detector.authenticator, detector.installer = (
|
||||
plugin_selection.cli_plugin_requests(detector))
|
||||
logger.debug("Default Detector is %r", detector)
|
||||
|
||||
try:
|
||||
# Is detector.var something that isn't false?
|
||||
change_detected = getattr(detector, var)
|
||||
except AttributeError:
|
||||
logger.warning("Missing default analysis for %r", var)
|
||||
return False
|
||||
if not isinstance(getattr(detector, var), _Default):
|
||||
return True
|
||||
|
||||
if change_detected:
|
||||
return True
|
||||
# Special case: we actually want account to be set to "" if the server
|
||||
# the account was on has changed
|
||||
elif var == "account" and (detector.server or detector.dry_run or detector.staging):
|
||||
return True
|
||||
# Special case: vars like --no-redirect that get set True -> False
|
||||
# default to None; False means they were set
|
||||
elif var in detector.store_false_vars and change_detected is not None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
for modifier in VAR_MODIFIERS.get(var, []):
|
||||
if set_by_cli(modifier):
|
||||
return True
|
||||
|
||||
return False
|
||||
# static housekeeping var
|
||||
set_by_cli.detector = None
|
||||
|
||||
|
|
@ -188,21 +234,23 @@ def config_help(name, hidden=False):
|
|||
return interfaces.IConfig[name].__doc__
|
||||
|
||||
|
||||
class SilentParser(object): # pylint: disable=too-few-public-methods
|
||||
"""Silent wrapper around argparse.
|
||||
class HelpfulArgumentGroup(object):
|
||||
"""Emulates an argparse group for use with HelpfulArgumentParser.
|
||||
|
||||
A mini parser wrapper that doesn't print help for its
|
||||
arguments. This is needed for the use of callbacks to define
|
||||
arguments within plugins.
|
||||
This class is used in the add_group method of HelpfulArgumentParser.
|
||||
Command line arguments can be added to the group, but help
|
||||
suppression and default detection is applied by
|
||||
HelpfulArgumentParser when necessary.
|
||||
|
||||
"""
|
||||
def __init__(self, parser):
|
||||
self.parser = parser
|
||||
def __init__(self, helpful_arg_parser, topic):
|
||||
self._parser = helpful_arg_parser
|
||||
self._topic = topic
|
||||
|
||||
def add_argument(self, *args, **kwargs):
|
||||
"""Wrap, but silence help"""
|
||||
kwargs["help"] = argparse.SUPPRESS
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
"""Add a new command line argument to the argument group."""
|
||||
self._parser.add(self._topic, *args, **kwargs)
|
||||
|
||||
|
||||
class HelpfulArgumentParser(object):
|
||||
"""Argparse Wrapper.
|
||||
|
|
@ -235,15 +283,8 @@ class HelpfulArgumentParser(object):
|
|||
|
||||
# This is the only way to turn off overly verbose config flag documentation
|
||||
self.parser._add_config_file_help = False # pylint: disable=protected-access
|
||||
self.silent_parser = SilentParser(self.parser)
|
||||
|
||||
# This setting attempts to force all default values to things that are
|
||||
# pythonically false; it is used to detect when values have been
|
||||
# explicitly set by the user, including when they are set to their
|
||||
# normal default value
|
||||
self.detect_defaults = detect_defaults
|
||||
if detect_defaults:
|
||||
self.store_false_vars = {} # vars that use "store_false"
|
||||
|
||||
self.args = args
|
||||
self.determine_verb()
|
||||
|
|
@ -269,6 +310,9 @@ class HelpfulArgumentParser(object):
|
|||
parsed_args.func = self.VERBS[self.verb]
|
||||
parsed_args.verb = self.verb
|
||||
|
||||
if self.detect_defaults:
|
||||
return parsed_args
|
||||
|
||||
# Do any post-parsing homework here
|
||||
|
||||
# we get domains from -d, but also from the webroot map...
|
||||
|
|
@ -304,9 +348,6 @@ class HelpfulArgumentParser(object):
|
|||
"cannot be used with --csr")
|
||||
self.handle_csr(parsed_args)
|
||||
|
||||
if self.detect_defaults: # plumbing
|
||||
parsed_args.store_false_vars = self.store_false_vars
|
||||
|
||||
hooks.validate_hooks(parsed_args)
|
||||
|
||||
return parsed_args
|
||||
|
|
@ -415,7 +456,7 @@ class HelpfulArgumentParser(object):
|
|||
"""
|
||||
|
||||
if self.detect_defaults:
|
||||
kwargs = self.modify_arg_for_default_detection(self, *args, **kwargs)
|
||||
kwargs = self.modify_kwargs_for_default_detection(**kwargs)
|
||||
|
||||
if self.visible_topics[topic]:
|
||||
if topic in self.groups:
|
||||
|
|
@ -427,39 +468,28 @@ class HelpfulArgumentParser(object):
|
|||
kwargs["help"] = argparse.SUPPRESS
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
|
||||
def modify_kwargs_for_default_detection(self, **kwargs):
|
||||
"""Modify an arg so we can check if it was set by the user.
|
||||
|
||||
def modify_arg_for_default_detection(self, *args, **kwargs):
|
||||
"""
|
||||
Adding an arg, but ensure that it has a default that evaluates to false,
|
||||
so that set_by_cli can tell if it was set. Only called if detect_defaults==True.
|
||||
Changes the parameters given to argparse when adding an argument
|
||||
so we can properly detect if the value was set by the user.
|
||||
|
||||
:param list *args: the names of this argument flag
|
||||
:param dict **kwargs: various argparse settings for this argument
|
||||
:param dict kwargs: various argparse settings for this argument
|
||||
|
||||
:returns: a modified versions of kwargs
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
# argument either doesn't have a default, or the default doesn't
|
||||
# isn't Pythonically false
|
||||
if kwargs.get("default", True):
|
||||
arg_type = kwargs.get("type", None)
|
||||
if arg_type == int or kwargs.get("action", "") == "count":
|
||||
kwargs["default"] = 0
|
||||
elif arg_type == read_file or "-c" in args:
|
||||
kwargs["default"] = ""
|
||||
kwargs["type"] = str
|
||||
else:
|
||||
kwargs["default"] = ""
|
||||
# This doesn't matter at present (none of the store_false args
|
||||
# are renewal-relevant), but implement it for future sanity:
|
||||
# detect the setting of args whose presence causes True -> False
|
||||
if kwargs.get("action", "") == "store_false":
|
||||
kwargs["default"] = None
|
||||
for var in args:
|
||||
self.store_false_vars[var] = True
|
||||
action = kwargs.get("action", None)
|
||||
if action not in EXIT_ACTIONS:
|
||||
kwargs["action"] = ("store_true" if action in ZERO_ARG_ACTIONS else
|
||||
"store")
|
||||
kwargs["default"] = _Default()
|
||||
for param in ARGPARSE_PARAMS_TO_REMOVE:
|
||||
kwargs.pop(param, None)
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def add_deprecated_argument(self, argument_name, num_args):
|
||||
"""Adds a deprecated argument with the name argument_name.
|
||||
|
||||
|
|
@ -475,22 +505,22 @@ class HelpfulArgumentParser(object):
|
|||
self.parser.add_argument, argument_name, num_args)
|
||||
|
||||
def add_group(self, topic, **kwargs):
|
||||
"""
|
||||
"""Create a new argument group.
|
||||
|
||||
This has to be called once for every topic; but we leave those calls
|
||||
next to the argument definitions for clarity. Return something
|
||||
arguments can be added to if necessary, either the parser or an argument
|
||||
group.
|
||||
This method must be called once for every topic, however, calls
|
||||
to this function are left next to the argument definitions for
|
||||
clarity.
|
||||
|
||||
:param str topic: Name of the new argument group.
|
||||
|
||||
:returns: The new argument group.
|
||||
:rtype: `HelpfulArgumentGroup`
|
||||
|
||||
"""
|
||||
if self.visible_topics[topic]:
|
||||
#print("Adding visible group " + topic)
|
||||
group = self.parser.add_argument_group(topic, **kwargs)
|
||||
self.groups[topic] = group
|
||||
return group
|
||||
else:
|
||||
#print("Invisible group " + topic)
|
||||
return self.silent_parser
|
||||
self.groups[topic] = self.parser.add_argument_group(topic, **kwargs)
|
||||
|
||||
return HelpfulArgumentGroup(self, topic)
|
||||
|
||||
def add_plugin_args(self, plugins):
|
||||
"""
|
||||
|
|
@ -501,7 +531,6 @@ class HelpfulArgumentParser(object):
|
|||
"""
|
||||
for name, plugin_ep in six.iteritems(plugins):
|
||||
parser_or_group = self.add_group(name, description=plugin_ep.description)
|
||||
#print(parser_or_group)
|
||||
plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name)
|
||||
|
||||
def determine_help_topics(self, chosen_topic):
|
||||
|
|
|
|||
|
|
@ -45,15 +45,14 @@ class Plugin(object):
|
|||
def add_parser_arguments(cls, add):
|
||||
"""Add plugin arguments to the CLI argument parser.
|
||||
|
||||
NOTE: If some of your flags interact with others, you can
|
||||
use cli.report_config_interaction to register this to ensure
|
||||
values are correctly saved/overridable during renewal.
|
||||
|
||||
:param callable add: Function that proxies calls to
|
||||
`argparse.ArgumentParser.add_argument` prepending options
|
||||
with unique plugin name prefix.
|
||||
|
||||
NOTE: if you add argpase arguments such that users setting them can
|
||||
create a config entry that python's bool() would consider false (ie,
|
||||
the use might set the variable to "", [], 0, etc), please ensure that
|
||||
cli.set_by_cli() works for your variable.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -102,16 +102,14 @@ def _restore_webroot_config(config, renewalparams):
|
|||
form.
|
||||
"""
|
||||
if "webroot_map" in renewalparams:
|
||||
# if the user does anything that would create a new webroot map on the
|
||||
# CLI, don't use the old one
|
||||
if not (cli.set_by_cli("webroot_map") or cli.set_by_cli("webroot_path")):
|
||||
setattr(config.namespace, "webroot_map", renewalparams["webroot_map"])
|
||||
if not cli.set_by_cli("webroot_map"):
|
||||
config.namespace.webroot_map = renewalparams["webroot_map"]
|
||||
elif "webroot_path" in renewalparams:
|
||||
logger.info("Ancient renewal conf file without webroot-map, restoring webroot-path")
|
||||
wp = renewalparams["webroot_path"]
|
||||
if isinstance(wp, str): # prior to 0.1.0, webroot_path was a string
|
||||
wp = [wp]
|
||||
setattr(config.namespace, "webroot_path", wp)
|
||||
config.namespace.webroot_path = wp
|
||||
|
||||
|
||||
def _restore_plugin_configs(config, renewalparams):
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import unittest
|
|||
|
||||
import mock
|
||||
import six
|
||||
from six.moves import reload_module # pylint: disable=import-error
|
||||
|
||||
from acme import jose
|
||||
|
||||
|
|
@ -1023,5 +1024,79 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest):
|
|||
self.assertEqual(result, (None, None))
|
||||
|
||||
|
||||
class DefaultTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.cli._Default."""
|
||||
|
||||
def setUp(self):
|
||||
# pylint: disable=protected-access
|
||||
self.default1 = cli._Default()
|
||||
self.default2 = cli._Default()
|
||||
|
||||
def test_boolean(self):
|
||||
self.assertFalse(self.default1)
|
||||
self.assertFalse(self.default2)
|
||||
|
||||
def test_equality(self):
|
||||
self.assertEqual(self.default1, self.default2)
|
||||
|
||||
def test_hash(self):
|
||||
self.assertEqual(hash(self.default1), hash(self.default2))
|
||||
|
||||
|
||||
class SetByCliTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.set_by_cli and related functions."""
|
||||
|
||||
def setUp(self):
|
||||
reload_module(cli)
|
||||
|
||||
def test_webroot_map(self):
|
||||
args = '-w /var/www/html -d example.com'.split()
|
||||
verb = 'renew'
|
||||
self.assertTrue(_call_set_by_cli('webroot_map', args, verb))
|
||||
|
||||
def test_report_config_interaction_str(self):
|
||||
cli.report_config_interaction('manual_public_ip_logging_ok',
|
||||
'manual_test_mode')
|
||||
cli.report_config_interaction('manual_test_mode', 'manual')
|
||||
|
||||
self._test_report_config_interaction_common()
|
||||
|
||||
def test_report_config_interaction_iterable(self):
|
||||
cli.report_config_interaction(('manual_public_ip_logging_ok',),
|
||||
('manual_test_mode',))
|
||||
cli.report_config_interaction(('manual_test_mode',), ('manual',))
|
||||
|
||||
self._test_report_config_interaction_common()
|
||||
|
||||
def _test_report_config_interaction_common(self):
|
||||
"""Tests implied interaction between manual flags.
|
||||
|
||||
--manual implies --manual-test-mode which implies
|
||||
--manual-public-ip-logging-ok. These interactions don't actually
|
||||
exist in the client, but are used here for testing purposes.
|
||||
|
||||
"""
|
||||
|
||||
args = ['--manual']
|
||||
verb = 'renew'
|
||||
for v in ('manual', 'manual_test_mode', 'manual_public_ip_logging_ok'):
|
||||
self.assertTrue(_call_set_by_cli(v, args, verb))
|
||||
|
||||
cli.set_by_cli.detector = None
|
||||
|
||||
args = ['--manual-test-mode']
|
||||
for v in ('manual_test_mode', 'manual_public_ip_logging_ok'):
|
||||
self.assertTrue(_call_set_by_cli(v, args, verb))
|
||||
|
||||
self.assertFalse(_call_set_by_cli('manual', args, verb))
|
||||
|
||||
|
||||
def _call_set_by_cli(var, args, verb):
|
||||
with mock.patch('letsencrypt.cli.helpful_parser') as mock_parser:
|
||||
mock_parser.args = args
|
||||
mock_parser.verb = verb
|
||||
return cli.set_by_cli(var)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
Loading…
Reference in a new issue