Port to argparse based cfg

Import latest cfg from oslo-incubator with these changes:

 Add deprecated --logdir common opt
 Add deprecated --logfile common opt.
 Allow nova and others to override some logging defaults
 Fixing the trim for ListOp when reading from config file
 Fix set_default() with boolean CLI options
 Improve cfg's argparse sub-parsers support
 Hide the GroupAttr conf and group attributes
 Fix regression with cfg CLI arguments
 Fix broken --help with CommonConfigOpts
 Fix ListOpt to trim whitespace
 updating sphinx documentation
 Don't reference argparse._StoreAction
 Fix minor coding style issue
 Remove ConfigCliParser class
 Add support for positional arguments
 Use stock argparse behaviour for optional args
 Use stock argparse --usage behaviour
 Use stock argparse --version behaviour
 Remove add_option() method
 Completely remove cfg's disable_interspersed_args()
 argparse support for cfg

The main cfg API change is that CONF() no longer returns the un-parsed
CLI arguments. To handle these args, you need to use the support for
positional arguments or sub-parsers.

Switching nova-manage to use sub-parser based CLI arguments means the
following changes in behaviour:

 - no more lazy matching of commands - e.g. 'nova-manage proj q' will
   no longer work. If we find out about common abbreviations used in
   peoples' scripts, we can easily add those.

 - the help output displayed if you run nova-manage without any args
   (or just a category) has changed

 - 'nova-manage version list' is no longer equivalent to
   'nova-manage version'

Change-Id: I19ef3a1c00e97af64d199e27cb1cdc5c63b46a82
This commit is contained in:
Mark McLoughlin 2012-11-26 07:21:03 +00:00
parent f354276930
commit 5e09b2cab7
9 changed files with 426 additions and 318 deletions

View File

@ -48,12 +48,18 @@ from nova.openstack.common import log as logging
from nova.openstack.common import rpc from nova.openstack.common import rpc
delete_exchange_opt = cfg.BoolOpt('delete_exchange', opts = [
cfg.MultiStrOpt('queues',
default=[],
positional=True,
help='Queues to delete'),
cfg.BoolOpt('delete_exchange',
default=False, default=False,
help='delete nova exchange too.') help='delete nova exchange too.'),
]
CONF = cfg.CONF CONF = cfg.CONF
CONF.register_cli_opt(delete_exchange_opt) CONF.register_cli_opts(opts)
def delete_exchange(exch): def delete_exchange(exch):
@ -69,8 +75,8 @@ def delete_queues(queues):
x.queue_delete(q) x.queue_delete(q)
if __name__ == '__main__': if __name__ == '__main__':
args = config.parse_args(sys.argv) config.parse_args(sys.argv)
logging.setup("nova") logging.setup("nova")
delete_queues(args[1:]) delete_queues(CONF.queues)
if CONF.delete_exchange: if CONF.delete_exchange:
delete_exchange(CONF.control_exchange) delete_exchange(CONF.control_exchange)

View File

@ -93,28 +93,44 @@ def init_leases(network_id):
return network_manager.get_dhcp_leases(ctxt, network_ref) 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(): def main():
"""Parse environment and arguments and call the approproate action.""" """Parse environment and arguments and call the approproate action."""
try: try:
config_file = os.environ['CONFIG_FILE'] config_file = os.environ['CONFIG_FILE']
except KeyError: except KeyError:
config_file = os.environ['FLAGFILE'] 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") logging.setup("nova")
if int(os.environ.get('TESTING', '0')): if int(os.environ.get('TESTING', '0')):
from nova.tests import fake_flags from nova.tests import fake_flags
action = argv[1] if CONF.action.name in ['add', 'del', 'old']:
if action in ['add', 'del', 'old']:
mac = argv[2]
ip = argv[3]
msg = (_("Called '%(action)s' for mac '%(mac)s' with ip '%(ip)s'") % msg = (_("Called '%(action)s' for mac '%(mac)s' with ip '%(ip)s'") %
{"action": action, {"action": CONF.action.name,
"mac": mac, "mac": CONF.action.mac,
"ip": ip}) "ip": CONF.action.ip})
LOG.debug(msg) LOG.debug(msg)
globals()[action + '_lease'](mac, ip) CONF.action.func(CONF.action.mac, CONF.action.ip)
else: else:
try: try:
network_id = int(os.environ.get('NETWORK_ID')) network_id = int(os.environ.get('NETWORK_ID'))

