diff options
Diffstat (limited to 'src/lib/Bcfg2/Options/Parser.py')
-rw-r--r-- | src/lib/Bcfg2/Options/Parser.py | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/src/lib/Bcfg2/Options/Parser.py b/src/lib/Bcfg2/Options/Parser.py new file mode 100644 index 000000000..677a69e4c --- /dev/null +++ b/src/lib/Bcfg2/Options/Parser.py @@ -0,0 +1,352 @@ +""" The option parser """ + +import os +import sys +import argparse +from Bcfg2.version import __version__ +from Bcfg2.Compat import ConfigParser +from Bcfg2.Options import Option, PathOption, BooleanOption, _debug + +__all__ = ["setup", "OptionParserException", "Parser", "get_parser"] + + +#: The repository option. This is specified here (and imported into +#: :module:`Bcfg2.Options.Common`) rather than vice-versa due to +#: circular imports. +repository = PathOption( # pylint: disable=C0103 + '-Q', '--repository', cf=('server', 'repository'), + default='var/lib/bcfg2', help="Server repository path") + + +#: A module-level :class:`argparse.Namespace` object that stores all +#: configuration for Bcfg2. +setup = argparse.Namespace(version=__version__, # pylint: disable=C0103 + name="Bcfg2", + uri='http://trac.mcs.anl.gov/projects/bcfg2') + + +class OptionParserException(Exception): + """ Base exception raised for generic option parser errors """ + + +class Parser(argparse.ArgumentParser): + """ The Bcfg2 option parser. Most interfaces should not need to + instantiate a parser, but should instead use + :func:`Bcfg2.Options.get_parser` to get the parser that already + exists.""" + + #: 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") + + #: Builtin options that apply to all commands + options = [configfile, + BooleanOption('--version', help="Print the version and exit"), + Option('-E', '--encoding', metavar='<encoding>', + default='UTF-8', help="Encoding of config files", + cf=('components', 'encoding'))] + + #: Flag used in unit tests to disable actual config file reads + unit_test = False + + def __init__(self, **kwargs): + """ See :class:`argparse.ArgumentParser` for a full list of + accepted parameters. + + In addition to supporting all arguments and keyword arguments + from :class:`argparse.ArgumentParser`, several additional + keyword arguments are allowed. + + :param components: A list of components to add to the parser. + :type components: list + :param namespace: The namespace to store options in. Default + is :attr:`Bcfg2.Options.setup`. + :type namespace: argparse.Namespace + :param add_base_options: Whether or not to add the options in + :attr:`Bcfg2.Options.Parser.options` + to the parser. Setting this to False + is default for subparsers. Default is + True. + :type add_base_options: bool + """ + self._cfp = ConfigParser.ConfigParser() + components = kwargs.pop('components', []) + + #: The namespace options will be stored in. + self.namespace = kwargs.pop('namespace', setup) + if self.namespace is None: + self.namespace = setup + add_base_options = kwargs.pop('add_base_options', True) + + #: Flag to indicate that this is the pre-parsing 'early' run + #: for important options like database settings that must be + #: loaded before other components can be. + self._early = kwargs.pop('early', False) + + if 'add_help' not in kwargs: + kwargs['add_help'] = add_base_options + argparse.ArgumentParser.__init__(self, **kwargs) + + #: Whether or not parsing has completed on all current options. + self.parsed = False + + #: The argument list that was parsed. + self.argv = None + + #: Components that have been added to the parser + self.components = [] + + #: Options that have been added to the parser + self.option_list = [] + self._defaults_set = [] + self._config_files = [] + if add_base_options: + self.add_component(self) + if components: + for component in components: + self.add_component(component) + + def add_options(self, options): + """ Add an explicit list of options to the parser. When + possible, prefer :func:`Bcfg2.Options.Parser.add_component` to + add a whole component instead.""" + self.parsed = False + for option in options: + if option not in self.option_list: + self.option_list.extend(option.list_options()) + option.add_to_parser(self) + + def add_component(self, component): + """ 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_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) + self._defaults_set.append(opt) + + def _parse_config_options(self): + """ 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 _, 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) + + def _finalize(self): + """ Finalize the value of any options that require that + additional post-processing step. (Mostly + :class:`Bcfg2.Options.Actions.ComponentAction` subclasses.) + """ + _debug("Finalizing options") + for opt in self.option_list[:]: + opt.finalize(self.namespace) + + def _reset_namespace(self): + """ 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_from_config() + if reparse: + self._parse_config_options() + self._config_files.append(dest) + + def reparse(self, argv=None): + """ Reparse options after they have already been parsed. + + :param argv: The argument list to parse. By default, + :attr:`Bcfg2.Options.Parser.argv` is reused. + (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) + + def parse(self, argv=None): + """ Parse options. + + :param argv: The argument list to parse. By default, + ``sys.argv[1:]`` is used. This is stored in + :attr:`Bcfg2.Options.Parser.argv` for reuse by + :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 + if not self.unit_test and not os.path.exists(bootstrap.config): + self.error("Could not read %s" % bootstrap.config) + self.add_config_file(self.configfile.dest, bootstrap.config, + reparse=False) + + # 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, + early=True) + # add the repo option so we can resolve <repository> + # macros + early_parser.add_options([repository]) + early_components = [] + for component in self.components: + if getattr(component, "parse_first", False): + 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_from_config() + self.parse_known_args(args=self.argv, namespace=self.namespace) + self._parse_config_options() + self._finalize() + + # 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") 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 + + +#: A module-level :class:`Bcfg2.Options.Parser` object that is used +#: for all parsing +_parser = Parser() # pylint: disable=C0103 + + +def get_parser(description=None, components=None, namespace=None): + """ Get an existing :class:`Bcfg2.Options.Parser` object. (One is + created at the module level when :mod:`Bcfg2.Options` is + imported.) If no arguments are given, then the existing parser is + simply fetched. + + If arguments are given, then one of two things happens: + + * If this is the first ``get_parser`` call with arguments, then + the values given are set accordingly in the parser, and it is + returned. + * If this is not the first such call, then + :class:`Bcfg2.Options.OptionParserException` is raised. + + That is, a ``get_parser`` call with options is considered to + initialize the parser that already exists, and that can only + happen once. + + :param description: Set the parser description + :type description: string + :param components: Load the given components in the parser + :type components: list + :param namespace: Use the given namespace instead of + :attr:`Bcfg2.Options.setup` + :type namespace: argparse.Namespace + :returns: Bcfg2.Options.Parser object + """ + if Parser.unit_test: + return Parser(description=description, components=components, + namespace=namespace) + elif (description or components or namespace): + if description: + _parser.description = description + if components is not None: + for component in components: + _parser.add_component(component) + if namespace: + _parser.namespace = namespace + return _parser |