summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Options
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Options')
-rw-r--r--src/lib/Bcfg2/Options/Actions.py45
-rw-r--r--src/lib/Bcfg2/Options/Common.py18
-rw-r--r--src/lib/Bcfg2/Options/Options.py82
-rw-r--r--src/lib/Bcfg2/Options/Parser.py62
-rw-r--r--src/lib/Bcfg2/Options/Types.py9
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)