diff --git a/bin/glance-cache-manage b/bin/glance-cache-manage index d01698a507..936c6b0aec 100755 --- a/bin/glance-cache-manage +++ b/bin/glance-cache-manage @@ -297,11 +297,11 @@ def create_options(parser): help="Print debugging output") parser.add_option('-H', '--host', metavar="ADDRESS", default="0.0.0.0", help="Address of Glance API host. " - "Default: %default") + "Default: %(default)s") parser.add_option('-p', '--port', dest="port", metavar="PORT", type=int, default=9292, help="Port the Glance API host listens on. " - "Default: %default") + "Default: %(default)s") parser.add_option('-k', '--insecure', dest="insecure", default=False, action="store_true", help="Explicitly allow glance to perform \"insecure\" " diff --git a/bin/glance-control b/bin/glance-control index 606478bd55..af012d67f0 100755 --- a/bin/glance-control +++ b/bin/glance-control @@ -23,6 +23,7 @@ Thanks for some of the code, Swifties ;) from __future__ import with_statement +import argparse import errno import fcntl import gettext @@ -51,12 +52,12 @@ CONF = cfg.CONF ALL_COMMANDS = ['start', 'status', 'stop', 'shutdown', 'restart', 'reload', 'force-reload'] -ALL_SERVERS = ['glance-api', 'glance-registry', 'glance-scrubber'] +ALL_SERVERS = ['api', 'registry', 'scrubber'] GRACEFUL_SHUTDOWN_SERVERS = ['glance-api', 'glance-registry', 'glance-scrubber'] MAX_DESCRIPTORS = 32768 MAX_MEMORY = (1024 * 1024 * 1024) * 2 # 2 GB -USAGE = """%prog [options] [CONFPATH] +USAGE = """%(prog)s [options] [CONFPATH] Where is one of: @@ -205,7 +206,7 @@ def do_check_status(pid_file, server): print "No %s running" % server -def get_pid_file(pid, pid_file): +def get_pid_file(server, pid_file): pid_file = (os.path.abspath(pid_file) if pid_file else '/var/run/glance/%s.pid' % server) dir, file = os.path.split(pid_file) @@ -259,10 +260,34 @@ def do_stop(server, args, graceful=False): print 'No %s running' % server +def add_command_parsers(subparsers): + cmd_parser = argparse.ArgumentParser(add_help=False) + cmd_subparsers = cmd_parser.add_subparsers(dest='command') + for cmd in ALL_COMMANDS: + parser = cmd_subparsers.add_parser(cmd) + parser.add_argument('args', nargs=argparse.REMAINDER) + + for server in ALL_SERVERS: + full_name = 'glance-' + server + + parser = subparsers.add_parser(server, parents=[cmd_parser]) + parser.set_defaults(servers=[full_name]) + + parser = subparsers.add_parser(full_name, parents=[cmd_parser]) + parser.set_defaults(servers=[full_name]) + + parser = subparsers.add_parser('all', parents=[cmd_parser]) + parser.set_defaults(servers=['glance-' + s for s in ALL_SERVERS]) + + if __name__ == '__main__': exitcode = 0 opts = [ + cfg.SubCommandOpt('server', + title='Server types', + help='Available server types', + handler=add_command_parsers), cfg.StrOpt('pid-file', metavar='PATH', help='File to use as pid file. Default: ' @@ -283,7 +308,7 @@ if __name__ == '__main__': ] CONF.register_cli_opts(opts) - args = config.parse_args(usage=USAGE) + config.parse_args(usage=USAGE) @gated_by(CONF.await_child) @gated_by(CONF.respawn) @@ -293,31 +318,6 @@ if __name__ == '__main__': mutually_exclusive() - if len(args) < 2: - CONF.print_usage() - sys.exit(1) - - server = args.pop(0).lower() - if server == 'all': - servers = ALL_SERVERS - else: - if not server.startswith('glance-'): - server = 'glance-%s' % server - if server not in ALL_SERVERS: - server_list = ", ".join([s.replace('glance-', '') - for s in ALL_SERVERS]) - msg = ("Unknown server '%(server)s' specified. Please specify " - "all, or one of the servers: %(server_list)s" % locals()) - sys.exit(msg) - servers = [server] - - command = args.pop(0).lower() - if command not in ALL_COMMANDS: - command_list = ", ".join(ALL_COMMANDS) - msg = ("Unknown command %(command)s specified. Please specify a " - "command in this list: %(command_list)s" % locals()) - sys.exit(msg) - @gated_by(CONF.respawn) def anticipate_respawn(children): while children: @@ -336,40 +336,41 @@ if __name__ == '__main__': rsn = 'bouncing' if bouncing else 'deliberately stopped' print 'Supressed respawn as %s was %s.' % (server, rsn) - if command == 'start': - pid_file = get_pid_file(server, CONF.pid_file) + if CONF.server.command == 'start': children = {} - for server in servers: - args = (pid_file, server, args) + for server in CONF.server.servers: + pid_file = get_pid_file(server, CONF.pid_file) + args = (pid_file, server, CONF.server.args) pid = do_start('Start', *args) children[pid] = args anticipate_respawn(children) - if command == 'status': - for server in servers: + if CONF.server.command == 'status': + for server in CONF.server.servers: pid_file = get_pid_file(server, CONF.pid_file) do_check_status(pid_file, server) - if command == 'stop': - for server in servers: - do_stop(server, args) + if CONF.server.command == 'stop': + for server in CONF.server.servers: + do_stop(server, CONF.server.args) - if command == 'shutdown': - for server in servers: - do_stop(server, args, graceful=True) + if CONF.server.command == 'shutdown': + for server in CONF.server.servers: + do_stop(server, CONF.server.args, graceful=True) - if command == 'restart': - for server in servers: - do_stop(server, args) - for server in servers: + if CONF.server.command == 'restart': + for server in CONF.server.servers: + do_stop(server, CONF.server.args) + for server in CONF.server.servers: pid_file = get_pid_file(server, CONF.pid_file) - do_start('Restart', pid_file, server, args) + do_start('Restart', pid_file, server, CONF.server.args) - if command == 'reload' or command == 'force-reload': - for server in servers: - do_stop(server, args, graceful=True) + if (CONF.server.command == 'reload' or + CONF.server.command == 'force-reload'): + for server in CONF.server.servers: + do_stop(server, CONF.server.args, graceful=True) pid_file = get_pid_file(server, CONF.pid_file) - do_start('Restart', pid_file, server, args) + do_start('Restart', pid_file, server, CONF.server.args) sys.exit(exitcode) diff --git a/bin/glance-manage b/bin/glance-manage index 03a8f938c3..c30a4d9113 100755 --- a/bin/glance-manage +++ b/bin/glance-manage @@ -49,58 +49,65 @@ import glance.db.sqlalchemy.migration CONF = cfg.CONF -def do_db_version(args): +def do_db_version(): """Print database's current migration level""" print glance.db.sqlalchemy.migration.db_version() -def do_upgrade(args): +def do_upgrade(): """Upgrade the database's migration level""" - version = args.pop(0) if args else None - glance.db.sqlalchemy.migration.upgrade(version) + glance.db.sqlalchemy.migration.upgrade(CONF.command.version) -def do_downgrade(args): +def do_downgrade(): """Downgrade the database's migration level""" - if not args: - raise exception.MissingArgumentError( - "downgrade requires a version argument") - - version = args.pop(0) - glance.db.sqlalchemy.migration.downgrade(version) + glance.db.sqlalchemy.migration.downgrade(CONF.command.version) -def do_version_control(args): +def do_version_control(): """Place a database under migration control""" - version = args.pop(0) if args else None - glance.db.sqlalchemy.migration.version_control(version) + glance.db.sqlalchemy.migration.version_control(CONF.command.version) -def do_db_sync(args): +def do_db_sync(): """ Place a database under migration control and upgrade, creating first if necessary. """ - version = args.pop(0) if args else None - current_version = args.pop(0) if args else None - glance.db.sqlalchemy.migration.db_sync(version, current_version) + glance.db.sqlalchemy.migration.db_sync(CONF.command.version, + CONF.command.current_version) -def dispatch_cmd(args): - """Search for do_* cmd in this module and then run it""" - cmd = args.pop(0) - try: - cmd_func = globals()['do_%s' % cmd] - except KeyError: - sys.exit("ERROR: unrecognized command '%s'" % cmd) +def add_command_parsers(subparsers): + parser = subparsers.add_parser('db_version') + parser.set_defaults(func=do_db_version) - try: - cmd_func(args) - except exception.GlanceException, e: - sys.exit("ERROR: %s" % e) + parser = subparsers.add_parser('upgrade') + parser.set_defaults(func=do_upgrade) + parser.add_argument('version', nargs='?') + + parser = subparsers.add_parser('downgrade') + parser.set_defaults(func=do_downgrade) + parser.add_argument('version') + + parser = subparsers.add_parser('version_control') + parser.set_defaults(func=do_version_control) + parser.add_argument('version', nargs='?') + + parser = subparsers.add_parser('db_sync') + parser.set_defaults(func=do_db_sync) + parser.add_argument('version', nargs='?') + parser.add_argument('current_version', nargs='?') + + +command_opt = cfg.SubCommandOpt('command', + title='Commands', + help='Available commands', + handler=add_command_parsers) def main(): + CONF.register_cli_opt(command_opt) try: # We load the glance-registry config section because # sql_connection is only part of the glance registry. @@ -109,17 +116,16 @@ def main(): default_cfg_files = cfg.find_config_files(project='glance', prog='glance-registry') - args = config.parse_args(default_config_files=default_cfg_files, - usage="%prog [options] ") + config.parse_args(default_config_files=default_cfg_files, + usage="%(prog)s [options] ") config.setup_logging() except RuntimeError, e: sys.exit("ERROR: %s" % e) - if not args: - CONF.print_usage() - sys.exit(1) - - dispatch_cmd(args) + try: + CONF.command.func() + except exception.GlanceException, e: + sys.exit("ERROR: %s" % e) if __name__ == '__main__': diff --git a/glance/common/config.py b/glance/common/config.py index bdd18735e8..ffff1711b2 100644 --- a/glance/common/config.py +++ b/glance/common/config.py @@ -66,16 +66,16 @@ CONF.register_opts(common_opts) def parse_args(args=None, usage=None, default_config_files=None): - return CONF(args=args, - project='glance', - version=version.deferred_version_string(prefix="%prog "), - usage=usage, - default_config_files=default_config_files) + CONF(args=args, + project='glance', + version=version.deferred_version_string(prefix="%prog "), + usage=usage, + default_config_files=default_config_files) def parse_cache_args(args=None): config_files = cfg.find_config_files(project='glance', prog='glance-cache') - return parse_args(args=args, default_config_files=config_files) + parse_args(args=args, default_config_files=config_files) def setup_logging(): diff --git a/glance/db/__init__.py b/glance/db/__init__.py index 54165baef4..ff884d6ee1 100644 --- a/glance/db/__init__.py +++ b/glance/db/__init__.py @@ -28,7 +28,7 @@ sql_connection_opt = cfg.StrOpt('sql_connection', metavar='CONNECTION', help='A valid SQLAlchemy connection ' 'string for the registry database. ' - 'Default: %default') + 'Default: %(default)s') CONF = cfg.CONF CONF.register_opt(sql_connection_opt) diff --git a/glance/openstack/common/cfg.py b/glance/openstack/common/cfg.py index 3677644313..5b1d6c8466 100644 --- a/glance/openstack/common/cfg.py +++ b/glance/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 openstack.common import cfg + from glance.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: @@ -1507,7 +1607,7 @@ class ConfigOpts(collections.Mapping): if ('default' in info or 'override' in info): continue - if self._get(opt.name, group) is None: + if self._get(opt.dest, group) is None: raise RequiredOptError(opt.name, group) def _parse_cli_opts(self, args): @@ -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/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py index da34e2bc89..9d7af9e5af 100644 --- a/glance/tests/functional/__init__.py +++ b/glance/tests/functional/__init__.py @@ -129,9 +129,9 @@ class Server(object): self.create_database() - cmd = ("%(server_control)s %(server_name)s start " - "%(conf_file_name)s --pid-file=%(pid_file)s " - "%(server_control_options)s" + cmd = ("%(server_control)s --pid-file=%(pid_file)s " + "%(server_control_options)s " + "%(server_name)s start %(conf_file_name)s" % self.__dict__) return execute(cmd, no_venv=self.no_venv, @@ -147,9 +147,9 @@ class Server(object): Any kwargs passed to this method will override the configuration value in the conf file used in starting the servers. """ - cmd = ("%(server_control)s %(server_name)s reload " - "%(conf_file_name)s --pid-file=%(pid_file)s " - "%(server_control_options)s" + cmd = ("%(server_control)s --pid-file=%(pid_file)s " + "%(server_control_options)s " + "%(server_name)s reload %(conf_file_name)s" % self.__dict__) return execute(cmd, no_venv=self.no_venv, @@ -169,7 +169,7 @@ class Server(object): conf_file.write('sql_connection = %s' % self.sql_connection) conf_file.flush() - cmd = ('bin/glance-manage db_sync --config-file %s' + cmd = ('bin/glance-manage --config-file %s db_sync' % conf_filepath) execute(cmd, no_venv=self.no_venv, exec_env=self.exec_env, expect_exit=True) @@ -178,8 +178,8 @@ class Server(object): """ Spin down the server. """ - cmd = ("%(server_control)s %(server_name)s stop " - "%(conf_file_name)s --pid-file=%(pid_file)s" + cmd = ("%(server_control)s --pid-file=%(pid_file)s " + "%(server_name)s stop %(conf_file_name)s" % self.__dict__) return execute(cmd, no_venv=self.no_venv, exec_env=self.exec_env, expect_exit=True) diff --git a/glance/tests/functional/test_glance_manage.py b/glance/tests/functional/test_glance_manage.py index 00054d18ec..5bb91c9eae 100644 --- a/glance/tests/functional/test_glance_manage.py +++ b/glance/tests/functional/test_glance_manage.py @@ -43,7 +43,7 @@ class TestGlanceManage(functional.FunctionalTest): conf_file.write(self.connection) conf_file.flush() - cmd = ('bin/glance-manage db_sync --config-file %s' % + cmd = ('bin/glance-manage --config-file %s db_sync' % self.conf_filepath) execute(cmd, raise_error=True)