# vim: tabstop=4 shiftwidth=4 softtabstop=4
-# Copyright 2011 Red Hat, Inc.
+# Copyright 2012 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
def add_common_opts(conf):
conf.register_cli_opts(cli_opts)
-The config manager has a single CLI option defined by default, --config-file::
+The config manager has two CLI options defined by default, --config-file
+and --config-dir::
class ConfigOpts(object):
- config_file_opt = MultiStrOpt('config-file',
- ...
+ def __call__(self, ...):
- def __init__(self, ...):
- ...
- self.register_cli_opt(self.config_file_opt)
+ opts = [
+ MultiStrOpt('config-file',
+ ...),
+ StrOpt('config-dir',
+ ...),
+ ]
+
+ self.register_cli_opts(opts)
Option values are parsed from any supplied config files using
openstack.common.iniparser. If none are specified, a default set is used
conf.register_opt(rabbit_host_opt, group=rabbit_group)
conf.register_opt(rabbit_port_opt, group='rabbit')
+If it no group attributes are required other than the group name, the group
+need not be explicitly registered e.g.
+
+ def register_rabbit_opts(conf):
+ # The group will automatically be created, equivalent calling::
+ # conf.register_group(OptGroup(name='rabbit'))
+ conf.register_opt(rabbit_port_opt, group='rabbit')
+
If no group is specified, options belong to the 'DEFAULT' section of config
files::
i.e. argument parsing is stopped at the first non-option argument.
+Options may be declared as required so that an error is raised if the user
+does not supply a value for the option.
+
Options may be declared as secret so that their values are not leaked into
log files:
...
]
+This module also contains a global instance of the CommonConfigOpts class
+in order to support a common usage pattern in OpenStack:
+
+ from openstack.common import cfg
+
+ opts = [
+ cfg.StrOpt('bind_host' default='0.0.0.0'),
+ cfg.IntOpt('bind_port', default=9292),
+ ]
+
+ CONF = cfg.CONF
+ CONF.register_opts(opts)
+
+ def start(server, app):
+ server.start(app, CONF.bind_port, CONF.bind_host)
+
"""
import collections
import copy
import functools
+import glob
import optparse
import os
import string
return "duplicate option: %s" % self.opt_name
+class RequiredOptError(Error):
+ """Raised if an option is required but no value is supplied by the user."""
+
+ def __init__(self, opt_name, group=None):
+ self.opt_name = opt_name
+ self.group = group
+
+ def __str__(self):
+ if self.group is None:
+ return "value required for option: %s" % self.opt_name
+ else:
+ return "value required for option: %s.%s" % (self.group.name,
+ self.opt_name)
+
+
class TemplateSubstitutionError(Error):
"""Raised if an error occurs substituting a variable in an opt value."""
pass
+def _get_config_dirs(project=None):
+ """Return a list of directors where config files may be located.
+
+ :param project: an optional project name
+
+ If a project is specified, following directories are returned::
+
+ ~/.${project}/
+ ~/
+ /etc/${project}/
+ /etc/
+
+ Otherwise, these directories::
+
+ ~/
+ /etc/
+ """
+ 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'
+ ]
+
+ return filter(bool, cfg_dirs)
+
+
+def _search_dirs(dirs, basename, extension=""):
+ """Search a list of directories for a given filename.
+
+ Iterator over the supplied directories, returning the first file
+ found with the supplied name and extension.
+
+ :param dirs: a list of directories
+ :param basename: the filename, e.g. 'glance-api'
+ :param extension: the file extension, e.g. '.conf'
+ :returns: the path to a matching file, or None
+ """
+ for d in dirs:
+ path = os.path.join(d, '%s%s' % (basename, extension))
+ if os.path.exists(path):
+ return path
+
+
def find_config_files(project=None, prog=None, extension='.conf'):
"""Return a list of default configuration files.
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, extension):
- for d in dirs:
- path = os.path.join(d, '%s%s' % (basename, extension))
- if os.path.exists(path):
- return path
+ cfg_dirs = _get_config_dirs(project)
config_files = []
if project:
- config_files.append(search_dirs(cfg_dirs, project, extension))
- config_files.append(search_dirs(cfg_dirs, prog, extension))
+ config_files.append(_search_dirs(cfg_dirs, project, extension))
+ config_files.append(_search_dirs(cfg_dirs, prog, extension))
return filter(bool, config_files)
multi = False
def __init__(self, name, dest=None, short=None, default=None,
- metavar=None, help=None, secret=False):
+ metavar=None, help=None, secret=False, required=False):
"""Construct an Opt object.
The only required parameter is the option's name. However, it is
:param metavar: the option argument to show in --help
:param help: an explanation of how the option is used
:param secret: true iff the value should be obfuscated in log output
+ :param required: true iff a value must be supplied for this option
"""
self.name = name
if dest is None:
self.metavar = metavar
self.help = help
self.secret = secret
+ self.required = required
def _get_from_config_parser(self, cparser, section):
"""Retrieves the option value from a MultiConfigParser object.
return True
+ def _unregister_opt(self, opt):
+ """Remove an opt from this group.
+
+ :param opt: an Opt object
+ """
+ if opt.dest in self._opts:
+ del self._opts[opt.dest]
+
def _get_optparse_group(self, parser):
"""Build an optparse.OptionGroup for this group."""
if self._optparse_group is None:
self.help)
return self._optparse_group
+ def _clear(self):
+ """Clear this group's option parsing state."""
+ self._optparse_group = None
+
class ParseError(iniparser.ParseError):
def __init__(self, msg, lineno, line, filename):
the values of options.
"""
- def __init__(self,
- project=None,
- prog=None,
- version=None,
- usage=None,
- default_config_files=None):
- """Construct a ConfigOpts object.
+ def __init__(self):
+ """Construct a ConfigOpts object."""
+ self._opts = {} # dict of dicts of (opt:, override:, default:)
+ self._groups = {}
- Automatically registers the --config-file option with either a supplied
- list of default config files, or a list from find_config_files().
+ self._args = None
+ self._oparser = None
+ self._cparser = None
+ self._cli_values = {}
+ self.__cache = {}
+ self._config_opts = []
+ self._disable_interspersed_args = False
- :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
- """
+ def _setup(self, project, prog, version, usage, default_config_files):
+ """Initialize a ConfigOpts object for option parsing."""
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._oparser = optparse.OptionParser(prog=prog,
+ version=version,
+ usage=usage)
+ if self._disable_interspersed_args:
+ self._oparser.disable_interspersed_args()
+
+ self._config_opts = [
+ MultiStrOpt('config-file',
+ default=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' % (default_config_files, )),
+ StrOpt('config-dir',
+ metavar='DIR',
+ help='Path to a config directory to pull *.conf '
+ 'files from. This file set is sorted, so as to '
+ 'provide a predictable parse order if individual '
+ 'options are over-ridden. The set is parsed after '
+ 'the file(s), if any, specified via --config-file, '
+ 'hence over-ridden options in the directory take '
+ 'precedence.'),
+ ]
+ self.register_cli_opts(self._config_opts)
+
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.__cache = {}
-
- 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 __clear_cache(f):
@functools.wraps(f)
def __inner(self, *args, **kwargs):
return __inner
- def __call__(self, args=None):
+ def __call__(self,
+ args=None,
+ project=None,
+ prog=None,
+ version=None,
+ usage=None,
+ default_config_files=None):
"""Parse command line arguments and config files.
Calling a ConfigOpts object causes the supplied command line arguments
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:])
+ Automatically registers the --config-file option with either a supplied
+ list of default config files, or a list from find_config_files().
+
+ If the --config-dir option is set, any *.conf files from this
+ directory are pulled in, after all the file(s) specified by the
+ --config-file option.
+
+ :param args: command line arguments (defaults to sys.argv[1:])
+ :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
:returns: the list of arguments left over after parsing options
- :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError
+ :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError,
+ RequiredOptError, DuplicateOptError
"""
- self.reset()
+ self.clear()
- self._args = args
+ self._setup(project, prog, version, usage, default_config_files)
- (values, args) = self._oparser.parse_args(self._args)
+ self._cli_values, leftovers = self._parse_cli_opts(args)
- self._cli_values = vars(values)
+ self._parse_config_files()
- if self.config_file:
- self._parse_config_files(self.config_file)
+ self._check_required_opts()
- return args
+ return leftovers
def __getattr__(self, name):
"""Look up an option value and perform string substitution.
"""Return the number of options and option groups."""
return len(self._opts) + len(self._groups)
- @__clear_cache
def reset(self):
- """Reset the state of the object to before it was called."""
+ """Clear the object state and unset overrides and defaults."""
+ self._unset_defaults_and_overrides()
+ self.clear()
+
+ @__clear_cache
+ def clear(self):
+ """Clear the state of the object to before it was called."""
self._args = None
- self._cli_values = None
+ self._cli_values.clear()
+ self._oparser = None
self._cparser = None
+ self.unregister_opts(self._config_opts)
+ for group in self._groups.values():
+ group._clear()
@__clear_cache
def register_opt(self, opt, group=None):
:raises: DuplicateOptError
"""
if group is not None:
- return self._get_group(group)._register_opt(opt)
+ return self._get_group(group, autocreate=True)._register_opt(opt)
if _is_opt_registered(self._opts, opt):
return False
if self._args is not None:
raise ArgsAlreadyParsedError("cannot register CLI option")
- if not self.register_opt(opt, group, clear_cache=False):
- return False
-
- if group is not None:
- group = self._get_group(group)
-
- opt._add_to_cli(self._oparser, group)
-
- return True
+ return self.register_opt(opt, group, clear_cache=False)
@__clear_cache
def register_cli_opts(self, opts, group=None):
self._groups[group.name] = copy.copy(group)
+ @__clear_cache
+ def unregister_opt(self, opt, group=None):
+ """Unregister an option.
+
+ :param opt: an Opt object
+ :param group: an optional OptGroup object or group name
+ :raises: ArgsAlreadyParsedError, NoSuchGroupError
+ """
+ if self._args is not None:
+ raise ArgsAlreadyParsedError("reset before unregistering options")
+
+ if group is not None:
+ self._get_group(group)._unregister_opt(opt)
+ elif opt.dest in self._opts:
+ del self._opts[opt.dest]
+
+ @__clear_cache
+ def unregister_opts(self, opts, group=None):
+ """Unregister multiple CLI option schemas at once."""
+ for opt in opts:
+ self.unregister_opt(opt, group, clear_cache=False)
+
@__clear_cache
def set_override(self, name, override, group=None):
"""Override an opt value.
opt_info = self._get_opt_info(name, group)
opt_info['default'] = default
+ def _all_opt_infos(self):
+ """A generator function for iteration opt infos."""
+ for info in self._opts.values():
+ yield info, None
+ for group in self._groups.values():
+ for info in group._opts.values():
+ yield info, group
+
+ def _all_opts(self):
+ """A generator function for iteration opts."""
+ for info, group in self._all_opt_infos():
+ yield info['opt'], group
+
+ def _unset_defaults_and_overrides(self):
+ """Unset any default or override on all options."""
+ for info, group in self._all_opt_infos():
+ info['default'] = None
+ info['override'] = None
+
def disable_interspersed_args(self):
"""Set parsing to stop on the first non-option.
i.e. argument parsing is stopped at the first non-option argument.
"""
- self._oparser.disable_interspersed_args()
+ self._disable_interspersed_args = True
def enable_interspersed_args(self):
"""Set parsing to not stop on the first non-option.
This it the default behaviour."""
- self._oparser.enable_interspersed_args()
+ self._disable_interspersed_args = False
+
+ def find_file(self, name):
+ """Locate a file located alongside the config files.
+
+ Search for a file with the supplied basename in the directories
+ which we have already loaded config files from and other known
+ configuration directories.
+
+ The directory, if any, supplied by the config_dir option is
+ searched first. Then the config_file option is iterated over
+ and each of the base directories of the config_files values
+ are searched. Failing both of these, the standard directories
+ searched by the module level find_config_files() function is
+ used. The first matching file is returned.
+
+ :param basename: the filename, e.g. 'policy.json'
+ :returns: the path to a matching file, or None
+ """
+ dirs = []
+ if self.config_dir:
+ dirs.append(self.config_dir)
+
+ for cf in reversed(self.config_file):
+ dirs.append(os.path.dirname(cf))
+
+ dirs.extend(_get_config_dirs(self.project))
+
+ return _search_dirs(dirs, name)
def log_opt_values(self, logger, lvl):
"""Log the value of all registered opts.
return self.GroupAttr(self, self._get_group(name))
info = self._get_opt_info(name, group)
- default, opt, override = map(lambda k: info[k], sorted(info.keys()))
+ default, opt, override = [info[k] for k in sorted(info.keys())]
if override is not None:
return override
else:
return value
- def _get_group(self, group_or_name):
+ def _get_group(self, group_or_name, autocreate=False):
"""Looks up a OptGroup object.
Helper function to return an OptGroup given a parameter which can
the API have access to.
:param group_or_name: the group's name or the OptGroup object itself
+ :param autocreate: whether to auto-create the group if it's not found
:raises: NoSuchGroupError
"""
- if isinstance(group_or_name, OptGroup):
- group_name = group_or_name.name
- else:
- group_name = group_or_name
+ group = group_or_name if isinstance(group_or_name, OptGroup) else None
+ group_name = group.name if group else group_or_name
if not group_name in self._groups:
- raise NoSuchGroupError(group_name)
+ if not group is None or not autocreate:
+ raise NoSuchGroupError(group_name)
+
+ self.register_group(OptGroup(name=group_name))
return self._groups[group_name]
return opts[opt_name]
- def _parse_config_files(self, config_files):
- """Parse the supplied configuration files.
+ def _parse_config_files(self):
+ """Parse the config files from --config-file and --config-dir.
:raises: ConfigFilesNotFoundError, ConfigFileParseError
"""
+ config_files = list(self.config_file)
+
+ if self.config_dir:
+ config_dir_glob = os.path.join(self.config_dir, '*.conf')
+ config_files += sorted(glob.glob(config_dir_glob))
+
self._cparser = MultiConfigParser()
try:
not_read_ok = filter(lambda f: f not in read_ok, config_files)
raise ConfigFilesNotFoundError(not_read_ok)
+ def _check_required_opts(self):
+ """Check that all opts marked as required have values specified.
+
+ :raises: RequiredOptError
+ """
+ for info, group in self._all_opt_infos():
+ default, opt, override = [info[k] for k in sorted(info.keys())]
+
+ if opt.required:
+ if (default is not None or
+ override is not None):
+ continue
+
+ if self._get(opt.name, group) is None:
+ raise RequiredOptError(opt.name, group)
+
+ def _parse_cli_opts(self, args):
+ """Parse command line options.
+
+ Initializes the command line option parser and parses the supplied
+ command line arguments.
+
+ :param args: the command line arguments
+ :returns: a dict of parsed option values
+ :raises: SystemExit, DuplicateOptError
+
+ """
+ self._args = args
+
+ for opt, group in self._all_opts():
+ opt._add_to_cli(self._oparser, group)
+
+ values, leftovers = self._oparser.parse_args(args)
+
+ return vars(values), leftovers
+
class GroupAttr(collections.Mapping):
"""
help='syslog facility to receive log lines')
]
- def __init__(self, **kwargs):
- super(CommonConfigOpts, self).__init__(**kwargs)
+ def __init__(self):
+ super(CommonConfigOpts, self).__init__()
self.register_cli_opts(self.common_cli_opts)
self.register_cli_opts(self.logging_cli_opts)
+
+
+CONF = CommonConfigOpts()