diff options
Diffstat (limited to 'openstackclient')
95 files changed, 3882 insertions, 773 deletions
diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index 820b4ecf..66272e42 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -185,7 +185,7 @@ def build_auth_plugins_option_parser(parser): metavar='<auth-type>', dest='auth_type', default=utils.env('OS_AUTH_TYPE'), - help='Select an auhentication type. Available types: ' + + help='Select an authentication type. Available types: ' + ', '.join(available_plugins) + '. Default: selected based on --os-username/--os-token' + ' (Env: OS_AUTH_TYPE)', diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index c52eeb3a..ab75a78c 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -14,8 +14,11 @@ """Object Store v1 API Library""" import io +import logging import os + import six +from six.moves import urllib try: from urllib.parse import urlparse # noqa @@ -23,6 +26,7 @@ except ImportError: from urlparse import urlparse # noqa from openstackclient.api import api +from openstackclient.common import utils class APIv1(api.BaseAPI): @@ -42,11 +46,9 @@ class APIv1(api.BaseAPI): :returns: dict of returned headers """ - - response = self.create(container, method='PUT') - url_parts = urlparse(self.endpoint) + response = self.create(urllib.parse.quote(container), method='PUT') data = { - 'account': url_parts.path.split('/')[-1], + 'account': self._find_account_id(), 'container': container, 'x-trans-id': response.headers.get('x-trans-id', None), } @@ -64,7 +66,7 @@ class APIv1(api.BaseAPI): """ if container: - self.delete(container) + self.delete(urllib.parse.quote(container)) def container_list( self, @@ -140,6 +142,23 @@ class APIv1(api.BaseAPI): for object in objects: self.object_save(container=container, object=object['name']) + def container_set( + self, + container, + properties, + ): + """Set container properties + + :param string container: + name of container to modify + :param dict properties: + properties to add or update for the container + """ + + headers = self._set_properties(properties, 'X-Container-Meta-%s') + if headers: + self.create(urllib.parse.quote(container), headers=headers) + def container_show( self, container=None, @@ -152,22 +171,52 @@ class APIv1(api.BaseAPI): dict of returned headers """ - response = self._request('HEAD', container) + response = self._request('HEAD', urllib.parse.quote(container)) data = { - 'account': response.headers.get('x-container-meta-owner', None), + 'account': self._find_account_id(), 'container': container, 'object_count': response.headers.get( 'x-container-object-count', None, ), - 'bytes_used': response.headers.get('x-container-bytes-used', None), - 'read_acl': response.headers.get('x-container-read', None), - 'write_acl': response.headers.get('x-container-write', None), - 'sync_to': response.headers.get('x-container-sync-to', None), - 'sync_key': response.headers.get('x-container-sync-key', None), + 'bytes_used': response.headers.get('x-container-bytes-used', None) } + + if 'x-container-read' in response.headers: + data['read_acl'] = response.headers.get('x-container-read', None) + if 'x-container-write' in response.headers: + data['write_acl'] = response.headers.get('x-container-write', None) + if 'x-container-sync-to' in response.headers: + data['sync_to'] = response.headers.get('x-container-sync-to', None) + if 'x-container-sync-key' in response.headers: + data['sync_key'] = response.headers.get('x-container-sync-key', + None) + + properties = self._get_properties(response.headers, + 'x-container-meta-') + if properties: + data['properties'] = properties + return data + def container_unset( + self, + container, + properties, + ): + """Unset container properties + + :param string container: + name of container to modify + :param dict properties: + properties to remove from the container + """ + + headers = self._unset_properties(properties, + 'X-Remove-Container-Meta-%s') + if headers: + self.create(urllib.parse.quote(container), headers=headers) + def object_create( self, container=None, @@ -187,16 +236,16 @@ class APIv1(api.BaseAPI): # TODO(dtroyer): What exception to raise here? return {} - full_url = "%s/%s" % (container, object) + full_url = "%s/%s" % (urllib.parse.quote(container), + urllib.parse.quote(object)) with io.open(object, 'rb') as f: response = self.create( full_url, method='PUT', data=f, ) - url_parts = urlparse(self.endpoint) data = { - 'account': url_parts.path.split('/')[-1], + 'account': self._find_account_id(), 'container': container, 'object': object, 'x-trans-id': response.headers.get('X-Trans-Id', None), @@ -221,7 +270,8 @@ class APIv1(api.BaseAPI): if container is None or object is None: return - self.delete("%s/%s" % (container, object)) + self.delete("%s/%s" % (urllib.parse.quote(container), + urllib.parse.quote(object))) def object_list( self, @@ -298,7 +348,7 @@ class APIv1(api.BaseAPI): if delimiter: params['delimiter'] = delimiter - return self.list(container, **params) + return self.list(urllib.parse.quote(container), **params) def object_save( self, @@ -321,7 +371,8 @@ class APIv1(api.BaseAPI): response = self._request( 'GET', - "%s/%s" % (container, object), + "%s/%s" % (urllib.parse.quote(container), + urllib.parse.quote(object)), stream=True, ) if response.status_code == 200: @@ -332,6 +383,50 @@ class APIv1(api.BaseAPI): for chunk in response.iter_content(): f.write(chunk) + def object_set( + self, + container, + object, + properties, + ): + """Set object properties + + :param string container: + container name for object to modify + :param string object: + name of object to modify + :param dict properties: + properties to add or update for the container + """ + + headers = self._set_properties(properties, 'X-Object-Meta-%s') + if headers: + self.create("%s/%s" % (urllib.parse.quote(container), + urllib.parse.quote(object)), + headers=headers) + + def object_unset( + self, + container, + object, + properties, + ): + """Unset object properties + + :param string container: + container name for object to modify + :param string object: + name of object to modify + :param dict properties: + properties to remove from the object + """ + + headers = self._unset_properties(properties, 'X-Remove-Object-Meta-%s') + if headers: + self.create("%s/%s" % (urllib.parse.quote(container), + urllib.parse.quote(object)), + headers=headers) + def object_show( self, container=None, @@ -350,9 +445,12 @@ class APIv1(api.BaseAPI): if container is None or object is None: return {} - response = self._request('HEAD', "%s/%s" % (container, object)) + response = self._request('HEAD', "%s/%s" % + (urllib.parse.quote(container), + urllib.parse.quote(object))) + data = { - 'account': response.headers.get('x-container-meta-owner', None), + 'account': self._find_account_id(), 'container': container, 'object': object, 'content-type': response.headers.get('content-type', None), @@ -371,18 +469,107 @@ class APIv1(api.BaseAPI): 'x-object-manifest', None, ) - for key, value in six.iteritems(response.headers): - if key.startswith('x-object-meta-'): - data[key[len('x-object-meta-'):].lower()] = value - elif key not in ( - 'content-type', - 'content-length', - 'last-modified', - 'etag', - 'date', - 'x-object-manifest', - 'x-container-meta-owner', - ): - data[key.lower()] = value + properties = self._get_properties(response.headers, 'x-object-meta-') + if properties: + data['properties'] = properties + + return data + + def account_set( + self, + properties, + ): + """Set account properties + + :param dict properties: + properties to add or update for the account + """ + + headers = self._set_properties(properties, 'X-Account-Meta-%s') + if headers: + # NOTE(stevemar): The URL (first argument) in this case is already + # set to the swift account endpoint, because that's how it's + # registered in the catalog + self.create("", headers=headers) + + def account_show(self): + """Show account details""" + + # NOTE(stevemar): Just a HEAD request to the endpoint already in the + # catalog should be enough. + response = self._request("HEAD", "") + data = {} + + properties = self._get_properties(response.headers, 'x-account-meta-') + if properties: + data['properties'] = properties + + # Map containers, bytes and objects a bit nicer + data['Containers'] = response.headers.get('x-account-container-count', + None) + data['Objects'] = response.headers.get('x-account-object-count', None) + data['Bytes'] = response.headers.get('x-account-bytes-used', None) + # Add in Account info too + data['Account'] = self._find_account_id() return data + + def account_unset( + self, + properties, + ): + """Unset account properties + + :param dict properties: + properties to remove from the account + """ + + headers = self._unset_properties(properties, + 'X-Remove-Account-Meta-%s') + if headers: + self.create("", headers=headers) + + def _find_account_id(self): + url_parts = urlparse(self.endpoint) + return url_parts.path.split('/')[-1] + + def _unset_properties(self, properties, header_tag): + # NOTE(stevemar): As per the API, the headers have to be in the form + # of "X-Remove-Account-Meta-Book: x". In the case where metadata is + # removed, we can set the value of the header to anything, so it's + # set to 'x'. In the case of a Container property we use: + # "X-Remove-Container-Meta-Book: x", and the same logic applies for + # Object properties + + headers = {} + for k in properties: + header_name = header_tag % k + headers[header_name] = 'x' + return headers + + def _set_properties(self, properties, header_tag): + # NOTE(stevemar): As per the API, the headers have to be in the form + # of "X-Account-Meta-Book: MobyDick". In the case of a Container + # property we use: "X-Add-Container-Meta-Book: MobyDick", and the same + # logic applies for Object properties + + log = logging.getLogger(__name__ + '._set_properties') + + headers = {} + for k, v in properties.iteritems(): + if not utils.is_ascii(k) or not utils.is_ascii(v): + log.error('Cannot set property %s to non-ascii value', k) + continue + + header_name = header_tag % k + headers[header_name] = v + return headers + + def _get_properties(self, headers, header_tag): + # Add in properties as a top level key, this is consistent with other + # OSC commands + properties = {} + for k, v in six.iteritems(headers): + if k.startswith(header_tag): + properties[k[len(header_tag):]] = v + return properties diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 55c6fe53..edabf65e 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -20,6 +20,7 @@ import logging import pkg_resources import sys +from oslo_utils import strutils import requests from openstackclient.api import auth @@ -167,7 +168,8 @@ class ClientManager(object): self._project_name = self._auth_params['tenant_name'] LOG.info('Using auth plugin: %s' % self.auth_plugin_name) - LOG.debug('Using parameters %s' % self._auth_params) + LOG.debug('Using parameters %s' % + strutils.mask_password(self._auth_params)) self.auth = auth_plugin.load_from_options(**self._auth_params) # needed by SAML authentication request_session = requests.session() diff --git a/openstackclient/common/configuration.py b/openstackclient/common/configuration.py index 83df73e2..ac2792dd 100644 --- a/openstackclient/common/configuration.py +++ b/openstackclient/common/configuration.py @@ -18,6 +18,8 @@ import logging from cliff import show import six +from openstackclient.common import utils + REDACTED = "<redacted>" @@ -44,8 +46,8 @@ class ShowConfiguration(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) info = self.app.client_manager.get_configuration() for key, value in six.iteritems(info.pop('auth', {})): diff --git a/openstackclient/common/limits.py b/openstackclient/common/limits.py index 4abcf169..582f70c5 100644 --- a/openstackclient/common/limits.py +++ b/openstackclient/common/limits.py @@ -31,7 +31,7 @@ class ShowLimits(lister.Lister): def get_parser(self, prog_name): parser = super(ShowLimits, self).get_parser(prog_name) - type_group = parser.add_mutually_exclusive_group() + type_group = parser.add_mutually_exclusive_group(required=True) type_group.add_argument( "--absolute", dest="is_absolute", @@ -64,8 +64,8 @@ class ShowLimits(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume diff --git a/openstackclient/common/logs.py b/openstackclient/common/logs.py new file mode 100644 index 00000000..6d1aec13 --- /dev/null +++ b/openstackclient/common/logs.py @@ -0,0 +1,170 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Application logging""" + +import logging +import sys +import warnings + + +def log_level_from_options(options): + # if --debug, --quiet or --verbose is not specified, + # the default logging level is warning + log_level = logging.WARNING + if options.verbose_level == 0: + # --quiet + log_level = logging.ERROR + elif options.verbose_level == 2: + # One --verbose + log_level = logging.INFO + elif options.verbose_level >= 3: + # Two or more --verbose + log_level = logging.DEBUG + return log_level + + +def log_level_from_config(config): + # Check the command line option + verbose_level = config.get('verbose_level') + if config.get('debug', False): + verbose_level = 3 + if verbose_level == 0: + verbose_level = 'error' + elif verbose_level == 1: + # If a command line option has not been specified, check the + # configuration file + verbose_level = config.get('log_level', 'warning') + elif verbose_level == 2: + verbose_level = 'info' + else: + verbose_level = 'debug' + + log_level = { + 'critical': logging.CRITICAL, + 'error': logging.ERROR, + 'warning': logging.WARNING, + 'info': logging.INFO, + 'debug': logging.DEBUG, + }.get(verbose_level, logging.WARNING) + return log_level + + +def set_warning_filter(log_level): + if log_level == logging.ERROR: + warnings.simplefilter("ignore") + elif log_level == logging.WARNING: + warnings.simplefilter("ignore") + elif log_level == logging.INFO: + warnings.simplefilter("once") + + +class _FileFormatter(logging.Formatter): + """Customize the logging format for logging handler""" + _LOG_MESSAGE_BEGIN = ( + '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s ') + _LOG_MESSAGE_CONTEXT = '[%(cloud)s %(username)s %(project)s] ' + _LOG_MESSAGE_END = '%(message)s' + _LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' + + def __init__(self, options=None, config=None, **kwargs): + context = {} + if options: + context = { + 'cloud': getattr(options, 'cloud', ''), + 'project': getattr(options, 'os_project_name', ''), + 'username': getattr(options, 'username', ''), + } + elif config: + context = { + 'cloud': config.config.get('cloud', ''), + 'project': config.auth.get('project_name', ''), + 'username': config.auth.get('username', ''), + } + if context: + self.fmt = (self._LOG_MESSAGE_BEGIN + + (self._LOG_MESSAGE_CONTEXT % context) + + self._LOG_MESSAGE_END) + else: + self.fmt = self._LOG_MESSAGE_BEGIN + self._LOG_MESSAGE_END + logging.Formatter.__init__(self, self.fmt, self._LOG_DATE_FORMAT) + + +class LogConfigurator(object): + + _CONSOLE_MESSAGE_FORMAT = '%(message)s' + + def __init__(self, options): + self.root_logger = logging.getLogger('') + self.root_logger.setLevel(logging.DEBUG) + + # Force verbose_level 3 on --debug + self.dump_trace = False + if options.debug: + options.verbose_level = 3 + self.dump_trace = True + + # Always send higher-level messages to the console via stderr + self.console_logger = logging.StreamHandler(sys.stderr) + log_level = log_level_from_options(options) + self.console_logger.setLevel(log_level) + formatter = logging.Formatter(self._CONSOLE_MESSAGE_FORMAT) + self.console_logger.setFormatter(formatter) + self.root_logger.addHandler(self.console_logger) + + # Set the warning filter now + set_warning_filter(log_level) + + # Set up logging to a file + self.file_logger = None + log_file = options.log_file + if log_file: + self.file_logger = logging.FileHandler(filename=log_file) + self.file_logger.setFormatter(_FileFormatter(options=options)) + self.file_logger.setLevel(log_level) + self.root_logger.addHandler(self.file_logger) + + # Requests logs some stuff at INFO that we don't want + # unless we have DEBUG + requests_log = logging.getLogger("requests") + + # Other modules we don't want DEBUG output for + cliff_log = logging.getLogger('cliff') + stevedore_log = logging.getLogger('stevedore') + iso8601_log = logging.getLogger("iso8601") + + if options.debug: + # --debug forces traceback + requests_log.setLevel(logging.DEBUG) + else: + requests_log.setLevel(logging.ERROR) + + cliff_log.setLevel(logging.ERROR) + stevedore_log.setLevel(logging.ERROR) + iso8601_log.setLevel(logging.ERROR) + + def configure(self, cloud_config): + log_level = log_level_from_config(cloud_config.config) + set_warning_filter(log_level) + self.dump_trace = cloud_config.config.get('debug', self.dump_trace) + self.console_logger.setLevel(log_level) + + log_file = cloud_config.config.get('log_file', None) + if log_file: + if not self.file_logger: + self.file_logger = logging.FileHandler(filename=log_file) + formatter = _FileFormatter(cloud_config=cloud_config) + self.file_logger.setFormatter(formatter) + self.file_logger.setFormatter(_FileFormatter(config=cloud_config)) + self.file_logger.setLevel(log_level) + self.root_logger.addHandler(self.file_logger) diff --git a/openstackclient/common/module.py b/openstackclient/common/module.py index 356cdca3..f0ed23b2 100644 --- a/openstackclient/common/module.py +++ b/openstackclient/common/module.py @@ -22,6 +22,8 @@ import sys from cliff import lister from cliff import show +from openstackclient.common import utils + class ListCommand(lister.Lister): """List recognized commands by group""" @@ -29,8 +31,8 @@ class ListCommand(lister.Lister): auth_required = False log = logging.getLogger(__name__ + '.ListCommand') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) cm = self.app.command_manager groups = cm.get_command_groups() @@ -54,8 +56,8 @@ class ListModule(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) data = {} # Get module versions diff --git a/openstackclient/common/parseractions.py b/openstackclient/common/parseractions.py index 8f6008e2..fd90369a 100644 --- a/openstackclient/common/parseractions.py +++ b/openstackclient/common/parseractions.py @@ -65,3 +65,18 @@ class RangeAction(argparse.Action): # Too many values msg = "Invalid range, too many values" raise argparse.ArgumentError(self, msg) + + +class NonNegativeAction(argparse.Action): + """A custom action to check whether the value is non-negative or not + + Ensures the value is >= 0. + """ + + def __call__(self, parser, namespace, values, option_string=None): + try: + assert(int(values) >= 0) + setattr(namespace, self.dest, values) + except Exception: + msg = "%s expected a non-negative integer" % (str(option_string)) + raise argparse.ArgumentTypeError(self, msg) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index e79fd7ed..b5d4eb87 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -23,6 +23,8 @@ import sys from cliff import command from cliff import show +from openstackclient.common import utils + # List the quota items, map the internal argument name to the option # name that the user sees. @@ -89,9 +91,10 @@ class SetQuota(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume @@ -103,8 +106,7 @@ class SetQuota(command.Command): volume_kwargs = {} for k, v in VOLUME_QUOTAS.items(): - # TODO(jiaxi): Should use k or v needs discuss - value = getattr(parsed_args, v, None) + value = getattr(parsed_args, k, None) if value is not None: if parsed_args.volume_type: k = k + '_%s' % parsed_args.volume_type @@ -114,23 +116,29 @@ class SetQuota(command.Command): sys.stderr.write("No quotas updated") return + if parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + if parsed_args.quota_class: if compute_kwargs: compute_client.quota_classes.update( - parsed_args.project, + project.id, **compute_kwargs) if volume_kwargs: volume_client.quota_classes.update( - parsed_args.project, + project.id, **volume_kwargs) else: if compute_kwargs: compute_client.quotas.update( - parsed_args.project, + project.id, **compute_kwargs) if volume_kwargs: volume_client.quotas.update( - parsed_args.project, + project.id, **volume_kwargs) @@ -188,8 +196,8 @@ class ShowQuota(show.ShowOne): else: return {} + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 2f8419f4..91b1e05a 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -26,6 +26,29 @@ from oslo_utils import importutils from openstackclient.common import exceptions +def log_method(log, level=logging.DEBUG): + """Logs a method and its arguments when entered.""" + + def decorator(func): + func_name = func.__name__ + + @six.wraps(func) + def wrapper(self, *args, **kwargs): + if log.isEnabledFor(level): + pretty_args = [] + if args: + pretty_args.extend(str(a) for a in args) + if kwargs: + pretty_args.extend( + "%s=%s" % (k, v) for k, v in six.iteritems(kwargs)) + log.log(level, "%s(%s)", func_name, ", ".join(pretty_args)) + return func(self, *args, **kwargs) + + return wrapper + + return decorator + + def find_resource(manager, name_or_id, **kwargs): """Helper for the _find_* methods. @@ -71,12 +94,15 @@ def find_resource(manager, name_or_id, **kwargs): if len(kwargs) == 0: kwargs = {} - # Prepare the kwargs for calling find - if 'NAME_ATTR' in manager.resource_class.__dict__: - # novaclient does this for oddball resources - kwargs[manager.resource_class.NAME_ATTR] = name_or_id - else: - kwargs['name'] = name_or_id + try: + # Prepare the kwargs for calling find + if 'NAME_ATTR' in manager.resource_class.__dict__: + # novaclient does this for oddball resources + kwargs[manager.resource_class.NAME_ATTR] = name_or_id + else: + kwargs['name'] = name_or_id + except Exception: + pass # finally try to find entity by name try: @@ -95,14 +121,30 @@ def find_resource(manager, name_or_id, **kwargs): (manager.resource_class.__name__.lower(), name_or_id) raise exceptions.CommandError(msg) else: - raise + pass + + try: + for resource in manager.list(): + # short circuit and return the first match + if (resource.get('id') == name_or_id or + resource.get('name') == name_or_id): + return resource + else: + # we found no match, keep going to bomb out + pass + except Exception: + # in case the list fails for some reason + pass + + # if we hit here, we've failed, report back this error: + msg = "Could not find resource %s" % name_or_id + raise exceptions.CommandError(msg) def format_dict(data): """Return a formatted string of key value pairs :param data: a dict - :param format: optional formatting hints :rtype: a string formatted to key='value' """ @@ -255,6 +297,7 @@ def wait_for_status(status_f, res_id, status_field='status', success_status=['active'], + error_status=['error'], sleep_time=5, callback=None): """Wait for status change on a resource during a long-running operation @@ -273,7 +316,7 @@ def wait_for_status(status_f, if status in success_status: retval = True break - elif status == 'error': + elif status in error_status: retval = False break if callback: @@ -376,3 +419,11 @@ def build_kwargs_dict(arg_name, value): if value: kwargs[arg_name] = value return kwargs + + +def is_ascii(string): + try: + string.decode('ascii') + return True + except UnicodeDecodeError: + return False diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 9dda32d6..23a4deca 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -15,6 +15,7 @@ import logging +from openstackclient.common import exceptions from openstackclient.common import utils LOG = logging.getLogger(__name__) @@ -26,32 +27,34 @@ API_VERSIONS = { "2": "novaclient.client", } +# Save the microversion if in use +_compute_api_version = None + def make_client(instance): """Returns a compute service client.""" - # Defer client imports until we actually need them + # Defer client import until we actually need them from novaclient import client as nova_client - from novaclient import extension - try: - from novaclient.v2.contrib import list_extensions - except ImportError: - from novaclient.v1_1.contrib import list_extensions - compute_client = nova_client.get_client_class( - instance._api_version[API_NAME], - ) - LOG.debug('Instantiating compute client: %s', compute_client) + if _compute_api_version is not None: + version = _compute_api_version + else: + version = instance._api_version[API_NAME] + + LOG.debug('Instantiating compute client for V%s' % version) # Set client http_log_debug to True if verbosity level is high enough http_log_debug = utils.get_effective_log_level() <= logging.DEBUG - extensions = [extension.Extension('list_extensions', list_extensions)] + extensions = [ext for ext in nova_client.discover_extensions(version) + if ext.name == "list_extensions"] # Remember interface only if it is set kwargs = utils.build_kwargs_dict('endpoint_type', instance._interface) - client = compute_client( + client = nova_client.Client( + version, session=instance.session, extensions=extensions, http_log_debug=http_log_debug, @@ -73,3 +76,45 @@ def build_option_parser(parser): DEFAULT_API_VERSION + ' (Env: OS_COMPUTE_API_VERSION)') return parser + + +def check_api_version(check_version): + """Validate version supplied by user + + Returns: + * True if version is OK + * False if the version has not been checked and the previous plugin + check should be performed + * throws an exception if the version is no good + + TODO(dtroyer): make the exception thrown a version-related one + """ + + # Defer client imports until we actually need them + try: + from novaclient import api_versions + except ImportError: + # Retain previous behaviour + return False + + import novaclient + + global _compute_api_version + + # Copy some logic from novaclient 2.27.0 for basic version detection + # NOTE(dtroyer): This is only enough to resume operations using API + # version 2.0 or any valid version supplied by the user. + _compute_api_version = api_versions.get_api_version(check_version) + + if _compute_api_version > api_versions.APIVersion("2.0"): + if not _compute_api_version.matches( + novaclient.API_MIN_VERSION, + novaclient.API_MAX_VERSION, + ): + raise exceptions.CommandError( + "versions supported by client: %s - %s" % ( + novaclient.API_MIN_VERSION.get_string(), + novaclient.API_MAX_VERSION.get_string(), + ), + ) + return True diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index 84ed5c7d..a1ba618f 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -123,8 +123,8 @@ class DeleteAggregate(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute data = utils.find_resource( @@ -256,8 +256,8 @@ class SetAggregate(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute aggregate = utils.find_resource( @@ -303,8 +303,8 @@ class ShowAggregate(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute data = utils.find_resource( diff --git a/openstackclient/compute/v2/availability_zone.py b/openstackclient/compute/v2/availability_zone.py index 648c0ee4..0fe6c73a 100644 --- a/openstackclient/compute/v2/availability_zone.py +++ b/openstackclient/compute/v2/availability_zone.py @@ -73,8 +73,8 @@ class ListAvailabilityZone(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('Zone Name', 'Zone Status', diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index 082a3a0c..aafa5d44 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -22,6 +22,7 @@ import sys from cliff import command from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -42,13 +43,14 @@ class ShowConsoleLog(command.Command): metavar='<num-lines>', type=int, default=None, + action=parseractions.NonNegativeAction, help='Number of lines to display from the end of the log ' '(default=all)', ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( @@ -103,8 +105,8 @@ class ShowConsoleURL(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( compute_client.servers, diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 3458cf79..7474580b 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -181,6 +181,15 @@ class ListFlavor(lister.Lister): action='store_true', default=False, help='List additional fields in output') + parser.add_argument( + '--marker', + metavar="<marker>", + help='The last flavor ID of the previous page') + parser.add_argument( + '--limit', + type=int, + metavar="<limit>", + help='Maximum number of flavors to display') return parser def take_action(self, parsed_args): @@ -202,7 +211,9 @@ class ListFlavor(lister.Lister): # and flavors from their own projects only. is_public = None if parsed_args.all else parsed_args.public - data = compute_client.flavors.list(is_public=is_public) + data = compute_client.flavors.list(is_public=is_public, + marker=parsed_args.marker, + limit=parsed_args.limit) if parsed_args.long: columns = columns + ( diff --git a/openstackclient/compute/v2/floatingip.py b/openstackclient/compute/v2/floatingip.py index 658f0d5a..c557c24b 100644 --- a/openstackclient/compute/v2/floatingip.py +++ b/openstackclient/compute/v2/floatingip.py @@ -69,8 +69,8 @@ class CreateFloatingIP(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute floating_ip = compute_client.floating_ips.create(parsed_args.pool) @@ -93,8 +93,8 @@ class DeleteFloatingIP(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute floating_ip = utils.find_resource( @@ -111,8 +111,8 @@ class ListFloatingIP(lister.Lister): log = logging.getLogger(__name__ + '.ListFloatingIP') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute columns = ('ID', 'Pool', 'IP', 'Fixed IP', 'Instance ID') diff --git a/openstackclient/compute/v2/floatingippool.py b/openstackclient/compute/v2/floatingippool.py index db1c9f0f..cc485552 100644 --- a/openstackclient/compute/v2/floatingippool.py +++ b/openstackclient/compute/v2/floatingippool.py @@ -27,8 +27,8 @@ class ListFloatingIPPool(lister.Lister): log = logging.getLogger(__name__ + '.ListFloatingIPPool') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute columns = ('Name',) diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index edf25f83..5c627c50 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -47,8 +47,8 @@ class CreateKeypair(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute public_key = parsed_args.public_key @@ -93,8 +93,8 @@ class DeleteKeypair(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute compute_client.keypairs.delete(parsed_args.name) return @@ -140,8 +140,8 @@ class ShowKeypair(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute keypair = utils.find_resource(compute_client.keypairs, parsed_args.name) diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py index 3dc9bae0..3dd0c49b 100644 --- a/openstackclient/compute/v2/security_group.py +++ b/openstackclient/compute/v2/security_group.py @@ -50,10 +50,10 @@ def _xform_security_group_rule(sgroup): info['ip_range'] = info['ip_range']['cidr'] else: info['ip_range'] = '' - if info['ip_protocol'] == 'icmp': - info['port_range'] = '' - elif info['ip_protocol'] is None: + if info['ip_protocol'] is None: info['ip_protocol'] = '' + elif info['ip_protocol'].lower() == 'icmp': + info['port_range'] = '' return info @@ -107,8 +107,8 @@ class DeleteSecurityGroup(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute data = utils.find_resource( @@ -199,8 +199,8 @@ class SetSecurityGroup(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute data = utils.find_resource( @@ -240,8 +240,8 @@ class ShowSecurityGroup(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute info = {} @@ -307,7 +307,10 @@ class CreateSecurityGroupRule(show.ShowOne): compute_client.security_groups, parsed_args.group, ) - from_port, to_port = parsed_args.dst_port + if parsed_args.proto.lower() == 'icmp': + from_port, to_port = -1, -1 + else: + from_port, to_port = parsed_args.dst_port data = compute_client.security_group_rules.create( group.id, parsed_args.proto, @@ -334,8 +337,8 @@ class DeleteSecurityGroupRule(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute compute_client.security_group_rules.delete(parsed_args.rule) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 4efef975..aa4569c3 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -36,13 +36,14 @@ from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils from openstackclient.i18n import _ # noqa +from openstackclient.identity import common as identity_common from openstackclient.network import common def _format_servers_list_networks(networks): """Return a formatted string of a server's networks - :param server: a Server.networks field + :param networks: a Server.networks field :rtype: a string of formatted network addresses """ output = [] @@ -55,6 +56,29 @@ def _format_servers_list_networks(networks): return '; '.join(output) +def _format_servers_list_power_state(state): + """Return a formatted string of a server's power state + + :param state: the power state number of a server + :rtype: a string mapped to the power state number + """ + power_states = [ + 'NOSTATE', # 0x00 + 'Running', # 0x01 + '', # 0x02 + 'Paused', # 0x03 + 'Shutdown', # 0x04 + '', # 0x05 + 'Crashed', # 0x06 + 'Suspended' # 0x07 + ] + + try: + return power_states[state] + except Exception: + return 'N/A' + + def _get_ip_address(addresses, address_type, ip_address_family): # Old style addresses if address_type in addresses: @@ -107,14 +131,20 @@ def _prep_server_detail(compute_client, server): image_info = info.get('image', {}) if image_info: image_id = image_info.get('id', '') - image = utils.find_resource(compute_client.images, image_id) - info['image'] = "%s (%s)" % (image.name, image_id) + try: + image = utils.find_resource(compute_client.images, image_id) + info['image'] = "%s (%s)" % (image.name, image_id) + except Exception: + info['image'] = image_id # Convert the flavor blob to a name flavor_info = info.get('flavor', {}) flavor_id = flavor_info.get('id', '') - flavor = utils.find_resource(compute_client.flavors, flavor_id) - info['flavor'] = "%s (%s)" % (flavor.name, flavor_id) + try: + flavor = utils.find_resource(compute_client.flavors, flavor_id) + info['flavor'] = "%s (%s)" % (flavor.name, flavor_id) + except Exception: + info['flavor'] = flavor_id # NOTE(dtroyer): novaclient splits these into separate entries... # Format addresses in a useful way @@ -355,8 +385,8 @@ class CreateServer(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume @@ -418,10 +448,17 @@ class CreateServer(show.ShowOne): dev_key, dev_vol = dev_map.split('=', 1) block_volume = None if dev_vol: - block_volume = utils.find_resource( - volume_client.volumes, - dev_vol, - ).id + vol = dev_vol.split(':', 1)[0] + if vol: + vol_id = utils.find_resource( + volume_client.volumes, + vol, + ).id + block_volume = dev_vol.replace(vol, vol_id) + else: + msg = _("Volume name or ID must be specified if " + "--block-device-mapping is specified") + raise exceptions.CommandError(msg) block_device_mapping.update({dev_key: block_volume}) nics = [] @@ -553,8 +590,8 @@ class CreateServerImage(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute image_client = self.app.client_manager.image server = utils.find_resource( @@ -600,7 +637,7 @@ class DeleteServer(command.Command): def get_parser(self, prog_name): parser = super(DeleteServer, self).get_parser(prog_name) parser.add_argument( - 'servers', + 'server', metavar='<server>', nargs="+", help=_('Server(s) to delete (name or ID)'), @@ -612,10 +649,10 @@ class DeleteServer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute - for server in parsed_args.servers: + for server in parsed_args.server: server_obj = utils.find_resource( compute_client.servers, server) compute_client.servers.delete(server_obj.id) @@ -694,6 +731,17 @@ class ListServer(lister.Lister): help=_('Include all projects (admin only)'), ) parser.add_argument( + '--project', + metavar='<project>', + help="Search by project (admin only) (name or ID)") + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--user', + metavar='<user>', + help=_('Search by user (admin only) (name or ID)'), + ) + identity_common.add_user_domain_option_to_parser(parser) + parser.add_argument( '--long', action='store_true', default=False, @@ -701,9 +749,28 @@ class ListServer(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute + identity_client = self.app.client_manager.identity + + project_id = None + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + parsed_args.all_projects = True + + user_id = None + if parsed_args.user: + user_id = identity_common.find_user( + identity_client, + parsed_args.user, + parsed_args.user_domain, + ).id + search_opts = { 'reservation_id': parsed_args.reservation_id, 'ip': parsed_args.ip, @@ -714,7 +781,9 @@ class ListServer(lister.Lister): 'flavor': parsed_args.flavor, 'image': parsed_args.image, 'host': parsed_args.host, + 'tenant_id': project_id, 'all_tenants': parsed_args.all_projects, + 'user_id': user_id, } self.log.debug('search options: %s', search_opts) @@ -723,6 +792,8 @@ class ListServer(lister.Lister): 'ID', 'Name', 'Status', + 'OS-EXT-STS:task_state', + 'OS-EXT-STS:power_state', 'Networks', 'OS-EXT-AZ:availability_zone', 'OS-EXT-SRV-ATTR:host', @@ -732,18 +803,32 @@ class ListServer(lister.Lister): 'ID', 'Name', 'Status', + 'Task State', + 'Power State', 'Networks', 'Availability Zone', 'Host', 'Properties', ) mixed_case_fields = [ + 'OS-EXT-STS:task_state', + 'OS-EXT-STS:power_state', 'OS-EXT-AZ:availability_zone', 'OS-EXT-SRV-ATTR:host', ] else: - columns = ('ID', 'Name', 'Status', 'Networks') - column_headers = columns + columns = ( + 'ID', + 'Name', + 'Status', + 'Networks', + ) + column_headers = ( + 'ID', + 'Name', + 'Status', + 'Networks', + ) mixed_case_fields = [] data = compute_client.servers.list(search_opts=search_opts) return (column_headers, @@ -751,6 +836,8 @@ class ListServer(lister.Lister): s, columns, mixed_case_fields=mixed_case_fields, formatters={ + 'OS-EXT-STS:power_state': + _format_servers_list_power_state, 'Networks': _format_servers_list_networks, 'Metadata': utils.format_dict, }, @@ -759,7 +846,7 @@ class ListServer(lister.Lister): class LockServer(command.Command): - """Lock a server. A non-admin user will not be able to execute actions""" + """Lock server(s). A non-admin user will not be able to execute actions""" log = logging.getLogger(__name__ + '.LockServer') @@ -768,18 +855,20 @@ class LockServer(command.Command): parser.add_argument( 'server', metavar='<server>', - help=_('Server (name or ID)'), + nargs='+', + help=_('Server(s) to lock (name or ID)'), ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute - utils.find_resource( - compute_client.servers, - parsed_args.server, - ).lock() + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).lock() # FIXME(dtroyer): Here is what I want, how with argparse/cliff? @@ -845,8 +934,8 @@ class MigrateServer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute @@ -876,7 +965,7 @@ class MigrateServer(command.Command): class PauseServer(command.Command): - """Pause server""" + """Pause server(s)""" log = logging.getLogger(__name__ + '.PauseServer') @@ -885,18 +974,19 @@ class PauseServer(command.Command): parser.add_argument( 'server', metavar='<server>', - help=_('Server (name or ID)'), + nargs='+', + help=_('Server(s) to pause (name or ID)'), ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) - compute_client = self.app.client_manager.compute - utils.find_resource( - compute_client.servers, - parsed_args.server, - ).pause() + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server + ).pause() class RebootServer(command.Command): @@ -935,8 +1025,8 @@ class RebootServer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( compute_client.servers, parsed_args.server) @@ -984,8 +1074,8 @@ class RebuildServer(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute # Lookup parsed_args.image @@ -1100,8 +1190,8 @@ class RescueServer(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute _, body = utils.find_resource( @@ -1146,8 +1236,8 @@ class ResizeServer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( @@ -1191,8 +1281,8 @@ class ResumeServer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute utils.find_resource( @@ -1232,8 +1322,8 @@ class SetServer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( @@ -1280,8 +1370,8 @@ class ShowServer(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource(compute_client.servers, parsed_args.server) @@ -1403,8 +1493,8 @@ class SshServer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( @@ -1444,6 +1534,56 @@ class SshServer(command.Command): os.system(cmd % (login, ip_address)) +class StartServer(command.Command): + """Start server(s).""" + + log = logging.getLogger(__name__ + '.StartServer') + + def get_parser(self, prog_name): + parser = super(StartServer, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='<server>', + nargs="+", + help=_('Server(s) to start (name or ID)'), + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).start() + + +class StopServer(command.Command): + """Stop server(s).""" + + log = logging.getLogger(__name__ + '.StopServer') + + def get_parser(self, prog_name): + parser = super(StopServer, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='<server>', + nargs="+", + help=_('Server(s) to stop (name or ID)'), + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).stop() + + class SuspendServer(command.Command): """Suspend server""" @@ -1458,8 +1598,8 @@ class SuspendServer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute utils.find_resource( @@ -1469,7 +1609,7 @@ class SuspendServer(command.Command): class UnlockServer(command.Command): - """Unlock server""" + """Unlock server(s)""" log = logging.getLogger(__name__ + '.UnlockServer') @@ -1478,22 +1618,24 @@ class UnlockServer(command.Command): parser.add_argument( 'server', metavar='<server>', - help=_('Server (name or ID)'), + nargs='+', + help=_('Server(s) to unlock (name or ID)'), ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute - utils.find_resource( - compute_client.servers, - parsed_args.server, - ).unlock() + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).unlock() class UnpauseServer(command.Command): - """Unpause server""" + """Unpause server(s)""" log = logging.getLogger(__name__ + '.UnpauseServer') @@ -1502,18 +1644,20 @@ class UnpauseServer(command.Command): parser.add_argument( 'server', metavar='<server>', - help=_('Server (name or ID)'), + nargs='+', + help=_('Server(s) to unpause (name or ID)'), ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute - utils.find_resource( - compute_client.servers, - parsed_args.server, - ).unpause() + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).unpause() class UnrescueServer(command.Command): @@ -1530,8 +1674,8 @@ class UnrescueServer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute utils.find_resource( @@ -1562,8 +1706,8 @@ class UnsetServer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( compute_client.servers, diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 4b2ebac6..c2d51c2a 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -17,11 +17,33 @@ import logging +from cliff import command from cliff import lister from openstackclient.common import utils +class DeleteService(command.Command): + """Delete service command""" + + log = logging.getLogger(__name__ + ".DeleteService") + + def get_parser(self, prog_name): + parser = super(DeleteService, self).get_parser(prog_name) + parser.add_argument( + "service", + metavar="<service>", + help="Compute service to delete (ID only)") + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + compute_client = self.app.client_manager.compute + + compute_client.services.delete(parsed_args.service) + return + + class ListService(lister.Lister): """List service command""" @@ -43,6 +65,7 @@ class ListService(lister.Lister): self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( + "Id", "Binary", "Host", "Zone", diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 31b90c22..4e7cf100 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -183,25 +183,16 @@ class ShowUsage(show.ShowOne): )) info = {} - info['Servers'] = len(usage.server_usages) - info['RAM MB-Hours'] = float("%.2f" % usage.total_memory_mb_usage) - info['CPU Hours'] = float("%.2f" % usage.total_vcpus_usage) - info['Disk GB-Hours'] = float("%.2f" % usage.total_local_gb_usage) + info['Servers'] = ( + len(usage.server_usages) + if hasattr(usage, "server_usages") else None) + info['RAM MB-Hours'] = ( + float("%.2f" % usage.total_memory_mb_usage) + if hasattr(usage, "total_memory_mb_usage") else None) + info['CPU Hours'] = ( + float("%.2f" % usage.total_vcpus_usage) + if hasattr(usage, "total_vcpus_usage") else None) + info['Disk GB-Hours'] = ( + float("%.2f" % usage.total_local_gb_usage) + if hasattr(usage, "total_local_gb_usage") else None) return zip(*sorted(six.iteritems(info))) - - -# This is out of order due to the subclass, will eventually be removed - -class ListProjectUsage(ListUsage): - """List resource usage per project""" - - deprecated = True - - log = logging.getLogger('DEPRECATED:') - - def take_action(self, parsed_args): - self.log.warning( - "%s is deprecated, use 'usage list'", - getattr(self, 'cmd_name', 'this command'), - ) - return super(ListProjectUsage, self).take_action(parsed_args) diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index b8bb33f4..bd882ce8 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -21,7 +21,7 @@ from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_API_VERSION = '2' +DEFAULT_API_VERSION = '3' API_VERSION_OPTION = 'os_identity_api_version' API_NAME = 'identity' API_VERSIONS = { diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index e166c855..9bc2755a 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -42,8 +42,8 @@ class ListCatalog(lister.Lister): log = logging.getLogger(__name__ + '.ListCatalog') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) # This is ugly because if auth hasn't happened yet we need # to trigger it here. @@ -76,8 +76,8 @@ class ShowCatalog(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) # This is ugly because if auth hasn't happened yet we need # to trigger it here. diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index 348479ac..a7730ce2 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -52,8 +52,8 @@ class CreateEC2Creds(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.project: @@ -105,8 +105,8 @@ class DeleteEC2Creds(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.user: @@ -135,8 +135,8 @@ class ListEC2Creds(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.user: @@ -178,8 +178,8 @@ class ShowEC2Creds(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.user: diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index 370a931d..1744cc72 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -62,8 +62,8 @@ class CreateEndpoint(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) endpoint = identity_client.endpoints.create( @@ -93,8 +93,8 @@ class DeleteEndpoint(command.Command): help=_('Endpoint ID to delete')) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_client.endpoints.delete(parsed_args.endpoint) return @@ -115,8 +115,8 @@ class ListEndpoint(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.long: columns = ('ID', 'Region', 'Service Name', 'Service Type', @@ -150,8 +150,8 @@ class ShowEndpoint(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity data = identity_client.endpoints.list() match = None diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index d01807a6..97a95f28 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -70,8 +70,8 @@ class CreateProject(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity enabled = True @@ -118,8 +118,8 @@ class DeleteProject(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity for project in parsed_args.projects: @@ -146,8 +146,8 @@ class ListProject(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('ID', 'Name', 'Description', 'Enabled') else: @@ -202,8 +202,8 @@ class SetProject(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if (not parsed_args.name @@ -253,8 +253,8 @@ class ShowProject(show.ShowOne): help=_('Project to display (name or ID)')) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity info = {} diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 3167f50f..e98f8cb3 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -54,8 +54,8 @@ class AddRole(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = utils.find_resource(identity_client.roles, parsed_args.role) project = utils.find_resource( @@ -93,8 +93,8 @@ class CreateRole(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity try: role = identity_client.roles.create(parsed_args.role_name) @@ -128,8 +128,8 @@ class DeleteRole(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity for role in parsed_args.roles: @@ -160,8 +160,8 @@ class ListRole(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity auth_ref = self.app.client_manager.auth_ref @@ -242,8 +242,8 @@ class ListUserRole(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity auth_ref = self.app.client_manager.auth_ref @@ -316,8 +316,8 @@ class RemoveRole(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = utils.find_resource(identity_client.roles, parsed_args.role) project = utils.find_resource( @@ -345,8 +345,8 @@ class ShowRole(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = utils.find_resource(identity_client.roles, parsed_args.role) diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index f8630238..c9d48441 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -59,8 +59,8 @@ class CreateService(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity type_or_name = parsed_args.type_or_name @@ -106,8 +106,8 @@ class DeleteService(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) identity_client.services.delete(service.id) @@ -129,8 +129,8 @@ class ListService(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('ID', 'Name', 'Type', 'Description') @@ -163,8 +163,8 @@ class ShowService(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity auth_ref = self.app.client_manager.auth_ref diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index c8b003ee..5fed58e5 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -21,6 +21,7 @@ import six from cliff import command from cliff import show +from openstackclient.common import utils from openstackclient.i18n import _ # noqa @@ -33,8 +34,8 @@ class IssueToken(show.ShowOne): parser = super(IssueToken, self).get_parser(prog_name) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) token = self.app.client_manager.auth_ref.service_catalog.get_token() token['project_id'] = token.pop('tenant_id') diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 500aeec3..76902e69 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -78,8 +78,8 @@ class CreateUser(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.project: @@ -142,8 +142,8 @@ class DeleteUser(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity for user in parsed_args.users: @@ -174,8 +174,8 @@ class ListUser(lister.Lister): help=_('List additional fields in output')) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity def _format_project(project): @@ -296,8 +296,8 @@ class SetUser(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.password_prompt: @@ -362,8 +362,8 @@ class ShowUser(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity info = {} diff --git a/openstackclient/identity/v3/catalog.py b/openstackclient/identity/v3/catalog.py index 1899f25e..76f7da51 100644 --- a/openstackclient/identity/v3/catalog.py +++ b/openstackclient/identity/v3/catalog.py @@ -39,8 +39,8 @@ class ListCatalog(lister.Lister): log = logging.getLogger(__name__ + '.ListCatalog') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) # This is ugly because if auth hasn't happened yet we need # to trigger it here. @@ -73,8 +73,8 @@ class ShowCatalog(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) # This is ugly because if auth hasn't happened yet we need # to trigger it here. diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index ffbd5104..0a6ade67 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -40,8 +40,8 @@ class CreateConsumer(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = identity_client.oauth1.consumers.create( parsed_args.description @@ -64,8 +64,8 @@ class DeleteConsumer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = utils.find_resource( identity_client.oauth1.consumers, parsed_args.consumer) @@ -78,8 +78,8 @@ class ListConsumer(lister.Lister): log = logging.getLogger(__name__ + '.ListConsumer') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Description') data = self.app.client_manager.identity.oauth1.consumers.list() return (columns, @@ -108,8 +108,8 @@ class SetConsumer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = utils.find_resource( identity_client.oauth1.consumers, parsed_args.consumer) @@ -140,8 +140,8 @@ class ShowConsumer(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = utils.find_resource( identity_client.oauth1.consumers, parsed_args.consumer) diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index dacbc9af..f22092d4 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -17,7 +17,6 @@ import logging import six -import sys from cliff import command from cliff import lister @@ -57,8 +56,8 @@ class CreateCredential(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user_id = utils.find_resource(identity_client.users, parsed_args.user).id @@ -91,8 +90,8 @@ class DeleteCredential(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_client.credentials.delete(parsed_args.credential) return @@ -103,8 +102,8 @@ class ListCredential(lister.Lister): log = logging.getLogger(__name__ + '.ListCredential') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Type', 'User ID', 'Blob', 'Project ID') column_headers = ('ID', 'Type', 'User ID', 'Data', 'Project ID') data = self.app.client_manager.identity.credentials.list() @@ -130,17 +129,20 @@ class SetCredential(command.Command): parser.add_argument( '--user', metavar='<user>', + required=True, help='Name or ID of user that owns the credential', ) parser.add_argument( '--type', metavar='<type>', choices=['ec2', 'cert'], + required=True, help='New credential type', ) parser.add_argument( '--data', metavar='<data>', + required=True, help='New credential data', ) parser.add_argument( @@ -150,28 +152,25 @@ class SetCredential(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - kwargs = {} - if parsed_args.user: - user_id = utils.find_resource(identity_client.users, - parsed_args.user).id - if user_id: - kwargs['user'] = user_id - if parsed_args.type: - kwargs['type'] = parsed_args.type - if parsed_args.data: - kwargs['data'] = parsed_args.data + + user_id = utils.find_resource(identity_client.users, + parsed_args.user).id + if parsed_args.project: project = utils.find_resource(identity_client.projects, parsed_args.project).id - kwargs['project'] = project + else: + project = None + + identity_client.credentials.update(parsed_args.credential, + user=user_id, + type=parsed_args.type, + blob=parsed_args.data, + project=project) - if not kwargs: - sys.stdout.write("Credential not updated, no arguments present") - return - identity_client.credentials.update(parsed_args.credential, **kwargs) return @@ -189,8 +188,8 @@ class ShowCredential(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity credential = utils.find_resource(identity_client.credentials, parsed_args.credential) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 38f99a97..aec530a8 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -63,8 +63,8 @@ class CreateDomain(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity enabled = True @@ -103,8 +103,8 @@ class DeleteDomain(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = utils.find_resource(identity_client.domains, parsed_args.domain) @@ -117,8 +117,8 @@ class ListDomain(lister.Lister): log = logging.getLogger(__name__ + '.ListDomain') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Name', 'Enabled', 'Description') data = self.app.client_manager.identity.domains.list() return (columns, @@ -163,8 +163,8 @@ class SetDomain(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = utils.find_resource(identity_client.domains, parsed_args.domain) @@ -200,8 +200,8 @@ class ShowDomain(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = utils.find_resource(identity_client.domains, parsed_args.domain) diff --git a/openstackclient/identity/v3/ec2creds.py b/openstackclient/identity/v3/ec2creds.py index 03314634..a12ee25e 100644 --- a/openstackclient/identity/v3/ec2creds.py +++ b/openstackclient/identity/v3/ec2creds.py @@ -79,8 +79,8 @@ class CreateEC2Creds(show.ShowOne): common.add_project_domain_option_to_parser(parser) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity client_manager = self.app.client_manager user = _determine_ec2_user(parsed_args, client_manager) @@ -136,8 +136,8 @@ class DeleteEC2Creds(command.Command): common.add_user_domain_option_to_parser(parser) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) client_manager = self.app.client_manager user = _determine_ec2_user(parsed_args, client_manager) client_manager.identity.ec2.delete(user, parsed_args.access_key) @@ -158,8 +158,8 @@ class ListEC2Creds(lister.Lister): common.add_user_domain_option_to_parser(parser) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) client_manager = self.app.client_manager user = _determine_ec2_user(parsed_args, client_manager) @@ -194,8 +194,8 @@ class ShowEC2Creds(show.ShowOne): common.add_user_domain_option_to_parser(parser) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) client_manager = self.app.client_manager user = _determine_ec2_user(parsed_args, client_manager) creds = client_manager.identity.ec2.get(user, parsed_args.access_key) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 52db5ace..3d1c6f54 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -78,8 +78,8 @@ class CreateEndpoint(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) @@ -113,8 +113,8 @@ class DeleteEndpoint(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity endpoint_id = utils.find_resource(identity_client.endpoints, parsed_args.endpoint).id @@ -147,8 +147,8 @@ class ListEndpoint(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity columns = ('ID', 'Region', 'Service Name', 'Service Type', 'Enabled', 'Interface', 'URL') @@ -221,8 +221,8 @@ class SetEndpoint(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity endpoint = utils.find_resource(identity_client.endpoints, parsed_args.endpoint) @@ -270,8 +270,8 @@ class ShowEndpoint(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity endpoint = utils.find_resource(identity_client.endpoints, parsed_args.endpoint) diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index 57e8255e..20877fcd 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -50,8 +50,8 @@ class CreateProtocol(show.ShowOne): return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity protocol = identity_client.federation.protocols.create( protocol_id=parsed_args.federation_protocol, @@ -88,8 +88,8 @@ class DeleteProtocol(command.Command): return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_client.federation.protocols.delete( parsed_args.identity_provider, parsed_args.federation_protocol) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index d659f71e..82975065 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -50,8 +50,8 @@ class AddUserToGroup(command.Command): common.add_user_domain_option_to_parser(parser) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user_id = common.find_user(identity_client, @@ -92,8 +92,8 @@ class CheckUserInGroup(command.Command): common.add_user_domain_option_to_parser(parser) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user_id = common.find_user(identity_client, @@ -142,8 +142,8 @@ class CreateGroup(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = None @@ -188,8 +188,8 @@ class DeleteGroup(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity for group in parsed_args.groups: @@ -226,8 +226,8 @@ class ListGroup(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = None @@ -284,8 +284,8 @@ class RemoveUserFromGroup(command.Command): common.add_user_domain_option_to_parser(parser) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user_id = common.find_user(identity_client, @@ -331,8 +331,8 @@ class SetGroup(command.Command): help='New group description') return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity group = common.find_group(identity_client, parsed_args.group, parsed_args.domain) @@ -368,8 +368,8 @@ class ShowGroup(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity group = common.find_group(identity_client, diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index 80965800..4e086b68 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -70,8 +70,8 @@ class CreateIdentityProvider(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.remote_id_file: file_content = utils.read_blob_file_contents( @@ -88,6 +88,8 @@ class CreateIdentityProvider(show.ShowOne): enabled=parsed_args.enabled) idp._info.pop('links', None) + remote_ids = utils.format_list(idp._info.pop('remote_ids', [])) + idp._info['remote_ids'] = remote_ids return zip(*sorted(six.iteritems(idp._info))) @@ -105,8 +107,8 @@ class DeleteIdentityProvider(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_client.federation.identity_providers.delete( parsed_args.identity_provider) @@ -118,8 +120,8 @@ class ListIdentityProvider(lister.Lister): log = logging.getLogger(__name__ + '.ListIdentityProvider') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Enabled', 'Description') identity_client = self.app.client_manager.identity data = identity_client.federation.identity_providers.list() @@ -142,6 +144,11 @@ class SetIdentityProvider(command.Command): metavar='<identity-provider>', help='Identity provider to modify', ) + parser.add_argument( + '--description', + metavar='<description>', + help='Set identity provider description', + ) identity_remote_id_provider = parser.add_mutually_exclusive_group() identity_remote_id_provider.add_argument( '--remote-id', @@ -169,13 +176,15 @@ class SetIdentityProvider(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) federation_client = self.app.client_manager.identity.federation # Basic argument checking - if (not parsed_args.enable and not parsed_args.disable and not - parsed_args.remote_id and not parsed_args.remote_id_file): + if (not parsed_args.enable and not parsed_args.disable and + not parsed_args.remote_id and + not parsed_args.remote_id_file and + not parsed_args.description): self.log.error('No changes requested') return (None, None) @@ -190,6 +199,8 @@ class SetIdentityProvider(command.Command): # Setup keyword args for the client kwargs = {} + if parsed_args.description: + kwargs['description'] = parsed_args.description if parsed_args.enable: kwargs['enabled'] = True if parsed_args.disable: @@ -218,12 +229,14 @@ class ShowIdentityProvider(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - identity_provider = utils.find_resource( + idp = utils.find_resource( identity_client.federation.identity_providers, parsed_args.identity_provider) - identity_provider._info.pop('links', None) - return zip(*sorted(six.iteritems(identity_provider._info))) + idp._info.pop('links', None) + remote_ids = utils.format_list(idp._info.pop('remote_ids', [])) + idp._info['remote_ids'] = remote_ids + return zip(*sorted(six.iteritems(idp._info))) diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index 82935423..9da94863 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -47,8 +47,8 @@ class CreatePolicy(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) blob = utils.read_blob_file_contents(parsed_args.rules) identity_client = self.app.client_manager.identity @@ -75,8 +75,8 @@ class DeletePolicy(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_client.policies.delete(parsed_args.policy) return @@ -97,8 +97,8 @@ class ListPolicy(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('ID', 'Type', 'Blob') column_headers = ('ID', 'Type', 'Rules') @@ -137,8 +137,8 @@ class SetPolicy(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity blob = None @@ -172,8 +172,8 @@ class ShowPolicy(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity policy = utils.find_resource(identity_client.policies, parsed_args.policy) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 8185d65a..96d7f97b 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -81,8 +81,8 @@ class CreateProject(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = None @@ -146,8 +146,8 @@ class DeleteProject(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = None @@ -190,8 +190,8 @@ class ListProject(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.long: columns = ('ID', 'Name', 'Domain ID', 'Description', 'Enabled') @@ -271,8 +271,8 @@ class SetProject(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if (not parsed_args.name @@ -337,8 +337,8 @@ class ShowProject(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.domain: diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py index 5cb51fc5..1ff0b8c0 100644 --- a/openstackclient/identity/v3/region.py +++ b/openstackclient/identity/v3/region.py @@ -48,21 +48,14 @@ class CreateRegion(show.ShowOne): metavar='<description>', help=_('New region description'), ) - parser.add_argument( - '--url', - metavar='<url>', - help=_('New region url'), - ) - return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity region = identity_client.regions.create( id=parsed_args.region, - url=parsed_args.url, parent_region=parsed_args.parent_region, description=parsed_args.description, ) @@ -87,8 +80,8 @@ class DeleteRegion(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_client.regions.delete(parsed_args.region) @@ -109,16 +102,16 @@ class ListRegion(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity kwargs = {} if parsed_args.parent_region: kwargs['parent_region_id'] = parsed_args.parent_region - columns_headers = ('Region', 'Parent Region', 'Description', 'URL') - columns = ('ID', 'Parent Region Id', 'Description', 'URL') + columns_headers = ('Region', 'Parent Region', 'Description') + columns = ('ID', 'Parent Region Id', 'Description') data = identity_client.regions.list(**kwargs) return (columns_headers, @@ -150,25 +143,16 @@ class SetRegion(command.Command): metavar='<description>', help=_('New region description'), ) - parser.add_argument( - '--url', - metavar='<url>', - help=_('New region url'), - ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - if (not parsed_args.url - and not parsed_args.parent_region - and not parsed_args.description): + if not parsed_args.parent_region and not parsed_args.description: return kwargs = {} - if parsed_args.url: - kwargs['url'] = parsed_args.url if parsed_args.description: kwargs['description'] = parsed_args.description if parsed_args.parent_region: @@ -192,8 +176,8 @@ class ShowRegion(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity region = utils.find_resource(identity_client.regions, diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 9243639e..0e8c51ca 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -39,7 +39,7 @@ def _add_identity_and_resource_options_to_parser(parser): domain_or_project.add_argument( '--project', metavar='<project>', - help='Include `<project>` (name or ID)', + help='Include <project> (name or ID)', ) user_or_group = parser.add_mutually_exclusive_group() user_or_group.add_argument( @@ -122,8 +122,8 @@ class AddRole(command.Command): _add_identity_and_resource_options_to_parser(parser) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if (not parsed_args.user and not parsed_args.domain @@ -166,8 +166,8 @@ class CreateRole(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity try: @@ -199,8 +199,8 @@ class DeleteRole(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity for role in parsed_args.roles: @@ -260,6 +260,7 @@ class ListRole(lister.Lister): data = identity_client.roles.list( user=user, domain=domain, + os_inherit_extension_inherited=parsed_args.inherited ) for user_role in data: user_role.user = user.name @@ -269,6 +270,7 @@ class ListRole(lister.Lister): data = identity_client.roles.list( user=user, project=project, + os_inherit_extension_inherited=parsed_args.inherited ) for user_role in data: user_role.user = user.name @@ -278,12 +280,14 @@ class ListRole(lister.Lister): data = identity_client.roles.list( user=user, domain='default', + os_inherit_extension_inherited=parsed_args.inherited ) elif parsed_args.group and parsed_args.domain: columns = ('ID', 'Name', 'Domain', 'Group') data = identity_client.roles.list( group=group, domain=domain, + os_inherit_extension_inherited=parsed_args.inherited ) for group_role in data: group_role.group = group.name @@ -293,6 +297,7 @@ class ListRole(lister.Lister): data = identity_client.roles.list( group=group, project=project, + os_inherit_extension_inherited=parsed_args.inherited ) for group_role in data: group_role.group = group.name @@ -325,8 +330,8 @@ class RemoveRole(command.Command): _add_identity_and_resource_options_to_parser(parser) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if (not parsed_args.user and not parsed_args.domain @@ -368,8 +373,8 @@ class SetRole(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if not parsed_args.name: @@ -398,8 +403,8 @@ class ShowRole(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = utils.find_resource( diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index d63a9537..85081aa7 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -61,8 +61,8 @@ class CreateService(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity enabled = True @@ -94,8 +94,8 @@ class DeleteService(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) @@ -119,8 +119,8 @@ class ListService(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('ID', 'Name', 'Type', 'Description', 'Enabled') @@ -173,8 +173,8 @@ class SetService(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if (not parsed_args.name @@ -219,8 +219,8 @@ class ShowService(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) diff --git a/openstackclient/identity/v3/service_provider.py b/openstackclient/identity/v3/service_provider.py index 78f96b6c..838ad4a2 100644 --- a/openstackclient/identity/v3/service_provider.py +++ b/openstackclient/identity/v3/service_provider.py @@ -73,8 +73,8 @@ class CreateServiceProvider(show.ShowOne): return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) service_client = self.app.client_manager.identity sp = service_client.federation.service_providers.create( id=parsed_args.service_provider_id, @@ -101,8 +101,8 @@ class DeleteServiceProvider(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) service_client = self.app.client_manager.identity service_client.federation.service_providers.delete( parsed_args.service_provider) @@ -114,8 +114,8 @@ class ListServiceProvider(lister.Lister): log = logging.getLogger(__name__ + '.ListServiceProvider') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) service_client = self.app.client_manager.identity data = service_client.federation.service_providers.list() @@ -168,8 +168,8 @@ class SetServiceProvider(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) federation_client = self.app.client_manager.identity.federation enabled = None @@ -207,8 +207,8 @@ class ShowServiceProvider(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) service_client = self.app.client_manager.identity service_provider = utils.find_resource( service_client.federation.service_providers, diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 7000b62c..cd3dc798 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -182,8 +182,8 @@ class IssueToken(show.ShowOne): parser = super(IssueToken, self).get_parser(prog_name) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) token = self.app.client_manager.auth_ref.service_catalog.get_token() if 'tenant_id' in token: token['project_id'] = token.pop('tenant_id') diff --git a/openstackclient/identity/v3/unscoped_saml.py b/openstackclient/identity/v3/unscoped_saml.py index fddac68f..e659e75e 100644 --- a/openstackclient/identity/v3/unscoped_saml.py +++ b/openstackclient/identity/v3/unscoped_saml.py @@ -49,8 +49,8 @@ class ListAccessibleDomains(lister.Lister): log = logging.getLogger(__name__ + '.ListAccessibleDomains') @auth_with_unscoped_saml + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Enabled', 'Name', 'Description') identity_client = self.app.client_manager.identity data = identity_client.federation.domains.list() @@ -67,8 +67,8 @@ class ListAccessibleProjects(lister.Lister): log = logging.getLogger(__name__ + '.ListAccessibleProjects') @auth_with_unscoped_saml + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Domain ID', 'Enabled', 'Name') identity_client = self.app.client_manager.identity data = identity_client.federation.projects.list() diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 459707d2..0e894544 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -91,8 +91,8 @@ class CreateUser(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity project_id = None @@ -155,8 +155,8 @@ class DeleteUser(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = None @@ -205,8 +205,8 @@ class ListUser(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = None @@ -214,13 +214,11 @@ class ListUser(lister.Lister): domain = common.find_domain(identity_client, parsed_args.domain).id + group = None if parsed_args.group: - group = utils.find_resource( - identity_client.groups, - parsed_args.group, - ).id - else: - group = None + group = common.find_group(identity_client, + parsed_args.group, + parsed_args.domain).id if parsed_args.project: if domain is not None: @@ -336,8 +334,8 @@ class SetUser(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.password_prompt: @@ -396,8 +394,8 @@ class SetPasswordUser(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity current_password = utils.get_password( @@ -430,8 +428,8 @@ class ShowUser(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.domain: diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 68c81cd5..35e9ef43 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -213,6 +213,8 @@ class CreateImage(show.ShowOne): if parsed_args.private: kwargs['is_public'] = False + info = {} + if not parsed_args.location and not parsed_args.copy_from: if parsed_args.volume: volume_client = self.app.client_manager.volume @@ -241,18 +243,18 @@ class CreateImage(show.ShowOne): # do a chunked transfer kwargs["data"] = sys.stdin - # Wrap the call to catch exceptions in order to close files - try: - image = image_client.images.create(**kwargs) - finally: - # Clean up open files - make sure data isn't a string - if ('data' in kwargs and hasattr(kwargs['data'], 'close') and - kwargs['data'] != sys.stdin): - kwargs['data'].close() - - info = {} - info.update(image._info) - info['properties'] = utils.format_dict(info.get('properties', {})) + if not parsed_args.volume: + # Wrap the call to catch exceptions in order to close files + try: + image = image_client.images.create(**kwargs) + finally: + # Clean up open files - make sure data isn't a string + if ('data' in kwargs and hasattr(kwargs['data'], 'close') and + kwargs['data'] != sys.stdin): + kwargs['data'].close() + + info.update(image._info) + info['properties'] = utils.format_dict(info.get('properties', {})) return zip(*sorted(six.iteritems(info))) @@ -452,7 +454,7 @@ class SaveImage(command.Command): gc_utils.save_image(data, parsed_args.file) -class SetImage(show.ShowOne): +class SetImage(command.Command): """Set image properties""" log = logging.getLogger(__name__ + ".SetImage") @@ -629,7 +631,7 @@ class SetImage(show.ShowOne): volume_client.volumes, parsed_args.volume, ) - response, body = volume_client.volumes.upload_to_image( + volume_client.volumes.upload_to_image( source_volume.id, parsed_args.force, parsed_args.image, @@ -640,7 +642,6 @@ class SetImage(show.ShowOne): if parsed_args.disk_format else image.disk_format), ) - info = body['os-volume_upload_image'] elif parsed_args.file: # Send an open file handle to glanceclient so it will # do a chunked transfer @@ -673,10 +674,7 @@ class SetImage(show.ShowOne): kwargs['data'] != sys.stdin): kwargs['data'].close() - info = {} - info.update(image._info) - info['properties'] = utils.format_dict(info.get('properties', {})) - return zip(*sorted(six.iteritems(info))) + return class ShowImage(show.ShowOne): diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 4c019db6..5552f033 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -22,14 +22,50 @@ import six from cliff import command from cliff import lister from cliff import show - from glanceclient.common import utils as gc_utils + from openstackclient.api import utils as api_utils +from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils from openstackclient.identity import common +DEFAULT_CONTAINER_FORMAT = 'bare' +DEFAULT_DISK_FORMAT = 'raw' + + +def _format_image(image): + """Format an image to make it more consistent with OSC operations. """ + + info = {} + properties = {} + + # the only fields we're not including is "links", "tags" and the properties + fields_to_show = ['status', 'name', 'container_format', 'created_at', + 'size', 'disk_format', 'updated_at', 'visibility', + 'min_disk', 'protected', 'id', 'file', 'checksum', + 'owner', 'virtual_size', 'min_ram', 'schema'] + + # split out the usual key and the properties which are top-level + for key in six.iterkeys(image): + if key in fields_to_show: + info[key] = image.get(key) + elif key == 'tags': + continue # handle this later + else: + properties[key] = image.get(key) + + # format the tags if they are there + info['tags'] = utils.format_list(image.get('tags')) + + # add properties back into the dictionary as a top-level key + if properties: + info['properties'] = utils.format_dict(properties) + + return info + + class AddProjectToImage(show.ShowOne): """Associate project with image""" @@ -69,7 +105,192 @@ class AddProjectToImage(show.ShowOne): project_id, ) - return zip(*sorted(six.iteritems(image_member._info))) + return zip(*sorted(six.iteritems(image_member))) + + +class CreateImage(show.ShowOne): + """Create/upload an image""" + + log = logging.getLogger(__name__ + ".CreateImage") + deadopts = ('size', 'location', 'copy-from', 'checksum', 'store') + + def get_parser(self, prog_name): + parser = super(CreateImage, self).get_parser(prog_name) + # TODO(mordred): add --volume and --force parameters and support + # TODO(bunting): There are additional arguments that v1 supported + # that v2 either doesn't support or supports weirdly. + # --checksum - could be faked clientside perhaps? + # --location - maybe location add? + # --size - passing image size is actually broken in python-glanceclient + # --copy-from - does not exist in v2 + # --store - does not exits in v2 + parser.add_argument( + "name", + metavar="<image-name>", + help="New image name", + ) + parser.add_argument( + "--id", + metavar="<id>", + help="Image ID to reserve", + ) + parser.add_argument( + "--container-format", + default=DEFAULT_CONTAINER_FORMAT, + metavar="<container-format>", + help="Image container format " + "(default: %s)" % DEFAULT_CONTAINER_FORMAT, + ) + parser.add_argument( + "--disk-format", + default=DEFAULT_DISK_FORMAT, + metavar="<disk-format>", + help="Image disk format " + "(default: %s)" % DEFAULT_DISK_FORMAT, + ) + parser.add_argument( + "--owner", + metavar="<owner>", + help="Image owner project name or ID", + ) + parser.add_argument( + "--min-disk", + metavar="<disk-gb>", + type=int, + help="Minimum disk size needed to boot image, in gigabytes", + ) + parser.add_argument( + "--min-ram", + metavar="<ram-mb>", + type=int, + help="Minimum RAM size needed to boot image, in megabytes", + ) + parser.add_argument( + "--file", + metavar="<file>", + help="Upload image from local file", + ) + protected_group = parser.add_mutually_exclusive_group() + protected_group.add_argument( + "--protected", + action="store_true", + help="Prevent image from being deleted", + ) + protected_group.add_argument( + "--unprotected", + action="store_true", + help="Allow image to be deleted (default)", + ) + public_group = parser.add_mutually_exclusive_group() + public_group.add_argument( + "--public", + action="store_true", + help="Image is accessible to the public", + ) + public_group.add_argument( + "--private", + action="store_true", + help="Image is inaccessible to the public (default)", + ) + parser.add_argument( + "--property", + dest="properties", + metavar="<key=value>", + action=parseractions.KeyValueAction, + help="Set a property on this image " + "(repeat option to set multiple properties)", + ) + parser.add_argument( + "--tag", + dest="tags", + metavar="<tag>", + action='append', + help="Set a tag on this image " + "(repeat option to set multiple tags)", + ) + for deadopt in self.deadopts: + parser.add_argument( + "--%s" % deadopt, + metavar="<%s>" % deadopt, + dest=deadopt.replace('-', '_'), + help=argparse.SUPPRESS, + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + image_client = self.app.client_manager.image + + for deadopt in self.deadopts: + if getattr(parsed_args, deadopt.replace('-', '_'), None): + raise exceptions.CommandError( + "ERROR: --%s was given, which is an Image v1 option" + " that is no longer supported in Image v2" % deadopt) + + # Build an attribute dict from the parsed args, only include + # attributes that were actually set on the command line + kwargs = {} + copy_attrs = ('name', 'id', + 'container_format', 'disk_format', + 'min_disk', 'min_ram', + 'tags', 'owner') + for attr in copy_attrs: + if attr in parsed_args: + val = getattr(parsed_args, attr, None) + if val: + # Only include a value in kwargs for attributes that + # are actually present on the command line + kwargs[attr] = val + # properties should get flattened into the general kwargs + if getattr(parsed_args, 'properties', None): + for k, v in six.iteritems(parsed_args.properties): + kwargs[k] = str(v) + # Handle exclusive booleans with care + # Avoid including attributes in kwargs if an option is not + # present on the command line. These exclusive booleans are not + # a single value for the pair of options because the default must be + # to do nothing when no options are present as opposed to always + # setting a default. + if parsed_args.protected: + kwargs['protected'] = True + if parsed_args.unprotected: + kwargs['protected'] = False + if parsed_args.public: + kwargs['visibility'] = 'public' + if parsed_args.private: + kwargs['visibility'] = 'private' + + # open the file first to ensure any failures are handled before the + # image is created + fp = gc_utils.get_data_file(parsed_args) + + if fp is None and parsed_args.file: + self.log.warning("Failed to get an image file.") + return {}, {} + + image = image_client.images.create(**kwargs) + + if fp is not None: + with fp: + try: + image_client.images.upload(image.id, fp) + except Exception as e: + # If the upload fails for some reason attempt to remove the + # dangling queued image made by the create() call above but + # only if the user did not specify an id which indicates + # the Image already exists and should be left alone. + try: + if 'id' not in kwargs: + image_client.images.delete(image.id) + except Exception: + pass # we don't care about this one + raise e # now, throw the upload exception again + + # update the image after the data has been uploaded + image = image_client.images.get(image.id) + + info = _format_image(image) + return zip(*sorted(six.iteritems(info))) class DeleteImage(command.Command): @@ -229,7 +450,7 @@ class ListImage(lister.Lister): s, columns, formatters={ - 'tags': utils.format_dict, + 'tags': utils.format_list, }, ) for s in data) ) @@ -304,41 +525,24 @@ class SaveImage(command.Command): gc_utils.save_image(data, parsed_args.file) -class ShowImage(show.ShowOne): - """Display image details""" - - log = logging.getLogger(__name__ + ".ShowImage") - - def get_parser(self, prog_name): - parser = super(ShowImage, self).get_parser(prog_name) - parser.add_argument( - "image", - metavar="<image>", - help="Image to display (name or ID)", - ) - return parser - - def take_action(self, parsed_args): - self.log.debug("take_action(%s)", parsed_args) - - image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - - info = {} - info.update(image) - return zip(*sorted(six.iteritems(info))) - - -class SetImage(show.ShowOne): +class SetImage(command.Command): """Set image properties""" log = logging.getLogger(__name__ + ".SetImage") + deadopts = ('visibility',) def get_parser(self, prog_name): parser = super(SetImage, self).get_parser(prog_name) + # TODO(bunting): There are additional arguments that v1 supported + # --size - does not exist in v2 + # --store - does not exist in v2 + # --location - maybe location add? + # --copy-from - does not exist in v2 + # --file - should be able to upload file + # --volume - needs adding + # --force - needs adding + # --checksum - maybe could be done client side + # --stdin - could be implemented parser.add_argument( "image", metavar="<image>", @@ -350,100 +554,133 @@ class SetImage(show.ShowOne): help="New image name" ) parser.add_argument( - "--architecture", - metavar="<architecture>", - help="Operating system Architecture" + "--owner", + metavar="<project>", + help="New image owner project (name or ID)", ) parser.add_argument( + "--min-disk", + type=int, + metavar="<disk-gb>", + help="Minimum disk size needed to boot image, in gigabytes" + ) + parser.add_argument( + "--min-ram", + type=int, + metavar="<ram-mb>", + help="Minimum RAM size needed to boot image, in megabytes", + ) + parser.add_argument( + "--container-format", + metavar="<container-format>", + help="Image container format " + "(default: %s)" % DEFAULT_CONTAINER_FORMAT, + ) + parser.add_argument( + "--disk-format", + metavar="<disk-format>", + help="Image disk format " + "(default: %s)" % DEFAULT_DISK_FORMAT, + ) + protected_group = parser.add_mutually_exclusive_group() + protected_group.add_argument( "--protected", - dest="protected", action="store_true", - help="Prevent image from being deleted" + help="Prevent image from being deleted", + ) + protected_group.add_argument( + "--unprotected", + action="store_true", + help="Allow image to be deleted (default)", + ) + public_group = parser.add_mutually_exclusive_group() + public_group.add_argument( + "--public", + action="store_true", + help="Image is accessible to the public", + ) + public_group.add_argument( + "--private", + action="store_true", + help="Image is inaccessible to the public (default)", ) parser.add_argument( - "--instance-uuid", - metavar="<instance_uuid>", - help="ID of instance used to create this image" + "--property", + dest="properties", + metavar="<key=value>", + action=parseractions.KeyValueAction, + help="Set a property on this image " + "(repeat option to set multiple properties)", ) parser.add_argument( - "--min-disk", - type=int, - metavar="<disk-gb>", - help="Minimum disk size needed to boot image, in gigabytes" + "--tag", + dest="tags", + metavar="<tag>", + default=[], + action='append', + help="Set a tag on this image " + "(repeat option to set multiple tags)", ) - visibility_choices = ["public", "private"] parser.add_argument( - "--visibility", - metavar="<visibility>", - choices=visibility_choices, - help="Scope of image accessibility. Valid values: %s" - % visibility_choices + "--architecture", + metavar="<architecture>", + help="Operating system architecture", ) - help_msg = ("ID of image in Glance that should be used as the kernel" - " when booting an AMI-style image") parser.add_argument( - "--kernel-id", - metavar="<kernel-id>", - help=help_msg + "--instance-id", + metavar="<instance-id>", + help="ID of server instance used to create this image", ) parser.add_argument( - "--os-version", - metavar="<os-version>", - help="Operating system version as specified by the distributor" + "--instance-uuid", + metavar="<instance-id>", + dest="instance_id", + help=argparse.SUPPRESS, ) - disk_choices = ["None", "ami", "ari", "aki", "vhd", "vmdk", "raw", - "qcow2", "vdi", "iso"] - help_msg = ("Format of the disk. Valid values: %s" % disk_choices) parser.add_argument( - "--disk-format", - metavar="<disk-format>", - choices=disk_choices, - help=help_msg + "--kernel-id", + metavar="<kernel-id>", + help="ID of kernel image used to boot this disk image", ) parser.add_argument( "--os-distro", metavar="<os-distro>", - help="Common name of operating system distribution" + help="Operating system distribution name", ) parser.add_argument( - "--owner", - metavar="<owner>", - help="New Owner of the image" + "--os-version", + metavar="<os-version>", + help="Operating system distribution version", ) - msg = ("ID of image stored in Glance that should be used as the " - "ramdisk when booting an AMI-style image") parser.add_argument( "--ramdisk-id", metavar="<ramdisk-id>", - help=msg - ) - parser.add_argument( - "--min-ram", - type=int, - metavar="<ram-mb>", - help="Amount of RAM (in MB) required to boot image" - ) - container_choices = ["None", "ami", "ari", "aki", "bare", "ovf", "ova"] - help_msg = ("Format of the container. Valid values: %s" - % container_choices) - parser.add_argument( - "--container-format", - metavar="<container-format>", - choices=container_choices, - help=help_msg - ) + help="ID of ramdisk image used to boot this disk image", + ) + for deadopt in self.deadopts: + parser.add_argument( + "--%s" % deadopt, + metavar="<%s>" % deadopt, + dest=deadopt.replace('-', '_'), + help=argparse.SUPPRESS, + ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image + for deadopt in self.deadopts: + if getattr(parsed_args, deadopt.replace('-', '_'), None): + raise exceptions.CommandError( + "ERROR: --%s was given, which is an Image v1 option" + " that is no longer supported in Image v2" % deadopt) + kwargs = {} copy_attrs = ('architecture', 'container_format', 'disk_format', - 'file', 'kernel_id', 'locations', 'name', + 'file', 'instance_id', 'kernel_id', 'locations', 'min_disk', 'min_ram', 'name', 'os_distro', 'os_version', - 'owner', 'prefix', 'progress', 'ramdisk_id', - 'visibility') + 'owner', 'prefix', 'progress', 'ramdisk_id', 'tags') for attr in copy_attrs: if attr in parsed_args: val = getattr(parsed_args, attr, None) @@ -451,10 +688,26 @@ class SetImage(show.ShowOne): # Only include a value in kwargs for attributes that are # actually present on the command line kwargs[attr] = val + + # Properties should get flattened into the general kwargs + if getattr(parsed_args, 'properties', None): + for k, v in six.iteritems(parsed_args.properties): + kwargs[k] = str(v) + + # Handle exclusive booleans with care + # Avoid including attributes in kwargs if an option is not + # present on the command line. These exclusive booleans are not + # a single value for the pair of options because the default must be + # to do nothing when no options are present as opposed to always + # setting a default. if parsed_args.protected: kwargs['protected'] = True - else: + if parsed_args.unprotected: kwargs['protected'] = False + if parsed_args.public: + kwargs['visibility'] = 'public' + if parsed_args.private: + kwargs['visibility'] = 'private' if not kwargs: self.log.warning("No arguments specified") @@ -463,7 +716,35 @@ class SetImage(show.ShowOne): image = utils.find_resource( image_client.images, parsed_args.image) + if parsed_args.tags: + # Tags should be extended, but duplicates removed + kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) + image = image_client.images.update(image.id, **kwargs) - info = {} - info.update(image) + + +class ShowImage(show.ShowOne): + """Display image details""" + + log = logging.getLogger(__name__ + ".ShowImage") + + def get_parser(self, prog_name): + parser = super(ShowImage, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="<image>", + help="Image to display (name or ID)", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + image_client = self.app.client_manager.image + image = utils.find_resource( + image_client.images, + parsed_args.image, + ) + + info = _format_image(image) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index 5f72782b..69ed11fe 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -18,10 +18,11 @@ from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_API_VERSION = '2' +DEFAULT_API_VERSION = '2.0' API_VERSION_OPTION = 'os_network_api_version' API_NAME = "network" API_VERSIONS = { + "2.0": "neutronclient.v2_0.client.Client", "2": "neutronclient.v2_0.client.Client", } # Translate our API version to auth plugin version prefix @@ -32,6 +33,7 @@ API_VERSION_MAP = { NETWORK_API_TYPE = 'network' NETWORK_API_VERSIONS = { + '2.0': 'openstackclient.api.network_v2.APIv2', '2': 'openstackclient.api.network_v2.APIv2', } diff --git a/openstackclient/object/v1/account.py b/openstackclient/object/v1/account.py new file mode 100644 index 00000000..aa94ff5c --- /dev/null +++ b/openstackclient/object/v1/account.py @@ -0,0 +1,85 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Account v1 action implementations""" + +import logging + +from cliff import command +from cliff import show +import six + +from openstackclient.common import parseractions +from openstackclient.common import utils + + +class SetAccount(command.Command): + """Set account properties""" + + log = logging.getLogger(__name__ + '.SetAccount') + + def get_parser(self, prog_name): + parser = super(SetAccount, self).get_parser(prog_name) + parser.add_argument( + "--property", + metavar="<key=value>", + required=True, + action=parseractions.KeyValueAction, + help="Set a property on this account " + "(repeat option to set multiple properties)" + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.account_set( + properties=parsed_args.property, + ) + + +class ShowAccount(show.ShowOne): + """Display account details""" + + log = logging.getLogger(__name__ + '.ShowAccount') + + @utils.log_method(log) + def take_action(self, parsed_args): + data = self.app.client_manager.object_store.account_show() + if 'properties' in data: + data['properties'] = utils.format_dict(data.pop('properties')) + return zip(*sorted(six.iteritems(data))) + + +class UnsetAccount(command.Command): + """Unset account properties""" + + log = logging.getLogger(__name__ + '.UnsetAccount') + + def get_parser(self, prog_name): + parser = super(UnsetAccount, self).get_parser(prog_name) + parser.add_argument( + '--property', + metavar='<key>', + required=True, + action='append', + default=[], + help='Property to remove from account ' + '(repeat option to remove multiple properties)', + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.account_unset( + properties=parsed_args.property, + ) diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index bc4fdec8..8c8844e2 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -23,6 +23,7 @@ from cliff import command from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -41,8 +42,8 @@ class CreateContainer(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) results = [] for container in parsed_args.containers: @@ -74,8 +75,8 @@ class DeleteContainer(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) for container in parsed_args.containers: self.app.client_manager.object_store.container_delete( @@ -125,8 +126,8 @@ class ListContainer(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('Name', 'Bytes', 'Count') @@ -178,6 +179,36 @@ class SaveContainer(command.Command): ) +class SetContainer(command.Command): + """Set container properties""" + + log = logging.getLogger(__name__ + '.SetContainer') + + def get_parser(self, prog_name): + parser = super(SetContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Container to modify', + ) + parser.add_argument( + "--property", + metavar="<key=value>", + required=True, + action=parseractions.KeyValueAction, + help="Set a property on this container " + "(repeat option to set multiple properties)" + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.container_set( + parsed_args.container, + properties=parsed_args.property, + ) + + class ShowContainer(show.ShowOne): """Display container details""" @@ -192,11 +223,44 @@ class ShowContainer(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) data = self.app.client_manager.object_store.container_show( container=parsed_args.container, ) + if 'properties' in data: + data['properties'] = utils.format_dict(data.pop('properties')) return zip(*sorted(six.iteritems(data))) + + +class UnsetContainer(command.Command): + """Unset container properties""" + + log = logging.getLogger(__name__ + '.UnsetContainer') + + def get_parser(self, prog_name): + parser = super(UnsetContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Container to modify', + ) + parser.add_argument( + '--property', + metavar='<key>', + required=True, + action='append', + default=[], + help='Property to remove from container ' + '(repeat option to remove multiple properties)', + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.container_unset( + parsed_args.container, + properties=parsed_args.property, + ) diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index 752d7842..4bd06124 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -23,6 +23,7 @@ from cliff import command from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -46,8 +47,8 @@ class CreateObject(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) results = [] for obj in parsed_args.objects: @@ -85,8 +86,8 @@ class DeleteObject(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) for obj in parsed_args.objects: self.app.client_manager.object_store.object_delete( @@ -147,8 +148,8 @@ class ListObject(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ( @@ -221,6 +222,42 @@ class SaveObject(command.Command): ) +class SetObject(command.Command): + """Set object properties""" + + log = logging.getLogger(__name__ + '.SetObject') + + def get_parser(self, prog_name): + parser = super(SetObject, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Modify <object> from <container>', + ) + parser.add_argument( + 'object', + metavar='<object>', + help='Object to modify', + ) + parser.add_argument( + "--property", + metavar="<key=value>", + required=True, + action=parseractions.KeyValueAction, + help="Set a property on this object " + "(repeat option to set multiple properties)" + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.object_set( + parsed_args.container, + parsed_args.object, + properties=parsed_args.property, + ) + + class ShowObject(show.ShowOne): """Display object details""" @@ -240,12 +277,51 @@ class ShowObject(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) data = self.app.client_manager.object_store.object_show( container=parsed_args.container, object=parsed_args.object, ) + if 'properties' in data: + data['properties'] = utils.format_dict(data.pop('properties')) return zip(*sorted(six.iteritems(data))) + + +class UnsetObject(command.Command): + """Unset object properties""" + + log = logging.getLogger(__name__ + '.UnsetObject') + + def get_parser(self, prog_name): + parser = super(UnsetObject, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Modify <object> from <container>', + ) + parser.add_argument( + 'object', + metavar='<object>', + help='Object to modify', + ) + parser.add_argument( + '--property', + metavar='<key>', + required=True, + action='append', + default=[], + help='Property to remove from object ' + '(repeat option to remove multiple properties)', + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.object_unset( + parsed_args.container, + parsed_args.object, + properties=parsed_args.property, + ) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index a8b5ac4c..5b4939a2 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -20,17 +20,18 @@ import getpass import logging import sys import traceback -import warnings from cliff import app from cliff import command from cliff import complete from cliff import help +from oslo_utils import strutils import openstackclient from openstackclient.common import clientmanager from openstackclient.common import commandmanager from openstackclient.common import exceptions as exc +from openstackclient.common import logs from openstackclient.common import timing from openstackclient.common import utils @@ -79,6 +80,10 @@ class OpenStackShell(app.App): help.HelpCommand.auth_required = False complete.CompleteCommand.auth_required = False + # Slight change to the meaning of --debug + self.DEFAULT_DEBUG_VALUE = None + self.DEFAULT_DEBUG_HELP = 'Set debug logging and traceback on errors.' + super(OpenStackShell, self).__init__( description=__doc__.strip(), version=openstackclient.__version__, @@ -94,62 +99,19 @@ class OpenStackShell(app.App): self.verify = True self.client_manager = None + self.command_options = None def configure_logging(self): - """Configure logging for the app - - Cliff sets some defaults we don't want so re-work it a bit - """ - - if self.options.debug: - # --debug forces verbose_level 3 - # Set this here so cliff.app.configure_logging() can work - self.options.verbose_level = 3 - - super(OpenStackShell, self).configure_logging() - root_logger = logging.getLogger('') - - # Set logging to the requested level - if self.options.verbose_level == 0: - # --quiet - root_logger.setLevel(logging.ERROR) - warnings.simplefilter("ignore") - elif self.options.verbose_level == 1: - # This is the default case, no --debug, --verbose or --quiet - root_logger.setLevel(logging.WARNING) - warnings.simplefilter("ignore") - elif self.options.verbose_level == 2: - # One --verbose - root_logger.setLevel(logging.INFO) - warnings.simplefilter("once") - elif self.options.verbose_level >= 3: - # Two or more --verbose - root_logger.setLevel(logging.DEBUG) - - # Requests logs some stuff at INFO that we don't want - # unless we have DEBUG - requests_log = logging.getLogger("requests") - - # Other modules we don't want DEBUG output for - cliff_log = logging.getLogger('cliff') - stevedore_log = logging.getLogger('stevedore') - iso8601_log = logging.getLogger("iso8601") - - if self.options.debug: - # --debug forces traceback - self.dump_stack_trace = True - requests_log.setLevel(logging.DEBUG) - else: - self.dump_stack_trace = False - requests_log.setLevel(logging.ERROR) - - cliff_log.setLevel(logging.ERROR) - stevedore_log.setLevel(logging.ERROR) - iso8601_log.setLevel(logging.ERROR) + """Configure logging for the app.""" + self.log_configurator = logs.LogConfigurator(self.options) + self.dump_stack_trace = self.log_configurator.dump_trace def run(self, argv): + ret_val = 1 + self.command_options = argv try: - return super(OpenStackShell, self).run(argv) + ret_val = super(OpenStackShell, self).run(argv) + return ret_val except Exception as e: if not logging.getLogger('').handlers: logging.basicConfig() @@ -157,7 +119,11 @@ class OpenStackShell(app.App): self.log.error(traceback.format_exc(e)) else: self.log.error('Exception raised: ' + str(e)) - return 1 + + return ret_val + + finally: + self.log.info("END return value: %s", ret_val) def build_option_parser(self, description, version): parser = super(OpenStackShell, self).build_option_parser( @@ -236,14 +202,17 @@ class OpenStackShell(app.App): # Parent __init__ parses argv into self.options super(OpenStackShell, self).initialize_app(argv) + self.log.info("START with options: %s", + strutils.mask_password(self.command_options)) + self.log.debug("options: %s", + strutils.mask_password(self.options)) # Set the default plugin to token_endpoint if url and token are given if (self.options.url and self.options.token): # Use service token authentication auth_type = 'token_endpoint' else: - auth_type = 'osc_password' - self.log.debug("options: %s", self.options) + auth_type = 'password' project_id = getattr(self.options, 'project_id', None) project_name = getattr(self.options, 'project_name', None) @@ -266,15 +235,26 @@ class OpenStackShell(app.App): # Ignore the default value of interface. Only if it is set later # will it be used. cc = cloud_config.OpenStackConfig( - override_defaults={'interface': None, - 'auth_type': auth_type, }) - self.log.debug("defaults: %s", cc.defaults) + override_defaults={ + 'interface': None, + 'auth_type': auth_type, + }, + ) + # TODO(thowe): Change cliff so the default value for debug + # can be set to None. + if not self.options.debug: + self.options.debug = None self.cloud = cc.get_one_cloud( cloud=self.options.cloud, argparse=self.options, ) - self.log.debug("cloud cfg: %s", self.cloud.config) + + self.log_configurator.configure(self.cloud) + self.dump_stack_trace = self.log_configurator.dump_trace + self.log.debug("defaults: %s", cc.defaults) + self.log.debug("cloud cfg: %s", + strutils.mask_password(self.cloud.config)) # Set up client TLS # NOTE(dtroyer): --insecure is the non-default condition that @@ -300,15 +280,27 @@ class OpenStackShell(app.App): for mod in clientmanager.PLUGIN_MODULES: default_version = getattr(mod, 'DEFAULT_API_VERSION', None) option = mod.API_VERSION_OPTION.replace('os_', '') - version_opt = self.cloud.config.get(option, default_version) + version_opt = str(self.cloud.config.get(option, default_version)) if version_opt: api = mod.API_NAME self.api_version[api] = version_opt - if version_opt not in mod.API_VERSIONS: - self.log.warning( - "The %s version <%s> is not in supported versions <%s>" - % (api, version_opt, - ', '.join(mod.API_VERSIONS.keys()))) + + # Add a plugin interface to let the module validate the version + # requested by the user + skip_old_check = False + mod_check_api_version = getattr(mod, 'check_api_version', None) + if mod_check_api_version: + # this throws an exception if invalid + skip_old_check = mod_check_api_version(version_opt) + + mod_versions = getattr(mod, 'API_VERSIONS', None) + if not skip_old_check and mod_versions: + if version_opt not in mod_versions: + self.log.warning( + "%s version %s is not in supported versions %s" + % (api, version_opt, + ', '.join(mod.API_VERSIONS.keys()))) + # Command groups deal only with major versions version = '.v' + version_opt.replace('.', '_').split('_')[0] cmd_group = 'openstack.' + api.replace('-', '_') + version diff --git a/openstackclient/tests/api/fakes.py b/openstackclient/tests/api/fakes.py index 85617ab7..e285a61c 100644 --- a/openstackclient/tests/api/fakes.py +++ b/openstackclient/tests/api/fakes.py @@ -15,7 +15,7 @@ from requests_mock.contrib import fixture -from keystoneclient import session +from keystoneauth1 import session from openstackclient.tests import utils diff --git a/openstackclient/tests/api/test_image_v1.py b/openstackclient/tests/api/test_image_v1.py index 34fcfca4..f3479756 100644 --- a/openstackclient/tests/api/test_image_v1.py +++ b/openstackclient/tests/api/test_image_v1.py @@ -15,7 +15,7 @@ from requests_mock.contrib import fixture -from keystoneclient import session +from keystoneauth1 import session from openstackclient.api import image_v1 from openstackclient.tests import utils diff --git a/openstackclient/tests/api/test_image_v2.py b/openstackclient/tests/api/test_image_v2.py index ddb160ee..77063997 100644 --- a/openstackclient/tests/api/test_image_v2.py +++ b/openstackclient/tests/api/test_image_v2.py @@ -15,7 +15,7 @@ from requests_mock.contrib import fixture -from keystoneclient import session +from keystoneauth1 import session from openstackclient.api import image_v2 from openstackclient.tests import utils diff --git a/openstackclient/tests/api/test_network_v2.py b/openstackclient/tests/api/test_network_v2.py index 13c5d6ea..80f1d9de 100644 --- a/openstackclient/tests/api/test_network_v2.py +++ b/openstackclient/tests/api/test_network_v2.py @@ -15,7 +15,7 @@ from requests_mock.contrib import fixture -from keystoneclient import session +from keystoneauth1 import session from openstackclient.api import network_v2 as network from openstackclient.tests import utils diff --git a/openstackclient/tests/api/test_object_store_v1.py b/openstackclient/tests/api/test_object_store_v1.py index b18a003d..992bf261 100644 --- a/openstackclient/tests/api/test_object_store_v1.py +++ b/openstackclient/tests/api/test_object_store_v1.py @@ -17,7 +17,7 @@ import mock from requests_mock.contrib import fixture -from keystoneclient import session +from keystoneauth1 import session from openstackclient.api import object_store_v1 as object_store from openstackclient.tests import utils @@ -157,10 +157,6 @@ class TestContainer(TestObjectAPIv1): 'container': 'qaz', 'object_count': '1', 'bytes_used': '577', - 'read_acl': None, - 'write_acl': None, - 'sync_to': None, - 'sync_key': None, } self.requests_mock.register_uri( 'HEAD', @@ -323,8 +319,7 @@ class TestObject(TestObjectAPIv1): 'content-length': '577', 'last-modified': '20130101', 'etag': 'qaz', - 'wife': 'Wilma', - 'x-tra-header': 'yabba-dabba-do', + 'properties': {'wife': 'Wilma'}, } self.requests_mock.register_uri( 'HEAD', diff --git a/openstackclient/tests/common/test_logs.py b/openstackclient/tests/common/test_logs.py new file mode 100644 index 00000000..fe054a3b --- /dev/null +++ b/openstackclient/tests/common/test_logs.py @@ -0,0 +1,202 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import logging +import mock + +from openstackclient.common import logs +from openstackclient.tests import utils + + +class TestContext(utils.TestCase): + + def test_log_level_from_options(self): + opts = mock.Mock() + opts.verbose_level = 0 + self.assertEqual(logging.ERROR, logs.log_level_from_options(opts)) + opts.verbose_level = 1 + self.assertEqual(logging.WARNING, logs.log_level_from_options(opts)) + opts.verbose_level = 2 + self.assertEqual(logging.INFO, logs.log_level_from_options(opts)) + opts.verbose_level = 3 + self.assertEqual(logging.DEBUG, logs.log_level_from_options(opts)) + + def test_log_level_from_config(self): + cfg = {'verbose_level': 0} + self.assertEqual(logging.ERROR, logs.log_level_from_config(cfg)) + cfg = {'verbose_level': 1} + self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) + cfg = {'verbose_level': 2} + self.assertEqual(logging.INFO, logs.log_level_from_config(cfg)) + cfg = {'verbose_level': 3} + self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) + cfg = {'verbose_level': 1, 'log_level': 'critical'} + self.assertEqual(logging.CRITICAL, logs.log_level_from_config(cfg)) + cfg = {'verbose_level': 1, 'log_level': 'error'} + self.assertEqual(logging.ERROR, logs.log_level_from_config(cfg)) + cfg = {'verbose_level': 1, 'log_level': 'warning'} + self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) + cfg = {'verbose_level': 1, 'log_level': 'info'} + self.assertEqual(logging.INFO, logs.log_level_from_config(cfg)) + cfg = {'verbose_level': 1, 'log_level': 'debug'} + self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) + cfg = {'verbose_level': 1, 'log_level': 'bogus'} + self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) + cfg = {'verbose_level': 1, 'log_level': 'info', 'debug': True} + self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) + + @mock.patch('warnings.simplefilter') + def test_set_warning_filter(self, simplefilter): + logs.set_warning_filter(logging.ERROR) + simplefilter.assert_called_with("ignore") + logs.set_warning_filter(logging.WARNING) + simplefilter.assert_called_with("ignore") + logs.set_warning_filter(logging.INFO) + simplefilter.assert_called_with("once") + + +class TestFileFormatter(utils.TestCase): + def test_nothing(self): + formatter = logs._FileFormatter() + self.assertEqual(('%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' + '%(name)s %(message)s'), formatter.fmt) + + def test_options(self): + class Opts(object): + cloud = 'cloudy' + os_project_name = 'projecty' + username = 'usernamey' + options = Opts() + formatter = logs._FileFormatter(options=options) + self.assertEqual(('%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' + '%(name)s [cloudy usernamey projecty] %(message)s'), + formatter.fmt) + + def test_config(self): + config = mock.Mock() + config.config = {'cloud': 'cloudy'} + config.auth = {'project_name': 'projecty', 'username': 'usernamey'} + formatter = logs._FileFormatter(config=config) + self.assertEqual(('%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' + '%(name)s [cloudy usernamey projecty] %(message)s'), + formatter.fmt) + + +class TestLogConfigurator(utils.TestCase): + def setUp(self): + super(TestLogConfigurator, self).setUp() + self.options = mock.Mock() + self.options.verbose_level = 1 + self.options.log_file = None + self.options.debug = False + self.root_logger = mock.Mock() + self.root_logger.setLevel = mock.Mock() + self.root_logger.addHandler = mock.Mock() + self.requests_log = mock.Mock() + self.requests_log.setLevel = mock.Mock() + self.cliff_log = mock.Mock() + self.cliff_log.setLevel = mock.Mock() + self.stevedore_log = mock.Mock() + self.stevedore_log.setLevel = mock.Mock() + self.iso8601_log = mock.Mock() + self.iso8601_log.setLevel = mock.Mock() + self.loggers = [ + self.root_logger, + self.requests_log, + self.cliff_log, + self.stevedore_log, + self.iso8601_log] + + @mock.patch('logging.StreamHandler') + @mock.patch('logging.getLogger') + @mock.patch('openstackclient.common.logs.set_warning_filter') + def test_init(self, warning_filter, getLogger, handle): + getLogger.side_effect = self.loggers + console_logger = mock.Mock() + console_logger.setFormatter = mock.Mock() + console_logger.setLevel = mock.Mock() + handle.return_value = console_logger + + configurator = logs.LogConfigurator(self.options) + + getLogger.assert_called_with('iso8601') # last call + warning_filter.assert_called_with(logging.WARNING) + self.root_logger.setLevel.assert_called_with(logging.DEBUG) + self.root_logger.addHandler.assert_called_with(console_logger) + self.requests_log.setLevel.assert_called_with(logging.ERROR) + self.cliff_log.setLevel.assert_called_with(logging.ERROR) + self.stevedore_log.setLevel.assert_called_with(logging.ERROR) + self.iso8601_log.setLevel.assert_called_with(logging.ERROR) + self.assertEqual(False, configurator.dump_trace) + + @mock.patch('logging.getLogger') + @mock.patch('openstackclient.common.logs.set_warning_filter') + def test_init_no_debug(self, warning_filter, getLogger): + getLogger.side_effect = self.loggers + self.options.debug = True + + configurator = logs.LogConfigurator(self.options) + + warning_filter.assert_called_with(logging.DEBUG) + self.requests_log.setLevel.assert_called_with(logging.DEBUG) + self.assertEqual(True, configurator.dump_trace) + + @mock.patch('logging.FileHandler') + @mock.patch('logging.getLogger') + @mock.patch('openstackclient.common.logs.set_warning_filter') + @mock.patch('openstackclient.common.logs._FileFormatter') + def test_init_log_file(self, formatter, warning_filter, getLogger, handle): + getLogger.side_effect = self.loggers + self.options.log_file = '/tmp/log_file' + file_logger = mock.Mock() + file_logger.setFormatter = mock.Mock() + file_logger.setLevel = mock.Mock() + handle.return_value = file_logger + mock_formatter = mock.Mock() + formatter.return_value = mock_formatter + + logs.LogConfigurator(self.options) + + handle.assert_called_with(filename=self.options.log_file) + self.root_logger.addHandler.assert_called_with(file_logger) + file_logger.setFormatter.assert_called_with(mock_formatter) + file_logger.setLevel.assert_called_with(logging.WARNING) + + @mock.patch('logging.FileHandler') + @mock.patch('logging.getLogger') + @mock.patch('openstackclient.common.logs.set_warning_filter') + @mock.patch('openstackclient.common.logs._FileFormatter') + def test_configure(self, formatter, warning_filter, getLogger, handle): + getLogger.side_effect = self.loggers + configurator = logs.LogConfigurator(self.options) + cloud_config = mock.Mock() + config_log = '/tmp/config_log' + cloud_config.config = { + 'log_file': config_log, + 'verbose_level': 1, + 'log_level': 'info'} + file_logger = mock.Mock() + file_logger.setFormatter = mock.Mock() + file_logger.setLevel = mock.Mock() + handle.return_value = file_logger + mock_formatter = mock.Mock() + formatter.return_value = mock_formatter + + configurator.configure(cloud_config) + + warning_filter.assert_called_with(logging.INFO) + handle.assert_called_with(filename=config_log) + self.root_logger.addHandler.assert_called_with(file_logger) + file_logger.setFormatter.assert_called_with(mock_formatter) + file_logger.setLevel.assert_called_with(logging.INFO) + self.assertEqual(False, configurator.dump_trace) diff --git a/openstackclient/tests/common/test_parseractions.py b/openstackclient/tests/common/test_parseractions.py index 8afcb632..b75c4814 100644 --- a/openstackclient/tests/common/test_parseractions.py +++ b/openstackclient/tests/common/test_parseractions.py @@ -102,3 +102,58 @@ class TestKeyValueAction(utils.TestCase): expect = {'green': '100%'} self.assertDictEqual(expect, actual) self.assertEqual(None, failhere) + + +class TestNonNegativeAction(utils.TestCase): + def test_negative_values(self): + parser = argparse.ArgumentParser() + + # Set up our typical usage + parser.add_argument( + '--foo', + metavar='<foo>', + type=int, + action=parseractions.NonNegativeAction, + ) + + self.assertRaises( + argparse.ArgumentTypeError, + parser.parse_args, + "--foo -1".split() + ) + + def test_zero_values(self): + parser = argparse.ArgumentParser() + + # Set up our typical usage + parser.add_argument( + '--foo', + metavar='<foo>', + type=int, + action=parseractions.NonNegativeAction, + ) + + results = parser.parse_args( + '--foo 0'.split() + ) + + actual = getattr(results, 'foo', None) + self.assertEqual(actual, 0) + + def test_positive_values(self): + parser = argparse.ArgumentParser() + + # Set up our typical usage + parser.add_argument( + '--foo', + metavar='<foo>', + type=int, + action=parseractions.NonNegativeAction, + ) + + results = parser.parse_args( + '--foo 1'.split() + ) + + actual = getattr(results, 'foo', None) + self.assertEqual(actual, 1) diff --git a/openstackclient/tests/common/test_quota.py b/openstackclient/tests/common/test_quota.py index f0013e48..047ef343 100644 --- a/openstackclient/tests/common/test_quota.py +++ b/openstackclient/tests/common/test_quota.py @@ -12,9 +12,12 @@ import copy +import mock + from openstackclient.common import quota from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes class FakeQuotaResource(fakes.FakeResource): @@ -38,6 +41,13 @@ class TestQuota(compute_fakes.TestComputev2): super(TestQuota, self).setUp() self.quotas_mock = self.app.client_manager.compute.quotas self.quotas_mock.reset_mock() + volume_mock = mock.Mock() + volume_mock.quotas = mock.Mock() + self.app.client_manager.volume = volume_mock + self.volume_quotas_mock = volume_mock.quotas + self.volume_quotas_mock.reset_mock() + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() class TestQuotaSet(TestQuota): @@ -57,6 +67,24 @@ class TestQuotaSet(TestQuota): loaded=True, ) + self.volume_quotas_mock.find.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.volume_quotas_mock.update.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.cmd = quota.SetQuota(self.app, None) def test_quota_set(self): @@ -65,14 +93,14 @@ class TestQuotaSet(TestQuota): '--fixed-ips', str(compute_fakes.fix_ip_num), '--injected-files', str(compute_fakes.injected_file_num), '--key-pairs', str(compute_fakes.key_pair_num), - compute_fakes.project_name, + identity_fakes.project_name, ] verifylist = [ ('floating_ips', compute_fakes.floating_ip_num), ('fixed_ips', compute_fakes.fix_ip_num), ('injected_files', compute_fakes.injected_file_num), ('key_pairs', compute_fakes.key_pair_num), - ('project', compute_fakes.project_name), + ('project', identity_fakes.project_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -86,4 +114,35 @@ class TestQuotaSet(TestQuota): 'key_pairs': compute_fakes.key_pair_num, } - self.quotas_mock.update.assert_called_with('project_test', **kwargs) + self.quotas_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) + + def test_quota_set_volume(self): + arglist = [ + '--gigabytes', str(compute_fakes.floating_ip_num), + '--snapshots', str(compute_fakes.fix_ip_num), + '--volumes', str(compute_fakes.injected_file_num), + identity_fakes.project_name, + ] + verifylist = [ + ('gigabytes', compute_fakes.floating_ip_num), + ('snapshots', compute_fakes.fix_ip_num), + ('volumes', compute_fakes.injected_file_num), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + kwargs = { + 'gigabytes': compute_fakes.floating_ip_num, + 'snapshots': compute_fakes.fix_ip_num, + 'volumes': compute_fakes.injected_file_num, + } + + self.volume_quotas_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py index a25a5ba5..b564ffab 100644 --- a/openstackclient/tests/common/test_utils.py +++ b/openstackclient/tests/common/test_utils.py @@ -20,6 +20,7 @@ import mock from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.tests import fakes from openstackclient.tests import utils as test_utils PASSWORD = "Pa$$w0rd" @@ -27,6 +28,18 @@ WASSPORD = "Wa$$p0rd" DROWSSAP = "dr0w$$aP" +class FakeOddballResource(fakes.FakeResource): + + def get(self, attr): + """get() is needed for utils.find_resource()""" + if attr == 'id': + return self.id + elif attr == 'name': + return self.name + else: + return None + + class TestUtils(test_utils.TestCase): def test_get_password_good(self): @@ -124,6 +137,46 @@ class TestUtils(test_utils.TestCase): items, sort_str) @mock.patch.object(time, 'sleep') + def test_wait_for_status_ok(self, mock_sleep): + # Tests the normal flow that the resource is status=active + resource = mock.MagicMock(status='ACTIVE') + status_f = mock.Mock(return_value=resource) + res_id = str(uuid.uuid4()) + self.assertTrue(utils.wait_for_status(status_f, res_id,)) + self.assertFalse(mock_sleep.called) + + @mock.patch.object(time, 'sleep') + def test_wait_for_status_ok__with_overrides(self, mock_sleep): + # Tests the normal flow that the resource is status=complete + resource = mock.MagicMock(my_status='COMPLETE') + status_f = mock.Mock(return_value=resource) + res_id = str(uuid.uuid4()) + self.assertTrue(utils.wait_for_status(status_f, res_id, + status_field='my_status', + success_status=['complete'])) + self.assertFalse(mock_sleep.called) + + @mock.patch.object(time, 'sleep') + def test_wait_for_status_error(self, mock_sleep): + # Tests that we fail if the resource is status=error + resource = mock.MagicMock(status='ERROR') + status_f = mock.Mock(return_value=resource) + res_id = str(uuid.uuid4()) + self.assertFalse(utils.wait_for_status(status_f, res_id)) + self.assertFalse(mock_sleep.called) + + @mock.patch.object(time, 'sleep') + def test_wait_for_status_error_with_overrides(self, mock_sleep): + # Tests that we fail if the resource is my_status=failed + resource = mock.MagicMock(my_status='FAILED') + status_f = mock.Mock(return_value=resource) + res_id = str(uuid.uuid4()) + self.assertFalse(utils.wait_for_status(status_f, res_id, + status_field='my_status', + error_status=['failed'])) + self.assertFalse(mock_sleep.called) + + @mock.patch.object(time, 'sleep') def test_wait_for_delete_ok(self, mock_sleep): # Tests the normal flow that the resource is deleted with a 404 coming # back on the 2nd iteration of the wait loop. @@ -242,6 +295,47 @@ class TestFindResource(test_utils.TestCase): self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) + def test_find_resource_silly_resource(self): + # We need a resource with no resource_class for this test, start fresh + self.manager = mock.Mock() + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock( + side_effect=AttributeError( + "'Controller' object has no attribute 'find'", + ) + ) + silly_resource = FakeOddballResource( + None, + {'id': '12345', 'name': self.name}, + loaded=True, + ) + self.manager.list = mock.Mock( + return_value=[silly_resource, ], + ) + result = utils.find_resource(self.manager, self.name) + self.assertEqual(silly_resource, result) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(name=self.name) + + def test_find_resource_silly_resource_not_found(self): + # We need a resource with no resource_class for this test, start fresh + self.manager = mock.Mock() + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock( + side_effect=AttributeError( + "'Controller' object has no attribute 'find'", + ) + ) + self.manager.list = mock.Mock(return_value=[]) + result = self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + self.name) + self.assertEqual("Could not find resource legos", + str(result)) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(name=self.name) + def test_format_dict(self): expected = "a='b', c='d', e='f'" self.assertEqual(expected, diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index e798bd40..13db0c01 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -20,11 +20,14 @@ from openstackclient.tests.identity.v2_0 import fakes as identity_fakes from openstackclient.tests.image.v2 import fakes as image_fakes from openstackclient.tests.network.v2 import fakes as network_fakes from openstackclient.tests import utils +from openstackclient.tests.volume.v2 import fakes as volume_fakes server_id = 'serv1' server_name = 'waiter' +service_id = '1' + SERVER = { 'id': server_id, 'name': server_name, @@ -78,6 +81,8 @@ QUOTA = { QUOTA_columns = tuple(sorted(QUOTA)) QUOTA_data = tuple(QUOTA[x] for x in sorted(QUOTA)) +block_device_mapping = 'vda=' + volume_fakes.volume_name + ':::0' + class FakeComputev2Client(object): def __init__(self, **kwargs): @@ -85,12 +90,16 @@ class FakeComputev2Client(object): self.images.resource_class = fakes.FakeResource(None, {}) self.servers = mock.Mock() self.servers.resource_class = fakes.FakeResource(None, {}) + self.services = mock.Mock() + self.services.resource_class = fakes.FakeResource(None, {}) self.extensions = mock.Mock() self.extensions.resource_class = fakes.FakeResource(None, {}) self.flavors = mock.Mock() self.flavors.resource_class = fakes.FakeResource(None, {}) self.quotas = mock.Mock() self.quotas.resource_class = fakes.FakeResource(None, {}) + self.volumes = mock.Mock() + self.volumes.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -118,3 +127,8 @@ class TestComputev2(utils.TestCommand): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) + + self.app.client_manager.volume = volume_fakes.FakeVolumeClient( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/compute/v2/test_flavor.py b/openstackclient/tests/compute/v2/test_flavor.py index 19be8124..523104f0 100644 --- a/openstackclient/tests/compute/v2/test_flavor.py +++ b/openstackclient/tests/compute/v2/test_flavor.py @@ -76,7 +76,9 @@ class TestFlavorList(TestFlavor): # Set expected values kwargs = { - 'is_public': True + 'is_public': True, + 'limit': None, + 'marker': None } self.flavors_mock.list.assert_called_with( @@ -119,7 +121,9 @@ class TestFlavorList(TestFlavor): # Set expected values kwargs = { - 'is_public': None + 'is_public': None, + 'limit': None, + 'marker': None } self.flavors_mock.list.assert_called_with( @@ -162,7 +166,9 @@ class TestFlavorList(TestFlavor): # Set expected values kwargs = { - 'is_public': False + 'is_public': False, + 'limit': None, + 'marker': None } self.flavors_mock.list.assert_called_with( @@ -205,7 +211,9 @@ class TestFlavorList(TestFlavor): # Set expected values kwargs = { - 'is_public': True + 'is_public': True, + 'limit': None, + 'marker': None } self.flavors_mock.list.assert_called_with( @@ -248,7 +256,9 @@ class TestFlavorList(TestFlavor): # Set expected values kwargs = { - 'is_public': True + 'is_public': True, + 'limit': None, + 'marker': None } self.flavors_mock.list.assert_called_with( diff --git a/openstackclient/tests/compute/v2/test_security_group_rule.py b/openstackclient/tests/compute/v2/test_security_group_rule.py new file mode 100644 index 00000000..9516f8dd --- /dev/null +++ b/openstackclient/tests/compute/v2/test_security_group_rule.py @@ -0,0 +1,338 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy +import mock + +from openstackclient.compute.v2 import security_group +from openstackclient.tests.compute.v2 import fakes as compute_fakes +from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes + + +security_group_id = '11' +security_group_name = 'wide-open' +security_group_description = 'nothing but net' + +security_group_rule_id = '1' + +SECURITY_GROUP = { + 'id': security_group_id, + 'name': security_group_name, + 'description': security_group_description, + 'tenant_id': identity_fakes.project_id, +} + +SECURITY_GROUP_RULE = { + 'id': security_group_rule_id, + 'group': {}, + 'ip_protocol': 'tcp', + 'ip_range': '0.0.0.0/0', + 'parent_group_id': security_group_id, + 'from_port': 0, + 'to_port': 0, +} + +SECURITY_GROUP_RULE_ICMP = { + 'id': security_group_rule_id, + 'group': {}, + 'ip_protocol': 'icmp', + 'ip_range': '0.0.0.0/0', + 'parent_group_id': security_group_id, + 'from_port': -1, + 'to_port': -1, +} + + +class FakeSecurityGroupRuleResource(fakes.FakeResource): + + def get_keys(self): + return {'property': 'value'} + + +class TestSecurityGroupRule(compute_fakes.TestComputev2): + + def setUp(self): + super(TestSecurityGroupRule, self).setUp() + + self.secgroups_mock = mock.Mock() + self.secgroups_mock.resource_class = fakes.FakeResource(None, {}) + self.app.client_manager.compute.security_groups = self.secgroups_mock + self.secgroups_mock.reset_mock() + + self.sg_rules_mock = mock.Mock() + self.sg_rules_mock.resource_class = fakes.FakeResource(None, {}) + self.app.client_manager.compute.security_group_rules = \ + self.sg_rules_mock + self.sg_rules_mock.reset_mock() + + +class TestSecurityGroupRuleCreate(TestSecurityGroupRule): + + def setUp(self): + super(TestSecurityGroupRuleCreate, self).setUp() + + self.secgroups_mock.get.return_value = FakeSecurityGroupRuleResource( + None, + copy.deepcopy(SECURITY_GROUP), + loaded=True, + ) + + # Get the command object to test + self.cmd = security_group.CreateSecurityGroupRule(self.app, None) + + def test_security_group_rule_create_no_options(self): + self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( + None, + copy.deepcopy(SECURITY_GROUP_RULE), + loaded=True, + ) + + arglist = [ + security_group_name, + ] + verifylist = [ + ('group', security_group_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # SecurityGroupManager.create(name, description) + self.sg_rules_mock.create.assert_called_with( + security_group_id, + 'tcp', + 0, + 0, + '0.0.0.0/0', + ) + + collist = ( + 'group', + 'id', + 'ip_protocol', + 'ip_range', + 'parent_group_id', + 'port_range', + ) + self.assertEqual(collist, columns) + datalist = ( + {}, + security_group_rule_id, + 'tcp', + '', + security_group_id, + '0:0', + ) + self.assertEqual(datalist, data) + + def test_security_group_rule_create_ftp(self): + sg_rule = copy.deepcopy(SECURITY_GROUP_RULE) + sg_rule['from_port'] = 20 + sg_rule['to_port'] = 21 + self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( + None, + sg_rule, + loaded=True, + ) + + arglist = [ + security_group_name, + '--dst-port', '20:21', + ] + verifylist = [ + ('group', security_group_name), + ('dst_port', (20, 21)), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # SecurityGroupManager.create(name, description) + self.sg_rules_mock.create.assert_called_with( + security_group_id, + 'tcp', + 20, + 21, + '0.0.0.0/0', + ) + + collist = ( + 'group', + 'id', + 'ip_protocol', + 'ip_range', + 'parent_group_id', + 'port_range', + ) + self.assertEqual(collist, columns) + datalist = ( + {}, + security_group_rule_id, + 'tcp', + '', + security_group_id, + '20:21', + ) + self.assertEqual(datalist, data) + + def test_security_group_rule_create_ssh(self): + sg_rule = copy.deepcopy(SECURITY_GROUP_RULE) + sg_rule['from_port'] = 22 + sg_rule['to_port'] = 22 + self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( + None, + sg_rule, + loaded=True, + ) + + arglist = [ + security_group_name, + '--dst-port', '22', + ] + verifylist = [ + ('group', security_group_name), + ('dst_port', (22, 22)), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # SecurityGroupManager.create(name, description) + self.sg_rules_mock.create.assert_called_with( + security_group_id, + 'tcp', + 22, + 22, + '0.0.0.0/0', + ) + + collist = ( + 'group', + 'id', + 'ip_protocol', + 'ip_range', + 'parent_group_id', + 'port_range', + ) + self.assertEqual(collist, columns) + datalist = ( + {}, + security_group_rule_id, + 'tcp', + '', + security_group_id, + '22:22', + ) + self.assertEqual(datalist, data) + + def test_security_group_rule_create_udp(self): + sg_rule = copy.deepcopy(SECURITY_GROUP_RULE) + sg_rule['ip_protocol'] = 'udp' + self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( + None, + sg_rule, + loaded=True, + ) + + arglist = [ + security_group_name, + '--proto', 'udp', + ] + verifylist = [ + ('group', security_group_name), + ('proto', 'udp'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # SecurityGroupManager.create(name, description) + self.sg_rules_mock.create.assert_called_with( + security_group_id, + 'udp', + 0, + 0, + '0.0.0.0/0', + ) + + collist = ( + 'group', + 'id', + 'ip_protocol', + 'ip_range', + 'parent_group_id', + 'port_range', + ) + self.assertEqual(collist, columns) + datalist = ( + {}, + security_group_rule_id, + 'udp', + '', + security_group_id, + '0:0', + ) + self.assertEqual(datalist, data) + + def test_security_group_rule_create_icmp(self): + self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( + None, + copy.deepcopy(SECURITY_GROUP_RULE_ICMP), + loaded=True, + ) + + arglist = [ + security_group_name, + '--proto', 'ICMP', + ] + verifylist = [ + ('group', security_group_name), + ('proto', 'ICMP'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # SecurityGroupManager.create(name, description) + self.sg_rules_mock.create.assert_called_with( + security_group_id, + 'ICMP', + -1, + -1, + '0.0.0.0/0', + ) + + collist = ( + 'group', + 'id', + 'ip_protocol', + 'ip_range', + 'parent_group_id', + 'port_range', + ) + self.assertEqual(collist, columns) + datalist = ( + {}, + security_group_rule_id, + 'icmp', + '', + security_group_id, + '', + ) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index 4df18f05..791a90ab 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -24,6 +24,7 @@ from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests import fakes from openstackclient.tests.image.v2 import fakes as image_fakes from openstackclient.tests import utils +from openstackclient.tests.volume.v2 import fakes as volume_fakes class TestServer(compute_fakes.TestComputev2): @@ -47,6 +48,10 @@ class TestServer(compute_fakes.TestComputev2): self.images_mock = self.app.client_manager.image.images self.images_mock.reset_mock() + # Get a shortcut to the VolumeManager Mock + self.volumes_mock = self.app.client_manager.volume.volumes + self.volumes_mock.reset_mock() + class TestServerCreate(TestServer): @@ -80,15 +85,22 @@ class TestServerCreate(TestServer): ) self.flavors_mock.get.return_value = self.flavor + self.volume = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True, + ) + self.volumes_mock.get.return_value = self.volume + # Get the command object to test self.cmd = server.CreateServer(self.app, None) def test_server_create_no_options(self): arglist = [ - compute_fakes.server_id, + compute_fakes.server_name, ] verifylist = [ - ('server_name', compute_fakes.server_id), + ('server_name', compute_fakes.server_name), ] try: # Missing required args should bail here @@ -100,13 +112,13 @@ class TestServerCreate(TestServer): arglist = [ '--image', 'image1', '--flavor', 'flavor1', - compute_fakes.server_id, + compute_fakes.server_name, ] verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), ('config_drive', False), - ('server_name', compute_fakes.server_id), + ('server_name', compute_fakes.server_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -131,7 +143,7 @@ class TestServerCreate(TestServer): ) # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( - compute_fakes.server_id, + compute_fakes.server_name, self.image, self.flavor, **kwargs @@ -154,14 +166,14 @@ class TestServerCreate(TestServer): '--flavor', 'flavor1', '--nic', 'net-id=net1', '--nic', 'port-id=port1', - compute_fakes.server_id, + compute_fakes.server_name, ] verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), ('nic', ['net-id=net1', 'port-id=port1']), ('config_drive', False), - ('server_name', compute_fakes.server_id), + ('server_name', compute_fakes.server_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -207,7 +219,7 @@ class TestServerCreate(TestServer): ) # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( - compute_fakes.server_id, + compute_fakes.server_name, self.image, self.flavor, **kwargs @@ -234,14 +246,14 @@ class TestServerCreate(TestServer): '--image', 'image1', '--flavor', 'flavor1', '--user-data', 'userdata.sh', - compute_fakes.server_id, + compute_fakes.server_name, ] verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), ('user_data', 'userdata.sh'), ('config_drive', False), - ('server_name', compute_fakes.server_id), + ('server_name', compute_fakes.server_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -272,7 +284,68 @@ class TestServerCreate(TestServer): ) # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( + compute_fakes.server_name, + self.image, + self.flavor, + **kwargs + ) + + collist = ('addresses', 'flavor', 'id', 'name', 'properties') + self.assertEqual(collist, columns) + datalist = ( + '', + 'Large ()', compute_fakes.server_id, + compute_fakes.server_name, + '', + ) + self.assertEqual(datalist, data) + + def test_server_create_with_block_device_mapping(self): + arglist = [ + '--image', 'image1', + '--flavor', compute_fakes.flavor_id, + '--block-device-mapping', compute_fakes.block_device_mapping, + compute_fakes.server_name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', compute_fakes.flavor_id), + ('block_device_mapping', [compute_fakes.block_device_mapping]), + ('config_drive', False), + ('server_name', compute_fakes.server_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # CreateServer.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + real_volume_mapping = ( + (compute_fakes.block_device_mapping.split('=', 1)[1]).replace( + volume_fakes.volume_name, + volume_fakes.volume_id)) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping={ + 'vda': real_volume_mapping + }, + nics=[], + scheduler_hints={}, + config_drive=None, + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + compute_fakes.server_name, self.image, self.flavor, **kwargs @@ -311,7 +384,7 @@ class TestServerDelete(TestServer): compute_fakes.server_id, ] verifylist = [ - ('servers', [compute_fakes.server_id]), + ('server', [compute_fakes.server_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -328,7 +401,7 @@ class TestServerDelete(TestServer): compute_fakes.server_id, '--wait' ] verifylist = [ - ('servers', [compute_fakes.server_id]), + ('server', [compute_fakes.server_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -351,7 +424,7 @@ class TestServerDelete(TestServer): compute_fakes.server_id, '--wait' ] verifylist = [ - ('servers', [compute_fakes.server_id]), + ('server', [compute_fakes.server_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -410,13 +483,14 @@ class TestServerImageCreate(TestServer): compute_fakes.server_name, ) - collist = ('id', 'name', 'owner', 'protected', 'visibility') + collist = ('id', 'name', 'owner', 'protected', 'tags', 'visibility') self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, image_fakes.image_name, image_fakes.image_owner, image_fakes.image_protected, + image_fakes.image_tags, image_fakes.image_visibility, ) self.assertEqual(datalist, data) @@ -441,18 +515,76 @@ class TestServerImageCreate(TestServer): 'img-nam', ) - collist = ('id', 'name', 'owner', 'protected', 'visibility') + collist = ('id', 'name', 'owner', 'protected', 'tags', 'visibility') self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, image_fakes.image_name, image_fakes.image_owner, image_fakes.image_protected, + image_fakes.image_tags, image_fakes.image_visibility, ) self.assertEqual(datalist, data) +class TestServerPause(TestServer): + + def setUp(self): + super(TestServerPause, self).setUp() + + # Get the command object to test + self.cmd = server.PauseServer(self.app, None) + + # Set methods to be tested. + self.methods = { + 'pause': None, + } + + def setup_servers_mock(self, count=1): + servers = fakes.FakeServer.create_servers(methods=self.methods, + count=count) + + # This is the return value for utils.find_resource() + self.servers_mock.get = fakes.FakeServer.get_servers(servers, 1) + + return servers + + def test_server_pause_one_server(self): + servers = self.setup_servers_mock(1) + + arglist = [ + servers[0].id, + ] + verifylist = [ + ('server', [servers[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + servers[0].pause.assert_called_with() + + def test_server_pause_multi_servers(self): + servers = self.setup_servers_mock(3) + arglist = [] + verifylist = [] + + for i in range(0, len(servers)): + arglist.append(servers[i].id) + verifylist = [ + ('server', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + for i in range(0, len(servers)): + servers[i].pause.assert_called_with() + + class TestServerResize(TestServer): def setUp(self): diff --git a/openstackclient/tests/compute/v2/test_service.py b/openstackclient/tests/compute/v2/test_service.py new file mode 100644 index 00000000..c6db30ac --- /dev/null +++ b/openstackclient/tests/compute/v2/test_service.py @@ -0,0 +1,54 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from openstackclient.compute.v2 import service +from openstackclient.tests.compute.v2 import fakes as compute_fakes + + +class TestService(compute_fakes.TestComputev2): + + def setUp(self): + super(TestService, self).setUp() + + # Get a shortcut to the ServiceManager Mock + self.service_mock = self.app.client_manager.compute.services + self.service_mock.reset_mock() + + +class TestServiceDelete(TestService): + + def setUp(self): + super(TestServiceDelete, self).setUp() + + self.service_mock.delete.return_value = None + + # Get the command object to test + self.cmd = service.DeleteService(self.app, None) + + def test_service_delete_no_options(self): + arglist = [ + compute_fakes.service_id, + ] + verifylist = [ + ('service', compute_fakes.service_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.service_mock.delete.assert_called_with( + compute_fakes.service_id, + ) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 979f9481..85e65fb1 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -13,11 +13,14 @@ # under the License. # +import copy import json +import mock import six import sys +import uuid -from keystoneclient import fixture +from keystoneauth1 import fixture import requests @@ -122,17 +125,41 @@ class FakeModule(object): class FakeResource(object): - def __init__(self, manager, info, loaded=False): + def __init__(self, manager=None, info={}, loaded=False, methods={}): + """Set attributes and methods for a resource. + + :param manager: + The resource manager + :param Dictionary info: + A dictionary with all attributes + :param bool loaded: + True if the resource is loaded in memory + :param Dictionary methods: + A dictionary with all methods + """ self.__name__ = type(self).__name__ self.manager = manager self._info = info self._add_details(info) + self._add_methods(methods) self._loaded = loaded def _add_details(self, info): for (k, v) in six.iteritems(info): setattr(self, k, v) + def _add_methods(self, methods): + """Fake methods with MagicMock objects. + + For each <@key, @value> pairs in methods, add an callable MagicMock + object named @key as an attribute, and set the mock's return_value to + @value. When users access the attribute with (), @value will be + returned, which looks like a function call. + """ + for (name, ret) in six.iteritems(methods): + method = mock.MagicMock(return_value=ret) + setattr(self, name, method) + def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') @@ -150,3 +177,79 @@ class FakeResponse(requests.Response): self._content = json.dumps(data) if not isinstance(self._content, six.binary_type): self._content = self._content.encode() + + +class FakeModel(dict): + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(key) + + +class FakeServer(object): + """Fake one or more compute servers.""" + + @staticmethod + def create_one_server(attrs={}, methods={}): + """Create a fake server. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :return: + A FakeResource object, with id, name, metadata + """ + # Set default attributes. + server_info = { + 'id': 'server-id-' + uuid.uuid4().hex, + 'name': 'server-name-' + uuid.uuid4().hex, + 'metadata': {}, + } + + # Overwrite default attributes. + server_info.update(attrs) + + server = FakeResource(info=copy.deepcopy(server_info), + methods=methods, + loaded=True) + return server + + @staticmethod + def create_servers(attrs={}, methods={}, count=2): + """Create multiple fake servers. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :param int count: + The number of servers to fake + :return: + A list of FakeResource objects faking the servers + """ + servers = [] + for i in range(0, count): + servers.append(FakeServer.create_one_server(attrs, methods)) + + return servers + + @staticmethod + def get_servers(servers=None, count=2): + """Get an iterable MagicMock object with a list of faked servers. + + If servers list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List servers: + A list of FakeResource objects faking servers + :param int count: + The number of servers to fake + :return: + An iterable Mock object with side_effect set to a list of faked + servers + """ + if servers is None: + servers = FakeServer.create_servers(count) + return mock.MagicMock(side_effect=servers) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 9c4de9cc..9fe341ed 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -123,13 +123,11 @@ PROJECT_2 = { } region_id = 'region_one' -region_url = 'http://localhost:1111' region_parent_region_id = 'region_two' region_description = 'region one' REGION = { 'id': region_id, - 'url': region_url, 'description': region_description, 'parent_region_id': region_parent_region_id, 'links': base_url + 'regions/' + region_id, @@ -195,6 +193,8 @@ SERVICE_WITHOUT_NAME = { 'links': base_url + 'services/' + service_id, } +credential_id = 'c-123' + endpoint_id = 'e-123' endpoint_url = 'http://127.0.0.1:35357' endpoint_region = 'RegionOne' @@ -261,6 +261,7 @@ TOKEN_WITH_DOMAIN_ID = { idp_id = 'test_idp' idp_description = 'super exciting IdP description' idp_remote_ids = ['entity1', 'entity2'] +formatted_idp_remote_ids = 'entity1, entity2' IDENTITY_PROVIDER = { 'id': idp_id, @@ -400,6 +401,8 @@ class FakeIdentityv3Client(object): def __init__(self, **kwargs): self.domains = mock.Mock() self.domains.resource_class = fakes.FakeResource(None, {}) + self.credentials = mock.Mock() + self.credentials.resource_class = fakes.FakeResource(None, {}) self.endpoints = mock.Mock() self.endpoints.resource_class = fakes.FakeResource(None, {}) self.groups = mock.Mock() diff --git a/openstackclient/tests/identity/v3/test_credential.py b/openstackclient/tests/identity/v3/test_credential.py new file mode 100644 index 00000000..e2e690c3 --- /dev/null +++ b/openstackclient/tests/identity/v3/test_credential.py @@ -0,0 +1,112 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from openstackclient.identity.v3 import credential +from openstackclient.tests.identity.v3 import fakes as identity_fakes +from openstackclient.tests import utils + + +class TestCredential(identity_fakes.TestIdentityv3): + data = { + "access": "abc123", + "secret": "hidden-message", + "trust_id": None + } + + def __init__(self, *args): + super(TestCredential, self).__init__(*args) + + self.json_data = json.dumps(self.data) + + def setUp(self): + super(TestCredential, self).setUp() + + # Get a shortcut to the CredentialManager Mock + self.credentials_mock = self.app.client_manager.identity.credentials + self.credentials_mock.reset_mock() + + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + +class TestCredentialSet(TestCredential): + def setUp(self): + super(TestCredentialSet, self).setUp() + self.cmd = credential.SetCredential(self.app, None) + + def test_credential_set_no_options(self): + arglist = [ + identity_fakes.credential_id, + ] + + self.assertRaises(utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_credential_set_missing_user(self): + arglist = [ + '--type', 'ec2', + '--data', self.json_data, + identity_fakes.credential_id, + ] + + self.assertRaises(utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_credential_set_missing_type(self): + arglist = [ + '--user', identity_fakes.user_name, + '--data', self.json_data, + identity_fakes.credential_id, + ] + + self.assertRaises(utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_credential_set_missing_data(self): + arglist = [ + '--user', identity_fakes.user_name, + '--type', 'ec2', + identity_fakes.credential_id, + ] + + self.assertRaises(utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_credential_set_valid(self): + arglist = [ + '--user', identity_fakes.user_name, + '--type', 'ec2', + '--data', self.json_data, + identity_fakes.credential_id, + ] + + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + + def test_credential_set_valid_with_project(self): + arglist = [ + '--user', identity_fakes.user_name, + '--type', 'ec2', + '--data', self.json_data, + '--project', identity_fakes.project_name, + identity_fakes.credential_id, + ] + + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) diff --git a/openstackclient/tests/identity/v3/test_identity_provider.py b/openstackclient/tests/identity/v3/test_identity_provider.py index cd328c1d..db097d5d 100644 --- a/openstackclient/tests/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/identity/v3/test_identity_provider.py @@ -69,7 +69,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -103,7 +103,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -137,7 +137,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -172,7 +172,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -211,7 +211,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -253,7 +253,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): None, False, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -363,7 +363,7 @@ class TestIdentityProviderShow(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -374,6 +374,50 @@ class TestIdentityProviderSet(TestIdentityProvider): super(TestIdentityProviderSet, self).setUp() self.cmd = identity_provider.SetIdentityProvider(self.app, None) + def test_identity_provider_set_description(self): + """Set Identity Provider's description. """ + def prepare(self): + """Prepare fake return objects before the test is executed""" + updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) + updated_idp['enabled'] = False + resources = fakes.FakeResource( + None, + updated_idp, + loaded=True + ) + self.identity_providers_mock.update.return_value = resources + + prepare(self) + new_description = 'new desc' + arglist = [ + '--description', new_description, + identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id), + ('description', new_description), + ('enable', False), + ('disable', False), + ('remote_id', None) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.identity_providers_mock.update.assert_called_with( + identity_fakes.idp_id, + description=new_description + ) + + collist = ('description', 'enabled', 'id', 'remote_ids') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.idp_description, + False, + identity_fakes.idp_id, + identity_fakes.idp_remote_ids + ) + self.assertEqual(datalist, data) + def test_identity_provider_disable(self): """Disable Identity Provider @@ -398,6 +442,7 @@ class TestIdentityProviderSet(TestIdentityProvider): ] verifylist = [ ('identity_provider', identity_fakes.idp_id), + ('description', None), ('enable', False), ('disable', True), ('remote_id', identity_fakes.idp_remote_ids) @@ -443,6 +488,7 @@ class TestIdentityProviderSet(TestIdentityProvider): ] verifylist = [ ('identity_provider', identity_fakes.idp_id), + ('description', None), ('enable', True), ('disable', False), ('remote_id', identity_fakes.idp_remote_ids) @@ -488,6 +534,7 @@ class TestIdentityProviderSet(TestIdentityProvider): ] verifylist = [ ('identity_provider', identity_fakes.idp_id), + ('description', None), ('enable', True), ('disable', False), ('remote_id', [self.new_remote_id]) @@ -533,6 +580,7 @@ class TestIdentityProviderSet(TestIdentityProvider): ] verifylist = [ ('identity_provider', identity_fakes.idp_id), + ('description', None), ('enable', True), ('disable', False), ('remote_id_file', self.new_remote_id), diff --git a/openstackclient/tests/identity/v3/test_region.py b/openstackclient/tests/identity/v3/test_region.py index 7f6ced9f..0ebbbecf 100644 --- a/openstackclient/tests/identity/v3/test_region.py +++ b/openstackclient/tests/identity/v3/test_region.py @@ -61,19 +61,17 @@ class TestRegionCreate(TestRegion): 'description': identity_fakes.region_description, 'id': identity_fakes.region_id, 'parent_region': None, - 'url': None, } self.regions_mock.create.assert_called_with( **kwargs ) - collist = ('description', 'parent_region', 'region', 'url') + collist = ('description', 'parent_region', 'region') self.assertEqual(collist, columns) datalist = ( identity_fakes.region_description, identity_fakes.region_parent_region_id, identity_fakes.region_id, - identity_fakes.region_url, ) self.assertEqual(datalist, data) @@ -94,19 +92,17 @@ class TestRegionCreate(TestRegion): 'description': None, 'id': identity_fakes.region_id, 'parent_region': None, - 'url': None, } self.regions_mock.create.assert_called_with( **kwargs ) - collist = ('description', 'parent_region', 'region', 'url') + collist = ('description', 'parent_region', 'region') self.assertEqual(collist, columns) datalist = ( identity_fakes.region_description, identity_fakes.region_parent_region_id, identity_fakes.region_id, - identity_fakes.region_url, ) self.assertEqual(datalist, data) @@ -129,54 +125,17 @@ class TestRegionCreate(TestRegion): 'description': None, 'id': identity_fakes.region_id, 'parent_region': identity_fakes.region_parent_region_id, - 'url': None, } self.regions_mock.create.assert_called_with( **kwargs ) - collist = ('description', 'parent_region', 'region', 'url') + collist = ('description', 'parent_region', 'region') self.assertEqual(collist, columns) datalist = ( identity_fakes.region_description, identity_fakes.region_parent_region_id, identity_fakes.region_id, - identity_fakes.region_url, - ) - self.assertEqual(datalist, data) - - def test_region_create_url(self): - arglist = [ - identity_fakes.region_id, - '--url', identity_fakes.region_url, - ] - verifylist = [ - ('region', identity_fakes.region_id), - ('url', identity_fakes.region_url), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - kwargs = { - 'description': None, - 'id': identity_fakes.region_id, - 'parent_region': None, - 'url': identity_fakes.region_url, - } - self.regions_mock.create.assert_called_with( - **kwargs - ) - - collist = ('description', 'parent_region', 'region', 'url') - self.assertEqual(collist, columns) - datalist = ( - identity_fakes.region_description, - identity_fakes.region_parent_region_id, - identity_fakes.region_id, - identity_fakes.region_url, ) self.assertEqual(datalist, data) @@ -233,13 +192,12 @@ class TestRegionList(TestRegion): columns, data = self.cmd.take_action(parsed_args) self.regions_mock.list.assert_called_with() - collist = ('Region', 'Parent Region', 'Description', 'URL') + collist = ('Region', 'Parent Region', 'Description') self.assertEqual(collist, columns) datalist = (( identity_fakes.region_id, identity_fakes.region_parent_region_id, identity_fakes.region_description, - identity_fakes.region_url, ), ) self.assertEqual(datalist, tuple(data)) @@ -257,13 +215,12 @@ class TestRegionList(TestRegion): self.regions_mock.list.assert_called_with( parent_region_id=identity_fakes.region_parent_region_id) - collist = ('Region', 'Parent Region', 'Description', 'URL') + collist = ('Region', 'Parent Region', 'Description') self.assertEqual(collist, columns) datalist = (( identity_fakes.region_id, identity_fakes.region_parent_region_id, identity_fakes.region_description, - identity_fakes.region_url, ), ) self.assertEqual(datalist, tuple(data)) @@ -319,29 +276,6 @@ class TestRegionSet(TestRegion): **kwargs ) - def test_region_set_url(self): - arglist = [ - '--url', 'new url', - identity_fakes.region_id, - ] - verifylist = [ - ('url', 'new url'), - ('region', identity_fakes.region_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) - - # Set expected values - kwargs = { - 'url': 'new url', - } - self.regions_mock.update.assert_called_with( - identity_fakes.region_id, - **kwargs - ) - def test_region_set_parent_region_id(self): arglist = [ '--parent-region', 'new_parent', @@ -395,12 +329,11 @@ class TestRegionShow(TestRegion): identity_fakes.region_id, ) - collist = ('description', 'parent_region', 'region', 'url') + collist = ('description', 'parent_region', 'region') self.assertEqual(collist, columns) datalist = ( identity_fakes.region_description, identity_fakes.region_parent_region_id, identity_fakes.region_id, - identity_fakes.region_url, ) self.assertEqual(datalist, data) diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py index 4a0ba066..8ad4b099 100644 --- a/openstackclient/tests/identity/v3/test_role.py +++ b/openstackclient/tests/identity/v3/test_role.py @@ -367,6 +367,39 @@ class TestRoleList(TestRole): ), ) self.assertEqual(datalist, tuple(data)) + def test_user_list_inherited(self): + arglist = [ + '--user', identity_fakes.user_id, + '--inherited', + ] + verifylist = [ + ('user', identity_fakes.user_id), + ('inherited', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': 'default', + 'user': self.users_mock.get(), + 'os_inherit_extension_inherited': True, + } + # RoleManager.list(user=, group=, domain=, project=, **kwargs) + self.roles_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + ), ) + self.assertEqual(datalist, tuple(data)) + def test_user_list_user(self): arglist = [ '--user', identity_fakes.user_id, @@ -383,6 +416,7 @@ class TestRoleList(TestRole): kwargs = { 'domain': 'default', 'user': self.users_mock.get(), + 'os_inherit_extension_inherited': False } # RoleManager.list(user=, group=, domain=, project=, **kwargs) self.roles_mock.list.assert_called_with( @@ -415,6 +449,7 @@ class TestRoleList(TestRole): kwargs = { 'domain': self.domains_mock.get(), 'user': self.users_mock.get(), + 'os_inherit_extension_inherited': False } # RoleManager.list(user=, group=, domain=, project=, **kwargs) self.roles_mock.list.assert_called_with( @@ -449,6 +484,7 @@ class TestRoleList(TestRole): kwargs = { 'domain': self.domains_mock.get(), 'group': self.groups_mock.get(), + 'os_inherit_extension_inherited': False } # RoleManager.list(user=, group=, domain=, project=, **kwargs) self.roles_mock.list.assert_called_with( @@ -483,6 +519,7 @@ class TestRoleList(TestRole): kwargs = { 'project': self.projects_mock.get(), 'user': self.users_mock.get(), + 'os_inherit_extension_inherited': False } # RoleManager.list(user=, group=, domain=, project=, **kwargs) self.roles_mock.list.assert_called_with( @@ -517,6 +554,7 @@ class TestRoleList(TestRole): kwargs = { 'project': self.projects_mock.get(), 'group': self.groups_mock.get(), + 'os_inherit_extension_inherited': False } # RoleManager.list(user=, group=, domain=, project=, **kwargs) self.roles_mock.list.assert_called_with( diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py index a79df8b4..d10d3b15 100644 --- a/openstackclient/tests/image/v1/test_image.py +++ b/openstackclient/tests/image/v1/test_image.py @@ -499,8 +499,7 @@ class TestImageSet(TestImage): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) kwargs = { 'name': 'new-name', @@ -517,9 +516,6 @@ class TestImageSet(TestImage): **kwargs ) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) - def test_image_set_bools1(self): arglist = [ '--protected', @@ -644,8 +640,7 @@ class TestImageSet(TestImage): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) # VolumeManager.upload_to_image(volume, force, image_name, # container_format, disk_format) @@ -664,9 +659,6 @@ class TestImageSet(TestImage): volume='volly', ) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) - class TestImageShow(TestImage): diff --git a/openstackclient/tests/image/v2/fakes.py b/openstackclient/tests/image/v2/fakes.py index 1a9e301a..11ad455d 100644 --- a/openstackclient/tests/image/v2/fakes.py +++ b/openstackclient/tests/image/v2/fakes.py @@ -13,6 +13,7 @@ # under the License. # +import copy import mock from openstackclient.tests import fakes @@ -25,6 +26,7 @@ image_name = 'graven' image_owner = 'baal' image_protected = False image_visibility = 'public' +image_tags = [] IMAGE = { 'id': image_id, @@ -32,11 +34,16 @@ IMAGE = { 'owner': image_owner, 'protected': image_protected, 'visibility': image_visibility, + 'tags': image_tags } IMAGE_columns = tuple(sorted(IMAGE)) IMAGE_data = tuple((IMAGE[x] for x in sorted(IMAGE))) +IMAGE_SHOW = copy.copy(IMAGE) +IMAGE_SHOW['tags'] = '' +IMAGE_SHOW_data = tuple((IMAGE_SHOW[x] for x in sorted(IMAGE_SHOW))) + member_status = 'pending' MEMBER = { 'member_id': identity_fakes.project_id, @@ -117,6 +124,14 @@ IMAGE_schema = { "type": "string", "description": "Status of the image (READ-ONLY)" }, + "tags": { + "items": { + "type": "string", + "maxLength": 255 + }, + "type": "array", + "description": "List of strings related to the image" + }, "visibility": { "enum": [ "public", diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index bfb94765..f71407a6 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -19,6 +19,7 @@ import mock import warlock from glanceclient.v2 import schemas +from openstackclient.common import exceptions from openstackclient.image.v2 import image from openstackclient.tests import fakes from openstackclient.tests.identity.v3 import fakes as identity_fakes @@ -41,6 +42,193 @@ class TestImage(image_fakes.TestImagev2): self.domain_mock.reset_mock() +class TestImageCreate(TestImage): + + def setUp(self): + super(TestImageCreate, self).setUp() + + self.images_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + # This is the return value for utils.find_resource() + self.images_mock.get.return_value = copy.deepcopy(image_fakes.IMAGE) + self.images_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + + # Get the command object to test + self.cmd = image.CreateImage(self.app, None) + + def test_image_reserve_no_options(self): + mock_exception = { + 'find.side_effect': exceptions.CommandError('x'), + } + self.images_mock.configure_mock(**mock_exception) + arglist = [ + image_fakes.image_name, + ] + verifylist = [ + ('container_format', image.DEFAULT_CONTAINER_FORMAT), + ('disk_format', image.DEFAULT_DISK_FORMAT), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ImageManager.create(name=, **) + self.images_mock.create.assert_called_with( + name=image_fakes.image_name, + container_format=image.DEFAULT_CONTAINER_FORMAT, + disk_format=image.DEFAULT_DISK_FORMAT, + ) + + # Verify update() was not called, if it was show the args + self.assertEqual(self.images_mock.update.call_args_list, []) + + self.images_mock.upload.assert_called_with( + mock.ANY, mock.ANY, + ) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) + + @mock.patch('glanceclient.common.utils.get_data_file', name='Open') + def test_image_reserve_options(self, mock_open): + mock_file = mock.MagicMock(name='File') + mock_open.return_value = mock_file + mock_open.read.return_value = None + mock_exception = { + 'find.side_effect': exceptions.CommandError('x'), + } + self.images_mock.configure_mock(**mock_exception) + arglist = [ + '--container-format', 'ovf', + '--disk-format', 'fs', + '--min-disk', '10', + '--min-ram', '4', + '--owner', '123456', + '--protected', + '--private', + image_fakes.image_name, + ] + verifylist = [ + ('container_format', 'ovf'), + ('disk_format', 'fs'), + ('min_disk', 10), + ('min_ram', 4), + ('owner', '123456'), + ('protected', True), + ('unprotected', False), + ('public', False), + ('private', True), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ImageManager.create(name=, **) + self.images_mock.create.assert_called_with( + name=image_fakes.image_name, + container_format='ovf', + disk_format='fs', + min_disk=10, + min_ram=4, + owner='123456', + protected=True, + visibility='private', + ) + + # Verify update() was not called, if it was show the args + self.assertEqual(self.images_mock.update.call_args_list, []) + + self.images_mock.upload.assert_called_with( + mock.ANY, mock.ANY, + ) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) + + @mock.patch('glanceclient.common.utils.get_data_file', name='Open') + def test_image_create_file(self, mock_open): + mock_file = mock.MagicMock(name='File') + mock_open.return_value = mock_file + mock_open.read.return_value = image_fakes.IMAGE_data + mock_exception = { + 'find.side_effect': exceptions.CommandError('x'), + } + self.images_mock.configure_mock(**mock_exception) + + arglist = [ + '--file', 'filer', + '--unprotected', + '--public', + '--property', 'Alpha=1', + '--property', 'Beta=2', + '--tag', 'awesome', + '--tag', 'better', + image_fakes.image_name, + ] + verifylist = [ + ('file', 'filer'), + ('protected', False), + ('unprotected', True), + ('public', True), + ('private', False), + ('properties', {'Alpha': '1', 'Beta': '2'}), + ('tags', ['awesome', 'better']), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ImageManager.create(name=, **) + self.images_mock.create.assert_called_with( + name=image_fakes.image_name, + container_format=image.DEFAULT_CONTAINER_FORMAT, + disk_format=image.DEFAULT_DISK_FORMAT, + protected=False, + visibility='public', + Alpha='1', + Beta='2', + tags=['awesome', 'better'], + ) + + # Verify update() was not called, if it was show the args + self.assertEqual(self.images_mock.update.call_args_list, []) + + self.images_mock.upload.assert_called_with( + mock.ANY, mock.ANY, + ) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) + + def test_image_create_dead_options(self): + + arglist = [ + '--store', 'somewhere', + image_fakes.image_name, + ] + verifylist = [ + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + + class TestAddProjectToImage(TestImage): def setUp(self): @@ -52,10 +240,8 @@ class TestAddProjectToImage(TestImage): copy.deepcopy(image_fakes.IMAGE), loaded=True, ) - self.image_members_mock.create.return_value = fakes.FakeResource( - None, + self.image_members_mock.create.return_value = fakes.FakeModel( copy.deepcopy(image_fakes.MEMBER), - loaded=True, ) self.project_mock.get.return_value = fakes.FakeResource( None, @@ -453,11 +639,10 @@ class TestRemoveProjectImage(TestImage): ) -class TestImageShow(TestImage): +class TestImageSet(TestImage): def setUp(self): - super(TestImageShow, self).setUp() - + super(TestImageSet, self).setUp() # Set up the schema self.model = warlock.model_factory( image_fakes.IMAGE_schema, @@ -465,33 +650,262 @@ class TestImageShow(TestImage): ) self.images_mock.get.return_value = self.model(**image_fakes.IMAGE) - + self.images_mock.update.return_value = self.model(**image_fakes.IMAGE) # Get the command object to test - self.cmd = image.ShowImage(self.app, None) + self.cmd = image.SetImage(self.app, None) - def test_image_show(self): + def test_image_set_options(self): arglist = [ + '--name', 'new-name', + '--owner', 'new-owner', + '--min-disk', '2', + '--min-ram', '4', + '--container-format', 'ovf', + '--disk-format', 'vmdk', image_fakes.image_id, ] verifylist = [ + ('name', 'new-name'), + ('owner', 'new-owner'), + ('min_disk', 2), + ('min_ram', 4), + ('container_format', 'ovf'), + ('disk_format', 'vmdk'), ('image', image_fakes.image_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - self.images_mock.get.assert_called_with( + self.cmd.take_action(parsed_args) + + kwargs = { + 'name': 'new-name', + 'owner': 'new-owner', + 'min_disk': 2, + 'min_ram': 4, + 'container_format': 'ovf', + 'disk_format': 'vmdk', + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, **kwargs) + + def test_image_set_bools1(self): + arglist = [ + '--protected', + '--private', + image_fakes.image_name, + ] + verifylist = [ + ('protected', True), + ('unprotected', False), + ('public', False), + ('private', True), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'protected': True, + 'visibility': 'private', + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( image_fakes.image_id, + **kwargs ) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + def test_image_set_bools2(self): + arglist = [ + '--unprotected', + '--public', + image_fakes.image_name, + ] + verifylist = [ + ('protected', False), + ('unprotected', True), + ('public', True), + ('private', False), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) -class TestImageSet(TestImage): + kwargs = { + 'protected': False, + 'visibility': 'public', + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_properties(self): + arglist = [ + '--property', 'Alpha=1', + '--property', 'Beta=2', + image_fakes.image_name, + ] + verifylist = [ + ('properties', {'Alpha': '1', 'Beta': '2'}), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'Alpha': '1', + 'Beta': '2', + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_fake_properties(self): + arglist = [ + '--architecture', 'z80', + '--instance-id', '12345', + '--kernel-id', '67890', + '--os-distro', 'cpm', + '--os-version', '2.2H', + '--ramdisk-id', 'xyzpdq', + image_fakes.image_name, + ] + verifylist = [ + ('architecture', 'z80'), + ('instance_id', '12345'), + ('kernel_id', '67890'), + ('os_distro', 'cpm'), + ('os_version', '2.2H'), + ('ramdisk_id', 'xyzpdq'), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'architecture': 'z80', + 'instance_id': '12345', + 'kernel_id': '67890', + 'os_distro': 'cpm', + 'os_version': '2.2H', + 'ramdisk_id': 'xyzpdq', + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_tag(self): + arglist = [ + '--tag', 'test-tag', + image_fakes.image_name, + ] + verifylist = [ + ('tags', ['test-tag']), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'tags': ['test-tag'], + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_tag_merge(self): + old_image = copy.copy(image_fakes.IMAGE) + old_image['tags'] = ['old1', 'new2'] + self.images_mock.get.return_value = self.model(**old_image) + arglist = [ + '--tag', 'test-tag', + image_fakes.image_name, + ] + verifylist = [ + ('tags', ['test-tag']), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'tags': ['old1', 'new2', 'test-tag'], + } + # ImageManager.update(image, **kwargs) + a, k = self.images_mock.update.call_args + self.assertEqual(image_fakes.image_id, a[0]) + self.assertTrue('tags' in k) + self.assertEqual(set(kwargs['tags']), set(k['tags'])) + + def test_image_set_tag_merge_dupe(self): + old_image = copy.copy(image_fakes.IMAGE) + old_image['tags'] = ['old1', 'new2'] + self.images_mock.get.return_value = self.model(**old_image) + arglist = [ + '--tag', 'old1', + image_fakes.image_name, + ] + verifylist = [ + ('tags', ['old1']), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'tags': ['new2', 'old1'], + } + # ImageManager.update(image, **kwargs) + a, k = self.images_mock.update.call_args + self.assertEqual(image_fakes.image_id, a[0]) + self.assertTrue('tags' in k) + self.assertEqual(set(kwargs['tags']), set(k['tags'])) + + def test_image_set_dead_options(self): + + arglist = [ + '--visibility', '1-mile', + image_fakes.image_name, + ] + verifylist = [ + ('visibility', '1-mile'), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + + +class TestImageShow(TestImage): def setUp(self): - super(TestImageSet, self).setUp() + super(TestImageShow, self).setUp() + # Set up the schema self.model = warlock.model_factory( image_fakes.IMAGE_schema, @@ -499,40 +913,24 @@ class TestImageSet(TestImage): ) self.images_mock.get.return_value = self.model(**image_fakes.IMAGE) - self.images_mock.update.return_value = self.model(**image_fakes.IMAGE) + # Get the command object to test - self.cmd = image.SetImage(self.app, None) + self.cmd = image.ShowImage(self.app, None) - def test_image_set_options(self): + def test_image_show(self): arglist = [ - '--name', 'new-name', - '--owner', 'new-owner', - '--min-disk', '2', - '--min-ram', '4', image_fakes.image_id, ] verifylist = [ - ('name', 'new-name'), - ('owner', 'new-owner'), - ('min_disk', 2), - ('min_ram', 4), ('image', image_fakes.image_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - - kwargs = { - 'name': 'new-name', - 'owner': 'new-owner', - 'min_disk': 2, - 'min_ram': 4, - 'protected': False - } - # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, **kwargs) + self.images_mock.get.assert_called_with( + image_fakes.image_id, + ) self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) diff --git a/openstackclient/tests/object/v1/fakes.py b/openstackclient/tests/object/v1/fakes.py index 6aef05b1..986ab2f3 100644 --- a/openstackclient/tests/object/v1/fakes.py +++ b/openstackclient/tests/object/v1/fakes.py @@ -13,7 +13,7 @@ # under the License. # -from keystoneclient import session +from keystoneauth1 import session from openstackclient.api import object_store_v1 as object_store from openstackclient.tests import utils diff --git a/openstackclient/tests/object/v1/test_container_all.py b/openstackclient/tests/object/v1/test_container_all.py index 8b200e09..69fc0f39 100644 --- a/openstackclient/tests/object/v1/test_container_all.py +++ b/openstackclient/tests/object/v1/test_container_all.py @@ -286,7 +286,6 @@ class TestContainerShow(TestContainerAll): def test_object_show_container(self): headers = { - 'x-container-meta-owner': object_fakes.ACCOUNT_ID, 'x-container-object-count': '42', 'x-container-bytes-used': '123', 'x-container-read': 'qaz', diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index 0e0cc50b..c548d890 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -77,6 +77,8 @@ CLOUD_2 = { 'username': 'zaphod', }, 'region_name': 'occ-cloud', + 'log_file': '/tmp/test_log_file', + 'log_level': 'debug', } } } diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py index f73260e9..70ff50de 100644 --- a/openstackclient/tests/volume/v1/test_volume.py +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -527,6 +527,188 @@ class TestVolumeCreate(TestVolume): self.assertEqual(datalist, data) +class TestVolumeList(TestVolume): + + def setUp(self): + super(TestVolumeList, self).setUp() + + self.volumes_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = volume.ListVolume(self.app, None) + + def test_volume_list_no_options(self): + arglist = [] + verifylist = [ + ('long', False), + ('all_projects', False), + ('name', None), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, columns) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + '', + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_name(self): + arglist = [ + '--name', volume_fakes.volume_name, + ] + verifylist = [ + ('long', False), + ('all_projects', False), + ('name', volume_fakes.volume_name), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + '', + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_status(self): + arglist = [ + '--status', volume_fakes.volume_status, + ] + verifylist = [ + ('long', False), + ('all_projects', False), + ('name', None), + ('status', volume_fakes.volume_status), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + '', + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_all_projects(self): + arglist = [ + '--all-projects', + ] + verifylist = [ + ('long', False), + ('all_projects', True), + ('name', None), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, columns) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + '', + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ('all_projects', False), + ('name', None), + ('status', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Type', + 'Bootable', + 'Attached to', + 'Properties', + ) + self.assertEqual(collist, columns) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + volume_fakes.volume_type, + '', + '', + "Alpha='a', Beta='b', Gamma='g'", + ), ) + self.assertEqual(datalist, tuple(data)) + + class TestVolumeSet(TestVolume): def setUp(self): diff --git a/openstackclient/tests/volume/v2/fakes.py b/openstackclient/tests/volume/v2/fakes.py index 0d8c2024..b9b2ae87 100644 --- a/openstackclient/tests/volume/v2/fakes.py +++ b/openstackclient/tests/volume/v2/fakes.py @@ -15,14 +15,15 @@ import copy import mock -from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests import fakes -from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes from openstackclient.tests.image.v2 import fakes as image_fakes from openstackclient.tests import utils -volume_attachment_server = copy.deepcopy(compute_fakes.SERVER) -volume_attachment_server['device'] = 'device' +volume_attachment_server = { + 'device': '/dev/ice', + 'server_id': '1233', +} volume_id = "ce26708d-a7f8-4b4b-9861-4a80256615a6" volume_name = "fake_volume" @@ -211,7 +212,7 @@ class TestVolume(utils.TestCommand): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) - self.app.client_manager.identity = identity_fakes.FakeIdentityv2Client( + self.app.client_manager.identity = identity_fakes.FakeIdentityv3Client( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) diff --git a/openstackclient/tests/volume/v2/test_volume.py b/openstackclient/tests/volume/v2/test_volume.py index 348ae377..70324b6e 100644 --- a/openstackclient/tests/volume/v2/test_volume.py +++ b/openstackclient/tests/volume/v2/test_volume.py @@ -15,7 +15,7 @@ import copy from openstackclient.tests import fakes -from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes from openstackclient.tests.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume @@ -27,7 +27,7 @@ class TestVolume(volume_fakes.TestVolume): self.volumes_mock = self.app.client_manager.volume.volumes self.volumes_mock.reset_mock() - self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock = self.app.client_manager.identity.projects self.projects_mock.reset_mock() self.users_mock = self.app.client_manager.identity.users @@ -548,7 +548,7 @@ class TestVolumeList(TestVolume): ] self.assertEqual(collist, columns) - server = volume_fakes.volume_attachment_server['id'] + server = volume_fakes.volume_attachment_server['server_id'] device = volume_fakes.volume_attachment_server['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( @@ -560,32 +560,145 @@ class TestVolumeList(TestVolume): ), ) self.assertEqual(datalist, tuple(data)) - def test_volume_list_all_projects_option(self): + def test_volume_list_project(self): arglist = [ - '--all-projects', + '--project', identity_fakes.project_name, ] verifylist = [ + ('project', identity_fakes.project_name), ('long', False), - ('all_projects', True), - ('name', None), + ('all_projects', False), ('status', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - collist = [ + collist = ( 'ID', 'Display Name', 'Status', 'Size', 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_project_domain(self): + arglist = [ + '--project', identity_fakes.project_name, + '--project-domain', identity_fakes.domain_name, ] - self.assertEqual(collist, columns) + verifylist = [ + ('project', identity_fakes.project_name), + ('project_domain', identity_fakes.domain_name), + ('long', False), + ('all_projects', False), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_user(self): + arglist = [ + '--user', identity_fakes.user_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('long', False), + ('all_projects', False), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) - server = volume_fakes.volume_attachment_server['id'] + server = volume_fakes.volume_attachment_server['server_id'] device = volume_fakes.volume_attachment_server['device'] msg = 'Attached to %s on %s ' % (server, device) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_user_domain(self): + arglist = [ + '--user', identity_fakes.user_name, + '--user-domain', identity_fakes.domain_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('user_domain', identity_fakes.domain_name), + ('long', False), + ('all_projects', False), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + datalist = (( volume_fakes.volume_id, volume_fakes.volume_name, @@ -618,7 +731,7 @@ class TestVolumeList(TestVolume): ) self.assertEqual(collist, tuple(columns)) - server = volume_fakes.volume_attachment_server['id'] + server = volume_fakes.volume_attachment_server['server_id'] device = volume_fakes.volume_attachment_server['device'] msg = 'Attached to %s on %s ' % (server, device) @@ -654,7 +767,42 @@ class TestVolumeList(TestVolume): ) self.assertEqual(collist, tuple(columns)) - server = volume_fakes.volume_attachment_server['id'] + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_all_projects(self): + arglist = [ + '--all-projects', + ] + verifylist = [ + ('long', False), + ('all_projects', True), + ('name', None), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = [ + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ] + self.assertEqual(collist, columns) + + server = volume_fakes.volume_attachment_server['server_id'] device = volume_fakes.volume_attachment_server['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( @@ -693,7 +841,7 @@ class TestVolumeList(TestVolume): ] self.assertEqual(collist, columns) - server = volume_fakes.volume_attachment_server['id'] + server = volume_fakes.volume_attachment_server['server_id'] device = volume_fakes.volume_attachment_server['device'] msg = 'Attached to %s on %s ' % (server, device) datalist = (( diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index d4800b8d..0973868b 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -19,7 +19,7 @@ from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_API_VERSION = '1' +DEFAULT_API_VERSION = '2' API_VERSION_OPTION = 'os_volume_api_version' API_NAME = "volume" API_VERSIONS = { diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index 03c63a05..c668e366 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -57,8 +57,8 @@ class CreateBackup(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_id = utils.find_resource(volume_client.volumes, parsed_args.volume).id @@ -88,8 +88,8 @@ class DeleteBackup(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume for backup in parsed_args.backups: backup_id = utils.find_resource(volume_client.backups, @@ -113,8 +113,8 @@ class ListBackup(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) def _format_volume_id(volume_id): """Return a volume name if available @@ -172,8 +172,8 @@ class RestoreBackup(command.Command): help='Volume to restore to (name or ID)') return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume backup = utils.find_resource(volume_client.backups, parsed_args.backup) @@ -196,8 +196,8 @@ class ShowBackup(show.ShowOne): help='Backup to display (ID only)') return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume backup = utils.find_resource(volume_client.backups, parsed_args.backup) diff --git a/openstackclient/volume/v1/qos_specs.py b/openstackclient/volume/v1/qos_specs.py index 8e909e3d..d1c70113 100644 --- a/openstackclient/volume/v1/qos_specs.py +++ b/openstackclient/volume/v1/qos_specs.py @@ -45,8 +45,8 @@ class AssociateQos(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_spec = utils.find_resource(volume_client.qos_specs, parsed_args.qos_spec) @@ -88,8 +88,8 @@ class CreateQos(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume specs = {} specs.update({'consumer': parsed_args.consumer}) @@ -117,8 +117,8 @@ class DeleteQos(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume for qos in parsed_args.qos_specs: qos_spec = utils.find_resource(volume_client.qos_specs, qos) @@ -153,8 +153,8 @@ class DisassociateQos(command.Command): return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_spec = utils.find_resource(volume_client.qos_specs, parsed_args.qos_spec) @@ -174,8 +174,8 @@ class ListQos(lister.Lister): log = logging.getLogger(__name__ + '.ListQos') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_specs_list = volume_client.qos_specs.list() @@ -218,8 +218,8 @@ class SetQos(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_spec = utils.find_resource(volume_client.qos_specs, parsed_args.qos_spec) @@ -247,8 +247,8 @@ class ShowQos(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_spec = utils.find_resource(volume_client.qos_specs, parsed_args.qos_spec) @@ -287,8 +287,8 @@ class UnsetQos(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_spec = utils.find_resource(volume_client.qos_specs, parsed_args.qos_spec) diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index e81efb5a..de7bb5b9 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -59,8 +59,8 @@ class CreateSnapshot(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_id = utils.find_resource(volume_client.volumes, parsed_args.volume).id @@ -93,8 +93,8 @@ class DeleteSnapshot(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume for snapshot in parsed_args.snapshots: snapshot_id = utils.find_resource(volume_client.volume_snapshots, @@ -118,8 +118,8 @@ class ListSnapshot(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) def _format_volume_id(volume_id): """Return a volume name if available @@ -194,8 +194,8 @@ class SetSnapshot(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) @@ -231,8 +231,8 @@ class ShowSnapshot(show.ShowOne): help='Snapshot to display (name or ID)') return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) @@ -267,8 +267,8 @@ class UnsetSnapshot(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume snapshot = utils.find_resource( volume_client.volume_snapshots, parsed_args.snapshot) diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 884611ec..0691d884 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -102,8 +102,8 @@ class CreateVolume(show.ShowOne): return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity image_client = self.app.client_manager.image @@ -186,8 +186,8 @@ class DeleteVolume(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume for volume in parsed_args.volumes: volume_obj = utils.find_resource( @@ -207,16 +207,16 @@ class ListVolume(lister.Lister): def get_parser(self, prog_name): parser = super(ListVolume, self).get_parser(prog_name) parser.add_argument( + '--name', + metavar='<name>', + help='Filter results by volume name', + ) + parser.add_argument( '--status', metavar='<status>', help='Filter results by status', ) parser.add_argument( - '--name', - metavar='<name>', - help='Filter results by name', - ) - parser.add_argument( '--all-projects', action='store_true', default=False, @@ -230,8 +230,8 @@ class ListVolume(lister.Lister): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume compute_client = self.app.client_manager.compute @@ -239,7 +239,7 @@ class ListVolume(lister.Lister): def _format_attach(attachments): """Return a formatted string of a volume's attached instances - :param volume: a volume.attachments field + :param attachments: a volume.attachments field :rtype: a string of formatted instances """ @@ -351,8 +351,8 @@ class SetVolume(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume = utils.find_resource(volume_client.volumes, parsed_args.volume) @@ -399,8 +399,8 @@ class ShowVolume(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume = utils.find_resource(volume_client.volumes, parsed_args.volume) # Map 'metadata' column to 'properties' @@ -441,8 +441,8 @@ class UnsetVolume(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume = utils.find_resource( volume_client.volumes, parsed_args.volume) diff --git a/openstackclient/volume/v1/volume_type.py b/openstackclient/volume/v1/volume_type.py index d5c617b2..d7765c79 100644 --- a/openstackclient/volume/v1/volume_type.py +++ b/openstackclient/volume/v1/volume_type.py @@ -47,8 +47,8 @@ class CreateVolumeType(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_type = volume_client.volume_types.create(parsed_args.name) volume_type._info.pop('extra_specs') @@ -75,8 +75,8 @@ class DeleteVolumeType(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_type_id = utils.find_resource( volume_client.volume_types, parsed_args.volume_type).id @@ -98,8 +98,8 @@ class ListVolumeType(lister.Lister): help='List additional fields in output') return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('ID', 'Name', 'Extra Specs') column_headers = ('ID', 'Name', 'Properties') @@ -135,8 +135,8 @@ class SetVolumeType(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_type = utils.find_resource( volume_client.volume_types, parsed_args.volume_type) @@ -170,8 +170,8 @@ class UnsetVolumeType(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_type = utils.find_resource( volume_client.volume_types, diff --git a/openstackclient/volume/v2/qos_specs.py b/openstackclient/volume/v2/qos_specs.py index ac78ca15..b3a34cac 100644 --- a/openstackclient/volume/v2/qos_specs.py +++ b/openstackclient/volume/v2/qos_specs.py @@ -45,8 +45,8 @@ class AssociateQos(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_spec = utils.find_resource(volume_client.qos_specs, parsed_args.qos_spec) @@ -88,8 +88,8 @@ class CreateQos(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume specs = {} specs.update({'consumer': parsed_args.consumer}) @@ -117,8 +117,8 @@ class DeleteQos(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume for qos in parsed_args.qos_specs: qos_spec = utils.find_resource(volume_client.qos_specs, qos) @@ -153,8 +153,8 @@ class DisassociateQos(command.Command): return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_spec = utils.find_resource(volume_client.qos_specs, parsed_args.qos_spec) @@ -174,8 +174,8 @@ class ListQos(lister.Lister): log = logging.getLogger(__name__ + '.ListQos') + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_specs_list = volume_client.qos_specs.list() @@ -218,8 +218,8 @@ class SetQos(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_spec = utils.find_resource(volume_client.qos_specs, parsed_args.qos_spec) @@ -247,8 +247,8 @@ class ShowQos(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_spec = utils.find_resource(volume_client.qos_specs, parsed_args.qos_spec) @@ -287,8 +287,8 @@ class UnsetQos(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume qos_spec = utils.find_resource(volume_client.qos_specs, parsed_args.qos_spec) diff --git a/openstackclient/volume/v2/snapshot.py b/openstackclient/volume/v2/snapshot.py index 4370cdeb..bbc92c48 100644 --- a/openstackclient/volume/v2/snapshot.py +++ b/openstackclient/volume/v2/snapshot.py @@ -186,8 +186,8 @@ class SetSnapshot(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) @@ -256,8 +256,8 @@ class UnsetSnapshot(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume snapshot = utils.find_resource( volume_client.volume_snapshots, parsed_args.snapshot) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index fe4a3ff6..430d1228 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -16,7 +16,6 @@ import copy import logging -import os from cliff import command from cliff import lister @@ -25,6 +24,7 @@ import six from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.identity import common as identity_common class CreateVolume(show.ShowOne): @@ -175,7 +175,7 @@ class DeleteVolume(command.Command): action="store_true", default=False, help="Attempt forced removal of volume(s), regardless of state " - "(defaults to False" + "(defaults to False)" ) return parser @@ -200,45 +200,58 @@ class ListVolume(lister.Lister): def get_parser(self, prog_name): parser = super(ListVolume, self).get_parser(prog_name) parser.add_argument( - '--all-projects', - action='store_true', - default=bool(int(os.environ.get("ALL_PROJECTS", 0))), - help='Include all projects (admin only)', + '--project', + metavar='<project-id>', + help='Filter results by project (name or ID) (admin only)' ) + identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( - '--long', - action='store_true', - default=False, - help='List additional fields in output', + '--user', + metavar='<user-id>', + help='Filter results by user (name or ID) (admin only)' ) + identity_common.add_user_domain_option_to_parser(parser) parser.add_argument( '--name', metavar='<name>', - help='Filter results by name', + help='Filter results by volume name', ) parser.add_argument( '--status', metavar='<status>', help='Filter results by status', ) + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help='Include all projects (admin only)', + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume compute_client = self.app.client_manager.compute + identity_client = self.app.client_manager.identity def _format_attach(attachments): """Return a formatted string of a volume's attached instances - :param volume: a volume.attachments field + :param attachments: a volume.attachments field :rtype: a string of formatted instances """ msg = '' for attachment in attachments: - server = attachment['id'] + server = attachment['server_id'] if server in server_cache: server = server_cache[server].name device = attachment['device'] @@ -282,8 +295,23 @@ class ListVolume(lister.Lister): # Just forget it if there's any trouble pass + project_id = None + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain) + + user_id = None + if parsed_args.user: + user_id = identity_common.find_user(identity_client, + parsed_args.user, + parsed_args.user_domain) + search_opts = { - 'all_projects': parsed_args.all_projects, + 'all_tenants': parsed_args.all_projects, + 'project_id': project_id, + 'user_id': user_id, 'display_name': parsed_args.name, 'status': parsed_args.status, } @@ -335,8 +363,8 @@ class SetVolume(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume = utils.find_resource(volume_client.volumes, parsed_args.volume) @@ -383,8 +411,8 @@ class ShowVolume(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume = utils.find_resource(volume_client.volumes, parsed_args.volume) @@ -416,8 +444,8 @@ class UnsetVolume(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume = utils.find_resource( volume_client.volumes, parsed_args.volume) diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index fb0342c5..8cca86f9 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -66,8 +66,8 @@ class CreateVolumeType(show.ShowOne): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume @@ -127,8 +127,8 @@ class ListVolumeType(lister.Lister): help='List additional fields in output') return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ['ID', 'Name', 'Description', 'Extra Specs'] column_headers = ['ID', 'Name', 'Description', 'Properties'] @@ -174,8 +174,8 @@ class SetVolumeType(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_type = utils.find_resource( volume_client.volume_types, parsed_args.volume_type) @@ -250,8 +250,8 @@ class UnsetVolumeType(command.Command): ) return parser + @utils.log_method(log) def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_type = utils.find_resource( volume_client.volume_types, |
