diff --git a/bin/nova-clear-rabbit-queues b/bin/nova-clear-rabbit-queues index cf595fecc158..618aa45871b0 100755 --- a/bin/nova-clear-rabbit-queues +++ b/bin/nova-clear-rabbit-queues @@ -48,12 +48,18 @@ from nova.openstack.common import log as logging from nova.openstack.common import rpc -delete_exchange_opt = cfg.BoolOpt('delete_exchange', - default=False, - help='delete nova exchange too.') +opts = [ + cfg.MultiStrOpt('queues', + default=[], + positional=True, + help='Queues to delete'), + cfg.BoolOpt('delete_exchange', + default=False, + help='delete nova exchange too.'), +] CONF = cfg.CONF -CONF.register_cli_opt(delete_exchange_opt) +CONF.register_cli_opts(opts) def delete_exchange(exch): @@ -69,8 +75,8 @@ def delete_queues(queues): x.queue_delete(q) if __name__ == '__main__': - args = config.parse_args(sys.argv) + config.parse_args(sys.argv) logging.setup("nova") - delete_queues(args[1:]) + delete_queues(CONF.queues) if CONF.delete_exchange: delete_exchange(CONF.control_exchange) diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge index 0e3d89e65d27..66f6ba6a33b5 100755 --- a/bin/nova-dhcpbridge +++ b/bin/nova-dhcpbridge @@ -93,28 +93,44 @@ def init_leases(network_id): return network_manager.get_dhcp_leases(ctxt, network_ref) +def add_action_parsers(subparsers): + parser = subparsers.add_parser('init') + + for action in ['add', 'del', 'old']: + parser = subparsers.add_parser(action) + parser.add_argument('mac') + parser.add_argument('ip') + parser.set_defaults(func=globals()[action + '_lease']) + + +CONF.register_cli_opt( + cfg.SubCommandOpt('action', + title='Action options', + help='Available dhcpbridge options', + handler=add_action_parsers)) + + def main(): """Parse environment and arguments and call the approproate action.""" try: config_file = os.environ['CONFIG_FILE'] except KeyError: config_file = os.environ['FLAGFILE'] - argv = config.parse_args(sys.argv, default_config_files=[config_file]) + + config.parse_args(sys.argv, default_config_files=[config_file]) + logging.setup("nova") if int(os.environ.get('TESTING', '0')): from nova.tests import conf_fixture - action = argv[1] - if action in ['add', 'del', 'old']: - mac = argv[2] - ip = argv[3] + if CONF.action.name in ['add', 'del', 'old']: msg = (_("Called '%(action)s' for mac '%(mac)s' with ip '%(ip)s'") % - {"action": action, - "mac": mac, - "ip": ip}) + {"action": CONF.action.name, + "mac": CONF.action.mac, + "ip": CONF.action.ip}) LOG.debug(msg) - globals()[action + '_lease'](mac, ip) + CONF.action.func(CONF.action.mac, CONF.action.ip) else: try: network_id = int(os.environ.get('NETWORK_ID')) diff --git a/bin/nova-manage b/bin/nova-manage index 1d2e297d236e..c761ff6cc7b2 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -56,7 +56,6 @@ import gettext import netaddr -import optparse import os import sys @@ -107,7 +106,7 @@ QUOTAS = quota.QUOTAS # Decorators for actions def args(*args, **kwargs): def _decorator(func): - func.__dict__.setdefault('options', []).insert(0, (args, kwargs)) + func.__dict__.setdefault('args', []).insert(0, (args, kwargs)) return func return _decorator @@ -770,21 +769,6 @@ class DbCommands(object): print migration.db_version() -class VersionCommands(object): - """Class for exposing the codebase version.""" - - def __init__(self): - pass - - def list(self): - print (_("%(version)s (%(vcs)s)") % - {'version': version.version_string(), - 'vcs': version.version_string_with_vcs()}) - - def __call__(self): - self.list() - - class InstanceTypeCommands(object): """Class for managing instance types / flavors.""" @@ -988,10 +972,10 @@ class GetLogCommands(object): def errors(self): """Get all of the errors from the log files""" error_found = 0 - if CONF.logdir: - logs = [x for x in os.listdir(CONF.logdir) if x.endswith('.log')] + if CONF.log_dir: + logs = [x for x in os.listdir(CONF.log_dir) if x.endswith('.log')] for file in logs: - log_file = os.path.join(CONF.logdir, file) + log_file = os.path.join(CONF.log_dir, file) lines = [line.strip() for line in open(log_file, "r")] lines.reverse() print_name = 0 @@ -1032,45 +1016,23 @@ class GetLogCommands(object): print _('No nova entries in syslog!') -CATEGORIES = [ - ('account', AccountCommands), - ('agent', AgentBuildCommands), - ('db', DbCommands), - ('fixed', FixedIpCommands), - ('flavor', InstanceTypeCommands), - ('floating', FloatingIpCommands), - ('host', HostCommands), - ('instance_type', InstanceTypeCommands), - ('logs', GetLogCommands), - ('network', NetworkCommands), - ('project', ProjectCommands), - ('service', ServiceCommands), - ('shell', ShellCommands), - ('version', VersionCommands), - ('vm', VmCommands), - ('vpn', VpnCommands), -] - - -def lazy_match(name, key_value_tuples): - """Finds all objects that have a key that case insensitively contains - [name] key_value_tuples is a list of tuples of the form (key, value) - returns a list of tuples of the form (key, value)""" - result = [] - for (k, v) in key_value_tuples: - if k.lower().find(name.lower()) == 0: - result.append((k, v)) - if len(result) == 0: - print _('%s does not match any options:') % name - for k, _v in key_value_tuples: - print "\t%s" % k - sys.exit(2) - if len(result) > 1: - print _('%s matched multiple options:') % name - for k, _v in result: - print "\t%s" % k - sys.exit(2) - return result +CATEGORIES = { + 'account': AccountCommands, + 'agent': AgentBuildCommands, + 'db': DbCommands, + 'fixed': FixedIpCommands, + 'flavor': InstanceTypeCommands, + 'floating': FloatingIpCommands, + 'host': HostCommands, + 'instance_type': InstanceTypeCommands, + 'logs': GetLogCommands, + 'network': NetworkCommands, + 'project': ProjectCommands, + 'service': ServiceCommands, + 'shell': ShellCommands, + 'vm': VmCommands, + 'vpn': VpnCommands, +} def methods_of(obj): @@ -1083,11 +1045,46 @@ def methods_of(obj): return result +def add_command_parsers(subparsers): + parser = subparsers.add_parser('version') + + parser = subparsers.add_parser('bash-completion') + parser.add_argument('query_category', nargs='?') + + for category in CATEGORIES: + command_object = CATEGORIES[category]() + + parser = subparsers.add_parser(category) + parser.set_defaults(command_object=command_object) + + category_subparsers = parser.add_subparsers(dest='action') + + for (action, action_fn) in methods_of(command_object): + parser = category_subparsers.add_parser(action) + + action_kwargs = [] + for args, kwargs in getattr(action_fn, 'args', []): + action_kwargs.append(kwargs['dest']) + kwargs['dest'] = 'action_kwarg_' + kwargs['dest'] + parser.add_argument(*args, **kwargs) + + parser.set_defaults(action_fn=action_fn) + parser.set_defaults(action_kwargs=action_kwargs) + + parser.add_argument('action_args', nargs='*') + + +category_opt = cfg.SubCommandOpt('category', + title='Command categories', + help='Available categories', + handler=add_command_parsers) + + def main(): """Parse options and call the appropriate class/method.""" - + CONF.register_cli_opt(category_opt) try: - argv = config.parse_args(sys.argv) + config.parse_args(sys.argv) logging.setup("nova") except cfg.ConfigFilesNotFoundError: cfgfile = CONF.config_file[-1] if CONF.config_file else None @@ -1102,68 +1099,32 @@ def main(): print _('Please re-run nova-manage as root.') sys.exit(2) - script_name = argv.pop(0) - if len(argv) < 1: - print (_("\nOpenStack Nova version: %(version)s (%(vcs)s)\n") % + if CONF.category.name == "version": + print (_("%(version)s (%(vcs)s)") % {'version': version.version_string(), 'vcs': version.version_string_with_vcs()}) - print script_name + " category action [<args>]" - print _("Available categories:") - for k, _v in CATEGORIES: - print "\t%s" % k - sys.exit(2) - category = argv.pop(0) - if category == "bash-completion": - if len(argv) < 1: - print " ".join([k for (k, v) in CATEGORIES]) - else: - query_category = argv.pop(0) - matches = lazy_match(query_category, CATEGORIES) - # instantiate the command group object - category, fn = matches[0] + sys.exit(0) + + if CONF.category.name == "bash-completion": + if not CONF.category.query_category: + print " ".join(CATEGORIES.keys()) + elif CONF.category.query_category in CATEGORIES: + fn = CATEGORIES[CONF.category.query_category] command_object = fn() actions = methods_of(command_object) print " ".join([k for (k, v) in actions]) sys.exit(0) - matches = lazy_match(category, CATEGORIES) - # instantiate the command group object - category, fn = matches[0] - command_object = fn() - actions = methods_of(command_object) - if len(argv) < 1: - if hasattr(command_object, '__call__'): - action = '' - fn = command_object.__call__ - else: - print script_name + " category action [<args>]" - print _("Available actions for %s category:") % category - for k, _v in actions: - print "\t%s" % k - sys.exit(2) - else: - action = argv.pop(0) - matches = lazy_match(action, actions) - action, fn = matches[0] - # For not decorated methods - options = getattr(fn, 'options', []) - - usage = "%%prog %s %s <args> [options]" % (category, action) - parser = optparse.OptionParser(usage=usage) - for ar, kw in options: - parser.add_option(*ar, **kw) - (opts, fn_args) = parser.parse_args(argv) - fn_kwargs = vars(opts) - - for k, v in fn_kwargs.items(): + fn = CONF.category.action_fn + fn_args = [arg.decode('utf-8') for arg in CONF.category.action_args] + fn_kwargs = {} + for k in CONF.category.action_kwargs: + v = getattr(CONF.category, 'action_kwarg_' + k) if v is None: - del fn_kwargs[k] - elif isinstance(v, basestring): - fn_kwargs[k] = v.decode('utf-8') - else: - fn_kwargs[k] = v - - fn_args = [arg.decode('utf-8') for arg in fn_args] + continue + if isinstance(v, basestring): + v = v.decode('utf-8') + fn_kwargs[k] = v # call the action with the remaining arguments # check arguments diff --git a/nova/config.py b/nova/config.py index 55439f7ed98c..52bac65ad0f4 100644 --- a/nova/config.py +++ b/nova/config.py @@ -225,7 +225,6 @@ cfg.CONF.register_opts(global_opts) def parse_args(argv, default_config_files=None): - cfg.CONF.disable_interspersed_args() - return argv[:1] + cfg.CONF(argv[1:], - project='nova', - default_config_files=default_config_files) + cfg.CONF(argv[1:], + project='nova', + default_config_files=default_config_files) diff --git a/nova/network/minidns.py b/nova/network/minidns.py index 435cca3b779b..11c59dcc94f1 100644 --- a/nova/network/minidns.py +++ b/nova/network/minidns.py @@ -37,8 +37,8 @@ class MiniDNS(dns_driver.DNSDriver): A proper implementation will need some manner of locking.""" def __init__(self): - if CONF.logdir: - self.filename = os.path.join(CONF.logdir, "dnstest.txt") + if CONF.log_dir: + self.filename = os.path.join(CONF.log_dir, "dnstest.txt") self.tempdir = None else: self.tempdir = tempfile.mkdtemp() diff --git a/nova/openstack/common/cfg.py b/nova/openstack/common/cfg.py index 8775a5f8ace0..ad1f2a8a69e1 100644 --- a/nova/openstack/common/cfg.py +++ b/nova/openstack/common/cfg.py @@ -205,27 +205,11 @@ Option values may reference other values using PEP 292 string substitution:: Note that interpolation can be avoided by using '$$'. -For command line utilities that dispatch to other command line utilities, the -disable_interspersed_args() method is available. If this this method is called, -then parsing e.g.:: - - script --verbose cmd --debug /tmp/mything - -will no longer return:: - - ['cmd', '/tmp/mything'] - -as the leftover arguments, but will instead return:: - - ['cmd', '--debug', '/tmp/mything'] - -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: +log files:: opts = [ cfg.StrOpt('s3_store_access_key', secret=True), @@ -234,28 +218,50 @@ log files: ] This module also contains a global instance of the CommonConfigOpts class -in order to support a common usage pattern in OpenStack: +in order to support a common usage pattern in OpenStack:: - from nova.openstack.common import cfg + from nova.openstack.common import cfg - opts = [ - cfg.StrOpt('bind_host', default='0.0.0.0'), - cfg.IntOpt('bind_port', default=9292), - ] + opts = [ + cfg.StrOpt('bind_host', default='0.0.0.0'), + cfg.IntOpt('bind_port', default=9292), + ] - CONF = cfg.CONF - CONF.register_opts(opts) + CONF = cfg.CONF + CONF.register_opts(opts) - def start(server, app): - server.start(app, CONF.bind_port, CONF.bind_host) + def start(server, app): + server.start(app, CONF.bind_port, CONF.bind_host) + +Positional command line arguments are supported via a 'positional' Opt +constructor argument:: + + >>> CONF.register_cli_opt(MultiStrOpt('bar', positional=True)) + True + >>> CONF(['a', 'b']) + >>> CONF.bar + ['a', 'b'] + +It is also possible to use argparse "sub-parsers" to parse additional +command line arguments using the SubCommandOpt class: + + >>> def add_parsers(subparsers): + ... list_action = subparsers.add_parser('list') + ... list_action.add_argument('id') + ... + >>> CONF.register_cli_opt(SubCommandOpt('action', handler=add_parsers)) + True + >>> CONF(['list', '10']) + >>> CONF.action.name, CONF.action.id + ('list', '10') """ +import argparse import collections import copy import functools import glob -import optparse import os import string import sys @@ -474,6 +480,13 @@ def _is_opt_registered(opts, opt): return False +def set_defaults(opts, **kwargs): + for opt in opts: + if opt.dest in kwargs: + opt.default = kwargs[opt.dest] + break + + class Opt(object): """Base class for all configuration options. @@ -489,6 +502,8 @@ class Opt(object): a single character CLI option name default: the default value of the option + positional: + True if the option is a positional CLI argument metavar: the name shown as the argument to a CLI option in --help output help: @@ -497,8 +512,8 @@ class Opt(object): multi = False def __init__(self, name, dest=None, short=None, default=None, - metavar=None, help=None, secret=False, required=False, - deprecated_name=None): + positional=False, metavar=None, help=None, + secret=False, required=False, deprecated_name=None): """Construct an Opt object. The only required parameter is the option's name. However, it is @@ -508,6 +523,7 @@ class Opt(object): :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 positional: True if the option is a positional CLI argument :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 @@ -521,6 +537,7 @@ class Opt(object): self.dest = dest self.short = short self.default = default + self.positional = positional self.metavar = metavar self.help = help self.secret = secret @@ -561,64 +578,73 @@ class Opt(object): :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, - self.deprecated_name) + container = self._get_argparse_container(parser, group) + kwargs = self._get_argparse_kwargs(group) + prefix = self._get_argparse_prefix('', group) + self._add_to_argparse(container, self.name, self.short, kwargs, prefix, + self.positional, self.deprecated_name) - def _add_to_optparse(self, container, name, short, kwargs, prefix='', - deprecated_name=None): - """Add an option to an optparse parser or group. + def _add_to_argparse(self, container, name, short, kwargs, prefix='', + positional=False, deprecated_name=None): + """Add an option to an argparse parser or group. - :param container: an optparse.OptionContainer object + :param container: an argparse._ArgumentGroup object :param name: the opt name :param short: the short opt name - :param kwargs: the keyword arguments for add_option() + :param kwargs: the keyword arguments for add_argument() :param prefix: an optional prefix to prepend to the opt name + :param position: whether the optional is a positional CLI argument :raises: DuplicateOptError if a naming confict is detected """ - args = ['--' + prefix + name] + def hyphen(arg): + return arg if not positional else '' + + args = [hyphen('--') + prefix + name] if short: - args += ['-' + short] + args.append(hyphen('-') + short) if deprecated_name: - args += ['--' + prefix + deprecated_name] - for a in args: - if container.has_option(a): - raise DuplicateOptError(a) - container.add_option(*args, **kwargs) + args.append(hyphen('--') + prefix + deprecated_name) - def _get_optparse_container(self, parser, group): - """Returns an optparse.OptionContainer. + try: + container.add_argument(*args, **kwargs) + except argparse.ArgumentError as e: + raise DuplicateOptError(e) - :param parser: an optparse.OptionParser + def _get_argparse_container(self, parser, group): + """Returns an argparse._ArgumentGroup. + + :param parser: an argparse.ArgumentParser :param group: an (optional) OptGroup object - :returns: an optparse.OptionGroup if a group is given, else the parser + :returns: an argparse._ArgumentGroup if group is given, else parser """ if group is not None: - return group._get_optparse_group(parser) + return group._get_argparse_group(parser) else: return parser - def _get_optparse_kwargs(self, group, **kwargs): - """Build a dict of keyword arguments for optparse's add_option(). + def _get_argparse_kwargs(self, group, **kwargs): + """Build a dict of keyword arguments for argparse's add_argument(). Most opt types extend this method to customize the behaviour of the - options added to optparse. + options added to argparse. :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, + if not self.positional: + dest = self.dest + if group is not None: + dest = group.name + '_' + dest + kwargs['dest'] = dest + else: + kwargs['nargs'] = '?' + kwargs.update({'default': None, 'metavar': self.metavar, 'help': self.help, }) return kwargs - def _get_optparse_prefix(self, prefix, group): + def _get_argparse_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 @@ -656,6 +682,11 @@ class BoolOpt(Opt): _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, '0': False, 'no': False, 'false': False, 'off': False} + def __init__(self, *args, **kwargs): + if 'positional' in kwargs: + raise ValueError('positional boolean args not supported') + super(BoolOpt, self).__init__(*args, **kwargs) + def _get_from_config_parser(self, cparser, section): """Retrieve the opt value as a boolean from ConfigParser.""" def convert_bool(v): @@ -671,21 +702,32 @@ class BoolOpt(Opt): 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) + self._add_inverse_to_argparse(parser, group) - def _add_inverse_to_optparse(self, parser, group): + def _add_inverse_to_argparse(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) + container = self._get_argparse_container(parser, group) + kwargs = self._get_argparse_kwargs(group, action='store_false') + prefix = self._get_argparse_prefix('no', group) kwargs["help"] = "The inverse of --" + self.name - self._add_to_optparse(container, self.name, None, kwargs, prefix, - self.deprecated_name) + self._add_to_argparse(container, self.name, None, kwargs, prefix, + self.positional, self.deprecated_name) - 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) + def _get_argparse_kwargs(self, group, action='store_true', **kwargs): + """Extends the base argparse keyword dict for boolean options.""" + + kwargs = super(BoolOpt, self)._get_argparse_kwargs(group, **kwargs) + + # metavar has no effect for BoolOpt + if 'metavar' in kwargs: + del kwargs['metavar'] + + if action != 'store_true': + action = 'store_false' + + kwargs['action'] = action + + return kwargs class IntOpt(Opt): @@ -697,10 +739,10 @@ class IntOpt(Opt): return [int(v) for v in self._cparser_get_with_deprecated(cparser, section)] - def _get_optparse_kwargs(self, group, **kwargs): - """Extends the base optparse keyword dict for integer options.""" + def _get_argparse_kwargs(self, group, **kwargs): + """Extends the base argparse keyword dict for integer options.""" return super(IntOpt, - self)._get_optparse_kwargs(group, type='int', **kwargs) + self)._get_argparse_kwargs(group, type=int, **kwargs) class FloatOpt(Opt): @@ -712,10 +754,10 @@ class FloatOpt(Opt): return [float(v) for v in self._cparser_get_with_deprecated(cparser, section)] - 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) + def _get_argparse_kwargs(self, group, **kwargs): + """Extends the base argparse keyword dict for float options.""" + return super(FloatOpt, self)._get_argparse_kwargs(group, + type=float, **kwargs) class ListOpt(Opt): @@ -725,23 +767,26 @@ class ListOpt(Opt): is a list containing these strings. """ + class _StoreListAction(argparse.Action): + """ + An argparse action for parsing an option value into a list. + """ + def __call__(self, parser, namespace, values, option_string=None): + if values is not None: + values = [a.strip() for a in values.split(',')] + setattr(namespace, self.dest, values) + def _get_from_config_parser(self, cparser, section): """Retrieve the opt value as a list from ConfigParser.""" - return [v.split(',') for v in + return [[a.strip() for a in v.split(',')] for v in self._cparser_get_with_deprecated(cparser, section)] - 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(',')) + def _get_argparse_kwargs(self, group, **kwargs): + """Extends the base argparse keyword dict for list options.""" + return Opt._get_argparse_kwargs(self, + group, + action=ListOpt._StoreListAction, + **kwargs) class MultiStrOpt(Opt): @@ -752,10 +797,14 @@ class MultiStrOpt(Opt): """ multi = True - 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') + def _get_argparse_kwargs(self, group, **kwargs): + """Extends the base argparse keyword dict for multi str options.""" + kwargs = super(MultiStrOpt, self)._get_argparse_kwargs(group) + if not self.positional: + kwargs['action'] = 'append' + else: + kwargs['nargs'] = '*' + return kwargs def _cparser_get_with_deprecated(self, cparser, section): """If cannot find option as dest try deprecated_name alias.""" @@ -765,6 +814,57 @@ class MultiStrOpt(Opt): return cparser.get(section, [self.dest], multi=True) +class SubCommandOpt(Opt): + + """ + Sub-command options allow argparse sub-parsers to be used to parse + additional command line arguments. + + The handler argument to the SubCommandOpt contructor is a callable + which is supplied an argparse subparsers object. Use this handler + callable to add sub-parsers. + + The opt value is SubCommandAttr object with the name of the chosen + sub-parser stored in the 'name' attribute and the values of other + sub-parser arguments available as additional attributes. + """ + + def __init__(self, name, dest=None, handler=None, + title=None, description=None, help=None): + """Construct an sub-command parsing option. + + This behaves similarly to other Opt sub-classes but adds a + 'handler' argument. The handler is a callable which is supplied + an subparsers object when invoked. The add_parser() method on + this subparsers object can be used to register parsers for + sub-commands. + + :param name: the option's name + :param dest: the name of the corresponding ConfigOpts property + :param title: title of the sub-commands group in help output + :param description: description of the group in help output + :param help: a help string giving an overview of available sub-commands + """ + super(SubCommandOpt, self).__init__(name, dest=dest, help=help) + self.handler = handler + self.title = title + self.description = description + + def _add_to_cli(self, parser, group=None): + """Add argparse sub-parsers and invoke the handler method.""" + dest = self.dest + if group is not None: + dest = group.name + '_' + dest + + subparsers = parser.add_subparsers(dest=dest, + title=self.title, + description=self.description, + help=self.help) + + if not self.handler is None: + self.handler(subparsers) + + class OptGroup(object): """ @@ -800,19 +900,20 @@ class OptGroup(object): self.help = help self._opts = {} # dict of dicts of (opt:, override:, default:) - self._optparse_group = None + self._argparse_group = None - def _register_opt(self, opt): + def _register_opt(self, opt, cli=False): """Add an opt to this group. :param opt: an Opt object + :param cli: whether this is a CLI option :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} + self._opts[opt.dest] = {'opt': opt, 'cli': cli} return True @@ -824,16 +925,16 @@ class OptGroup(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._optparse_group = optparse.OptionGroup(parser, self.title, - self.help) - return self._optparse_group + def _get_argparse_group(self, parser): + if self._argparse_group is None: + """Build an argparse._ArgumentGroup for this group.""" + self._argparse_group = parser.add_argument_group(self.title, + self.help) + return self._argparse_group def _clear(self): """Clear this group's option parsing state.""" - self._optparse_group = None + self._argparse_group = None class ParseError(iniparser.ParseError): @@ -928,26 +1029,31 @@ class ConfigOpts(collections.Mapping): self._groups = {} self._args = None + self._oparser = None self._cparser = None self._cli_values = {} self.__cache = {} self._config_opts = [] - self._disable_interspersed_args = False - def _setup(self, project, prog, version, usage, default_config_files): - """Initialize a ConfigOpts object for option parsing.""" + def _pre_setup(self, project, prog, version, usage, default_config_files): + """Initialize a ConfigCliParser 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._oparser = argparse.ArgumentParser(prog=prog, usage=usage) + self._oparser.add_argument('--version', + action='version', + version=version) + + return prog, default_config_files + + def _setup(self, project, prog, version, usage, default_config_files): + """Initialize a ConfigOpts object for option parsing.""" self._config_opts = [ MultiStrOpt('config-file', @@ -1017,18 +1123,23 @@ class ConfigOpts(collections.Mapping): :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError, RequiredOptError, DuplicateOptError """ + self.clear() + prog, default_config_files = self._pre_setup(project, + prog, + version, + usage, + default_config_files) + self._setup(project, prog, version, usage, default_config_files) - self._cli_values, leftovers = self._parse_cli_opts(args) + self._cli_values = self._parse_cli_opts(args) self._parse_config_files() self._check_required_opts() - return leftovers - def __getattr__(self, name): """Look up an option value and perform string substitution. @@ -1062,17 +1173,21 @@ class ConfigOpts(collections.Mapping): @__clear_cache def clear(self): - """Clear the state of the object to before it was called.""" + """Clear the state of the object to before it was called. + + Any subparsers added using the add_cli_subparsers() will also be + removed as a side-effect of this method. + """ self._args = None self._cli_values.clear() - self._oparser = None + self._oparser = argparse.ArgumentParser() 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): + def register_opt(self, opt, group=None, cli=False): """Register an option schema. Registering an option schema makes any option value which is previously @@ -1080,17 +1195,19 @@ class ConfigOpts(collections.Mapping): as an attribute of this object. :param opt: an instance of an Opt sub-class + :param cli: whether this is a CLI option :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, autocreate=True)._register_opt(opt) + group = self._get_group(group, autocreate=True) + return group._register_opt(opt, cli) if _is_opt_registered(self._opts, opt): return False - self._opts[opt.dest] = {'opt': opt} + self._opts[opt.dest] = {'opt': opt, 'cli': cli} return True @@ -1116,7 +1233,7 @@ class ConfigOpts(collections.Mapping): if self._args is not None: raise ArgsAlreadyParsedError("cannot register CLI option") - return self.register_opt(opt, group, clear_cache=False) + return self.register_opt(opt, group, cli=True, clear_cache=False) @__clear_cache def register_cli_opts(self, opts, group=None): @@ -1243,10 +1360,11 @@ class ConfigOpts(collections.Mapping): for info in group._opts.values(): yield info, group - def _all_opts(self): - """A generator function for iteration opts.""" + def _all_cli_opts(self): + """A generator function for iterating CLI opts.""" for info, group in self._all_opt_infos(): - yield info['opt'], group + if info['cli']: + yield info['opt'], group def _unset_defaults_and_overrides(self): """Unset any default or override on all options.""" @@ -1254,31 +1372,6 @@ class ConfigOpts(collections.Mapping): info.pop('default', None) info.pop('override', None) - def disable_interspersed_args(self): - """Set parsing to stop on the first non-option. - - If this this method is called, then parsing e.g. - - script --verbose cmd --debug /tmp/mything - - will no longer return: - - ['cmd', '/tmp/mything'] - - as the leftover arguments, but will instead return: - - ['cmd', '--debug', '/tmp/mything'] - - i.e. argument parsing is stopped at the first non-option argument. - """ - 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._disable_interspersed_args = False - def find_file(self, name): """Locate a file located alongside the config files. @@ -1377,6 +1470,9 @@ class ConfigOpts(collections.Mapping): info = self._get_opt_info(name, group) opt = info['opt'] + if isinstance(opt, SubCommandOpt): + return self.SubCommandAttr(self, group, opt.dest) + if 'override' in info: return info['override'] @@ -1401,6 +1497,10 @@ class ConfigOpts(collections.Mapping): if not opt.multi: return value + # argparse ignores default=None for nargs='*' + if opt.positional and not value: + value = opt.default + return value + values if values: @@ -1523,12 +1623,10 @@ class ConfigOpts(collections.Mapping): """ self._args = args - for opt, group in self._all_opts(): + for opt, group in self._all_cli_opts(): opt._add_to_cli(self._oparser, group) - values, leftovers = self._oparser.parse_args(args) - - return vars(values), leftovers + return vars(self._oparser.parse_args(args)) class GroupAttr(collections.Mapping): @@ -1543,12 +1641,12 @@ class ConfigOpts(collections.Mapping): :param conf: a ConfigOpts object :param group: an OptGroup object """ - self.conf = conf - self.group = group + self._conf = conf + self._group = group def __getattr__(self, name): """Look up an option value and perform template substitution.""" - return self.conf._get(name, self.group) + return self._conf._get(name, self._group) def __getitem__(self, key): """Look up an option value and perform string substitution.""" @@ -1556,16 +1654,50 @@ class ConfigOpts(collections.Mapping): def __contains__(self, key): """Return True if key is the name of a registered opt or group.""" - return key in self.group._opts + return key in self._group._opts def __iter__(self): """Iterate over all registered opt and group names.""" - for key in self.group._opts.keys(): + for key in self._group._opts.keys(): yield key def __len__(self): """Return the number of options and option groups.""" - return len(self.group._opts) + return len(self._group._opts) + + class SubCommandAttr(object): + + """ + A helper class representing the name and arguments of an argparse + sub-parser. + """ + + def __init__(self, conf, group, dest): + """Construct a SubCommandAttr object. + + :param conf: a ConfigOpts object + :param group: an OptGroup object + :param dest: the name of the sub-parser + """ + self._conf = conf + self._group = group + self._dest = dest + + def __getattr__(self, name): + """Look up a sub-parser name or argument value.""" + if name == 'name': + name = self._dest + if self._group is not None: + name = self._group.name + '_' + name + return self._conf._cli_values[name] + + if name in self._conf: + raise DuplicateOptError(name) + + try: + return self._conf._cli_values[name] + except KeyError: + raise NoSuchOptError(name) class StrSubWrapper(object): @@ -1623,19 +1755,21 @@ class CommonConfigOpts(ConfigOpts): metavar='FORMAT', help='A logging.Formatter log message format string which may ' 'use any of the available logging.LogRecord attributes. ' - 'Default: %default'), + 'Default: %(default)s'), StrOpt('log-date-format', default=DEFAULT_LOG_DATE_FORMAT, metavar='DATE_FORMAT', - help='Format string for %(asctime)s in log records. ' - 'Default: %default'), + help='Format string for %%(asctime)s in log records. ' + 'Default: %(default)s'), StrOpt('log-file', metavar='PATH', + deprecated_name='logfile', help='(Optional) Name of log file to output to. ' 'If not set, logging will go to stdout.'), StrOpt('log-dir', + deprecated_name='logdir', help='(Optional) The directory to keep log files in ' - '(will be prepended to --logfile)'), + '(will be prepended to --log-file)'), BoolOpt('use-syslog', default=False, help='Use syslog for logging.'), diff --git a/nova/openstack/common/log.py b/nova/openstack/common/log.py index 67a06a7af547..b0bcdf9e2699 100644 --- a/nova/openstack/common/log.py +++ b/nova/openstack/common/log.py @@ -95,12 +95,6 @@ log_opts = [ generic_log_opts = [ - cfg.StrOpt('logdir', - default=None, - help='Log output to a per-service log file in named directory'), - cfg.StrOpt('logfile', - default=None, - help='Log output to a named file'), cfg.BoolOpt('use_stderr', default=True, help='Log output to standard error'), @@ -148,18 +142,15 @@ def _get_binary_name(): def _get_log_file_path(binary=None): - logfile = CONF.log_file or CONF.logfile - logdir = CONF.log_dir or CONF.logdir + if CONF.log_file and not CONF.log_dir: + return CONF.log_file - if logfile and not logdir: - return logfile + if CONF.log_file and CONF.log_dir: + return os.path.join(CONF.log_dir, CONF.log_file) - if logfile and logdir: - return os.path.join(logdir, logfile) - - if logdir: + if CONF.log_dir: binary = binary or _get_binary_name() - return '%s.log' % (os.path.join(logdir, binary),) + return '%s.log' % (os.path.join(CONF.log_dir, binary),) class ContextAdapter(logging.LoggerAdapter): diff --git a/nova/tests/network/test_manager.py b/nova/tests/network/test_manager.py index 2338e6259eca..0e441eef87aa 100644 --- a/nova/tests/network/test_manager.py +++ b/nova/tests/network/test_manager.py @@ -140,7 +140,7 @@ class FlatNetworkTestCase(test.TestCase): def setUp(self): super(FlatNetworkTestCase, self).setUp() self.tempdir = tempfile.mkdtemp() - self.flags(logdir=self.tempdir) + self.flags(log_dir=self.tempdir) self.network = network_manager.FlatManager(host=HOST) self.network.instance_dns_domain = '' self.network.db = db @@ -1614,7 +1614,7 @@ class FloatingIPTestCase(test.TestCase): def setUp(self): super(FloatingIPTestCase, self).setUp() self.tempdir = tempfile.mkdtemp() - self.flags(logdir=self.tempdir) + self.flags(log_dir=self.tempdir) self.network = TestFloatingIPManager() self.network.db = db self.project_id = 'testproject' @@ -2004,7 +2004,7 @@ class InstanceDNSTestCase(test.TestCase): def setUp(self): super(InstanceDNSTestCase, self).setUp() self.tempdir = tempfile.mkdtemp() - self.flags(logdir=self.tempdir) + self.flags(log_dir=self.tempdir) self.network = TestFloatingIPManager() self.network.db = db self.project_id = 'testproject' diff --git a/tools/pip-requires b/tools/pip-requires index d10072546a23..4b11798fb6c0 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -2,6 +2,7 @@ SQLAlchemy>=0.7.8,<=0.7.9 Cheetah==2.4.4 amqplib>=0.6.1 anyjson>=0.2.4 +argparse boto eventlet>=0.9.17 kombu>=1.0.4