diff options
Diffstat (limited to 'src/lib/Bcfg2/Options')
-rw-r--r-- | src/lib/Bcfg2/Options/Actions.py | 45 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/Common.py | 18 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/Options.py | 82 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/Parser.py | 62 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options/Types.py | 9 |
5 files changed, 159 insertions, 57 deletions
diff --git a/src/lib/Bcfg2/Options/Actions.py b/src/lib/Bcfg2/Options/Actions.py index 8b97f1da8..8b941f2bb 100644 --- a/src/lib/Bcfg2/Options/Actions.py +++ b/src/lib/Bcfg2/Options/Actions.py @@ -7,7 +7,27 @@ from Bcfg2.Options.Parser import get_parser __all__ = ["ConfigFileAction", "ComponentAction", "PluginsAction"] -class ComponentAction(argparse.Action): +class FinalizableAction(argparse.Action): + """ A FinalizableAction requires some additional action to be taken + when storing the value, and as a result must be finalized if the + default value is used.""" + + def __init__(self, *args, **kwargs): + argparse.Action.__init__(self, *args, **kwargs) + self._final = False + + def finalize(self, parser, namespace): + """ Finalize a default value by calling the action callable. """ + if not self._final: + self.__call__(parser, namespace, getattr(namespace, self.dest, + self.default)) + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, values) + self._final = True + + +class ComponentAction(FinalizableAction): """ ComponentAction automatically imports classes and modules based on the value of the option, and automatically collects options from the loaded classes and modules. It cannot be used by @@ -84,8 +104,7 @@ class ComponentAction(argparse.Action): if self.mapping: if 'choices' not in kwargs: kwargs['choices'] = self.mapping.keys() - self._final = False - argparse.Action.__init__(self, *args, **kwargs) + FinalizableAction.__init__(self, *args, **kwargs) def _import(self, module, name): """ Import the given name from the given module, handling @@ -123,17 +142,10 @@ class ComponentAction(argparse.Action): break if cls: get_parser().add_component(cls) - else: + elif not self.fail_silently: print("Could not load component %s" % name) return cls - def finalize(self, parser, namespace): - """ Finalize a default value by loading the components given - in it. This lets a default be specified with a list of - strings instead of a list of classes. """ - if not self._final: - self.__call__(parser, namespace, self.default) - def __call__(self, parser, namespace, values, option_string=None): if values is None: result = None @@ -146,18 +158,19 @@ class ComponentAction(argparse.Action): result.append(cls) else: result = self._load_component(values) - self._final = True - setattr(namespace, self.dest, result) + FinalizableAction.__call__(self, parser, namespace, result, + option_string=option_string) -class ConfigFileAction(argparse.Action): +class ConfigFileAction(FinalizableAction): """ ConfigFileAction automatically loads and parses a supplementary config file (e.g., ``bcfg2-web.conf`` or ``bcfg2-lint.conf``). """ def __call__(self, parser, namespace, values, option_string=None): - get_parser().add_config_file(self.dest, values) - setattr(namespace, self.dest, values) + parser.add_config_file(self.dest, values, reparse=False) + FinalizableAction.__call__(self, parser, namespace, values, + option_string=option_string) class PluginsAction(ComponentAction): diff --git a/src/lib/Bcfg2/Options/Common.py b/src/lib/Bcfg2/Options/Common.py index 9ba08eb87..620a7604c 100644 --- a/src/lib/Bcfg2/Options/Common.py +++ b/src/lib/Bcfg2/Options/Common.py @@ -94,7 +94,7 @@ class Common(object): #: Log to syslog syslog = BooleanOption( - cf=('logging', 'syslog'), help="Log to syslog") + cf=('logging', 'syslog'), help="Log to syslog", default=True) #: Server location location = Option( @@ -107,20 +107,16 @@ class Common(object): '-x', '--password', cf=('communication', 'password'), metavar='<password>', help="Communication Password") - #: Path to SSL key - ssl_key = PathOption( - '--ssl-key', cf=('communication', 'key'), dest="key", - help='Path to SSL key', default="/etc/pki/tls/private/bcfg2.key") - - #: Path to SSL certificate - ssl_cert = PathOption( - cf=('communication', 'certificate'), dest="cert", - help='Path to SSL certificate', default="/etc/pki/tls/certs/bcfg2.crt") - #: Path to SSL CA certificate ssl_ca = PathOption( cf=('communication', 'ca'), help='Path to SSL CA Cert') + #: Communication protocol + protocol = Option( + cf=('communication', 'protocol'), default='xmlrpc/tlsv1', + choices=['xmlrpc/ssl', 'xmlrpc/tlsv1'], + help='Communication protocol to use.') + #: Default Path paranoid setting default_paranoid = Option( cf=('mdata', 'paranoid'), dest="default_paranoid", default='true', diff --git a/src/lib/Bcfg2/Options/Options.py b/src/lib/Bcfg2/Options/Options.py index be7e7c646..3874f810d 100644 --- a/src/lib/Bcfg2/Options/Options.py +++ b/src/lib/Bcfg2/Options/Options.py @@ -10,7 +10,18 @@ from Bcfg2.Options import Types from Bcfg2.Compat import ConfigParser -__all__ = ["Option", "BooleanOption", "PathOption", "PositionalArgument"] +__all__ = ["Option", "BooleanOption", "PathOption", "PositionalArgument", + "_debug"] + + +def _debug(msg): + """ Option parsing happens before verbose/debug have been set -- + they're options, after all -- so option parsing verbosity is + enabled by changing this to True. The verbosity here is primarily + of use to developers. """ + if os.environ.get('BCFG2_OPTIONS_DEBUG', '0') == '1': + print(msg) + #: A dict that records a mapping of argparse action name (e.g., #: "store_true") to the argparse Action class for it. See @@ -158,6 +169,10 @@ class Option(object): the appropriate default value in the appropriate format.""" for parser, action in self.actions.items(): if hasattr(action, "finalize"): + if parser: + _debug("Finalizing %s for %s" % (self, parser)) + else: + _debug("Finalizing %s" % self) action.finalize(parser, namespace) def from_config(self, cfp): @@ -181,23 +196,25 @@ class Option(object): exclude.update(o.cf[1] for o in parser.option_list if o.cf and o.cf[0] == self.cf[0]) - return dict([(o, cfp.get(self.cf[0], o)) - for o in fnmatch.filter(cfp.options(self.cf[0]), - self.cf[1]) - if o not in exclude]) + rv = dict([(o, cfp.get(self.cf[0], o)) + for o in fnmatch.filter(cfp.options(self.cf[0]), + self.cf[1]) + if o not in exclude]) else: - return dict() + rv = dict() else: + if self.type: + rtype = self.type + else: + rtype = lambda x: x try: - val = cfp.getboolean(*self.cf) + rv = rtype(cfp.getboolean(*self.cf)) except ValueError: - val = cfp.get(*self.cf) + rv = rtype(cfp.get(*self.cf)) except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): - return None - if self.type: - return self.type(val) - else: - return val + rv = None + _debug("Setting %s from config file(s): %s" % (self, rv)) + return rv def default_from_config(self, cfp): """ Set the default value of this option from the config file @@ -208,9 +225,13 @@ class Option(object): """ if self.env and self.env in os.environ: self.default = os.environ[self.env] + _debug("Setting the default of %s from environment: %s" % + (self, self.default)) else: val = self.from_config(cfp) if val is not None: + _debug("Setting the default of %s from config: %s" % + (self, val)) self.default = val def _get_default(self): @@ -250,13 +271,17 @@ class Option(object): self.parsers.append(parser) if self.args: # cli option + _debug("Adding %s to %s as a CLI option" % (self, parser)) action = parser.add_argument(*self.args, **self._kwargs) if not self._dest: self._dest = action.dest if self._default: action.default = self._default self.actions[parser] = action - # else, config file-only option + else: + # else, config file-only option + _debug("Adding %s to %s as a config file-only option" % + (self, parser)) class PathOption(Option): @@ -281,6 +306,26 @@ class PathOption(Option): Option.__init__(self, *args, **kwargs) +class _BooleanOptionAction(argparse.Action): + """ BooleanOptionAction sets a boolean value in the following ways: + - if None is passed, store the default + - if the option_string is not None, then the option was passed on the + command line, thus store the opposite of the default (this is the + argparse store_true and store_false behavior) + - if a boolean value is passed, use that + + Defined here instead of :mod:`Bcfg2.Options.Actions` because otherwise + there is a circular import Options -> Actions -> Parser -> Options """ + + def __call__(self, parser, namespace, values, option_string=None): + if values is None: + setattr(namespace, self.dest, self.default) + elif option_string is not None: + setattr(namespace, self.dest, not self.default) + else: + setattr(namespace, self.dest, bool(values)) + + class BooleanOption(Option): """ Shortcut for boolean options. The default is False, but this can easily be overridden: @@ -292,11 +337,10 @@ class BooleanOption(Option): "--dwim", default=True, help="Do What I Mean")] """ def __init__(self, *args, **kwargs): - if 'default' in kwargs and kwargs['default']: - kwargs.setdefault('action', 'store_false') - else: - kwargs.setdefault('action', 'store_true') - kwargs.setdefault('default', False) + kwargs.setdefault('action', _BooleanOptionAction) + kwargs.setdefault('nargs', 0) + kwargs.setdefault('default', False) + Option.__init__(self, *args, **kwargs) diff --git a/src/lib/Bcfg2/Options/Parser.py b/src/lib/Bcfg2/Options/Parser.py index 80f966246..677a69e4c 100644 --- a/src/lib/Bcfg2/Options/Parser.py +++ b/src/lib/Bcfg2/Options/Parser.py @@ -5,7 +5,7 @@ import sys import argparse from Bcfg2.version import __version__ from Bcfg2.Compat import ConfigParser -from Bcfg2.Options import Option, PathOption, BooleanOption +from Bcfg2.Options import Option, PathOption, BooleanOption, _debug __all__ = ["setup", "OptionParserException", "Parser", "get_parser"] @@ -37,6 +37,7 @@ class Parser(argparse.ArgumentParser): #: Option for specifying the path to the Bcfg2 config file configfile = PathOption('-C', '--config', + env="BCFG2_CONFIG_FILE", help="Path to configuration file", default="/etc/bcfg2.conf") @@ -121,14 +122,16 @@ class Parser(argparse.ArgumentParser): """ Add a component (and all of its options) to the parser. """ if component not in self.components: + _debug("Adding component %s to %s" % (component, self)) self.components.append(component) if hasattr(component, "options"): self.add_options(getattr(component, "options")) - def _set_defaults(self): + def _set_defaults_from_config(self): """ Set defaults from the config file for all options that can come from the config file, but haven't yet had their default set """ + _debug("Setting defaults on all options") for opt in self.option_list: if opt not in self._defaults_set: opt.default_from_config(self._cfp) @@ -138,15 +141,15 @@ class Parser(argparse.ArgumentParser): """ populate the namespace with default values for any options that aren't already in the namespace (i.e., options without CLI arguments) """ + _debug("Parsing config file-only options") for opt in self.option_list[:]: if not opt.args and opt.dest not in self.namespace: value = opt.default if value: - for parser, action in opt.actions.items(): - if parser is None: - action(self, self.namespace, value) - else: - action(parser, parser.namespace, value) + for _, action in opt.actions.items(): + _debug("Setting config file-only option %s to %s" % + (opt, value)) + action(self, self.namespace, value) else: setattr(self.namespace, opt.dest, value) @@ -155,6 +158,7 @@ class Parser(argparse.ArgumentParser): additional post-processing step. (Mostly :class:`Bcfg2.Options.Actions.ComponentAction` subclasses.) """ + _debug("Finalizing options") for opt in self.option_list[:]: opt.finalize(self.namespace) @@ -162,20 +166,23 @@ class Parser(argparse.ArgumentParser): """ Delete all options from the namespace except for a few predefined values and config file options. """ self.parsed = False + _debug("Resetting namespace") for attr in dir(self.namespace): if (not attr.startswith("_") and attr not in ['uri', 'version', 'name'] and attr not in self._config_files): + _debug("Deleting %s" % attr) delattr(self.namespace, attr) def add_config_file(self, dest, cfile, reparse=True): """ Add a config file, which triggers a full reparse of all options. """ if dest not in self._config_files: + _debug("Adding new config file %s for %s" % (cfile, dest)) self._reset_namespace() self._cfp.read([cfile]) self._defaults_set = [] - self._set_defaults() + self._set_defaults_from_config() if reparse: self._parse_config_options() self._config_files.append(dest) @@ -188,6 +195,7 @@ class Parser(argparse.ArgumentParser): (I.e., the argument list that was initially parsed.) :type argv: list """ + _debug("Reparsing all options") self._reset_namespace() self.parse(argv or self.argv) @@ -200,15 +208,19 @@ class Parser(argparse.ArgumentParser): :func:`Bcfg2.Options.Parser.reparse`. :type argv: list """ + _debug("Parsing options") if argv is None: argv = sys.argv[1:] if self.parsed and self.argv == argv: + _debug("Returning already parsed namespace") return self.namespace self.argv = argv # phase 1: get and read config file + _debug("Option parsing phase 1: Get and read main config file") bootstrap_parser = argparse.ArgumentParser(add_help=False) self.configfile.add_to_parser(bootstrap_parser) + self.configfile.default_from_config(self._cfp) bootstrap = bootstrap_parser.parse_known_args(args=self.argv)[0] # check whether the specified bcfg2.conf exists @@ -219,6 +231,7 @@ class Parser(argparse.ArgumentParser): # phase 2: re-parse command line for early options; currently, # that's database options + _debug("Option parsing phase 2: Parse early options") if not self._early: early_opts = argparse.Namespace() early_parser = Parser(add_help=False, namespace=early_opts, @@ -232,35 +245,62 @@ class Parser(argparse.ArgumentParser): early_components.append(component) early_parser.add_component(component) early_parser.parse(self.argv) + _debug("Early parsing complete, calling hooks") for component in early_components: if hasattr(component, "component_parsed_hook"): + _debug("Calling component_parsed_hook on %s" % component) getattr(component, "component_parsed_hook")(early_opts) # phase 3: re-parse command line, loading additional # components, until all components have been loaded. On each # iteration, set defaults from config file/environment # variables + _debug("Option parsing phase 3: Main parser loop") + # _set_defaults_from_config must be called before _parse_config_options + # This is due to a tricky interaction between the two methods: + # + # (1) _set_defaults_from_config does what its name implies, it updates + # the "default" property of each Option based on the value that exists + # in the config. + # + # (2) _parse_config_options will look at each option and set it to the + # default value that is _currently_ defined. If the option does not + # exist in the namespace, it will be added. The method carefully + # avoids overwriting the value of an option that is already defined in + # the namespace. + # + # Thus, if _set_defaults_from_config has not been called yet when + # _parse_config_options is called, all config file options will get set + # to their hardcoded defaults. This process defines the options in the + # namespace and _parse_config_options will never look at them again. + self._set_defaults_from_config() self._parse_config_options() while not self.parsed: self.parsed = True - self._set_defaults() + self._set_defaults_from_config() self.parse_known_args(args=self.argv, namespace=self.namespace) self._parse_config_options() self._finalize() - self._parse_config_options() # phase 4: fix up <repository> macros + _debug("Option parsing phase 4: Fix up macros") repo = getattr(self.namespace, "repository", repository.default) for attr in dir(self.namespace): value = getattr(self.namespace, attr) - if not attr.startswith("_") and hasattr(value, "replace"): + if (not attr.startswith("_") and + hasattr(value, "replace") and + "<repository>" in value): setattr(self.namespace, attr, value.replace("<repository>", repo, 1)) + _debug("Fixing up macros in %s: %s -> %s" % + (attr, value, getattr(self.namespace, attr))) # phase 5: call post-parsing hooks + _debug("Option parsing phase 5: Call hooks") if not self._early: for component in self.components: if hasattr(component, "options_parsed_hook"): + _debug("Calling post-parsing hook on %s" % component) getattr(component, "options_parsed_hook")() return self.namespace diff --git a/src/lib/Bcfg2/Options/Types.py b/src/lib/Bcfg2/Options/Types.py index 2f0fd7d52..d11e54fba 100644 --- a/src/lib/Bcfg2/Options/Types.py +++ b/src/lib/Bcfg2/Options/Types.py @@ -50,6 +50,15 @@ def comma_dict(value): return result +def anchored_regex_list(value): + """ Split an option string on whitespace and compile each element as + an anchored regex """ + try: + return [re.compile('^' + x + '$') for x in re.split(r'\s+', value)] + except re.error: + raise ValueError("Not a list of regexes", value) + + def octal(value): """ Given an octal string, get an integer representation. """ return int(value, 8) |