From fabebb356eec47607aae05ea174b0aeef4fef5b1 Mon Sep 17 00:00:00 2001 From: Mark McLoughlin Date: Mon, 28 Nov 2011 14:38:34 +0000 Subject: [PATCH] Add new cfg module As discussed on the mailing list and described here: http://wiki.openstack.org/CommonConfigModule The module implements an API for defining configuration options and reading values for those options that a user may have set in a config file or on the command line. The module will be part of openstack-common and glance will consume it from there once openstack-common makes a release with an API compatibility commitment. Change-Id: Ib604c3de75d9066cd913c2de3007024b1f5ec9f8 --- nova/common/__init__.py | 15 + nova/common/cfg.py | 1126 +++++++++++++++++++++++++++++++++++++++ nova/tests/test_cfg.py | 794 +++++++++++++++++++++++++++ 3 files changed, 1935 insertions(+) create mode 100644 nova/common/__init__.py create mode 100644 nova/common/cfg.py create mode 100644 nova/tests/test_cfg.py diff --git a/nova/common/__init__.py b/nova/common/__init__.py new file mode 100644 index 000000000000..0a3b98867a26 --- /dev/null +++ b/nova/common/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/nova/common/cfg.py b/nova/common/cfg.py new file mode 100644 index 000000000000..d0e7cc82cc7e --- /dev/null +++ b/nova/common/cfg.py @@ -0,0 +1,1126 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +r""" +Configuration options which may be set on the command line or in config files. + +The schema for each option is defined using the Opt sub-classes e.g. + + common_opts = [ + cfg.StrOpt('bind_host', + default='0.0.0.0', + help='IP address to listen on'), + cfg.IntOpt('bind_port', + default=9292, + help='Port number to listen on') + ] + +Options can be strings, integers, floats, booleans, lists or 'multi strings': + + enabled_apis_opt = \ + cfg.ListOpt('enabled_apis', + default=['ec2', 'osapi'], + help='List of APIs to enable by default') + + DEFAULT_EXTENSIONS = [ + 'nova.api.openstack.contrib.standard_extensions' + ] + osapi_extension_opt = \ + cfg.MultiStrOpt('osapi_extension', + default=DEFAULT_EXTENSIONS) + +Option schemas are registered with with the config manager at runtime, but +before the option is referenced: + + class ExtensionManager(object): + + enabled_apis_opt = cfg.ListOpt(...) + + def __init__(self, conf): + self.conf = conf + self.conf.register_opt(enabled_apis_opt) + ... + + def _load_extensions(self): + for ext_factory in self.conf.osapi_extension: + .... + +A common usage pattern is for each option schema to be defined in the module or +class which uses the option: + + opts = ... + + def add_common_opts(conf): + conf.register_opts(opts) + + def get_bind_host(conf): + return conf.bind_host + + def get_bind_port(conf): + return conf.bind_port + +An option may optionally be made available via the command line. Such options +must registered with the config manager before the command line is parsed (for +the purposes of --help and CLI arg validation): + + cli_opts = [ + cfg.BoolOpt('verbose', + short='v', + default=False, + help='Print more verbose output'), + cfg.BoolOpt('debug', + short='d', + default=False, + help='Print debugging output'), + ] + + def add_common_opts(conf): + conf.register_cli_opts(cli_opts) + +The config manager has a single CLI option defined by default, --config-file: + + class ConfigOpts(object): + + config_file_opt = \ + MultiStrOpt('config-file', + ... + + def __init__(self, ...): + ... + self.register_cli_opt(self.config_file_opt) + +Option values are parsed from any supplied config files using SafeConfigParser. +If none are specified, a default set is used e.g. glance-api.conf and +glance-common.conf: + + glance-api.conf: + [DEFAULT] + bind_port = 9292 + + glance-common.conf: + [DEFAULT] + bind_host = 0.0.0.0 + +Option values in config files override those on the command line. Config files +are parsed in order, with values in later files overriding those in earlier +files. + +The parsing of CLI args and config files is initiated by invoking the config +manager e.g. + + conf = ConfigOpts() + conf.register_opt(BoolOpt('verbose', ...)) + conf(sys.argv[1:]) + if conf.verbose: + ... + +Options can be registered as belonging to a group: + + rabbit_group = cfg.OptionGroup(name='rabbit', + title='RabbitMQ options') + + rabbit_host_opt = \ + cfg.StrOpt('host', + group='rabbit', + default='localhost', + help='IP/hostname to listen on'), + rabbit_port_opt = \ + cfg.IntOpt('port', + default=5672, + help='Port number to listen on') + rabbit_ssl_opt = \ + conf.BoolOpt('use_ssl', + default=False, + help='Whether to support SSL connections') + + def register_rabbit_opts(conf): + conf.register_group(rabbit_group) + # options can be registered under a group in any of these ways: + conf.register_opt(rabbit_host_opt) + conf.register_opt(rabbit_port_opt, group='rabbit') + conf.register_opt(rabbit_ssl_opt, group=rabbit_group) + +If no group is specified, options belong to the 'DEFAULT' section of config +files: + + glance-api.conf: + [DEFAULT] + bind_port = 9292 + ... + + [rabbit] + host = localhost + port = 5672 + use_ssl = False + userid = guest + password = guest + virtual_host = / + +Command-line options in a group are automatically prefixed with the group name: + + --rabbit-host localhost --rabbit-use-ssl False + +Option values in the default group are referenced as attributes/properties on +the config manager; groups are also attributes on the config manager, with +attributes for each of the options associated with the group: + + server.start(app, conf.bind_port, conf.bind_host, conf) + + self.connection = kombu.connection.BrokerConnection( + hostname=conf.rabbit.host, + port=conf.rabbit.port, + ...) + +Option values may reference other values using PEP 292 string substitution: + + opts = [ + cfg.StrOpt('state_path', + default=os.path.join(os.path.dirname(__file__), '../'), + help='Top-level directory for maintaining nova state'), + cfg.StrOpt('sqlite_db', + default='nova.sqlite', + help='file name for sqlite'), + cfg.StrOpt('sql_connection', + default='sqlite:///$state_path/$sqlite_db', + help='connection string for sql database'), + ] + +Note that interpolation can be avoided by using '$$'. +""" + +import sys +import ConfigParser +import copy +import optparse +import os +import string + + +class Error(Exception): + """Base class for cfg exceptions.""" + + def __init__(self, msg=None): + self.msg = msg + + def __str__(self): + return self.msg + + +class ArgsAlreadyParsedError(Error): + """Raised if a CLI opt is registered after parsing.""" + + def __str__(self): + ret = "arguments already parsed" + if self.msg: + ret += ": " + self.msg + return ret + + +class NoSuchOptError(Error): + """Raised if an opt which doesn't exist is referenced.""" + + def __init__(self, opt_name, group=None): + self.opt_name = opt_name + self.group = group + + def __str__(self): + if self.group is None: + return "no such option: %s" % self.opt_name + else: + return "no such option in group %s: %s" % (self.group.name, + self.opt_name) + + +class NoSuchGroupError(Error): + """Raised if a group which doesn't exist is referenced.""" + + def __init__(self, group_name): + self.group_name = group_name + + def __str__(self): + return "no such group: %s" % self.group_name + + +class DuplicateOptError(Error): + """Raised if multiple opts with the same name are registered.""" + + def __init__(self, opt_name): + self.opt_name = opt_name + + def __str__(self): + return "duplicate option: %s" % self.opt_name + + +class TemplateSubstitutionError(Error): + """Raised if an error occurs substituting a variable in an opt value.""" + + def __str__(self): + return "template substitution error: %s" % self.msg + + +class ConfigFilesNotFoundError(Error): + """Raised if one or more config files are not found.""" + + def __init__(self, config_files): + self.config_files = config_files + + def __str__(self): + return 'Failed to read some config files: %s' % \ + string.join(self.config_files, ',') + + +class ConfigFileParseError(Error): + """Raised if there is an error parsing a config file.""" + + def __init__(self, config_file, msg): + self.config_file = config_file + self.msg = msg + + def __str__(self): + return 'Failed to parse %s: %s' % (self.config_file, self.msg) + + +class ConfigFileValueError(Error): + """Raised if a config file value does not match its opt type.""" + pass + + +def find_config_files(project=None, prog=None): + """Return a list of default configuration files. + + We default to two config files: [${project}.conf, ${prog}.conf] + + And we look for those config files in the following directories: + + ~/.${project}/ + ~/ + /etc/${project}/ + /etc/ + + We return an absolute path for (at most) one of each the default config + files, for the topmost directory it exists in. + + For example, if project=foo, prog=bar and /etc/foo/foo.conf, /etc/bar.conf + and ~/.foo/bar.conf all exist, then we return ['/etc/foo/foo.conf', + '~/.foo/bar.conf'] + + If no project name is supplied, we only look for ${prog.conf}. + + :param project: an optional project name + :param prog: the program name, defaulting to the basename of sys.argv[0] + """ + if prog is None: + prog = os.path.basename(sys.argv[0]) + + fix_path = lambda p: os.path.abspath(os.path.expanduser(p)) + + cfg_dirs = [ + fix_path(os.path.join('~', '.' + project)) if project else None, + fix_path('~'), + os.path.join('/etc', project) if project else None, + '/etc' + ] + cfg_dirs = filter(bool, cfg_dirs) + + def search_dirs(dirs, basename): + for d in dirs: + path = os.path.join(d, basename) + if os.path.exists(path): + return path + + config_files = [] + if project: + config_files.append(search_dirs(cfg_dirs, '%s.conf' % project)) + config_files.append(search_dirs(cfg_dirs, '%s.conf' % prog)) + + return filter(bool, config_files) + + +def _is_opt_registered(opts, opt): + """Check whether an opt with the same name is already registered. + + The same opt may be registered multiple times, with only the first + registration having any effect. However, it is an error to attempt + to register a different opt with the same name. + + :param opts: the set of opts already registered + :param opt: the opt to be registered + :returns: True if the opt was previously registered, False otherwise + :raises: DuplicateOptError if a naming conflict is detected + """ + if opt.dest in opts: + if opts[opt.dest]['opt'] is not opt: + raise DuplicateOptError(opt.name) + return True + else: + return False + + +class Opt(object): + + """Base class for all configuration options. + + An Opt object has no public methods, but has a number of public string + properties: + + name: + the name of the option, which may include hyphens + dest: + the (hyphen-less) ConfigOpts property which contains the option value + short: + a single character CLI option name + default: + the default value of the option + metavar: + the name shown as the argument to a CLI option in --help output + help: + an string explaining how the options value is used + """ + + def __init__(self, name, dest=None, short=None, + default=None, metavar=None, help=None): + """Construct an Opt object. + + The only required parameter is the option's name. However, it is + common to also supply a default and help string for all options. + + :param name: the option's name + :param dest: the name of the corresponding ConfigOpts property + :param short: a single character CLI option name + :param default: the default value of the option + :param metavar: the option argument to show in --help + :param help: an explanation of how the option is used + """ + self.name = name + if dest is None: + self.dest = self.name.replace('-', '_') + else: + self.dest = dest + self.short = short + self.default = default + self.metavar = metavar + self.help = help + + def _get_from_config_parser(self, cparser, section): + """Retrieves the option value from a ConfigParser object. + + This is the method ConfigOpts uses to look up the option value from + config files. Most opt types override this method in order to perform + type appropriate conversion of the returned value. + + :param cparser: a ConfigParser object + :param section: a section name + """ + return cparser.get(section, self.dest) + + def _add_to_cli(self, parser, group=None): + """Makes the option available in the command line interface. + + This is the method ConfigOpts uses to add the opt to the CLI interface + as appropriate for the opt type. Some opt types may extend this method, + others may just extend the helper methods it uses. + + :param parser: the CLI option parser + :param group: an optional OptGroup object + """ + container = self._get_optparse_container(parser, group) + kwargs = self._get_optparse_kwargs(group) + prefix = self._get_optparse_prefix('', group) + self._add_to_optparse(container, self.name, self.short, kwargs, prefix) + + def _add_to_optparse(self, container, name, short, kwargs, prefix=''): + """Add an option to an optparse parser or group. + + :param container: an optparse.OptionContainer object + :param name: the opt name + :param short: the short opt name + :param kwargs: the keyword arguments for add_option() + :param prefix: an optional prefix to prepend to the opt name + :raises: DuplicateOptError if a naming confict is detected + """ + args = ['--' + prefix + name] + if short: + args += ['-' + short] + for a in args: + if container.has_option(a): + raise DuplicateOptError(a) + container.add_option(*args, **kwargs) + + def _get_optparse_container(self, parser, group): + """Returns an optparse.OptionContainer. + + :param parser: an optparse.OptionParser + :param group: an (optional) OptGroup object + :returns: an optparse.OptionGroup if a group is given, else the parser + """ + if group is not None: + return group._get_optparse_group(parser) + else: + return parser + + def _get_optparse_kwargs(self, group, **kwargs): + """Build a dict of keyword arguments for optparse's add_option(). + + Most opt types extend this method to customize the behaviour of the + options added to optparse. + + :param group: an optional group + :param kwargs: optional keyword arguments to add to + :returns: a dict of keyword arguments + """ + dest = self.dest + if group is not None: + dest = group.name + '_' + dest + kwargs.update({ + 'dest': dest, + 'metavar': self.metavar, + 'help': self.help, + }) + return kwargs + + def _get_optparse_prefix(self, prefix, group): + """Build a prefix for the CLI option name, if required. + + CLI options in a group are prefixed with the group's name in order + to avoid conflicts between similarly named options in different + groups. + + :param prefix: an existing prefix to append to (e.g. 'no' or '') + :param group: an optional OptGroup object + :returns: a CLI option prefix including the group name, if appropriate + """ + if group is not None: + return group.name + '-' + prefix + else: + return prefix + + +class StrOpt(Opt): + """ + String opts do not have their values transformed and are returned as + str objects. + """ + pass + + +class BoolOpt(Opt): + + """ + Bool opts are set to True or False on the command line using --optname or + --noopttname respectively. + + In config files, boolean values are case insensitive and can be set using + 1/0, yes/no, true/false or on/off. + """ + + def _get_from_config_parser(self, cparser, section): + """Retrieve the opt value as a boolean from ConfigParser.""" + return cparser.getboolean(section, self.dest) + + def _add_to_cli(self, parser, group=None): + """Extends the base class method to add the --nooptname option.""" + super(BoolOpt, self)._add_to_cli(parser, group) + self._add_inverse_to_optparse(parser, group) + + def _add_inverse_to_optparse(self, parser, group): + """Add the --nooptname option to the option parser.""" + container = self._get_optparse_container(parser, group) + kwargs = self._get_optparse_kwargs(group, action='store_false') + prefix = self._get_optparse_prefix('no', group) + self._add_to_optparse(container, self.name, None, kwargs, prefix) + + def _get_optparse_kwargs(self, group, action='store_true', **kwargs): + """Extends the base optparse keyword dict for boolean options.""" + return super(BoolOpt, + self)._get_optparse_kwargs(group, action=action, **kwargs) + + +class IntOpt(Opt): + + """Int opt values are converted to integers using the int() builtin.""" + + def _get_from_config_parser(self, cparser, section): + """Retrieve the opt value as a integer from ConfigParser.""" + return cparser.getint(section, self.dest) + + def _get_optparse_kwargs(self, group, **kwargs): + """Extends the base optparse keyword dict for integer options.""" + return super(IntOpt, + self)._get_optparse_kwargs(group, type='int', **kwargs) + + +class FloatOpt(Opt): + + """Float opt values are converted to floats using the float() builtin.""" + + def _get_from_config_parser(self, cparser, section): + """Retrieve the opt value as a float from ConfigParser.""" + return cparser.getfloat(section, self.dest) + + def _get_optparse_kwargs(self, group, **kwargs): + """Extends the base optparse keyword dict for float options.""" + return super(FloatOpt, + self)._get_optparse_kwargs(group, type='float', **kwargs) + + +class ListOpt(Opt): + + """ + List opt values are simple string values separated by commas. The opt value + is a list containing these strings. + """ + + def _get_from_config_parser(self, cparser, section): + """Retrieve the opt value as a list from ConfigParser.""" + return cparser.get(section, self.dest).split(',') + + def _get_optparse_kwargs(self, group, **kwargs): + """Extends the base optparse keyword dict for list options.""" + return super(ListOpt, + self)._get_optparse_kwargs(group, + type='string', + action='callback', + callback=self._parse_list, + **kwargs) + + def _parse_list(self, option, opt, value, parser): + """An optparse callback for parsing an option value into a list.""" + setattr(parser.values, self.dest, value.split(',')) + + +class MultiStrOpt(Opt): + + """ + Multistr opt values are string opts which may be specified multiple times. + The opt value is a list containing all the string values specified. + """ + + def _get_from_config_parser(self, cparser, section): + """Retrieve the opt value as a multistr from ConfigParser.""" + # FIXME(markmc): values spread across the CLI and multiple + # config files should be appended + value = \ + super(MultiStrOpt, self)._get_from_config_parser(cparser, section) + return value if value is None else [value] + + def _get_optparse_kwargs(self, group, **kwargs): + """Extends the base optparse keyword dict for multi str options.""" + return super(MultiStrOpt, + self)._get_optparse_kwargs(group, action='append') + + +class OptGroup(object): + + """ + Represents a group of opts. + + CLI opts in the group are automatically prefixed with the group name. + + Each group corresponds to a section in config files. + + An OptGroup object has no public methods, but has a number of public string + properties: + + name: + the name of the group + title: + the group title as displayed in --help + help: + the group description as displayed in --help + """ + + def __init__(self, name, title=None, help=None): + """Constructs an OptGroup object. + + :param name: the group name + :param title: the group title for --help + :param help: the group description for --help + """ + self.name = name + if title is None: + self.title = "%s options" % title + else: + self.title = title + self.help = help + + self._opts = {} # dict of dicts of {opt:, override:, default:) + self._optparse_group = None + + def _register_opt(self, opt): + """Add an opt to this group. + + :param opt: an Opt object + :returns: False if previously registered, True otherwise + :raises: DuplicateOptError if a naming conflict is detected + """ + if _is_opt_registered(self._opts, opt): + return False + + self._opts[opt.dest] = {'opt': opt, 'override': None, 'default': None} + + return True + + def _get_optparse_group(self, parser): + """Build an optparse.OptionGroup for this group.""" + if self._optparse_group is None: + self._optparse_group = \ + optparse.OptionGroup(parser, self.title, self.help) + return self._optparse_group + + +class ConfigOpts(object): + + """ + Config options which may be set on the command line or in config files. + + ConfigOpts is a configuration option manager with APIs for registering + option schemas, grouping options, parsing option values and retrieving + the values of options. + """ + + def __init__(self, + project=None, + prog=None, + version=None, + usage=None, + default_config_files=None): + """Construct a ConfigOpts object. + + Automatically registers the --config-file option with either a supplied + list of default config files, or a list from find_config_files(). + + :param project: the toplevel project name, used to locate config files + :param prog: the name of the program (defaults to sys.argv[0] basename) + :param version: the program version (for --version) + :param usage: a usage string (%prog will be expanded) + :param default_config_files: config files to use by default + """ + if prog is None: + prog = os.path.basename(sys.argv[0]) + + if default_config_files is None: + default_config_files = find_config_files(project, prog) + + self.project = project + self.prog = prog + self.version = version + self.usage = usage + self.default_config_files = default_config_files + + self._opts = {} # dict of dicts of (opt:, override:, default:) + self._groups = {} + + self._args = None + self._cli_values = {} + + self._oparser = optparse.OptionParser(prog=self.prog, + version=self.version, + usage=self.usage) + self._cparser = None + + self.register_cli_opt(\ + MultiStrOpt('config-file', + default=self.default_config_files, + metavar='PATH', + help='Path to a config file to use. Multiple config ' + 'files can be specified, with values in later ' + 'files taking precedence. The default files used ' + 'are: %s' % (self.default_config_files, ))) + + def __call__(self, args=None): + """Parse command line arguments and config files. + + Calling a ConfigOpts object causes the supplied command line arguments + and config files to be parsed, causing opt values to be made available + as attributes of the object. + + The object may be called multiple times, each time causing the previous + set of values to be overwritten. + + :params args: command line arguments (defaults to sys.argv[1:]) + :returns: the list of arguments left over after parsing options + :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError + """ + self.reset() + + self._args = args + + (values, args) = self._oparser.parse_args(self._args) + + self._cli_values = vars(values) + + if self.config_file: + self._parse_config_files(self.config_file) + + return args + + def __getattr__(self, name): + """Look up an option value and perform string substitution. + + :param name: the opt name (or 'dest', more precisely) + :returns: the option value (after string subsititution) or a GroupAttr + :raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError + """ + return self._substitute(self._get(name)) + + def reset(self): + """Reset the state of the object to before it was called.""" + self._args = None + self._cli_values = None + self._cparser = None + + def register_opt(self, opt, group=None): + """Register an option schema. + + Registering an option schema makes any option value which is previously + or subsequently parsed from the command line or config files available + as an attribute of this object. + + :param opt: an instance of an Opt sub-class + :param group: an optional OptGroup object or group name + :return: False if the opt was already register, True otherwise + :raises: DuplicateOptError + """ + if group is not None: + return self._get_group(group)._register_opt(opt) + + if _is_opt_registered(self._opts, opt): + return False + + self._opts[opt.dest] = {'opt': opt, 'override': None, 'default': None} + + return True + + def register_opts(self, opts, group=None): + """Register multiple option schemas at once.""" + for opt in opts: + self.register_opt(opt, group) + + def register_cli_opt(self, opt, group=None): + """Register a CLI option schema. + + CLI option schemas must be registered before the command line and + config files are parsed. This is to ensure that all CLI options are + show in --help and option validation works as expected. + + :param opt: an instance of an Opt sub-class + :param group: an optional OptGroup object or group name + :return: False if the opt was already register, True otherwise + :raises: DuplicateOptError, ArgsAlreadyParsedError + """ + if self._args != None: + raise ArgsAlreadyParsedError("cannot register CLI option") + + if not self.register_opt(opt, group): + return False + + if group is not None: + group = self._get_group(group) + + opt._add_to_cli(self._oparser, group) + + return True + + def register_cli_opts(self, opts, group=None): + """Register multiple CLI option schemas at once.""" + for opt in opts: + self.register_cli_opt(opt, group) + + def register_group(self, group): + """Register an option group. + + An option group must be registered before options can be registered + with the group. + + :param group: an OptGroup object + """ + if group.name in self._groups: + return + + self._groups[group.name] = copy.copy(group) + + def set_override(self, name, override, group=None): + """Override an opt value. + + Override the command line, config file and default values of a + given option. + + :param name: the name/dest of the opt + :param override: the override value + :param group: an option OptGroup object or group name + :raises: NoSuchOptError, NoSuchGroupError + """ + opt_info = self._get_opt_info(name, group) + opt_info['override'] = override + + def set_default(self, name, default, group=None): + """Override an opt's default value. + + Override the default value of given option. A command line or + config file value will still take precedence over this default. + + :param name: the name/dest of the opt + :param default: the default value + :param group: an option OptGroup object or group name + :raises: NoSuchOptError, NoSuchGroupError + """ + opt_info = self._get_opt_info(name, group) + opt_info['default'] = default + + def log_opt_values(self, logger, lvl): + """Log the value of all registered opts. + + It's often useful for an app to log its configuration to a log file at + startup for debugging. This method dumps to the entire config state to + the supplied logger at a given log level. + + :param logger: a logging.Logger object + :param lvl: the log level (e.g. logging.DEBUG) arg to logger.log() + """ + logger.log(lvl, "*" * 80) + logger.log(lvl, "Configuration options gathered from:") + logger.log(lvl, "command line args: %s", self._args) + logger.log(lvl, "config files: %s", self.config_file) + logger.log(lvl, "=" * 80) + + for opt_name in sorted(self._opts): + logger.log(lvl, "%-30s = %s", opt_name, getattr(self, opt_name)) + + for group_name in self._groups: + group_attr = self.GroupAttr(self, group_name) + for opt_name in sorted(self._groups[group_name]._opts): + logger.log(lvl, "%-30s = %s", + "%s.%s" % (group_name, opt_name), + getattr(group_attr, opt_name)) + + logger.log(lvl, "*" * 80) + + def print_usage(self, file=None): + """Print the usage message for the current program.""" + self._oparser.print_usage(file) + + def _get(self, name, group=None): + """Look up an option value. + + :param name: the opt name (or 'dest', more precisely) + :param group: an option OptGroup + :returns: the option value, or a GroupAttr object + :raises: NoSuchOptError, NoSuchGroupError, ConfigFileValueError, + TemplateSubstitutionError + """ + if group is None and name in self._groups: + return self.GroupAttr(self, name) + + if group is not None: + group = self._get_group(group) + + info = self._get_opt_info(name, group) + default, opt, override = map(lambda k: info[k], sorted(info.keys())) + + if override is not None: + return override + + if self._cparser is not None: + section = group.name if group is not None else 'DEFAULT' + try: + return opt._get_from_config_parser(self._cparser, section) + except (ConfigParser.NoOptionError, + ConfigParser.NoSectionError): + pass + except ValueError, ve: + raise ConfigFileValueError(str(ve)) + + name = name if group is None else group.name + '_' + name + value = self._cli_values.get(name, None) + if value is not None: + return value + + if default is not None: + return default + + return opt.default + + def _substitute(self, value): + """Perform string template substitution. + + Substititue any template variables (e.g. $foo, ${bar}) in the supplied + string value(s) with opt values. + + :param value: the string value, or list of string values + :returns: the substituted string(s) + """ + if type(value) is list: + return [self._substitute(i) for i in value] + elif type(value) is str: + tmpl = string.Template(value) + return tmpl.safe_substitute(self.StrSubWrapper(self)) + else: + return value + + def _get_group(self, group_or_name): + """Looks up a OptGroup object. + + Helper function to return an OptGroup given a parameter which can + either be the group's name or an OptGroup object. + + The OptGroup object returned is from the internal dict of OptGroup + objects, which will be a copy of any OptGroup object that users of + the API have access to. + + :param group_or_name: the group's name or the OptGroup object itself + :raises: NoSuchGroupError + """ + if isinstance(group_or_name, OptGroup): + group_name = group_or_name.name + else: + group_name = group_or_name + + if not group_name in self._groups: + raise NoSuchGroupError(group_name) + + return self._groups[group_name] + + def _get_opt_info(self, opt_name, group=None): + """Return the (opt, override, default) dict for an opt. + + :param opt_name: an opt name/dest + :param group: an optional group name or OptGroup object + :raises: NoSuchOptError, NoSuchGroupError + """ + if group is None: + opts = self._opts + else: + group = self._get_group(group) + opts = group._opts + + if not opt_name in opts: + raise NoSuchOptError(opt_name, group) + + return opts[opt_name] + + def _parse_config_files(self, config_files): + """Parse the supplied configuration files. + + :raises: ConfigFilesNotFoundError, ConfigFileParseError + """ + self._cparser = ConfigParser.SafeConfigParser(allow_no_value=True) + + try: + read_ok = self._cparser.read(config_files) + except ConfigParser.ParsingError, cpe: + raise ConfigFileParseError(cpe.filename, cpe.message) + + if read_ok != config_files: + not_read_ok = filter(lambda f: f not in read_ok, config_files) + raise ConfigFilesNotFoundError(not_read_ok) + + class GroupAttr(object): + + """ + A helper class representing the option values of a group as attributes. + """ + + def __init__(self, conf, group): + """Construct a GroupAttr object. + + :param conf: a ConfigOpts object + :param group: a group name or OptGroup object + """ + self.conf = conf + self.group = group + + def __getattr__(self, name): + """Look up an option value and perform template substitution.""" + return self.conf._substitute(self.conf._get(name, self.group)) + + class StrSubWrapper(object): + + """ + A helper class exposing opt values as a dict for string substitution. + """ + + def __init__(self, conf): + """Construct a StrSubWrapper object. + + :param conf: a ConfigOpts object + """ + self.conf = conf + + def __getitem__(self, key): + """Look up an opt value from the ConfigOpts object. + + :param key: an opt name + :returns: an opt value + :raises: TemplateSubstitutionError if attribute is a group + """ + value = getattr(self.conf, key) + if isinstance(value, self.conf.GroupAttr): + raise TemplateSubstitutionError( + 'substituting group %s not supported' % key) + return value + + +class CommonConfigOpts(ConfigOpts): + + DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" + DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + + common_cli_opts = [ + BoolOpt('debug', + short='d', + default=False, + help='Print debugging output'), + BoolOpt('verbose', + short='v', + default=False, + help='Print more verbose output'), + ] + + logging_cli_opts = [ + StrOpt('log-config', + metavar='PATH', + help='If this option is specified, the logging configuration ' + 'file specified is used and overrides any other logging ' + 'options specified. Please see the Python logging module ' + 'documentation for details on logging configuration ' + 'files.'), + StrOpt('log-format', + default=DEFAULT_LOG_FORMAT, + metavar='FORMAT', + help='A logging.Formatter log message format string which may ' + 'use any of the available logging.LogRecord attributes. ' + 'Default: %default'), + StrOpt('log-date-format', + default=DEFAULT_LOG_DATE_FORMAT, + metavar='DATE_FORMAT', + help='Format string for %(asctime)s in log records. ' + 'Default: %default'), + StrOpt('log-file', + metavar='PATH', + help='(Optional) Name of log file to output to. ' + 'If not set, logging will go to stdout.'), + StrOpt('log-dir', + help='(Optional) The directory to keep log files in ' + '(will be prepended to --logfile)'), + BoolOpt('use-syslog', + default=False, + help='Use syslog for logging.'), + ] + + def __init__(self, **kwargs): + super(CommonConfigOpts, self).__init__(**kwargs) + self.register_cli_opts(self.common_cli_opts) + self.register_cli_opts(self.logging_cli_opts) diff --git a/nova/tests/test_cfg.py b/nova/tests/test_cfg.py new file mode 100644 index 000000000000..90147f589467 --- /dev/null +++ b/nova/tests/test_cfg.py @@ -0,0 +1,794 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import sys +import StringIO +import tempfile +import unittest + +import stubout + +from nova import test +from nova.common.cfg import * + + +class BaseTestCase(test.TestCase): + + def setUp(self): + self.conf = ConfigOpts(prog='test', + version='1.0', + usage='%prog FOO BAR', + default_config_files=[]) + self.tempfiles = [] + self.stubs = stubout.StubOutForTesting() + + def tearDown(self): + self.remove_tempfiles() + self.stubs.UnsetAll() + + def create_tempfiles(self, files): + for (basename, contents) in files: + (fd, path) = tempfile.mkstemp(prefix=basename) + self.tempfiles.append(path) + try: + os.write(fd, contents) + finally: + os.close(fd) + return self.tempfiles[-len(files):] + + def remove_tempfiles(self): + for p in self.tempfiles: + os.remove(p) + + +class LeftoversTestCase(BaseTestCase): + + def test_leftovers(self): + self.conf.register_cli_opt(StrOpt('foo')) + self.conf.register_cli_opt(StrOpt('bar')) + + leftovers = self.conf(['those', '--foo', 'this', + 'thems', '--bar', 'that', 'these']) + + self.assertEquals(leftovers, ['those', 'thems', 'these']) + + +class FindConfigFilesTestCase(BaseTestCase): + + def test_find_config_files(self): + config_files = \ + [os.path.expanduser('~/.blaa/blaa.conf'), '/etc/foo.conf'] + + self.stubs.Set(os.path, 'exists', lambda p: p in config_files) + + self.assertEquals(find_config_files(project='blaa', prog='foo'), + config_files) + + +class CliOptsTestCase(BaseTestCase): + + def _do_cli_test(self, opt_class, default, cli_args, value): + self.conf.register_cli_opt(opt_class('foo', default=default)) + + self.conf(cli_args) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, value) + + def test_str_default(self): + self._do_cli_test(StrOpt, None, [], None) + + def test_str_arg(self): + self._do_cli_test(StrOpt, None, ['--foo', 'bar'], 'bar') + + def test_bool_default(self): + self._do_cli_test(BoolOpt, False, [], False) + + def test_bool_arg(self): + self._do_cli_test(BoolOpt, None, ['--foo'], True) + + def test_bool_arg_inverse(self): + self._do_cli_test(BoolOpt, None, ['--foo', '--nofoo'], False) + + def test_int_default(self): + self._do_cli_test(IntOpt, 10, [], 10) + + def test_int_arg(self): + self._do_cli_test(IntOpt, None, ['--foo=20'], 20) + + def test_float_default(self): + self._do_cli_test(FloatOpt, 1.0, [], 1.0) + + def test_float_arg(self): + self._do_cli_test(FloatOpt, None, ['--foo', '2.0'], 2.0) + + def test_list_default(self): + self._do_cli_test(ListOpt, ['bar'], [], ['bar']) + + def test_list_arg(self): + self._do_cli_test(ListOpt, None, + ['--foo', 'blaa,bar'], ['blaa', 'bar']) + + def test_multistr_default(self): + self._do_cli_test(MultiStrOpt, ['bar'], [], ['bar']) + + def test_multistr_arg(self): + self._do_cli_test(MultiStrOpt, None, + ['--foo', 'blaa', '--foo', 'bar'], ['blaa', 'bar']) + + def test_help(self): + self.stubs.Set(sys, 'stdout', StringIO.StringIO()) + self.assertRaisesRegexp(SystemExit, '0', self.conf, ['--help']) + self.assertIn('FOO BAR', sys.stdout.getvalue()) + self.assertIn('--version', sys.stdout.getvalue()) + self.assertIn('--help', sys.stdout.getvalue()) + self.assertIn('--config-file=PATH', sys.stdout.getvalue()) + + def test_version(self): + self.stubs.Set(sys, 'stdout', StringIO.StringIO()) + self.assertRaisesRegexp(SystemExit, '0', self.conf, ['--version']) + self.assertIn('1.0', sys.stdout.getvalue()) + + def test_config_file(self): + paths = self.create_tempfiles([('1.conf', '[DEFAULT]'), + ('2.conf', '[DEFAULT]')]) + + self.conf(['--config-file', paths[0], '--config-file', paths[1]]) + + self.assertEquals(self.conf.config_file, paths) + + +class ConfigFileOptsTestCase(BaseTestCase): + + def test_str_default(self): + self.conf.register_opt(StrOpt('foo', default='bar')) + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, 'bar') + + def test_str_value(self): + self.conf.register_opt(StrOpt('foo')) + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, 'bar') + + def test_str_value_override(self): + self.conf.register_cli_opt(StrOpt('foo')) + + paths = self.create_tempfiles([('1.conf', + '[DEFAULT]\n' + 'foo = baar\n'), + ('2.conf', + '[DEFAULT]\n' + 'foo = baaar\n')]) + + self.conf(['--foo', 'bar', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, 'baaar') + + def test_int_default(self): + self.conf.register_opt(IntOpt('foo', default=666)) + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, 666) + + def test_int_value(self): + self.conf.register_opt(IntOpt('foo')) + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n' + 'foo = 666\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, 666) + + def test_int_value_override(self): + self.conf.register_cli_opt(IntOpt('foo')) + + paths = self.create_tempfiles([('1.conf', + '[DEFAULT]\n' + 'foo = 66\n'), + ('2.conf', + '[DEFAULT]\n' + 'foo = 666\n')]) + + self.conf(['--foo', '6', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, 666) + + def test_float_default(self): + self.conf.register_opt(FloatOpt('foo', default=6.66)) + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, 6.66) + + def test_float_value(self): + self.conf.register_opt(FloatOpt('foo')) + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n' + 'foo = 6.66\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, 6.66) + + def test_float_value_override(self): + self.conf.register_cli_opt(FloatOpt('foo')) + + paths = self.create_tempfiles([('1.conf', + '[DEFAULT]\n' + 'foo = 6.6\n'), + ('2.conf', + '[DEFAULT]\n' + 'foo = 6.66\n')]) + + self.conf(['--foo', '6', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, 6.66) + + def test_list_default(self): + self.conf.register_opt(ListOpt('foo', default=['bar'])) + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, ['bar']) + + def test_list_value(self): + self.conf.register_opt(ListOpt('foo')) + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, ['bar']) + + def test_list_value_override(self): + self.conf.register_cli_opt(ListOpt('foo')) + + paths = self.create_tempfiles([('1.conf', + '[DEFAULT]\n' + 'foo = bar,bar\n'), + ('2.conf', + '[DEFAULT]\n' + 'foo = b,a,r\n')]) + + self.conf(['--foo', 'bar', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, ['b', 'a', 'r']) + + def test_multistr_default(self): + self.conf.register_opt(MultiStrOpt('foo', default=['bar'])) + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, ['bar']) + + def test_multistr_value(self): + self.conf.register_opt(MultiStrOpt('foo')) + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, ['bar']) + + @unittest.skip('FIXME(markmc): values spread across the CLI and multiple ' + 'config files should be appended') + def test_multistr_values_append(self): + self.conf.register_cli_opt(ListOpt('foo')) + + paths = self.create_tempfiles([('1.conf', + '[DEFAULT]\n' + 'foo = bar\n'), + ('2.conf', + '[DEFAULT]\n' + 'foo = bar\n')]) + + self.conf(['--foo', 'bar', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, ['bar', 'bar', 'bar']) + + +class OptGroupsTestCase(BaseTestCase): + + def test_arg_group(self): + blaa_group = OptGroup('blaa') + self.conf.register_group(blaa_group) + self.conf.register_cli_opt(StrOpt('foo'), group=blaa_group) + + self.conf(['--blaa-foo', 'bar']) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEquals(self.conf.blaa.foo, 'bar') + + def test_arg_group_by_name(self): + self.conf.register_group(OptGroup('blaa')) + self.conf.register_cli_opt(StrOpt('foo'), group='blaa') + + self.conf(['--blaa-foo', 'bar']) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEquals(self.conf.blaa.foo, 'bar') + + def test_arg_group_with_default(self): + self.conf.register_group(OptGroup('blaa')) + self.conf.register_cli_opt(StrOpt('foo', default='bar'), group='blaa') + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEquals(self.conf.blaa.foo, 'bar') + + def test_arg_group_in_config_file(self): + self.conf.register_group(OptGroup('blaa')) + self.conf.register_opt(StrOpt('foo'), group='blaa') + + paths = self.create_tempfiles([('test.conf', + '[blaa]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEquals(self.conf.blaa.foo, 'bar') + + +class TemplateSubstitutionTestCase(BaseTestCase): + + def _prep_test_str_sub(self, foo_default=None, bar_default=None): + self.conf.register_cli_opt(StrOpt('foo', default=foo_default)) + self.conf.register_cli_opt(StrOpt('bar', default=bar_default)) + + def _assert_str_sub(self): + self.assertTrue(hasattr(self.conf, 'bar')) + self.assertEquals(self.conf.bar, 'blaa') + + def test_str_sub_default_from_default(self): + self._prep_test_str_sub(foo_default='blaa', bar_default='$foo') + + self.conf([]) + + self._assert_str_sub() + + def test_str_sub_default_from_arg(self): + self._prep_test_str_sub(bar_default='$foo') + + self.conf(['--foo', 'blaa']) + + self._assert_str_sub() + + def test_str_sub_default_from_config_file(self): + self._prep_test_str_sub(bar_default='$foo') + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n' + 'foo = blaa\n')]) + + self.conf(['--config-file', paths[0]]) + + self._assert_str_sub() + + def test_str_sub_arg_from_default(self): + self._prep_test_str_sub(foo_default='blaa') + + self.conf(['--bar', '$foo']) + + self._assert_str_sub() + + def test_str_sub_arg_from_arg(self): + self._prep_test_str_sub() + + self.conf(['--foo', 'blaa', '--bar', '$foo']) + + self._assert_str_sub() + + def test_str_sub_arg_from_config_file(self): + self._prep_test_str_sub() + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n' + 'foo = blaa\n')]) + + self.conf(['--config-file', paths[0], '--bar=$foo']) + + self._assert_str_sub() + + def test_str_sub_config_file_from_default(self): + self._prep_test_str_sub(foo_default='blaa') + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n' + 'bar = $foo\n')]) + + self.conf(['--config-file', paths[0]]) + + self._assert_str_sub() + + def test_str_sub_config_file_from_arg(self): + self._prep_test_str_sub() + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n' + 'bar = $foo\n')]) + + self.conf(['--config-file', paths[0], '--foo=blaa']) + + self._assert_str_sub() + + def test_str_sub_config_file_from_config_file(self): + self._prep_test_str_sub() + + paths = self.create_tempfiles([('test.conf', + '[DEFAULT]\n' + 'bar = $foo\n' + 'foo = blaa\n')]) + + self.conf(['--config-file', paths[0]]) + + self._assert_str_sub() + + def test_str_sub_group_from_default(self): + self.conf.register_cli_opt(StrOpt('foo', default='blaa')) + self.conf.register_group(OptGroup('ba')) + self.conf.register_cli_opt(StrOpt('r', default='$foo'), group='ba') + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'ba')) + self.assertTrue(hasattr(self.conf.ba, 'r')) + self.assertEquals(self.conf.ba.r, 'blaa') + + +class ReparseTestCase(BaseTestCase): + + def test_reparse(self): + self.conf.register_group(OptGroup('blaa')) + self.conf.register_cli_opt(StrOpt('foo', default='r'), group='blaa') + + paths = self.create_tempfiles([('test.conf', + '[blaa]\n' + 'foo = b\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEquals(self.conf.blaa.foo, 'b') + + self.conf(['--blaa-foo', 'a']) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEquals(self.conf.blaa.foo, 'a') + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEquals(self.conf.blaa.foo, 'r') + + +class OverridesTestCase(BaseTestCase): + + def test_no_default_override(self): + self.conf.register_opt(StrOpt('foo')) + self.conf([]) + self.assertEquals(self.conf.foo, None) + self.conf.set_default('foo', 'bar') + self.assertEquals(self.conf.foo, 'bar') + + def test_default_override(self): + self.conf.register_opt(StrOpt('foo', default='foo')) + self.conf([]) + self.assertEquals(self.conf.foo, 'foo') + self.conf.set_default('foo', 'bar') + self.assertEquals(self.conf.foo, 'bar') + self.conf.set_default('foo', None) + self.assertEquals(self.conf.foo, 'foo') + + def test_override(self): + self.conf.register_opt(StrOpt('foo')) + self.conf.set_override('foo', 'bar') + self.conf([]) + self.assertEquals(self.conf.foo, 'bar') + + def test_group_no_default_override(self): + self.conf.register_group(OptGroup('blaa')) + self.conf.register_opt(StrOpt('foo'), group='blaa') + self.conf([]) + self.assertEquals(self.conf.blaa.foo, None) + self.conf.set_default('foo', 'bar', group='blaa') + self.assertEquals(self.conf.blaa.foo, 'bar') + + def test_default_override(self): + self.conf.register_group(OptGroup('blaa')) + self.conf.register_opt(StrOpt('foo', default='foo'), group='blaa') + self.conf([]) + self.assertEquals(self.conf.blaa.foo, 'foo') + self.conf.set_default('foo', 'bar', group='blaa') + self.assertEquals(self.conf.blaa.foo, 'bar') + self.conf.set_default('foo', None, group='blaa') + self.assertEquals(self.conf.blaa.foo, 'foo') + + def test_override(self): + self.conf.register_group(OptGroup('blaa')) + self.conf.register_opt(StrOpt('foo'), group='blaa') + self.conf.set_override('foo', 'bar', group='blaa') + self.conf([]) + self.assertEquals(self.conf.blaa.foo, 'bar') + + +class SadPathTestCase(BaseTestCase): + + def test_unknown_attr(self): + self.conf([]) + self.assertFalse(hasattr(self.conf, 'foo')) + self.assertRaisesRegexp(NoSuchOptError, 'foo', + getattr, self.conf, 'foo') + + def test_unknown_group_attr(self): + self.conf.register_group(OptGroup('blaa')) + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertFalse(hasattr(self.conf.blaa, 'foo')) + self.assertRaisesRegexp(NoSuchOptError, 'blaa.*foo', + getattr, self.conf.blaa, 'foo') + + def test_ok_duplicate(self): + opt = StrOpt('foo') + self.conf.register_cli_opt(opt) + self.conf.register_cli_opt(opt) + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEquals(self.conf.foo, None) + + def test_error_duplicate(self): + self.conf.register_cli_opt(StrOpt('foo')) + self.assertRaisesRegexp(DuplicateOptError, 'foo', + self.conf.register_cli_opt, + StrOpt('foo')) + + def test_error_duplicate_with_different_dest(self): + self.conf.register_cli_opt(StrOpt('foo', dest='f')) + self.assertRaisesRegexp(DuplicateOptError, 'foo', + self.conf.register_cli_opt, + StrOpt('foo')) + + def test_error_duplicate_short(self): + self.conf.register_cli_opt(StrOpt('foo', short='f')) + self.assertRaises(DuplicateOptError, + self.conf.register_cli_opt, + StrOpt('bar', short='f')) + + def test_no_such_group(self): + self.assertRaisesRegexp(NoSuchGroupError, 'blaa', + self.conf.register_cli_opt, + StrOpt('foo'), group='blaa') + + def test_already_parsed(self): + self.conf([]) + + self.assertRaises(ArgsAlreadyParsedError, + self.conf.register_cli_opt, StrOpt('foo')) + + def test_bad_cli_arg(self): + self.stubs.Set(sys, 'stderr', StringIO.StringIO()) + + self.assertRaisesRegexp(SystemExit, '2', self.conf, ['--foo']) + + self.assertIn('error', sys.stderr.getvalue()) + self.assertIn('--foo', sys.stderr.getvalue()) + + def _do_test_bad_cli_value(self, opt_class): + self.conf.register_cli_opt(opt_class('foo')) + + self.stubs.Set(sys, 'stderr', StringIO.StringIO()) + + self.assertRaisesRegexp(SystemExit, '2', self.conf, ['--foo', 'bar']) + + self.assertIn('foo', sys.stderr.getvalue()) + self.assertIn('bar', sys.stderr.getvalue()) + + def test_bad_int_arg(self): + self._do_test_bad_cli_value(IntOpt) + + def test_bad_float_arg(self): + self._do_test_bad_cli_value(FloatOpt) + + def test_conf_file_not_found(self): + paths = self.create_tempfiles([('test.conf', '')]) + os.remove(paths[0]) + self.tempfiles.remove(paths[0]) + + self.assertRaisesRegexp(ConfigFilesNotFoundError, paths[0], + self.conf, ['--config-file', paths[0]]) + + def test_conf_file_not_found(self): + paths = self.create_tempfiles([('test.conf', 'foo')]) + + self.assertRaisesRegexp(ConfigFileParseError, paths[0], + self.conf, ['--config-file', paths[0]]) + + def _do_test_conf_file_bad_value(self, opt_class): + self.conf.register_opt(opt_class('foo')) + + def test_conf_file_bad_bool(self): + self._do_test_conf_file_bad_value(BoolOpt) + + def test_conf_file_bad_int(self): + self._do_test_conf_file_bad_value(IntOpt) + + def test_conf_file_bad_float(self): + self._do_test_conf_file_bad_value(FloatOpt) + + def test_str_sub_from_group(self): + self.conf.register_group(OptGroup('f')) + self.conf.register_cli_opt(StrOpt('oo', default='blaa'), group='f') + self.conf.register_cli_opt(StrOpt('bar', default='$f.oo')) + + self.conf([]) + + self.assertFalse(hasattr(self.conf, 'bar')) + self.assertRaises(TemplateSubstitutionError, getattr, self.conf, 'bar') + + def test_set_default_unknown_attr(self): + self.conf([]) + self.assertRaisesRegexp(NoSuchOptError, 'foo', + self.conf.set_default, 'foo', 'bar') + + def test_set_default_unknown_group(self): + self.conf([]) + self.assertRaisesRegexp(NoSuchGroupError, 'blaa', + self.conf.set_default, + 'foo', 'bar', group='blaa') + + def test_set_override_unknown_attr(self): + self.conf([]) + self.assertRaisesRegexp(NoSuchOptError, 'foo', + self.conf.set_override, 'foo', 'bar') + + def test_set_override_unknown_group(self): + self.conf([]) + self.assertRaisesRegexp(NoSuchGroupError, 'blaa', + self.conf.set_override, + 'foo', 'bar', group='blaa') + + +class OptDumpingTestCase(BaseTestCase): + + class FakeLogger: + + def __init__(self, test_case, expected_lvl): + self.test_case = test_case + self.expected_lvl = expected_lvl + self.logged = [] + + def log(self, lvl, fmt, *args): + self.test_case.assertEquals(lvl, self.expected_lvl) + self.logged.append(fmt % args) + + def test_log_opt_values(self): + self.conf.register_cli_opt(StrOpt('foo')) + self.conf.register_group(OptGroup('blaa')) + self.conf.register_cli_opt(StrOpt('bar'), 'blaa') + + self.conf(['--foo', 'this', '--blaa-bar', 'that']) + + logger = self.FakeLogger(self, 666) + + self.conf.log_opt_values(logger, 666) + + self.assertEquals(logger.logged, [ + "*" * 80, + "Configuration options gathered from:", + "command line args: ['--foo', 'this', '--blaa-bar', 'that']", + "config files: []", + "=" * 80, + "config_file = []", + "foo = this", + "blaa.bar = that", + "*" * 80, + ]) + + +class CommonOptsTestCase(BaseTestCase): + + def setUp(self): + super(CommonOptsTestCase, self).setUp() + self.conf = CommonConfigOpts() + + def test_debug_verbose(self): + self.conf(['--debug', '--verbose']) + + self.assertEquals(self.conf.debug, True) + self.assertEquals(self.conf.verbose, True) + + def test_logging_opts(self): + self.conf([]) + + self.assertIsNone(self.conf.log_config) + self.assertIsNone(self.conf.log_file) + self.assertIsNone(self.conf.log_dir) + + self.assertEquals(self.conf.log_format, + CommonConfigOpts.DEFAULT_LOG_FORMAT) + self.assertEquals(self.conf.log_date_format, + CommonConfigOpts.DEFAULT_LOG_DATE_FORMAT) + + self.assertEquals(self.conf.use_syslog, False)