View File

@ -56,7 +56,6 @@
import gettext import gettext
import netaddr import netaddr
import optparse
import os import os
import sys import sys
@ -106,7 +105,7 @@ QUOTAS = quota.QUOTAS
# Decorators for actions # Decorators for actions
def args(*args, **kwargs): def args(*args, **kwargs):
def _decorator(func): def _decorator(func):
func.__dict__.setdefault('options', []).insert(0, (args, kwargs)) func.__dict__.setdefault('args', []).insert(0, (args, kwargs))
return func return func
return _decorator return _decorator
@ -764,21 +763,6 @@ class DbCommands(object):
print migration.db_version() 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 InstanceTypeCommands(object):
"""Class for managing instance types / flavors.""" """Class for managing instance types / flavors."""
@ -982,10 +966,10 @@ class GetLogCommands(object):
def errors(self): def errors(self):
"""Get all of the errors from the log files""" """Get all of the errors from the log files"""
error_found = 0 error_found = 0
if CONF.logdir: if CONF.log_dir:
logs = [x for x in os.listdir(CONF.logdir) if x.endswith('.log')] logs = [x for x in os.listdir(CONF.log_dir) if x.endswith('.log')]
for file in logs: 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 = [line.strip() for line in open(log_file, "r")]
lines.reverse() lines.reverse()
print_name = 0 print_name = 0
@ -1026,45 +1010,23 @@ class GetLogCommands(object):
print _('No nova entries in syslog!') print _('No nova entries in syslog!')
CATEGORIES = [ CATEGORIES = {
('account', AccountCommands), 'account': AccountCommands,
('agent', AgentBuildCommands), 'agent': AgentBuildCommands,
('db', DbCommands), 'db': DbCommands,
('fixed', FixedIpCommands), 'fixed': FixedIpCommands,
('flavor', InstanceTypeCommands), 'flavor': InstanceTypeCommands,
('floating', FloatingIpCommands), 'floating': FloatingIpCommands,
('host', HostCommands), 'host': HostCommands,
('instance_type', InstanceTypeCommands), 'instance_type': InstanceTypeCommands,
('logs', GetLogCommands), 'logs': GetLogCommands,
('network', NetworkCommands), 'network': NetworkCommands,
('project', ProjectCommands), 'project': ProjectCommands,
('service', ServiceCommands), 'service': ServiceCommands,
('shell', ShellCommands), 'shell': ShellCommands,
('version', VersionCommands), 'vm': VmCommands,
('vm', VmCommands), 'vpn': VpnCommands,
('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
def methods_of(obj): def methods_of(obj):
@ -1077,11 +1039,46 @@ def methods_of(obj):
return result 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(): def main():
"""Parse options and call the appropriate class/method.""" """Parse options and call the appropriate class/method."""
CONF.register_cli_opt(category_opt)
try: try:
argv = config.parse_args(sys.argv) config.parse_args(sys.argv)
logging.setup("nova") logging.setup("nova")
except cfg.ConfigFilesNotFoundError: except cfg.ConfigFilesNotFoundError:
cfgfile = CONF.config_file[-1] if CONF.config_file else None cfgfile = CONF.config_file[-1] if CONF.config_file else None
@ -1096,69 +1093,33 @@ def main():
print _('Please re-run nova-manage as root.') print _('Please re-run nova-manage as root.')
sys.exit(2) sys.exit(2)
script_name = argv.pop(0) if CONF.category.name == "version":
if len(argv) < 1: print (_("%(version)s (%(vcs)s)") %
print (_("\nOpenStack Nova version: %(version)s (%(vcs)s)\n") %
{'version': version.version_string(), {'version': version.version_string(),
'vcs': version.version_string_with_vcs()}) 'vcs': version.version_string_with_vcs()})
print script_name + " category action [<args>]" sys.exit(0)
print _("Available categories:")
for k, _v in CATEGORIES: if CONF.category.name == "bash-completion":
print "\t%s" % k if not CONF.category.query_category:
sys.exit(2) print " ".join(CATEGORIES.keys())
category = argv.pop(0) elif CONF.category.query_category in CATEGORIES:
if category == "bash-completion": fn = CATEGORIES[CONF.category.query_category]
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]
command_object = fn() command_object = fn()
actions = methods_of(command_object) actions = methods_of(command_object)
print " ".join([k for (k, v) in actions]) print " ".join([k for (k, v) in actions])
sys.exit(0) 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 fn = CONF.category.action_fn
options = getattr(fn, 'options', []) fn_args = [arg.decode('utf-8') for arg in CONF.category.action_args]
fn_kwargs = {}
usage = "%%prog %s %s <args> [options]" % (category, action) for k in CONF.category.action_kwargs:
parser = optparse.OptionParser(usage=usage) v = getattr(CONF.category, 'action_kwarg_' + k)
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():
if v is None: if v is None:
del fn_kwargs[k] continue
elif isinstance(v, basestring): if isinstance(v, basestring):
fn_kwargs[k] = v.decode('utf-8') v = v.decode('utf-8')
else:
fn_kwargs[k] = v fn_kwargs[k] = v
fn_args = [arg.decode('utf-8') for arg in fn_args]
# call the action with the remaining arguments # call the action with the remaining arguments
try: try:
fn(*fn_args, **fn_kwargs) fn(*fn_args, **fn_kwargs)

View File

@ -285,7 +285,6 @@ cfg.CONF.register_opts(global_opts)
def parse_args(argv, default_config_files=None): def parse_args(argv, default_config_files=None):
cfg.CONF.disable_interspersed_args() cfg.CONF(argv[1:],
return argv[:1] + cfg.CONF(argv[1:],
project='nova', project='nova',
default_config_files=default_config_files) default_config_files=default_config_files)

View File

@ -35,8 +35,8 @@ class MiniDNS(object):
A proper implementation will need some manner of locking.""" A proper implementation will need some manner of locking."""
def __init__(self): def __init__(self):
if CONF.logdir: if CONF.log_dir:
self.filename = os.path.join(CONF.logdir, "dnstest.txt") self.filename = os.path.join(CONF.log_dir, "dnstest.txt")
self.tempdir = None self.tempdir = None
else: else:
self.tempdir = tempfile.mkdtemp() self.tempdir = tempfile.mkdtemp()

View File

@ -205,27 +205,11 @@ Option values may reference other values using PEP 292 string substitution::
Note that interpolation can be avoided by using '$$'. 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 Options may be declared as required so that an error is raised if the user
does not supply a value for the option. does not supply a value for the option.
Options may be declared as secret so that their values are not leaked into Options may be declared as secret so that their values are not leaked into
log files: log files::
opts = [ opts = [
cfg.StrOpt('s3_store_access_key', secret=True), cfg.StrOpt('s3_store_access_key', secret=True),
@ -234,7 +218,7 @@ log files:
] ]
This module also contains a global instance of the CommonConfigOpts class 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
@ -249,13 +233,35 @@ in order to support a common usage pattern in OpenStack:
def start(server, app): def start(server, app):
server.start(app, CONF.bind_port, CONF.bind_host) 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 collections
import copy import copy
import functools import functools
import glob import glob
import optparse
import os import os
import string import string
import sys import sys
@ -474,6 +480,13 @@ def _is_opt_registered(opts, opt):
return False 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): class Opt(object):
"""Base class for all configuration options. """Base class for all configuration options.
@ -489,6 +502,8 @@ class Opt(object):
a single character CLI option name a single character CLI option name
default: default:
the default value of the option the default value of the option
positional:
True if the option is a positional CLI argument
metavar: metavar:
the name shown as the argument to a CLI option in --help output the name shown as the argument to a CLI option in --help output
help: help:
@ -497,8 +512,8 @@ class Opt(object):
multi = False multi = False
def __init__(self, name, dest=None, short=None, default=None, def __init__(self, name, dest=None, short=None, default=None,
metavar=None, help=None, secret=False, required=False, positional=False, metavar=None, help=None,
deprecated_name=None): secret=False, required=False, deprecated_name=None):
"""Construct an Opt object. """Construct an Opt object.
The only required parameter is the option's name. However, it is 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 dest: the name of the corresponding ConfigOpts property
:param short: a single character CLI option name :param short: a single character CLI option name
:param default: the default value of the option :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 metavar: the option argument to show in --help
:param help: an explanation of how the option is used :param help: an explanation of how the option is used
:param secret: true iff the value should be obfuscated in log output :param secret: true iff the value should be obfuscated in log output
@ -521,6 +537,7 @@ class Opt(object):
self.dest = dest self.dest = dest
self.short = short self.short = short
self.default = default self.default = default
self.positional = positional
self.metavar = metavar self.metavar = metavar
self.help = help self.help = help
self.secret = secret self.secret = secret
@ -561,64 +578,73 @@ class Opt(object):
:param parser: the CLI option parser :param parser: the CLI option parser
:param group: an optional OptGroup object :param group: an optional OptGroup object
""" """
container = self._get_optparse_container(parser, group) container = self._get_argparse_container(parser, group)
kwargs = self._get_optparse_kwargs(group) kwargs = self._get_argparse_kwargs(group)
prefix = self._get_optparse_prefix('', group) prefix = self._get_argparse_prefix('', group)
self._add_to_optparse(container, self.name, self.short, kwargs, prefix, self._add_to_argparse(container, self.name, self.short, kwargs, prefix,
self.deprecated_name) self.positional, self.deprecated_name)
def _add_to_optparse(self, container, name, short, kwargs, prefix='', def _add_to_argparse(self, container, name, short, kwargs, prefix='',
deprecated_name=None): positional=False, deprecated_name=None):
"""Add an option to an optparse parser or group. """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 name: the opt name
:param short: the short 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 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 :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: if short:
args += ['-' + short] args.append(hyphen('-') + short)
if deprecated_name: if deprecated_name:
args += ['--' + prefix + deprecated_name] args.append(hyphen('--') + prefix + deprecated_name)
for a in args:
if container.has_option(a):
raise DuplicateOptError(a)
container.add_option(*args, **kwargs)
def _get_optparse_container(self, parser, group): try:
"""Returns an optparse.OptionContainer. 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 :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: if group is not None:
return group._get_optparse_group(parser) return group._get_argparse_group(parser)
else: else:
return parser return parser
def _get_optparse_kwargs(self, group, **kwargs): def _get_argparse_kwargs(self, group, **kwargs):
"""Build a dict of keyword arguments for optparse's add_option(). """Build a dict of keyword arguments for argparse's add_argument().
Most opt types extend this method to customize the behaviour of the 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 group: an optional group
:param kwargs: optional keyword arguments to add to :param kwargs: optional keyword arguments to add to
:returns: a dict of keyword arguments :returns: a dict of keyword arguments
""" """
if not self.positional:
dest = self.dest dest = self.dest
if group is not None: if group is not None:
dest = group.name + '_' + dest dest = group.name + '_' + dest
kwargs.update({'dest': dest, kwargs['dest'] = dest
else:
kwargs['nargs'] = '?'
kwargs.update({'default': None,
'metavar': self.metavar, 'metavar': self.metavar,
'help': self.help, }) 'help': self.help, })
return kwargs 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. """Build a prefix for the CLI option name, if required.
CLI options in a group are prefixed with the group's name in order 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, _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False} '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): def _get_from_config_parser(self, cparser, section):
"""Retrieve the opt value as a boolean from ConfigParser.""" """Retrieve the opt value as a boolean from ConfigParser."""
def convert_bool(v): def convert_bool(v):
@ -671,21 +702,32 @@ class BoolOpt(Opt):
def _add_to_cli(self, parser, group=None): def _add_to_cli(self, parser, group=None):
"""Extends the base class method to add the --nooptname option.""" """Extends the base class method to add the --nooptname option."""
super(BoolOpt, self)._add_to_cli(parser, group) 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.""" """Add the --nooptname option to the option parser."""
container = self._get_optparse_container(parser, group) container = self._get_argparse_container(parser, group)
kwargs = self._get_optparse_kwargs(group, action='store_false') kwargs = self._get_argparse_kwargs(group, action='store_false')
prefix = self._get_optparse_prefix('no', group) prefix = self._get_argparse_prefix('no', group)
kwargs["help"] = "The inverse of --" + self.name kwargs["help"] = "The inverse of --" + self.name
self._add_to_optparse(container, self.name, None, kwargs, prefix, self._add_to_argparse(container, self.name, None, kwargs, prefix,
self.deprecated_name) self.positional, self.deprecated_name)
def _get_optparse_kwargs(self, group, action='store_true', **kwargs): def _get_argparse_kwargs(self, group, action='store_true', **kwargs):
"""Extends the base optparse keyword dict for boolean options.""" """Extends the base argparse keyword dict for boolean options."""
return super(BoolOpt,
self)._get_optparse_kwargs(group, action=action, **kwargs) 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): class IntOpt(Opt):
@ -697,10 +739,10 @@ class IntOpt(Opt):
return [int(v) for v in self._cparser_get_with_deprecated(cparser, return [int(v) for v in self._cparser_get_with_deprecated(cparser,
section)] section)]
def _get_optparse_kwargs(self, group, **kwargs): def _get_argparse_kwargs(self, group, **kwargs):
"""Extends the base optparse keyword dict for integer options.""" """Extends the base argparse keyword dict for integer options."""
return super(IntOpt, return super(IntOpt,
self)._get_optparse_kwargs(group, type='int', **kwargs) self)._get_argparse_kwargs(group, type=int, **kwargs)
class FloatOpt(Opt): class FloatOpt(Opt):
@ -712,10 +754,10 @@ class FloatOpt(Opt):
return [float(v) for v in return [float(v) for v in
self._cparser_get_with_deprecated(cparser, section)] self._cparser_get_with_deprecated(cparser, section)]
def _get_optparse_kwargs(self, group, **kwargs): def _get_argparse_kwargs(self, group, **kwargs):
"""Extends the base optparse keyword dict for float options.""" """Extends the base argparse keyword dict for float options."""
return super(FloatOpt, return super(FloatOpt, self)._get_argparse_kwargs(group,
self)._get_optparse_kwargs(group, type='float', **kwargs) type=float, **kwargs)
class ListOpt(Opt): class ListOpt(Opt):
@ -725,24 +767,27 @@ class ListOpt(Opt):
is a list containing these strings. 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): def _get_from_config_parser(self, cparser, section):
"""Retrieve the opt value as a list from ConfigParser.""" """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)] self._cparser_get_with_deprecated(cparser, section)]
def _get_optparse_kwargs(self, group, **kwargs): def _get_argparse_kwargs(self, group, **kwargs):
"""Extends the base optparse keyword dict for list options.""" """Extends the base argparse keyword dict for list options."""
return super(ListOpt, return Opt._get_argparse_kwargs(self,
self)._get_optparse_kwargs(group, group,
type='string', action=ListOpt._StoreListAction,
action='callback',
callback=self._parse_list,
**kwargs) **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): class MultiStrOpt(Opt):
@ -752,10 +797,14 @@ class MultiStrOpt(Opt):
""" """
multi = True multi = True
def _get_optparse_kwargs(self, group, **kwargs): def _get_argparse_kwargs(self, group, **kwargs):
"""Extends the base optparse keyword dict for multi str options.""" """Extends the base argparse keyword dict for multi str options."""
return super(MultiStrOpt, kwargs = super(MultiStrOpt, self)._get_argparse_kwargs(group)
self)._get_optparse_kwargs(group, action='append') if not self.positional:
kwargs['action'] = 'append'
else:
kwargs['nargs'] = '*'
return kwargs
def _cparser_get_with_deprecated(self, cparser, section): def _cparser_get_with_deprecated(self, cparser, section):
"""If cannot find option as dest try deprecated_name alias.""" """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) 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): class OptGroup(object):
""" """
@ -800,19 +900,20 @@ class OptGroup(object):
self.help = help self.help = help
self._opts = {} # dict of dicts of (opt:, override:, default:) 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. """Add an opt to this group.
:param opt: an Opt object :param opt: an Opt object
:param cli: whether this is a CLI option
:returns: False if previously registered, True otherwise :returns: False if previously registered, True otherwise
:raises: DuplicateOptError if a naming conflict is detected :raises: DuplicateOptError if a naming conflict is detected
""" """
if _is_opt_registered(self._opts, opt): if _is_opt_registered(self._opts, opt):
return False return False
self._opts[opt.dest] = {'opt': opt} self._opts[opt.dest] = {'opt': opt, 'cli': cli}
return True return True
@ -824,16 +925,16 @@ class OptGroup(object):
if opt.dest in self._opts: if opt.dest in self._opts:
del self._opts[opt.dest] del self._opts[opt.dest]
def _get_optparse_group(self, parser): def _get_argparse_group(self, parser):
"""Build an optparse.OptionGroup for this group.""" if self._argparse_group is None:
if self._optparse_group is None: """Build an argparse._ArgumentGroup for this group."""
self._optparse_group = optparse.OptionGroup(parser, self.title, self._argparse_group = parser.add_argument_group(self.title,
self.help) self.help)
return self._optparse_group return self._argparse_group
def _clear(self): def _clear(self):
"""Clear this group's option parsing state.""" """Clear this group's option parsing state."""
self._optparse_group = None self._argparse_group = None
class ParseError(iniparser.ParseError): class ParseError(iniparser.ParseError):
@ -928,26 +1029,31 @@ class ConfigOpts(collections.Mapping):
self._groups = {} self._groups = {}
self._args = None self._args = None
self._oparser = None self._oparser = None
self._cparser = None self._cparser = None
self._cli_values = {} self._cli_values = {}
self.__cache = {} self.__cache = {}
self._config_opts = [] self._config_opts = []
self._disable_interspersed_args = False
def _setup(self, project, prog, version, usage, default_config_files): def _pre_setup(self, project, prog, version, usage, default_config_files):
"""Initialize a ConfigOpts object for option parsing.""" """Initialize a ConfigCliParser object for option parsing."""
if prog is None: if prog is None:
prog = os.path.basename(sys.argv[0]) prog = os.path.basename(sys.argv[0])
if default_config_files is None: if default_config_files is None:
default_config_files = find_config_files(project, prog) default_config_files = find_config_files(project, prog)
self._oparser = optparse.OptionParser(prog=prog, self._oparser = argparse.ArgumentParser(prog=prog, usage=usage)
version=version, self._oparser.add_argument('--version',
usage=usage) action='version',
if self._disable_interspersed_args: version=version)
self._oparser.disable_interspersed_args()
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 = [ self._config_opts = [
MultiStrOpt('config-file', MultiStrOpt('config-file',
@ -1017,18 +1123,23 @@ class ConfigOpts(collections.Mapping):
:raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError, :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError,
RequiredOptError, DuplicateOptError RequiredOptError, DuplicateOptError
""" """
self.clear() 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._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._parse_config_files()
self._check_required_opts() self._check_required_opts()
return leftovers
def __getattr__(self, name): def __getattr__(self, name):
"""Look up an option value and perform string substitution. """Look up an option value and perform string substitution.
@ -1062,17 +1173,21 @@ class ConfigOpts(collections.Mapping):
@__clear_cache @__clear_cache
def clear(self): 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._args = None
self._cli_values.clear() self._cli_values.clear()
self._oparser = None self._oparser = argparse.ArgumentParser()
self._cparser = None self._cparser = None
self.unregister_opts(self._config_opts) self.unregister_opts(self._config_opts)
for group in self._groups.values(): for group in self._groups.values():
group._clear() group._clear()
@__clear_cache @__clear_cache
def register_opt(self, opt, group=None): def register_opt(self, opt, group=None, cli=False):
"""Register an option schema. """Register an option schema.
Registering an option schema makes any option value which is previously 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. as an attribute of this object.
:param opt: an instance of an Opt sub-class :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 :param group: an optional OptGroup object or group name
:return: False if the opt was already register, True otherwise :return: False if the opt was already register, True otherwise
:raises: DuplicateOptError :raises: DuplicateOptError
""" """
if group is not None: 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): if _is_opt_registered(self._opts, opt):
return False return False
self._opts[opt.dest] = {'opt': opt} self._opts[opt.dest] = {'opt': opt, 'cli': cli}
return True return True
@ -1116,7 +1233,7 @@ class ConfigOpts(collections.Mapping):
if self._args is not None: if self._args is not None:
raise ArgsAlreadyParsedError("cannot register CLI option") 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 @__clear_cache
def register_cli_opts(self, opts, group=None): def register_cli_opts(self, opts, group=None):
@ -1243,9 +1360,10 @@ class ConfigOpts(collections.Mapping):
for info in group._opts.values(): for info in group._opts.values():
yield info, group yield info, group
def _all_opts(self): def _all_cli_opts(self):
"""A generator function for iteration opts.""" """A generator function for iterating CLI opts."""
for info, group in self._all_opt_infos(): for info, group in self._all_opt_infos():
if info['cli']:
yield info['opt'], group yield info['opt'], group
def _unset_defaults_and_overrides(self): def _unset_defaults_and_overrides(self):
@ -1254,31 +1372,6 @@ class ConfigOpts(collections.Mapping):
info.pop('default', None) info.pop('default', None)
info.pop('override', 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): def find_file(self, name):
"""Locate a file located alongside the config files. """Locate a file located alongside the config files.
@ -1377,6 +1470,9 @@ class ConfigOpts(collections.Mapping):
info = self._get_opt_info(name, group) info = self._get_opt_info(name, group)
opt = info['opt'] opt = info['opt']
if isinstance(opt, SubCommandOpt):
return self.SubCommandAttr(self, group, opt.dest)
if 'override' in info: if 'override' in info:
return info['override'] return info['override']
@ -1401,6 +1497,10 @@ class ConfigOpts(collections.Mapping):
if not opt.multi: if not opt.multi:
return value return value
# argparse ignores default=None for nargs='*'
if opt.positional and not value:
value = opt.default
return value + values return value + values
if values: if values:
@ -1523,12 +1623,10 @@ class ConfigOpts(collections.Mapping):
""" """
self._args = args 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) opt._add_to_cli(self._oparser, group)
values, leftovers = self._oparser.parse_args(args) return vars(self._oparser.parse_args(args))
return vars(values), leftovers
class GroupAttr(collections.Mapping): class GroupAttr(collections.Mapping):
@ -1543,12 +1641,12 @@ class ConfigOpts(collections.Mapping):
:param conf: a ConfigOpts object :param conf: a ConfigOpts object
:param group: an OptGroup object :param group: an OptGroup object
""" """
self.conf = conf self._conf = conf
self.group = group self._group = group
def __getattr__(self, name): def __getattr__(self, name):
"""Look up an option value and perform template substitution.""" """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): def __getitem__(self, key):
"""Look up an option value and perform string substitution.""" """Look up an option value and perform string substitution."""
@ -1556,16 +1654,50 @@ class ConfigOpts(collections.Mapping):
def __contains__(self, key): def __contains__(self, key):
"""Return True if key is the name of a registered opt or group.""" """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): def __iter__(self):
"""Iterate over all registered opt and group names.""" """Iterate over all registered opt and group names."""
for key in self.group._opts.keys(): for key in self._group._opts.keys():
yield key yield key
def __len__(self): def __len__(self):
"""Return the number of options and option groups.""" """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): class StrSubWrapper(object):
@ -1623,19 +1755,21 @@ class CommonConfigOpts(ConfigOpts):
metavar='FORMAT', metavar='FORMAT',
help='A logging.Formatter log message format string which may ' help='A logging.Formatter log message format string which may '
'use any of the available logging.LogRecord attributes. ' 'use any of the available logging.LogRecord attributes. '
'Default: %default'), 'Default: %(default)s'),
StrOpt('log-date-format', StrOpt('log-date-format',
default=DEFAULT_LOG_DATE_FORMAT, default=DEFAULT_LOG_DATE_FORMAT,
metavar='DATE_FORMAT', metavar='DATE_FORMAT',
help='Format string for %(asctime)s in log records. ' help='Format string for %%(asctime)s in log records. '
'Default: %default'), 'Default: %(default)s'),
StrOpt('log-file', StrOpt('log-file',
metavar='PATH', metavar='PATH',
deprecated_name='logfile',
help='(Optional) Name of log file to output to. ' help='(Optional) Name of log file to output to. '
'If not set, logging will go to stdout.'), 'If not set, logging will go to stdout.'),
StrOpt('log-dir', StrOpt('log-dir',
deprecated_name='logdir',
help='(Optional) The directory to keep log files in ' help='(Optional) The directory to keep log files in '
'(will be prepended to --logfile)'), '(will be prepended to --log-file)'),
BoolOpt('use-syslog', BoolOpt('use-syslog',
default=False, default=False,
help='Use syslog for logging.'), help='Use syslog for logging.'),

View File

@ -95,12 +95,6 @@ log_opts = [
generic_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', cfg.BoolOpt('use_stderr',
default=True, default=True,
help='Log output to standard error'), help='Log output to standard error'),
@ -148,18 +142,15 @@ def _get_binary_name():
def _get_log_file_path(binary=None): def _get_log_file_path(binary=None):
logfile = CONF.log_file or CONF.logfile if CONF.log_file and not CONF.log_dir:
logdir = CONF.log_dir or CONF.logdir return CONF.log_file
if logfile and not logdir: if CONF.log_file and CONF.log_dir:
return logfile return os.path.join(CONF.log_dir, CONF.log_file)
if logfile and logdir: if CONF.log_dir:
return os.path.join(logdir, logfile)
if logdir:
binary = binary or _get_binary_name() 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): class ContextAdapter(logging.LoggerAdapter):

View File

@ -137,7 +137,7 @@ class FlatNetworkTestCase(test.TestCase):
def setUp(self): def setUp(self):
super(FlatNetworkTestCase, self).setUp() super(FlatNetworkTestCase, self).setUp()
self.tempdir = tempfile.mkdtemp() self.tempdir = tempfile.mkdtemp()
self.flags(logdir=self.tempdir) self.flags(log_dir=self.tempdir)
self.network = network_manager.FlatManager(host=HOST) self.network = network_manager.FlatManager(host=HOST)
self.network.instance_dns_domain = '' self.network.instance_dns_domain = ''
self.network.db = db self.network.db = db
@ -1597,7 +1597,7 @@ class FloatingIPTestCase(test.TestCase):
def setUp(self): def setUp(self):
super(FloatingIPTestCase, self).setUp() super(FloatingIPTestCase, self).setUp()
self.tempdir = tempfile.mkdtemp() self.tempdir = tempfile.mkdtemp()
self.flags(logdir=self.tempdir) self.flags(log_dir=self.tempdir)
self.network = TestFloatingIPManager() self.network = TestFloatingIPManager()
self.network.db = db self.network.db = db
self.project_id = 'testproject' self.project_id = 'testproject'
@ -1944,7 +1944,7 @@ class InstanceDNSTestCase(test.TestCase):
def setUp(self): def setUp(self):
super(InstanceDNSTestCase, self).setUp() super(InstanceDNSTestCase, self).setUp()
self.tempdir = tempfile.mkdtemp() self.tempdir = tempfile.mkdtemp()
self.flags(logdir=self.tempdir) self.flags(log_dir=self.tempdir)
self.network = TestFloatingIPManager() self.network = TestFloatingIPManager()
self.network.db = db self.network.db = db
self.project_id = 'testproject' self.project_id = 'testproject'

View File

@ -2,6 +2,7 @@ SQLAlchemy>=0.7.8,<=0.7.9
Cheetah==2.4.4 Cheetah==2.4.4
amqplib>=0.6.1 amqplib>=0.6.1
anyjson>=0.2.4 anyjson>=0.2.4
argparse
boto boto
eventlet>=0.9.17 eventlet>=0.9.17
kombu>=1.0.4 kombu>=1.0.4