From 5e09b2cab7088f393e9f1fb58fdcfa3243a2e064 Mon Sep 17 00:00:00 2001
From: Mark McLoughlin <markmc@redhat.com>
Date: Mon, 26 Nov 2012 07:21:03 +0000
Subject: [PATCH] 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
---
 bin/nova-clear-rabbit-queues       |  18 +-
 bin/nova-dhcpbridge                |  34 ++-
 bin/nova-manage                    | 191 +++++-------
 nova/config.py                     |   7 +-
 nova/network/minidns.py            |   4 +-
 nova/openstack/common/cfg.py       | 462 +++++++++++++++++++----------
 nova/openstack/common/log.py       |  21 +-
 nova/tests/network/test_manager.py |   6 +-
 tools/pip-requires                 |   1 +
 9 files changed, 426 insertions(+), 318 deletions(-)

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 8ff2fac49c8f..9b3e9f2143b9 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 fake_flags
 
-    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 6e90e2e000a4..a201afa5c70b 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -56,7 +56,6 @@
 
 import gettext
 import netaddr
-import optparse
 import os
 import sys
 
@@ -106,7 +105,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
 
@@ -764,21 +763,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."""
 
@@ -982,10 +966,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
@@ -1026,45 +1010,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):
@@ -1077,11 +1039,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
@@ -1096,68 +1093,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
     try:
diff --git a/nova/config.py b/nova/config.py
index 9ce068e58500..d370b0f8f778 100644
--- a/nova/config.py
+++ b/nova/config.py
@@ -285,7 +285,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 4f0eab1d429a..365aeaf56d1f 100644
--- a/nova/network/minidns.py
+++ b/nova/network/minidns.py
@@ -35,8 +35,8 @@ class MiniDNS(object):
         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 b3ba161c6cc1..9ea8f93ce706 100644
--- a/nova/tests/network/test_manager.py
+++ b/nova/tests/network/test_manager.py
@@ -137,7 +137,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
@@ -1597,7 +1597,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'
@@ -1944,7 +1944,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 6b3c83ec680c..d98a4a3aa611 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