diff --git a/zunclient/common/cliutils.py b/zunclient/common/cliutils.py index 4169bed3..f188cf4e 100644 --- a/zunclient/common/cliutils.py +++ b/zunclient/common/cliutils.py @@ -18,6 +18,7 @@ from __future__ import print_function +import collections import getpass import inspect import os @@ -90,6 +91,15 @@ def arg(*args, **kwargs): return _decorator +def exclusive_arg(group_name, *args, **kwargs): + """Decorator for CLI mutually exclusive args.""" + def _decorator(func): + required = kwargs.pop('required', None) + add_exclusive_arg(func, group_name, required, *args, **kwargs) + return func + return _decorator + + def env(*args, **kwargs): """Returns the first environment variable set. @@ -116,6 +126,24 @@ def add_arg(func, *args, **kwargs): func.arguments.insert(0, (args, kwargs)) +def add_exclusive_arg(func, group_name, required, *args, **kwargs): + """Bind CLI mutally exclusive arguments to a shell.py `do_foo` function.""" + + if not hasattr(func, 'exclusive_args'): + func.exclusive_args = collections.defaultdict(list) + # Default required to False + func.exclusive_args['__required__'] = collections.defaultdict(bool) + + # NOTE(sirp): avoid dups that can occur when the module is shared across + # tests. + if (args, kwargs) not in func.exclusive_args[group_name]: + # Because of the semantics of decorator composition if we just append + # to the options list positional options will appear to be backwards. + func.exclusive_args[group_name].insert(0, (args, kwargs)) + if required is not None: + func.exclusive_args['__required__'][group_name] = required + + def unauthenticated(func): """Adds 'unauthenticated' attribute to decorated function. diff --git a/zunclient/osc/v1/containers.py b/zunclient/osc/v1/containers.py index 55ae04b3..2163b806 100644 --- a/zunclient/osc/v1/containers.py +++ b/zunclient/osc/v1/containers.py @@ -87,11 +87,18 @@ class CreateContainer(command.ShowOne): 'already exist on the node. ' '"always": Always pull the image from repository.' '"never": never pull the image') - parser.add_argument( + restart_auto_remove_args = parser.add_mutually_exclusive_group() + restart_auto_remove_args.add_argument( '--restart', metavar='', help='Restart policy to apply when a container exits' '(no, on-failure[:max-retry], always, unless-stopped)') + restart_auto_remove_args.add_argument( + '--auto-remove', + dest='auto_remove', + action='store_true', + default=False, + help='Automatically remove the container when it exits') parser.add_argument( '--image-driver', metavar='', @@ -142,12 +149,6 @@ class CreateContainer(command.ShowOne): metavar='', help='A dictionary to configure volumes mounted inside the ' 'container.') - parser.add_argument( - '--auto-remove', - dest='auto_remove', - action='store_true', - default=False, - help='Automatically remove the container when it exits') parser.add_argument( '--runtime', metavar='', @@ -705,11 +706,18 @@ class RunContainer(command.ShowOne): 'already exist on the node. ' '"always": Always pull the image from repository.' '"never": never pull the image') - parser.add_argument( + restart_auto_remove_args = parser.add_mutually_exclusive_group() + restart_auto_remove_args.add_argument( '--restart', metavar='', help='Restart policy to apply when a container exits' '(no, on-failure[:max-retry], always, unless-stopped)') + restart_auto_remove_args.add_argument( + '--auto-remove', + dest='auto_remove', + action='store_true', + default=False, + help='Automatically remove the container when it exits') parser.add_argument( '--image-driver', metavar='', @@ -760,12 +768,6 @@ class RunContainer(command.ShowOne): metavar='', help='A dictionary to configure volumes mounted inside the ' 'container.') - parser.add_argument( - '--auto-remove', - dest='auto_remove', - action='store_true', - default=False, - help='Automatically remove the container when it exits') parser.add_argument( '--runtime', metavar='', diff --git a/zunclient/shell.py b/zunclient/shell.py index e921a7d3..d6988d56 100644 --- a/zunclient/shell.py +++ b/zunclient/shell.py @@ -429,6 +429,7 @@ class OpenStackZunShell(object): continue action_help = desc.strip() + exclusive_args = getattr(callback, 'exclusive_args', {}) arguments = getattr(callback, 'arguments', []) subparser = ( @@ -443,29 +444,46 @@ class OpenStackZunShell(object): help=argparse.SUPPRESS,) self.subcommands[command] = subparser - for (args, kwargs) in arguments: - start_version = kwargs.get("start_version", None) - if start_version: - start_version = api_versions.APIVersion(start_version) - end_version = kwargs.get("end_version", None) - if end_version: - end_version = api_versions.APIVersion(end_version) - else: - end_version = api_versions.APIVersion( - "%s.latest" % start_version.ver_major) - if do_help: - kwargs["help"] = kwargs.get("help", "") + (msg % { - "start": start_version.get_string(), - "end": end_version.get_string()}) - else: - if not version.matches(start_version, end_version): - continue - kw = kwargs.copy() - kw.pop("start_version", None) - kw.pop("end_version", None) - subparser.add_argument(*args, **kwargs) + self._add_subparser_args(subparser, arguments, version, do_help, + msg) + self._add_subparser_exclusive_args(subparser, exclusive_args, + version, do_help, msg) subparser.set_defaults(func=callback) + def _add_subparser_exclusive_args(self, subparser, exclusive_args, + version, do_help, msg): + for group_name, arguments in exclusive_args.items(): + if group_name == '__required__': + continue + required = exclusive_args['__required__'][group_name] + exclusive_group = subparser.add_mutually_exclusive_group( + required=required) + self._add_subparser_args(exclusive_group, arguments, + version, do_help, msg) + + def _add_subparser_args(self, subparser, arguments, version, do_help, msg): + for (args, kwargs) in arguments: + start_version = kwargs.get("start_version", None) + if start_version: + start_version = api_versions.APIVersion(start_version) + end_version = kwargs.get("end_version", None) + if end_version: + end_version = api_versions.APIVersion(end_version) + else: + end_version = api_versions.APIVersion( + "%s.latest" % start_version.ver_major) + if do_help: + kwargs["help"] = kwargs.get("help", "") + (msg % { + "start": start_version.get_string(), + "end": end_version.get_string()}) + else: + if not version.matches(start_version, end_version): + continue + kw = kwargs.copy() + kw.pop("start_version", None) + kw.pop("end_version", None) + subparser.add_argument(*args, **kwargs) + def setup_debugging(self, debug): if debug: streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s" diff --git a/zunclient/v1/containers_shell.py b/zunclient/v1/containers_shell.py index 90f3f91f..d02a105d 100644 --- a/zunclient/v1/containers_shell.py +++ b/zunclient/v1/containers_shell.py @@ -39,6 +39,17 @@ def _show_container(container): utils.print_dict(container._info) +@utils.exclusive_arg( + 'restart_auto_remove', + '--auto-remove', + required=False, action='store_true', + help='Automatically remove the container when it exits') +@utils.exclusive_arg( + 'restart_auto_remove', + '--restart', + required=False, metavar='', + help='Restart policy to apply when a container exits' + '(no, on-failure[:max-retry], always, unless-stopped)') @utils.arg('-n', '--name', metavar='', help='name of the container') @@ -55,9 +66,6 @@ def _show_container(container): @utils.arg('--workdir', metavar='', help='The working directory for commands to run in') -@utils.arg('--auto-remove', - action='store_true', - help='Automatically remove the container when it exits') @utils.arg('--label', metavar='', action='append', default=[], @@ -75,10 +83,6 @@ def _show_container(container): '"always": Always pull the image from repository.' '"never": never pull the image') @utils.arg('image', metavar='', help='name or ID of the image') -@utils.arg('--restart', - metavar='', - help='Restart policy to apply when a container exits' - '(no, on-failure[:max-retry], always, unless-stopped)') @utils.arg('-i', '--interactive', dest='interactive', action='store_true', @@ -523,6 +527,17 @@ def do_kill(cs, args): {'container': container, 'e': e}) +@utils.exclusive_arg( + 'restart_auto_remove', + '--auto-remove', + required=False, action='store_true', + help='Automatically remove the container when it exits') +@utils.exclusive_arg( + 'restart_auto_remove', + '--restart', + required=False, metavar='', + help='Restart policy to apply when a container exits' + '(no, on-failure[:max-retry], always, unless-stopped)') @utils.arg('-n', '--name', metavar='', help='name of the container') @@ -539,9 +554,6 @@ def do_kill(cs, args): @utils.arg('--workdir', metavar='', help='The working directory for commands to run in') -@utils.arg('--auto-remove', - action='store_true', - help='Automatically remove the container when it exits') @utils.arg('--label', metavar='', action='append', default=[], @@ -559,10 +571,6 @@ def do_kill(cs, args): '"always": Always pull the image from repository.' '"never": never pull the image') @utils.arg('image', metavar='', help='name or ID of the image') -@utils.arg('--restart', - metavar='', - help='Restart policy to apply when a container exits' - '(no, on-failure[:max-retry], always, unless-stopped)') @utils.arg('-i', '--interactive', dest='interactive', action='store_true',