diff options
Diffstat (limited to 'openstackclient')
233 files changed, 23912 insertions, 3129 deletions
diff --git a/openstackclient/api/image_v1.py b/openstackclient/api/image_v1.py index 534c7750..e15d825a 100644 --- a/openstackclient/api/image_v1.py +++ b/openstackclient/api/image_v1.py @@ -19,7 +19,7 @@ from openstackclient.api import api class APIv1(api.BaseAPI): """Image v1 API""" - _endpoint_suffix = 'v1' + _endpoint_suffix = '/v1' def __init__(self, endpoint=None, **kwargs): super(APIv1, self).__init__(endpoint=endpoint, **kwargs) @@ -29,8 +29,8 @@ class APIv1(api.BaseAPI): def _munge_url(self): # Hack this until discovery is up - if self._endpoint_suffix not in self.endpoint.split('/')[-1]: - self.endpoint = '/'.join([self.endpoint, self._endpoint_suffix]) + if not self.endpoint.endswith(self._endpoint_suffix): + self.endpoint = self.endpoint + self._endpoint_suffix def image_list( self, diff --git a/openstackclient/api/image_v2.py b/openstackclient/api/image_v2.py index 026498fa..c3628121 100644 --- a/openstackclient/api/image_v2.py +++ b/openstackclient/api/image_v2.py @@ -19,12 +19,12 @@ from openstackclient.api import image_v1 class APIv2(image_v1.APIv1): """Image v2 API""" - _endpoint_suffix = 'v2' + _endpoint_suffix = '/v2' def _munge_url(self): # Hack this until discovery is up, and ignore parent endpoint setting - if 'v2' not in self.endpoint.split('/')[-1]: - self.endpoint = '/'.join([self.endpoint, 'v2']) + if not self.endpoint.endswith(self._endpoint_suffix): + self.endpoint = self.endpoint + self._endpoint_suffix def image_list( self, diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index 184814c6..74c4a46f 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -380,7 +380,7 @@ class APIv1(api.BaseAPI): if len(os.path.dirname(file)) > 0: os.makedirs(os.path.dirname(file)) with open(file, 'wb') as f: - for chunk in response.iter_content(): + for chunk in response.iter_content(64 * 1024): f.write(chunk) def object_set( diff --git a/openstackclient/common/availability_zone.py b/openstackclient/common/availability_zone.py index 63c55370..b2385ef7 100644 --- a/openstackclient/common/availability_zone.py +++ b/openstackclient/common/availability_zone.py @@ -88,7 +88,7 @@ def _xform_network_availability_zone(az): class ListAvailabilityZone(command.Lister): - """List availability zones and their status""" + _description = _("List availability zones and their status") def get_parser(self, prog_name): parser = super(ListAvailabilityZone, self).get_parser(prog_name) diff --git a/openstackclient/common/client_config.py b/openstackclient/common/client_config.py index 30286df8..a22dd0cb 100644 --- a/openstackclient/common/client_config.py +++ b/openstackclient/common/client_config.py @@ -13,122 +13,15 @@ """OpenStackConfig subclass for argument compatibility""" -import logging - -from os_client_config import config -from os_client_config import exceptions as occ_exceptions - - -LOG = logging.getLogger(__name__) +from osc_lib.cli import client_config # Sublcass OpenStackConfig in order to munge config values # before auth plugins are loaded -class OSC_Config(config.OpenStackConfig): - - # TODO(dtroyer): Once os-client-config with pw_func argument is in - # global-requirements we can remove __init()__ - def __init__( - self, - config_files=None, - vendor_files=None, - override_defaults=None, - force_ipv4=None, - envvar_prefix=None, - secure_files=None, - pw_func=None, - ): - ret = super(OSC_Config, self).__init__( - config_files=config_files, - vendor_files=vendor_files, - override_defaults=override_defaults, - force_ipv4=force_ipv4, - envvar_prefix=envvar_prefix, - secure_files=secure_files, - ) - - # NOTE(dtroyer): This will be pushed down into os-client-config - # The default is there is no callback, the calling - # application must specify what to use, typically - # it will be osc_lib.shell.prompt_for_password() - if '_pw_callback' not in vars(self): - # Set the default if it doesn't already exist - self._pw_callback = None - if pw_func is not None: - # Set the passed in value - self._pw_callback = pw_func - - return ret - - def _auth_select_default_plugin(self, config): - """Select a default plugin based on supplied arguments - - Migrated from auth.select_auth_plugin() - """ - - identity_version = config.get('identity_api_version', '') - - if config.get('username', None) and not config.get('auth_type', None): - if identity_version == '3': - config['auth_type'] = 'v3password' - elif identity_version.startswith('2'): - config['auth_type'] = 'v2password' - else: - # let keystoneauth figure it out itself - config['auth_type'] = 'password' - elif config.get('token', None) and not config.get('auth_type', None): - if identity_version == '3': - config['auth_type'] = 'v3token' - elif identity_version.startswith('2'): - config['auth_type'] = 'v2token' - else: - # let keystoneauth figure it out itself - config['auth_type'] = 'token' - else: - # The ultimate default is similar to the original behaviour, - # but this time with version discovery - if not config.get('auth_type', None): - config['auth_type'] = 'password' - - LOG.debug("Auth plugin %s selected" % config['auth_type']) - return config - - def _auth_v2_arguments(self, config): - """Set up v2-required arguments from v3 info - - Migrated from auth.build_auth_params() - """ - - if ('auth_type' in config and config['auth_type'].startswith("v2")): - if 'project_id' in config['auth']: - config['auth']['tenant_id'] = config['auth']['project_id'] - if 'project_name' in config['auth']: - config['auth']['tenant_name'] = config['auth']['project_name'] - return config - - def _auth_v2_ignore_v3(self, config): - """Remove v3 arguemnts if present for v2 plugin - - Migrated from clientmanager.setup_auth() - """ - - # NOTE(hieulq): If USER_DOMAIN_NAME, USER_DOMAIN_ID, PROJECT_DOMAIN_ID - # or PROJECT_DOMAIN_NAME is present and API_VERSION is 2.0, then - # ignore all domain related configs. - if (config.get('identity_api_version', '').startswith('2') and - config.get('auth_type', None).endswith('password')): - domain_props = [ - 'project_domain_id', - 'project_domain_name', - 'user_domain_id', - 'user_domain_name', - ] - for prop in domain_props: - if config['auth'].pop(prop, None) is not None: - LOG.warning("Ignoring domain related config " + - prop + " because identity API version is 2.0") - return config +class OSC_Config(client_config.OSC_Config): + # TODO(dtroyer): Remove _auth_default_domain when the v3otp fix is + # backported to osc-lib, should be in release 1.3.0 def _auth_default_domain(self, config): """Set a default domain from available arguments @@ -149,6 +42,7 @@ class OSC_Config(config.OpenStackConfig): # present, then do not change the behaviour. Otherwise, set the # PROJECT_DOMAIN_ID to 'OS_DEFAULT_DOMAIN' for better usability. if ( + auth_type in ("password", "v3password", "v3totp") and not config['auth'].get('project_domain_id') and not config['auth'].get('project_domain_name') ): @@ -168,21 +62,6 @@ class OSC_Config(config.OpenStackConfig): config['auth']['user_domain_id'] = default_domain return config - def auth_config_hook(self, config): - """Allow examination of config values before loading auth plugin - - OpenStackClient will override this to perform additional chacks - on auth_type. - """ - - config = self._auth_select_default_plugin(config) - config = self._auth_v2_arguments(config) - config = self._auth_v2_ignore_v3(config) - config = self._auth_default_domain(config) - - LOG.debug("auth_config_hook(): %s" % config) - return config - def load_auth_plugin(self, config): """Get auth plugin and validate args""" @@ -190,68 +69,3 @@ class OSC_Config(config.OpenStackConfig): config = self._validate_auth(config, loader) auth_plugin = loader.load_from_options(**config['auth']) return auth_plugin - - def _validate_auth_ksc(self, config, cloud, fixed_argparse=None): - """Old compatibility hack for OSC, no longer needed/wanted""" - return config - - def _validate_auth(self, config, loader, fixed_argparse=None): - """Validate auth plugin arguments""" - # May throw a keystoneauth1.exceptions.NoMatchingPlugin - - plugin_options = loader.get_options() - - msgs = [] - prompt_options = [] - for p_opt in plugin_options: - # if it's in config, win, move it and kill it from config dict - # if it's in config.auth but not in config we're good - # deprecated loses to current - # provided beats default, deprecated or not - winning_value = self._find_winning_auth_value(p_opt, config) - if not winning_value: - winning_value = self._find_winning_auth_value( - p_opt, config['auth']) - - # if the plugin tells us that this value is required - # then error if it's doesn't exist now - if not winning_value and p_opt.required: - msgs.append( - 'Missing value {auth_key}' - ' required for auth plugin {plugin}'.format( - auth_key=p_opt.name, plugin=config.get('auth_type'), - ) - ) - - # Clean up after ourselves - for opt in [p_opt.name] + [o.name for o in p_opt.deprecated]: - opt = opt.replace('-', '_') - config.pop(opt, None) - config['auth'].pop(opt, None) - - if winning_value: - # Prefer the plugin configuration dest value if the value's key - # is marked as depreciated. - if p_opt.dest is None: - config['auth'][p_opt.name.replace('-', '_')] = ( - winning_value) - else: - config['auth'][p_opt.dest] = winning_value - - # See if this needs a prompting - if ( - 'prompt' in vars(p_opt) and - p_opt.prompt is not None and - p_opt.dest not in config['auth'] and - self._pw_callback is not None - ): - # Defer these until we know all required opts are present - prompt_options.append(p_opt) - - if msgs: - raise occ_exceptions.OpenStackConfigException('\n'.join(msgs)) - else: - for p_opt in prompt_options: - config['auth'][p_opt.dest] = self._pw_callback(p_opt.prompt) - - return config diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 23c35a3b..3e1a50e3 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -59,6 +59,8 @@ class ClientManager(clientmanager.ClientManager): self._interface = self.interface self._cacert = self.cacert self._insecure = not self.verify + # store original auth_type + self._original_auth_type = cli_options.auth_type def setup_auth(self): """Set up authentication""" @@ -73,12 +75,33 @@ class ClientManager(clientmanager.ClientManager): if self._cli_options._openstack_config is not None: self._cli_options._openstack_config._pw_callback = \ shell.prompt_for_password + try: + self._cli_options._auth = \ + self._cli_options._openstack_config.load_auth_plugin( + self._cli_options.config, + ) + except TypeError as e: + self._fallback_load_auth_plugin(e) + + return super(ClientManager, self).setup_auth() + + def _fallback_load_auth_plugin(self, e): + # NOTES(RuiChen): Hack to avoid auth plugins choking on data they don't + # expect, delete fake token and endpoint, then try to + # load auth plugin again with user specified options. + # We know it looks ugly, but it's necessary. + if self._cli_options.config['auth']['token'] == 'x': + # restore original auth_type + self._cli_options.config['auth_type'] = \ + self._original_auth_type + del self._cli_options.config['auth']['token'] + del self._cli_options.config['auth']['endpoint'] self._cli_options._auth = \ self._cli_options._openstack_config.load_auth_plugin( self._cli_options.config, ) - - return super(ClientManager, self).setup_auth() + else: + raise e def is_network_endpoint_enabled(self): """Check if the network endpoint is enabled""" diff --git a/openstackclient/common/configuration.py b/openstackclient/common/configuration.py index 016e9191..57825bb0 100644 --- a/openstackclient/common/configuration.py +++ b/openstackclient/common/configuration.py @@ -23,7 +23,7 @@ REDACTED = "<redacted>" class ShowConfiguration(command.ShowOne): - """Display configuration details""" + _description = _("Display configuration details") def get_parser(self, prog_name): parser = super(ShowConfiguration, self).get_parser(prog_name) diff --git a/openstackclient/common/extension.py b/openstackclient/common/extension.py index 07c407f6..d5b72238 100644 --- a/openstackclient/common/extension.py +++ b/openstackclient/common/extension.py @@ -28,7 +28,7 @@ LOG = logging.getLogger(__name__) class ListExtension(command.Lister): - """List API extensions""" + _description = _("List API extensions") def get_parser(self, prog_name): parser = super(ListExtension, self).get_parser(prog_name) diff --git a/openstackclient/common/limits.py b/openstackclient/common/limits.py index f7aa82f6..957f1d02 100644 --- a/openstackclient/common/limits.py +++ b/openstackclient/common/limits.py @@ -25,7 +25,7 @@ from openstackclient.identity import common as identity_common class ShowLimits(command.Lister): - """Show compute and block storage limits""" + _description = _("Show compute and block storage limits") def get_parser(self, prog_name): parser = super(ShowLimits, self).get_parser(prog_name) diff --git a/openstackclient/common/module.py b/openstackclient/common/module.py index 7c5fcd55..20497f21 100644 --- a/openstackclient/common/module.py +++ b/openstackclient/common/module.py @@ -25,16 +25,30 @@ from openstackclient.i18n import _ class ListCommand(command.Lister): - """List recognized commands by group""" + _description = _("List recognized commands by group") auth_required = False + def get_parser(self, prog_name): + parser = super(ListCommand, self).get_parser(prog_name) + parser.add_argument( + '--group', + metavar='<group-keyword>', + help=_('Show commands filtered by a command group, for example: ' + 'identity, volume, compute, image, network and ' + 'other keywords'), + ) + return parser + def take_action(self, parsed_args): cm = self.app.command_manager groups = cm.get_command_groups() groups = sorted(groups) columns = ('Command Group', 'Commands') + if parsed_args.group: + groups = (group for group in groups if parsed_args.group in group) + commands = [] for group in groups: command_names = cm.get_command_names(group) @@ -53,7 +67,7 @@ class ListCommand(command.Lister): class ListModule(command.ShowOne): - """List module versions""" + _description = _("List module versions") auth_required = False @@ -74,15 +88,29 @@ class ListModule(command.ShowOne): mods = sys.modules for k in mods.keys(): k = k.split('.')[0] - # TODO(dtroyer): Need a better way to decide which modules to - # show for the default (not --all) invocation. - # It should be just the things we actually care - # about like client and plugin modules... - if (parsed_args.all or 'client' in k): - try: - data[k] = mods[k].__version__ - except AttributeError: - # aw, just skip it - pass + # Skip private modules and the modules that had been added, + # like: keystoneclient, keystoneclient.exceptions and + # keystoneclient.auth + if not k.startswith('_') and k not in data: + # TODO(dtroyer): Need a better way to decide which modules to + # show for the default (not --all) invocation. + # It should be just the things we actually care + # about like client and plugin modules... + if (parsed_args.all or + # Handle xxxclient and openstacksdk + (k.endswith('client') or k == 'openstack')): + try: + # NOTE(RuiChen): openstacksdk bug/1588823 exist, + # no good way to add __version__ for + # openstack module properly, hard code + # looks bad, but openstacksdk module + # information is important. + if k == 'openstack': + data[k] = mods[k].version.__version__ + else: + data[k] = mods[k].__version__ + except Exception: + # Catch all exceptions, just skip it + pass return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 8f099cc9..ec4c8b51 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -16,6 +16,7 @@ """Quota action implementations""" import itertools +import logging import sys from osc_lib.command import command @@ -25,6 +26,8 @@ import six from openstackclient.i18n import _ +LOG = logging.getLogger(__name__) + # List the quota items, map the internal argument name to the option # name that the user sees. @@ -78,9 +81,179 @@ NETWORK_QUOTAS = { 'l7policy': 'l7policies', } +NETWORK_KEYS = ['floating_ips', 'networks', 'rbac_policies', 'routers', + 'ports', 'security_group_rules', 'security_groups', + 'subnet_pools', 'subnets'] + + +def _xform_get_quota(data, value, keys): + res = [] + res_info = {} + for key in keys: + res_info[key] = getattr(data, key, '') + + res_info['id'] = value + res.append(res_info) + return res + + +class ListQuota(command.Lister): + _description = _("List quotas for all projects " + "with non-default quota values") + + def get_parser(self, prog_name): + parser = super(ListQuota, self).get_parser(prog_name) + option = parser.add_mutually_exclusive_group(required=True) + option.add_argument( + '--compute', + action='store_true', + default=False, + help=_('List compute quota'), + ) + option.add_argument( + '--volume', + action='store_true', + default=False, + help=_('List volume quota'), + ) + option.add_argument( + '--network', + action='store_true', + default=False, + help=_('List network quota'), + ) + return parser + + def take_action(self, parsed_args): + projects = self.app.client_manager.identity.projects.list() + result = [] + project_ids = [getattr(p, 'id', '') for p in projects] + + if parsed_args.compute: + compute_client = self.app.client_manager.compute + for p in project_ids: + data = compute_client.quotas.get(p) + result_data = _xform_get_quota(data, p, + COMPUTE_QUOTAS.keys()) + default_data = compute_client.quotas.defaults(p) + result_default = _xform_get_quota(default_data, + p, + COMPUTE_QUOTAS.keys()) + if result_default != result_data: + result += result_data + + columns = ( + 'id', + 'cores', + 'fixed_ips', + 'injected_files', + 'injected_file_content_bytes', + 'injected_file_path_bytes', + 'instances', + 'key_pairs', + 'metadata_items', + 'ram', + 'server_groups', + 'server_group_members', + ) + column_headers = ( + 'Project ID', + 'Cores', + 'Fixed IPs', + 'Injected Files', + 'Injected File Content Bytes', + 'Injected File Path Bytes', + 'Instances', + 'Key Pairs', + 'Metadata Items', + 'Ram', + 'Server Groups', + 'Server Group Members', + ) + return (column_headers, + (utils.get_dict_properties( + s, columns, + ) for s in result)) + if parsed_args.volume: + volume_client = self.app.client_manager.volume + for p in project_ids: + data = volume_client.quotas.get(p) + result_data = _xform_get_quota(data, p, + VOLUME_QUOTAS.keys()) + default_data = volume_client.quotas.defaults(p) + result_default = _xform_get_quota(default_data, + p, + VOLUME_QUOTAS.keys()) + if result_default != result_data: + result += result_data + + columns = ( + 'id', + 'backups', + 'backup_gigabytes', + 'gigabytes', + 'per_volume_gigabytes', + 'snapshots', + 'volumes', + ) + column_headers = ( + 'Project ID', + 'Backups', + 'Backup Gigabytes', + 'Gigabytes', + 'Per Volume Gigabytes', + 'Snapshots', + 'Volumes', + ) + return (column_headers, + (utils.get_dict_properties( + s, columns, + ) for s in result)) + if parsed_args.network: + client = self.app.client_manager.network + for p in project_ids: + data = client.get_quota(p) + result_data = _xform_get_quota(data, p, NETWORK_KEYS) + default_data = client.get_quota_default(p) + result_default = _xform_get_quota(default_data, + p, NETWORK_KEYS) + if result_default != result_data: + result += result_data + + columns = ( + 'id', + 'floating_ips', + 'networks', + 'ports', + 'rbac_policies', + 'routers', + 'security_groups', + 'security_group_rules', + 'subnets', + 'subnet_pools', + ) + column_headers = ( + 'Project ID', + 'Floating IPs', + 'Networks', + 'Ports', + 'RBAC Policies', + 'Routers', + 'Security Groups', + 'Security Group Rules', + 'Subnets', + 'Subnet Pools' + ) + return (column_headers, + (utils.get_dict_properties( + s, columns, + ) for s in result)) + + return ((), ()) + class SetQuota(command.Command): - """Set quotas for project or class""" + _description = _("Set quotas for project or class") def _build_options_list(self): if self.app.client_manager.is_network_endpoint_enabled(): @@ -165,7 +338,7 @@ class SetQuota(command.Command): **volume_kwargs) if network_kwargs: sys.stderr.write("Network quotas are ignored since quota class" - "is not supported.") + " is not supported.") else: project = utils.find_resource( identity_client.projects, @@ -186,7 +359,7 @@ class SetQuota(command.Command): class ShowQuota(command.ShowOne): - """Show quotas for project or class""" + _description = _("Show quotas for project or class") def get_parser(self, prog_name): parser = super(ShowQuota, self).get_parser(prog_name) @@ -273,6 +446,10 @@ class ShowQuota(command.ShowOne): volume_quota_info = self.get_compute_volume_quota(volume_client, parsed_args) network_quota_info = self.get_network_quota(parsed_args) + # NOTE(reedip): Remove the below check once requirement for + # Openstack SDK is fixed to version 0.9.12 and above + if type(network_quota_info) is not dict: + network_quota_info = network_quota_info.to_dict() info = {} info.update(compute_quota_info) diff --git a/openstackclient/compute/v2/agent.py b/openstackclient/compute/v2/agent.py index 76c1b3b7..151dcc1e 100644 --- a/openstackclient/compute/v2/agent.py +++ b/openstackclient/compute/v2/agent.py @@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__) class CreateAgent(command.ShowOne): - """Create compute agent""" + _description = _("Create compute agent") def get_parser(self, prog_name): parser = super(CreateAgent, self).get_parser(prog_name) @@ -81,7 +81,7 @@ class CreateAgent(command.ShowOne): class DeleteAgent(command.Command): - """Delete compute agent(s)""" + _description = _("Delete compute agent(s)") def get_parser(self, prog_name): parser = super(DeleteAgent, self).get_parser(prog_name) @@ -112,7 +112,7 @@ class DeleteAgent(command.Command): class ListAgent(command.Lister): - """List compute agents""" + _description = _("List compute agents") def get_parser(self, prog_name): parser = super(ListAgent, self).get_parser(prog_name) @@ -142,7 +142,7 @@ class ListAgent(command.Lister): class SetAgent(command.Command): - """Set compute agent properties""" + _description = _("Set compute agent properties") def get_parser(self, prog_name): parser = super(SetAgent, self).get_parser(prog_name) diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index 58d529e9..7f9161a9 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__) class AddAggregateHost(command.ShowOne): - """Add host to aggregate""" + _description = _("Add host to aggregate") def get_parser(self, prog_name): parser = super(AddAggregateHost, self).get_parser(prog_name) @@ -62,7 +62,7 @@ class AddAggregateHost(command.ShowOne): class CreateAggregate(command.ShowOne): - """Create a new aggregate""" + _description = _("Create a new aggregate") def get_parser(self, prog_name): parser = super(CreateAggregate, self).get_parser(prog_name) @@ -105,7 +105,7 @@ class CreateAggregate(command.ShowOne): class DeleteAggregate(command.Command): - """Delete existing aggregate(s)""" + _description = _("Delete existing aggregate(s)") def get_parser(self, prog_name): parser = super(DeleteAggregate, self).get_parser(prog_name) @@ -128,8 +128,8 @@ class DeleteAggregate(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete aggregate with name or " - "ID '%(aggregate)s': %(e)s") - % {'aggregate': a, 'e': e}) + "ID '%(aggregate)s': %(e)s"), + {'aggregate': a, 'e': e}) if result > 0: total = len(parsed_args.aggregate) @@ -139,7 +139,7 @@ class DeleteAggregate(command.Command): class ListAggregate(command.Lister): - """List all aggregates""" + _description = _("List all aggregates") def get_parser(self, prog_name): parser = super(ListAggregate, self).get_parser(prog_name) @@ -188,7 +188,7 @@ class ListAggregate(command.Lister): class RemoveAggregateHost(command.ShowOne): - """Remove host from aggregate""" + _description = _("Remove host from aggregate") def get_parser(self, prog_name): parser = super(RemoveAggregateHost, self).get_parser(prog_name) @@ -222,7 +222,7 @@ class RemoveAggregateHost(command.ShowOne): class SetAggregate(command.Command): - """Set aggregate properties""" + _description = _("Set aggregate properties") def get_parser(self, prog_name): parser = super(SetAggregate, self).get_parser(prog_name) @@ -298,7 +298,7 @@ class SetAggregate(command.Command): class ShowAggregate(command.ShowOne): - """Display aggregate details""" + _description = _("Display aggregate details") def get_parser(self, prog_name): parser = super(ShowAggregate, self).get_parser(prog_name) @@ -334,7 +334,7 @@ class ShowAggregate(command.ShowOne): class UnsetAggregate(command.Command): - """Unset aggregate properties""" + _description = _("Unset aggregate properties") def get_parser(self, prog_name): parser = super(UnsetAggregate, self).get_parser(prog_name) diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index 02be99d5..25f92108 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -26,7 +26,7 @@ from openstackclient.i18n import _ class ShowConsoleLog(command.Command): - """Show server's console output""" + _description = _("Show server's console output") def get_parser(self, prog_name): parser = super(ShowConsoleLog, self).get_parser(prog_name) @@ -64,7 +64,7 @@ class ShowConsoleLog(command.Command): class ShowConsoleURL(command.ShowOne): - """Show server's remote console URL""" + _description = _("Show server's remote console URL") def get_parser(self, prog_name): parser = super(ShowConsoleURL, self).get_parser(prog_name) @@ -126,18 +126,7 @@ class ShowConsoleURL(command.ShowOne): parsed_args.server, ) - data = None - if parsed_args.url_type in ['novnc', 'xvpvnc']: - data = server.get_vnc_console(parsed_args.url_type) - if parsed_args.url_type in ['spice-html5']: - data = server.get_spice_console(parsed_args.url_type) - if parsed_args.url_type in ['rdp-html5']: - data = server.get_rdp_console(parsed_args.url_type) - if parsed_args.url_type in ['serial']: - data = server.get_serial_console(parsed_args.url_type) - if parsed_args.url_type in ['webmks']: - data = server.get_mks_console() - + data = server.get_console_url(parsed_args.url_type) if not data: return ({}, {}) diff --git a/openstackclient/compute/v2/fixedip.py b/openstackclient/compute/v2/fixedip.py index c14d29fa..0c0b619e 100644 --- a/openstackclient/compute/v2/fixedip.py +++ b/openstackclient/compute/v2/fixedip.py @@ -24,7 +24,7 @@ from openstackclient.i18n import _ class AddFixedIP(command.Command): - """Add fixed IP address to server""" + _description = _("Add fixed IP address to server") # TODO(tangchen): Remove this class and ``ip fixed add`` command # two cycles after Mitaka. @@ -64,7 +64,7 @@ class AddFixedIP(command.Command): class RemoveFixedIP(command.Command): - """Remove fixed IP address from server""" + _description = _("Remove fixed IP address from server") # TODO(tangchen): Remove this class and ``ip fixed remove`` command # two cycles after Mitaka. diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index b3f09ce5..009c9bd1 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -49,7 +49,7 @@ def _find_flavor(compute_client, flavor): class CreateFlavor(command.ShowOne): - """Create new flavor""" + _description = _("Create new flavor") def get_parser(self, prog_name): parser = super(CreateFlavor, self).get_parser(prog_name) @@ -89,9 +89,9 @@ class CreateFlavor(command.ShowOne): parser.add_argument( "--swap", type=int, - metavar="<size-gb>", + metavar="<size-mb>", default=0, - help=_("Swap space size in GB (default 0G)") + help=_("Swap space size in MB (default 0M)") ) parser.add_argument( "--vcpus", @@ -167,11 +167,11 @@ class CreateFlavor(command.ShowOne): parsed_args.project_domain, ).id compute_client.flavor_access.add_tenant_access( - parsed_args.id, project_id) + flavor.id, project_id) except Exception as e: msg = _("Failed to add project %(project)s access to " "flavor: %(e)s") - LOG.error(msg % {'project': parsed_args.project, 'e': e}) + LOG.error(msg, {'project': parsed_args.project, 'e': e}) if parsed_args.property: try: flavor.set_keys(parsed_args.property) @@ -186,7 +186,7 @@ class CreateFlavor(command.ShowOne): class DeleteFlavor(command.Command): - """Delete flavor(s)""" + _description = _("Delete flavor(s)") def get_parser(self, prog_name): parser = super(DeleteFlavor, self).get_parser(prog_name) @@ -208,8 +208,7 @@ class DeleteFlavor(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete flavor with name or " - "ID '%(flavor)s': %(e)s") - % {'flavor': f, 'e': e}) + "ID '%(flavor)s': %(e)s"), {'flavor': f, 'e': e}) if result > 0: total = len(parsed_args.flavor) @@ -219,7 +218,7 @@ class DeleteFlavor(command.Command): class ListFlavor(command.Lister): - """List flavors""" + _description = _("List flavors") def get_parser(self, prog_name): parser = super(ListFlavor, self).get_parser(prog_name) @@ -303,7 +302,7 @@ class ListFlavor(command.Lister): class SetFlavor(command.Command): - """Set flavor properties""" + _description = _("Set flavor properties") def get_parser(self, prog_name): parser = super(SetFlavor, self).get_parser(prog_name) @@ -313,6 +312,14 @@ class SetFlavor(command.Command): help=_("Flavor to modify (name or ID)") ) parser.add_argument( + "--no-property", + action="store_true", + help=_("Remove all properties from this flavor " + "(specify both --no-property and --property" + " to remove the current properties before setting" + " new properties.)"), + ) + parser.add_argument( "--property", metavar="<key=value>", action=parseractions.KeyValueAction, @@ -336,6 +343,15 @@ class SetFlavor(command.Command): flavor = _find_flavor(compute_client, parsed_args.flavor) result = 0 + key_list = [] + if parsed_args.no_property: + try: + for key in flavor.get_keys().keys(): + key_list.append(key) + flavor.unset_keys(key_list) + except Exception as e: + LOG.error(_("Failed to clear flavor property: %s"), e) + result += 1 if parsed_args.property: try: flavor.set_keys(parsed_args.property) @@ -366,7 +382,7 @@ class SetFlavor(command.Command): class ShowFlavor(command.ShowOne): - """Display flavor details""" + _description = _("Display flavor details") def get_parser(self, prog_name): parser = super(ShowFlavor, self).get_parser(prog_name) @@ -395,7 +411,7 @@ class ShowFlavor(command.ShowOne): except Exception as e: msg = _("Failed to get access projects list " "for flavor '%(flavor)s': %(e)s") - LOG.error(msg % {'flavor': parsed_args.flavor, 'e': e}) + LOG.error(msg, {'flavor': parsed_args.flavor, 'e': e}) flavor = resource_flavor._info.copy() flavor.update({ @@ -409,7 +425,7 @@ class ShowFlavor(command.ShowOne): class UnsetFlavor(command.Command): - """Unset flavor properties""" + _description = _("Unset flavor properties") def get_parser(self, prog_name): parser = super(UnsetFlavor, self).get_parser(prog_name) diff --git a/openstackclient/compute/v2/floatingip.py b/openstackclient/compute/v2/floatingip.py index 8398ea57..69595bed 100644 --- a/openstackclient/compute/v2/floatingip.py +++ b/openstackclient/compute/v2/floatingip.py @@ -24,7 +24,7 @@ from openstackclient.i18n import _ class AddFloatingIP(command.Command): - """Add floating IP address to server""" + _description = _("Add floating IP address to server") # TODO(tangchen): Remove this class and ``ip floating add`` command # two cycles after Mitaka. @@ -61,7 +61,7 @@ class AddFloatingIP(command.Command): class RemoveFloatingIP(command.Command): - """Remove floating IP address from server""" + _description = _("Remove floating IP address from server") # TODO(tangchen): Remove this class and ``ip floating remove`` command # two cycles after Mitaka. diff --git a/openstackclient/compute/v2/host.py b/openstackclient/compute/v2/host.py index 4785377e..a495b367 100644 --- a/openstackclient/compute/v2/host.py +++ b/openstackclient/compute/v2/host.py @@ -22,7 +22,7 @@ from openstackclient.i18n import _ class ListHost(command.Lister): - """List hosts""" + _description = _("List hosts") def get_parser(self, prog_name): parser = super(ListHost, self).get_parser(prog_name) @@ -48,7 +48,8 @@ class ListHost(command.Lister): class SetHost(command.Command): - """Set host properties""" + _description = _("Set host properties") + def get_parser(self, prog_name): parser = super(SetHost, self).get_parser(prog_name) parser.add_argument( @@ -107,7 +108,7 @@ class SetHost(command.Command): class ShowHost(command.Lister): - """Display host details""" + _description = _("Display host details") def get_parser(self, prog_name): parser = super(ShowHost, self).get_parser(prog_name) diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index 0222e899..406aa917 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -26,7 +26,7 @@ from openstackclient.i18n import _ class ListHypervisor(command.Lister): - """List hypervisors""" + _description = _("List hypervisors") def get_parser(self, prog_name): parser = super(ListHypervisor, self).get_parser(prog_name) @@ -35,14 +35,24 @@ class ListHypervisor(command.Lister): metavar="<hostname>", help=_("Filter hypervisors using <hostname> substring") ) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output") + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute columns = ( "ID", - "Hypervisor Hostname" + "Hypervisor Hostname", + "Hypervisor Type", + "Host IP", + "State" ) + if parsed_args.long: + columns += ("vCPUs Used", "vCPUs", "Memory MB Used", "Memory MB") if parsed_args.matching: data = compute_client.hypervisors.search(parsed_args.matching) @@ -56,7 +66,7 @@ class ListHypervisor(command.Lister): class ShowHypervisor(command.ShowOne): - """Display hypervisor details""" + _description = _("Display hypervisor details") def get_parser(self, prog_name): parser = super(ShowHypervisor, self).get_parser(prog_name) diff --git a/openstackclient/compute/v2/hypervisor_stats.py b/openstackclient/compute/v2/hypervisor_stats.py index c6fd2992..b0413005 100644 --- a/openstackclient/compute/v2/hypervisor_stats.py +++ b/openstackclient/compute/v2/hypervisor_stats.py @@ -17,9 +17,11 @@ from osc_lib.command import command import six +from openstackclient.i18n import _ + class ShowHypervisorStats(command.ShowOne): - """Display hypervisor stats details""" + _description = _("Display hypervisor stats details") def take_action(self, parsed_args): compute_client = self.app.client_manager.compute diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index d30fd429..851cced0 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -32,19 +32,27 @@ LOG = logging.getLogger(__name__) class CreateKeypair(command.ShowOne): - """Create new public key""" + _description = _("Create new public or private key for server ssh access") def get_parser(self, prog_name): parser = super(CreateKeypair, self).get_parser(prog_name) parser.add_argument( 'name', metavar='<name>', - help=_("New public key name") + help=_("New public or private key name") ) - parser.add_argument( + key_group = parser.add_mutually_exclusive_group() + key_group.add_argument( '--public-key', metavar='<file>', - help=_("Filename for public key to add") + help=_("Filename for public key to add. If not used, " + "creates a private key.") + ) + key_group.add_argument( + '--private-key', + metavar='<file>', + help=_("Filename for private key to save. If not used, " + "print private key in console.") ) return parser @@ -68,13 +76,31 @@ class CreateKeypair(command.ShowOne): public_key=public_key, ) + private_key = parsed_args.private_key + # Save private key into specified file + if private_key: + try: + with io.open( + os.path.expanduser(parsed_args.private_key), 'w+' + ) as p: + p.write(keypair.private_key) + except IOError as e: + msg = _("Key file %(private_key)s can not be saved: " + "%(exception)s") + raise exceptions.CommandError( + msg % {"private_key": parsed_args.private_key, + "exception": e} + ) # NOTE(dtroyer): how do we want to handle the display of the private # key when it needs to be communicated back to the user # For now, duplicate nova keypair-add command output info = {} - if public_key: + if public_key or private_key: info.update(keypair._info) - del info['public_key'] + if 'public_key' in info: + del info['public_key'] + if 'private_key' in info: + del info['private_key'] return zip(*sorted(six.iteritems(info))) else: sys.stdout.write(keypair.private_key) @@ -82,7 +108,7 @@ class CreateKeypair(command.ShowOne): class DeleteKeypair(command.Command): - """Delete public key(s)""" + _description = _("Delete public or private key(s)") def get_parser(self, prog_name): parser = super(DeleteKeypair, self).get_parser(prog_name) @@ -90,7 +116,7 @@ class DeleteKeypair(command.Command): 'name', metavar='<key>', nargs='+', - help=_("Public key(s) to delete (name only)") + help=_("Name of key(s) to delete (name only)") ) return parser @@ -104,19 +130,18 @@ class DeleteKeypair(command.Command): compute_client.keypairs.delete(data.name) except Exception as e: result += 1 - LOG.error(_("Failed to delete public key with name " - "'%(name)s': %(e)s") - % {'name': n, 'e': e}) + LOG.error(_("Failed to delete key with name " + "'%(name)s': %(e)s"), {'name': n, 'e': e}) if result > 0: total = len(parsed_args.name) - msg = (_("%(result)s of %(total)s public keys failed " + msg = (_("%(result)s of %(total)s keys failed " "to delete.") % {'result': result, 'total': total}) raise exceptions.CommandError(msg) class ListKeypair(command.Lister): - """List public key fingerprints""" + _description = _("List key fingerprints") def take_action(self, parsed_args): compute_client = self.app.client_manager.compute @@ -133,20 +158,20 @@ class ListKeypair(command.Lister): class ShowKeypair(command.ShowOne): - """Display public key details""" + _description = _("Display key details") def get_parser(self, prog_name): parser = super(ShowKeypair, self).get_parser(prog_name) parser.add_argument( 'name', metavar='<key>', - help=_("Public key to display (name only)") + help=_("Public or private key to display (name only)") ) parser.add_argument( '--public-key', action='store_true', default=False, - help=_("Show only bare public key (name only)") + help=_("Show only bare public key paired with the generated key") ) return parser diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1ca31497..1fe5bb0d 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -26,6 +26,7 @@ from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from oslo_utils import timeutils import six try: @@ -113,7 +114,7 @@ def _get_ip_address(addresses, address_type, ip_address_family): ) -def _prep_server_detail(compute_client, server): +def _prep_server_detail(compute_client, image_client, server): """Prepare the detailed server dict for printing :param compute_client: a compute client instance @@ -130,7 +131,7 @@ def _prep_server_detail(compute_client, server): if image_info: image_id = image_info.get('id', '') try: - image = utils.find_resource(compute_client.images, image_id) + image = utils.find_resource(image_client.images, image_id) info['image'] = "%s (%s)" % (image.name, image_id) except Exception: info['image'] = image_id @@ -144,6 +145,20 @@ def _prep_server_detail(compute_client, server): except Exception: info['flavor'] = flavor_id + if 'os-extended-volumes:volumes_attached' in info: + info.update( + { + 'volumes_attached': utils.format_list_of_dicts( + info.pop('os-extended-volumes:volumes_attached')) + } + ) + if 'security_groups' in info: + info.update( + { + 'security_groups': utils.format_list_of_dicts( + info.pop('security_groups')) + } + ) # NOTE(dtroyer): novaclient splits these into separate entries... # Format addresses in a useful way info['addresses'] = _format_servers_list_networks(server.networks) @@ -175,7 +190,7 @@ def _show_progress(progress): class AddFixedIP(command.Command): - """Add fixed IP address to server""" + _description = _("Add fixed IP address to server") def get_parser(self, prog_name): parser = super(AddFixedIP, self).get_parser(prog_name) @@ -205,7 +220,7 @@ class AddFixedIP(command.Command): class AddFloatingIP(command.Command): - """Add floating IP address to server""" + _description = _("Add floating IP address to server") def get_parser(self, prog_name): parser = super(AddFloatingIP, self).get_parser(prog_name) @@ -220,6 +235,12 @@ class AddFloatingIP(command.Command): help=_("Floating IP address (IP address only) to assign " "to server"), ) + parser.add_argument( + "--fixed-ip-address", + metavar="<fixed-ip-address>", + help=_("Fixed IP address to associate with this floating IP " + "address"), + ) return parser def take_action(self, parsed_args): @@ -228,11 +249,12 @@ class AddFloatingIP(command.Command): server = utils.find_resource( compute_client.servers, parsed_args.server) - server.add_floating_ip(parsed_args.ip_address) + server.add_floating_ip(parsed_args.ip_address, + parsed_args.fixed_ip_address) class AddServerSecurityGroup(command.Command): - """Add security group to server""" + _description = _("Add security group to server") def get_parser(self, prog_name): parser = super(AddServerSecurityGroup, self).get_parser(prog_name) @@ -264,7 +286,7 @@ class AddServerSecurityGroup(command.Command): class AddServerVolume(command.Command): - """Add volume to server""" + _description = _("Add volume to server") def get_parser(self, prog_name): parser = super(AddServerVolume, self).get_parser(prog_name) @@ -306,7 +328,7 @@ class AddServerVolume(command.Command): class CreateServer(command.ShowOne): - """Create a new server""" + _description = _("Create a new server") def get_parser(self, prog_name): parser = super(CreateServer, self).get_parser(prog_name) @@ -321,12 +343,15 @@ class CreateServer(command.ShowOne): disk_group.add_argument( '--image', metavar='<image>', - help=_('Create server from this image (name or ID)'), + help=_('Create server boot disk from this image (name or ID)'), ) disk_group.add_argument( '--volume', metavar='<volume>', - help=_('Create server from this volume (name or ID)'), + help=_( + 'Create server using this volume as the boot disk ' + '(name or ID)' + ), ) parser.add_argument( '--flavor', @@ -384,7 +409,7 @@ class CreateServer(command.ShowOne): parser.add_argument( '--nic', metavar="<net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr," - "port-id=port-uuid>", + "port-id=port-uuid,auto,none>", action='append', default=[], help=_("Create a NIC on the server. " @@ -393,7 +418,11 @@ class CreateServer(command.ShowOne): "net-id: attach NIC to network with this UUID, " "port-id: attach NIC to port with this UUID, " "v4-fixed-ip: IPv4 fixed address for NIC (optional), " - "v6-fixed-ip: IPv6 fixed address for NIC (optional)."), + "v6-fixed-ip: IPv6 fixed address for NIC (optional), " + "none: (v2.37+) no network is attached, " + "auto: (v2.37+) the compute service will automatically " + "allocate a network. Specifying a --nic of auto or none " + "cannot be used with any other --nic value."), ) parser.add_argument( '--hint', @@ -433,12 +462,13 @@ class CreateServer(command.ShowOne): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume + image_client = self.app.client_manager.image # Lookup parsed_args.image image = None if parsed_args.image: image = utils.find_resource( - compute_client.images, + image_client.images, parsed_args.image, ) @@ -489,60 +519,94 @@ class CreateServer(command.ShowOne): "exception": e} ) - block_device_mapping = {} + block_device_mapping_v2 = [] if volume: - # When booting from volume, for now assume no other mappings - # This device value is likely KVM-specific - block_device_mapping = {'vda': volume} - else: - for dev_map in parsed_args.block_device_mapping: - dev_key, dev_vol = dev_map.split('=', 1) - block_volume = None - if dev_vol: - vol = dev_vol.split(':', 1)[0] - if vol: - vol_id = utils.find_resource( + block_device_mapping_v2 = [{'uuid': volume, + 'boot_index': '0', + 'source_type': 'volume', + 'destination_type': 'volume' + }] + for dev_map in parsed_args.block_device_mapping: + dev_name, dev_map = dev_map.split('=', 1) + if dev_map: + dev_map = dev_map.split(':') + if len(dev_map) > 0: + mapping = { + 'device_name': dev_name, + 'uuid': utils.find_resource( volume_client.volumes, - vol, - ).id - block_volume = dev_vol.replace(vol, vol_id) + dev_map[0], + ).id} + # Block device mapping v1 compatibility + if len(dev_map) > 1 and \ + dev_map[1] in ('volume', 'snapshot'): + mapping['source_type'] = dev_map[1] 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}) + mapping['source_type'] = 'volume' + mapping['destination_type'] = 'volume' + if len(dev_map) > 2: + mapping['volume_size'] = dev_map[2] + if len(dev_map) > 3: + mapping['delete_on_termination'] = dev_map[3] + else: + msg = _("Volume name or ID must be specified if " + "--block-device-mapping is specified") + raise exceptions.CommandError(msg) + block_device_mapping_v2.append(mapping) nics = [] + auto_or_none = False for nic_str in parsed_args.nic: - nic_info = {"net-id": "", "v4-fixed-ip": "", - "v6-fixed-ip": "", "port-id": ""} - nic_info.update(dict(kv_str.split("=", 1) - for kv_str in nic_str.split(","))) - if bool(nic_info["net-id"]) == bool(nic_info["port-id"]): - msg = _("either net-id or port-id should be specified " - "but not both") - raise exceptions.CommandError(msg) - if self.app.client_manager.is_network_endpoint_enabled(): - network_client = self.app.client_manager.network - if nic_info["net-id"]: - net = network_client.find_network( - nic_info["net-id"], ignore_missing=False) - nic_info["net-id"] = net.id - if nic_info["port-id"]: - port = network_client.find_port( - nic_info["port-id"], ignore_missing=False) - nic_info["port-id"] = port.id + # Handle the special auto/none cases + if nic_str in ('auto', 'none'): + auto_or_none = True + nics.append(nic_str) else: - if nic_info["net-id"]: - nic_info["net-id"] = utils.find_resource( - compute_client.networks, - nic_info["net-id"] - ).id - if nic_info["port-id"]: - msg = _("can't create server with port specified " - "since network endpoint not enabled") + nic_info = {"net-id": "", "v4-fixed-ip": "", + "v6-fixed-ip": "", "port-id": ""} + try: + nic_info.update(dict(kv_str.split("=", 1) + for kv_str in nic_str.split(","))) + except ValueError: + msg = _('Invalid --nic argument %s.') % nic_str raise exceptions.CommandError(msg) - nics.append(nic_info) + if bool(nic_info["net-id"]) == bool(nic_info["port-id"]): + msg = _("either net-id or port-id should be specified " + "but not both") + raise exceptions.CommandError(msg) + if self.app.client_manager.is_network_endpoint_enabled(): + network_client = self.app.client_manager.network + if nic_info["net-id"]: + net = network_client.find_network( + nic_info["net-id"], ignore_missing=False) + nic_info["net-id"] = net.id + if nic_info["port-id"]: + port = network_client.find_port( + nic_info["port-id"], ignore_missing=False) + nic_info["port-id"] = port.id + else: + if nic_info["net-id"]: + nic_info["net-id"] = utils.find_resource( + compute_client.networks, + nic_info["net-id"] + ).id + if nic_info["port-id"]: + msg = _("can't create server with port specified " + "since network endpoint not enabled") + raise exceptions.CommandError(msg) + nics.append(nic_info) + + if nics: + if auto_or_none: + if len(nics) > 1: + msg = _('Specifying a --nic of auto or none cannot ' + 'be used with any other --nic value.') + raise exceptions.CommandError(msg) + nics = nics[0] + else: + # Default to empty list if nothing was specified, let nova side to + # decide the default behavior. + nics = [] hints = {} for hint in parsed_args.hint: @@ -577,7 +641,7 @@ class CreateServer(command.ShowOne): userdata=userdata, key_name=parsed_args.key_name, availability_zone=parsed_args.availability_zone, - block_device_mapping=block_device_mapping, + block_device_mapping_v2=block_device_mapping_v2, nics=nics, scheduler_hints=hints, config_drive=config_drive) @@ -609,7 +673,7 @@ class CreateServer(command.ShowOne): sys.stdout.write(_('Error creating server\n')) raise SystemExit - details = _prep_server_detail(compute_client, server) + details = _prep_server_detail(compute_client, image_client, server) return zip(*sorted(six.iteritems(details))) @@ -642,7 +706,7 @@ class CreateServerDump(command.Command): class DeleteServer(command.Command): - """Delete server(s)""" + _description = _("Delete server(s)") def get_parser(self, prog_name): parser = super(DeleteServer, self).get_parser(prog_name) @@ -680,7 +744,7 @@ class DeleteServer(command.Command): class ListServer(command.Lister): - """List servers""" + _description = _("List servers") def get_parser(self, prog_name): parser = super(ListServer, self).get_parser(prog_name) @@ -772,11 +836,26 @@ class ListServer(command.Lister): " 'osapi_max_limit' option of Nova API," " 'osapi_max_limit' will be used instead."), ) + parser.add_argument( + '--deleted', + action="store_true", + default=False, + help=_('Only display deleted servers (Admin only).') + ) + parser.add_argument( + '--changes-since', + metavar='<changes-since>', + default=None, + help=_("List only servers changed after a certain point of time." + " The provided time should be an ISO 8061 formatted time." + " ex 2016-03-04T06:27:59Z .") + ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute identity_client = self.app.client_manager.identity + image_client = self.app.client_manager.image project_id = None if parsed_args.project: @@ -806,7 +885,7 @@ class ListServer(command.Lister): # image name is given, map it to ID. image_id = None if parsed_args.image: - image_id = utils.find_resource(compute_client.images, + image_id = utils.find_resource(image_client.images, parsed_args.image).id search_opts = { @@ -822,9 +901,19 @@ class ListServer(command.Lister): 'tenant_id': project_id, 'all_tenants': parsed_args.all_projects, 'user_id': user_id, + 'deleted': parsed_args.deleted, + 'changes_since': parsed_args.changes_since, } LOG.debug('search options: %s', search_opts) + if search_opts['changes_since']: + try: + timeutils.parse_isotime(search_opts['changes_since']) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value:' + ' %s') % search_opts['changes' + '_since']) + if parsed_args.long: columns = ( 'ID', @@ -835,6 +924,8 @@ class ListServer(command.Lister): 'Networks', 'Image Name', 'Image ID', + 'Flavor Name', + 'Flavor ID', 'OS-EXT-AZ:availability_zone', 'OS-EXT-SRV-ATTR:host', 'Metadata', @@ -848,6 +939,8 @@ class ListServer(command.Lister): 'Networks', 'Image Name', 'Image ID', + 'Flavor Name', + 'Flavor ID', 'Availability Zone', 'Host', 'Properties', @@ -895,8 +988,19 @@ class ListServer(command.Lister): except Exception: pass - # Populate image_name and image_id attributes of server objects - # so that we can display "Image Name" and "Image ID" columns. + flavors = {} + # Create a dict that maps flavor_id to flavor object. + # Needed so that we can display the "Flavor Name" column. + # "Flavor Name" is not crucial, so we swallow any exceptions. + try: + flavors_list = compute_client.flavors.list() + for i in flavors_list: + flavors[i.id] = i + except Exception: + pass + + # Populate image_name, image_id, flavor_name and flavor_id attributes + # of server objects so that we can display those columns. for s in data: if 'id' in s.image: image = images.get(s.image['id']) @@ -906,6 +1010,14 @@ class ListServer(command.Lister): else: s.image_name = '' s.image_id = '' + if 'id' in s.flavor: + flavor = flavors.get(s.flavor['id']) + if flavor: + s.flavor_name = flavor.name + s.flavor_id = s.flavor['id'] + else: + s.flavor_name = '' + s.flavor_id = '' table = (column_headers, (utils.get_item_properties( @@ -923,7 +1035,8 @@ class ListServer(command.Lister): class LockServer(command.Command): - """Lock server(s). A non-admin user will not be able to execute actions""" + _description = _("Lock server(s). A non-admin user will not be able to " + "execute actions") def get_parser(self, prog_name): parser = super(LockServer, self).get_parser(prog_name) @@ -956,7 +1069,7 @@ class LockServer(command.Command): # then adding the groups doesn't seem to work class MigrateServer(command.Command): - """Migrate server to different host""" + _description = _("Migrate server to different host") def get_parser(self, prog_name): parser = super(MigrateServer, self).get_parser(prog_name) @@ -973,15 +1086,15 @@ class MigrateServer(command.Command): migration_group = parser.add_mutually_exclusive_group() migration_group.add_argument( '--shared-migration', - dest='shared_migration', - action='store_true', - default=True, + dest='block_migration', + action='store_false', + default=False, help=_('Perform a shared live migration (default)'), ) migration_group.add_argument( '--block-migration', - dest='shared_migration', - action='store_false', + dest='block_migration', + action='store_true', help=_('Perform a block live migration'), ) disk_group = parser.add_mutually_exclusive_group() @@ -1002,7 +1115,7 @@ class MigrateServer(command.Command): parser.add_argument( '--wait', action='store_true', - help=_('Wait for resize to complete'), + help=_('Wait for migrate to complete'), ) return parser @@ -1016,11 +1129,15 @@ class MigrateServer(command.Command): ) if parsed_args.live: server.live_migrate( - parsed_args.live, - parsed_args.shared_migration, - parsed_args.disk_overcommit, + host=parsed_args.live, + block_migration=parsed_args.block_migration, + disk_over_commit=parsed_args.disk_overcommit, ) else: + if parsed_args.block_migration or parsed_args.disk_overcommit: + raise exceptions.CommandError("--live must be specified if " + "--block-migration or " + "--disk-overcommit is specified") server.migrate() if parsed_args.wait: @@ -1038,7 +1155,7 @@ class MigrateServer(command.Command): class PauseServer(command.Command): - """Pause server(s)""" + _description = _("Pause server(s)") def get_parser(self, prog_name): parser = super(PauseServer, self).get_parser(prog_name) @@ -1060,7 +1177,7 @@ class PauseServer(command.Command): class RebootServer(command.Command): - """Perform a hard or soft server reboot""" + _description = _("Perform a hard or soft server reboot") def get_parser(self, prog_name): parser = super(RebootServer, self).get_parser(prog_name) @@ -1114,7 +1231,7 @@ class RebootServer(command.Command): class RebuildServer(command.ShowOne): - """Rebuild server""" + _description = _("Rebuild server") def get_parser(self, prog_name): parser = super(RebuildServer, self).get_parser(prog_name) @@ -1143,13 +1260,14 @@ class RebuildServer(command.ShowOne): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + image_client = self.app.client_manager.image server = utils.find_resource( compute_client.servers, parsed_args.server) # If parsed_args.image is not set, default to the currently used one. image_id = parsed_args.image or server._info.get('image', {}).get('id') - image = utils.find_resource(compute_client.images, image_id) + image = utils.find_resource(image_client.images, image_id) server = server.rebuild(image, parsed_args.password) if parsed_args.wait: @@ -1165,12 +1283,12 @@ class RebuildServer(command.ShowOne): sys.stdout.write(_('Error rebuilding server\n')) raise SystemExit - details = _prep_server_detail(compute_client, server) + details = _prep_server_detail(compute_client, image_client, server) return zip(*sorted(six.iteritems(details))) class RemoveFixedIP(command.Command): - """Remove fixed IP address from server""" + _description = _("Remove fixed IP address from server") def get_parser(self, prog_name): parser = super(RemoveFixedIP, self).get_parser(prog_name) @@ -1197,7 +1315,7 @@ class RemoveFixedIP(command.Command): class RemoveFloatingIP(command.Command): - """Remove floating IP address from server""" + _description = _("Remove floating IP address from server") def get_parser(self, prog_name): parser = super(RemoveFloatingIP, self).get_parser(prog_name) @@ -1225,7 +1343,7 @@ class RemoveFloatingIP(command.Command): class RemoveServerSecurityGroup(command.Command): - """Remove security group from server""" + _description = _("Remove security group from server") def get_parser(self, prog_name): parser = super(RemoveServerSecurityGroup, self).get_parser(prog_name) @@ -1257,7 +1375,7 @@ class RemoveServerSecurityGroup(command.Command): class RemoveServerVolume(command.Command): - """Remove volume from server""" + _description = _("Remove volume from server") def get_parser(self, prog_name): parser = super(RemoveServerVolume, self).get_parser(prog_name) @@ -1293,7 +1411,7 @@ class RemoveServerVolume(command.Command): class RescueServer(command.ShowOne): - """Put server in rescue mode""" + _description = _("Put server in rescue mode") def get_parser(self, prog_name): parser = super(RescueServer, self).get_parser(prog_name) @@ -1315,7 +1433,7 @@ class RescueServer(command.ShowOne): class ResizeServer(command.Command): - """Scale server to a new flavor""" + _description = _("Scale server to a new flavor") def get_parser(self, prog_name): parser = super(ResizeServer, self).get_parser(prog_name) @@ -1380,7 +1498,7 @@ class ResizeServer(command.Command): class RestoreServer(command.Command): - """Restore server(s)""" + _description = _("Restore server(s)") def get_parser(self, prog_name): parser = super(RestoreServer, self).get_parser(prog_name) @@ -1402,7 +1520,7 @@ class RestoreServer(command.Command): class ResumeServer(command.Command): - """Resume server(s)""" + _description = _("Resume server(s)") def get_parser(self, prog_name): parser = super(ResumeServer, self).get_parser(prog_name) @@ -1425,7 +1543,7 @@ class ResumeServer(command.Command): class SetServer(command.Command): - """Set server properties""" + _description = _("Set server properties") def get_parser(self, prog_name): parser = super(SetServer, self).get_parser(prog_name) @@ -1490,7 +1608,7 @@ class SetServer(command.Command): class ShelveServer(command.Command): - """Shelve server(s)""" + _description = _("Shelve server(s)") def get_parser(self, prog_name): parser = super(ShelveServer, self).get_parser(prog_name) @@ -1512,7 +1630,7 @@ class ShelveServer(command.Command): class ShowServer(command.ShowOne): - """Show server details""" + _description = _("Show server details") def get_parser(self, prog_name): parser = super(ShowServer, self).get_parser(prog_name) @@ -1540,13 +1658,14 @@ class ShowServer(command.ShowOne): sys.stderr.write(_("Error retrieving diagnostics data\n")) return ({}, {}) else: - data = _prep_server_detail(compute_client, server) + data = _prep_server_detail(compute_client, + self.app.client_manager.image, server) return zip(*sorted(six.iteritems(data))) class SshServer(command.Command): - """SSH to server""" + _description = _("SSH to server") def get_parser(self, prog_name): parser = super(SshServer, self).get_parser(prog_name) @@ -1690,7 +1809,7 @@ class SshServer(command.Command): class StartServer(command.Command): - """Start server(s).""" + _description = _("Start server(s).") def get_parser(self, prog_name): parser = super(StartServer, self).get_parser(prog_name) @@ -1712,7 +1831,7 @@ class StartServer(command.Command): class StopServer(command.Command): - """Stop server(s).""" + _description = _("Stop server(s).") def get_parser(self, prog_name): parser = super(StopServer, self).get_parser(prog_name) @@ -1734,7 +1853,7 @@ class StopServer(command.Command): class SuspendServer(command.Command): - """Suspend server(s)""" + _description = _("Suspend server(s)") def get_parser(self, prog_name): parser = super(SuspendServer, self).get_parser(prog_name) @@ -1757,7 +1876,7 @@ class SuspendServer(command.Command): class UnlockServer(command.Command): - """Unlock server(s)""" + _description = _("Unlock server(s)") def get_parser(self, prog_name): parser = super(UnlockServer, self).get_parser(prog_name) @@ -1780,7 +1899,7 @@ class UnlockServer(command.Command): class UnpauseServer(command.Command): - """Unpause server(s)""" + _description = _("Unpause server(s)") def get_parser(self, prog_name): parser = super(UnpauseServer, self).get_parser(prog_name) @@ -1803,7 +1922,7 @@ class UnpauseServer(command.Command): class UnrescueServer(command.Command): - """Restore server from rescue mode""" + _description = _("Restore server from rescue mode") def get_parser(self, prog_name): parser = super(UnrescueServer, self).get_parser(prog_name) @@ -1824,7 +1943,7 @@ class UnrescueServer(command.Command): class UnsetServer(command.Command): - """Unset server properties""" + _description = _("Unset server properties") def get_parser(self, prog_name): parser = super(UnsetServer, self).get_parser(prog_name) @@ -1858,7 +1977,7 @@ class UnsetServer(command.Command): class UnshelveServer(command.Command): - """Unshelve server(s)""" + _description = _("Unshelve server(s)") def get_parser(self, prog_name): parser = super(UnshelveServer, self).get_parser(prog_name) diff --git a/openstackclient/compute/v2/server_backup.py b/openstackclient/compute/v2/server_backup.py index c0e2e5ee..ddcf9101 100644 --- a/openstackclient/compute/v2/server_backup.py +++ b/openstackclient/compute/v2/server_backup.py @@ -33,7 +33,7 @@ def _show_progress(progress): class CreateServerBackup(command.ShowOne): - """Create a server backup image""" + _description = _("Create a server backup image") IMAGE_API_VERSIONS = { "1": "openstackclient.image.v1.image", diff --git a/openstackclient/compute/v2/server_event.py b/openstackclient/compute/v2/server_event.py new file mode 100644 index 00000000..ccb19ef7 --- /dev/null +++ b/openstackclient/compute/v2/server_event.py @@ -0,0 +1,117 @@ +# Copyright 2017 Huawei, Inc. All rights reserved. +# +# 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. +# + +"""Compute v2 Server operation event implementations""" + +import logging +import six + +from osc_lib.command import command +from osc_lib import utils + +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class ListServerEvent(command.Lister): + _description = _("List recent events of a server") + + def get_parser(self, prog_name): + parser = super(ListServerEvent, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='<server>', + help=_('Server to list events (name or ID)'), + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + server_id = utils.find_resource(compute_client.servers, + parsed_args.server).id + data = compute_client.instance_action.list(server_id) + + if parsed_args.long: + columns = ( + 'request_id', + 'instance_uuid', + 'action', + 'start_time', + 'message', + 'project_id', + 'user_id', + ) + column_headers = ( + 'Request ID', + 'Server ID', + 'Action', + 'Start Time', + 'Message', + 'Project ID', + 'User ID', + ) + else: + columns = ( + 'request_id', + 'instance_uuid', + 'action', + 'start_time', + ) + column_headers = ( + 'Request ID', + 'Server ID', + 'Action', + 'Start Time', + ) + + return (column_headers, + (utils.get_item_properties( + s, columns, + ) for s in data)) + + +class ShowServerEvent(command.ShowOne): + _description = _("Show server event details") + + def get_parser(self, prog_name): + parser = super(ShowServerEvent, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='<server>', + help=_('Server to show event details (name or ID)'), + ) + parser.add_argument( + 'request_id', + metavar='<request-id>', + help=_('Request ID of the event to show (ID only)'), + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + server_id = utils.find_resource(compute_client.servers, + parsed_args.server).id + action_detail = compute_client.instance_action.get( + server_id, parsed_args.request_id) + + return zip(*sorted(six.iteritems(action_detail._info))) diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index d51b1ec2..c6e2161f 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -43,7 +43,7 @@ def _get_columns(info): class CreateServerGroup(command.ShowOne): - """Create a new server group.""" + _description = _("Create a new server group.") def get_parser(self, prog_name): parser = super(CreateServerGroup, self).get_parser(prog_name) @@ -55,10 +55,11 @@ class CreateServerGroup(command.ShowOne): parser.add_argument( '--policy', metavar='<policy>', - action='append', - required=True, + choices=['affinity', 'anti-affinity'], + default='affinity', help=_("Add a policy to <name> " - "(repeat option to add multiple policies)") + "('affinity' or 'anti-affinity', " + "default to 'affinity')") ) return parser @@ -67,7 +68,7 @@ class CreateServerGroup(command.ShowOne): info = {} server_group = compute_client.server_groups.create( name=parsed_args.name, - policies=parsed_args.policy) + policies=[parsed_args.policy]) info.update(server_group._info) columns = _get_columns(info) @@ -77,7 +78,7 @@ class CreateServerGroup(command.ShowOne): class DeleteServerGroup(command.Command): - """Delete existing server group(s).""" + _description = _("Delete existing server group(s).") def get_parser(self, prog_name): parser = super(DeleteServerGroup, self).get_parser(prog_name) @@ -112,7 +113,7 @@ class DeleteServerGroup(command.Command): class ListServerGroup(command.Lister): - """List all server groups.""" + _description = _("List all server groups.") def get_parser(self, prog_name): parser = super(ListServerGroup, self).get_parser(prog_name) @@ -161,7 +162,7 @@ class ListServerGroup(command.Lister): class ShowServerGroup(command.ShowOne): - """Display server group details.""" + _description = _("Display server group details.") def get_parser(self, prog_name): parser = super(ShowServerGroup, self).get_parser(prog_name) diff --git a/openstackclient/compute/v2/server_image.py b/openstackclient/compute/v2/server_image.py index 285c7fd2..c66e0674 100644 --- a/openstackclient/compute/v2/server_image.py +++ b/openstackclient/compute/v2/server_image.py @@ -37,7 +37,7 @@ def _show_progress(progress): class CreateServerImage(command.ShowOne): - """Create a new server disk image from an existing server""" + _description = _("Create a new server disk image from an existing server") IMAGE_API_VERSIONS = { "1": "openstackclient.image.v1.image", diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 53624f55..7b18c2e5 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__) class DeleteService(command.Command): - """Delete compute service(s)""" + _description = _("Delete compute service(s)") def get_parser(self, prog_name): parser = super(DeleteService, self).get_parser(prog_name) @@ -50,8 +50,7 @@ class DeleteService(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete compute service with " - "ID '%(service)s': %(e)s") - % {'service': s, 'e': e}) + "ID '%(service)s': %(e)s"), {'service': s, 'e': e}) if result > 0: total = len(parsed_args.service) @@ -61,7 +60,7 @@ class DeleteService(command.Command): class ListService(command.Lister): - """List compute services""" + _description = _("List compute services") def get_parser(self, prog_name): parser = super(ListService, self).get_parser(prog_name) @@ -115,7 +114,7 @@ class ListService(command.Lister): class SetService(command.Command): - """Set compute service properties""" + _description = _("Set compute service properties") def get_parser(self, prog_name): parser = super(SetService, self).get_parser(prog_name) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 2f35b01b..3edcffe4 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -26,7 +26,7 @@ from openstackclient.i18n import _ class ListUsage(command.Lister): - """List resource usage per project""" + _description = _("List resource usage per project") def get_parser(self, prog_name): parser = super(ListUsage, self).get_parser(prog_name) @@ -89,7 +89,7 @@ class ListUsage(command.Lister): # Cache the project list project_cache = {} try: - for p in self.app.client_manager.identity.tenants.list(): + for p in self.app.client_manager.identity.projects.list(): project_cache[p.id] = p except Exception: # Just forget it if there's any trouble @@ -115,7 +115,7 @@ class ListUsage(command.Lister): class ShowUsage(command.ShowOne): - """Show resource usage for a single project""" + _description = _("Show resource usage for a single project") def get_parser(self, prog_name): parser = super(ShowUsage, self).get_parser(prog_name) diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 7a15cf3a..993bdd53 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -43,7 +43,7 @@ def _format_endpoints(eps=None): class ListCatalog(command.Lister): - """List services in the service catalog""" + _description = _("List services in the service catalog") def take_action(self, parsed_args): @@ -66,7 +66,7 @@ class ListCatalog(command.Lister): class ShowCatalog(command.ShowOne): - """Display service catalog details""" + _description = _("Display service catalog details") def get_parser(self, prog_name): parser = super(ShowCatalog, self).get_parser(prog_name) diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index 4873cd55..0bc48322 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__) class CreateEC2Creds(command.ShowOne): - """Create EC2 credentials""" + _description = _("Create EC2 credentials") def get_parser(self, prog_name): parser = super(CreateEC2Creds, self).get_parser(prog_name) @@ -86,7 +86,7 @@ class CreateEC2Creds(command.ShowOne): class DeleteEC2Creds(command.Command): - """Delete EC2 credentials""" + _description = _("Delete EC2 credentials") def get_parser(self, prog_name): parser = super(DeleteEC2Creds, self).get_parser(prog_name) @@ -122,8 +122,8 @@ class DeleteEC2Creds(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete EC2 credentials with " - "access key '%(access_key)s': %(e)s") - % {'access_key': access_key, 'e': e}) + "access key '%(access_key)s': %(e)s"), + {'access_key': access_key, 'e': e}) if result > 0: total = len(parsed_args.access_keys) @@ -133,7 +133,7 @@ class DeleteEC2Creds(command.Command): class ListEC2Creds(command.Lister): - """List EC2 credentials""" + _description = _("List EC2 credentials") def get_parser(self, prog_name): parser = super(ListEC2Creds, self).get_parser(prog_name) @@ -168,7 +168,7 @@ class ListEC2Creds(command.Lister): class ShowEC2Creds(command.ShowOne): - """Display EC2 credentials details""" + _description = _("Display EC2 credentials details") def get_parser(self, prog_name): parser = super(ShowEC2Creds, self).get_parser(prog_name) diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index 5a3b3186..1628e488 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__) class CreateEndpoint(command.ShowOne): - """Create new endpoint""" + _description = _("Create new endpoint") def get_parser(self, prog_name): parser = super(CreateEndpoint, self).get_parser(prog_name) @@ -80,7 +80,7 @@ class CreateEndpoint(command.ShowOne): class DeleteEndpoint(command.Command): - """Delete endpoint(s)""" + _description = _("Delete endpoint(s)") def get_parser(self, prog_name): parser = super(DeleteEndpoint, self).get_parser(prog_name) @@ -102,8 +102,8 @@ class DeleteEndpoint(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete endpoint with " - "ID '%(endpoint)s': %(e)s") - % {'endpoint': endpoint, 'e': e}) + "ID '%(endpoint)s': %(e)s"), + {'endpoint': endpoint, 'e': e}) if result > 0: total = len(parsed_args.endpoints) @@ -113,7 +113,7 @@ class DeleteEndpoint(command.Command): class ListEndpoint(command.Lister): - """List endpoints""" + _description = _("List endpoints") def get_parser(self, prog_name): parser = super(ListEndpoint, self).get_parser(prog_name) @@ -146,7 +146,7 @@ class ListEndpoint(command.Lister): class ShowEndpoint(command.ShowOne): - """Display endpoint details""" + _description = _("Display endpoint details") def get_parser(self, prog_name): parser = super(ShowEndpoint, self).get_parser(prog_name) diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index fc5c9201..04d422ec 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -20,6 +20,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.cli import parseractions from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -30,7 +31,7 @@ LOG = logging.getLogger(__name__) class CreateProject(command.ShowOne): - """Create new project""" + _description = _("Create new project") def get_parser(self, prog_name): parser = super(CreateProject, self).get_parser(prog_name) @@ -102,7 +103,7 @@ class CreateProject(command.ShowOne): class DeleteProject(command.Command): - """Delete project(s)""" + _description = _("Delete project(s)") def get_parser(self, prog_name): parser = super(DeleteProject, self).get_parser(prog_name) @@ -117,16 +118,29 @@ class DeleteProject(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for project in parsed_args.projects: - project_obj = utils.find_resource( - identity_client.tenants, - project, - ) - identity_client.tenants.delete(project_obj.id) + try: + project_obj = utils.find_resource( + identity_client.tenants, + project, + ) + identity_client.tenants.delete(project_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete project with " + "name or ID '%(project)s': %(e)s"), + {'project': project, 'e': e}) + + if errors > 0: + total = len(parsed_args.projects) + msg = (_("%(errors)s of %(total)s projects failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListProject(command.Lister): - """List projects""" + _description = _("List projects") def get_parser(self, prog_name): parser = super(ListProject, self).get_parser(prog_name) @@ -136,6 +150,13 @@ class ListProject(command.Lister): default=False, help=_('List additional fields in output'), ) + parser.add_argument( + '--sort', + metavar='<key>[:<direction>]', + help=_('Sort output by selected keys and directions (asc or desc) ' + '(default: asc), repeat this option to specify multiple ' + 'keys and directions.'), + ) return parser def take_action(self, parsed_args): @@ -144,6 +165,8 @@ class ListProject(command.Lister): else: columns = ('ID', 'Name') data = self.app.client_manager.identity.tenants.list() + if parsed_args.sort: + data = utils.sort_items(data, parsed_args.sort) return (columns, (utils.get_item_properties( s, columns, @@ -152,7 +175,7 @@ class ListProject(command.Lister): class SetProject(command.Command): - """Set project properties""" + _description = _("Set project properties") def get_parser(self, prog_name): parser = super(SetProject, self).get_parser(prog_name) @@ -221,7 +244,7 @@ class SetProject(command.Command): class ShowProject(command.ShowOne): - """Display project details""" + _description = _("Display project details") def get_parser(self, prog_name): parser = super(ShowProject, self).get_parser(prog_name) @@ -279,7 +302,7 @@ class ShowProject(command.ShowOne): class UnsetProject(command.Command): - """Unset project properties""" + _description = _("Unset project properties") def get_parser(self, prog_name): parser = super(UnsetProject, self).get_parser(prog_name) diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index b4b67bad..e254e05f 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__) class AddRole(command.ShowOne): - """Add role to project:user""" + _description = _("Add role to project:user") def get_parser(self, prog_name): parser = super(AddRole, self).get_parser(prog_name) @@ -73,7 +73,7 @@ class AddRole(command.ShowOne): class CreateRole(command.ShowOne): - """Create new role""" + _description = _("Create new role") def get_parser(self, prog_name): parser = super(CreateRole, self).get_parser(prog_name) @@ -109,7 +109,7 @@ class CreateRole(command.ShowOne): class DeleteRole(command.Command): - """Delete role(s)""" + _description = _("Delete role(s)") def get_parser(self, prog_name): parser = super(DeleteRole, self).get_parser(prog_name) @@ -124,16 +124,29 @@ class DeleteRole(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for role in parsed_args.roles: - role_obj = utils.find_resource( - identity_client.roles, - role, - ) - identity_client.roles.delete(role_obj.id) + try: + role_obj = utils.find_resource( + identity_client.roles, + role, + ) + identity_client.roles.delete(role_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete role with " + "name or ID '%(role)s': %(e)s"), + {'role': role, 'e': e}) + + if errors > 0: + total = len(parsed_args.roles) + msg = (_("%(errors)s of %(total)s roles failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListRole(command.Lister): - """List roles""" + _description = _("List roles") def get_parser(self, prog_name): parser = super(ListRole, self).get_parser(prog_name) @@ -223,7 +236,7 @@ class ListRole(command.Lister): class ListUserRole(command.Lister): - """List user-role assignments""" + _description = _("List user-role assignments") def get_parser(self, prog_name): parser = super(ListUserRole, self).get_parser(prog_name) @@ -293,7 +306,7 @@ class ListUserRole(command.Lister): class RemoveRole(command.Command): - """Remove role from project : user""" + _description = _("Remove role from project : user") def get_parser(self, prog_name): parser = super(RemoveRole, self).get_parser(prog_name) @@ -331,7 +344,7 @@ class RemoveRole(command.Command): class ShowRole(command.ShowOne): - """Display role details""" + _description = _("Display role details") def get_parser(self, prog_name): parser = super(ShowRole, self).get_parser(prog_name) diff --git a/openstackclient/identity/v2_0/role_assignment.py b/openstackclient/identity/v2_0/role_assignment.py index 44f55c3b..8236bbfc 100644 --- a/openstackclient/identity/v2_0/role_assignment.py +++ b/openstackclient/identity/v2_0/role_assignment.py @@ -21,7 +21,7 @@ from openstackclient.i18n import _ # noqa class ListRoleAssignment(command.Lister): - """List role assignments""" + _description = _("List role assignments") def get_parser(self, prog_name): parser = super(ListRoleAssignment, self).get_parser(prog_name) diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index e318643c..80f2d72a 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__) class CreateService(command.ShowOne): - """Create new service""" + _description = _("Create new service") def get_parser(self, prog_name): parser = super(CreateService, self).get_parser(prog_name) @@ -91,7 +91,7 @@ class CreateService(command.ShowOne): class DeleteService(command.Command): - """Delete service(s)""" + _description = _("Delete service(s)") def get_parser(self, prog_name): parser = super(DeleteService, self).get_parser(prog_name) @@ -114,8 +114,8 @@ class DeleteService(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete service with " - "name or ID '%(service)s': %(e)s") - % {'service': service, 'e': e}) + "name or ID '%(service)s': %(e)s"), + {'service': service, 'e': e}) if result > 0: total = len(parsed_args.services) @@ -125,7 +125,7 @@ class DeleteService(command.Command): class ListService(command.Lister): - """List services""" + _description = _("List services") def get_parser(self, prog_name): parser = super(ListService, self).get_parser(prog_name) @@ -151,7 +151,7 @@ class ListService(command.Lister): class ShowService(command.ShowOne): - """Display service details""" + _description = _("Display service details") def get_parser(self, prog_name): parser = super(ShowService, self).get_parser(prog_name) diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index 4b7e988b..3b08b475 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -23,7 +23,7 @@ from openstackclient.i18n import _ class IssueToken(command.ShowOne): - """Issue new token""" + _description = _("Issue new token") # scoped token is optional required_scope = False @@ -53,7 +53,7 @@ class IssueToken(command.ShowOne): class RevokeToken(command.Command): - """Revoke existing token""" + _description = _("Revoke existing token") def get_parser(self, prog_name): parser = super(RevokeToken, self).get_parser(prog_name) diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index d2075150..2a3dde6b 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -19,6 +19,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -29,7 +30,7 @@ LOG = logging.getLogger(__name__) class CreateUser(command.ShowOne): - """Create new user""" + _description = _("Create new user") def get_parser(self, prog_name): parser = super(CreateUser, self).get_parser(prog_name) @@ -94,6 +95,10 @@ class CreateUser(command.ShowOne): if parsed_args.password_prompt: parsed_args.password = utils.get_password(self.app.stdin) + if not parsed_args.password: + LOG.warning(_("No password was supplied, authentication will fail " + "when a user does not have a password.")) + try: user = identity_client.users.create( parsed_args.name, @@ -126,7 +131,7 @@ class CreateUser(command.ShowOne): class DeleteUser(command.Command): - """Delete user(s)""" + _description = _("Delete user(s)") def get_parser(self, prog_name): parser = super(DeleteUser, self).get_parser(prog_name) @@ -141,16 +146,29 @@ class DeleteUser(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for user in parsed_args.users: - user_obj = utils.find_resource( - identity_client.users, - user, - ) - identity_client.users.delete(user_obj.id) + try: + user_obj = utils.find_resource( + identity_client.users, + user, + ) + identity_client.users.delete(user_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete user with " + "name or ID '%(user)s': %(e)s"), + {'user': user, 'e': e}) + + if errors > 0: + total = len(parsed_args.users) + msg = (_("%(errors)s of %(total)s users failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListUser(command.Lister): - """List users""" + _description = _("List users") def get_parser(self, prog_name): parser = super(ListUser, self).get_parser(prog_name) @@ -238,7 +256,7 @@ class ListUser(command.Lister): class SetUser(command.Command): - """Set user properties""" + _description = _("Set user properties") def get_parser(self, prog_name): parser = super(SetUser, self).get_parser(prog_name) @@ -292,6 +310,10 @@ class SetUser(command.Command): if parsed_args.password_prompt: parsed_args.password = utils.get_password(self.app.stdin) + if '' == parsed_args.password: + LOG.warning(_("No password was supplied, authentication will fail " + "when a user does not have a password.")) + user = utils.find_resource( identity_client.users, parsed_args.user, @@ -328,7 +350,7 @@ class SetUser(command.Command): class ShowUser(command.ShowOne): - """Display user details""" + _description = _("Display user details") def get_parser(self, prog_name): parser = super(ShowUser, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/catalog.py b/openstackclient/identity/v3/catalog.py index a62d0a93..28f4fada 100644 --- a/openstackclient/identity/v3/catalog.py +++ b/openstackclient/identity/v3/catalog.py @@ -38,7 +38,7 @@ def _format_endpoints(eps=None): class ListCatalog(command.Lister): - """List services in the service catalog""" + _description = _("List services in the service catalog") def take_action(self, parsed_args): @@ -61,7 +61,7 @@ class ListCatalog(command.Lister): class ShowCatalog(command.ShowOne): - """Display service catalog details""" + _description = _("Display service catalog details") def get_parser(self, prog_name): parser = super(ShowCatalog, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index b41a37ca..6dd24dcc 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__) class CreateConsumer(command.ShowOne): - """Create new consumer""" + _description = _("Create new consumer") def get_parser(self, prog_name): parser = super(CreateConsumer, self).get_parser(prog_name) @@ -50,7 +50,7 @@ class CreateConsumer(command.ShowOne): class DeleteConsumer(command.Command): - """Delete consumer(s)""" + _description = _("Delete consumer(s)") def get_parser(self, prog_name): parser = super(DeleteConsumer, self).get_parser(prog_name) @@ -73,8 +73,7 @@ class DeleteConsumer(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete consumer with name or " - "ID '%(consumer)s': %(e)s") - % {'consumer': i, 'e': e}) + "ID '%(consumer)s': %(e)s"), {'consumer': i, 'e': e}) if result > 0: total = len(parsed_args.consumer) @@ -84,7 +83,7 @@ class DeleteConsumer(command.Command): class ListConsumer(command.Lister): - """List consumers""" + _description = _("List consumers") def take_action(self, parsed_args): columns = ('ID', 'Description') @@ -97,7 +96,7 @@ class ListConsumer(command.Lister): class SetConsumer(command.Command): - """Set consumer properties""" + _description = _("Set consumer properties") def get_parser(self, prog_name): parser = super(SetConsumer, self).get_parser(prog_name) @@ -126,7 +125,7 @@ class SetConsumer(command.Command): class ShowConsumer(command.ShowOne): - """Display consumer details""" + _description = _("Display consumer details") def get_parser(self, prog_name): parser = super(ShowConsumer, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index 0ef94cf4..2fd00626 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__) class CreateCredential(command.ShowOne): - """Create new credential""" + _description = _("Create new credential") def get_parser(self, prog_name): parser = super(CreateCredential, self).get_parser(prog_name) @@ -78,7 +78,7 @@ class CreateCredential(command.ShowOne): class DeleteCredential(command.Command): - """Delete credential(s)""" + _description = _("Delete credential(s)") def get_parser(self, prog_name): parser = super(DeleteCredential, self).get_parser(prog_name) @@ -99,8 +99,8 @@ class DeleteCredential(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete credentials with " - "ID '%(credential)s': %(e)s") - % {'credential': i, 'e': e}) + "ID '%(credential)s': %(e)s"), + {'credential': i, 'e': e}) if result > 0: total = len(parsed_args.credential) @@ -110,7 +110,7 @@ class DeleteCredential(command.Command): class ListCredential(command.Lister): - """List credentials""" + _description = _("List credentials") def take_action(self, parsed_args): columns = ('ID', 'Type', 'User ID', 'Blob', 'Project ID') @@ -124,7 +124,7 @@ class ListCredential(command.Lister): class SetCredential(command.Command): - """Set credential properties""" + _description = _("Set credential properties") def get_parser(self, prog_name): parser = super(SetCredential, self).get_parser(prog_name) @@ -180,7 +180,7 @@ class SetCredential(command.Command): class ShowCredential(command.ShowOne): - """Display credential details""" + _description = _("Display credential details") def get_parser(self, prog_name): parser = super(ShowCredential, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 76e47d32..064624ab 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__) class CreateDomain(command.ShowOne): - """Create new domain""" + _description = _("Create new domain") def get_parser(self, prog_name): parser = super(CreateDomain, self).get_parser(prog_name) @@ -89,7 +89,7 @@ class CreateDomain(command.ShowOne): class DeleteDomain(command.Command): - """Delete domain(s)""" + _description = _("Delete domain(s)") def get_parser(self, prog_name): parser = super(DeleteDomain, self).get_parser(prog_name) @@ -111,8 +111,7 @@ class DeleteDomain(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete domain with name or " - "ID '%(domain)s': %(e)s") - % {'domain': i, 'e': e}) + "ID '%(domain)s': %(e)s"), {'domain': i, 'e': e}) if result > 0: total = len(parsed_args.domain) @@ -122,7 +121,7 @@ class DeleteDomain(command.Command): class ListDomain(command.Lister): - """List domains""" + _description = _("List domains") def take_action(self, parsed_args): columns = ('ID', 'Name', 'Enabled', 'Description') @@ -135,7 +134,7 @@ class ListDomain(command.Lister): class SetDomain(command.Command): - """Set domain properties""" + _description = _("Set domain properties") def get_parser(self, prog_name): parser = super(SetDomain, self).get_parser(prog_name) @@ -186,7 +185,7 @@ class SetDomain(command.Command): class ShowDomain(command.ShowOne): - """Display domain details""" + _description = _("Display domain details") def get_parser(self, prog_name): parser = super(ShowDomain, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/ec2creds.py b/openstackclient/identity/v3/ec2creds.py index 7ad01719..44e9a2c7 100644 --- a/openstackclient/identity/v3/ec2creds.py +++ b/openstackclient/identity/v3/ec2creds.py @@ -55,7 +55,7 @@ def _determine_ec2_user(parsed_args, client_manager): class CreateEC2Creds(command.ShowOne): - """Create EC2 credentials""" + _description = _("Create EC2 credentials") def get_parser(self, prog_name): parser = super(CreateEC2Creds, self).get_parser(prog_name) @@ -112,7 +112,7 @@ class CreateEC2Creds(command.ShowOne): class DeleteEC2Creds(command.Command): - """Delete EC2 credentials""" + _description = _("Delete EC2 credentials") def get_parser(self, prog_name): parser = super(DeleteEC2Creds, self).get_parser(prog_name) @@ -140,8 +140,8 @@ class DeleteEC2Creds(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete EC2 credentials with " - "access key '%(access_key)s': %(e)s") - % {'access_key': i, 'e': e}) + "access key '%(access_key)s': %(e)s"), + {'access_key': i, 'e': e}) if result > 0: total = len(parsed_args.access_key) @@ -151,7 +151,7 @@ class DeleteEC2Creds(command.Command): class ListEC2Creds(command.Lister): - """List EC2 credentials""" + _description = _("List EC2 credentials") def get_parser(self, prog_name): parser = super(ListEC2Creds, self).get_parser(prog_name) @@ -179,7 +179,7 @@ class ListEC2Creds(command.Lister): class ShowEC2Creds(command.ShowOne): - """Display EC2 credentials details""" + _description = _("Display EC2 credentials details") def get_parser(self, prog_name): parser = super(ShowEC2Creds, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 73b37a43..15760a17 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -37,7 +37,7 @@ def get_service_name(service): class CreateEndpoint(command.ShowOne): - """Create new endpoint""" + _description = _("Create new endpoint") def get_parser(self, prog_name): parser = super(CreateEndpoint, self).get_parser(prog_name) @@ -99,7 +99,7 @@ class CreateEndpoint(command.ShowOne): class DeleteEndpoint(command.Command): - """Delete endpoint(s)""" + _description = _("Delete endpoint(s)") def get_parser(self, prog_name): parser = super(DeleteEndpoint, self).get_parser(prog_name) @@ -122,8 +122,7 @@ class DeleteEndpoint(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete endpoint with " - "ID '%(endpoint)s': %(e)s") - % {'endpoint': i, 'e': e}) + "ID '%(endpoint)s': %(e)s"), {'endpoint': i, 'e': e}) if result > 0: total = len(parsed_args.endpoint) @@ -133,14 +132,14 @@ class DeleteEndpoint(command.Command): class ListEndpoint(command.Lister): - """List endpoints""" + _description = _("List endpoints") def get_parser(self, prog_name): parser = super(ListEndpoint, self).get_parser(prog_name) parser.add_argument( '--service', metavar='<service>', - help=_('Filter by service (name or ID)'), + help=_('Filter by service (type, name or ID)'), ) parser.add_argument( '--interface', @@ -181,7 +180,7 @@ class ListEndpoint(command.Lister): class SetEndpoint(command.Command): - """Set endpoint properties""" + _description = _("Set endpoint properties") def get_parser(self, prog_name): parser = super(SetEndpoint, self).get_parser(prog_name) @@ -252,7 +251,7 @@ class SetEndpoint(command.Command): class ShowEndpoint(command.ShowOne): - """Display endpoint details""" + _description = _("Display endpoint details") def get_parser(self, prog_name): parser = super(ShowEndpoint, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index 3fde9027..6429d934 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -28,7 +28,7 @@ LOG = logging.getLogger(__name__) class CreateProtocol(command.ShowOne): - """Create new federation protocol""" + _description = _("Create new federation protocol") def get_parser(self, prog_name): parser = super(CreateProtocol, self).get_parser(prog_name) @@ -72,7 +72,7 @@ class CreateProtocol(command.ShowOne): class DeleteProtocol(command.Command): - """Delete federation protocol(s)""" + _description = _("Delete federation protocol(s)") def get_parser(self, prog_name): parser = super(DeleteProtocol, self).get_parser(prog_name) @@ -102,8 +102,8 @@ class DeleteProtocol(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete federation protocol " - "with name or ID '%(protocol)s': %(e)s") - % {'protocol': i, 'e': e}) + "with name or ID '%(protocol)s': %(e)s"), + {'protocol': i, 'e': e}) if result > 0: total = len(parsed_args.federation_protocol) @@ -113,7 +113,7 @@ class DeleteProtocol(command.Command): class ListProtocols(command.Lister): - """List federation protocols""" + _description = _("List federation protocols") def get_parser(self, prog_name): parser = super(ListProtocols, self).get_parser(prog_name) @@ -139,7 +139,7 @@ class ListProtocols(command.Lister): class SetProtocol(command.Command): - """Set federation protocol properties""" + _description = _("Set federation protocol properties") def get_parser(self, prog_name): parser = super(SetProtocol, self).get_parser(prog_name) @@ -179,7 +179,7 @@ class SetProtocol(command.Command): class ShowProtocol(command.ShowOne): - """Display federation protocol details""" + _description = _("Display federation protocol details") def get_parser(self, prog_name): parser = super(ShowProtocol, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index f780810a..39c8547c 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -20,6 +20,7 @@ import sys from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -31,7 +32,7 @@ LOG = logging.getLogger(__name__) class AddUserToGroup(command.Command): - """Add user to group""" + _description = _("Add user to group") def get_parser(self, prog_name): parser = super(AddUserToGroup, self).get_parser(prog_name) @@ -43,7 +44,9 @@ class AddUserToGroup(command.Command): parser.add_argument( 'user', metavar='<user>', - help=_('User to add to <group> (name or ID)'), + nargs='+', + help=_('User(s) to add to <group> (name or ID) ' + '(repeat option to add multiple users)'), ) common.add_group_domain_option_to_parser(parser) common.add_user_domain_option_to_parser(parser) @@ -52,31 +55,38 @@ class AddUserToGroup(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - user_id = common.find_user(identity_client, - parsed_args.user, - parsed_args.user_domain).id group_id = common.find_group(identity_client, parsed_args.group, parsed_args.group_domain).id - try: - identity_client.users.add_to_group(user_id, group_id) - except Exception: - msg = _("%(user)s not added to group %(group)s\n") % { - 'user': parsed_args.user, - 'group': parsed_args.group, - } - sys.stderr.write(msg) - else: - msg = _("%(user)s added to group %(group)s\n") % { - 'user': parsed_args.user, + result = 0 + for i in parsed_args.user: + try: + user_id = common.find_user(identity_client, + i, + parsed_args.user_domain).id + identity_client.users.add_to_group(user_id, group_id) + except Exception as e: + result += 1 + msg = _("%(user)s not added to group %(group)s: %(e)s") % { + 'user': i, + 'group': parsed_args.group, + 'e': e, + } + LOG.error(msg) + if result > 0: + total = len(parsed_args.user) + msg = (_("%(result)s of %(total)s users not added to group " + "%(group)s.")) % { + 'result': result, + 'total': total, 'group': parsed_args.group, } - sys.stdout.write(msg) + raise exceptions.CommandError(msg) class CheckUserInGroup(command.Command): - """Check user membership in group""" + _description = _("Check user membership in group") def get_parser(self, prog_name): parser = super(CheckUserInGroup, self).get_parser(prog_name) @@ -106,12 +116,15 @@ class CheckUserInGroup(command.Command): try: identity_client.users.check_in_group(user_id, group_id) - except Exception: - msg = _("%(user)s not in group %(group)s\n") % { - 'user': parsed_args.user, - 'group': parsed_args.group, - } - sys.stderr.write(msg) + except ks_exc.http.HTTPClientError as e: + if e.http_status == 403 or e.http_status == 404: + msg = _("%(user)s not in group %(group)s\n") % { + 'user': parsed_args.user, + 'group': parsed_args.group, + } + sys.stderr.write(msg) + else: + raise e else: msg = _("%(user)s in group %(group)s\n") % { 'user': parsed_args.user, @@ -121,7 +134,7 @@ class CheckUserInGroup(command.Command): class CreateGroup(command.ShowOne): - """Create new group""" + _description = _("Create new group") def get_parser(self, prog_name): parser = super(CreateGroup, self).get_parser(prog_name) @@ -174,7 +187,7 @@ class CreateGroup(command.ShowOne): class DeleteGroup(command.Command): - """Delete group(s)""" + _description = _("Delete group(s)") def get_parser(self, prog_name): parser = super(DeleteGroup, self).get_parser(prog_name) @@ -194,15 +207,28 @@ class DeleteGroup(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + errors = 0 for group in parsed_args.groups: - group_obj = common.find_group(identity_client, - group, - parsed_args.domain) - identity_client.groups.delete(group_obj.id) + try: + group_obj = common.find_group(identity_client, + group, + parsed_args.domain) + identity_client.groups.delete(group_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete group with " + "name or ID '%(group)s': %(e)s"), + {'group': group, 'e': e}) + + if errors > 0: + total = len(parsed_args.groups) + msg = (_("%(errors)s of %(total)s groups failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListGroup(command.Lister): - """List groups""" + _description = _("List groups") def get_parser(self, prog_name): parser = super(ListGroup, self).get_parser(prog_name) @@ -262,7 +288,7 @@ class ListGroup(command.Lister): class RemoveUserFromGroup(command.Command): - """Remove user from group""" + _description = _("Remove user from group") def get_parser(self, prog_name): parser = super(RemoveUserFromGroup, self).get_parser(prog_name) @@ -274,7 +300,9 @@ class RemoveUserFromGroup(command.Command): parser.add_argument( 'user', metavar='<user>', - help=_('User to remove from <group> (name or ID)'), + nargs='+', + help=_('User(s) to remove from <group> (name or ID) ' + '(repeat option to remove multiple users)'), ) common.add_group_domain_option_to_parser(parser) common.add_user_domain_option_to_parser(parser) @@ -283,31 +311,38 @@ class RemoveUserFromGroup(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - user_id = common.find_user(identity_client, - parsed_args.user, - parsed_args.user_domain).id group_id = common.find_group(identity_client, parsed_args.group, parsed_args.group_domain).id - try: - identity_client.users.remove_from_group(user_id, group_id) - except Exception: - msg = _("%(user)s not removed from group %(group)s\n") % { - 'user': parsed_args.user, - 'group': parsed_args.group, - } - sys.stderr.write(msg) - else: - msg = _("%(user)s removed from group %(group)s\n") % { - 'user': parsed_args.user, + result = 0 + for i in parsed_args.user: + try: + user_id = common.find_user(identity_client, + i, + parsed_args.user_domain).id + identity_client.users.remove_from_group(user_id, group_id) + except Exception as e: + result += 1 + msg = _("%(user)s not removed from group %(group)s: %(e)s") % { + 'user': i, + 'group': parsed_args.group, + 'e': e, + } + LOG.error(msg) + if result > 0: + total = len(parsed_args.user) + msg = (_("%(result)s of %(total)s users not removed from group " + "%(group)s.")) % { + 'result': result, + 'total': total, 'group': parsed_args.group, } - sys.stdout.write(msg) + raise exceptions.CommandError(msg) class SetGroup(command.Command): - """Set group properties""" + _description = _("Set group properties") def get_parser(self, prog_name): parser = super(SetGroup, self).get_parser(prog_name) @@ -347,7 +382,7 @@ class SetGroup(command.Command): class ShowGroup(command.ShowOne): - """Display group details""" + _description = _("Display group details") def get_parser(self, prog_name): parser = super(ShowGroup, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index b6b03188..e8b3a2f4 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -27,7 +27,7 @@ LOG = logging.getLogger(__name__) class CreateIdentityProvider(command.ShowOne): - """Create new identity provider""" + _description = _("Create new identity provider") def get_parser(self, prog_name): parser = super(CreateIdentityProvider, self).get_parser(prog_name) @@ -94,7 +94,7 @@ class CreateIdentityProvider(command.ShowOne): class DeleteIdentityProvider(command.Command): - """Delete identity provider(s)""" + _description = _("Delete identity provider(s)") def get_parser(self, prog_name): parser = super(DeleteIdentityProvider, self).get_parser(prog_name) @@ -115,8 +115,8 @@ class DeleteIdentityProvider(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete identity providers with " - "name or ID '%(provider)s': %(e)s") - % {'provider': i, 'e': e}) + "name or ID '%(provider)s': %(e)s"), + {'provider': i, 'e': e}) if result > 0: total = len(parsed_args.identity_provider) @@ -126,7 +126,7 @@ class DeleteIdentityProvider(command.Command): class ListIdentityProvider(command.Lister): - """List identity providers""" + _description = _("List identity providers") def take_action(self, parsed_args): columns = ('ID', 'Enabled', 'Description') @@ -140,7 +140,7 @@ class ListIdentityProvider(command.Lister): class SetIdentityProvider(command.Command): - """Set identity provider properties""" + _description = _("Set identity provider properties") def get_parser(self, prog_name): parser = super(SetIdentityProvider, self).get_parser(prog_name) @@ -211,7 +211,7 @@ class SetIdentityProvider(command.Command): class ShowIdentityProvider(command.ShowOne): - """Display identity provider details""" + _description = _("Display identity provider details") def get_parser(self, prog_name): parser = super(ShowIdentityProvider, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py index 09181a0b..e729c410 100644 --- a/openstackclient/identity/v3/mapping.py +++ b/openstackclient/identity/v3/mapping.py @@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__) class _RulesReader(object): - """Helper class capable of reading rules from files""" + _description = _("Helper class capable of reading rules from files") def _read_rules(self, path): """Read and parse rules from path @@ -82,7 +82,7 @@ class _RulesReader(object): class CreateMapping(command.ShowOne, _RulesReader): - """Create new mapping""" + _description = _("Create new mapping") def get_parser(self, prog_name): parser = super(CreateMapping, self).get_parser(prog_name) @@ -111,7 +111,7 @@ class CreateMapping(command.ShowOne, _RulesReader): class DeleteMapping(command.Command): - """Delete mapping(s)""" + _description = _("Delete mapping(s)") def get_parser(self, prog_name): parser = super(DeleteMapping, self).get_parser(prog_name) @@ -132,8 +132,7 @@ class DeleteMapping(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete mapping with name or " - "ID '%(mapping)s': %(e)s") - % {'mapping': i, 'e': e}) + "ID '%(mapping)s': %(e)s"), {'mapping': i, 'e': e}) if result > 0: total = len(parsed_args.mapping) @@ -143,7 +142,7 @@ class DeleteMapping(command.Command): class ListMapping(command.Lister): - """List mappings""" + _description = _("List mappings") def take_action(self, parsed_args): # NOTE(marek-denis): Since rules can be long and tedious I have decided @@ -157,7 +156,7 @@ class ListMapping(command.Lister): class SetMapping(command.Command, _RulesReader): - """Set mapping properties""" + _description = _("Set mapping properties") def get_parser(self, prog_name): parser = super(SetMapping, self).get_parser(prog_name) @@ -183,11 +182,10 @@ class SetMapping(command.Command, _RulesReader): rules=rules) mapping._info.pop('links', None) - return zip(*sorted(six.iteritems(mapping._info))) class ShowMapping(command.ShowOne): - """Display mapping details""" + _description = _("Display mapping details") def get_parser(self, prog_name): parser = super(ShowMapping, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index 596eae01..3b644195 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__) class CreatePolicy(command.ShowOne): - """Create new policy""" + _description = _("Create new policy") def get_parser(self, prog_name): parser = super(CreatePolicy, self).get_parser(prog_name) @@ -61,7 +61,7 @@ class CreatePolicy(command.ShowOne): class DeletePolicy(command.Command): - """Delete policy(s)""" + _description = _("Delete policy(s)") def get_parser(self, prog_name): parser = super(DeletePolicy, self).get_parser(prog_name) @@ -82,8 +82,7 @@ class DeletePolicy(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete policy with name or " - "ID '%(policy)s': %(e)s") - % {'policy': i, 'e': e}) + "ID '%(policy)s': %(e)s"), {'policy': i, 'e': e}) if result > 0: total = len(parsed_args.policy) @@ -93,7 +92,7 @@ class DeletePolicy(command.Command): class ListPolicy(command.Lister): - """List policies""" + _description = _("List policies") def get_parser(self, prog_name): parser = super(ListPolicy, self).get_parser(prog_name) @@ -121,7 +120,7 @@ class ListPolicy(command.Lister): class SetPolicy(command.Command): - """Set policy properties""" + _description = _("Set policy properties") def get_parser(self, prog_name): parser = super(SetPolicy, self).get_parser(prog_name) @@ -159,7 +158,7 @@ class SetPolicy(command.Command): class ShowPolicy(command.ShowOne): - """Display policy details""" + _description = _("Display policy details") def get_parser(self, prog_name): parser = super(ShowPolicy, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 56c4fbc8..873ee9c7 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -20,6 +20,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.cli import parseractions from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -31,7 +32,7 @@ LOG = logging.getLogger(__name__) class CreateProject(command.ShowOne): - """Create new project""" + _description = _("Create new project") def get_parser(self, prog_name): parser = super(CreateProject, self).get_parser(prog_name) @@ -125,7 +126,7 @@ class CreateProject(command.ShowOne): class DeleteProject(command.Command): - """Delete project(s)""" + _description = _("Delete project(s)") def get_parser(self, prog_name): parser = super(DeleteProject, self).get_parser(prog_name) @@ -148,19 +149,32 @@ class DeleteProject(command.Command): domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) + errors = 0 for project in parsed_args.projects: - if domain is not None: - project_obj = utils.find_resource(identity_client.projects, - project, - domain_id=domain.id) - else: - project_obj = utils.find_resource(identity_client.projects, - project) - identity_client.projects.delete(project_obj.id) + try: + if domain is not None: + project_obj = utils.find_resource(identity_client.projects, + project, + domain_id=domain.id) + else: + project_obj = utils.find_resource(identity_client.projects, + project) + identity_client.projects.delete(project_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete project with " + "name or ID '%(project)s': %(e)s"), + {'project': project, 'e': e}) + + if errors > 0: + total = len(parsed_args.projects) + msg = (_("%(errors)s of %(total)s projects failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListProject(command.Lister): - """List projects""" + _description = _("List projects") def get_parser(self, prog_name): parser = super(ListProject, self).get_parser(prog_name) @@ -175,11 +189,24 @@ class ListProject(command.Lister): help=_('Filter projects by <user> (name or ID)'), ) parser.add_argument( + '--my-projects', + action='store_true', + help=_('List projects for the authenticated user. ' + 'Supersedes other filters.'), + ) + parser.add_argument( '--long', action='store_true', default=False, help=_('List additional fields in output'), ) + parser.add_argument( + '--sort', + metavar='<key>[:<direction>]', + help=_('Sort output by selected keys and directions (asc or desc) ' + '(default: asc), repeat this option to specify multiple ' + 'keys and directions.'), + ) return parser def take_action(self, parsed_args): @@ -207,7 +234,25 @@ class ListProject(command.Lister): kwargs['user'] = user_id - data = identity_client.projects.list(**kwargs) + if parsed_args.my_projects: + # NOTE(adriant): my-projects supersedes all the other filters. + kwargs = {'user': self.app.client_manager.auth_ref.user_id} + + try: + data = identity_client.projects.list(**kwargs) + except ks_exc.Forbidden: + # NOTE(adriant): if no filters, assume a forbidden is non-admin + # wanting their own project list. + if not kwargs: + user = self.app.client_manager.auth_ref.user_id + data = identity_client.projects.list( + user=user) + else: + raise + + if parsed_args.sort: + data = utils.sort_items(data, parsed_args.sort) + return (columns, (utils.get_item_properties( s, columns, @@ -216,7 +261,7 @@ class ListProject(command.Lister): class SetProject(command.Command): - """Set project properties""" + _description = _("Set project properties") def get_parser(self, prog_name): parser = super(SetProject, self).get_parser(prog_name) @@ -283,7 +328,7 @@ class SetProject(command.Command): class ShowProject(command.ShowOne): - """Display project details""" + _description = _("Display project details") def get_parser(self, prog_name): parser = super(ShowProject, self).get_parser(prog_name) @@ -322,13 +367,18 @@ class ShowProject(command.ShowOne): project = utils.find_resource( identity_client.projects, project_str, - domain_id=domain.id, - parents_as_list=parsed_args.parents, - subtree_as_list=parsed_args.children) + domain_id=domain.id) else: project = utils.find_resource( identity_client.projects, - project_str, + project_str) + + if parsed_args.parents or parsed_args.children: + # NOTE(RuiChen): utils.find_resource() can't pass kwargs, + # if id query hit the result at first, so call + # identity manager.get() with kwargs directly. + project = identity_client.projects.get( + project.id, parents_as_list=parsed_args.parents, subtree_as_list=parsed_args.children) diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py index b7c51f93..69c8b506 100644 --- a/openstackclient/identity/v3/region.py +++ b/openstackclient/identity/v3/region.py @@ -27,7 +27,7 @@ LOG = logging.getLogger(__name__) class CreateRegion(command.ShowOne): - """Create new region""" + _description = _("Create new region") def get_parser(self, prog_name): parser = super(CreateRegion, self).get_parser(prog_name) @@ -66,7 +66,7 @@ class CreateRegion(command.ShowOne): class DeleteRegion(command.Command): - """Delete region(s)""" + _description = _("Delete region(s)") def get_parser(self, prog_name): parser = super(DeleteRegion, self).get_parser(prog_name) @@ -87,8 +87,7 @@ class DeleteRegion(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete region with " - "ID '%(region)s': %(e)s") - % {'region': i, 'e': e}) + "ID '%(region)s': %(e)s"), {'region': i, 'e': e}) if result > 0: total = len(parsed_args.region) @@ -98,7 +97,7 @@ class DeleteRegion(command.Command): class ListRegion(command.Lister): - """List regions""" + _description = _("List regions") def get_parser(self, prog_name): parser = super(ListRegion, self).get_parser(prog_name) @@ -128,7 +127,7 @@ class ListRegion(command.Lister): class SetRegion(command.Command): - """Set region properties""" + _description = _("Set region properties") def get_parser(self, prog_name): parser = super(SetRegion, self).get_parser(prog_name) @@ -162,7 +161,7 @@ class SetRegion(command.Command): class ShowRegion(command.ShowOne): - """Display region details""" + _description = _("Display region details") def get_parser(self, prog_name): parser = super(ShowRegion, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 8b911746..1bbf5f07 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -16,10 +16,10 @@ """Identity v3 Role action implementations""" import logging -import sys from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -109,7 +109,8 @@ def _process_identity_and_resource_options(parsed_args, class AddRole(command.Command): - """Adds a role assignment to a user or group on a domain or project""" + _description = _("Adds a role assignment to a user or group on a domain " + "or project") def get_parser(self, prog_name): parser = super(AddRole, self).get_parser(prog_name) @@ -127,7 +128,9 @@ class AddRole(command.Command): if (not parsed_args.user and not parsed_args.domain and not parsed_args.group and not parsed_args.project): - return + msg = _("Role not added, incorrect set of arguments " + "provided. See openstack --help for more details") + raise exceptions.CommandError(msg) domain_id = None if parsed_args.role_domain: @@ -141,17 +144,12 @@ class AddRole(command.Command): kwargs = _process_identity_and_resource_options( parsed_args, self.app.client_manager.identity) - if not kwargs: - sys.stderr.write(_("Role not added, incorrect set of arguments " - "provided. See openstack --help for more " - "details\n")) - return identity_client.roles.grant(role.id, **kwargs) class CreateRole(command.ShowOne): - """Create new role""" + _description = _("Create new role") def get_parser(self, prog_name): parser = super(CreateRole, self).get_parser(prog_name) @@ -198,7 +196,7 @@ class CreateRole(command.ShowOne): class DeleteRole(command.Command): - """Delete role(s)""" + _description = _("Delete role(s)") def get_parser(self, prog_name): parser = super(DeleteRole, self).get_parser(prog_name) @@ -222,18 +220,30 @@ class DeleteRole(command.Command): if parsed_args.domain: domain_id = common.find_domain(identity_client, parsed_args.domain).id - + errors = 0 for role in parsed_args.roles: - role_obj = utils.find_resource( - identity_client.roles, - role, - domain_id=domain_id - ) - identity_client.roles.delete(role_obj.id) + try: + role_obj = utils.find_resource( + identity_client.roles, + role, + domain_id=domain_id + ) + identity_client.roles.delete(role_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete role with " + "name or ID '%(role)s': %(e)s"), + {'role': role, 'e': e}) + + if errors > 0: + total = len(parsed_args.roles) + msg = (_("%(errors)s of %(total)s roles failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListRole(command.Lister): - """List roles""" + _description = _("List roles") def get_parser(self, prog_name): parser = super(ListRole, self).get_parser(prog_name) @@ -358,10 +368,10 @@ class ListRole(command.Lister): '<group-name> --project <project-name> --names ' 'instead.')) else: - sys.stderr.write(_("Error: If a user or group is specified, " - "either --domain or --project must also be " - "specified to list role grants.\n")) - return ([], []) + msg = _("Error: If a user or group is specified, " + "either --domain or --project must also be " + "specified to list role grants.") + raise exceptions.CommandError(msg) return (columns, (utils.get_item_properties( @@ -371,7 +381,8 @@ class ListRole(command.Lister): class RemoveRole(command.Command): - """Removes a role assignment from domain/project : user/group""" + _description = _("Removes a role assignment from domain/project : " + "user/group") def get_parser(self, prog_name): parser = super(RemoveRole, self).get_parser(prog_name) @@ -390,9 +401,9 @@ class RemoveRole(command.Command): if (not parsed_args.user and not parsed_args.domain and not parsed_args.group and not parsed_args.project): - sys.stderr.write(_("Incorrect set of arguments provided. " - "See openstack --help for more details\n")) - return + msg = _("Incorrect set of arguments provided. " + "See openstack --help for more details") + raise exceptions.CommandError(msg) domain_id = None if parsed_args.role_domain: @@ -406,16 +417,11 @@ class RemoveRole(command.Command): kwargs = _process_identity_and_resource_options( parsed_args, self.app.client_manager.identity) - if not kwargs: - sys.stderr.write(_("Role not removed, incorrect set of arguments " - "provided. See openstack --help for more " - "details\n")) - return identity_client.roles.revoke(role.id, **kwargs) class SetRole(command.Command): - """Set role properties""" + _description = _("Set role properties") def get_parser(self, prog_name): parser = super(SetRole, self).get_parser(prog_name) @@ -452,7 +458,7 @@ class SetRole(command.Command): class ShowRole(command.ShowOne): - """Display role details""" + _description = _("Display role details") def get_parser(self, prog_name): parser = super(ShowRole, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/role_assignment.py b/openstackclient/identity/v3/role_assignment.py index d25cc6ce..9da050de 100644 --- a/openstackclient/identity/v3/role_assignment.py +++ b/openstackclient/identity/v3/role_assignment.py @@ -11,7 +11,7 @@ # under the License. # -"""Identity v3 Assignment action implementations """ +"""Identity v3 Assignment action implementations""" from osc_lib.command import command from osc_lib import utils @@ -21,7 +21,7 @@ from openstackclient.identity import common class ListRoleAssignment(command.Lister): - """List role assignments""" + _description = _("List role assignments") def get_parser(self, prog_name): parser = super(ListRoleAssignment, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index 97e64dc6..ac8d8d9e 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__) class CreateService(command.ShowOne): - """Create new service""" + _description = _("Create new service") def get_parser(self, prog_name): parser = super(CreateService, self).get_parser(prog_name) @@ -81,7 +81,7 @@ class CreateService(command.ShowOne): class DeleteService(command.Command): - """Delete service(s)""" + _description = _("Delete service(s)") def get_parser(self, prog_name): parser = super(DeleteService, self).get_parser(prog_name) @@ -103,8 +103,8 @@ class DeleteService(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete consumer with type, " - "name or ID '%(service)s': %(e)s") - % {'service': i, 'e': e}) + "name or ID '%(service)s': %(e)s"), + {'service': i, 'e': e}) if result > 0: total = len(parsed_args.service) @@ -114,7 +114,7 @@ class DeleteService(command.Command): class ListService(command.Lister): - """List services""" + _description = _("List services") def get_parser(self, prog_name): parser = super(ListService, self).get_parser(prog_name) @@ -140,7 +140,7 @@ class ListService(command.Lister): class SetService(command.Command): - """Set service properties""" + _description = _("Set service properties") def get_parser(self, prog_name): parser = super(SetService, self).get_parser(prog_name) @@ -201,7 +201,7 @@ class SetService(command.Command): class ShowService(command.ShowOne): - """Display service details""" + _description = _("Display service details") def get_parser(self, prog_name): parser = super(ShowService, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/service_provider.py b/openstackclient/identity/v3/service_provider.py index 8548ae1f..bb2d9917 100644 --- a/openstackclient/identity/v3/service_provider.py +++ b/openstackclient/identity/v3/service_provider.py @@ -27,7 +27,7 @@ LOG = logging.getLogger(__name__) class CreateServiceProvider(command.ShowOne): - """Create new service provider""" + _description = _("Create new service provider") def get_parser(self, prog_name): parser = super(CreateServiceProvider, self).get_parser(prog_name) @@ -87,7 +87,7 @@ class CreateServiceProvider(command.ShowOne): class DeleteServiceProvider(command.Command): - """Delete service provider(s)""" + _description = _("Delete service provider(s)") def get_parser(self, prog_name): parser = super(DeleteServiceProvider, self).get_parser(prog_name) @@ -108,8 +108,8 @@ class DeleteServiceProvider(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete service provider with " - "name or ID '%(provider)s': %(e)s") - % {'provider': i, 'e': e}) + "name or ID '%(provider)s': %(e)s"), + {'provider': i, 'e': e}) if result > 0: total = len(parsed_args.service_provider) @@ -119,7 +119,7 @@ class DeleteServiceProvider(command.Command): class ListServiceProvider(command.Lister): - """List service providers""" + _description = _("List service providers") def take_action(self, parsed_args): service_client = self.app.client_manager.identity @@ -134,7 +134,7 @@ class ListServiceProvider(command.Lister): class SetServiceProvider(command.Command): - """Set service provider properties""" + _description = _("Set service provider properties") def get_parser(self, prog_name): parser = super(SetServiceProvider, self).get_parser(prog_name) @@ -192,7 +192,7 @@ class SetServiceProvider(command.Command): class ShowServiceProvider(command.ShowOne): - """Display service provider details""" + _description = _("Display service provider details") def get_parser(self, prog_name): parser = super(ShowServiceProvider, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 2cd304e6..effb9e35 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -25,7 +25,7 @@ from openstackclient.identity import common class AuthorizeRequestToken(command.ShowOne): - """Authorize a request token""" + _description = _("Authorize a request token") def get_parser(self, prog_name): parser = super(AuthorizeRequestToken, self).get_parser(prog_name) @@ -42,7 +42,7 @@ class AuthorizeRequestToken(command.ShowOne): default=[], required=True, help=_('Roles to authorize (name or ID) ' - '(repeat option to set multiple values, required)'), + '(repeat option to set multiple values) (required)'), ) return parser @@ -66,7 +66,7 @@ class AuthorizeRequestToken(command.ShowOne): class CreateAccessToken(command.ShowOne): - """Create an access token""" + _description = _("Create an access token") def get_parser(self, prog_name): parser = super(CreateAccessToken, self).get_parser(prog_name) @@ -112,7 +112,7 @@ class CreateAccessToken(command.ShowOne): class CreateRequestToken(command.ShowOne): - """Create a request token""" + _description = _("Create a request token") def get_parser(self, prog_name): parser = super(CreateRequestToken, self).get_parser(prog_name) @@ -164,7 +164,7 @@ class CreateRequestToken(command.ShowOne): class IssueToken(command.ShowOne): - """Issue new token""" + _description = _("Issue new token") # scoped token is optional required_scope = False @@ -196,7 +196,7 @@ class IssueToken(command.ShowOne): class RevokeToken(command.Command): - """Revoke existing token""" + _description = _("Revoke existing token") def get_parser(self, prog_name): parser = super(RevokeToken, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/trust.py b/openstackclient/identity/v3/trust.py index bbc86adb..52daeb4d 100644 --- a/openstackclient/identity/v3/trust.py +++ b/openstackclient/identity/v3/trust.py @@ -14,8 +14,11 @@ """Identity v3 Trust action implementations""" import datetime +import logging +from keystoneclient import exceptions as identity_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -23,8 +26,11 @@ from openstackclient.i18n import _ from openstackclient.identity import common +LOG = logging.getLogger(__name__) + + class CreateTrust(command.ShowOne): - """Create new trust""" + _description = _("Create new trust") def get_parser(self, prog_name): parser = super(CreateTrust, self).get_parser(prog_name) @@ -100,10 +106,13 @@ class CreateTrust(command.ShowOne): role_names = [] for role in parsed_args.role: - role_name = utils.find_resource( - identity_client.roles, - role, - ).name + try: + role_name = utils.find_resource( + identity_client.roles, + role, + ).name + except identity_exc.Forbidden: + role_name = role role_names.append(role_name) expires_at = None @@ -131,7 +140,7 @@ class CreateTrust(command.ShowOne): class DeleteTrust(command.Command): - """Delete trust(s)""" + _description = _("Delete trust(s)") def get_parser(self, prog_name): parser = super(DeleteTrust, self).get_parser(prog_name) @@ -145,13 +154,28 @@ class DeleteTrust(command.Command): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - for t in parsed_args.trust: - trust_obj = utils.find_resource(identity_client.trusts, t) - identity_client.trusts.delete(trust_obj.id) + + errors = 0 + for trust in parsed_args.trust: + try: + trust_obj = utils.find_resource(identity_client.trusts, + trust) + identity_client.trusts.delete(trust_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete trust with " + "name or ID '%(trust)s': %(e)s"), + {'trust': trust, 'e': e}) + + if errors > 0: + total = len(parsed_args.trust) + msg = (_("%(errors)s of %(total)s trusts failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListTrust(command.Lister): - """List trusts""" + _description = _("List trusts") def take_action(self, parsed_args): columns = ('ID', 'Expires At', 'Impersonation', 'Project ID', @@ -165,7 +189,7 @@ class ListTrust(command.Lister): class ShowTrust(command.ShowOne): - """Display trust details""" + _description = _("Display trust details") def get_parser(self, prog_name): parser = super(ShowTrust, self).get_parser(prog_name) diff --git a/openstackclient/identity/v3/unscoped_saml.py b/openstackclient/identity/v3/unscoped_saml.py index f116174b..f7598f17 100644 --- a/openstackclient/identity/v3/unscoped_saml.py +++ b/openstackclient/identity/v3/unscoped_saml.py @@ -18,35 +18,14 @@ the user can list domains and projects they are allowed to access, and request a scoped token.""" from osc_lib.command import command -from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ -UNSCOPED_AUTH_PLUGINS = ['v3unscopedsaml', 'v3unscopedadfs', 'v3oidc'] - - -def auth_with_unscoped_saml(func): - """Check the unscoped federated context""" - - def _decorated(self, parsed_args): - auth_plugin_name = self.app.client_manager.auth_plugin_name - if auth_plugin_name in UNSCOPED_AUTH_PLUGINS: - return func(self, parsed_args) - else: - msg = (_('This command requires the use of an unscoped SAML ' - 'authentication plugin. Please use argument ' - '--os-auth-type with one of the following ' - 'plugins: %s') % ', '.join(UNSCOPED_AUTH_PLUGINS)) - raise exceptions.CommandError(msg) - return _decorated - - class ListAccessibleDomains(command.Lister): - """List accessible domains""" + _description = _("List accessible domains") - @auth_with_unscoped_saml def take_action(self, parsed_args): columns = ('ID', 'Enabled', 'Name', 'Description') identity_client = self.app.client_manager.identity @@ -59,9 +38,8 @@ class ListAccessibleDomains(command.Lister): class ListAccessibleProjects(command.Lister): - """List accessible projects""" + _description = _("List accessible projects") - @auth_with_unscoped_saml def take_action(self, parsed_args): columns = ('ID', 'Domain ID', 'Enabled', 'Name') identity_client = self.app.client_manager.identity diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index dc47ef8d..9c289a6d 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -20,6 +20,7 @@ import logging from keystoneauth1 import exceptions as ks_exc from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils import six @@ -31,7 +32,7 @@ LOG = logging.getLogger(__name__) class CreateUser(command.ShowOne): - """Create new user""" + _description = _("Create new user") def get_parser(self, prog_name): parser = super(CreateUser, self).get_parser(prog_name) @@ -110,6 +111,10 @@ class CreateUser(command.ShowOne): if parsed_args.password_prompt: parsed_args.password = utils.get_password(self.app.stdin) + if not parsed_args.password: + LOG.warning(_("No password was supplied, authentication will fail " + "when a user does not have a password.")) + try: user = identity_client.users.create( name=parsed_args.name, @@ -134,7 +139,7 @@ class CreateUser(command.ShowOne): class DeleteUser(command.Command): - """Delete user(s)""" + _description = _("Delete user(s)") def get_parser(self, prog_name): parser = super(DeleteUser, self).get_parser(prog_name) @@ -157,19 +162,32 @@ class DeleteUser(command.Command): domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) + errors = 0 for user in parsed_args.users: - if domain is not None: - user_obj = utils.find_resource(identity_client.users, - user, - domain_id=domain.id) - else: - user_obj = utils.find_resource(identity_client.users, - user) - identity_client.users.delete(user_obj.id) + try: + if domain is not None: + user_obj = utils.find_resource(identity_client.users, + user, + domain_id=domain.id) + else: + user_obj = utils.find_resource(identity_client.users, + user) + identity_client.users.delete(user_obj.id) + except Exception as e: + errors += 1 + LOG.error(_("Failed to delete user with " + "name or ID '%(user)s': %(e)s"), + {'user': user, 'e': e}) + + if errors > 0: + total = len(parsed_args.users) + msg = (_("%(errors)s of %(total)s users failed " + "to delete.") % {'errors': errors, 'total': total}) + raise exceptions.CommandError(msg) class ListUser(command.Lister): - """List users""" + _description = _("List users") def get_parser(self, prog_name): parser = super(ListUser, self).get_parser(prog_name) @@ -269,7 +287,7 @@ class ListUser(command.Lister): class SetUser(command.Command): - """Set user properties""" + _description = _("Set user properties") def get_parser(self, prog_name): parser = super(SetUser, self).get_parser(prog_name) @@ -284,6 +302,12 @@ class SetUser(command.Command): help=_('Set user name'), ) parser.add_argument( + '--domain', + metavar='<domain>', + help=_('Domain the user belongs to (name or ID). This can be ' + 'used in case collisions between user names exist.'), + ) + parser.add_argument( '--project', metavar='<project>', help=_('Set default project (name or ID)'), @@ -329,10 +353,23 @@ class SetUser(command.Command): if parsed_args.password_prompt: parsed_args.password = utils.get_password(self.app.stdin) - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) + if '' == parsed_args.password: + LOG.warning(_("No password was supplied, authentication will fail " + "when a user does not have a password.")) + + user_str = common._get_token_resource(identity_client, 'user', + parsed_args.user) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + user = utils.find_resource(identity_client.users, + user_str, + domain_id=domain.id) + else: + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + kwargs = {} if parsed_args.name: kwargs['name'] = parsed_args.name @@ -357,7 +394,7 @@ class SetUser(command.Command): class SetPasswordUser(command.Command): - """Change current user password""" + _description = _("Change current user password") required_scope = False @@ -408,11 +445,15 @@ class SetPasswordUser(command.Command): password = utils.get_password( self.app.stdin, prompt="New Password:") + if '' == password: + LOG.warning(_("No password was supplied, authentication will fail " + "when a user does not have a password.")) + identity_client.users.update_password(current_password, password) class ShowUser(command.ShowOne): - """Display user details""" + _description = _("Display user details") def get_parser(self, prog_name): parser = super(ShowUser, self).get_parser(prog_name) diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index 1be6c765..b67c291f 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -22,7 +22,7 @@ from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -DEFAULT_API_VERSION = '1' +DEFAULT_API_VERSION = '2' API_VERSION_OPTION = 'os_image_api_version' API_NAME = "image" API_VERSIONS = { diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 5f669c64..60d61709 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -36,8 +36,11 @@ from openstackclient.api import utils as api_utils from openstackclient.i18n import _ +CONTAINER_CHOICES = ["ami", "ari", "aki", "bare", "docker", "ova", "ovf"] DEFAULT_CONTAINER_FORMAT = 'bare' DEFAULT_DISK_FORMAT = 'raw' +DISK_CHOICES = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vhdx", + "vdi", "iso", "ploop"] LOG = logging.getLogger(__name__) @@ -59,7 +62,7 @@ def _format_visibility(data): class CreateImage(command.ShowOne): - """Create/upload an image""" + _description = _("Create/upload an image") def get_parser(self, prog_name): parser = super(CreateImage, self).get_parser(prog_name) @@ -82,15 +85,20 @@ class CreateImage(command.ShowOne): "--container-format", default=DEFAULT_CONTAINER_FORMAT, metavar="<container-format>", - help=_("Image container format " - "(default: %s)") % DEFAULT_CONTAINER_FORMAT, + choices=CONTAINER_CHOICES, + help=(_("Image container format. " + "The supported options are: %(option_list)s. " + "The default format is: %(default_opt)s") % + {'option_list': ', '.join(CONTAINER_CHOICES), + 'default_opt': DEFAULT_CONTAINER_FORMAT}) ) parser.add_argument( "--disk-format", default=DEFAULT_DISK_FORMAT, metavar="<disk-format>", - help=_("Image disk format " - "(default: %s)") % DEFAULT_DISK_FORMAT, + choices=DISK_CHOICES, + help=_("Image disk format. The supported options are: %s. " + "The default format is: raw") % ', '.join(DISK_CHOICES) ) parser.add_argument( "--size", @@ -120,12 +128,13 @@ class CreateImage(command.ShowOne): metavar="<image-url>", help=_("Copy image from the data store (similar to --location)"), ) - parser.add_argument( + source_group = parser.add_mutually_exclusive_group() + source_group.add_argument( "--file", metavar="<file>", help=_("Upload image from local file"), ) - parser.add_argument( + source_group.add_argument( "--volume", metavar="<volume>", help=_("Create image from a volume"), @@ -277,7 +286,7 @@ class CreateImage(command.ShowOne): class DeleteImage(command.Command): - """Delete image(s)""" + _description = _("Delete image(s)") def get_parser(self, prog_name): parser = super(DeleteImage, self).get_parser(prog_name) @@ -300,7 +309,7 @@ class DeleteImage(command.Command): class ListImage(command.Lister): - """List available images""" + _description = _("List available images") def get_parser(self, prog_name): parser = super(ListImage, self).get_parser(prog_name) @@ -350,8 +359,9 @@ class ListImage(command.Lister): parser.add_argument( '--sort', metavar="<key>[:<direction>]", + default='name:asc', help=_("Sort output by selected keys and directions(asc or desc) " - "(default: asc), multiple keys and directions can be " + "(default: name:asc), multiple keys and directions can be " "specified separated by comma"), ) return parser @@ -440,7 +450,7 @@ class ListImage(command.Lister): class SaveImage(command.Command): - """Save an image locally""" + _description = _("Save an image locally") def get_parser(self, prog_name): parser = super(SaveImage, self).get_parser(prog_name) @@ -468,7 +478,7 @@ class SaveImage(command.Command): class SetImage(command.Command): - """Set image properties""" + _description = _("Set image properties") def get_parser(self, prog_name): parser = super(SetImage, self).get_parser(prog_name) @@ -494,22 +504,19 @@ class SetImage(command.Command): type=int, help=_("Minimum RAM size needed to boot image, in megabytes"), ) - container_choices = ["ami", "ari", "aki", "bare", "ovf"] parser.add_argument( "--container-format", metavar="<container-format>", - help=_("Container format of image. Acceptable formats: %s") % - container_choices, - choices=container_choices + choices=CONTAINER_CHOICES, + help=_("Image container format. The supported options are: %s") % + ', '.join(CONTAINER_CHOICES) ) - disk_choices = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", - "vdi", "iso"] parser.add_argument( "--disk-format", metavar="<disk-format>", - help=_("Disk format of image. Acceptable formats: %s") % - disk_choices, - choices=disk_choices + choices=DISK_CHOICES, + help=_("Image disk format. The supported options are: %s.") % + ', '.join(DISK_CHOICES) ) parser.add_argument( "--size", @@ -702,7 +709,7 @@ class SetImage(command.Command): class ShowImage(command.ShowOne): - """Display image details""" + _description = _("Display image details") def get_parser(self, prog_name): parser = super(ShowImage, self).get_parser(prog_name) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 0712e09c..c4be69f0 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -30,15 +30,18 @@ from openstackclient.i18n import _ from openstackclient.identity import common +CONTAINER_CHOICES = ["ami", "ari", "aki", "bare", "docker", "ova", "ovf"] DEFAULT_CONTAINER_FORMAT = 'bare' DEFAULT_DISK_FORMAT = 'raw' +DISK_CHOICES = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vhdx", + "vdi", "iso", "ploop"] LOG = logging.getLogger(__name__) def _format_image(image): - """Format an image to make it more consistent with OSC operations. """ + """Format an image to make it more consistent with OSC operations.""" info = {} properties = {} @@ -69,7 +72,7 @@ def _format_image(image): class AddProjectToImage(command.ShowOne): - """Associate project with image""" + _description = _("Associate project with image") def get_parser(self, prog_name): parser = super(AddProjectToImage, self).get_parser(prog_name) @@ -107,7 +110,7 @@ class AddProjectToImage(command.ShowOne): class CreateImage(command.ShowOne): - """Create/upload an image""" + _description = _("Create/upload an image") deadopts = ('size', 'location', 'copy-from', 'checksum', 'store') @@ -133,16 +136,21 @@ class CreateImage(command.ShowOne): parser.add_argument( "--container-format", default=DEFAULT_CONTAINER_FORMAT, + choices=CONTAINER_CHOICES, metavar="<container-format>", - help=_("Image container format " - "(default: %s)") % DEFAULT_CONTAINER_FORMAT, + help=(_("Image container format. " + "The supported options are: %(option_list)s. " + "The default format is: %(default_opt)s") % + {'option_list': ', '.join(CONTAINER_CHOICES), + 'default_opt': DEFAULT_CONTAINER_FORMAT}) ) parser.add_argument( "--disk-format", default=DEFAULT_DISK_FORMAT, + choices=DISK_CHOICES, metavar="<disk-format>", - help=_("Image disk format " - "(default: %s)") % DEFAULT_DISK_FORMAT, + help=_("Image disk format. The supported options are: %s. " + "The default format is: raw") % ', '.join(DISK_CHOICES) ) parser.add_argument( "--min-disk", @@ -156,12 +164,13 @@ class CreateImage(command.ShowOne): type=int, help=_("Minimum RAM size needed to boot image, in megabytes"), ) - parser.add_argument( + source_group = parser.add_mutually_exclusive_group() + source_group.add_argument( "--file", metavar="<file>", help=_("Upload image from local file"), ) - parser.add_argument( + source_group.add_argument( "--volume", metavar="<volume>", help=_("Create image from a volume"), @@ -361,7 +370,7 @@ class CreateImage(command.ShowOne): class DeleteImage(command.Command): - """Delete image(s)""" + _description = _("Delete image(s)") def get_parser(self, prog_name): parser = super(DeleteImage, self).get_parser(prog_name) @@ -398,7 +407,7 @@ class DeleteImage(command.Command): class ListImage(command.Lister): - """List available images""" + _description = _("List available images") def get_parser(self, prog_name): parser = super(ListImage, self).get_parser(prog_name) @@ -447,8 +456,9 @@ class ListImage(command.Lister): parser.add_argument( '--sort', metavar="<key>[:<direction>]", + default='name:asc', help=_("Sort output by selected keys and directions(asc or desc) " - "(default: asc), multiple keys and directions can be " + "(default: name:asc), multiple keys and directions can be " "specified separated by comma"), ) parser.add_argument( @@ -482,7 +492,6 @@ class ListImage(command.Lister): if parsed_args.marker: kwargs['marker'] = utils.find_resource(image_client.images, parsed_args.marker).id - if parsed_args.long: columns = ( 'ID', @@ -515,7 +524,19 @@ class ListImage(command.Lister): column_headers = columns # List of image data received - data = image_client.api.image_list(**kwargs) + data = [] + if 'marker' in kwargs: + data = image_client.api.image_list(**kwargs) + else: + # No pages received yet, so start the page marker at None. + marker = None + while True: + page = image_client.api.image_list(marker=marker, **kwargs) + if not page: + break + data.extend(page) + # Set the marker to the id of the last item we received + marker = page[-1]['id'] if parsed_args.property: # NOTE(dtroyer): coerce to a list to subscript it in py3 @@ -542,7 +563,7 @@ class ListImage(command.Lister): class RemoveProjectImage(command.Command): - """Disassociate project with image""" + _description = _("Disassociate project with image") def get_parser(self, prog_name): parser = super(RemoveProjectImage, self).get_parser(prog_name) @@ -575,7 +596,7 @@ class RemoveProjectImage(command.Command): class SaveImage(command.Command): - """Save an image locally""" + _description = _("Save an image locally") def get_parser(self, prog_name): parser = super(SaveImage, self).get_parser(prog_name) @@ -603,7 +624,7 @@ class SaveImage(command.Command): class SetImage(command.Command): - """Set image properties""" + _description = _("Set image properties") deadopts = ('visibility',) @@ -644,14 +665,16 @@ class SetImage(command.Command): parser.add_argument( "--container-format", metavar="<container-format>", - help=_("Image container format " - "(default: %s)") % DEFAULT_CONTAINER_FORMAT, + choices=CONTAINER_CHOICES, + help=_("Image container format. The supported options are: %s") % + ', '.join(CONTAINER_CHOICES) ) parser.add_argument( "--disk-format", metavar="<disk-format>", - help=_("Image disk format " - "(default: %s)") % DEFAULT_DISK_FORMAT, + choices=DISK_CHOICES, + help=_("Image disk format. The supported options are: %s") % + ', '.join(DISK_CHOICES) ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( @@ -761,6 +784,23 @@ class SetImage(command.Command): dest=deadopt.replace('-', '_'), help=argparse.SUPPRESS, ) + + membership_group = parser.add_mutually_exclusive_group() + membership_group.add_argument( + "--accept", + action="store_true", + help=_("Accept the image membership"), + ) + membership_group.add_argument( + "--reject", + action="store_true", + help=_("Reject the image membership"), + ) + membership_group.add_argument( + "--pending", + action="store_true", + help=_("Reset the image membership to 'pending'"), + ) return parser def take_action(self, parsed_args): @@ -812,12 +852,14 @@ class SetImage(command.Command): project_arg = parsed_args.owner LOG.warning(_('The --owner option is deprecated, ' 'please use --project instead.')) + project_id = None if project_arg: - kwargs['owner'] = common.find_project( + project_id = common.find_project( identity_client, project_arg, parsed_args.project_domain, ).id + kwargs['owner'] = project_id image = utils.find_resource( image_client.images, parsed_args.image) @@ -830,6 +872,21 @@ class SetImage(command.Command): image_client.images.reactivate(image.id) activation_status = "activated" + membership_group_args = ('accept', 'reject', 'pending') + membership_status = [status for status in membership_group_args + if getattr(parsed_args, status)] + if membership_status: + # If a specific project is not passed, assume we want to update + # our own membership + if not project_id: + project_id = self.app.client_manager.auth_ref.project_id + # The mutually exclusive group of the arg parser ensure we have at + # most one item in the membership_status list. + if membership_status[0] != 'pending': + membership_status[0] += 'ed' # Glance expects the past form + image_client.image_members.update( + image.id, project_id, membership_status[0]) + if parsed_args.tags: # Tags should be extended, but duplicates removed kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) @@ -844,7 +901,7 @@ class SetImage(command.Command): class ShowImage(command.ShowOne): - """Display image details""" + _description = _("Display image details") def get_parser(self, prog_name): parser = super(ShowImage, self).get_parser(prog_name) @@ -867,7 +924,7 @@ class ShowImage(command.ShowOne): class UnsetImage(command.Command): - """Unset image tags and properties""" + _description = _("Unset image tags and properties") def get_parser(self, prog_name): parser = super(UnsetImage, self).get_parser(prog_name) diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index c562058d..9525b947 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -44,6 +44,11 @@ def make_client(instance): LOG.debug('Connection: %s', conn) LOG.debug('Network client initialized using OpenStack SDK: %s', conn.network) + + # NOTE(dtroyer): Horrible ugly hack since we don't actually save + # the connection anywhere yet, so stash it in the + # instance directly from here for other uses + instance.sdk_connection = conn return conn.network diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py index 2b1a5656..a2e700be 100644 --- a/openstackclient/network/common.py +++ b/openstackclient/network/common.py @@ -14,6 +14,7 @@ import abc import logging +import openstack.exceptions from osc_lib.command import command from osc_lib import exceptions import six @@ -181,12 +182,16 @@ class NetworkAndComputeShowOne(command.ShowOne): """ def take_action(self, parsed_args): - if self.app.client_manager.is_network_endpoint_enabled(): - return self.take_action_network(self.app.client_manager.network, - parsed_args) - else: - return self.take_action_compute(self.app.client_manager.compute, - parsed_args) + try: + if self.app.client_manager.is_network_endpoint_enabled(): + return self.take_action_network( + self.app.client_manager.network, parsed_args) + else: + return self.take_action_compute( + self.app.client_manager.compute, parsed_args) + except openstack.exceptions.HttpException as exc: + msg = _("Error while executing command: %s") % exc.message + raise exceptions.CommandError(msg) def get_parser(self, prog_name): LOG.debug('get_parser(%s)', prog_name) diff --git a/openstackclient/network/sdk_utils.py b/openstackclient/network/sdk_utils.py new file mode 100644 index 00000000..04f168be --- /dev/null +++ b/openstackclient/network/sdk_utils.py @@ -0,0 +1,59 @@ +# 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 six + + +def get_osc_show_columns_for_sdk_resource( + sdk_resource, + osc_column_map, + invisible_columns=[] +): + """Get and filter the display and attribute columns for an SDK resource. + + Common utility function for preparing the output of an OSC show command. + Some of the columns may need to get renamed, others made invisible. + + :param sdk_resource: An SDK resource + :param osc_column_map: A hash of mappings for display column names + :param invisible_columns: A list of invisible column names + + :returns: Two tuples containing the names of the display and attribute + columns + """ + + if getattr(sdk_resource, 'allow_get', None) is not None: + resource_dict = sdk_resource.to_dict( + body=True, headers=False, ignore_none=False) + else: + resource_dict = sdk_resource + + # Build the OSC column names to display for the SDK resource. + attr_map = {} + display_columns = list(resource_dict.keys()) + for col_name in invisible_columns: + if col_name in display_columns: + display_columns.remove(col_name) + for sdk_attr, osc_attr in six.iteritems(osc_column_map): + if sdk_attr in display_columns: + attr_map[osc_attr] = sdk_attr + display_columns.remove(sdk_attr) + if osc_attr not in display_columns: + display_columns.append(osc_attr) + sorted_display_columns = sorted(display_columns) + + # Build the SDK attribute names for the OSC column names. + attr_columns = [] + for column in sorted_display_columns: + new_column = attr_map[column] if column in attr_map else column + attr_columns.append(new_column) + return tuple(sorted_display_columns), tuple(attr_columns) diff --git a/openstackclient/network/v2/address_scope.py b/openstackclient/network/v2/address_scope.py index 6cd13f8c..71c1a9af 100644 --- a/openstackclient/network/v2/address_scope.py +++ b/openstackclient/network/v2/address_scope.py @@ -21,18 +21,18 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - - return tuple(sorted(columns)) + column_map = { + 'is_shared': 'shared', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): @@ -55,8 +55,10 @@ def _get_attrs(client_manager, parsed_args): return attrs +# TODO(rtheis): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateAddressScope(command.ShowOne): - """Create a new Address Scope""" + _description = _("Create a new Address Scope") def get_parser(self, prog_name): parser = super(CreateAddressScope, self).get_parser(prog_name) @@ -97,14 +99,14 @@ class CreateAddressScope(command.ShowOne): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_address_scope(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters={}) - return (columns, data) + return (display_columns, data) class DeleteAddressScope(command.Command): - """Delete address scope(s)""" + _description = _("Delete address scope(s)") def get_parser(self, prog_name): parser = super(DeleteAddressScope, self).get_parser(prog_name) @@ -138,8 +140,47 @@ class DeleteAddressScope(command.Command): raise exceptions.CommandError(msg) +# TODO(yanxing'an): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class ListAddressScope(command.Lister): - """List address scopes""" + _description = _("List address scopes") + + def get_parser(self, prog_name): + parser = super(ListAddressScope, self).get_parser(prog_name) + + parser.add_argument( + '--name', + metavar='<name>', + help=_("List only address scopes of given name in output") + ) + parser.add_argument( + '--ip-version', + type=int, + choices=[4, 6], + metavar='<ip-version>', + dest='ip_version', + help=_("List address scopes of given IP version networks (4 or 6)") + ) + parser.add_argument( + '--project', + metavar="<project>", + help=_("List address scopes according to their project " + "(name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + + shared_group = parser.add_mutually_exclusive_group() + shared_group.add_argument( + '--share', + action='store_true', + help=_("List address scopes shared between projects") + ) + shared_group.add_argument( + '--no-share', + action='store_true', + help=_("List address scopes not shared between projects") + ) + return parser def take_action(self, parsed_args): client = self.app.client_manager.network @@ -147,8 +188,8 @@ class ListAddressScope(command.Lister): 'id', 'name', 'ip_version', - 'shared', - 'tenant_id', + 'is_shared', + 'project_id', ) column_headers = ( 'ID', @@ -157,7 +198,25 @@ class ListAddressScope(command.Lister): 'Shared', 'Project', ) - data = client.address_scopes() + attrs = {} + if parsed_args.name: + attrs['name'] = parsed_args.name + if parsed_args.ip_version: + attrs['ip_version'] = parsed_args.ip_version + if parsed_args.share: + attrs['is_shared'] = True + if parsed_args.no_share: + attrs['is_shared'] = False + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = self.app.client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + attrs['project_id'] = project_id + data = client.address_scopes(**attrs) return (column_headers, (utils.get_item_properties( @@ -165,8 +224,10 @@ class ListAddressScope(command.Lister): ) for s in data)) +# TODO(rtheis): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetAddressScope(command.Command): - """Set address scope properties""" + _description = _("Set address scope properties") def get_parser(self, prog_name): parser = super(SetAddressScope, self).get_parser(prog_name) @@ -210,7 +271,7 @@ class SetAddressScope(command.Command): class ShowAddressScope(command.ShowOne): - """Display address scope details""" + _description = _("Display address scope details") def get_parser(self, prog_name): parser = super(ShowAddressScope, self).get_parser(prog_name) @@ -227,7 +288,7 @@ class ShowAddressScope(command.ShowOne): obj = client.find_address_scope( parsed_args.address_scope, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters={}) - return (columns, data) + return (display_columns, data) diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index bb75540c..41b208aa 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -15,10 +15,21 @@ import logging +from openstack import exceptions as sdk_exceptions +from openstack.network.v2 import floating_ip as _floating_ip from osc_lib import utils from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common from openstackclient.network import common +from openstackclient.network import sdk_utils + + +def _get_network_columns(item): + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_columns(item): @@ -58,11 +69,72 @@ def _get_attrs(client_manager, parsed_args): if parsed_args.description is not None: attrs['description'] = parsed_args.description + if parsed_args.project: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + return attrs +def _find_floating_ip( + session, + ip_cache, + name_or_id, + ignore_missing=True, + **params +): + """Find a floating IP by IP or ID + + The SDK's find_ip() can only locate a floating IP by ID so we have + to do this ourselves. + """ + + def _get_one_match(name_or_id): + """Given a list of results, return the match""" + the_result = None + for maybe_result in ip_cache: + id_value = maybe_result.id + ip_value = maybe_result.floating_ip_address + + if (id_value == name_or_id) or (ip_value == name_or_id): + # Only allow one resource to be found. If we already + # found a match, raise an exception to show it. + if the_result is None: + the_result = maybe_result + else: + msg = "More than one %s exists with the name '%s'." + msg = (msg % (_floating_ip.FloatingIP, name_or_id)) + raise sdk_exceptions.DuplicateResource(msg) + + return the_result + + # Try to short-circuit by looking directly for a matching ID. + try: + match = _floating_ip.FloatingIP.existing(id=name_or_id, **params) + return (match.get(session), ip_cache) + except sdk_exceptions.NotFoundException: + pass + + if len(ip_cache) == 0: + ip_cache = list(_floating_ip.FloatingIP.list(session, **params)) + + result = _get_one_match(name_or_id) + if result is not None: + return (result, ip_cache) + + if ignore_missing: + return (None, ip_cache) + raise sdk_exceptions.ResourceNotFound( + "No %s found for %s" % (_floating_ip.FloatingIP.__name__, name_or_id)) + + class CreateFloatingIP(common.NetworkAndComputeShowOne): - """Create floating IP""" + _description = _("Create floating IP") def update_parser_common(self, parser): # In Compute v2 network, floating IPs could be allocated from floating @@ -105,14 +177,20 @@ class CreateFloatingIP(common.NetworkAndComputeShowOne): metavar='<description>', help=_('Set floating IP description') ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) return parser def take_action_network(self, client, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_ip(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_network_columns(obj) data = utils.get_item_properties(obj, columns) - return (columns, data) + return (display_columns, data) def take_action_compute(self, client, parsed_args): obj = client.floating_ips.create(parsed_args.network) @@ -122,7 +200,7 @@ class CreateFloatingIP(common.NetworkAndComputeShowOne): class CreateIPFloating(CreateFloatingIP): - """Create floating IP""" + _description = _("Create floating IP") # TODO(tangchen): Remove this class and ``ip floating create`` command # two cycles after Mitaka. @@ -146,7 +224,7 @@ class CreateIPFloating(CreateFloatingIP): class DeleteFloatingIP(common.NetworkAndComputeDelete): - """Delete floating IP(s)""" + _description = _("Delete floating IP(s)") # Used by base class to find resources in parsed_args. resource = 'floating_ip' @@ -162,16 +240,31 @@ class DeleteFloatingIP(common.NetworkAndComputeDelete): return parser def take_action_network(self, client, parsed_args): - obj = client.find_ip(self.r, ignore_missing=False) + (obj, self.ip_cache) = _find_floating_ip( + self.app.client_manager.sdk_connection.session, + self.ip_cache, + self.r, + ignore_missing=False, + ) client.delete_ip(obj) def take_action_compute(self, client, parsed_args): obj = utils.find_resource(client.floating_ips, self.r) client.floating_ips.delete(obj.id) + def take_action(self, parsed_args): + """Implements a naive cache for the list of floating IPs""" + + # NOTE(dtroyer): This really only prevents multiple list() + # calls when performing multiple resource deletes + # in a single command. In an interactive session + # each delete command will call list(). + self.ip_cache = [] + super(DeleteFloatingIP, self).take_action(parsed_args) + class DeleteIPFloating(DeleteFloatingIP): - """Delete floating IP(s)""" + _description = _("Delete floating IP(s)") # TODO(tangchen): Remove this class and ``ip floating delete`` command # two cycles after Mitaka. @@ -195,23 +288,117 @@ class DeleteIPFloating(DeleteFloatingIP): class ListFloatingIP(common.NetworkAndComputeLister): - """List floating IP(s)""" + # TODO(songminglong): Use SDK resource mapped attribute names once + # the OSC minimum requirements include SDK 1.0 + _description = _("List floating IP(s)") + + def update_parser_network(self, parser): + parser.add_argument( + '--network', + metavar='<network>', + help=_("List floating IP(s) according to " + "given network (name or ID)") + ) + parser.add_argument( + '--port', + metavar='<port>', + help=_("List floating IP(s) according to " + "given port (name or ID)") + ) + parser.add_argument( + '--fixed-ip-address', + metavar='<fixed-ip-address>', + help=_("List floating IP(s) according to " + "given fixed IP address") + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + parser.add_argument( + '--status', + metavar='<status>', + choices=['ACTIVE', 'DOWN'], + help=_("List floating IP(s) according to " + "given status ('ACTIVE', 'DOWN')") + ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("List floating IP(s) according to " + "given project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--router', + metavar='<router>', + help=_("List floating IP(s) according to " + "given router (name or ID)") + ) + + return parser def take_action_network(self, client, parsed_args): + network_client = self.app.client_manager.network + identity_client = self.app.client_manager.identity + columns = ( 'id', 'floating_ip_address', 'fixed_ip_address', 'port_id', + 'floating_network_id', + 'project_id', ) headers = ( 'ID', 'Floating IP Address', 'Fixed IP Address', 'Port', + 'Floating Network', + 'Project', ) + if parsed_args.long: + columns = columns + ( + 'router_id', + 'status', + 'description', + ) + headers = headers + ( + 'Router', + 'Status', + 'Description', + ) query = {} + + if parsed_args.network is not None: + network = network_client.find_network(parsed_args.network, + ignore_missing=False) + query['floating_network_id'] = network.id + if parsed_args.port is not None: + port = network_client.find_port(parsed_args.port, + ignore_missing=False) + query['port_id'] = port.id + if parsed_args.fixed_ip_address is not None: + query['fixed_ip_address'] = parsed_args.fixed_ip_address + if parsed_args.status: + query['status'] = parsed_args.status + if parsed_args.project is not None: + project = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + query['tenant_id'] = project.id + query['project_id'] = project.id + if parsed_args.router is not None: + router = network_client.find_router(parsed_args.router, + ignore_missing=False) + query['router_id'] = router.id + data = client.ips(**query) return (headers, @@ -246,7 +433,7 @@ class ListFloatingIP(common.NetworkAndComputeLister): class ListIPFloating(ListFloatingIP): - """List floating IP(s)""" + _description = _("List floating IP(s)") # TODO(tangchen): Remove this class and ``ip floating list`` command # two cycles after Mitaka. @@ -270,7 +457,10 @@ class ListIPFloating(ListFloatingIP): class ShowFloatingIP(common.NetworkAndComputeShowOne): - """Display floating IP details""" + _description = _("Display floating IP details") + + # ip_cache is unused here but is a side effect of _find_floating_ip() + ip_cache = [] def update_parser_common(self, parser): parser.add_argument( @@ -281,10 +471,15 @@ class ShowFloatingIP(common.NetworkAndComputeShowOne): return parser def take_action_network(self, client, parsed_args): - obj = client.find_ip(parsed_args.floating_ip, ignore_missing=False) - columns = _get_columns(obj) + (obj, self.ip_cache) = _find_floating_ip( + self.app.client_manager.sdk_connection.session, + [], + parsed_args.floating_ip, + ignore_missing=False, + ) + display_columns, columns = _get_network_columns(obj) data = utils.get_item_properties(obj, columns) - return (columns, data) + return (display_columns, data) def take_action_compute(self, client, parsed_args): obj = utils.find_resource( @@ -297,7 +492,7 @@ class ShowFloatingIP(common.NetworkAndComputeShowOne): class ShowIPFloating(ShowFloatingIP): - """Display floating IP details""" + _description = _("Display floating IP details") # TODO(tangchen): Remove this class and ``ip floating show`` command # two cycles after Mitaka. diff --git a/openstackclient/network/v2/floating_ip_pool.py b/openstackclient/network/v2/floating_ip_pool.py index c78ca06a..73e94ead 100644 --- a/openstackclient/network/v2/floating_ip_pool.py +++ b/openstackclient/network/v2/floating_ip_pool.py @@ -23,7 +23,7 @@ from openstackclient.network import common class ListFloatingIPPool(common.NetworkAndComputeLister): - """List pools of floating IP addresses""" + _description = _("List pools of floating IP addresses") def take_action_network(self, client, parsed_args): msg = _("Floating ip pool operations are only available for " @@ -43,7 +43,7 @@ class ListFloatingIPPool(common.NetworkAndComputeLister): class ListIPFloatingPool(ListFloatingIPPool): - """List pools of floating IP addresses""" + _description = _("List pools of floating IP addresses") # TODO(tangchen): Remove this class and ``ip floating pool list`` command # two cycles after Mitaka. diff --git a/openstackclient/network/v2/ip_availability.py b/openstackclient/network/v2/ip_availability.py index 1d7b2aed..1d963580 100644 --- a/openstackclient/network/v2/ip_availability.py +++ b/openstackclient/network/v2/ip_availability.py @@ -18,7 +18,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common - +from openstackclient.network import sdk_utils _formatters = { 'subnet_ip_availability': utils.format_list_of_dicts, @@ -26,15 +26,16 @@ _formatters = { def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - return tuple(sorted(columns)) + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) +# TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once +# the OSC minimum requirements include SDK 1.0. class ListIPAvailability(command.Lister): - """List IP availability for network""" + _description = _("List IP availability for network") def get_parser(self, prog_name): parser = super(ListIPAvailability, self).get_parser(prog_name) @@ -84,6 +85,7 @@ class ListIPAvailability(command.Lister): parsed_args.project_domain, ).id filters['tenant_id'] = project_id + filters['project_id'] = project_id data = client.network_ip_availabilities(**filters) return (column_headers, (utils.get_item_properties( @@ -92,7 +94,7 @@ class ListIPAvailability(command.Lister): class ShowIPAvailability(command.ShowOne): - """Show network IP availability details""" + _description = _("Show network IP availability details") def get_parser(self, prog_name): parser = super(ShowIPAvailability, self).get_parser(prog_name) @@ -105,8 +107,10 @@ class ShowIPAvailability(command.ShowOne): def take_action(self, parsed_args): client = self.app.client_manager.network - obj = client.find_network_ip_availability(parsed_args.network, + network_id = client.find_network(parsed_args.network, + ignore_missing=False).id + obj = client.find_network_ip_availability(network_id, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index dbf1b601..3e0bb776 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -19,6 +19,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common +from openstackclient.network import sdk_utils def _format_admin_state(item): @@ -31,19 +32,38 @@ def _format_router_external(item): _formatters = { 'subnets': utils.format_list, + 'subnet_ids': utils.format_list, 'admin_state_up': _format_admin_state, + 'is_admin_state_up': _format_admin_state, 'router:external': _format_router_external, + 'is_router_external': _format_router_external, 'availability_zones': utils.format_list, 'availability_zone_hints': utils.format_list, } +def _get_network_columns(item): + column_map = { + 'subnet_ids': 'subnets', + 'is_admin_state_up': 'admin_state_up', + 'is_router_external': 'router:external', + 'is_port_security_enabled': 'port_security_enabled', + 'provider_network_type': 'provider:network_type', + 'provider_physical_network': 'provider:physical_network', + 'provider_segmentation_id': 'provider:segmentation_id', + 'is_shared': 'shared', + 'ipv4_address_scope_id': 'ipv4_address_scope', + 'ipv6_address_scope_id': 'ipv6_address_scope', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - return tuple(sorted(columns)) + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): @@ -71,7 +91,9 @@ def _get_attrs(client_manager, parsed_args): parsed_args.project, parsed_args.project_domain, ).id + # TODO(dtroyer): Remove tenant_id when we clean up the SDK refactor attrs['tenant_id'] = project_id + attrs['project_id'] = project_id # "network set" command doesn't support setting availability zone hints. if 'availability_zone_hints' in parsed_args and \ @@ -87,10 +109,10 @@ def _get_attrs(client_manager, parsed_args): attrs['router:external'] = False if parsed_args.external: attrs['router:external'] = True - if parsed_args.no_default: - attrs['is_default'] = False - if parsed_args.default: - attrs['is_default'] = True + if parsed_args.no_default: + attrs['is_default'] = False + if parsed_args.default: + attrs['is_default'] = True # Update Provider network options if parsed_args.provider_network_type: attrs['provider:network_type'] = parsed_args.provider_network_type @@ -98,6 +120,13 @@ def _get_attrs(client_manager, parsed_args): attrs['provider:physical_network'] = parsed_args.physical_network if parsed_args.segmentation_id: attrs['provider:segmentation_id'] = parsed_args.segmentation_id + if parsed_args.qos_policy is not None: + network_client = client_manager.network + _qos_policy = network_client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + attrs['qos_policy_id'] = _qos_policy.id + if 'no_qos_policy' in parsed_args and parsed_args.no_qos_policy: + attrs['qos_policy_id'] = None # Update VLAN Transparency for networks if parsed_args.transparent_vlan: attrs['vlan_transparent'] = True @@ -112,10 +141,8 @@ def _add_additional_network_options(parser): parser.add_argument( '--provider-network-type', metavar='<provider-network-type>', - choices=['flat', 'geneve', 'gre', 'local', - 'vlan', 'vxlan'], help=_("The physical mechanism by which the virtual network " - "is implemented. The supported options are: " + "is implemented. For example: " "flat, geneve, gre, local, vlan, vxlan.")) parser.add_argument( '--provider-physical-network', @@ -154,8 +181,10 @@ def _get_attrs_compute(client_manager, parsed_args): return attrs +# TODO(sindhu): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateNetwork(common.NetworkAndComputeShowOne): - """Create new network""" + _description = _("Create new network") def update_parser_common(self, parser): parser.add_argument( @@ -248,6 +277,11 @@ class CreateNetwork(common.NetworkAndComputeShowOne): help=_("Do not use the network as the default external network " "(default)") ) + parser.add_argument( + '--qos-policy', + metavar='<qos-policy>', + help=_("QoS policy to attach to this network (name or ID)") + ) _add_additional_network_options(parser) return parser @@ -262,20 +296,20 @@ class CreateNetwork(common.NetworkAndComputeShowOne): def take_action_network(self, client, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_network(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_network_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) def take_action_compute(self, client, parsed_args): attrs = _get_attrs_compute(self.app.client_manager, parsed_args) obj = client.networks.create(**attrs) - columns = _get_columns(obj._info) + display_columns, columns = _get_columns(obj._info) data = utils.get_dict_properties(obj._info, columns) - return (columns, data) + return (display_columns, data) class DeleteNetwork(common.NetworkAndComputeDelete): - """Delete network(s)""" + _description = _("Delete network(s)") # Used by base class to find resources in parsed_args. resource = 'network' @@ -300,10 +334,12 @@ class DeleteNetwork(common.NetworkAndComputeDelete): client.networks.delete(network.id) +# TODO(sindhu): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class ListNetwork(common.NetworkAndComputeLister): - """List networks""" + _description = _("List networks") - def update_parser_common(self, parser): + def update_parser_network(self, parser): router_ext_group = parser.add_mutually_exclusive_group() router_ext_group.add_argument( '--external', @@ -360,6 +396,33 @@ class ListNetwork(common.NetworkAndComputeLister): help=_("List networks according to their status " "('ACTIVE', 'BUILD', 'DOWN', 'ERROR')") ) + parser.add_argument( + '--provider-network-type', + metavar='<provider-network-type>', + choices=['flat', 'geneve', 'gre', 'local', + 'vlan', 'vxlan'], + help=_("List networks according to their physical mechanisms. " + "The supported options are: flat, geneve, gre, local, " + "vlan, vxlan.") + ) + parser.add_argument( + '--provider-physical-network', + metavar='<provider-physical-network>', + dest='physical_network', + help=_("List networks according to name of the physical network") + ) + parser.add_argument( + '--provider-segment', + metavar='<provider-segment>', + dest='segmentation_id', + help=_("List networks according to VLAN ID for VLAN networks " + "or Tunnel ID for GENEVE/GRE/VXLAN networks") + ) + parser.add_argument( + '--agent', + metavar='<agent-id>', + dest='agent_id', + help=_('List networks hosted by agent (ID only)')) return parser def take_action_network(self, client, parsed_args): @@ -369,12 +432,12 @@ class ListNetwork(common.NetworkAndComputeLister): 'id', 'name', 'status', - 'tenant_id', - 'admin_state_up', - 'shared', - 'subnets', + 'project_id', + 'is_admin_state_up', + 'is_shared', + 'subnet_ids', 'provider_network_type', - 'router:external', + 'is_router_external', 'availability_zones', ) column_headers = ( @@ -389,11 +452,31 @@ class ListNetwork(common.NetworkAndComputeLister): 'Router Type', 'Availability Zones', ) + elif parsed_args.agent_id: + columns = ( + 'id', + 'name', + 'subnet_ids' + ) + column_headers = ( + 'ID', + 'Name', + 'Subnets', + ) + client = self.app.client_manager.network + dhcp_agent = client.get_agent(parsed_args.agent_id) + data = client.dhcp_agent_hosting_networks(dhcp_agent) + + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters=_formatters, + ) for s in data)) else: columns = ( 'id', 'name', - 'subnets' + 'subnet_ids' ) column_headers = ( 'ID', @@ -405,16 +488,20 @@ class ListNetwork(common.NetworkAndComputeLister): if parsed_args.external: args['router:external'] = True + args['is_router_external'] = True elif parsed_args.internal: args['router:external'] = False + args['is_router_external'] = False if parsed_args.name is not None: args['name'] = parsed_args.name if parsed_args.enable: args['admin_state_up'] = True + args['is_admin_state_up'] = True elif parsed_args.disable: args['admin_state_up'] = False + args['is_admin_state_up'] = False if parsed_args.project: project = identity_common.find_project( @@ -423,15 +510,28 @@ class ListNetwork(common.NetworkAndComputeLister): parsed_args.project_domain, ) args['tenant_id'] = project.id + args['project_id'] = project.id if parsed_args.share: args['shared'] = True + args['is_shared'] = True elif parsed_args.no_share: args['shared'] = False + args['is_shared'] = False if parsed_args.status: args['status'] = parsed_args.status + if parsed_args.provider_network_type: + args['provider:network_type'] = parsed_args.provider_network_type + args['provider_network_type'] = parsed_args.provider_network_type + if parsed_args.physical_network: + args['provider:physical_network'] = parsed_args.physical_network + args['provider_physical_network'] = parsed_args.physical_network + if parsed_args.segmentation_id: + args['provider:segmentation_id'] = parsed_args.segmentation_id + args['provider_segmentation_id'] = parsed_args.segmentation_id + data = client.networks(**args) return (column_headers, @@ -461,8 +561,10 @@ class ListNetwork(common.NetworkAndComputeLister): ) for s in data)) +# TODO(sindhu): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetNetwork(command.Command): - """Set network properties""" + _description = _("Set network properties") def get_parser(self, prog_name): parser = super(SetNetwork, self).get_parser(prog_name) @@ -541,6 +643,17 @@ class SetNetwork(command.Command): action='store_true', help=_("Do not use the network as the default external network") ) + qos_group = parser.add_mutually_exclusive_group() + qos_group.add_argument( + '--qos-policy', + metavar='<qos-policy>', + help=_("QoS policy to attach to this network (name or ID)") + ) + qos_group.add_argument( + '--no-qos-policy', + action='store_true', + help=_("Remove the QoS policy attached to this network") + ) _add_additional_network_options(parser) return parser @@ -553,7 +666,7 @@ class SetNetwork(command.Command): class ShowNetwork(common.NetworkAndComputeShowOne): - """Show network details""" + _description = _("Show network details") def update_parser_common(self, parser): parser.add_argument( @@ -565,15 +678,15 @@ class ShowNetwork(common.NetworkAndComputeShowOne): def take_action_network(self, client, parsed_args): obj = client.find_network(parsed_args.network, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_network_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) def take_action_compute(self, client, parsed_args): obj = utils.find_resource( client.networks, parsed_args.network, ) - columns = _get_columns(obj._info) + display_columns, columns = _get_columns(obj._info) data = utils.get_dict_properties(obj._info, columns) - return (columns, data) + return (display_columns, data) diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index fdb34bb7..dd3fdc24 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -20,6 +20,7 @@ from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -28,15 +29,57 @@ LOG = logging.getLogger(__name__) def _format_admin_state(state): return 'UP' if state else 'DOWN' - _formatters = { 'admin_state_up': _format_admin_state, + 'is_admin_state_up': _format_admin_state, 'configurations': utils.format_dict, } +def _get_network_columns(item): + column_map = { + 'is_admin_state_up': 'admin_state_up', + 'is_alive': 'alive', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +class AddNetworkToAgent(command.Command): + _description = _("Add network to an agent") + + def get_parser(self, prog_name): + parser = super(AddNetworkToAgent, self).get_parser(prog_name) + parser.add_argument( + '--dhcp', + action='store_true', + help=_('Add network to a DHCP agent')) + parser.add_argument( + 'agent_id', + metavar='<agent-id>', + help=_('Agent to which a network is added (ID only)')) + parser.add_argument( + 'network', + metavar='<network>', + help=_('Network to be added to an agent (ID or name)')) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + agent = client.get_agent(parsed_args.agent_id) + network = client.find_network( + parsed_args.network, ignore_missing=False) + if parsed_args.dhcp: + try: + client.add_dhcp_agent_to_network(agent, network) + except Exception: + msg = 'Failed to add {} to {}'.format( + network.name, agent.agent_type) + exceptions.CommandError(msg) + + class DeleteNetworkAgent(command.Command): - """Delete network agent(s)""" + _description = _("Delete network agent(s)") def get_parser(self, prog_name): parser = super(DeleteNetworkAgent, self).get_parser(prog_name) @@ -69,8 +112,35 @@ class DeleteNetworkAgent(command.Command): raise exceptions.CommandError(msg) +# TODO(huanxuan): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class ListNetworkAgent(command.Lister): - """List network agents""" + _description = _("List network agents") + + def get_parser(self, prog_name): + parser = super(ListNetworkAgent, self).get_parser(prog_name) + parser.add_argument( + '--agent-type', + metavar='<agent-type>', + choices=["bgp", "dhcp", "open-vswitch", "linux-bridge", "ofa", + "l3", "loadbalancer", "metering", "metadata", "macvtap", + "nic"], + help=_("List only agents with the specified agent type. " + "The supported agent types are: bgp, dhcp, open-vswitch, " + "linux-bridge, ofa, l3, loadbalancer, metering, " + "metadata, macvtap, nic.") + ) + parser.add_argument( + '--host', + metavar='<host>', + help=_("List only agents running on the specified host") + ) + parser.add_argument( + '--network', + metavar='<network>', + help=_('List agents hosting a network (name or ID)') + ) + return parser def take_action(self, parsed_args): client = self.app.client_manager.network @@ -79,8 +149,8 @@ class ListNetworkAgent(command.Lister): 'agent_type', 'host', 'availability_zone', - 'alive', - 'admin_state_up', + 'is_alive', + 'is_admin_state_up', 'binary' ) column_headers = ( @@ -92,15 +162,94 @@ class ListNetworkAgent(command.Lister): 'State', 'Binary' ) - data = client.agents() - return (column_headers, - (utils.get_item_properties( - s, columns, formatters=_formatters, - ) for s in data)) + + key_value = { + 'bgp': 'BGP dynamic routing agent', + 'dhcp': 'DHCP agent', + 'open-vswitch': 'Open vSwitch agent', + 'linux-bridge': 'Linux bridge agent', + 'ofa': 'OFA driver agent', + 'l3': 'L3 agent', + 'loadbalancer': 'Loadbalancer agent', + 'metering': 'Metering agent', + 'metadata': 'Metadata agent', + 'macvtap': 'Macvtap agent', + 'nic': 'NIC Switch agent' + } + + filters = {} + if parsed_args.network is not None: + columns = ( + 'id', + 'host', + 'is_admin_state_up', + 'is_alive', + ) + column_headers = ( + 'ID', + 'Host', + 'Admin State Up', + 'Alive', + ) + network = client.find_network( + parsed_args.network, ignore_missing=False) + data = client.network_hosting_dhcp_agents(network) + + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters=_formatters, + ) for s in data)) + else: + if parsed_args.agent_type is not None: + filters['agent_type'] = key_value[parsed_args.agent_type] + if parsed_args.host is not None: + filters['host'] = parsed_args.host + + data = client.agents(**filters) + return (column_headers, + (utils.get_item_properties( + s, columns, formatters=_formatters, + ) for s in data)) + + +class RemoveNetworkFromAgent(command.Command): + _description = _("Remove network from an agent.") + + def get_parser(self, prog_name): + parser = super(RemoveNetworkFromAgent, self).get_parser(prog_name) + parser.add_argument( + '--dhcp', + action='store_true', + help=_('Remove network from DHCP agent')) + parser.add_argument( + 'agent_id', + metavar='<agent-id>', + help=_('Agent to which a network is removed (ID only)')) + parser.add_argument( + 'network', + metavar='<network>', + help=_('Network to be removed from an agent (ID or name)')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + agent = client.get_agent(parsed_args.agent_id) + network = client.find_network( + parsed_args.network, ignore_missing=False) + if parsed_args.dhcp: + try: + client.remove_dhcp_agent_from_network(agent, network) + except Exception: + msg = 'Failed to remove {} to {}'.format( + network.name, agent.agent_type) + exceptions.CommandError(msg) +# TODO(huanxuan): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetNetworkAgent(command.Command): - """Set network agent properties""" + _description = _("Set network agent properties") def get_parser(self, prog_name): parser = super(SetNetworkAgent, self).get_parser(prog_name) @@ -129,10 +278,12 @@ class SetNetworkAgent(command.Command): def take_action(self, parsed_args): client = self.app.client_manager.network - obj = client.get_agent(parsed_args.network_agent, ignore_missing=False) + obj = client.get_agent(parsed_args.network_agent) attrs = {} if parsed_args.description is not None: attrs['description'] = str(parsed_args.description) + # TODO(huanxuan): Also update by the new attribute name + # "is_admin_state_up" after sdk 0.9.12 if parsed_args.enable: attrs['admin_state_up'] = True if parsed_args.disable: @@ -141,7 +292,7 @@ class SetNetworkAgent(command.Command): class ShowNetworkAgent(command.ShowOne): - """Display network agent details""" + _description = _("Display network agent details") def get_parser(self, prog_name): parser = super(ShowNetworkAgent, self).get_parser(prog_name) @@ -154,7 +305,7 @@ class ShowNetworkAgent(command.ShowOne): def take_action(self, parsed_args): client = self.app.client_manager.network - obj = client.get_agent(parsed_args.network_agent, ignore_missing=False) - columns = tuple(sorted(list(obj.keys()))) + obj = client.get_agent(parsed_args.network_agent) + display_columns, columns = _get_network_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters,) - return columns, data + return display_columns, data diff --git a/openstackclient/network/v2/network_auto_allocated_topology.py b/openstackclient/network/v2/network_auto_allocated_topology.py new file mode 100644 index 00000000..36f39200 --- /dev/null +++ b/openstackclient/network/v2/network_auto_allocated_topology.py @@ -0,0 +1,136 @@ +# 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. +# + +"""Auto-allocated Topology Implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +def _format_check_resource_columns(): + return ('dry_run',) + + +def _format_check_resource(item): + item_id = getattr(item, 'id', False) + if item_id == 'dry-run=pass': + item.check_resource = 'pass' + return item + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + if parsed_args.project: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + if parsed_args.check_resources: + attrs['check_resources'] = True + + return attrs + + +# TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class CreateAutoAllocatedTopology(command.ShowOne): + _description = _("Create the auto allocated topology for project") + + def get_parser(self, prog_name): + parser = super(CreateAutoAllocatedTopology, self).get_parser(prog_name) + parser.add_argument( + '--project', + metavar='<project>', + help=_("Return the auto allocated topology for a given project. " + "Default is current project") + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--check-resources', + action='store_true', + help=_("Validate the requirements for auto allocated topology. " + "Does not return a topology.") + ) + parser.add_argument( + '--or-show', + action='store_true', + default=True, + help=_("If topology exists returns the topology's " + "information (Default)") + ) + + return parser + + def check_resource_topology(self, client, parsed_args): + obj = client.validate_auto_allocated_topology(parsed_args.project) + + columns = _format_check_resource_columns() + data = utils.get_item_properties(_format_check_resource(obj), + columns, + formatters={}) + + return (columns, data) + + def get_topology(self, client, parsed_args): + obj = client.get_auto_allocated_topology(parsed_args.project) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + return (display_columns, data) + + def take_action(self, parsed_args): + client = self.app.client_manager.network + if parsed_args.check_resources: + columns, data = self.check_resource_topology(client, parsed_args) + else: + columns, data = self.get_topology(client, parsed_args) + return (columns, data) + + +# TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class DeleteAutoAllocatedTopology(command.Command): + _description = _("Delete auto allocated topology for project") + + def get_parser(self, prog_name): + parser = super(DeleteAutoAllocatedTopology, self).get_parser(prog_name) + parser.add_argument( + '--project', + metavar='<project>', + help=_('Delete auto allocated topology for a given project. ' + 'Default is the current project') + ) + identity_common.add_project_domain_option_to_parser(parser) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + client.delete_auto_allocated_topology(parsed_args.project) diff --git a/openstackclient/network/v2/network_flavor.py b/openstackclient/network/v2/network_flavor.py new file mode 100644 index 00000000..3a3324c0 --- /dev/null +++ b/openstackclient/network/v2/network_flavor.py @@ -0,0 +1,247 @@ +# 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. +# + +"""Flavor action implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils + + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = { + 'is_enabled': 'enabled', + 'tenant_id': 'project_id', + } + + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + attrs['name'] = parsed_args.name + attrs['service_type'] = parsed_args.service_type + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if parsed_args.enable: + attrs['enabled'] = True + if parsed_args.disable: + attrs['enabled'] = False + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + return attrs + + +# TODO(dasanind): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class CreateNetworkFlavor(command.ShowOne): + _description = _("Create new network flavor") + + def get_parser(self, prog_name): + parser = super(CreateNetworkFlavor, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar="<name>", + help=_("Name for the flavor") + ) + parser.add_argument( + '--service-type', + metavar="<service-type>", + required=True, + help=_('Service type to which the flavor applies to: e.g. VPN ' + '(See openstack network service provider list for loaded ' + 'examples.)') + ) + parser.add_argument( + '--description', + help=_('Description for the flavor') + ) + parser.add_argument( + '--project', + metavar="<project>", + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--enable', + action='store_true', + help=_("Enable the flavor (default)") + ) + enable_group.add_argument( + '--disable', + action='store_true', + help=_("Disable the flavor") + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_flavor(**attrs) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return (display_columns, data) + + +class DeleteNetworkFlavor(command.Command): + _description = _("Delete network flavors") + + def get_parser(self, prog_name): + parser = super(DeleteNetworkFlavor, self).get_parser(prog_name) + + parser.add_argument( + 'flavor', + metavar='<flavor>', + nargs='+', + help=_('Flavor(s) to delete (name or ID)') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + + for flavor in parsed_args.flavor: + try: + obj = client.find_flavor(flavor, ignore_missing=False) + client.delete_flavor(obj) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete flavor with " + "name or ID '%(flavor)s': %(e)s"), + {"flavor": flavor, "e": e}) + if result > 0: + total = len(parsed_args.flavor) + msg = (_("%(result)s of %(total)s flavors failed " + "to delete.") % {"result": result, "total": total}) + raise exceptions.CommandError(msg) + + +class ListNetworkFlavor(command.Lister): + _description = _("List network flavors") + + def take_action(self, parsed_args): + client = self.app.client_manager.network + + columns = ( + 'id', + 'name', + 'is_enabled', + 'service_type', + 'description' + ) + column_headers = ( + 'ID', + 'Name', + 'Enabled', + 'Service Type', + 'Description' + ) + + data = client.flavors() + return (column_headers, + (utils.get_item_properties( + s, columns, + ) for s in data)) + + +# TODO(dasanind): Use only the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class SetNetworkFlavor(command.Command): + _description = _("Set network flavor properties") + + def get_parser(self, prog_name): + parser = super(SetNetworkFlavor, self).get_parser(prog_name) + parser.add_argument( + 'flavor', + metavar="<flavor>", + help=_("Flavor to update (name or ID)") + ) + parser.add_argument( + '--description', + help=_('Set network flavor description') + ) + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--disable', + action='store_true', + help=_("Disable network flavor") + ) + enable_group.add_argument( + '--enable', + action='store_true', + help=_("Enable network flavor") + ) + parser.add_argument( + '--name', + metavar="<name>", + help=_('Set flavor name') + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_flavor( + parsed_args.flavor, + ignore_missing=False) + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if parsed_args.enable: + attrs['enabled'] = True + if parsed_args.disable: + attrs['enabled'] = False + client.update_flavor(obj, **attrs) + + +class ShowNetworkFlavor(command.ShowOne): + _description = _("Display network flavor details") + + def get_parser(self, prog_name): + parser = super(ShowNetworkFlavor, self).get_parser(prog_name) + parser.add_argument( + 'flavor', + metavar='<flavor>', + help=_('Flavor to display (name or ID)') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_flavor(parsed_args.flavor, ignore_missing=False) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return display_columns, data diff --git a/openstackclient/network/v2/network_flavor_profile.py b/openstackclient/network/v2/network_flavor_profile.py new file mode 100644 index 00000000..6cf0c412 --- /dev/null +++ b/openstackclient/network/v2/network_flavor_profile.py @@ -0,0 +1,250 @@ +# 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 + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils + + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = { + 'is_enabled': 'enabled', + 'tenant_id': 'project_id', + } + + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if parsed_args.driver is not None: + attrs['driver'] = parsed_args.driver + if parsed_args.metainfo is not None: + attrs['metainfo'] = parsed_args.metainfo + if parsed_args.enable: + attrs['enabled'] = True + if parsed_args.disable: + attrs['enabled'] = False + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + return attrs + + +# TODO(ndahiwade): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class CreateNetworkFlavorProfile(command.ShowOne): + _description = _("Create new network flavor profile") + + def get_parser(self, prog_name): + parser = super(CreateNetworkFlavorProfile, self).get_parser(prog_name) + parser.add_argument( + '--project', + metavar="<project>", + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--description', + metavar="<description>", + help=_("Description for the flavor profile") + ) + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--enable', + action='store_true', + help=_("Enable the flavor profile") + ) + enable_group.add_argument( + '--disable', + action='store_true', + help=_("Disable the flavor profile") + ) + parser.add_argument( + '--driver', + help=_("Python module path to driver. This becomes " + "required if --metainfo is missing and vice versa") + ) + parser.add_argument( + '--metainfo', + help=_("Metainfo for the flavor profile. This becomes " + "required if --driver is missing and vice versa") + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + + if parsed_args.driver is None and parsed_args.metainfo is None: + msg = _("Either --driver or --metainfo or both are required") + raise exceptions.CommandError(msg) + + obj = client.create_service_profile(**attrs) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return (display_columns, data) + + +class DeleteNetworkFlavorProfile(command.Command): + _description = _("Delete network flavor profile") + + def get_parser(self, prog_name): + parser = super(DeleteNetworkFlavorProfile, self).get_parser(prog_name) + + parser.add_argument( + 'flavor_profile', + metavar='<flavor-profile>', + nargs='+', + help=_("Flavor profile(s) to delete (ID only)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + + for flavor_profile in parsed_args.flavor_profile: + try: + obj = client.find_service_profile(flavor_profile, + ignore_missing=False) + client.delete_service_profile(obj) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete flavor profile with " + "ID '%(flavor_profile)s': %(e)s"), + {"flavor_profile": flavor_profile, "e": e}) + if result > 0: + total = len(parsed_args.flavor_profile) + msg = (_("%(result)s of %(total)s flavor_profiles failed " + "to delete.") % {"result": result, "total": total}) + raise exceptions.CommandError(msg) + + +class ListNetworkFlavorProfile(command.Lister): + _description = _("List network flavor profile(s)") + + def take_action(self, parsed_args): + client = self.app.client_manager.network + + columns = ( + 'id', + 'driver', + 'is_enabled', + 'metainfo', + 'description', + ) + column_headers = ( + 'ID', + 'Driver', + 'Enabled', + 'Metainfo', + 'Description', + ) + + data = client.service_profiles() + return (column_headers, + (utils.get_item_properties( + s, columns, + ) for s in data)) + + +# TODO(ndahiwade): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class SetNetworkFlavorProfile(command.Command): + _description = _("Set network flavor profile properties") + + def get_parser(self, prog_name): + parser = super(SetNetworkFlavorProfile, self).get_parser(prog_name) + parser.add_argument( + 'flavor_profile', + metavar="<flavor-profile>", + help=_("Flavor profile to update (ID only)") + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--description', + metavar="<description>", + help=_("Description for the flavor profile") + ) + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--enable', + action='store_true', + help=_("Enable the flavor profile") + ) + enable_group.add_argument( + '--disable', + action='store_true', + help=_("Disable the flavor profile") + ) + parser.add_argument( + '--driver', + help=_("Python module path to driver. This becomes " + "required if --metainfo is missing and vice versa") + ) + parser.add_argument( + '--metainfo', + help=_("Metainfo for the flavor profile. This becomes " + "required if --driver is missing and vice versa") + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_service_profile(parsed_args.flavor_profile, + ignore_missing=False) + attrs = _get_attrs(self.app.client_manager, parsed_args) + + client.update_service_profile(obj, **attrs) + + +class ShowNetworkFlavorProfile(command.ShowOne): + _description = _("Display network flavor profile details") + + def get_parser(self, prog_name): + parser = super(ShowNetworkFlavorProfile, self).get_parser(prog_name) + parser.add_argument( + 'flavor_profile', + metavar='<flavor-profile>', + help=_("Flavor profile to display (ID only)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_service_profile(parsed_args.flavor_profile, + ignore_missing=False) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return (display_columns, data) diff --git a/openstackclient/network/v2/network_meter.py b/openstackclient/network/v2/network_meter.py new file mode 100644 index 00000000..df0e1da1 --- /dev/null +++ b/openstackclient/network/v2/network_meter.py @@ -0,0 +1,190 @@ +# 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. +# + +"""Metering Label Implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = { + 'is_shared': 'shared', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if parsed_args.project is not None and 'project' in parsed_args: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + if parsed_args.share: + attrs['shared'] = True + if parsed_args.no_share: + attrs['shared'] = False + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + + return attrs + + +# TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class CreateMeter(command.ShowOne): + _description = _("Create network meter") + + def get_parser(self, prog_name): + parser = super(CreateMeter, self).get_parser(prog_name) + + parser.add_argument( + '--description', + metavar='<description>', + help=_("Create description for meter") + ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("Owner's project (name or ID)") + ) + + identity_common.add_project_domain_option_to_parser(parser) + share_group = parser.add_mutually_exclusive_group() + share_group.add_argument( + '--share', + action='store_true', + default=None, + help=_("Share meter between projects") + ) + share_group.add_argument( + '--no-share', + action='store_true', + help=_("Do not share meter between projects") + ) + parser.add_argument( + 'name', + metavar='<name>', + help=_('Name of meter'), + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_metering_label(**attrs) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return (display_columns, data) + + +# TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class DeleteMeter(command.Command): + _description = _("Delete network meter") + + def get_parser(self, prog_name): + parser = super(DeleteMeter, self).get_parser(prog_name) + + parser.add_argument( + 'meter', + metavar='<meter>', + nargs='+', + help=_('Meter to delete (name or ID)') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + + for meter in parsed_args.meter: + try: + obj = client.find_metering_label(meter, ignore_missing=False) + client.delete_metering_label(obj) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete meter with " + "ID '%(meter)s': %(e)s"), + {"meter": meter, "e": e}) + if result > 0: + total = len(parsed_args.meter) + msg = (_("%(result)s of %(total)s meters failed " + "to delete.") % {"result": result, "total": total}) + raise exceptions.CommandError(msg) + + +class ListMeter(command.Lister): + _description = _("List network meters") + + def take_action(self, parsed_args): + client = self.app.client_manager.network + + columns = ( + 'id', + 'name', + 'description', + 'shared', + ) + column_headers = ( + 'ID', + 'Name', + 'Description', + 'Shared', + ) + + data = client.metering_labels() + return (column_headers, + (utils.get_item_properties( + s, columns, + ) for s in data)) + + +class ShowMeter(command.ShowOne): + _description = _("Show network meter") + + def get_parser(self, prog_name): + parser = super(ShowMeter, self).get_parser(prog_name) + parser.add_argument( + 'meter', + metavar='<meter>', + help=_('Meter to display (name or ID)') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_metering_label(parsed_args.meter, + ignore_missing=False) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return display_columns, data diff --git a/openstackclient/network/v2/network_meter_rule.py b/openstackclient/network/v2/network_meter_rule.py new file mode 100644 index 00000000..49ff9e1b --- /dev/null +++ b/openstackclient/network/v2/network_meter_rule.py @@ -0,0 +1,203 @@ +# 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. +# + +"""Meter Rule Implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + + if parsed_args.exclude: + attrs['excluded'] = True + if parsed_args.include: + attrs['excluded'] = False + if parsed_args.ingress or not parsed_args.egress: + attrs['direction'] = 'ingress' + if parsed_args.egress: + attrs['direction'] = 'egress' + if parsed_args.remote_ip_prefix is not None: + attrs['remote_ip_prefix'] = parsed_args.remote_ip_prefix + if parsed_args.meter is not None: + attrs['metering_label_id'] = parsed_args.meter + if parsed_args.project is not None: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + return attrs + + +class CreateMeterRule(command.ShowOne): + _description = _("Create a new meter rule") + + def get_parser(self, prog_name): + parser = super(CreateMeterRule, self).get_parser(prog_name) + + parser.add_argument( + '--project', + metavar='<project>', + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + exclude_group = parser.add_mutually_exclusive_group() + exclude_group.add_argument( + '--exclude', + action='store_true', + help=_("Exclude remote IP prefix from traffic count") + ) + exclude_group.add_argument( + '--include', + action='store_true', + help=_("Include remote IP prefix from traffic count (default)") + ) + direction_group = parser.add_mutually_exclusive_group() + direction_group.add_argument( + '--ingress', + action='store_true', + help=_("Apply rule to incoming network traffic (default)") + ) + direction_group.add_argument( + '--egress', + action='store_true', + help=_('Apply rule to outgoing network traffic') + ) + parser.add_argument( + '--remote-ip-prefix', + metavar='<remote-ip-prefix>', + required=True, + help=_('The remote IP prefix to associate with this rule'), + ) + parser.add_argument( + 'meter', + metavar='<meter>', + help=_('Label to associate with this metering rule (name or ID)'), + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + _meter = client.find_metering_label(parsed_args.meter, + ignore_missing=False) + parsed_args.meter = _meter.id + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_metering_label_rule(**attrs) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + + return (display_columns, data) + + +class DeleteMeterRule(command.Command): + _description = _("Delete meter rule(s)") + + def get_parser(self, prog_name): + parser = super(DeleteMeterRule, self).get_parser(prog_name) + + parser.add_argument( + 'meter_rule_id', + metavar='<meter-rule-id>', + nargs='+', + help=_('Meter rule to delete (ID only)') + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + + for id in parsed_args.meter_rule_id: + try: + obj = client.find_metering_label_rule(id, ignore_missing=False) + client.delete_metering_label_rule(obj) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete meter rule with " + "ID '%(id)s': %(e)s"), + {"id": id, "e": e}) + + if result > 0: + total = len(parsed_args.meter_rule_id) + msg = (_("%(result)s of %(total)s meter rules failed " + "to delete.") % {"result": result, "total": total}) + raise exceptions.CommandError(msg) + + +class ListMeterRule(command.Lister): + _description = _("List meter rules") + + def take_action(self, parsed_args): + client = self.app.client_manager.network + + columns = ( + 'id', + 'excluded', + 'direction', + 'remote_ip_prefix', + ) + column_headers = ( + 'ID', + 'Excluded', + 'Direction', + 'Remote IP Prefix', + ) + data = client.metering_label_rules() + return (column_headers, + (utils.get_item_properties( + s, columns, + ) for s in data)) + + +class ShowMeterRule(command.ShowOne): + _description = _("Display meter rules details") + + def get_parser(self, prog_name): + parser = super(ShowMeterRule, self).get_parser(prog_name) + parser.add_argument( + 'meter_rule_id', + metavar='<meter-rule-id>', + help=_('Meter rule (ID only)') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_metering_label_rule(parsed_args.meter_rule_id, + ignore_missing=False) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return display_columns, data diff --git a/openstackclient/network/v2/network_qos_policy.py b/openstackclient/network/v2/network_qos_policy.py new file mode 100644 index 00000000..fef3ec88 --- /dev/null +++ b/openstackclient/network/v2/network_qos_policy.py @@ -0,0 +1,259 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils + + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + column_map = { + 'is_shared': 'shared', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + if 'name' in parsed_args and parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + if 'description' in parsed_args and parsed_args.description is not None: + attrs['description'] = parsed_args.description + if parsed_args.share: + attrs['shared'] = True + if parsed_args.no_share: + attrs['shared'] = False + if parsed_args.project is not None: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + return attrs + + +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class CreateNetworkQosPolicy(command.ShowOne): + _description = _("Create a QoS policy") + + def get_parser(self, prog_name): + parser = super(CreateNetworkQosPolicy, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='<name>', + help=_("Name of QoS policy to create") + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_("Description of the QoS policy") + ) + share_group = parser.add_mutually_exclusive_group() + share_group.add_argument( + '--share', + action='store_true', + default=None, + help=_("Make the QoS policy accessible by other projects") + ) + share_group.add_argument( + '--no-share', + action='store_true', + help=_("Make the QoS policy not accessible by other projects " + "(default)") + ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("Owner's project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_qos_policy(**attrs) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters={}) + return (display_columns, data) + + +class DeleteNetworkQosPolicy(command.Command): + _description = _("Delete Qos Policy(s)") + + def get_parser(self, prog_name): + parser = super(DeleteNetworkQosPolicy, self).get_parser(prog_name) + parser.add_argument( + 'policy', + metavar="<qos-policy>", + nargs="+", + help=_("QoS policy(s) to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + result = 0 + + for policy in parsed_args.policy: + try: + obj = client.find_qos_policy(policy, ignore_missing=False) + client.delete_qos_policy(obj) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete QoS policy " + "name or ID '%(qos_policy)s': %(e)s"), + {'qos_policy': policy, 'e': e}) + + if result > 0: + total = len(parsed_args.policy) + msg = (_("%(result)s of %(total)s QoS policies failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +# TODO(abhiraut): Use only the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class ListNetworkQosPolicy(command.Lister): + _description = _("List QoS policies") + + def get_parser(self, prog_name): + parser = super(ListNetworkQosPolicy, self).get_parser(prog_name) + parser.add_argument( + '--project', + metavar='<project>', + help=_("List qos policies according to their project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + shared_group = parser.add_mutually_exclusive_group() + shared_group.add_argument( + '--share', + action='store_true', + help=_("List qos policies shared between projects") + ) + shared_group.add_argument( + '--no-share', + action='store_true', + help=_("List qos policies not shared between projects") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + columns = ( + 'id', + 'name', + 'is_shared', + 'project_id', + ) + column_headers = ( + 'ID', + 'Name', + 'Shared', + 'Project', + ) + attrs = _get_attrs(self.app.client_manager, parsed_args) + data = client.qos_policies(**attrs) + return (column_headers, + (utils.get_item_properties( + s, columns, formatters={}, + ) for s in data)) + + +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +class SetNetworkQosPolicy(command.Command): + _description = _("Set QoS policy properties") + + def get_parser(self, prog_name): + parser = super(SetNetworkQosPolicy, self).get_parser(prog_name) + parser.add_argument( + 'policy', + metavar="<qos-policy>", + help=_("QoS policy to modify (name or ID)") + ) + parser.add_argument( + '--name', + metavar="<name>", + help=_('Set QoS policy name') + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_("Description of the QoS policy") + ) + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--share', + action='store_true', + help=_('Make the QoS policy accessible by other projects'), + ) + enable_group.add_argument( + '--no-share', + action='store_true', + help=_('Make the QoS policy not accessible by other projects'), + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_qos_policy( + parsed_args.policy, + ignore_missing=False) + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.share: + attrs['shared'] = True + if parsed_args.no_share: + attrs['shared'] = False + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + client.update_qos_policy(obj, **attrs) + + +class ShowNetworkQosPolicy(command.ShowOne): + _description = _("Display QoS policy details") + + def get_parser(self, prog_name): + parser = super(ShowNetworkQosPolicy, self).get_parser(prog_name) + parser.add_argument( + 'policy', + metavar="<qos-policy>", + help=_("QoS policy to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_qos_policy(parsed_args.policy, + ignore_missing=False) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return (display_columns, data) diff --git a/openstackclient/network/v2/network_qos_rule.py b/openstackclient/network/v2/network_qos_rule.py new file mode 100644 index 00000000..baed0424 --- /dev/null +++ b/openstackclient/network/v2/network_qos_rule.py @@ -0,0 +1,353 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 itertools +import six + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ +from openstackclient.network import sdk_utils + + +RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth-limit' +RULE_TYPE_DSCP_MARKING = 'dscp-marking' +RULE_TYPE_MINIMUM_BANDWIDTH = 'minimum-bandwidth' +REQUIRED_PARAMETERS = { + RULE_TYPE_MINIMUM_BANDWIDTH: ['min_kbps', 'direction'], + RULE_TYPE_DSCP_MARKING: ['dscp_mark'], + RULE_TYPE_BANDWIDTH_LIMIT: ['max_kbps', 'max_burst_kbps']} +DIRECTION_EGRESS = 'egress' +DIRECTION_INGRESS = 'ingress' +DSCP_VALID_MARKS = [0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, + 34, 36, 38, 40, 46, 48, 56] + +ACTION_CREATE = 'create' +ACTION_DELETE = 'delete' +ACTION_FIND = 'find' +ACTION_SET = 'update' +ACTION_SHOW = 'get' + + +def _get_columns(item): + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + + +def _check_type_parameters(attrs, type, is_create): + req_params = REQUIRED_PARAMETERS[type] + notreq_params = list(itertools.chain( + *[v for k, v in six.iteritems(REQUIRED_PARAMETERS) if k != type])) + if is_create and None in map(attrs.get, req_params): + msg = (_('"Create" rule command for type "%(rule_type)s" requires ' + 'arguments %(args)s') % {'rule_type': type, + 'args': ", ".join(req_params)}) + raise exceptions.CommandError(msg) + if set(six.iterkeys(attrs)) & set(notreq_params): + msg = (_('Rule type "%(rule_type)s" only requires arguments %(args)s') + % {'rule_type': type, 'args': ", ".join(req_params)}) + raise exceptions.CommandError(msg) + + +def _get_attrs(network_client, parsed_args, is_create=False): + attrs = {} + qos = network_client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + attrs['qos_policy_id'] = qos.id + if not is_create: + attrs['id'] = parsed_args.id + rule_type = _find_rule_type(qos, parsed_args.id) + if not rule_type: + msg = (_('Rule ID %(rule_id)s not found') % + {'rule_id': parsed_args.id}) + raise exceptions.CommandError(msg) + else: + if not parsed_args.type: + msg = _('"Create" rule command requires argument "type"') + raise exceptions.CommandError(msg) + rule_type = parsed_args.type + if parsed_args.max_kbps is not None: + attrs['max_kbps'] = parsed_args.max_kbps + if parsed_args.max_burst_kbits is not None: + # NOTE(ralonsoh): this parameter must be changed in SDK and then in + # Neutron API, from 'max_burst_kbps' to + # 'max_burst_kbits' + attrs['max_burst_kbps'] = parsed_args.max_burst_kbits + if parsed_args.dscp_mark is not None: + attrs['dscp_mark'] = parsed_args.dscp_mark + if parsed_args.min_kbps is not None: + attrs['min_kbps'] = parsed_args.min_kbps + if parsed_args.ingress: + attrs['direction'] = 'ingress' + if parsed_args.egress: + attrs['direction'] = 'egress' + _check_type_parameters(attrs, rule_type, is_create) + return attrs + + +def _get_item_properties(item, fields): + """Return a tuple containing the item properties.""" + row = [] + for field in fields: + row.append(item.get(field, '')) + return tuple(row) + + +def _rule_action_call(client, action, rule_type): + rule_type = rule_type.replace('-', '_') + func_name = '%(action)s_qos_%(rule_type)s_rule' % {'action': action, + 'rule_type': rule_type} + return getattr(client, func_name) + + +def _find_rule_type(qos, rule_id): + for rule in (r for r in qos.rules if r['id'] == rule_id): + return rule['type'].replace('_', '-') + return None + + +def _add_rule_arguments(parser): + parser.add_argument( + '--max-kbps', + dest='max_kbps', + metavar='<max-kbps>', + type=int, + help=_('Maximum bandwidth in kbps') + ) + parser.add_argument( + '--max-burst-kbits', + dest='max_burst_kbits', + metavar='<max-burst-kbits>', + type=int, + help=_('Maximum burst in kilobits, 0 means automatic') + ) + parser.add_argument( + '--dscp-mark', + dest='dscp_mark', + metavar='<dscp-mark>', + type=int, + help=_('DSCP mark: value can be 0, even numbers from 8-56, ' + 'excluding 42, 44, 50, 52, and 54') + ) + parser.add_argument( + '--min-kbps', + dest='min_kbps', + metavar='<min-kbps>', + type=int, + help=_('Minimum guaranteed bandwidth in kbps') + ) + direction_group = parser.add_mutually_exclusive_group() + direction_group.add_argument( + '--ingress', + action='store_true', + help=_("Ingress traffic direction from the project point of view") + ) + direction_group.add_argument( + '--egress', + action='store_true', + help=_("Egress traffic direction from the project point of view") + ) + + +class CreateNetworkQosRule(command.ShowOne): + _description = _("Create new Network QoS rule") + + def get_parser(self, prog_name): + parser = super(CreateNetworkQosRule, self).get_parser( + prog_name) + parser.add_argument( + 'qos_policy', + metavar='<qos-policy>', + help=_('QoS policy that contains the rule (name or ID)') + ) + parser.add_argument( + '--type', + metavar='<type>', + choices=[RULE_TYPE_MINIMUM_BANDWIDTH, + RULE_TYPE_DSCP_MARKING, + RULE_TYPE_BANDWIDTH_LIMIT], + help=(_('QoS rule type (%s)') % + ", ".join(six.iterkeys(REQUIRED_PARAMETERS))) + ) + _add_rule_arguments(parser) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + attrs = _get_attrs(network_client, parsed_args, is_create=True) + try: + obj = _rule_action_call( + network_client, ACTION_CREATE, parsed_args.type)( + attrs.pop('qos_policy_id'), **attrs) + except Exception as e: + msg = (_('Failed to create Network QoS rule: %(e)s') % {'e': e}) + raise exceptions.CommandError(msg) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return display_columns, data + + +class DeleteNetworkQosRule(command.Command): + _description = _("Delete Network QoS rule") + + def get_parser(self, prog_name): + parser = super(DeleteNetworkQosRule, self).get_parser(prog_name) + parser.add_argument( + 'qos_policy', + metavar='<qos-policy>', + help=_('QoS policy that contains the rule (name or ID)') + ) + parser.add_argument( + 'id', + metavar='<rule-id>', + help=_('Network QoS rule to delete (ID)') + ) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + rule_id = parsed_args.id + try: + qos = network_client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + rule_type = _find_rule_type(qos, rule_id) + if not rule_type: + raise Exception('Rule %s not found' % rule_id) + _rule_action_call(network_client, ACTION_DELETE, rule_type)( + rule_id, qos.id) + except Exception as e: + msg = (_('Failed to delete Network QoS rule ID "%(rule)s": %(e)s') + % {'rule': rule_id, 'e': e}) + raise exceptions.CommandError(msg) + + +class ListNetworkQosRule(command.Lister): + _description = _("List Network QoS rules") + + def get_parser(self, prog_name): + parser = super(ListNetworkQosRule, self).get_parser(prog_name) + parser.add_argument( + 'qos_policy', + metavar='<qos-policy>', + help=_('QoS policy that contains the rule (name or ID)') + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + columns = ( + 'id', + 'qos_policy_id', + 'type', + 'max_kbps', + 'max_burst_kbps', + 'min_kbps', + 'dscp_mark', + 'direction', + ) + column_headers = ( + 'ID', + 'QoS Policy ID', + 'Type', + 'Max Kbps', + 'Max Burst Kbits', + 'Min Kbps', + 'DSCP mark', + 'Direction', + ) + qos = client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + data = qos.rules + return (column_headers, + (_get_item_properties(s, columns) for s in data)) + + +class SetNetworkQosRule(command.Command): + _description = _("Set Network QoS rule properties") + + def get_parser(self, prog_name): + parser = super(SetNetworkQosRule, self).get_parser(prog_name) + parser.add_argument( + 'qos_policy', + metavar='<qos-policy>', + help=_('QoS policy that contains the rule (name or ID)') + ) + parser.add_argument( + 'id', + metavar='<rule-id>', + help=_('Network QoS rule to delete (ID)') + ) + _add_rule_arguments(parser) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + try: + qos = network_client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + rule_type = _find_rule_type(qos, parsed_args.id) + if not rule_type: + raise Exception('Rule not found') + attrs = _get_attrs(network_client, parsed_args) + qos_id = attrs.pop('qos_policy_id') + qos_rule = _rule_action_call(network_client, ACTION_FIND, + rule_type)(attrs.pop('id'), qos_id) + _rule_action_call(network_client, ACTION_SET, rule_type)( + qos_rule, qos_id, **attrs) + except Exception as e: + msg = (_('Failed to set Network QoS rule ID "%(rule)s": %(e)s') % + {'rule': parsed_args.id, 'e': e}) + raise exceptions.CommandError(msg) + + +class ShowNetworkQosRule(command.ShowOne): + _description = _("Display Network QoS rule details") + + def get_parser(self, prog_name): + parser = super(ShowNetworkQosRule, self).get_parser(prog_name) + parser.add_argument( + 'qos_policy', + metavar='<qos-policy>', + help=_('QoS policy that contains the rule (name or ID)') + ) + parser.add_argument( + 'id', + metavar='<rule-id>', + help=_('Network QoS rule to delete (ID)') + ) + return parser + + def take_action(self, parsed_args): + network_client = self.app.client_manager.network + rule_id = parsed_args.id + try: + qos = network_client.find_qos_policy(parsed_args.qos_policy, + ignore_missing=False) + rule_type = _find_rule_type(qos, rule_id) + if not rule_type: + raise Exception('Rule not found') + obj = _rule_action_call(network_client, ACTION_SHOW, rule_type)( + rule_id, qos.id) + except Exception as e: + msg = (_('Failed to set Network QoS rule ID "%(rule)s": %(e)s') % + {'rule': rule_id, 'e': e}) + raise exceptions.CommandError(msg) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return display_columns, data diff --git a/openstackclient/network/v2/network_qos_rule_type.py b/openstackclient/network/v2/network_qos_rule_type.py new file mode 100644 index 00000000..52f8e235 --- /dev/null +++ b/openstackclient/network/v2/network_qos_rule_type.py @@ -0,0 +1,38 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 osc_lib.command import command +from osc_lib import utils + +from openstackclient.i18n import _ + + +class ListNetworkQosRuleType(command.Lister): + _description = _("List QoS rule types") + + def take_action(self, parsed_args): + client = self.app.client_manager.network + columns = ( + 'type', + ) + column_headers = ( + 'Type', + ) + data = client.qos_rule_types() + + return (column_headers, + (utils.get_item_properties( + s, columns, formatters={}, + ) for s in data)) diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index bb29579f..90754737 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -21,20 +21,18 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - if 'target_tenant' in columns: - columns.remove('target_tenant') - columns.append('target_project_id') - return tuple(sorted(columns)) + column_map = { + 'target_tenant': 'target_project_id', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): @@ -47,9 +45,9 @@ def _get_attrs(client_manager, parsed_args): object_id = network_client.find_network( parsed_args.rbac_object, ignore_missing=False).id if parsed_args.type == 'qos_policy': - # TODO(Huanxuan Ao): Support finding a object ID by obejct name - # after qos policy finding supported in SDK. - object_id = parsed_args.rbac_object + object_id = network_client.find_qos_policy( + parsed_args.rbac_object, + ignore_missing=False).id attrs['object_id'] = object_id identity_client = client_manager.identity @@ -70,16 +68,17 @@ def _get_attrs(client_manager, parsed_args): return attrs +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateNetworkRBAC(command.ShowOne): - """Create network RBAC policy""" + _description = _("Create network RBAC policy") def get_parser(self, prog_name): parser = super(CreateNetworkRBAC, self).get_parser(prog_name) parser.add_argument( 'rbac_object', metavar="<rbac-object>", - help=_("The object to which this RBAC policy affects (name or " - "ID for network objects, ID only for QoS policy objects)") + help=_("The object to which this RBAC policy affects (name or ID)") ) parser.add_argument( '--type', @@ -123,13 +122,13 @@ class CreateNetworkRBAC(command.ShowOne): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_rbac_policy(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return columns, data + return display_columns, data class DeleteNetworkRBAC(command.Command): - """Delete network RBAC policy(s)""" + _description = _("Delete network RBAC policy(s)") def get_parser(self, prog_name): parser = super(DeleteNetworkRBAC, self).get_parser(prog_name) @@ -163,7 +162,31 @@ class DeleteNetworkRBAC(command.Command): class ListNetworkRBAC(command.Lister): - """List network RBAC policies""" + _description = _("List network RBAC policies") + + def get_parser(self, prog_name): + parser = super(ListNetworkRBAC, self).get_parser(prog_name) + parser.add_argument( + '--type', + metavar='<type>', + choices=['qos_policy', 'network'], + help=_('List network RBAC policies according to ' + 'given object type ("qos_policy" or "network")') + ) + parser.add_argument( + '--action', + metavar='<action>', + choices=['access_as_external', 'access_as_shared'], + help=_('List network RBAC policies according to given ' + 'action ("access_as_external" or "access_as_shared")') + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser def take_action(self, parsed_args): client = self.app.client_manager.network @@ -179,15 +202,27 @@ class ListNetworkRBAC(command.Lister): 'Object ID', ) - data = client.rbac_policies() + query = {} + if parsed_args.long: + columns += ('action',) + column_headers += ('Action',) + if parsed_args.type is not None: + query['object_type'] = parsed_args.type + if parsed_args.action is not None: + query['action'] = parsed_args.action + + data = client.rbac_policies(**query) + return (column_headers, (utils.get_item_properties( s, columns, ) for s in data)) +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetNetworkRBAC(command.Command): - """Set network RBAC policy properties""" + _description = _("Set network RBAC policy properties") def get_parser(self, prog_name): parser = super(SetNetworkRBAC, self).get_parser(prog_name) @@ -228,7 +263,7 @@ class SetNetworkRBAC(command.Command): class ShowNetworkRBAC(command.ShowOne): - """Display network RBAC policy details""" + _description = _("Display network RBAC policy details") def get_parser(self, prog_name): parser = super(ShowNetworkRBAC, self).get_parser(prog_name) @@ -243,6 +278,6 @@ class ShowNetworkRBAC(command.ShowOne): client = self.app.client_manager.network obj = client.find_rbac_policy(parsed_args.rbac_policy, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return columns, data + return display_columns, data diff --git a/openstackclient/network/v2/network_segment.py b/openstackclient/network/v2/network_segment.py index 94722f1e..c1a672e2 100644 --- a/openstackclient/network/v2/network_segment.py +++ b/openstackclient/network/v2/network_segment.py @@ -20,13 +20,18 @@ from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) +def _get_columns(item): + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) + + class CreateNetworkSegment(command.ShowOne): - """Create new network segment""" + _description = _("Create new network segment") def get_parser(self, prog_name): parser = super(CreateNetworkSegment, self).get_parser(prog_name) @@ -84,13 +89,13 @@ class CreateNetworkSegment(command.ShowOne): if parsed_args.segment is not None: attrs['segmentation_id'] = parsed_args.segment obj = client.create_segment(**attrs) - columns = tuple(sorted(obj.keys())) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return (columns, data) + return (display_columns, data) class DeleteNetworkSegment(command.Command): - """Delete network segment(s)""" + _description = _("Delete network segment(s)") def get_parser(self, prog_name): parser = super(DeleteNetworkSegment, self).get_parser(prog_name) @@ -114,8 +119,8 @@ class DeleteNetworkSegment(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete network segment with " - "ID '%(network_segment)s': %(e)s") - % {'network_segment': network_segment, 'e': e}) + "ID '%(network_segment)s': %(e)s"), + {'network_segment': network_segment, 'e': e}) if result > 0: total = len(parsed_args.network_segment) @@ -125,7 +130,7 @@ class DeleteNetworkSegment(command.Command): class ListNetworkSegment(command.Lister): - """List network segments""" + _description = _("List network segments") def get_parser(self, prog_name): parser = super(ListNetworkSegment, self).get_parser(prog_name) @@ -185,7 +190,7 @@ class ListNetworkSegment(command.Lister): class SetNetworkSegment(command.Command): - """Set network segment properties""" + _description = _("Set network segment properties") def get_parser(self, prog_name): parser = super(SetNetworkSegment, self).get_parser(prog_name) @@ -219,7 +224,7 @@ class SetNetworkSegment(command.Command): class ShowNetworkSegment(command.ShowOne): - """Display network segment details""" + _description = _("Display network segment details") def get_parser(self, prog_name): parser = super(ShowNetworkSegment, self).get_parser(prog_name) @@ -236,6 +241,6 @@ class ShowNetworkSegment(command.ShowOne): parsed_args.network_segment, ignore_missing=False ) - columns = tuple(sorted(obj.keys())) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return (columns, data) + return (display_columns, data) diff --git a/openstackclient/network/v2/network_service_provider.py b/openstackclient/network/v2/network_service_provider.py new file mode 100644 index 00000000..157948cc --- /dev/null +++ b/openstackclient/network/v2/network_service_provider.py @@ -0,0 +1,43 @@ +# 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. +# + +"""Network Service Providers Implementation""" + +from osc_lib.command import command +from osc_lib import utils + +from openstackclient.i18n import _ + + +class ListNetworkServiceProvider(command.Lister): + _description = _("List Service Providers") + + def take_action(self, parsed_args): + client = self.app.client_manager.network + + columns = ( + 'service_type', + 'name', + 'is_default', + ) + column_headers = ( + 'Service Type', + 'Name', + 'Default', + ) + + data = client.service_providers() + return(column_headers, + (utils.get_item_properties( + s, columns, + ) for s in data)) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 92b286a9..eced93ce 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -25,6 +25,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -34,35 +35,37 @@ def _format_admin_state(state): return 'UP' if state else 'DOWN' +def _format_dns_assignment(dns_assignment): + return utils.format_list_of_dicts(dns_assignment) \ + if dns_assignment else None + _formatters = { 'admin_state_up': _format_admin_state, + 'is_admin_state_up': _format_admin_state, 'allowed_address_pairs': utils.format_list_of_dicts, 'binding_profile': utils.format_dict, 'binding_vif_details': utils.format_dict, - 'dns_assignment': utils.format_list_of_dicts, + 'binding:profile': utils.format_dict, + 'binding:vif_details': utils.format_dict, + 'dns_assignment': _format_dns_assignment, 'extra_dhcp_opts': utils.format_list_of_dicts, 'fixed_ips': utils.format_list_of_dicts, - 'security_groups': utils.format_list, + 'security_group_ids': utils.format_list, } def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - binding_columns = [ - 'binding:host_id', - 'binding:profile', - 'binding:vif_details', - 'binding:vif_type', - 'binding:vnic_type', - ] - for binding_column in binding_columns: - if binding_column in columns: - columns.remove(binding_column) - columns.append(binding_column.replace('binding:', 'binding_', 1)) - return tuple(sorted(columns)) + column_map = { + 'binding:host_id': 'binding_host_id', + 'binding:profile': 'binding_profile', + 'binding:vif_details': 'binding_vif_details', + 'binding:vif_type': 'binding_vif_type', + 'binding:vnic_type': 'binding_vnic_type', + 'is_admin_state_up': 'admin_state_up', + 'is_port_security_enabled': 'port_security_enabled', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) class JSONKeyValueAction(argparse.Action): @@ -109,6 +112,8 @@ def _get_attrs(client_manager, parsed_args): 'The --host-id option is deprecated, ' 'please use --host instead.' )) + if parsed_args.description is not None: + attrs['description'] = parsed_args.description if parsed_args.fixed_ip is not None: attrs['fixed_ips'] = parsed_args.fixed_ip if parsed_args.device: @@ -125,14 +130,16 @@ def _get_attrs(client_manager, parsed_args): attrs['binding:vnic_type'] = parsed_args.vnic_type if parsed_args.host: attrs['binding:host_id'] = parsed_args.host + if parsed_args.mac_address is not None: + attrs['mac_address'] = parsed_args.mac_address + if parsed_args.dns_name is not None: + attrs['dns_name'] = parsed_args.dns_name # It is possible that name is not updated during 'port set' if parsed_args.name is not None: attrs['name'] = str(parsed_args.name) # The remaining options do not support 'port set' command, so they require # additional check - if 'mac_address' in parsed_args and parsed_args.mac_address is not None: - attrs['mac_address'] = parsed_args.mac_address if 'network' in parsed_args and parsed_args.network is not None: attrs['network_id'] = parsed_args.network if 'project' in parsed_args and parsed_args.project is not None: @@ -146,6 +153,12 @@ def _get_attrs(client_manager, parsed_args): ).id attrs['tenant_id'] = project_id + if parsed_args.disable_port_security: + attrs['port_security_enabled'] = False + + if parsed_args.enable_port_security: + attrs['port_security_enabled'] = True + return attrs @@ -179,51 +192,103 @@ def _prepare_fixed_ips(client_manager, parsed_args): parsed_args.fixed_ip = ips +def _prepare_filter_fixed_ips(client_manager, parsed_args): + """Fix and properly format fixed_ip option for filtering. + + Appropriately convert any subnet names to their respective ids. + Convert fixed_ips in parsed args to be in valid list format for filter: + ['subnet_id=foo']. + """ + client = client_manager.network + ips = [] + + for ip_spec in parsed_args.fixed_ip: + if 'subnet' in ip_spec: + subnet_name_id = ip_spec['subnet'] + if subnet_name_id: + _subnet = client.find_subnet(subnet_name_id, + ignore_missing=False) + ips.append('subnet_id=%s' % _subnet.id) + + if 'ip-address' in ip_spec: + ips.append('ip_address=%s' % ip_spec['ip-address']) + return ips + + def _add_updatable_args(parser): - # NOTE(dtroyer): --device-id is deprecated in Mar 2016. Do not - # remove before 3.x release or Mar 2017. - device_group = parser.add_mutually_exclusive_group() - device_group.add_argument( - '--device', - metavar='<device-id>', - help=_("Port device ID") - ) - device_group.add_argument( - '--device-id', - metavar='<device-id>', - help=argparse.SUPPRESS, - ) - parser.add_argument( - '--device-owner', - metavar='<device-owner>', - help=_("Device owner of this port. This is the entity that uses " - "the port (for example, network:dhcp).") - ) - parser.add_argument( - '--vnic-type', - metavar='<vnic-type>', - choices=['direct', 'direct-physical', 'macvtap', - 'normal', 'baremetal'], - help=_("VNIC type for this port (direct | direct-physical | " - "macvtap | normal | baremetal, default: normal)") - ) - # NOTE(dtroyer): --host-id is deprecated in Mar 2016. Do not - # remove before 3.x release or Mar 2017. - host_group = parser.add_mutually_exclusive_group() - host_group.add_argument( - '--host', - metavar='<host-id>', - help=_("Allocate port on host <host-id> (ID only)") - ) - host_group.add_argument( - '--host-id', - metavar='<host-id>', - help=argparse.SUPPRESS, - ) + parser.add_argument( + '--description', + metavar='<description>', + help=_("Description of this port") + ) + # NOTE(dtroyer): --device-id is deprecated in Mar 2016. Do not + # remove before 3.x release or Mar 2017. + device_group = parser.add_mutually_exclusive_group() + device_group.add_argument( + '--device', + metavar='<device-id>', + help=_("Port device ID") + ) + device_group.add_argument( + '--device-id', + metavar='<device-id>', + help=argparse.SUPPRESS, + ) + parser.add_argument( + '--mac-address', + metavar='<mac-address>', + help=_("MAC address of this port (admin only)") + ) + parser.add_argument( + '--device-owner', + metavar='<device-owner>', + help=_("Device owner of this port. This is the entity that uses " + "the port (for example, network:dhcp).") + ) + parser.add_argument( + '--vnic-type', + metavar='<vnic-type>', + choices=['direct', 'direct-physical', 'macvtap', + 'normal', 'baremetal'], + help=_("VNIC type for this port (direct | direct-physical | " + "macvtap | normal | baremetal, default: normal)") + ) + # NOTE(dtroyer): --host-id is deprecated in Mar 2016. Do not + # remove before 3.x release or Mar 2017. + host_group = parser.add_mutually_exclusive_group() + host_group.add_argument( + '--host', + metavar='<host-id>', + help=_("Allocate port on host <host-id> (ID only)") + ) + host_group.add_argument( + '--host-id', + metavar='<host-id>', + help=argparse.SUPPRESS, + ) + parser.add_argument( + '--dns-name', + metavar='dns-name', + help=_("Set DNS name to this port " + "(requires DNS integration extension)") + ) + + +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. +def _convert_address_pairs(parsed_args): + ops = [] + for opt in parsed_args.allowed_address_pairs: + addr = {} + addr['ip_address'] = opt['ip-address'] + if 'mac-address' in opt: + addr['mac_address'] = opt['mac-address'] + ops.append(addr) + return ops class CreatePort(command.ShowOne): - """Create a new port""" + _description = _("Create a new port") def get_parser(self, prog_name): parser = super(CreatePort, self).get_parser(prog_name) @@ -265,11 +330,6 @@ class CreatePort(command.ShowOne): help=_("Disable port") ) parser.add_argument( - '--mac-address', - metavar='<mac-address>', - help=_("MAC address of this port") - ) - parser.add_argument( '--project', metavar='<project>', help=_("Owner's project (name or ID)") @@ -281,7 +341,44 @@ class CreatePort(command.ShowOne): help=_("Name of this port") ) # TODO(singhj): Add support for extended options: - # qos,security groups,dhcp, address pairs + # qos,dhcp + secgroups = parser.add_mutually_exclusive_group() + secgroups.add_argument( + '--security-group', + metavar='<security-group>', + action='append', + dest='security_group', + help=_("Security group to associate with this port (name or ID) " + "(repeat option to set multiple security groups)") + ) + secgroups.add_argument( + '--no-security-group', + dest='no_security_group', + action='store_true', + help=_("Associate no security groups with this port") + ) + port_security = parser.add_mutually_exclusive_group() + port_security.add_argument( + '--enable-port-security', + action='store_true', + help=_("Enable port security for this port (Default)") + ) + port_security.add_argument( + '--disable-port-security', + action='store_true', + help=_("Disable port security for this port") + ) + parser.add_argument( + '--allowed-address', + metavar='ip-address=<ip-address>[,mac-address=<mac-address>]', + action=parseractions.MultiKeyValueAction, + dest='allowed_address_pairs', + required_keys=['ip-address'], + optional_keys=['mac-address'], + help=_("Add allowed-address pair associated with this port: " + "ip-address=<ip-address>[,mac-address=<mac-address>] " + "(repeat option to set multiple allowed-address pairs)") + ) return parser def take_action(self, parsed_args): @@ -291,15 +388,27 @@ class CreatePort(command.ShowOne): parsed_args.network = _network.id _prepare_fixed_ips(self.app.client_manager, parsed_args) attrs = _get_attrs(self.app.client_manager, parsed_args) + + if parsed_args.security_group: + attrs['security_group_ids'] = [client.find_security_group( + sg, ignore_missing=False).id + for sg in + parsed_args.security_group] + elif parsed_args.no_security_group: + attrs['security_group_ids'] = [] + if parsed_args.allowed_address_pairs: + attrs['allowed_address_pairs'] = ( + _convert_address_pairs(parsed_args)) + obj = client.create_port(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) class DeletePort(command.Command): - """Delete port(s)""" + _description = _("Delete port(s)") def get_parser(self, prog_name): parser = super(DeletePort, self).get_parser(prog_name) @@ -332,8 +441,10 @@ class DeletePort(command.Command): raise exceptions.CommandError(msg) +# TODO(abhiraut): Use only the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class ListPort(command.Lister): - """List ports""" + _description = _("List ports") def get_parser(self, prog_name): parser = super(ListPort, self).get_parser(prog_name) @@ -360,26 +471,58 @@ class ListPort(command.Lister): metavar='<server>', help=_("List only ports attached to this server (name or ID)"), ) + parser.add_argument( + '--mac-address', + metavar='<mac-address>', + help=_("List only ports with this MAC address") + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("List ports according to their project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--fixed-ip', + metavar='subnet=<subnet>,ip-address=<ip-address>', + action=parseractions.MultiKeyValueAction, + optional_keys=['subnet', 'ip-address'], + help=_("Desired IP and/or subnet (name or ID) for filtering " + "ports: subnet=<subnet>,ip-address=<ip-address> " + "(repeat option to set multiple fixed IP addresses)") + ) return parser def take_action(self, parsed_args): network_client = self.app.client_manager.network compute_client = self.app.client_manager.compute + identity_client = self.app.client_manager.identity columns = ( 'id', 'name', 'mac_address', 'fixed_ips', + 'status', ) column_headers = ( 'ID', 'Name', 'MAC Address', 'Fixed IP Addresses', + 'Status', ) filters = {} + if parsed_args.long: + columns += ('security_group_ids', 'device_owner',) + column_headers += ('Security Groups', 'Device Owner',) if parsed_args.device_owner is not None: filters['device_owner'] = parsed_args.device_owner if parsed_args.router: @@ -394,6 +537,19 @@ class ListPort(command.Lister): network = network_client.find_network(parsed_args.network, ignore_missing=False) filters['network_id'] = network.id + if parsed_args.mac_address: + filters['mac_address'] = parsed_args.mac_address + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + filters['tenant_id'] = project_id + filters['project_id'] = project_id + if parsed_args.fixed_ip: + filters['fixed_ips'] = _prepare_filter_fixed_ips( + self.app.client_manager, parsed_args) data = network_client.ports(**filters) @@ -404,8 +560,10 @@ class ListPort(command.Lister): ) for s in data)) +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetPort(command.Command): - """Set port properties""" + _description = _("Set port properties") def get_parser(self, prog_name): parser = super(SetPort, self).get_parser(prog_name) @@ -463,6 +621,51 @@ class SetPort(command.Command): metavar="<port>", help=_("Port to modify (name or ID)") ) + parser.add_argument( + '--security-group', + metavar='<security-group>', + action='append', + dest='security_group', + help=_("Security group to associate with this port (name or ID) " + "(repeat option to set multiple security groups)") + ) + parser.add_argument( + '--no-security-group', + dest='no_security_group', + action='store_true', + help=_("Clear existing security groups associated with this port") + ) + port_security = parser.add_mutually_exclusive_group() + port_security.add_argument( + '--enable-port-security', + action='store_true', + help=_("Enable port security for this port") + ) + port_security.add_argument( + '--disable-port-security', + action='store_true', + help=_("Disable port security for this port") + ) + parser.add_argument( + '--allowed-address', + metavar='ip-address=<ip-address>[,mac-address=<mac-address>]', + action=parseractions.MultiKeyValueAction, + dest='allowed_address_pairs', + required_keys=['ip-address'], + optional_keys=['mac-address'], + help=_("Add allowed-address pair associated with this port: " + "ip-address=<ip-address>[,mac-address=<mac-address>] " + "(repeat option to set multiple allowed-address pairs)") + ) + parser.add_argument( + '--no-allowed-address', + dest='no_allowed_address_pair', + action='store_true', + help=_("Clear existing allowed-address pairs associated" + "with this port." + "(Specify both --allowed-address and --no-allowed-address" + "to overwrite the current allowed-address pairs)") + ) return parser def take_action(self, parsed_args): @@ -491,11 +694,34 @@ class SetPort(command.Command): elif parsed_args.no_fixed_ip: attrs['fixed_ips'] = [] + if parsed_args.security_group: + attrs['security_group_ids'] = [ + client.find_security_group(sg, ignore_missing=False).id for + sg in parsed_args.security_group] + if not parsed_args.no_security_group: + attrs['security_group_ids'] += obj.security_group_ids + + elif parsed_args.no_security_group: + attrs['security_group_ids'] = [] + + if (parsed_args.allowed_address_pairs and + parsed_args.no_allowed_address_pair): + attrs['allowed_address_pairs'] = ( + _convert_address_pairs(parsed_args)) + + elif parsed_args.allowed_address_pairs: + attrs['allowed_address_pairs'] = ( + [addr for addr in obj.allowed_address_pairs if addr] + + _convert_address_pairs(parsed_args)) + + elif parsed_args.no_allowed_address_pair: + attrs['allowed_address_pairs'] = [] + client.update_port(obj, **attrs) class ShowPort(command.ShowOne): - """Display port details""" + _description = _("Display port details") def get_parser(self, prog_name): parser = super(ShowPort, self).get_parser(prog_name) @@ -509,13 +735,15 @@ class ShowPort(command.ShowOne): def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_port(parsed_args.port, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class UnsetPort(command.Command): - """Unset port properties""" + _description = _("Unset port properties") def get_parser(self, prog_name): parser = super(UnsetPort, self).get_parser(prog_name) @@ -536,20 +764,44 @@ class UnsetPort(command.Command): help=_("Desired key which should be removed from binding:profile" "(repeat option to unset multiple binding:profile data)")) parser.add_argument( + '--security-group', + metavar='<security-group>', + action='append', + dest='security_group_ids', + help=_("Security group which should be removed this port (name " + "or ID) (repeat option to unset multiple security groups)") + ) + + parser.add_argument( 'port', metavar="<port>", help=_("Port to modify (name or ID)") ) + parser.add_argument( + '--allowed-address', + metavar='ip-address=<ip-address>[,mac-address=<mac-address>]', + action=parseractions.MultiKeyValueAction, + dest='allowed_address_pairs', + required_keys=['ip-address'], + optional_keys=['mac-address'], + help=_("Desired allowed-address pair which should be removed " + "from this port: ip-address=<ip-address> " + "[,mac-address=<mac-address>] (repeat option to set " + "multiple allowed-address pairs)") + ) + return parser def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_port(parsed_args.port, ignore_missing=False) - # SDK ignores update() if it recieves a modified obj and attrs + # SDK ignores update() if it receives a modified obj and attrs # To handle the same tmp_obj is created in all take_action of # Unset* classes tmp_fixed_ips = copy.deepcopy(obj.fixed_ips) tmp_binding_profile = copy.deepcopy(obj.binding_profile) + tmp_secgroups = copy.deepcopy(obj.security_group_ids) + tmp_addr_pairs = copy.deepcopy(obj.allowed_address_pairs) _prepare_fixed_ips(self.app.client_manager, parsed_args) attrs = {} if parsed_args.fixed_ip: @@ -568,5 +820,24 @@ class UnsetPort(command.Command): msg = _("Port does not contain binding-profile %s") % key raise exceptions.CommandError(msg) attrs['binding:profile'] = tmp_binding_profile + if parsed_args.security_group_ids: + try: + for sg in parsed_args.security_group_ids: + sg_id = client.find_security_group( + sg, ignore_missing=False).id + tmp_secgroups.remove(sg_id) + except ValueError: + msg = _("Port does not contain security group %s") % sg + raise exceptions.CommandError(msg) + attrs['security_group_ids'] = tmp_secgroups + if parsed_args.allowed_address_pairs: + try: + for addr in _convert_address_pairs(parsed_args): + tmp_addr_pairs.remove(addr) + except ValueError: + msg = _("Port does not contain allowed-address-pair %s") % addr + raise exceptions.CommandError(msg) + attrs['allowed_address_pairs'] = tmp_addr_pairs + if attrs: client.update_port(obj, **attrs) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 48a3a92c..f46c8696 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -25,6 +25,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -51,6 +52,7 @@ def _format_routes(routes): _formatters = { 'admin_state_up': _format_admin_state, + 'is_admin_state_up': _format_admin_state, 'external_gateway_info': _format_external_gateway_info, 'availability_zones': utils.format_list, 'availability_zone_hints': utils.format_list, @@ -59,11 +61,13 @@ _formatters = { def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - return tuple(sorted(columns)) + column_map = { + 'tenant_id': 'project_id', + 'is_ha': 'ha', + 'is_distributed': 'distributed', + 'is_admin_state_up': 'admin_state_up', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _get_attrs(client_manager, parsed_args): @@ -94,13 +98,11 @@ def _get_attrs(client_manager, parsed_args): ).id attrs['tenant_id'] = project_id - # TODO(tangchen): Support getting 'external_gateway_info' property. - return attrs class AddPortToRouter(command.Command): - """Add a port to a router""" + _description = _("Add a port to a router") def get_parser(self, prog_name): parser = super(AddPortToRouter, self).get_parser(prog_name) @@ -124,7 +126,7 @@ class AddPortToRouter(command.Command): class AddSubnetToRouter(command.Command): - """Add a subnet to a router""" + _description = _("Add a subnet to a router") def get_parser(self, prog_name): parser = super(AddSubnetToRouter, self).get_parser(prog_name) @@ -150,8 +152,10 @@ class AddSubnetToRouter(command.Command): subnet_id=subnet.id) +# TODO(yanxing'an): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateRouter(command.ShowOne): - """Create a new router""" + _description = _("Create a new router") def get_parser(self, prog_name): parser = super(CreateRouter, self).get_parser(prog_name) @@ -215,14 +219,14 @@ class CreateRouter(command.ShowOne): attrs['ha'] = parsed_args.ha obj = client.create_router(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) class DeleteRouter(command.Command): - """Delete router(s)""" + _description = _("Delete router(s)") def get_parser(self, prog_name): parser = super(DeleteRouter, self).get_parser(prog_name) @@ -245,8 +249,8 @@ class DeleteRouter(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete router with " - "name or ID '%(router)s': %(e)s") - % {'router': router, 'e': e}) + "name or ID '%(router)s': %(e)s"), + {'router': router, 'e': e}) if result > 0: total = len(parsed_args.router) @@ -255,30 +259,54 @@ class DeleteRouter(command.Command): raise exceptions.CommandError(msg) +# TODO(yanxing'an): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class ListRouter(command.Lister): - """List routers""" + _description = _("List routers") def get_parser(self, prog_name): parser = super(ListRouter, self).get_parser(prog_name) parser.add_argument( + '--name', + metavar='<name>', + help=_("List routers according to their name") + ) + admin_state_group = parser.add_mutually_exclusive_group() + admin_state_group.add_argument( + '--enable', + action='store_true', + help=_("List enabled routers") + ) + admin_state_group.add_argument( + '--disable', + action='store_true', + help=_("List disabled routers") + ) + parser.add_argument( '--long', action='store_true', default=False, help=_("List additional fields in output") ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("List routers according to their project (name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) return parser def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity client = self.app.client_manager.network - columns = ( 'id', 'name', 'status', - 'admin_state_up', - 'distributed', - 'ha', - 'tenant_id', + 'is_admin_state_up', + 'is_distributed', + 'is_ha', + 'project_id', ) column_headers = ( 'ID', @@ -289,6 +317,27 @@ class ListRouter(command.Lister): 'HA', 'Project', ) + + args = {} + + if parsed_args.name is not None: + args['name'] = parsed_args.name + + if parsed_args.enable: + args['admin_state_up'] = True + args['is_admin_state_up'] = True + elif parsed_args.disable: + args['admin_state_up'] = False + args['is_admin_state_up'] = False + + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + args['tenant_id'] = project_id + args['project_id'] = project_id if parsed_args.long: columns = columns + ( 'routes', @@ -308,7 +357,7 @@ class ListRouter(command.Lister): 'Availability zones', ) - data = client.routers() + data = client.routers(**args) return (column_headers, (utils.get_item_properties( s, columns, @@ -317,7 +366,7 @@ class ListRouter(command.Lister): class RemovePortFromRouter(command.Command): - """Remove a port from a router""" + _description = _("Remove a port from a router") def get_parser(self, prog_name): parser = super(RemovePortFromRouter, self).get_parser(prog_name) @@ -341,7 +390,7 @@ class RemovePortFromRouter(command.Command): class RemoveSubnetFromRouter(command.Command): - """Remove a subnet from a router""" + _description = _("Remove a subnet from a router") def get_parser(self, prog_name): parser = super(RemoveSubnetFromRouter, self).get_parser(prog_name) @@ -367,8 +416,10 @@ class RemoveSubnetFromRouter(command.Command): subnet_id=subnet.id) +# TODO(yanxing'an): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetRouter(command.Command): - """Set router properties""" + _description = _("Set router properties") def get_parser(self, prog_name): parser = super(SetRouter, self).get_parser(prog_name) @@ -411,7 +462,8 @@ class SetRouter(command.Command): help=_("Set router to centralized mode (disabled router only)") ) routes_group = parser.add_mutually_exclusive_group() - routes_group.add_argument( + # ToDo(Reedip):Remove mutual exclusiveness once clear-routes is removed + parser.add_argument( '--route', metavar='destination=<subnet>,gateway=<ip-address>', action=parseractions.MultiKeyValueAction, @@ -426,21 +478,54 @@ class SetRouter(command.Command): routes_group.add_argument( '--no-route', action='store_true', - help=_("Clear routes associated with the router") + help=_("Clear routes associated with the router. " + "Specify both --route and --no-route to overwrite " + "current value of route.") ) routes_group.add_argument( '--clear-routes', action='store_true', help=argparse.SUPPRESS, ) - - # TODO(tangchen): Support setting 'ha' property in 'router set' - # command. It appears that changing the ha state is supported by - # neutron under certain conditions. - - # TODO(tangchen): Support setting 'external_gateway_info' property in - # 'router set' command. - + routes_ha = parser.add_mutually_exclusive_group() + routes_ha.add_argument( + '--ha', + action='store_true', + help=_("Set the router as highly available " + "(disabled router only)") + ) + routes_ha.add_argument( + '--no-ha', + action='store_true', + help=_("Clear high availability attribute of the router " + "(disabled router only)") + ) + parser.add_argument( + '--external-gateway', + metavar="<network>", + help=_("External Network used as router's gateway (name or ID)") + ) + parser.add_argument( + '--fixed-ip', + metavar='subnet=<subnet>,ip-address=<ip-address>', + action=parseractions.MultiKeyValueAction, + optional_keys=['subnet', 'ip-address'], + help=_("Desired IP and/or subnet (name or ID)" + "on external gateway: " + "subnet=<subnet>,ip-address=<ip-address> " + "(repeat option to set multiple fixed IP addresses)") + ) + snat_group = parser.add_mutually_exclusive_group() + snat_group.add_argument( + '--enable-snat', + action='store_true', + help=_("Enable Source NAT on external gateway") + ) + snat_group.add_argument( + '--disable-snat', + action='store_true', + help=_("Disable Source NAT on external gateway") + ) return parser def take_action(self, parsed_args): @@ -451,26 +536,59 @@ class SetRouter(command.Command): attrs = _get_attrs(self.app.client_manager, parsed_args) # Get the route attributes. - if parsed_args.no_route: - attrs['routes'] = [] - elif parsed_args.clear_routes: - attrs['routes'] = [] + if parsed_args.ha: + attrs['ha'] = True + elif parsed_args.no_ha: + attrs['ha'] = False + if parsed_args.clear_routes: LOG.warning(_( 'The --clear-routes option is deprecated, ' 'please use --no-route instead.' )) - elif parsed_args.routes is not None: - # Map the route keys and append to the current routes. - # The REST API will handle route validation and duplicates. + + if parsed_args.routes is not None: for route in parsed_args.routes: route['nexthop'] = route.pop('gateway') - attrs['routes'] = obj.routes + parsed_args.routes - + attrs['routes'] = parsed_args.routes + if not (parsed_args.no_route or parsed_args.clear_routes): + # Map the route keys and append to the current routes. + # The REST API will handle route validation and duplicates. + attrs['routes'] += obj.routes + elif parsed_args.no_route or parsed_args.clear_routes: + attrs['routes'] = [] + if (parsed_args.disable_snat or parsed_args.enable_snat or + parsed_args.fixed_ip) and not parsed_args.external_gateway: + msg = (_("You must specify '--external-gateway' in order" + "to update the SNAT or fixed-ip values")) + raise exceptions.CommandError(msg) + if parsed_args.external_gateway: + gateway_info = {} + network = client.find_network( + parsed_args.external_gateway, ignore_missing=False) + gateway_info['network_id'] = network.id + if parsed_args.disable_snat: + gateway_info['enable_snat'] = False + if parsed_args.enable_snat: + gateway_info['enable_snat'] = True + if parsed_args.fixed_ip: + ips = [] + for ip_spec in parsed_args.fixed_ip: + if ip_spec.get('subnet', False): + subnet_name_id = ip_spec.pop('subnet') + if subnet_name_id: + subnet = client.find_subnet(subnet_name_id, + ignore_missing=False) + ip_spec['subnet_id'] = subnet.id + if ip_spec.get('ip-address', False): + ip_spec['ip_address'] = ip_spec.pop('ip-address') + ips.append(ip_spec) + gateway_info['external_fixed_ips'] = ips + attrs['external_gateway_info'] = gateway_info client.update_router(obj, **attrs) class ShowRouter(command.ShowOne): - """Display router details""" + _description = _("Display router details") def get_parser(self, prog_name): parser = super(ShowRouter, self).get_parser(prog_name) @@ -484,13 +602,14 @@ class ShowRouter(command.ShowOne): def take_action(self, parsed_args): client = self.app.client_manager.network obj = client.find_router(parsed_args.router, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + + return (display_columns, data) class UnsetRouter(command.Command): - """Unset router properties""" + _description = _("Unset router properties") def get_parser(self, prog_name): parser = super(UnsetRouter, self).get_parser(prog_name) @@ -506,6 +625,11 @@ class UnsetRouter(command.Command): "gateway: nexthop IP address " "(repeat option to unset multiple routes)")) parser.add_argument( + '--external-gateway', + action='store_true', + default=False, + help=_("Remove external gateway information from the router")) + parser.add_argument( 'router', metavar="<router>", help=_("Router to modify (name or ID)") @@ -520,12 +644,13 @@ class UnsetRouter(command.Command): if parsed_args.routes: try: for route in parsed_args.routes: + route['nexthop'] = route.pop('gateway') tmp_routes.remove(route) except ValueError: msg = (_("Router does not contain route %s") % route) raise exceptions.CommandError(msg) - for route in tmp_routes: - route['nexthop'] = route.pop('gateway') attrs['routes'] = tmp_routes + if parsed_args.external_gateway: + attrs['external_gateway_info'] = {} if attrs: client.update_router(obj, **attrs) diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index f832f721..182d4817 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -21,6 +21,7 @@ import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common +from openstackclient.network import sdk_utils from openstackclient.network import utils as network_utils @@ -34,6 +35,7 @@ def _format_network_security_group_rules(sg_rules): sg_rule.pop(key) sg_rule.pop('security_group_id', None) sg_rule.pop('tenant_id', None) + sg_rule.pop('project_id', None) return utils.format_list_of_dicts(sg_rules) @@ -72,30 +74,17 @@ _formatters_compute = { def _get_columns(item): - # Build the display columns and a list of the property columns - # that need to be mapped (display column name, property name). - columns = list(item.keys()) - property_column_mappings = [] - if 'security_group_rules' in columns: - columns.append('rules') - columns.remove('security_group_rules') - property_column_mappings.append(('rules', 'security_group_rules')) - if 'tenant_id' in columns: - columns.append('project_id') - columns.remove('tenant_id') - property_column_mappings.append(('project_id', 'tenant_id')) - display_columns = sorted(columns) - - # Build the property columns and apply any column mappings. - property_columns = sorted(columns) - for property_column_mapping in property_column_mappings: - property_index = property_columns.index(property_column_mapping[0]) - property_columns[property_index] = property_column_mapping[1] - return tuple(display_columns), property_columns + column_map = { + 'security_group_rules': 'rules', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateSecurityGroup(common.NetworkAndComputeShowOne): - """Create a new security group""" + _description = _("Create a new security group") def update_parser_common(self, parser): parser.add_argument( @@ -155,7 +144,7 @@ class CreateSecurityGroup(common.NetworkAndComputeShowOne): parsed_args.name, description, ) - display_columns, property_columns = _get_columns(obj._info) + display_columns, property_columns = _get_columns(obj) data = utils.get_dict_properties( obj._info, property_columns, @@ -165,7 +154,7 @@ class CreateSecurityGroup(common.NetworkAndComputeShowOne): class DeleteSecurityGroup(common.NetworkAndComputeDelete): - """Delete security group(s)""" + _description = _("Delete security group(s)") # Used by base class to find resources in parsed_args. resource = 'group' @@ -189,8 +178,10 @@ class DeleteSecurityGroup(common.NetworkAndComputeDelete): client.security_groups.delete(data.id) +# TODO(rauta): Use the SDK resource mapped attribute names once +# the OSC minimum requirements include SDK 1.0. class ListSecurityGroup(common.NetworkAndComputeLister): - """List security groups""" + _description = _("List security groups") def update_parser_network(self, parser): # Maintain and hide the argument for backwards compatibility. @@ -201,6 +192,13 @@ class ListSecurityGroup(common.NetworkAndComputeLister): default=False, help=argparse.SUPPRESS, ) + parser.add_argument( + '--project', + metavar='<project>', + help=_("List security groups according to the project " + "(name or ID)") + ) + identity_common.add_project_domain_option_to_parser(parser) return parser def update_parser_compute(self, parser): @@ -212,33 +210,57 @@ class ListSecurityGroup(common.NetworkAndComputeLister): ) return parser - def _get_return_data(self, data, include_project=True): + def take_action_network(self, client, parsed_args): + filters = {} + if parsed_args.project: + identity_client = self.app.client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + filters['tenant_id'] = project_id + filters['project_id'] = project_id + data = client.security_groups(**filters) + columns = ( "ID", "Name", "Description", + "Project ID" + ) + column_headers = ( + "ID", + "Name", + "Description", + "Project" ) - column_headers = columns - if include_project: - columns = columns + ('Tenant ID',) - column_headers = column_headers + ('Project',) return (column_headers, (utils.get_item_properties( s, columns, ) for s in data)) - def take_action_network(self, client, parsed_args): - return self._get_return_data(client.security_groups()) - def take_action_compute(self, client, parsed_args): search = {'all_tenants': parsed_args.all_projects} data = client.security_groups.list(search_opts=search) - return self._get_return_data(data, - include_project=parsed_args.all_projects) + + columns = ( + "ID", + "Name", + "Description", + ) + column_headers = columns + if parsed_args.all_projects: + columns = columns + ('Tenant ID',) + column_headers = column_headers + ('Project',) + return (column_headers, + (utils.get_item_properties( + s, columns, + ) for s in data)) class SetSecurityGroup(common.NetworkAndComputeCommand): - """Set security group properties""" + _description = _("Set security group properties") def update_parser_common(self, parser): parser.add_argument( @@ -293,7 +315,7 @@ class SetSecurityGroup(common.NetworkAndComputeCommand): class ShowSecurityGroup(common.NetworkAndComputeShowOne): - """Display security group details""" + _description = _("Display security group details") def update_parser_common(self, parser): parser.add_argument( @@ -319,7 +341,7 @@ class ShowSecurityGroup(common.NetworkAndComputeShowOne): client.security_groups, parsed_args.group, ) - display_columns, property_columns = _get_columns(obj._info) + display_columns, property_columns = _get_columns(obj) data = utils.get_dict_properties( obj._info, property_columns, diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index e3be44ec..8f07c5a4 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -14,6 +14,7 @@ """Security Group Rule action implementations""" import argparse +import logging try: from novaclient.v2 import security_group_rules as compute_secgroup_rules @@ -28,9 +29,13 @@ import six from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common +from openstackclient.network import sdk_utils from openstackclient.network import utils as network_utils +LOG = logging.getLogger(__name__) + + def _format_security_group_rule_show(obj): data = network_utils.transform_compute_security_group_rule(obj) return zip(*sorted(six.iteritems(data))) @@ -46,28 +51,27 @@ def _format_network_port_range(rule): # - Single port: '80:80' # - No port range: '' port_range = '' - if _is_icmp_protocol(rule.protocol): - if rule.port_range_min: - port_range += 'type=' + str(rule.port_range_min) - if rule.port_range_max: - port_range += ':code=' + str(rule.port_range_max) - elif rule.port_range_min or rule.port_range_max: - port_range_min = str(rule.port_range_min) - port_range_max = str(rule.port_range_max) - if rule.port_range_min is None: + if _is_icmp_protocol(rule['protocol']): + if rule['port_range_min']: + port_range += 'type=' + str(rule['port_range_min']) + if rule['port_range_max']: + port_range += ':code=' + str(rule['port_range_max']) + elif rule['port_range_min'] or rule['port_range_max']: + port_range_min = str(rule['port_range_min']) + port_range_max = str(rule['port_range_max']) + if rule['port_range_min'] is None: port_range_min = port_range_max - if rule.port_range_max is None: + if rule['port_range_max'] is None: port_range_max = port_range_min port_range = port_range_min + ':' + port_range_max return port_range def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - return tuple(sorted(columns)) + column_map = { + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) def _convert_to_lowercase(string): @@ -85,8 +89,10 @@ def _is_icmp_protocol(protocol): return False +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): - """Create a new security group rule""" + _description = _("Create a new security group rule") def update_parser_common(self, parser): parser.add_argument( @@ -94,22 +100,40 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): metavar='<group>', help=_("Create rule in this security group (name or ID)") ) - source_group = parser.add_mutually_exclusive_group() - source_group.add_argument( + remote_group = parser.add_mutually_exclusive_group() + remote_group.add_argument( + "--remote-ip", + metavar="<ip-address>", + help=_("Remote IP address block (may use CIDR notation; " + "default for IPv4 rule: 0.0.0.0/0)"), + ) + remote_group.add_argument( + "--remote-group", + metavar="<group>", + help=_("Remote security group (name or ID)"), + ) + # Handle deprecated options + # NOTE(dtroyer): --src-ip and --src-group were deprecated in Nov 2016. + # Do not remove before 4.x release or Nov 2017. + remote_group.add_argument( "--src-ip", metavar="<ip-address>", - help=_("Source IP address block (may use CIDR notation; " - "default for IPv4 rule: 0.0.0.0/0)") + help=argparse.SUPPRESS, ) - source_group.add_argument( + remote_group.add_argument( "--src-group", metavar="<group>", - help=_("Source security group (name or ID)") + help=argparse.SUPPRESS, ) return parser def update_parser_network(self, parser): parser.add_argument( + '--description', + metavar='<description>', + help=_("Set security group rule description") + ) + parser.add_argument( '--dst-port', metavar='<port-range>', action=parseractions.RangeAction, @@ -235,6 +259,9 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): attrs = {} attrs['protocol'] = self._get_protocol(parsed_args) + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + # NOTE(rtheis): A direction must be specified and ingress # is the default. if parsed_args.ingress or not parsed_args.egress: @@ -277,13 +304,31 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): if parsed_args.icmp_code: attrs['port_range_max'] = parsed_args.icmp_code - if parsed_args.src_group is not None: + # NOTE(dtroyer): --src-ip and --src-group were deprecated in Nov 2016. + # Do not remove before 4.x release or Nov 2017. + if not (parsed_args.remote_group is None and + parsed_args.src_group is None): attrs['remote_group_id'] = client.find_security_group( - parsed_args.src_group, + parsed_args.remote_group or parsed_args.src_group, ignore_missing=False ).id - elif parsed_args.src_ip is not None: - attrs['remote_ip_prefix'] = parsed_args.src_ip + if parsed_args.src_group: + LOG.warning( + _("The %(old)s option is deprecated, " + "please use %(new)s instead."), + {'old': '--src-group', 'new': '--remote-group'}, + ) + elif not (parsed_args.remote_ip is None and + parsed_args.src_ip is None): + attrs['remote_ip_prefix'] = ( + parsed_args.remote_ip or parsed_args.src_ip + ) + if parsed_args.src_ip: + LOG.warning( + _("The %(old)s option is deprecated, " + "please use %(new)s instead."), + {'old': '--src-ip', 'new': '--remote-ip'}, + ) elif attrs['ethertype'] == 'IPv4': attrs['remote_ip_prefix'] = '0.0.0.0/0' attrs['security_group_id'] = security_group_id @@ -298,9 +343,9 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): # Create and show the security group rule. obj = client.create_security_group_rule(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return (columns, data) + return (display_columns, data) def take_action_compute(self, client, parsed_args): group = utils.find_resource( @@ -312,29 +357,46 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): from_port, to_port = -1, -1 else: from_port, to_port = parsed_args.dst_port - src_ip = None - if parsed_args.src_group is not None: - parsed_args.src_group = utils.find_resource( + + # NOTE(dtroyer): --src-ip and --src-group were deprecated in Nov 2016. + # Do not remove before 4.x release or Nov 2017. + remote_ip = None + if not (parsed_args.remote_group is None and + parsed_args.src_group is None): + parsed_args.remote_group = utils.find_resource( client.security_groups, - parsed_args.src_group, + parsed_args.remote_group or parsed_args.src_group, ).id - if parsed_args.src_ip is not None: - src_ip = parsed_args.src_ip + if parsed_args.src_group: + LOG.warning( + _("The %(old)s option is deprecated, " + "please use %(new)s instead."), + {'old': '--src-group', 'new': '--remote-group'}, + ) + if not (parsed_args.remote_ip is None and + parsed_args.src_ip is None): + remote_ip = parsed_args.remote_ip or parsed_args.src_ip + if parsed_args.src_ip: + LOG.warning( + _("The %(old)s option is deprecated, " + "please use %(new)s instead."), + {'old': '--src-ip', 'new': '--remote-ip'}, + ) else: - src_ip = '0.0.0.0/0' + remote_ip = '0.0.0.0/0' obj = client.security_group_rules.create( group.id, protocol, from_port, to_port, - src_ip, - parsed_args.src_group, + remote_ip, + parsed_args.remote_group, ) return _format_security_group_rule_show(obj._info) class DeleteSecurityGroupRule(common.NetworkAndComputeDelete): - """Delete security group rule(s)""" + _description = _("Delete security group rule(s)") # Used by base class to find resources in parsed_args. resource = 'rule' @@ -359,7 +421,17 @@ class DeleteSecurityGroupRule(common.NetworkAndComputeDelete): class ListSecurityGroupRule(common.NetworkAndComputeLister): - """List security group rules""" + _description = _("List security group rules") + + def _format_network_security_group_rule(self, rule): + """Transform the SDK SecurityGroupRule object to a dict + + The SDK object gets in the way of reformatting columns... + Create port_range column from port_range_min and port_range_max + """ + rule = rule.to_dict() + rule['port_range'] = _format_network_port_range(rule) + return rule def update_parser_common(self, parser): parser.add_argument( @@ -380,6 +452,28 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister): help=argparse.SUPPRESS ) parser.add_argument( + '--protocol', + metavar='<protocol>', + type=_convert_to_lowercase, + help=_("List rules by the IP protocol (" + "ah, dhcp, egp, esp, gre, icmp, igmp, " + "ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, " + "ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, " + "udp, udplite, vrrp and integer representations [0-255])." + ) + ) + direction_group = parser.add_mutually_exclusive_group() + direction_group.add_argument( + '--ingress', + action='store_true', + help=_("List rules applied to incoming network traffic") + ) + direction_group.add_argument( + '--egress', + action='store_true', + help=_("List rules applied to outgoing network traffic") + ) + parser.add_argument( '--long', action='store_true', default=False, @@ -424,10 +518,10 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister): 'id', 'protocol', 'remote_ip_prefix', - 'port_range_min', + 'port_range', ) if parsed_args.long: - columns = columns + ('direction', 'ethertype',) + columns = columns + ('direction', 'ether_type',) columns = columns + ('remote_group_id',) # Get the security group rules using the requested query. @@ -443,16 +537,21 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister): query = {'security_group_id': security_group_id} else: columns = columns + ('security_group_id',) - rules = list(client.security_group_rules(**query)) - # Reformat the rules to display a port range instead - # of just the port range minimum. This maintains - # output compatibility with compute. - for rule in rules: - rule.port_range_min = _format_network_port_range(rule) + if parsed_args.ingress: + query['direction'] = 'ingress' + if parsed_args.egress: + query['direction'] = 'egress' + if parsed_args.protocol is not None: + query['protocol'] = parsed_args.protocol + + rules = [ + self._format_network_security_group_rule(r) + for r in client.security_group_rules(**query) + ] return (column_headers, - (utils.get_item_properties( + (utils.get_dict_properties( s, columns, ) for s in rules)) @@ -494,7 +593,7 @@ class ListSecurityGroupRule(common.NetworkAndComputeLister): class ShowSecurityGroupRule(common.NetworkAndComputeShowOne): - """Display security group rule details""" + _description = _("Display security group rule details") def update_parser_common(self, parser): parser.add_argument( @@ -507,9 +606,9 @@ class ShowSecurityGroupRule(common.NetworkAndComputeShowOne): def take_action_network(self, client, parsed_args): obj = client.find_security_group_rule(parsed_args.rule, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) - return (columns, data) + return (display_columns, data) def take_action_compute(self, client, parsed_args): # NOTE(rtheis): Unfortunately, compute does not have an API diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 1b778c91..403b4cd2 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -23,6 +23,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) @@ -84,6 +85,15 @@ def _get_common_parse_arguments(parser, is_create=True): help=_("DNS server for this subnet " "(repeat option to set multiple DNS servers)") ) + + if not is_create: + parser.add_argument( + '--no-dns-nameservers', + action='store_true', + help=_("Clear existing information of DNS Nameservers. " + "Specify both --dns-nameserver and --no-dns-nameserver " + "to overwrite the current DNS Nameserver information.") + ) parser.add_argument( '--host-route', metavar='destination=<subnet>,gateway=<ip-address>', @@ -117,11 +127,18 @@ def _get_common_parse_arguments(parser, is_create=True): def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - return tuple(sorted(columns)) + column_map = { + 'is_dhcp_enabled': 'enable_dhcp', + 'subnet_pool_id': 'subnetpool_id', + 'tenant_id': 'project_id', + } + # Do not show this column when displaying a subnet + invisible_columns = ['use_default_subnetpool'] + return sdk_utils.get_osc_show_columns_for_sdk_resource( + item, + column_map, + invisible_columns=invisible_columns + ) def convert_entries_to_nexthop(entries): @@ -168,7 +185,7 @@ def _get_attrs(client_manager, parsed_args, is_create=True): ignore_missing=False) attrs['subnetpool_id'] = subnet_pool.id if parsed_args.use_default_subnet_pool: - attrs['use_default_subnetpool'] = True + attrs['use_default_subnet_pool'] = True if parsed_args.prefix_length is not None: attrs['prefixlen'] = parsed_args.prefix_length if parsed_args.subnet_range is not None: @@ -217,8 +234,10 @@ def _get_attrs(client_manager, parsed_args, is_create=True): return attrs +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateSubnet(command.ShowOne): - """Create a subnet""" + _description = _("Create a subnet") def get_parser(self, prog_name): parser = super(CreateSubnet, self).get_parser(prog_name) @@ -323,13 +342,13 @@ class CreateSubnet(command.ShowOne): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_subnet(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) class DeleteSubnet(command.Command): - """Delete subnet(s)""" + _description = _("Delete subnet(s)") def get_parser(self, prog_name): parser = super(DeleteSubnet, self).get_parser(prog_name) @@ -352,8 +371,8 @@ class DeleteSubnet(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete subnet with " - "name or ID '%(subnet)s': %(e)s") - % {'subnet': subnet, 'e': e}) + "name or ID '%(subnet)s': %(e)s"), + {'subnet': subnet, 'e': e}) if result > 0: total = len(parsed_args.subnet) @@ -362,8 +381,10 @@ class DeleteSubnet(command.Command): raise exceptions.CommandError(msg) +# TODO(abhiraut): Use only the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class ListSubnet(command.Lister): - """List subnets""" + _description = _("List subnets") def get_parser(self, prog_name): parser = super(ListSubnet, self).get_parser(prog_name) @@ -443,8 +464,10 @@ class ListSubnet(command.Lister): filters['ip_version'] = parsed_args.ip_version if parsed_args.dhcp: filters['enable_dhcp'] = True + filters['is_dhcp_enabled'] = True elif parsed_args.no_dhcp: filters['enable_dhcp'] = False + filters['is_dhcp_enabled'] = False if parsed_args.service_types: filters['service_types'] = parsed_args.service_types if parsed_args.project: @@ -454,6 +477,7 @@ class ListSubnet(command.Lister): parsed_args.project_domain, ).id filters['tenant_id'] = project_id + filters['project_id'] = project_id if parsed_args.network: network_id = network_client.find_network(parsed_args.network, ignore_missing=False).id @@ -472,7 +496,7 @@ class ListSubnet(command.Lister): headers += ('Project', 'DHCP', 'Name Servers', 'Allocation Pools', 'Host Routes', 'IP Version', 'Gateway', 'Service Types') - columns += ('tenant_id', 'enable_dhcp', 'dns_nameservers', + columns += ('project_id', 'is_dhcp_enabled', 'dns_nameservers', 'allocation_pools', 'host_routes', 'ip_version', 'gateway_ip', 'service_types') @@ -483,8 +507,10 @@ class ListSubnet(command.Lister): ) for s in data)) +# TODO(abhiraut): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetSubnet(command.Command): - """Set subnet properties""" + _description = _("Set subnet properties") def get_parser(self, prog_name): parser = super(SetSubnet, self).get_parser(prog_name) @@ -532,7 +558,10 @@ class SetSubnet(command.Command): attrs = _get_attrs(self.app.client_manager, parsed_args, is_create=False) if 'dns_nameservers' in attrs: - attrs['dns_nameservers'] += obj.dns_nameservers + if not parsed_args.no_dns_nameservers: + attrs['dns_nameservers'] += obj.dns_nameservers + elif parsed_args.no_dns_nameservers: + attrs['dns_nameservers'] = [] if 'host_routes' in attrs: if not parsed_args.no_host_route: attrs['host_routes'] += obj.host_routes @@ -542,7 +571,7 @@ class SetSubnet(command.Command): if not parsed_args.no_allocation_pool: attrs['allocation_pools'] += obj.allocation_pools elif parsed_args.no_allocation_pool: - attrs['allocation_pools'] = '' + attrs['allocation_pools'] = [] if 'service_types' in attrs: attrs['service_types'] += obj.service_types client.update_subnet(obj, **attrs) @@ -550,7 +579,7 @@ class SetSubnet(command.Command): class ShowSubnet(command.ShowOne): - """Display subnet details""" + _description = _("Display subnet details") def get_parser(self, prog_name): parser = super(ShowSubnet, self).get_parser(prog_name) @@ -564,13 +593,13 @@ class ShowSubnet(command.ShowOne): def take_action(self, parsed_args): obj = self.app.client_manager.network.find_subnet(parsed_args.subnet, ignore_missing=False) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) class UnsetSubnet(command.Command): - """Unset subnet properties""" + _description = _("Unset subnet properties") def get_parser(self, prog_name): parser = super(UnsetSubnet, self).get_parser(prog_name) diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index a01d2f7b..04731111 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -12,6 +12,7 @@ # """Subnet pool action implementations""" + import copy import logging @@ -22,17 +23,21 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils LOG = logging.getLogger(__name__) def _get_columns(item): - columns = list(item.keys()) - if 'tenant_id' in columns: - columns.remove('tenant_id') - columns.append('project_id') - return tuple(sorted(columns)) + column_map = { + 'default_prefix_length': 'default_prefixlen', + 'is_shared': 'shared', + 'maximum_prefix_length': 'max_prefixlen', + 'minimum_prefix_length': 'min_prefixlen', + 'tenant_id': 'project_id', + } + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) _formatters = { @@ -134,8 +139,10 @@ def _add_default_options(parser): ) +# TODO(rtheis): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class CreateSubnetPool(command.ShowOne): - """Create subnet pool""" + _description = _("Create subnet pool") def get_parser(self, prog_name): parser = super(CreateSubnetPool, self).get_parser(prog_name) @@ -184,13 +191,13 @@ class CreateSubnetPool(command.ShowOne): if "prefixes" not in attrs: attrs['prefixes'] = [] obj = client.create_subnet_pool(**attrs) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) class DeleteSubnetPool(command.Command): - """Delete subnet pool(s)""" + _description = _("Delete subnet pool(s)") def get_parser(self, prog_name): parser = super(DeleteSubnetPool, self).get_parser(prog_name) @@ -213,8 +220,8 @@ class DeleteSubnetPool(command.Command): except Exception as e: result += 1 LOG.error(_("Failed to delete subnet pool with " - "name or ID '%(pool)s': %(e)s") - % {'pool': pool, 'e': e}) + "name or ID '%(pool)s': %(e)s"), + {'pool': pool, 'e': e}) if result > 0: total = len(parsed_args.subnet_pool) @@ -223,8 +230,10 @@ class DeleteSubnetPool(command.Command): raise exceptions.CommandError(msg) +# TODO(rtheis): Use only the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class ListSubnetPool(command.Lister): - """List subnet pools""" + _description = _("List subnet pools") def get_parser(self, prog_name): parser = super(ListSubnetPool, self).get_parser(prog_name) @@ -238,40 +247,42 @@ class ListSubnetPool(command.Lister): shared_group.add_argument( '--share', action='store_true', - help=_("List subnets shared between projects"), + help=_("List subnet pools shared between projects"), ) shared_group.add_argument( '--no-share', action='store_true', - help=_("List subnets not shared between projects"), + help=_("List subnet pools not shared between projects"), ) default_group = parser.add_mutually_exclusive_group() default_group.add_argument( '--default', action='store_true', - help=_("List subnets used as the default external subnet pool"), + help=_("List subnet pools used as the default external " + "subnet pool"), ) default_group.add_argument( '--no-default', action='store_true', - help=_("List subnets not used as the default external subnet pool") + help=_("List subnet pools not used as the default external " + "subnet pool") ) parser.add_argument( '--project', metavar='<project>', - help=_("List subnets according to their project (name or ID)") + help=_("List subnet pools according to their project (name or ID)") ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--name', metavar='<name>', - help=_("List only subnets of given name in output") + help=_("List only subnet pools of given name in output") ) parser.add_argument( '--address-scope', metavar='<address-scope>', - help=_("List only subnets of given address scope (name or ID) " - "in output") + help=_("List only subnet pools of given address scope " + "(name or ID) in output") ) return parser @@ -281,8 +292,10 @@ class ListSubnetPool(command.Lister): filters = {} if parsed_args.share: filters['shared'] = True + filters['is_shared'] = True elif parsed_args.no_share: filters['shared'] = False + filters['is_shared'] = False if parsed_args.default: filters['is_default'] = True elif parsed_args.no_default: @@ -294,6 +307,7 @@ class ListSubnetPool(command.Lister): parsed_args.project_domain, ).id filters['tenant_id'] = project_id + filters['project_id'] = project_id if parsed_args.name is not None: filters['name'] = parsed_args.name if parsed_args.address_scope: @@ -308,8 +322,8 @@ class ListSubnetPool(command.Lister): if parsed_args.long: headers += ('Default Prefix Length', 'Address Scope', 'Default Subnet Pool', 'Shared') - columns += ('default_prefixlen', 'address_scope_id', - 'is_default', 'shared') + columns += ('default_prefix_length', 'address_scope_id', + 'is_default', 'is_shared') return (headers, (utils.get_item_properties( @@ -318,8 +332,10 @@ class ListSubnetPool(command.Lister): ) for s in data)) +# TODO(rtheis): Use the SDK resource mapped attribute names once the +# OSC minimum requirements include SDK 1.0. class SetSubnetPool(command.Command): - """Set subnet pool properties""" + _description = _("Set subnet pool properties") def get_parser(self, prog_name): parser = super(SetSubnetPool, self).get_parser(prog_name) @@ -371,7 +387,7 @@ class SetSubnetPool(command.Command): class ShowSubnetPool(command.ShowOne): - """Display subnet pool details""" + _description = _("Display subnet pool details") def get_parser(self, prog_name): parser = super(ShowSubnetPool, self).get_parser(prog_name) @@ -388,13 +404,13 @@ class ShowSubnetPool(command.ShowOne): parsed_args.subnet_pool, ignore_missing=False ) - columns = _get_columns(obj) + display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (columns, data) + return (display_columns, data) class UnsetSubnetPool(command.Command): - """Unset subnet pool properties""" + _description = _("Unset subnet pool properties") def get_parser(self, prog_name): parser = super(UnsetSubnetPool, self).get_parser(prog_name) diff --git a/openstackclient/object/v1/account.py b/openstackclient/object/v1/account.py index 801fe450..4847f8bb 100644 --- a/openstackclient/object/v1/account.py +++ b/openstackclient/object/v1/account.py @@ -18,9 +18,11 @@ from osc_lib.command import command from osc_lib import utils import six +from openstackclient.i18n import _ + class SetAccount(command.Command): - """Set account properties""" + _description = _("Set account properties") def get_parser(self, prog_name): parser = super(SetAccount, self).get_parser(prog_name) @@ -29,8 +31,8 @@ class SetAccount(command.Command): metavar="<key=value>", required=True, action=parseractions.KeyValueAction, - help="Set a property on this account " - "(repeat option to set multiple properties)" + help=_("Set a property on this account " + "(repeat option to set multiple properties)") ) return parser @@ -41,7 +43,7 @@ class SetAccount(command.Command): class ShowAccount(command.ShowOne): - """Display account details""" + _description = _("Display account details") def take_action(self, parsed_args): data = self.app.client_manager.object_store.account_show() @@ -51,7 +53,7 @@ class ShowAccount(command.ShowOne): class UnsetAccount(command.Command): - """Unset account properties""" + _description = _("Unset account properties") def get_parser(self, prog_name): parser = super(UnsetAccount, self).get_parser(prog_name) @@ -61,8 +63,8 @@ class UnsetAccount(command.Command): required=True, action='append', default=[], - help='Property to remove from account ' - '(repeat option to remove multiple properties)', + help=_('Property to remove from account ' + '(repeat option to remove multiple properties)'), ) return parser diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 2f0d4ac2..88fb8602 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__) class CreateContainer(command.Lister): - """Create new container""" + _description = _("Create new container") def get_parser(self, prog_name): parser = super(CreateContainer, self).get_parser(prog_name) @@ -37,7 +37,7 @@ class CreateContainer(command.Lister): 'containers', metavar='<container-name>', nargs="+", - help='New container name(s)', + help=_('New container name(s)'), ) return parser @@ -63,7 +63,7 @@ class CreateContainer(command.Lister): class DeleteContainer(command.Command): - """Delete container""" + _description = _("Delete container") def get_parser(self, prog_name): parser = super(DeleteContainer, self).get_parser(prog_name) @@ -71,13 +71,13 @@ class DeleteContainer(command.Command): '--recursive', '-r', action='store_true', default=False, - help='Recursively delete objects and container', + help=_('Recursively delete objects and container'), ) parser.add_argument( 'containers', metavar='<container>', nargs="+", - help='Container(s) to delete', + help=_('Container(s) to delete'), ) return parser @@ -98,42 +98,42 @@ class DeleteContainer(command.Command): class ListContainer(command.Lister): - """List containers""" + _description = _("List containers") def get_parser(self, prog_name): parser = super(ListContainer, self).get_parser(prog_name) parser.add_argument( "--prefix", metavar="<prefix>", - help="Filter list using <prefix>", + help=_("Filter list using <prefix>"), ) parser.add_argument( "--marker", metavar="<marker>", - help="Anchor for paging", + help=_("Anchor for paging"), ) parser.add_argument( "--end-marker", metavar="<end-marker>", - help="End anchor for paging", + help=_("End anchor for paging"), ) parser.add_argument( "--limit", metavar="<limit>", type=int, - help="Limit the number of containers returned", + help=_("Limit the number of containers returned"), ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) parser.add_argument( '--all', action='store_true', default=False, - help='List all containers (default is 10000)', + help=_('List all containers (default is 10000)'), ) return parser @@ -168,14 +168,14 @@ class ListContainer(command.Lister): class SaveContainer(command.Command): - """Save container contents locally""" + _description = _("Save container contents locally") def get_parser(self, prog_name): parser = super(SaveContainer, self).get_parser(prog_name) parser.add_argument( 'container', metavar='<container>', - help='Container to save', + help=_('Container to save'), ) return parser @@ -186,22 +186,22 @@ class SaveContainer(command.Command): class SetContainer(command.Command): - """Set container properties""" + _description = _("Set container properties") def get_parser(self, prog_name): parser = super(SetContainer, self).get_parser(prog_name) parser.add_argument( 'container', metavar='<container>', - help='Container to modify', + 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)" + help=_("Set a property on this container " + "(repeat option to set multiple properties)") ) return parser @@ -213,14 +213,14 @@ class SetContainer(command.Command): class ShowContainer(command.ShowOne): - """Display container details""" + _description = _("Display container details") def get_parser(self, prog_name): parser = super(ShowContainer, self).get_parser(prog_name) parser.add_argument( 'container', metavar='<container>', - help='Container to display', + help=_('Container to display'), ) return parser @@ -236,14 +236,14 @@ class ShowContainer(command.ShowOne): class UnsetContainer(command.Command): - """Unset container properties""" + _description = _("Unset container properties") def get_parser(self, prog_name): parser = super(UnsetContainer, self).get_parser(prog_name) parser.add_argument( 'container', metavar='<container>', - help='Container to modify', + help=_('Container to modify'), ) parser.add_argument( '--property', @@ -251,8 +251,8 @@ class UnsetContainer(command.Command): required=True, action='append', default=[], - help='Property to remove from container ' - '(repeat option to remove multiple properties)', + help=_('Property to remove from container ' + '(repeat option to remove multiple properties)'), ) return parser diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index 88f6815e..71b6f520 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -30,26 +30,26 @@ LOG = logging.getLogger(__name__) class CreateObject(command.Lister): - """Upload object to container""" + _description = _("Upload object to container") def get_parser(self, prog_name): parser = super(CreateObject, self).get_parser(prog_name) parser.add_argument( 'container', metavar='<container>', - help='Container for new object', + help=_('Container for new object'), ) parser.add_argument( 'objects', metavar='<filename>', nargs="+", - help='Local filename(s) to upload', + help=_('Local filename(s) to upload'), ) parser.add_argument( '--name', metavar='<name>', - help='Upload a file and rename it. ' - 'Can only be used when uploading a single object' + help=_('Upload a file and rename it. ' + 'Can only be used when uploading a single object') ) return parser @@ -81,20 +81,20 @@ class CreateObject(command.Lister): class DeleteObject(command.Command): - """Delete object from container""" + _description = _("Delete object from container") def get_parser(self, prog_name): parser = super(DeleteObject, self).get_parser(prog_name) parser.add_argument( 'container', metavar='<container>', - help='Delete object(s) from <container>', + help=_('Delete object(s) from <container>'), ) parser.add_argument( 'objects', metavar='<object>', nargs="+", - help='Object(s) to delete', + help=_('Object(s) to delete'), ) return parser @@ -108,52 +108,52 @@ class DeleteObject(command.Command): class ListObject(command.Lister): - """List objects""" + _description = _("List objects") def get_parser(self, prog_name): parser = super(ListObject, self).get_parser(prog_name) parser.add_argument( "container", metavar="<container>", - help="Container to list", + help=_("Container to list"), ) parser.add_argument( "--prefix", metavar="<prefix>", - help="Filter list using <prefix>", + help=_("Filter list using <prefix>"), ) parser.add_argument( "--delimiter", metavar="<delimiter>", - help="Roll up items with <delimiter>", + help=_("Roll up items with <delimiter>"), ) parser.add_argument( "--marker", metavar="<marker>", - help="Anchor for paging", + help=_("Anchor for paging"), ) parser.add_argument( "--end-marker", metavar="<end-marker>", - help="End anchor for paging", + help=_("End anchor for paging"), ) parser.add_argument( "--limit", metavar="<limit>", type=int, - help="Limit the number of objects returned", + help=_("Limit the number of objects returned"), ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) parser.add_argument( '--all', action='store_true', default=False, - help='List all objects in container (default is 10000)', + help=_('List all objects in container (default is 10000)'), ) return parser @@ -197,24 +197,24 @@ class ListObject(command.Lister): class SaveObject(command.Command): - """Save object locally""" + _description = _("Save object locally") def get_parser(self, prog_name): parser = super(SaveObject, self).get_parser(prog_name) parser.add_argument( "--file", metavar="<filename>", - help="Destination filename (defaults to object name)", + help=_("Destination filename (defaults to object name)"), ) parser.add_argument( 'container', metavar='<container>', - help='Download <object> from <container>', + help=_('Download <object> from <container>'), ) parser.add_argument( "object", metavar="<object>", - help="Object to save", + help=_("Object to save"), ) return parser @@ -227,27 +227,27 @@ class SaveObject(command.Command): class SetObject(command.Command): - """Set object properties""" + _description = _("Set object properties") 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>', + help=_('Modify <object> from <container>'), ) parser.add_argument( 'object', metavar='<object>', - help='Object to modify', + 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)" + help=_("Set a property on this object " + "(repeat option to set multiple properties)") ) return parser @@ -260,19 +260,19 @@ class SetObject(command.Command): class ShowObject(command.ShowOne): - """Display object details""" + _description = _("Display object details") def get_parser(self, prog_name): parser = super(ShowObject, self).get_parser(prog_name) parser.add_argument( 'container', metavar='<container>', - help='Display <object> from <container>', + help=_('Display <object> from <container>'), ) parser.add_argument( 'object', metavar='<object>', - help='Object to display', + help=_('Object to display'), ) return parser @@ -289,19 +289,19 @@ class ShowObject(command.ShowOne): class UnsetObject(command.Command): - """Unset object properties""" + _description = _("Unset object properties") 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>', + help=_('Modify <object> from <container>'), ) parser.add_argument( 'object', metavar='<object>', - help='Object to modify', + help=_('Object to modify'), ) parser.add_argument( '--property', @@ -309,8 +309,8 @@ class UnsetObject(command.Command): required=True, action='append', default=[], - help='Property to remove from object ' - '(repeat option to remove multiple properties)', + help=_('Property to remove from object ' + '(repeat option to remove multiple properties)'), ) return parser diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 3971b6ef..d7fe6ac1 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -76,7 +76,9 @@ class OpenStackShell(shell.OpenStackShell): # Loop through extensions to get API versions for mod in clientmanager.PLUGIN_MODULES: default_version = getattr(mod, 'DEFAULT_API_VERSION', None) - option = mod.API_VERSION_OPTION.replace('os_', '') + # Only replace the first instance of "os", some service names will + # have "os" in their name, like: "antiddos" + option = mod.API_VERSION_OPTION.replace('os_', '', 1) version_opt = str(self.cloud.config.get(option, default_version)) if version_opt: api = mod.API_NAME @@ -93,10 +95,12 @@ class OpenStackShell(shell.OpenStackShell): mod_versions = getattr(mod, 'API_VERSIONS', None) if not skip_old_check and mod_versions: if version_opt not in mod_versions: + sorted_versions = sorted( + mod.API_VERSIONS.keys(), + key=lambda s: list(map(int, s.split('.')))) self.log.warning( - "%s version %s is not in supported versions %s" - % (api, version_opt, - ', '.join(list(mod.API_VERSIONS.keys())))) + "%s version %s is not in supported versions: %s" + % (api, version_opt, ', '.join(sorted_versions))) # Command groups deal only with major versions version = '.v' + version_opt.replace('.', '_').split('_')[0] @@ -146,10 +150,10 @@ class OpenStackShell(shell.OpenStackShell): 'auth_type': self._auth_type, }, ) - except (IOError, OSError) as e: + except (IOError, OSError): self.log.critical("Could not read clouds.yaml configuration file") self.print_help_if_requested() - raise e + raise if not self.options.debug: self.options.debug = None diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 298b2454..85743296 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -16,7 +16,6 @@ import shlex import subprocess import testtools -import six from tempest.lib.cli import output_parser from tempest.lib import exceptions @@ -63,8 +62,8 @@ class TestCase(testtools.TestCase): return cls.openstack('extension list ' + opts) @classmethod - def get_opts(cls, fields, format='value'): - return ' -f {0} {1}'.format(format, + def get_opts(cls, fields, output_format='value'): + return ' -f {0} {1}'.format(output_format, ' '.join(['-c ' + it for it in fields])) @classmethod @@ -77,29 +76,28 @@ class TestCase(testtools.TestCase): if expected not in actual: raise Exception(expected + ' not in ' + actual) + @classmethod + def assertsOutputNotNone(cls, observed): + if observed is None: + raise Exception('No output observed') + def assert_table_structure(self, items, field_names): """Verify that all items have keys listed in field_names.""" for item in items: for field in field_names: self.assertIn(field, item) - def assert_show_fields(self, items, field_names): + def assert_show_fields(self, show_output, field_names): """Verify that all items have keys listed in field_names.""" - for item in items: - for key in six.iterkeys(item): - self.assertIn(key, field_names) - - def assert_show_structure(self, items, field_names): - """Verify that all field_names listed in keys of all items.""" - if isinstance(items, list): - o = {} - for d in items: - o.update(d) - else: - o = items - item_keys = o.keys() - for field in field_names: - self.assertIn(field, item_keys) + + # field_names = ['name', 'description'] + # show_output = [{'name': 'fc2b98d8faed4126b9e371eda045ade2'}, + # {'description': 'description-821397086'}] + # this next line creates a flattened list of all 'keys' (like 'name', + # and 'description' out of the output + all_headers = [item for sublist in show_output for item in sublist] + for field_name in field_names: + self.assertIn(field_name, all_headers) def parse_show_as_object(self, raw_output): """Return a dict with values parsed from cli output.""" diff --git a/openstackclient/tests/functional/common/test_availability_zone.py b/openstackclient/tests/functional/common/test_availability_zone.py index f73e1ed9..025da95c 100644 --- a/openstackclient/tests/functional/common/test_availability_zone.py +++ b/openstackclient/tests/functional/common/test_availability_zone.py @@ -10,16 +10,23 @@ # License for the specific language governing permissions and limitations # under the License. +import json + from openstackclient.tests.functional import base class AvailabilityZoneTests(base.TestCase): """Functional tests for availability zone. """ - HEADERS = ["'Zone Name'"] - # So far, all components have the same default availability zone name. - DEFAULT_AZ_NAME = 'nova' def test_availability_zone_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('availability zone list' + opts) - self.assertIn(self.DEFAULT_AZ_NAME, raw_output) + cmd_output = json.loads(self.openstack( + 'availability zone list -f json')) + zones = [x['Zone Name'] for x in cmd_output] + self.assertIn( + 'internal', + zones + ) + self.assertIn( + 'nova', + zones + ) diff --git a/openstackclient/tests/functional/common/test_configuration.py b/openstackclient/tests/functional/common/test_configuration.py index f47d3b00..63a17d0e 100644 --- a/openstackclient/tests/functional/common/test_configuration.py +++ b/openstackclient/tests/functional/common/test_configuration.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import os from openstackclient.common import configuration @@ -20,25 +21,47 @@ BASIC_CONFIG_HEADERS = ['Field', 'Value'] class ConfigurationTests(base.TestCase): - - opts = "-f value -c auth.password" + """Functional test for configuration.""" def test_configuration_show(self): + + # Test show without option raw_output = self.openstack('configuration show') items = self.parse_listing(raw_output) self.assert_table_structure(items, BASIC_CONFIG_HEADERS) - def test_configuration_show_unmask(self): - raw_output = self.openstack('configuration show --unmask ' + self.opts) + cmd_output = json.loads(self.openstack( + 'configuration show -f json' + )) + self.assertEqual( + configuration.REDACTED, + cmd_output['auth.password'] + ) + + # Test show --mask + cmd_output = json.loads(self.openstack( + 'configuration show --mask -f json' + )) + self.assertEqual( + configuration.REDACTED, + cmd_output['auth.password'] + ) + + # Test show --unmask + cmd_output = json.loads(self.openstack( + 'configuration show --unmask -f json' + )) # If we are using os-client-config, this will not be set. Rather than # parse clouds.yaml to get the right value, just make sure # we are not getting redacted. passwd = os.environ.get('OS_PASSWORD') if passwd: - self.assertEqual(passwd + '\n', raw_output) + self.assertEqual( + passwd, + cmd_output['auth.password'] + ) else: - self.assertNotEqual(configuration.REDACTED + '\n', raw_output) - - def test_configuration_show_mask(self): - raw_output = self.openstack('configuration show --mask ' + self.opts) - self.assertEqual(configuration.REDACTED + '\n', raw_output) + self.assertNotEqual( + configuration.REDACTED, + cmd_output['auth.password'] + ) diff --git a/openstackclient/tests/functional/common/test_help.py b/openstackclient/tests/functional/common/test_help.py index bbc52197..211c52b1 100644 --- a/openstackclient/tests/functional/common/test_help.py +++ b/openstackclient/tests/functional/common/test_help.py @@ -64,3 +64,10 @@ class HelpTests(base.TestCase): raw_output = self.openstack('help server') for command in [row[0] for row in self.SERVER_COMMANDS]: self.assertIn(command, raw_output) + + def test_networking_commands_help(self): + """Check networking related commands in help message.""" + raw_output = self.openstack('help network list') + self.assertIn('List networks', raw_output) + raw_output = self.openstack('network create --help') + self.assertIn('Create new network', raw_output) diff --git a/openstackclient/tests/functional/common/test_module.py b/openstackclient/tests/functional/common/test_module.py new file mode 100644 index 00000000..e9e4ee3a --- /dev/null +++ b/openstackclient/tests/functional/common/test_module.py @@ -0,0 +1,79 @@ +# Copyright 2017 Huawei, Inc. All rights reserved. +# +# 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.tests.functional import base + + +class ModuleTest(base.TestCase): + """Functional tests for openstackclient module list output.""" + + CLIENTS = ['openstackclient', + 'keystoneclient', + 'novaclient', + 'openstack'] + + LIBS = ['osc_lib', + 'os_client_config', + 'keystoneauth1'] + + def test_module_list(self): + # Test module list + cmd_output = json.loads(self.openstack('module list -f json')) + for one_module in self.CLIENTS: + self.assertIn(one_module, cmd_output.keys()) + for one_module in self.LIBS: + self.assertNotIn(one_module, cmd_output.keys()) + + # Test module list --all + cmd_output = json.loads(self.openstack('module list --all -f json')) + for one_module in self.CLIENTS + self.LIBS: + self.assertIn(one_module, cmd_output.keys()) + + +class CommandTest(base.TestCase): + """Functional tests for openstackclient command list.""" + GROUPS = [ + 'openstack.volume.v2', + 'openstack.network.v2', + 'openstack.image.v2', + 'openstack.identity.v3', + 'openstack.compute.v2', + 'openstack.common', + 'openstack.cli', + ] + + def test_command_list_no_option(self): + cmd_output = json.loads(self.openstack('command list -f json')) + group_names = [each.get('Command Group') for each in cmd_output] + for one_group in self.GROUPS: + self.assertIn(one_group, group_names) + + def test_command_list_with_group(self): + input_groups = [ + 'volume', + 'network', + 'image', + 'identity', + 'compute.v2' + ] + for each_input in input_groups: + cmd_output = json.loads(self.openstack( + 'command list --group %s -f json' % each_input + )) + group_names = [each.get('Command Group') for each in cmd_output] + for each_name in group_names: + self.assertIn(each_input, each_name) diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index c1de9aa9..8092b3ce 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -25,6 +25,27 @@ class QuotaTests(base.TestCase): cls.PROJECT_NAME =\ cls.get_openstack_configuration_value('auth.project_name') + def test_quota_list_network_option(self): + self.openstack('quota set --networks 40 ' + + self.PROJECT_NAME) + raw_output = self.openstack('quota list --network') + self.assertIsNotNone(raw_output) + self.assertIn("40", raw_output) + + def test_quota_list_compute_option(self): + self.openstack('quota set --instances 40 ' + + self.PROJECT_NAME) + raw_output = self.openstack('quota list --compute') + self.assertIsNotNone(raw_output) + self.assertIn("40", raw_output) + + def test_quota_list_volume_option(self): + self.openstack('quota set --backups 40 ' + + self.PROJECT_NAME) + raw_output = self.openstack('quota list --volume') + self.assertIsNotNone(raw_output) + self.assertIn("40", raw_output) + def test_quota_set(self): self.openstack('quota set --instances 11 --volumes 11 --networks 11 ' + self.PROJECT_NAME) diff --git a/openstackclient/tests/functional/compute/v2/common.py b/openstackclient/tests/functional/compute/v2/common.py new file mode 100644 index 00000000..99d87bb4 --- /dev/null +++ b/openstackclient/tests/functional/compute/v2/common.py @@ -0,0 +1,145 @@ +# 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 +import time +import uuid + +from tempest.lib import exceptions + +from openstackclient.tests.functional import base + + +class ComputeTestCase(base.TestCase): + """Common functional test bits for Compute commands""" + + flavor_name = None + image_name = None + network_arg = None + + def setUp(self): + """Select common resources""" + super(ComputeTestCase, self).setUp() + self.flavor_name = self.get_flavor() + self.image_name = self.get_image() + self.network_arg = self.get_network() + + @classmethod + def get_flavor(cls): + # NOTE(rtheis): Get cirros256 or m1.tiny flavors since functional + # tests may create other flavors. + flavors = json.loads(cls.openstack( + "flavor list -f json " + )) + server_flavor = None + for flavor in flavors: + if flavor['Name'] in ['m1.tiny', 'cirros256']: + server_flavor = flavor['Name'] + break + return server_flavor + + @classmethod + def get_image(cls): + # NOTE(rtheis): Get first Cirros image since functional tests may + # create other images. Image may be named '-uec' or + # '-disk'. + images = json.loads(cls.openstack( + "image list -f json " + )) + server_image = None + for image in images: + if (image['Name'].startswith('cirros-') and + (image['Name'].endswith('-uec') or + image['Name'].endswith('-disk'))): + server_image = image['Name'] + break + return server_image + + @classmethod + def get_network(cls): + try: + # NOTE(rtheis): Get private network since functional tests may + # create other networks. + cmd_output = json.loads(cls.openstack( + 'network show private -f json' + )) + except exceptions.CommandFailed: + return '' + return '--nic net-id=' + cmd_output['id'] + + def server_create(self, name=None, cleanup=True): + """Create server, with cleanup""" + if not self.flavor_name: + self.flavor_name = self.get_flavor() + if not self.image_name: + self.image_name = self.get_image() + if not self.network_arg: + self.network_arg = self.get_network() + name = name or uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'server create -f json ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + self.network_arg + ' ' + + '--wait ' + + name + )) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + name, + cmd_output["name"], + ) + if cleanup: + self.addCleanup(self.server_delete, name) + return cmd_output + + def server_delete(self, name): + """Delete server by name""" + raw_output = self.openstack('server delete ' + name) + self.assertOutput('', raw_output) + + def wait_for_status( + self, + name, + expected_status='ACTIVE', + wait=900, + interval=10, + ): + """Wait until server reaches expected status""" + # TODO(thowe): Add a server wait command to osc + failures = ['ERROR'] + total_sleep = 0 + while total_sleep < wait: + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + name + )) + status = cmd_output['status'] + print('Waiting for {}, current status: {}'.format( + expected_status, + status, + )) + if status == expected_status: + break + self.assertNotIn(status, failures) + time.sleep(interval) + total_sleep += interval + + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + name + )) + status = cmd_output['status'] + self.assertEqual(status, expected_status) + # give it a little bit more time + time.sleep(5) diff --git a/openstackclient/tests/functional/compute/v2/test_agent.py b/openstackclient/tests/functional/compute/v2/test_agent.py index 7115db1f..1a112e82 100644 --- a/openstackclient/tests/functional/compute/v2/test_agent.py +++ b/openstackclient/tests/functional/compute/v2/test_agent.py @@ -11,6 +11,7 @@ # under the License. import hashlib +import json from openstackclient.tests.functional import base @@ -18,60 +19,178 @@ from openstackclient.tests.functional import base class ComputeAgentTests(base.TestCase): """Functional tests for compute agent.""" - ID = None - MD5HASH = hashlib.md5().hexdigest() - URL = "http://localhost" - VER = "v1" - OS = "TEST_OS" - ARCH = "x86_64" - HYPER = "kvm" - - HEADERS = ['agent_id', 'md5hash'] - FIELDS = ['agent_id', 'md5hash'] - - @classmethod - def setUpClass(cls): - opts = cls.get_opts(cls.HEADERS) - raw_output = cls.openstack('compute agent create ' + - cls.OS + ' ' + cls.ARCH + ' ' + - cls.VER + ' ' + cls.URL + ' ' + - cls.MD5HASH + ' ' + cls.HYPER + ' ' + - opts) - - # Get agent id because agent can only be deleted by ID - output_list = raw_output.split('\n', 1) - cls.ID = output_list[0] - - cls.assertOutput(cls.MD5HASH + '\n', output_list[1]) - - @classmethod - def tearDownClass(cls): - raw_output = cls.openstack('compute agent delete ' + cls.ID) - cls.assertOutput('', raw_output) - - def test_agent_list(self): - raw_output = self.openstack('compute agent list') - self.assertIn(self.ID, raw_output) - self.assertIn(self.OS, raw_output) - self.assertIn(self.ARCH, raw_output) - self.assertIn(self.VER, raw_output) - self.assertIn(self.URL, raw_output) - self.assertIn(self.MD5HASH, raw_output) - self.assertIn(self.HYPER, raw_output) - - def test_agent_set(self): - ver = 'v2' - url = "http://openstack" - md5hash = hashlib.md5().hexdigest() - - self.openstack('compute agent set ' - + self.ID - + ' --agent-version ' + ver - + ' --url ' + url - + ' --md5hash ' + md5hash) - - raw_output = self.openstack('compute agent list') - self.assertIn(self.ID, raw_output) - self.assertIn(ver, raw_output) - self.assertIn(url, raw_output) - self.assertIn(md5hash, raw_output) + # Generate two different md5hash + MD5HASH1 = hashlib.md5() + MD5HASH1.update('agent_1') + MD5HASH1 = MD5HASH1.hexdigest() + MD5HASH2 = hashlib.md5() + MD5HASH2.update('agent_2') + MD5HASH2 = MD5HASH2.hexdigest() + + def test_compute_agent_delete(self): + """Test compute agent create, delete multiple""" + os1 = "os_1" + arch1 = "x86_64" + ver1 = "v1" + url1 = "http://localhost" + md5hash1 = self.MD5HASH1 + hyper1 = "kvm" + cmd1 = ' '.join((os1, arch1, ver1, url1, md5hash1, hyper1)) + + cmd_output = json.loads(self.openstack( + 'compute agent create -f json ' + + cmd1 + )) + agent_id1 = str(cmd_output["agent_id"]) + + os2 = "os_2" + arch2 = "x86" + ver2 = "v2" + url2 = "http://openstack" + md5hash2 = self.MD5HASH2 + hyper2 = "xen" + cmd2 = ' '.join((os2, arch2, ver2, url2, md5hash2, hyper2)) + + cmd_output = json.loads(self.openstack( + 'compute agent create -f json ' + + cmd2 + )) + agent_id2 = str(cmd_output["agent_id"]) + + # Test compute agent delete + del_output = self.openstack( + 'compute agent delete ' + + agent_id1 + ' ' + agent_id2 + ) + self.assertOutput('', del_output) + + def test_compute_agent_list(self): + """Test compute agent create and list""" + os1 = "os_1" + arch1 = "x86_64" + ver1 = "v1" + url1 = "http://localhost" + md5hash1 = self.MD5HASH1 + hyper1 = "kvm" + cmd1 = ' '.join((os1, arch1, ver1, url1, md5hash1, hyper1)) + + cmd_output = json.loads(self.openstack( + 'compute agent create -f json ' + + cmd1 + )) + agent_id1 = str(cmd_output["agent_id"]) + self.addCleanup(self.openstack, 'compute agent delete ' + agent_id1) + + os2 = "os_2" + arch2 = "x86" + ver2 = "v2" + url2 = "http://openstack" + md5hash2 = self.MD5HASH2 + hyper2 = "xen" + cmd2 = ' '.join((os2, arch2, ver2, url2, md5hash2, hyper2)) + + cmd_output = json.loads(self.openstack( + 'compute agent create -f json ' + + cmd2 + )) + agent_id2 = str(cmd_output["agent_id"]) + self.addCleanup(self.openstack, 'compute agent delete ' + agent_id2) + + # Test compute agent list + cmd_output = json.loads(self.openstack( + 'compute agent list -f json' + )) + + hypervisors = [x["Hypervisor"] for x in cmd_output] + self.assertIn(hyper1, hypervisors) + self.assertIn(hyper2, hypervisors) + + os = [x['OS'] for x in cmd_output] + self.assertIn(os1, os) + self.assertIn(os2, os) + + archs = [x['Architecture'] for x in cmd_output] + self.assertIn(arch1, archs) + self.assertIn(arch2, archs) + + versions = [x['Version'] for x in cmd_output] + self.assertIn(ver1, versions) + self.assertIn(ver2, versions) + + md5hashes = [x['Md5Hash'] for x in cmd_output] + self.assertIn(md5hash1, md5hashes) + self.assertIn(md5hash2, md5hashes) + + urls = [x['URL'] for x in cmd_output] + self.assertIn(url1, urls) + self.assertIn(url2, urls) + + # Test compute agent list --hypervisor + cmd_output = json.loads(self.openstack( + 'compute agent list -f json ' + + '--hypervisor kvm' + )) + + hypervisors = [x["Hypervisor"] for x in cmd_output] + self.assertIn(hyper1, hypervisors) + self.assertNotIn(hyper2, hypervisors) + + os = [x['OS'] for x in cmd_output] + self.assertIn(os1, os) + self.assertNotIn(os2, os) + + archs = [x['Architecture'] for x in cmd_output] + self.assertIn(arch1, archs) + self.assertNotIn(arch2, archs) + + versions = [x['Version'] for x in cmd_output] + self.assertIn(ver1, versions) + self.assertNotIn(ver2, versions) + + md5hashes = [x['Md5Hash'] for x in cmd_output] + self.assertIn(md5hash1, md5hashes) + self.assertNotIn(md5hash2, md5hashes) + + urls = [x['URL'] for x in cmd_output] + self.assertIn(url1, urls) + self.assertNotIn(url2, urls) + + def test_compute_agent_set(self): + """Test compute agent set""" + os1 = "os_1" + arch1 = "x86_64" + ver1 = "v1" + ver2 = "v2" + url1 = "http://localhost" + url2 = "http://openstack" + md5hash1 = self.MD5HASH1 + md5hash2 = self.MD5HASH2 + hyper1 = "kvm" + cmd = ' '.join((os1, arch1, ver1, url1, md5hash1, hyper1)) + + cmd_output = json.loads(self.openstack( + 'compute agent create -f json ' + + cmd + )) + agent_id = str(cmd_output["agent_id"]) + self.assertEqual(ver1, cmd_output["version"]) + self.assertEqual(url1, cmd_output["url"]) + self.assertEqual(md5hash1, cmd_output["md5hash"]) + + self.addCleanup(self.openstack, 'compute agent delete ' + agent_id) + + raw_output = self.openstack( + 'compute agent set ' + + agent_id + ' ' + + '--agent-version ' + ver2 + ' ' + + '--url ' + url2 + ' ' + + '--md5hash ' + md5hash2 + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'compute agent list -f json' + )) + self.assertEqual(ver2, cmd_output[0]["Version"]) + self.assertEqual(url2, cmd_output[0]["URL"]) + self.assertEqual(md5hash2, cmd_output[0]["Md5Hash"]) diff --git a/openstackclient/tests/functional/compute/v2/test_aggregate.py b/openstackclient/tests/functional/compute/v2/test_aggregate.py index 2bc88e7b..95068fc2 100644 --- a/openstackclient/tests/functional/compute/v2/test_aggregate.py +++ b/openstackclient/tests/functional/compute/v2/test_aggregate.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from openstackclient.tests.functional import base @@ -18,50 +19,164 @@ from openstackclient.tests.functional import base class AggregateTests(base.TestCase): """Functional tests for aggregate.""" - NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['name'] + def test_aggregate_create_and_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'aggregate create -f json ' + + '--zone nova ' + + name1)) + self.assertEqual( + name1, + cmd_output['name'] + ) + self.assertEqual( + 'nova', + cmd_output['availability_zone'] + ) - @classmethod - def setUpClass(cls): - opts = cls.get_opts(cls.FIELDS) - # Use the default 'nova' availability zone for the aggregate. - raw_output = cls.openstack( - 'aggregate create --zone nova ' + cls.NAME + opts + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'aggregate create -f json ' + + '--zone nova ' + + name2)) + self.assertEqual( + name2, + cmd_output['name'] + ) + self.assertEqual( + 'nova', + cmd_output['availability_zone'] ) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) - @classmethod - def tearDownClass(cls): - raw_output = cls.openstack('aggregate delete ' + cls.NAME) - cls.assertOutput('', raw_output) + del_output = self.openstack( + 'aggregate delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) def test_aggregate_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('aggregate list' + opts) - self.assertIn(self.NAME, raw_output) + """Test aggregate list""" + name1 = uuid.uuid4().hex + self.openstack( + 'aggregate create ' + + '--zone nova ' + + '--property a=b ' + + name1) + self.addCleanup(self.openstack, 'aggregate delete ' + name1) + + name2 = uuid.uuid4().hex + self.openstack( + 'aggregate create ' + + '--zone internal ' + + '--property c=d ' + + name2) + self.addCleanup(self.openstack, 'aggregate delete ' + name2) - def test_aggregate_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('aggregate show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + cmd_output = json.loads(self.openstack( + 'aggregate list -f json')) + names = [x['Name'] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + zones = [x['Availability Zone'] for x in cmd_output] + self.assertIn('nova', zones) + self.assertIn('internal', zones) - def test_aggregate_properties(self): - opts = self.get_opts(['properties']) + # Test aggregate list --long + cmd_output = json.loads(self.openstack( + 'aggregate list --long -f json')) + names = [x['Name'] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + zones = [x['Availability Zone'] for x in cmd_output] + self.assertIn('nova', zones) + self.assertIn('internal', zones) + properties = [x['Properties'] for x in cmd_output] + self.assertIn({'a': 'b'}, properties) + self.assertIn({'c': 'd'}, properties) + + def test_aggregate_set_and_unset(self): + """Test aggregate set, show and unset""" + name1 = uuid.uuid4().hex + name2 = uuid.uuid4().hex + self.openstack( + 'aggregate create ' + + '--zone nova ' + + '--property a=b ' + + name1) + self.addCleanup(self.openstack, 'aggregate delete ' + name2) raw_output = self.openstack( - 'aggregate set --property a=b --property c=d ' + self.NAME + 'aggregate set --name ' + + name2 + + ' --zone internal ' + + '--no-property ' + + '--property c=d ' + + name1 ) - self.assertEqual('', raw_output) + self.assertOutput('', raw_output) - raw_output = self.openstack('aggregate show ' + self.NAME + opts) - self.assertIn("a='b', c='d'\n", raw_output) + cmd_output = json.loads(self.openstack( + 'aggregate show -f json ' + name2)) + self.assertEqual( + name2, + cmd_output['name'] + ) + self.assertEqual( + 'internal', + cmd_output['availability_zone'] + ) + self.assertIn( + "c='d'", + cmd_output['properties'] + ) + self.assertNotIn( + "a='b'", + cmd_output['properties'] + ) + # Test unset raw_output = self.openstack( - 'aggregate unset --property a ' + self.NAME + 'aggregate unset --property c ' + + name2 + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'aggregate show -f json ' + name2)) + self.assertNotIn( + "c='d'", + cmd_output['properties'] + ) + + def test_aggregate_add_and_remove_host(self): + """Test aggregate add and remove host""" + name = uuid.uuid4().hex + self.openstack( + 'aggregate create ' + name) + self.addCleanup(self.openstack, 'aggregate delete ' + name) + + # Get a host + cmd_output = json.loads(self.openstack( + 'host list -f json')) + host_name = cmd_output[0]['Host Name'] + + # Test add host + cmd_output = json.loads(self.openstack( + 'aggregate add host -f json ' + + name + ' ' + + host_name + )) + self.assertIn( + host_name, + cmd_output['hosts'] ) - self.assertEqual('', raw_output) - raw_output = self.openstack('aggregate show ' + self.NAME + opts) - self.assertIn("c='d'\n", raw_output) + # Test remove host + cmd_output = json.loads(self.openstack( + 'aggregate remove host -f json ' + + name + ' ' + + host_name + )) + self.assertNotIn( + host_name, + cmd_output['hosts'] + ) diff --git a/openstackclient/tests/functional/compute/v2/test_flavor.py b/openstackclient/tests/functional/compute/v2/test_flavor.py index 794a6cc3..0b01da51 100644 --- a/openstackclient/tests/functional/compute/v2/test_flavor.py +++ b/openstackclient/tests/functional/compute/v2/test_flavor.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from openstackclient.tests.functional import base @@ -18,52 +19,224 @@ from openstackclient.tests.functional import base class FlavorTests(base.TestCase): """Functional tests for flavor.""" - NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['name'] + PROJECT_NAME = uuid.uuid4().hex @classmethod def setUpClass(cls): - opts = cls.get_opts(cls.FIELDS) - raw_output = cls.openstack( - 'flavor create --property a=b --property c=d ' + cls.NAME + opts) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) + # Make a project + cmd_output = json.loads(cls.openstack( + "project create -f json --enable " + cls.PROJECT_NAME + )) + cls.project_id = cmd_output["id"] @classmethod def tearDownClass(cls): - raw_output = cls.openstack('flavor delete ' + cls.NAME) + raw_output = cls.openstack("project delete " + cls.PROJECT_NAME) cls.assertOutput('', raw_output) + def test_flavor_delete(self): + """Test create w/project, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + "flavor create -f json " + + "--project " + self.PROJECT_NAME + " " + + "--private " + + name1 + )) + self.assertIsNotNone(cmd_output["id"]) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + "flavor create -f json " + + "--id qaz " + + "--project " + self.PROJECT_NAME + " " + + "--private " + + name2 + )) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + "qaz", + cmd_output["id"], + ) + + raw_output = self.openstack( + "flavor delete " + name1 + " " + name2, + ) + self.assertOutput('', raw_output) + def test_flavor_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('flavor list' + opts) - self.assertIn("small", raw_output) - self.assertIn(self.NAME, raw_output) + """Test create defaults, list filters, delete""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + "flavor create -f json " + + "--property a=b " + + "--property c=d " + + name1 + )) + self.addCleanup(self.openstack, "flavor delete " + name1) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + name1, + cmd_output["name"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + "flavor create -f json " + + "--id qaz " + + "--ram 123 " + + "--private " + + "--property a=b2 " + + "--property b=d2 " + + name2 + )) + self.addCleanup(self.openstack, "flavor delete " + name2) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + "qaz", + cmd_output["id"], + ) + self.assertEqual( + name2, + cmd_output["name"], + ) + self.assertEqual( + 123, + cmd_output["ram"], + ) + self.assertEqual( + 0, + cmd_output["disk"], + ) + self.assertEqual( + False, + cmd_output["os-flavor-access:is_public"], + ) + self.assertEqual( + "a='b2', b='d2'", + cmd_output["properties"], + ) - def test_flavor_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('flavor show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + # Test list + cmd_output = json.loads(self.openstack( + "flavor list -f json" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertNotIn(name2, col_name) + + # Test list --long + cmd_output = json.loads(self.openstack( + "flavor list -f json " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + col_properties = [x['Properties'] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertIn("a='b', c='d'", col_properties) + self.assertNotIn(name2, col_name) + self.assertNotIn("b2', b='d2'", col_properties) + + # Test list --public + cmd_output = json.loads(self.openstack( + "flavor list -f json " + + "--public" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertNotIn(name2, col_name) + + # Test list --private + cmd_output = json.loads(self.openstack( + "flavor list -f json " + + "--private" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, col_name) + self.assertIn(name2, col_name) + + # Test list --all + cmd_output = json.loads(self.openstack( + "flavor list -f json " + + "--all" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertIn(name2, col_name) def test_flavor_properties(self): - opts = self.get_opts(['properties']) - # check the properties we added in create command. - raw_output = self.openstack('flavor show ' + self.NAME + opts) - self.assertEqual("a='b', c='d'\n", raw_output) + """Test create defaults, list filters, delete""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + "flavor create -f json " + + "--id qaz " + + "--ram 123 " + + "--disk 20 " + + "--private " + + "--property a=first " + + "--property b=second " + + name1 + )) + self.addCleanup(self.openstack, "flavor delete " + name1) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + "qaz", + cmd_output["id"], + ) + self.assertEqual( + name1, + cmd_output["name"], + ) + self.assertEqual( + 123, + cmd_output["ram"], + ) + self.assertEqual( + 20, + cmd_output["disk"], + ) + self.assertEqual( + False, + cmd_output["os-flavor-access:is_public"], + ) + self.assertEqual( + "a='first', b='second'", + cmd_output["properties"], + ) raw_output = self.openstack( - 'flavor set --property e=f --property g=h ' + self.NAME + "flavor set " + + "--property a='third and 10' " + + "--property g=fourth " + + name1 ) self.assertEqual('', raw_output) - raw_output = self.openstack('flavor show ' + self.NAME + opts) - self.assertEqual("a='b', c='d', e='f', g='h'\n", raw_output) + cmd_output = json.loads(self.openstack( + "flavor show -f json " + + name1 + )) + self.assertEqual( + "qaz", + cmd_output["id"], + ) + self.assertEqual( + "a='third and 10', b='second', g='fourth'", + cmd_output['properties'], + ) raw_output = self.openstack( - 'flavor unset --property a --property c ' + self.NAME + "flavor unset " + + "--property b " + + name1 ) self.assertEqual('', raw_output) - raw_output = self.openstack('flavor show ' + self.NAME + opts) - self.assertEqual("e='f', g='h'\n", raw_output) + cmd_output = json.loads(self.openstack( + "flavor show -f json " + + name1 + )) + self.assertEqual( + "a='third and 10', g='fourth'", + cmd_output["properties"], + ) diff --git a/openstackclient/tests/functional/compute/v2/test_keypair.py b/openstackclient/tests/functional/compute/v2/test_keypair.py index 01078c61..1e1a03d6 100644 --- a/openstackclient/tests/functional/compute/v2/test_keypair.py +++ b/openstackclient/tests/functional/compute/v2/test_keypair.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import tempfile from openstackclient.tests.functional import base @@ -100,6 +101,26 @@ class KeypairTests(KeypairBase): ) self.assertIn('tmpkey', raw_output) + def test_keypair_create_private_key(self): + """Test for create keypair with --private-key option. + + Test steps: + 1) Create keypair with private key file + 2) Delete keypair + """ + with tempfile.NamedTemporaryFile() as f: + cmd_output = json.loads(self.openstack( + 'keypair create -f json --private-key %s tmpkey' % f.name, + )) + self.addCleanup(self.openstack, 'keypair delete tmpkey') + self.assertEqual('tmpkey', cmd_output.get('name')) + self.assertIsNotNone(cmd_output.get('user_id')) + self.assertIsNotNone(cmd_output.get('fingerprint')) + pk_content = f.read() + self.assertInOutput('-----BEGIN RSA PRIVATE KEY-----', pk_content) + self.assertRegex(pk_content, "[0-9A-Za-z+/]+[=]{0,3}\n") + self.assertInOutput('-----END RSA PRIVATE KEY-----', pk_content) + def test_keypair_create(self): """Test keypair create command. diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 6eedf408..f152de80 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -10,299 +10,398 @@ # License for the specific language governing permissions and limitations # under the License. -import time +import json +import uuid -from tempest.lib.common.utils import data_utils - -from openstackclient.tests.functional import base from tempest.lib import exceptions +from openstackclient.tests.functional.compute.v2 import common +from openstackclient.tests.functional.volume.v2 import test_volume -class ServerTests(base.TestCase): - """Functional tests for openstack server commands.""" - - @classmethod - def get_flavor(cls): - # NOTE(rtheis): Get cirros256 or m1.tiny flavors since functional - # tests may create other flavors. - flavors = cls.openstack('flavor list -c Name -f value').split('\n') - server_flavor = None - for flavor in flavors: - if flavor in ['m1.tiny', 'cirros256']: - server_flavor = flavor - break - return server_flavor - - @classmethod - def get_image(cls): - # NOTE(rtheis): Get cirros image since functional tests may - # create other images. - images = cls.openstack('image list -c Name -f value').split('\n') - server_image = None - for image in images: - if image.startswith('cirros-') and image.endswith('-uec'): - server_image = image - break - return server_image - - @classmethod - def get_network(cls): - try: - # NOTE(rtheis): Get private network since functional tests may - # create other networks. - raw_output = cls.openstack('network show private -c id -f value') - except exceptions.CommandFailed: - return '' - return ' --nic net-id=' + raw_output.strip('\n') - - def server_create(self, name=None): - """Create server. Add cleanup.""" - name = name or data_utils.rand_uuid() - opts = self.get_opts(self.FIELDS) - flavor = self.get_flavor() - image = self.get_image() - network = self.get_network() - raw_output = self.openstack('--debug server create --flavor ' + - flavor + - ' --image ' + image + network + ' ' + - name + opts) - if not raw_output: - self.fail('Server has not been created!') - self.addCleanup(self.server_delete, name) - - def server_list(self, params=[]): - """List servers.""" - opts = self.get_opts(params) - return self.openstack('server list' + opts) - - def server_delete(self, name): - """Delete server by name.""" - self.openstack('server delete ' + name) - - def setUp(self): - """Set necessary variables and create server.""" - super(ServerTests, self).setUp() - self.NAME = data_utils.rand_name('TestServer') - self.OTHER_NAME = data_utils.rand_name('TestServer') - self.HEADERS = ['"Name"'] - self.FIELDS = ['name'] - self.IP_POOL = 'public' - self.server_create(self.NAME) - - def test_server_rename(self): - """Test server rename command. - - Test steps: - 1) Boot server in setUp - 2) Rename server - 3) Check output - 4) Rename server back to original name - """ - raw_output = self.openstack('server set --name ' + self.OTHER_NAME + - ' ' + self.NAME) - self.assertOutput("", raw_output) - self.assertNotIn(self.NAME, self.server_list(['Name'])) - self.assertIn(self.OTHER_NAME, self.server_list(['Name'])) - self.openstack('server set --name ' + self.NAME + ' ' + - self.OTHER_NAME) + +class ServerTests(common.ComputeTestCase): + """Functional tests for openstack server commands""" def test_server_list(self): - """Test server list command. + """Test server list, set""" + cmd_output = self.server_create() + name1 = cmd_output['name'] + cmd_output = self.server_create() + name2 = cmd_output['name'] + self.wait_for_status(name1, "ACTIVE") + self.wait_for_status(name2, "ACTIVE") + + cmd_output = json.loads(self.openstack( + 'server list -f json' + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertIn(name2, col_name) + + # Test list --status PAUSED + raw_output = self.openstack('server pause ' + name2) + self.assertEqual("", raw_output) + self.wait_for_status(name2, "PAUSED") + cmd_output = json.loads(self.openstack( + 'server list -f json ' + + '--status ACTIVE' + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertNotIn(name2, col_name) + cmd_output = json.loads(self.openstack( + 'server list -f json ' + + '--status PAUSED' + )) + col_name = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, col_name) + self.assertIn(name2, col_name) + + def test_server_set(self): + """Test server create, delete, set, show""" + cmd_output = self.server_create() + name = cmd_output['name'] + # self.wait_for_status(name, "ACTIVE") + + # Have a look at some other fields + flavor = json.loads(self.openstack( + 'flavor show -f json ' + + self.flavor_name + )) + self.assertEqual( + self.flavor_name, + flavor['name'], + ) + self.assertEqual( + '%s (%s)' % (flavor['name'], flavor['id']), + cmd_output["flavor"], + ) + image = json.loads(self.openstack( + 'image show -f json ' + + self.image_name + )) + self.assertEqual( + self.image_name, + image['name'], + ) + self.assertEqual( + '%s (%s)' % (image['name'], image['id']), + cmd_output["image"], + ) + + # Test properties set + raw_output = self.openstack( + 'server set ' + + '--property a=b --property c=d ' + + name + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + name + )) + # Really, shouldn't this be a list? + self.assertEqual( + "a='b', c='d'", + cmd_output['properties'], + ) - Test steps: - 1) Boot server in setUp - 2) List servers - 3) Check output - """ - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('server list' + opts) - self.assertIn(self.NAME, raw_output) + raw_output = self.openstack( + 'server unset ' + + '--property a ' + + name + ) + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + name + )) + self.assertEqual( + "c='d'", + cmd_output['properties'], + ) + + # Test set --name + new_name = uuid.uuid4().hex + raw_output = self.openstack( + 'server set ' + + '--name ' + new_name + ' ' + + name + ) + self.assertOutput("", raw_output) + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + new_name + )) + self.assertEqual( + new_name, + cmd_output["name"], + ) + # Put it back so we clean up properly + raw_output = self.openstack( + 'server set ' + + '--name ' + name + ' ' + + new_name + ) + self.assertOutput("", raw_output) - def test_server_show(self): - """Test server show command. + def test_server_actions(self): + """Test server action pairs - Test steps: - 1) Boot server in setUp - 2) Show server - 3) Check output - """ - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('server show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) - - def test_server_metadata(self): - """Test command to set server metadata. - - Test steps: - 1) Boot server in setUp - 2) Set properties for server - 3) Check server properties in server show output - 4) Unset properties for server - 5) Check server properties in server show output + suspend/resume + pause/unpause + rescue/unrescue + lock/unlock """ - self.wait_for_status("ACTIVE") - # metadata - raw_output = self.openstack( - 'server set --property a=b --property c=d ' + self.NAME) - opts = self.get_opts(["name", "properties"]) - raw_output = self.openstack('server show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\na='b', c='d'\n", raw_output) + cmd_output = self.server_create() + name = cmd_output['name'] - raw_output = self.openstack( - 'server unset --property a ' + self.NAME) - opts = self.get_opts(["name", "properties"]) - raw_output = self.openstack('server show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\nc='d'\n", raw_output) - - def test_server_suspend_resume(self): - """Test server suspend and resume commands. - - Test steps: - 1) Boot server in setUp - 2) Suspend server - 3) Check for SUSPENDED server status - 4) Resume server - 5) Check for ACTIVE server status - """ - self.wait_for_status("ACTIVE") # suspend - raw_output = self.openstack('server suspend ' + self.NAME) - self.assertEqual("", raw_output) - self.wait_for_status("SUSPENDED") - # resume - raw_output = self.openstack('server resume ' + self.NAME) + raw_output = self.openstack('server suspend ' + name) self.assertEqual("", raw_output) - self.wait_for_status("ACTIVE") - - def test_server_lock_unlock(self): - """Test server lock and unlock commands. + self.wait_for_status(name, "SUSPENDED") - Test steps: - 1) Boot server in setUp - 2) Lock server - 3) Check output - 4) Unlock server - 5) Check output - """ - self.wait_for_status("ACTIVE") - # lock - raw_output = self.openstack('server lock ' + self.NAME) - self.assertEqual("", raw_output) - # unlock - raw_output = self.openstack('server unlock ' + self.NAME) + # resume + raw_output = self.openstack('server resume ' + name) self.assertEqual("", raw_output) + self.wait_for_status(name, "ACTIVE") - def test_server_pause_unpause(self): - """Test server pause and unpause commands. - - Test steps: - 1) Boot server in setUp - 2) Pause server - 3) Check for PAUSED server status - 4) Unpause server - 5) Check for ACTIVE server status - """ - self.wait_for_status("ACTIVE") # pause - raw_output = self.openstack('server pause ' + self.NAME) + raw_output = self.openstack('server pause ' + name) self.assertEqual("", raw_output) - self.wait_for_status("PAUSED") + self.wait_for_status(name, "PAUSED") + # unpause - raw_output = self.openstack('server unpause ' + self.NAME) + raw_output = self.openstack('server unpause ' + name) self.assertEqual("", raw_output) - self.wait_for_status("ACTIVE") - - def test_server_rescue_unrescue(self): - """Test server rescue and unrescue commands. + self.wait_for_status(name, "ACTIVE") - Test steps: - 1) Boot server in setUp - 2) Rescue server - 3) Check for RESCUE server status - 4) Unrescue server - 5) Check for ACTIVE server status - """ - self.wait_for_status("ACTIVE") # rescue - opts = self.get_opts(["adminPass"]) - raw_output = self.openstack('server rescue ' + self.NAME + opts) + raw_output = self.openstack('server rescue ' + name) self.assertNotEqual("", raw_output) - self.wait_for_status("RESCUE") + self.wait_for_status(name, "RESCUE") + # unrescue - raw_output = self.openstack('server unrescue ' + self.NAME) + raw_output = self.openstack('server unrescue ' + name) + self.assertEqual("", raw_output) + self.wait_for_status(name, "ACTIVE") + + # lock + raw_output = self.openstack('server lock ' + name) self.assertEqual("", raw_output) - self.wait_for_status("ACTIVE") + # NOTE(dtroyer): No way to verify this status??? + + # unlock + raw_output = self.openstack('server unlock ' + name) + self.assertEqual("", raw_output) + # NOTE(dtroyer): No way to verify this status??? def test_server_attach_detach_floating_ip(self): - """Test commands to attach and detach floating IP for server. - - Test steps: - 1) Boot server in setUp - 2) Create floating IP - 3) Add floating IP to server - 4) Check for floating IP in server show output - 5) Remove floating IP from server - 6) Check that floating IP is not in server show output - 7) Delete floating IP - 8) Check output - """ - self.wait_for_status("ACTIVE") + """Test floating ip create/delete; server add/remove floating ip""" + cmd_output = self.server_create() + name = cmd_output['name'] + self.wait_for_status(name, "ACTIVE") + # attach ip - opts = self.get_opts(["id", "floating_ip_address"]) - raw_output = self.openstack('floating ip create ' + - self.IP_POOL + - opts) - ip, ipid, rol = tuple(raw_output.split('\n')) - self.assertNotEqual("", ipid) - self.assertNotEqual("", ip) - raw_output = self.openstack('server add floating ip ' + self.NAME + - ' ' + ip) + cmd_output = json.loads(self.openstack( + 'floating ip create -f json ' + + 'public' + )) + floating_ip = cmd_output['floating_ip_address'] + self.assertNotEqual('', cmd_output['id']) + self.assertNotEqual('', floating_ip) + self.addCleanup( + self.openstack, + 'floating ip delete ' + cmd_output['id'] + ) + + raw_output = self.openstack( + 'server add floating ip ' + + name + ' ' + + floating_ip + ) self.assertEqual("", raw_output) - raw_output = self.openstack('server show ' + self.NAME) - self.assertIn(ip, raw_output) + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + name + )) + self.assertIn( + floating_ip, + cmd_output['addresses'], + ) # detach ip - raw_output = self.openstack('server remove floating ip ' + self.NAME + - ' ' + ip) - self.assertEqual("", raw_output) - raw_output = self.openstack('server show ' + self.NAME) - self.assertNotIn(ip, raw_output) - raw_output = self.openstack('floating ip delete ' + ipid) + raw_output = self.openstack( + 'server remove floating ip ' + + name + ' ' + + floating_ip + ) self.assertEqual("", raw_output) + cmd_output = json.loads(self.openstack( + 'server show -f json ' + + name + )) + self.assertNotIn( + floating_ip, + cmd_output['addresses'], + ) + def test_server_reboot(self): - """Test server reboot command. + """Test server reboot""" + cmd_output = self.server_create() + name = cmd_output['name'] - Test steps: - 1) Boot server in setUp - 2) Reboot server - 3) Check for ACTIVE server status - """ - self.wait_for_status("ACTIVE") # reboot - raw_output = self.openstack('server reboot ' + self.NAME) + raw_output = self.openstack('server reboot ' + name) self.assertEqual("", raw_output) - self.wait_for_status("ACTIVE") - - def wait_for_status(self, expected_status='ACTIVE', wait=900, interval=30): - """Wait until server reaches expected status.""" - # TODO(thowe): Add a server wait command to osc - failures = ['ERROR'] - total_sleep = 0 - opts = self.get_opts(['status']) - while total_sleep < wait: - status = self.openstack('server show ' + self.NAME + opts) - status = status.rstrip() - print('Waiting for {} current status: {}'.format(expected_status, - status)) - if status == expected_status: - break - self.assertNotIn(status, failures) - time.sleep(interval) - total_sleep += interval - - status = self.openstack('server show ' + self.NAME + opts) - status = status.rstrip() - self.assertEqual(status, expected_status) - # give it a little bit more time - time.sleep(5) + self.wait_for_status(name, "ACTIVE") + + def test_server_boot_from_volume(self): + """Test server create from volume, server delete""" + # get volume status wait function + volume_wait_for = test_volume.VolumeTests( + methodName='wait_for', + ).wait_for + + # get image size + cmd_output = json.loads(self.openstack( + 'image show -f json ' + + self.image_name + )) + try: + image_size = cmd_output['min_disk'] + if image_size < 1: + image_size = 1 + except ValueError: + image_size = 1 + + # create volume from image + volume_name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--image ' + self.image_name + ' ' + + '--size ' + str(image_size) + ' ' + + volume_name + )) + self.assertIsNotNone(cmd_output["id"]) + self.addCleanup(self.openstack, 'volume delete ' + volume_name) + self.assertEqual( + volume_name, + cmd_output['name'], + ) + volume_wait_for("volume", volume_name, "available") + + # create empty volume + empty_volume_name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size ' + str(image_size) + ' ' + + empty_volume_name + )) + self.assertIsNotNone(cmd_output["id"]) + self.addCleanup(self.openstack, 'volume delete ' + empty_volume_name) + self.assertEqual( + empty_volume_name, + cmd_output['name'], + ) + volume_wait_for("volume", empty_volume_name, "available") + + # create server + server_name = uuid.uuid4().hex + server = json.loads(self.openstack( + 'server create -f json ' + + '--flavor ' + self.flavor_name + ' ' + + '--volume ' + volume_name + ' ' + + '--block-device-mapping vdb=' + empty_volume_name + ' ' + + self.network_arg + ' ' + + '--wait ' + + server_name + )) + self.assertIsNotNone(server["id"]) + self.addCleanup(self.openstack, 'server delete --wait ' + server_name) + self.assertEqual( + server_name, + server['name'], + ) + + # check volumes + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + volume_name + )) + attachments = cmd_output['attachments'] + self.assertEqual( + 1, + len(attachments), + ) + self.assertEqual( + server['id'], + attachments[0]['server_id'], + ) + self.assertEqual( + "in-use", + cmd_output['status'], + ) + + # NOTE(dtroyer): Prior to https://review.openstack.org/#/c/407111 + # --block-device-mapping was ignored if --volume + # present on the command line. Now we should see the + # attachment. + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + empty_volume_name + )) + attachments = cmd_output['attachments'] + self.assertEqual( + 1, + len(attachments), + ) + self.assertEqual( + server['id'], + attachments[0]['server_id'], + ) + self.assertEqual( + "in-use", + cmd_output['status'], + ) + + def test_server_create_with_none_network(self): + """Test server create with none network option.""" + server_name = uuid.uuid4().hex + server = json.loads(self.openstack( + # auto/none enable in nova micro version (v2.37+) + '--os-compute-api-version 2.latest ' + + 'server create -f json ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + '--nic none ' + + server_name + )) + self.assertIsNotNone(server["id"]) + self.addCleanup(self.openstack, 'server delete --wait ' + server_name) + self.assertEqual(server_name, server['name']) + self.wait_for_status(server_name, "ACTIVE") + server = json.loads(self.openstack( + 'server show -f json ' + server_name + )) + self.assertIsNotNone(server['addresses']) + self.assertEqual('', server['addresses']) + + def test_server_create_with_empty_network_option_latest(self): + """Test server create with empty network option in nova 2.latest.""" + server_name = uuid.uuid4().hex + try: + self.openstack( + # auto/none enable in nova micro version (v2.37+) + '--os-compute-api-version 2.latest ' + + 'server create -f json ' + + '--flavor ' + self.flavor_name + ' ' + + '--image ' + self.image_name + ' ' + + server_name + ) + except exceptions.CommandFailed as e: + self.assertIn('nics are required after microversion 2.36', + e.stderr) + else: + self.fail('CommandFailed should be raised.') diff --git a/openstackclient/tests/functional/compute/v2/test_server_event.py b/openstackclient/tests/functional/compute/v2/test_server_event.py new file mode 100644 index 00000000..953ade43 --- /dev/null +++ b/openstackclient/tests/functional/compute/v2/test_server_event.py @@ -0,0 +1,84 @@ +# Copyright 2017 Huawei, Inc. All rights reserved. +# +# 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.tests.functional.compute.v2 import common + + +class ServerEventTests(common.ComputeTestCase): + """Functional tests for server event""" + + def setUp(self): + super(ServerEventTests, self).setUp() + + # NOTE(dtroyer): As long as these tests are read-only we can get away + # with using the same server instance for all of them. + cmd_output = self.server_create() + self.server_id = cmd_output.get('id') + self.server_name = cmd_output['name'] + + def test_server_event_list_and_show(self): + """Test list, show server event""" + # Test 'server event list' for creating + cmd_output = json.loads(self.openstack( + 'server event list -f json ' + self.server_name + )) + request_id = None + for each_event in cmd_output: + self.assertNotIn('Message', each_event) + self.assertNotIn('Project ID', each_event) + self.assertNotIn('User ID', each_event) + if each_event.get('Action') == 'create': + self.assertEqual(self.server_id, each_event.get('Server ID')) + request_id = each_event.get('Request ID') + break + self.assertIsNotNone(request_id) + # Test 'server event show' for creating + cmd_output = json.loads(self.openstack( + 'server event show -f json ' + self.server_name + ' ' + request_id + )) + self.assertEqual(self.server_id, cmd_output.get('instance_uuid')) + self.assertEqual(request_id, cmd_output.get('request_id')) + self.assertEqual('create', cmd_output.get('action')) + self.assertIsNotNone(cmd_output.get('events')) + self.assertIsInstance(cmd_output.get('events'), list) + + # Reboot server, trigger reboot event + self.openstack('server reboot --wait ' + self.server_name) + # Test 'server event list --long' for rebooting + cmd_output = json.loads(self.openstack( + 'server event list --long -f json ' + self.server_name + )) + request_id = None + for each_event in cmd_output: + self.assertIn('Message', each_event) + self.assertIn('Project ID', each_event) + self.assertIn('User ID', each_event) + if each_event.get('Action') == 'reboot': + request_id = each_event.get('Request ID') + self.assertEqual(self.server_id, each_event.get('Server ID')) + break + self.assertIsNotNone(request_id) + # Test 'server event show' for rebooting + cmd_output = json.loads(self.openstack( + 'server event show -f json ' + self.server_name + ' ' + request_id + )) + + self.assertEqual(self.server_id, cmd_output.get('instance_uuid')) + self.assertEqual(request_id, cmd_output.get('request_id')) + self.assertEqual('reboot', cmd_output.get('action')) + self.assertIsNotNone(cmd_output.get('events')) + self.assertIsInstance(cmd_output.get('events'), list) diff --git a/openstackclient/tests/functional/compute/v2/test_server_group.py b/openstackclient/tests/functional/compute/v2/test_server_group.py index 3f0a24e5..44ecda1d 100644 --- a/openstackclient/tests/functional/compute/v2/test_server_group.py +++ b/openstackclient/tests/functional/compute/v2/test_server_group.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from openstackclient.tests.functional import base @@ -18,29 +19,88 @@ from openstackclient.tests.functional import base class ServerGroupTests(base.TestCase): """Functional tests for servergroup.""" - NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['name'] - - @classmethod - def setUpClass(cls): - opts = cls.get_opts(cls.FIELDS) - raw_output = cls.openstack('server group create --policy affinity ' + - cls.NAME + opts) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) - - @classmethod - def tearDownClass(cls): - raw_output = cls.openstack('server group delete ' + cls.NAME) - cls.assertOutput('', raw_output) - - def test_server_group_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('server group list' + opts) - self.assertIn(self.NAME, raw_output) - - def test_server_group_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('server group show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + def test_server_group_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'server group create -f json ' + + '--policy affinity ' + + name1 + )) + self.assertEqual( + name1, + cmd_output['name'] + ) + self.assertEqual( + 'affinity', + cmd_output['policies'] + ) + + cmd_output = json.loads(self.openstack( + 'server group create -f json ' + + '--policy anti-affinity ' + + name2 + )) + self.assertEqual( + name2, + cmd_output['name'] + ) + self.assertEqual( + 'anti-affinity', + cmd_output['policies'] + ) + + del_output = self.openstack( + 'server group delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) + + def test_server_group_show_and_list(self): + """Test server group create, show, and list""" + name1 = uuid.uuid4().hex + name2 = uuid.uuid4().hex + + # test server gorup show + cmd_output = json.loads(self.openstack( + 'server group create -f json ' + + '--policy affinity ' + + name1 + )) + self.addCleanup(self.openstack, 'server group delete ' + name1) + cmd_output = json.loads(self.openstack( + 'server group show -f json ' + name1)) + self.assertEqual( + name1, + cmd_output['name'] + ) + self.assertEqual( + 'affinity', + cmd_output['policies'] + ) + + cmd_output = json.loads(self.openstack( + 'server group create -f json ' + + '--policy anti-affinity ' + + name2 + )) + self.addCleanup(self.openstack, 'server group delete ' + name2) + cmd_output = json.loads(self.openstack( + 'server group show -f json ' + name2)) + self.assertEqual( + name2, + cmd_output['name'] + ) + self.assertEqual( + 'anti-affinity', + cmd_output['policies'] + ) + + # test server group list + cmd_output = json.loads(self.openstack( + 'server group list -f json')) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + policies = [x["Policies"] for x in cmd_output] + self.assertIn('affinity', policies) + self.assertIn('anti-affinity', policies) diff --git a/openstackclient/tests/functional/identity/v2/common.py b/openstackclient/tests/functional/identity/v2/common.py index b390c5bc..4f3e1801 100644 --- a/openstackclient/tests/functional/identity/v2/common.py +++ b/openstackclient/tests/functional/identity/v2/common.py @@ -22,14 +22,13 @@ BASIC_LIST_HEADERS = ['ID', 'Name'] class IdentityTests(base.TestCase): """Functional tests for Identity commands. """ - USER_FIELDS = ['email', 'enabled', 'id', 'name', 'project_id', - 'username', 'domain_id', 'default_project_id'] - PROJECT_FIELDS = ['enabled', 'id', 'name', 'description', 'domain_id'] + USER_FIELDS = ['email', 'enabled', 'id', 'name', 'project_id', 'username'] + PROJECT_FIELDS = ['enabled', 'id', 'name', 'description'] TOKEN_FIELDS = ['expires', 'id', 'project_id', 'user_id'] - ROLE_FIELDS = ['id', 'name', 'links', 'domain_id'] + ROLE_FIELDS = ['id', 'name', 'domain_id'] SERVICE_FIELDS = ['id', 'enabled', 'name', 'type', 'description'] ENDPOINT_FIELDS = ['id', 'region', 'service_id', 'service_name', - 'service_type', 'enabled', 'publicurl', + 'service_type', 'publicurl', 'adminurl', 'internalurl'] EC2_CREDENTIALS_FIELDS = ['access', 'project_id', 'secret', @@ -44,8 +43,8 @@ class IdentityTests(base.TestCase): # prepare v2 env os.environ['OS_IDENTITY_API_VERSION'] = '2.0' auth_url = os.environ.get('OS_AUTH_URL') - auth_url = auth_url.replace('v3', 'v2.0') - os.environ['OS_AUTH_URL'] = auth_url + if auth_url: + os.environ['OS_AUTH_URL'] = auth_url.replace('v3', 'v2.0') # create dummy project cls.project_name = data_utils.rand_name('TestProject') diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py index 5dd42e70..1ec3ac92 100644 --- a/openstackclient/tests/functional/identity/v3/common.py +++ b/openstackclient/tests/functional/identity/v3/common.py @@ -23,15 +23,15 @@ BASIC_LIST_HEADERS = ['ID', 'Name'] class IdentityTests(base.TestCase): """Functional tests for Identity commands. """ - DOMAIN_FIELDS = ['description', 'enabled', 'id', 'name', 'links'] - GROUP_FIELDS = ['description', 'domain_id', 'id', 'name', 'links'] + DOMAIN_FIELDS = ['description', 'enabled', 'id', 'name'] + GROUP_FIELDS = ['description', 'domain_id', 'id', 'name'] TOKEN_FIELDS = ['expires', 'id', 'project_id', 'user_id'] USER_FIELDS = ['email', 'enabled', 'id', 'name', 'name', 'domain_id', 'default_project_id', 'description', 'password_expires_at'] PROJECT_FIELDS = ['description', 'id', 'domain_id', 'is_domain', - 'enabled', 'name', 'parent_id', 'links'] - ROLE_FIELDS = ['id', 'name', 'links', 'domain_id'] + 'enabled', 'name', 'parent_id'] + ROLE_FIELDS = ['id', 'name', 'domain_id'] SERVICE_FIELDS = ['id', 'enabled', 'name', 'type', 'description'] REGION_FIELDS = ['description', 'enabled', 'parent_region', 'region'] ENDPOINT_FIELDS = ['id', 'region', 'region_id', 'service_id', @@ -42,7 +42,8 @@ class IdentityTests(base.TestCase): ENDPOINT_LIST_HEADERS = ['ID', 'Region', 'Service Name', 'Service Type', 'Enabled', 'Interface', 'URL'] - IDENTITY_PROVIDER_FIELDS = ['description', 'enabled', 'id', 'remote_ids'] + IDENTITY_PROVIDER_FIELDS = ['description', 'enabled', 'id', 'remote_ids', + 'domain_id'] IDENTITY_PROVIDER_LIST_HEADERS = ['ID', 'Enabled', 'Description'] SERVICE_PROVIDER_FIELDS = ['auth_url', 'description', 'enabled', @@ -55,8 +56,8 @@ class IdentityTests(base.TestCase): # prepare v3 env os.environ['OS_IDENTITY_API_VERSION'] = '3' auth_url = os.environ.get('OS_AUTH_URL') - auth_url = auth_url.replace('v2.0', 'v3') - os.environ['OS_AUTH_URL'] = auth_url + if auth_url: + os.environ['OS_AUTH_URL'] = auth_url.replace('v2.0', 'v3') # create dummy domain cls.domain_name = data_utils.rand_name('TestDomain') diff --git a/openstackclient/tests/functional/identity/v3/test_group.py b/openstackclient/tests/functional/identity/v3/test_group.py index 70491183..917d5df0 100644 --- a/openstackclient/tests/functional/identity/v3/test_group.py +++ b/openstackclient/tests/functional/identity/v3/test_group.py @@ -102,11 +102,7 @@ class GroupTests(common.IdentityTests): 'user_domain': self.domain_name, 'group': group_name, 'user': username}) - self.assertEqual( - '%(user)s added to group %(group)s\n' % {'user': username, - 'group': group_name}, - raw_output - ) + self.assertOutput('', raw_output) def test_group_contains_user(self): group_name = self._create_dummy_group() @@ -128,11 +124,7 @@ class GroupTests(common.IdentityTests): 'user_domain': self.domain_name, 'group': group_name, 'user': username}) - self.assertEqual( - '%(user)s added to group %(group)s\n' % {'user': username, - 'group': group_name}, - raw_output - ) + self.assertOutput('', raw_output) raw_output = self.openstack( 'group contains user ' '--group-domain %(group_domain)s ' @@ -165,14 +157,5 @@ class GroupTests(common.IdentityTests): 'user_domain': self.domain_name, 'group': group_name, 'user': username}) - self.assertEqual( - '%(user)s added to group %(group)s\n' % {'user': username, - 'group': group_name}, - add_raw_output - ) - self.assertEqual( - '%(user)s removed from ' - 'group %(group)s\n' % {'user': username, - 'group': group_name}, - remove_raw_output - ) + self.assertOutput('', add_raw_output) + self.assertOutput('', remove_raw_output) diff --git a/openstackclient/tests/functional/identity/v3/test_idp.py b/openstackclient/tests/functional/identity/v3/test_idp.py index f9d8cb80..5db3610a 100644 --- a/openstackclient/tests/functional/identity/v3/test_idp.py +++ b/openstackclient/tests/functional/identity/v3/test_idp.py @@ -10,9 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstackclient.tests.functional.identity.v3 import common from tempest.lib.common.utils import data_utils +from openstackclient.tests.functional.identity.v3 import common + class IdentityProviderTests(common.IdentityTests): # Introduce functional test case for command 'Identity Provider' diff --git a/openstackclient/tests/functional/identity/v3/test_project.py b/openstackclient/tests/functional/identity/v3/test_project.py index 5639dc16..96d41c3a 100644 --- a/openstackclient/tests/functional/identity/v3/test_project.py +++ b/openstackclient/tests/functional/identity/v3/test_project.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import json + from tempest.lib.common.utils import data_utils from openstackclient.tests.functional.identity.v3 import common @@ -111,3 +113,14 @@ class ProjectTests(common.IdentityTests): 'name': self.project_name}) items = self.parse_show(raw_output) self.assert_show_fields(items, self.PROJECT_FIELDS) + + def test_project_show_with_parents_children(self): + json_output = json.loads(self.openstack( + 'project show ' + '--parents --children -f json ' + '--domain %(domain)s ' + '%(name)s' % {'domain': self.domain_name, + 'name': self.project_name})) + for attr_name in (self.PROJECT_FIELDS + ['parents', 'subtree']): + self.assertIn(attr_name, json_output) + self.assertEqual(self.project_name, json_output.get('name')) diff --git a/openstackclient/tests/functional/image/v2/test_image.py b/openstackclient/tests/functional/image/v2/test_image.py index 3f432b02..b6baf570 100644 --- a/openstackclient/tests/functional/image/v2/test_image.py +++ b/openstackclient/tests/functional/image/v2/test_image.py @@ -74,3 +74,25 @@ class ImageTests(base.TestCase): self.openstack('image unset --property a --property c ' + self.NAME) raw_output = self.openstack('image show ' + self.NAME + opts) self.assertEqual(self.NAME + "\n\n", raw_output) + + def test_image_members(self): + opts = self.get_opts(['project_id']) + my_project_id = self.openstack('token issue' + opts).strip() + self.openstack( + 'image add project {} {}'.format(self.NAME, my_project_id)) + + self.openstack( + 'image set --accept ' + self.NAME) + shared_img_list = self.parse_listing( + self.openstack('image list --shared') + ) + self.assertIn(self.NAME, [img['Name'] for img in shared_img_list]) + + self.openstack( + 'image set --reject ' + self.NAME) + shared_img_list = self.parse_listing( + self.openstack('image list --shared', self.get_opts(['name'])) + ) + + self.openstack( + 'image remove project {} {}'.format(self.NAME, my_project_id)) diff --git a/openstackclient/tests/functional/network/v2/test_address_scope.py b/openstackclient/tests/functional/network/v2/test_address_scope.py index ef4b5756..eaf88969 100644 --- a/openstackclient/tests/functional/network/v2/test_address_scope.py +++ b/openstackclient/tests/functional/network/v2/test_address_scope.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from openstackclient.tests.functional import base @@ -17,33 +18,157 @@ from openstackclient.tests.functional import base class AddressScopeTests(base.TestCase): """Functional tests for address scope. """ - NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['name'] - @classmethod - def setUpClass(cls): - opts = cls.get_opts(cls.FIELDS) - raw_output = cls.openstack('address scope create ' + cls.NAME + opts) - cls.assertOutput(cls.NAME + "\n", raw_output) + # NOTE(dtroyer): Do not normalize the setup and teardown of the resource + # creation and deletion. Little is gained when each test + # has its own needs and there are collisions when running + # tests in parallel. - @classmethod - def tearDownClass(cls): - raw_output = cls.openstack('address scope delete ' + cls.NAME) - cls.assertOutput('', raw_output) + def test_address_scope_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'address scope create -f json ' + + name1 + )) + self.assertEqual( + name1, + cmd_output['name'], + ) + # Check the default values + self.assertEqual( + False, + cmd_output['shared'], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'address scope create -f json ' + + name2 + )) + self.assertEqual( + name2, + cmd_output['name'], + ) + + raw_output = self.openstack( + 'address scope delete ' + name1 + ' ' + name2, + ) + self.assertOutput('', raw_output) def test_address_scope_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('address scope list' + opts) - self.assertIn(self.NAME, raw_output) + """Test create defaults, list filters, delete""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'address scope create -f json ' + + '--ip-version 4 ' + + '--share ' + + name1 + )) + self.addCleanup(self.openstack, 'address scope delete ' + name1) + self.assertEqual( + name1, + cmd_output['name'], + ) + self.assertEqual( + 4, + cmd_output['ip_version'], + ) + self.assertEqual( + True, + cmd_output['shared'], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'address scope create -f json ' + + '--ip-version 6 ' + + '--no-share ' + + name2 + )) + self.addCleanup(self.openstack, 'address scope delete ' + name2) + self.assertEqual( + name2, + cmd_output['name'], + ) + self.assertEqual( + 6, + cmd_output['ip_version'], + ) + self.assertEqual( + False, + cmd_output['shared'], + ) + + # Test list + cmd_output = json.loads(self.openstack( + 'address scope list -f json ', + )) + col_data = [x["IP Version"] for x in cmd_output] + self.assertIn(4, col_data) + self.assertIn(6, col_data) - def test_address_scope_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('address scope show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + # Test list --share + cmd_output = json.loads(self.openstack( + 'address scope list -f json --share', + )) + col_data = [x["Shared"] for x in cmd_output] + self.assertIn(True, col_data) + self.assertNotIn(False, col_data) + + # Test list --no-share + cmd_output = json.loads(self.openstack( + 'address scope list -f json --no-share', + )) + col_data = [x["Shared"] for x in cmd_output] + self.assertIn(False, col_data) + self.assertNotIn(True, col_data) def test_address_scope_set(self): - self.openstack('address scope set --share ' + self.NAME) - opts = self.get_opts(['shared']) - raw_output = self.openstack('address scope show ' + self.NAME + opts) - self.assertEqual("True\n", raw_output) + """Tests create options, set, show, delete""" + name = uuid.uuid4().hex + newname = name + "_" + cmd_output = json.loads(self.openstack( + 'address scope create -f json ' + + '--ip-version 4 ' + + '--no-share ' + + name + )) + self.addCleanup(self.openstack, 'address scope delete ' + newname) + self.assertEqual( + name, + cmd_output['name'], + ) + self.assertEqual( + 4, + cmd_output['ip_version'], + ) + self.assertEqual( + False, + cmd_output['shared'], + ) + + raw_output = self.openstack( + 'address scope set ' + + '--name ' + newname + + ' --share ' + + name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'address scope show -f json ' + + newname, + )) + self.assertEqual( + newname, + cmd_output['name'], + ) + self.assertEqual( + 4, + cmd_output['ip_version'], + ) + self.assertEqual( + True, + cmd_output['shared'], + ) diff --git a/openstackclient/tests/functional/network/v2/test_floating_ip.py b/openstackclient/tests/functional/network/v2/test_floating_ip.py index f3a1971f..8fbec3d5 100644 --- a/openstackclient/tests/functional/network/v2/test_floating_ip.py +++ b/openstackclient/tests/functional/network/v2/test_floating_ip.py @@ -10,49 +10,161 @@ # License for the specific language governing permissions and limitations # under the License. +import random +import re import uuid from openstackclient.tests.functional import base class FloatingIpTests(base.TestCase): - """Functional tests for floating ip. """ + """Functional tests for floating ip""" SUBNET_NAME = uuid.uuid4().hex NETWORK_NAME = uuid.uuid4().hex - ID = None - HEADERS = ['ID'] - FIELDS = ['id'] @classmethod def setUpClass(cls): - # Create a network for the floating ip. - cls.openstack('network create --external ' + cls.NETWORK_NAME) - # Create a subnet for the network. - cls.openstack( - 'subnet create --network ' + cls.NETWORK_NAME + - ' --subnet-range 10.10.10.0/24 ' + - cls.SUBNET_NAME - ) - opts = cls.get_opts(cls.FIELDS) + # Set up some regex for matching below + cls.re_id = re.compile("id\s+\|\s+(\S+)") + cls.re_floating_ip = re.compile("floating_ip_address\s+\|\s+(\S+)") + cls.re_fixed_ip = re.compile("fixed_ip_address\s+\|\s+(\S+)") + cls.re_description = re.compile("description\s+\|\s+([^|]+?)\s+\|") + cls.re_network_id = re.compile("floating_network_id\s+\|\s+(\S+)") + + # Create a network for the floating ip raw_output = cls.openstack( - 'floating ip create ' + cls.NETWORK_NAME + opts) - cls.ID = raw_output.strip('\n') + 'network create --external ' + cls.NETWORK_NAME + ) + cls.network_id = re.search(cls.re_id, raw_output).group(1) + + # Try random subnet range for subnet creating + # Because we can not determine ahead of time what subnets are already + # in use, possibly by another test running in parallel, try 4 times + for i in range(4): + # Make a random subnet + cls.subnet = ".".join(map( + str, + (random.randint(0, 223) for _ in range(3)) + )) + ".0/26" + try: + # Create a subnet for the network + raw_output = cls.openstack( + 'subnet create ' + + '--network ' + cls.NETWORK_NAME + ' ' + + '--subnet-range ' + cls.subnet + ' ' + + cls.SUBNET_NAME + ) + except Exception: + if (i == 3): + # raise the exception at the last time + raise + pass + else: + # break and no longer retry if create sucessfully + break + + cls.subnet_id = re.search(cls.re_id, raw_output).group(1) @classmethod def tearDownClass(cls): - raw_output = cls.openstack('floating ip delete ' + cls.ID) - cls.assertOutput('', raw_output) raw_output = cls.openstack('subnet delete ' + cls.SUBNET_NAME) cls.assertOutput('', raw_output) raw_output = cls.openstack('network delete ' + cls.NETWORK_NAME) cls.assertOutput('', raw_output) + def test_floating_ip_delete(self): + """Test create, delete multiple""" + raw_output = self.openstack( + 'floating ip create ' + + '--description aaaa ' + + self.NETWORK_NAME + ) + re_ip = re.search(self.re_floating_ip, raw_output) + self.assertIsNotNone(re_ip) + ip1 = re_ip.group(1) + self.assertEqual( + 'aaaa', + re.search(self.re_description, raw_output).group(1), + ) + + raw_output = self.openstack( + 'floating ip create ' + + '--description bbbb ' + + self.NETWORK_NAME + ) + ip2 = re.search(self.re_floating_ip, raw_output).group(1) + self.assertEqual( + 'bbbb', + re.search(self.re_description, raw_output).group(1), + ) + + # Clean up after ourselves + raw_output = self.openstack('floating ip delete ' + ip1 + ' ' + ip2) + self.assertOutput('', raw_output) + def test_floating_ip_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('floating ip list' + opts) - self.assertIn(self.ID, raw_output) + """Test create defaults, list filters, delete""" + raw_output = self.openstack( + 'floating ip create ' + + '--description aaaa ' + + self.NETWORK_NAME + ) + re_ip = re.search(self.re_floating_ip, raw_output) + self.assertIsNotNone(re_ip) + ip1 = re_ip.group(1) + self.addCleanup(self.openstack, 'floating ip delete ' + ip1) + self.assertEqual( + 'aaaa', + re.search(self.re_description, raw_output).group(1), + ) + self.assertIsNotNone(re.search(self.re_network_id, raw_output)) + + raw_output = self.openstack( + 'floating ip create ' + + '--description bbbb ' + + self.NETWORK_NAME + ) + ip2 = re.search(self.re_floating_ip, raw_output).group(1) + self.addCleanup(self.openstack, 'floating ip delete ' + ip2) + self.assertEqual( + 'bbbb', + re.search(self.re_description, raw_output).group(1), + ) + + # Test list + raw_output = self.openstack('floating ip list') + self.assertIsNotNone(re.search("\|\s+" + ip1 + "\s+\|", raw_output)) + self.assertIsNotNone(re.search("\|\s+" + ip2 + "\s+\|", raw_output)) + + # Test list --long + raw_output = self.openstack('floating ip list --long') + self.assertIsNotNone(re.search("\|\s+" + ip1 + "\s+\|", raw_output)) + self.assertIsNotNone(re.search("\|\s+" + ip2 + "\s+\|", raw_output)) + + # TODO(dtroyer): add more filter tests def test_floating_ip_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('floating ip show ' + self.ID + opts) - self.assertEqual(self.ID + "\n", raw_output) + """Test show""" + raw_output = self.openstack( + 'floating ip create ' + + '--description shosho ' + + # '--fixed-ip-address 1.2.3.4 ' + + self.NETWORK_NAME + ) + re_ip = re.search(self.re_floating_ip, raw_output) + self.assertIsNotNone(re_ip) + ip = re_ip.group(1) + + raw_output = self.openstack('floating ip show ' + ip) + self.addCleanup(self.openstack, 'floating ip delete ' + ip) + + self.assertEqual( + 'shosho', + re.search(self.re_description, raw_output).group(1), + ) + # TODO(dtroyer): not working??? + # self.assertEqual( + # '1.2.3.4', + # re.search(self.re_floating_ip, raw_output).group(1), + # ) + self.assertIsNotNone(re.search(self.re_network_id, raw_output)) diff --git a/openstackclient/tests/functional/network/v2/test_ip_availability.py b/openstackclient/tests/functional/network/v2/test_ip_availability.py index b5c908f4..7440f250 100644 --- a/openstackclient/tests/functional/network/v2/test_ip_availability.py +++ b/openstackclient/tests/functional/network/v2/test_ip_availability.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from openstackclient.tests.functional import base @@ -17,22 +18,19 @@ from openstackclient.tests.functional import base class IPAvailabilityTests(base.TestCase): """Functional tests for IP availability. """ - NAME = uuid.uuid4().hex - NETWORK_NAME = uuid.uuid4().hex - FIELDS = ['network_name'] @classmethod def setUpClass(cls): # Create a network for the subnet. + cls.NAME = uuid.uuid4().hex + cls.NETWORK_NAME = uuid.uuid4().hex cls.openstack('network create ' + cls.NETWORK_NAME) - opts = cls.get_opts(['name']) - raw_output = cls.openstack( - 'subnet create --network ' + cls.NETWORK_NAME + + cmd_output = json.loads(cls.openstack( + 'subnet create -f json --network ' + cls.NETWORK_NAME + ' --subnet-range 10.10.10.0/24 ' + - cls.NAME + opts - ) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) + cls.NAME + )) + cls.assertOutput(cls.NAME, cmd_output['name']) @classmethod def tearDownClass(cls): @@ -42,12 +40,17 @@ class IPAvailabilityTests(base.TestCase): cls.assertOutput('', raw_network) def test_ip_availability_list(self): - opts = ' -f csv -c "Network Name"' - raw_output = self.openstack('ip availability list' + opts) - self.assertIn(self.NETWORK_NAME, raw_output) + """Test ip availability list""" + cmd_output = json.loads(self.openstack( + 'ip availability list -f json')) + names = [x['Network Name'] for x in cmd_output] + self.assertIn(self.NETWORK_NAME, names) def test_ip_availability_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack( - 'ip availability show ' + self.NETWORK_NAME + opts) - self.assertEqual(self.NETWORK_NAME + "\n", raw_output) + """Test ip availability show""" + cmd_output = json.loads(self.openstack( + 'ip availability show -f json ' + self.NETWORK_NAME)) + self.assertEqual( + self.NETWORK_NAME, + cmd_output['network_name'], + ) diff --git a/openstackclient/tests/functional/network/v2/test_network.py b/openstackclient/tests/functional/network/v2/test_network.py index c77ff642..0e10bfce 100644 --- a/openstackclient/tests/functional/network/v2/test_network.py +++ b/openstackclient/tests/functional/network/v2/test_network.py @@ -10,41 +10,354 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from openstackclient.tests.functional import base class NetworkTests(base.TestCase): - """Functional tests for network. """ - NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['name'] - - @classmethod - def setUpClass(cls): - opts = cls.get_opts(cls.FIELDS) - raw_output = cls.openstack('network create ' + cls.NAME + opts) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) - - @classmethod - def tearDownClass(cls): - raw_output = cls.openstack('network delete ' + cls.NAME) - cls.assertOutput('', raw_output) + """Functional tests for network""" + + def test_network_create(self): + """Test create options, delete""" + # Get project IDs + cmd_output = json.loads(self.openstack('token issue -f json ')) + auth_project_id = cmd_output['project_id'] + + cmd_output = json.loads(self.openstack('project list -f json ')) + admin_project_id = None + demo_project_id = None + for p in cmd_output: + if p['Name'] == 'admin': + admin_project_id = p['ID'] + if p['Name'] == 'demo': + demo_project_id = p['ID'] + + # Verify assumptions: + # * admin and demo projects are present + # * demo and admin are distinct projects + # * tests run as admin + self.assertIsNotNone(admin_project_id) + self.assertIsNotNone(demo_project_id) + self.assertNotEqual(admin_project_id, demo_project_id) + self.assertEqual(admin_project_id, auth_project_id) + + # network create with no options + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network create -f json ' + + name1 + )) + self.addCleanup(self.openstack, 'network delete ' + name1) + self.assertIsNotNone(cmd_output["id"]) + + # Check the default values + self.assertEqual( + admin_project_id, + cmd_output["project_id"], + ) + self.assertEqual( + '', + cmd_output["description"], + ) + self.assertEqual( + 'UP', + cmd_output["admin_state_up"], + ) + self.assertEqual( + False, + cmd_output["shared"], + ) + self.assertEqual( + 'Internal', + cmd_output["router:external"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network create -f json ' + + '--project demo ' + + name2 + )) + self.addCleanup(self.openstack, 'network delete ' + name2) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + demo_project_id, + cmd_output["project_id"], + ) + self.assertEqual( + '', + cmd_output["description"], + ) + + def test_network_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network create -f json ' + + '--description aaaa ' + + name1 + )) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + 'aaaa', + cmd_output["description"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network create -f json ' + + '--description bbbb ' + + name2 + )) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + 'bbbb', + cmd_output["description"], + ) + + del_output = self.openstack('network delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) def test_network_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('network list' + opts) - self.assertIn(self.NAME, raw_output) + """Test create defaults, list filters, delete""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network create -f json ' + + '--description aaaa ' + + '--no-default ' + + name1 + )) + self.addCleanup(self.openstack, 'network delete ' + name1) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + 'aaaa', + cmd_output["description"], + ) + # Check the default values + self.assertEqual( + 'UP', + cmd_output["admin_state_up"], + ) + self.assertEqual( + False, + cmd_output["shared"], + ) + self.assertEqual( + 'Internal', + cmd_output["router:external"], + ) + + self.assertEqual( + False, + cmd_output["is_default"], + ) + self.assertEqual( + True, + cmd_output["port_security_enabled"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network create -f json ' + + '--description bbbb ' + + '--disable ' + + '--share ' + + name2 + )) + self.addCleanup(self.openstack, 'network delete ' + name2) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + 'bbbb', + cmd_output["description"], + ) + self.assertEqual( + 'DOWN', + cmd_output["admin_state_up"], + ) + self.assertEqual( + True, + cmd_output["shared"], + ) + self.assertEqual( + None, + cmd_output["is_default"], + ) + self.assertEqual( + True, + cmd_output["port_security_enabled"], + ) + + # Test list --long + cmd_output = json.loads(self.openstack( + "network list -f json " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertIn(name2, col_name) + + # Test list --long --enable + cmd_output = json.loads(self.openstack( + "network list -f json " + + "--enable " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertNotIn(name2, col_name) + + # Test list --long --disable + cmd_output = json.loads(self.openstack( + "network list -f json " + + "--disable " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, col_name) + self.assertIn(name2, col_name) + + # Test list --long --share + cmd_output = json.loads(self.openstack( + "network list -f json " + + "--share " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, col_name) + self.assertIn(name2, col_name) + + # Test list --long --no-share + cmd_output = json.loads(self.openstack( + "network list -f json " + + "--no-share " + + "--long" + )) + col_name = [x["Name"] for x in cmd_output] + self.assertIn(name1, col_name) + self.assertNotIn(name2, col_name) + + def test_network_dhcp_agent(self): + name1 = uuid.uuid4().hex + cmd_output1 = json.loads(self.openstack( + 'network create -f json ' + + '--description aaaa ' + + name1 + )) + + self.addCleanup(self.openstack, 'network delete ' + name1) + + # Get network ID + network_id = cmd_output1['id'] + + # Get DHCP Agent ID + cmd_output2 = json.loads(self.openstack( + 'network agent list -f json --agent-type dhcp' + )) + agent_id = cmd_output2[0]['ID'] + + # Add Agent to Network + self.openstack( + 'network agent add network --dhcp ' + + agent_id + ' ' + network_id + ) + + # Test network list --agent + cmd_output3 = json.loads(self.openstack( + 'network list -f json --agent ' + agent_id + )) + + # Cleanup + # Remove Agent from Network + self.openstack( + 'network agent remove network --dhcp ' + + agent_id + ' ' + network_id + ) + + # Assert + col_name = [x["ID"] for x in cmd_output3] + self.assertIn( + network_id, col_name + ) def test_network_set(self): - raw_output = self.openstack('network set --disable ' + self.NAME) - opts = self.get_opts(['name', 'admin_state_up']) - raw_output = self.openstack('network show ' + self.NAME + opts) - self.assertEqual("DOWN\n" + self.NAME + "\n", raw_output) - - def test_network_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('network show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + """Tests create options, set, show, delete""" + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network create -f json ' + + '--description aaaa ' + + '--enable ' + + '--no-share ' + + '--internal ' + + '--no-default ' + + '--enable-port-security ' + + name + )) + self.addCleanup(self.openstack, 'network delete ' + name) + self.assertIsNotNone(cmd_output["id"]) + self.assertEqual( + 'aaaa', + cmd_output["description"], + ) + self.assertEqual( + 'UP', + cmd_output["admin_state_up"], + ) + self.assertEqual( + False, + cmd_output["shared"], + ) + self.assertEqual( + 'Internal', + cmd_output["router:external"], + ) + + self.assertEqual( + False, + cmd_output["is_default"], + ) + self.assertEqual( + True, + cmd_output["port_security_enabled"], + ) + + raw_output = self.openstack( + 'network set ' + + '--description cccc ' + + '--disable ' + + '--share ' + + '--external ' + + '--disable-port-security ' + + name + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'network show -f json ' + name + )) + + self.assertEqual( + 'cccc', + cmd_output["description"], + ) + self.assertEqual( + 'DOWN', + cmd_output["admin_state_up"], + ) + self.assertEqual( + True, + cmd_output["shared"], + ) + self.assertEqual( + 'External', + cmd_output["router:external"], + ) + self.assertEqual( + False, + cmd_output["is_default"], + ) + self.assertEqual( + False, + cmd_output["port_security_enabled"], + ) diff --git a/openstackclient/tests/functional/network/v2/test_network_agent.py b/openstackclient/tests/functional/network/v2/test_network_agent.py index dd6112e7..6da721d1 100644 --- a/openstackclient/tests/functional/network/v2/test_network_agent.py +++ b/openstackclient/tests/functional/network/v2/test_network_agent.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import json +import uuid + from openstackclient.tests.functional import base @@ -39,3 +42,52 @@ class NetworkAgentTests(base.TestCase): self.openstack('network agent set --enable ' + self.IDs[0]) raw_output = self.openstack('network agent show ' + self.IDs[0] + opts) self.assertEqual("UP\n", raw_output) + + +class NetworkAgentListTests(base.TestCase): + """Functional test for network agent list --network. """ + + def test_network_dhcp_agent_list(self): + """Test network agent list""" + + name1 = uuid.uuid4().hex + cmd_output1 = json.loads(self.openstack( + 'network create -f json ' + + '--description aaaa ' + + name1 + )) + + self.addCleanup(self.openstack, 'network delete ' + name1) + + # Get network ID + network_id = cmd_output1['id'] + + # Get DHCP Agent ID + cmd_output2 = json.loads(self.openstack( + 'network agent list -f json --agent-type dhcp' + )) + agent_id = cmd_output2[0]['ID'] + + # Add Agent to Network + self.openstack( + 'network agent add network --dhcp ' + + agent_id + ' ' + network_id + ) + + # Test network agent list --network + cmd_output3 = json.loads(self.openstack( + 'network agent list -f json --network ' + network_id + )) + + # Cleanup + # Remove Agent from Network + self.openstack( + 'network agent remove network --dhcp ' + + agent_id + ' ' + network_id + ) + + # Assert + col_name = [x["ID"] for x in cmd_output3] + self.assertIn( + agent_id, col_name + ) diff --git a/openstackclient/tests/functional/network/v2/test_network_flavor.py b/openstackclient/tests/functional/network/v2/test_network_flavor.py new file mode 100644 index 00000000..b2fc2eae --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_flavor.py @@ -0,0 +1,176 @@ +# 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 +import uuid + +from openstackclient.tests.functional import base + + +class NetworkFlavorTests(base.TestCase): + """Functional tests for network flavor.""" + + def test_network_flavor_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network flavor create -f json --description testdescription ' + '--enable --service-type L3_ROUTER_NAT ' + name1, + )) + self.assertEqual( + name1, + cmd_output['name'], + ) + self.assertEqual( + True, + cmd_output['enabled'], + ) + self.assertEqual( + 'testdescription', + cmd_output['description'], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network flavor create -f json --description testdescription1 ' + '--disable --service-type L3_ROUTER_NAT ' + name2, + )) + self.assertEqual( + name2, + cmd_output['name'], + ) + self.assertEqual( + False, + cmd_output['enabled'], + ) + self.assertEqual( + 'testdescription1', + cmd_output['description'], + ) + raw_output = self.openstack( + 'network flavor delete ' + name1 + " " + name2) + self.assertOutput('', raw_output) + + def test_network_flavor_list(self): + """Test create defaults, list filters, delete""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network flavor create -f json --description testdescription ' + '--enable --service-type L3_ROUTER_NAT ' + name1, + )) + self.addCleanup(self.openstack, "network flavor delete " + name1) + self.assertEqual( + name1, + cmd_output['name'], + ) + self.assertEqual( + True, + cmd_output['enabled'], + ) + self.assertEqual( + 'testdescription', + cmd_output['description'], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network flavor create -f json --description testdescription1 ' + '--disable --service-type L3_ROUTER_NAT ' + name2, + )) + self.assertEqual( + name2, + cmd_output['name'], + ) + self.assertEqual( + False, + cmd_output['enabled'], + ) + self.assertEqual( + 'testdescription1', + cmd_output['description'], + ) + self.addCleanup(self.openstack, "network flavor delete " + name2) + + # Test list + cmd_output = json.loads(self.openstack( + 'network flavor list -f json ',)) + self.assertIsNotNone(cmd_output) + + name_list = [item.get('Name') for item in cmd_output] + self.assertIn(name1, name_list) + self.assertIn(name2, name_list) + + def test_network_flavor_set(self): + """Tests create options, set, show, delete""" + name = uuid.uuid4().hex + newname = name + "_" + cmd_output = json.loads(self.openstack( + 'network flavor create -f json --description testdescription ' + '--disable --service-type L3_ROUTER_NAT ' + name, + )) + self.addCleanup(self.openstack, "network flavor delete " + newname) + self.assertEqual( + name, + cmd_output['name'], + ) + self.assertEqual( + False, + cmd_output['enabled'], + ) + self.assertEqual( + 'testdescription', + cmd_output['description'], + ) + + raw_output = self.openstack( + 'network flavor set --name ' + newname + ' --disable ' + name + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'network flavor show -f json ' + newname,)) + self.assertEqual( + newname, + cmd_output['name'], + ) + self.assertEqual( + False, + cmd_output['enabled'], + ) + self.assertEqual( + 'testdescription', + cmd_output['description'], + ) + + def test_network_flavor_show(self): + """Test show network flavor""" + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'network flavor create -f json --description testdescription ' + '--disable --service-type L3_ROUTER_NAT ' + name, + )) + self.addCleanup(self.openstack, "network flavor delete " + name) + cmd_output = json.loads(self.openstack( + 'network flavor show -f json ' + name,)) + self.assertEqual( + name, + cmd_output['name'], + ) + self.assertEqual( + False, + cmd_output['enabled'], + ) + self.assertEqual( + 'testdescription', + cmd_output['description'], + ) diff --git a/openstackclient/tests/functional/network/v2/test_network_flavor_profile.py b/openstackclient/tests/functional/network/v2/test_network_flavor_profile.py new file mode 100644 index 00000000..1a82c82b --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_flavor_profile.py @@ -0,0 +1,151 @@ +# 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.tests.functional import base + + +class NetworkFlavorProfileTests(base.TestCase): + """Functional tests for network flavor-profile.""" + + DESCRIPTION = 'fakedescription' + METAINFO = 'Extrainfo' + + def test_network_flavor_profile_create(self): + json_output = json.loads(self.openstack( + ' network flavor profile create -f json --description ' + + self.DESCRIPTION + ' --enable --metainfo ' + self.METAINFO)) + ID = json_output.get('id') + self.assertIsNotNone(ID) + self.assertEqual( + True, + json_output.get('enabled')) + self.assertEqual( + 'fakedescription', + json_output.get('description')) + self.assertEqual( + 'Extrainfo', + json_output.get('meta_info') + ) + + # Clean up + raw_output = self.openstack('network flavor profile delete ' + ID) + self.assertOutput('', raw_output) + + def test_network_flavor_profile_list(self): + json_output = json.loads(self.openstack( + ' network flavor profile create -f json --description ' + + self.DESCRIPTION + ' --enable --metainfo ' + self.METAINFO)) + ID1 = json_output.get('id') + self.assertIsNotNone(ID1) + self.assertEqual( + True, + json_output.get('enabled')) + self.assertEqual( + 'fakedescription', + json_output.get('description')) + self.assertEqual( + 'Extrainfo', + json_output.get('meta_info') + ) + + json_output = json.loads(self.openstack( + ' network flavor profile create -f json --description ' + + self.DESCRIPTION + ' --disable --metainfo ' + self.METAINFO)) + ID2 = json_output.get('id') + self.assertIsNotNone(ID2) + self.assertEqual( + False, + json_output.get('enabled')) + self.assertEqual( + 'fakedescription', + json_output.get('description')) + self.assertEqual( + 'Extrainfo', + json_output.get('meta_info') + ) + + # Test list + json_output = json.loads(self.openstack( + 'network flavor profile list -f json')) + self.assertIsNotNone(json_output) + + id_list = [item.get('ID') for item in json_output] + self.assertIn(ID1, id_list) + self.assertIn(ID2, id_list) + + # Clean up + raw_output = self.openstack( + 'network flavor profile delete ' + ID1 + " " + ID2) + self.assertOutput('', raw_output) + + def test_network_flavor_profile_set(self): + json_output_1 = json.loads(self.openstack( + ' network flavor profile create -f json --description ' + + self.DESCRIPTION + ' --enable --metainfo ' + self.METAINFO)) + ID = json_output_1.get('id') + self.assertIsNotNone(ID) + self.assertEqual( + True, + json_output_1.get('enabled')) + self.assertEqual( + 'fakedescription', + json_output_1.get('description')) + self.assertEqual( + 'Extrainfo', + json_output_1.get('meta_info') + ) + + self.openstack('network flavor profile set --disable ' + ID) + + json_output = json.loads(self.openstack('network flavor profile show ' + '-f json ' + ID)) + self.assertEqual( + False, + json_output.get('enabled')) + self.assertEqual( + 'fakedescription', + json_output.get('description')) + self.assertEqual( + 'Extrainfo', + json_output.get('meta_info') + ) + + # Clean up + raw_output = self.openstack('network flavor profile delete ' + ID) + self.assertOutput('', raw_output) + + def test_network_flavor_profile_show(self): + json_output_1 = json.loads(self.openstack( + ' network flavor profile create -f json --description ' + + self.DESCRIPTION + ' --enable --metainfo ' + self.METAINFO)) + ID = json_output_1.get('id') + self.assertIsNotNone(ID) + json_output = json.loads(self.openstack('network flavor profile show ' + '-f json ' + ID)) + self.assertEqual( + ID, + json_output["id"]) + self.assertEqual( + True, + json_output["enabled"]) + self.assertEqual( + 'fakedescription', + json_output["description"]) + self.assertEqual( + 'Extrainfo', + json_output["meta_info"]) + + # Clean up + raw_output = self.openstack('network flavor profile delete ' + ID) + self.assertOutput('', raw_output) diff --git a/openstackclient/tests/functional/network/v2/test_network_meter.py b/openstackclient/tests/functional/network/v2/test_network_meter.py new file mode 100644 index 00000000..f73f4812 --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_meter.py @@ -0,0 +1,158 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 +import uuid + +from openstackclient.tests.functional import base + + +class TestMeter(base.TestCase): + """Functional tests for network meter.""" + + # NOTE(dtroyer): Do not normalize the setup and teardown of the resource + # creation and deletion. Little is gained when each test + # has its own needs and there are collisions when running + # tests in parallel. + + def test_meter_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + name2 = uuid.uuid4().hex + description = 'fakedescription' + json_output = json.loads(self.openstack( + 'network meter create -f json ' + name1 + ' --description ' + + description) + ) + self.assertEqual( + name1, + json_output.get('name'), + ) + # Check if default shared values + self.assertEqual( + False, + json_output.get('shared') + ) + self.assertEqual( + 'fakedescription', + json_output.get('description') + ) + + json_output_2 = json.loads(self.openstack( + 'network meter create -f json ' + name2 + ' --description ' + + description) + ) + self.assertEqual( + name2, + json_output_2.get('name'), + ) + # Check if default shared values + self.assertEqual( + False, + json_output_2.get('shared') + ) + self.assertEqual( + 'fakedescription', + json_output_2.get('description') + ) + + raw_output = self.openstack( + 'network meter delete ' + name1 + ' ' + name2, + ) + self.assertOutput('', raw_output) + + def test_meter_list(self): + """Test create, list filters, delete""" + name1 = uuid.uuid4().hex + json_output = json.loads(self.openstack( + 'network meter create -f json --description Test1 --share ' + + name1) + ) + self.addCleanup(self.openstack, 'network meter delete ' + name1) + + self.assertEqual( + 'Test1', + json_output.get('description'), + ) + self.assertEqual( + True, + json_output.get('shared'), + ) + + name2 = uuid.uuid4().hex + json_output_2 = json.loads(self.openstack( + 'network meter create -f json --description Test2 --no-share ' + + name2) + ) + self.addCleanup(self.openstack, 'network meter delete ' + name2) + + self.assertEqual( + 'Test2', + json_output_2.get('description') + ) + self.assertEqual( + False, + json_output_2.get('shared') + ) + + raw_output = json.loads(self.openstack('network meter list -f json')) + name_list = [item.get('Name') for item in raw_output] + self.assertIn(name1, name_list) + self.assertIn(name2, name_list) + + def test_meter_show(self): + """Test create, show, delete""" + name1 = uuid.uuid4().hex + description = 'fakedescription' + json_output = json.loads(self.openstack( + 'network meter create -f json ' + name1 + ' --description ' + + description) + ) + meter_id = json_output.get('id') + self.addCleanup(self.openstack, 'network meter delete ' + name1) + + # Test show with ID + json_output = json.loads(self.openstack( + 'network meter show -f json ' + meter_id) + ) + self.assertEqual( + False, + json_output.get('shared') + ) + self.assertEqual( + 'fakedescription', + json_output.get('description') + ) + self.assertEqual( + name1, + json_output.get('name') + ) + + # Test show with name + json_output = json.loads(self.openstack( + 'network meter show -f json ' + name1) + ) + self.assertEqual( + meter_id, + json_output.get('id') + ) + self.assertEqual( + False, + json_output.get('shared') + ) + self.assertEqual( + 'fakedescription', + json_output.get('description') + ) diff --git a/openstackclient/tests/functional/network/v2/test_network_meter_rule.py b/openstackclient/tests/functional/network/v2/test_network_meter_rule.py new file mode 100644 index 00000000..d15cdf77 --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_meter_rule.py @@ -0,0 +1,123 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 +import uuid + +from openstackclient.tests.functional import base + + +class TestMeterRule(base.TestCase): + """Functional tests for meter rule""" + METER_NAME = uuid.uuid4().hex + METER_ID = None + METER_RULE_ID = None + + @classmethod + def setUpClass(cls): + json_output = json.loads(cls.openstack( + 'network meter create -f json ' + cls.METER_NAME + )) + + cls.METER_ID = json_output.get('id') + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('network meter delete ' + cls.METER_ID) + cls.assertOutput('', raw_output) + + def test_meter_rule_delete(self): + """test create, delete""" + + json_output = json.loads(self.openstack( + 'network meter rule create -f json ' + + '--remote-ip-prefix 10.0.0.0/8 ' + + self.METER_ID + )) + rule_id = json_output.get('id') + re_ip = json_output.get('remote_ip_prefix') + + self.addCleanup(self.openstack, + 'network meter rule delete ' + rule_id) + self.assertIsNotNone(re_ip) + self.assertIsNotNone(rule_id) + self.assertEqual( + '10.0.0.0/8', re_ip + ) + + def test_meter_rule_list(self): + """Test create, list, delete""" + json_output = json.loads(self.openstack( + 'network meter rule create -f json ' + + '--remote-ip-prefix 10.0.0.0/8 ' + + self.METER_ID + )) + rule_id_1 = json_output.get('id') + self.addCleanup(self.openstack, + 'network meter rule delete ' + rule_id_1) + self.assertEqual( + '10.0.0.0/8', + json_output.get('remote_ip_prefix') + ) + + json_output_1 = json.loads(self.openstack( + 'network meter rule create -f json ' + + '--remote-ip-prefix 11.0.0.0/8 ' + + self.METER_ID + )) + rule_id_2 = json_output_1.get('id') + self.addCleanup(self.openstack, + 'network meter rule delete ' + rule_id_2) + self.assertEqual( + '11.0.0.0/8', + json_output_1.get('remote_ip_prefix') + ) + + json_output = json.loads(self.openstack('network meter rule list -f ' + 'json')) + rule_id_list = [item.get('ID') for item in json_output] + ip_prefix_list = [item.get('Remote IP Prefix') for item in json_output] + self.assertIn(rule_id_1, rule_id_list) + self.assertIn(rule_id_2, rule_id_list) + self.assertIn('10.0.0.0/8', ip_prefix_list) + self.assertIn('11.0.0.0/8', ip_prefix_list) + + def test_meter_rule_show(self): + + """Test create, show, delete""" + json_output = json.loads(self.openstack( + 'network meter rule create -f json ' + + '--remote-ip-prefix 10.0.0.0/8 ' + + '--egress ' + + self.METER_ID + )) + rule_id = json_output.get('id') + + self.assertEqual( + 'egress', + json_output.get('direction') + ) + + json_output = json.loads(self.openstack('network meter rule show' + ' -f json ' + rule_id)) + + self.assertEqual( + '10.0.0.0/8', + json_output.get('remote_ip_prefix') + ) + self.assertIsNotNone(rule_id) + + self.addCleanup(self.openstack, + 'network meter rule delete ' + rule_id) diff --git a/openstackclient/tests/functional/network/v2/test_network_qos_policy.py b/openstackclient/tests/functional/network/v2/test_network_qos_policy.py new file mode 100644 index 00000000..07dea31b --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_qos_policy.py @@ -0,0 +1,55 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 uuid + +from openstackclient.tests.functional import base + + +class QosPolicyTests(base.TestCase): + """Functional tests for QoS policy. """ + NAME = uuid.uuid4().hex + HEADERS = ['Name'] + FIELDS = ['name'] + + @classmethod + def setUpClass(cls): + opts = cls.get_opts(cls.FIELDS) + raw_output = cls.openstack('network qos policy create ' + cls.NAME + + opts) + cls.assertOutput(cls.NAME + "\n", raw_output) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('network qos policy delete ' + cls.NAME) + cls.assertOutput('', raw_output) + + def test_qos_policy_list(self): + opts = self.get_opts(self.HEADERS) + raw_output = self.openstack('network qos policy list' + opts) + self.assertIn(self.NAME, raw_output) + + def test_qos_policy_show(self): + opts = self.get_opts(self.FIELDS) + raw_output = self.openstack('network qos policy show ' + self.NAME + + opts) + self.assertEqual(self.NAME + "\n", raw_output) + + def test_qos_policy_set(self): + self.openstack('network qos policy set --share ' + self.NAME) + opts = self.get_opts(['shared']) + raw_output = self.openstack('network qos policy show ' + self.NAME + + opts) + self.assertEqual("True\n", raw_output) diff --git a/openstackclient/tests/functional/network/v2/test_network_qos_rule.py b/openstackclient/tests/functional/network/v2/test_network_qos_rule.py new file mode 100644 index 00000000..af0c9bac --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_qos_rule.py @@ -0,0 +1,181 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 uuid + +from openstackclient.tests.functional import base + + +class NetworkQosRuleTestsMinimumBandwidth(base.TestCase): + """Functional tests for QoS minimum bandwidth rule.""" + RULE_ID = None + QOS_POLICY_NAME = 'qos_policy_' + uuid.uuid4().hex + MIN_KBPS = 2800 + MIN_KBPS_MODIFIED = 7500 + DIRECTION = '--egress' + HEADERS = ['ID'] + FIELDS = ['id'] + TYPE = 'minimum-bandwidth' + + @classmethod + def setUpClass(cls): + opts = cls.get_opts(cls.FIELDS) + cls.openstack('network qos policy create ' + cls.QOS_POLICY_NAME) + cls.RULE_ID = cls.openstack('network qos rule create --type ' + + cls.TYPE + ' --min-kbps ' + + str(cls.MIN_KBPS) + ' ' + cls.DIRECTION + + ' ' + cls.QOS_POLICY_NAME + opts) + cls.assertsOutputNotNone(cls.RULE_ID) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('network qos rule delete ' + + cls.QOS_POLICY_NAME + ' ' + cls.RULE_ID) + cls.openstack('network qos policy delete ' + cls.QOS_POLICY_NAME) + cls.assertOutput('', raw_output) + + def test_qos_policy_list(self): + opts = self.get_opts(self.HEADERS) + raw_output = self.openstack('network qos rule list ' + + self.QOS_POLICY_NAME + opts) + self.assertIn(self.RULE_ID, raw_output) + + def test_qos_policy_show(self): + opts = self.get_opts(self.FIELDS) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(self.RULE_ID, raw_output) + + def test_qos_policy_set(self): + self.openstack('network qos rule set --min-kbps ' + + str(self.MIN_KBPS_MODIFIED) + ' ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID) + opts = self.get_opts(['min_kbps']) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(str(self.MIN_KBPS_MODIFIED) + "\n", raw_output) + + +class NetworkQosRuleTestsDSCPMarking(base.TestCase): + """Functional tests for QoS DSCP marking rule.""" + RULE_ID = None + QOS_POLICY_NAME = 'qos_policy_' + uuid.uuid4().hex + DSCP_MARK = 8 + DSCP_MARK_MODIFIED = 32 + HEADERS = ['ID'] + FIELDS = ['id'] + TYPE = 'dscp-marking' + + @classmethod + def setUpClass(cls): + opts = cls.get_opts(cls.FIELDS) + cls.openstack('network qos policy create ' + cls.QOS_POLICY_NAME) + cls.RULE_ID = cls.openstack('network qos rule create --type ' + + cls.TYPE + ' --dscp-mark ' + + str(cls.DSCP_MARK) + ' ' + + cls.QOS_POLICY_NAME + opts) + cls.assertsOutputNotNone(cls.RULE_ID) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('network qos rule delete ' + + cls.QOS_POLICY_NAME + ' ' + cls.RULE_ID) + cls.openstack('network qos policy delete ' + cls.QOS_POLICY_NAME) + cls.assertOutput('', raw_output) + + def test_qos_policy_list(self): + opts = self.get_opts(self.HEADERS) + raw_output = self.openstack('network qos rule list ' + + self.QOS_POLICY_NAME + opts) + self.assertIn(self.RULE_ID, raw_output) + + def test_qos_policy_show(self): + opts = self.get_opts(self.FIELDS) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(self.RULE_ID, raw_output) + + def test_qos_policy_set(self): + self.openstack('network qos rule set --dscp-mark ' + + str(self.DSCP_MARK_MODIFIED) + ' ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID) + opts = self.get_opts(['dscp_mark']) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(str(self.DSCP_MARK_MODIFIED) + "\n", raw_output) + + +class NetworkQosRuleTestsBandwidthLimit(base.TestCase): + """Functional tests for QoS bandwidth limit rule.""" + RULE_ID = None + QOS_POLICY_NAME = 'qos_policy_' + uuid.uuid4().hex + MAX_KBPS = 10000 + MAX_KBPS_MODIFIED = 15000 + MAX_BURST_KBITS = 1400 + MAX_BURST_KBITS_MODIFIED = 1800 + HEADERS = ['ID'] + FIELDS = ['id'] + TYPE = 'bandwidth-limit' + + @classmethod + def setUpClass(cls): + opts = cls.get_opts(cls.FIELDS) + cls.openstack('network qos policy create ' + cls.QOS_POLICY_NAME) + cls.RULE_ID = cls.openstack('network qos rule create --type ' + + cls.TYPE + ' --max-kbps ' + + str(cls.MAX_KBPS) + ' --max-burst-kbits ' + + str(cls.MAX_BURST_KBITS) + ' ' + + cls.QOS_POLICY_NAME + opts) + cls.assertsOutputNotNone(cls.RULE_ID) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('network qos rule delete ' + + cls.QOS_POLICY_NAME + ' ' + cls.RULE_ID) + cls.openstack('network qos policy delete ' + cls.QOS_POLICY_NAME) + cls.assertOutput('', raw_output) + + def test_qos_policy_list(self): + opts = self.get_opts(self.HEADERS) + raw_output = self.openstack('network qos rule list ' + + self.QOS_POLICY_NAME + opts) + self.assertIn(self.RULE_ID, raw_output) + + def test_qos_policy_show(self): + opts = self.get_opts(self.FIELDS) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(self.RULE_ID, raw_output) + + def test_qos_policy_set(self): + self.openstack('network qos rule set --max-kbps ' + + str(self.MAX_KBPS_MODIFIED) + ' --max-burst-kbits ' + + str(self.MAX_BURST_KBITS_MODIFIED) + ' ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID) + opts = self.get_opts(['max_kbps']) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(str(self.MAX_KBPS_MODIFIED) + "\n", raw_output) + opts = self.get_opts(['max_burst_kbps']) + raw_output = self.openstack('network qos rule show ' + + self.QOS_POLICY_NAME + ' ' + self.RULE_ID + + opts) + self.assertEqual(str(self.MAX_BURST_KBITS_MODIFIED) + "\n", raw_output) diff --git a/openstackclient/tests/functional/network/v2/test_network_qos_rule_type.py b/openstackclient/tests/functional/network/v2/test_network_qos_rule_type.py new file mode 100644 index 00000000..7dff0cbd --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_qos_rule_type.py @@ -0,0 +1,28 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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.tests.functional import base + + +class NetworkQosRuleTypeTests(base.TestCase): + """Functional tests for Network QoS rule type. """ + + AVAILABLE_RULE_TYPES = ['dscp_marking', + 'bandwidth_limit'] + + def test_qos_rule_type_list(self): + raw_output = self.openstack('network qos rule type list') + for rule_type in self.AVAILABLE_RULE_TYPES: + self.assertIn(rule_type, raw_output) diff --git a/openstackclient/tests/functional/network/v2/test_network_service_provider.py b/openstackclient/tests/functional/network/v2/test_network_service_provider.py new file mode 100644 index 00000000..6fbff6c8 --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_service_provider.py @@ -0,0 +1,26 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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.tests.functional import base + + +class TestNetworkServiceProvider(base.TestCase): + """Functional tests for network service provider""" + + SERVICE_TYPE = 'L3_ROUTER_NAT' + + def test_network_service_provider_list(self): + raw_output = self.openstack('network service provider list') + self.assertIn(self.SERVICE_TYPE, raw_output) diff --git a/openstackclient/tests/functional/network/v2/test_port.py b/openstackclient/tests/functional/network/v2/test_port.py index decd9553..bd5eefa5 100644 --- a/openstackclient/tests/functional/network/v2/test_port.py +++ b/openstackclient/tests/functional/network/v2/test_port.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from openstackclient.tests.functional import base @@ -19,40 +20,149 @@ class PortTests(base.TestCase): """Functional tests for port. """ NAME = uuid.uuid4().hex NETWORK_NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['name'] + SG_NAME = uuid.uuid4().hex @classmethod def setUpClass(cls): - # Create a network for the subnet. + # Create a network for the port cls.openstack('network create ' + cls.NETWORK_NAME) - opts = cls.get_opts(cls.FIELDS) - raw_output = cls.openstack( - 'port create --network ' + cls.NETWORK_NAME + ' ' + - cls.NAME + opts - ) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) @classmethod def tearDownClass(cls): - raw_output = cls.openstack('port delete ' + cls.NAME) - cls.assertOutput('', raw_output) raw_output = cls.openstack('network delete ' + cls.NETWORK_NAME) cls.assertOutput('', raw_output) + def test_port_delete(self): + """Test create, delete multiple""" + json_output = json.loads(self.openstack( + 'port create -f json --network ' + + self.NETWORK_NAME + ' ' + self.NAME + )) + id1 = json_output.get('id') + self.assertIsNotNone(id1) + self.assertIsNotNone(json_output.get('mac_address')) + self.assertEqual(self.NAME, json_output.get('name')) + + json_output = json.loads(self.openstack( + 'port create -f json --network ' + self.NETWORK_NAME + ' ' + + self.NAME + 'x' + )) + id2 = json_output.get('id') + self.assertIsNotNone(id2) + self.assertIsNotNone(json_output.get('mac_address')) + self.assertEqual(self.NAME + 'x', json_output.get('name')) + + # Clean up after ourselves + raw_output = self.openstack('port delete ' + id1 + ' ' + id2) + self.assertOutput('', raw_output) + def test_port_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('port list' + opts) - self.assertIn(self.NAME, raw_output) + """Test create defaults, list, delete""" + json_output = json.loads(self.openstack( + 'port create -f json --network ' + self.NETWORK_NAME + ' ' + + self.NAME + )) + id1 = json_output.get('id') + self.assertIsNotNone(id1) + mac1 = json_output.get('mac_address') + self.assertIsNotNone(mac1) + self.addCleanup(self.openstack, 'port delete ' + id1) + self.assertEqual(self.NAME, json_output.get('name')) + + json_output = json.loads(self.openstack( + 'port create -f json --network ' + self.NETWORK_NAME + ' ' + + self.NAME + 'x' + )) + id2 = json_output.get('id') + self.assertIsNotNone(id2) + mac2 = json_output.get('mac_address') + self.assertIsNotNone(mac2) + self.addCleanup(self.openstack, 'port delete ' + id2) + self.assertEqual(self.NAME + 'x', json_output.get('name')) + + # Test list + json_output = json.loads(self.openstack( + 'port list -f json' + )) + item_map = {item.get('ID'): item.get('MAC Address') for item in + json_output} + self.assertIn(id1, item_map.keys()) + self.assertIn(id2, item_map.keys()) + self.assertIn(mac1, item_map.values()) + self.assertIn(mac2, item_map.values()) + + # Test list --long + json_output = json.loads(self.openstack( + 'port list --long -f json' + )) + id_list = [item.get('ID') for item in json_output] + self.assertIn(id1, id_list) + self.assertIn(id2, id_list) + + # Test list --mac-address + json_output = json.loads(self.openstack( + 'port list -f json --mac-address ' + mac2 + )) + item_map = {item.get('ID'): item.get('MAC Address') for item in + json_output} + self.assertNotIn(id1, item_map.keys()) + self.assertIn(id2, item_map.keys()) + self.assertNotIn(mac1, item_map.values()) + self.assertIn(mac2, item_map.values()) def test_port_set(self): - self.openstack('port set --disable ' + self.NAME) - opts = self.get_opts(['name', 'admin_state_up']) - raw_output = self.openstack('port show ' + self.NAME + opts) - self.assertEqual("DOWN\n" + self.NAME + "\n", raw_output) - - def test_port_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('port show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + """Test create, set, show, delete""" + json_output = json.loads(self.openstack( + 'port create -f json ' + + '--network ' + self.NETWORK_NAME + ' ' + + '--description xyzpdq ' + '--disable ' + + self.NAME + )) + id1 = json_output.get('id') + self.addCleanup(self.openstack, 'port delete ' + id1) + self.assertEqual(self.NAME, json_output.get('name')) + self.assertEqual('xyzpdq', json_output.get('description')) + self.assertEqual('DOWN', json_output.get('admin_state_up')) + + raw_output = self.openstack( + 'port set ' + '--enable ' + self.NAME) + self.assertOutput('', raw_output) + + json_output = json.loads(self.openstack( + 'port show -f json ' + self.NAME + )) + sg_id = json_output.get('security_group_ids') + + self.assertEqual(self.NAME, json_output.get('name')) + self.assertEqual('xyzpdq', json_output.get('description')) + self.assertEqual('UP', json_output.get('admin_state_up')) + self.assertIsNotNone(json_output.get('mac_address')) + + raw_output = self.openstack( + 'port unset --security-group ' + sg_id + ' ' + id1) + self.assertOutput('', raw_output) + + json_output = json.loads(self.openstack( + 'port show -f json ' + self.NAME + )) + self.assertEqual('', json_output.get('security_group_ids')) + + def test_port_admin_set(self): + """Test create, set (as admin), show, delete""" + json_output = json.loads(self.openstack( + 'port create -f json ' + + '--network ' + self.NETWORK_NAME + ' ' + self.NAME + )) + id_ = json_output.get('id') + self.addCleanup(self.openstack, 'port delete ' + id_) + + raw_output = self.openstack( + '--os-username admin ' + + 'port set --mac-address 11:22:33:44:55:66 ' + + self.NAME) + self.assertOutput('', raw_output) + json_output = json.loads(self.openstack( + 'port show -f json ' + self.NAME + )) + self.assertEqual(json_output.get('mac_address'), '11:22:33:44:55:66') diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py index 789c3825..aa708e0a 100644 --- a/openstackclient/tests/functional/network/v2/test_router.py +++ b/openstackclient/tests/functional/network/v2/test_router.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from openstackclient.tests.functional import base @@ -17,34 +18,207 @@ from openstackclient.tests.functional import base class RouterTests(base.TestCase): """Functional tests for router. """ - NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['name'] - - @classmethod - def setUpClass(cls): - opts = cls.get_opts(cls.FIELDS) - raw_output = cls.openstack('router create ' + cls.NAME + opts) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) - - @classmethod - def tearDownClass(cls): - raw_output = cls.openstack('router delete ' + cls.NAME) - cls.assertOutput('', raw_output) + + def test_router_create_and_delete(self): + """Test create options, delete multiple""" + name1 = uuid.uuid4().hex + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'router create -f json ' + + name1 + )) + self.assertEqual( + name1, + cmd_output["name"], + ) + cmd_output = json.loads(self.openstack( + 'router create -f json ' + + name2 + )) + self.assertEqual( + name2, + cmd_output["name"], + ) + + del_output = self.openstack( + 'router delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) def test_router_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('router list' + opts) - self.assertIn(self.NAME, raw_output) - - def test_router_set(self): - self.openstack('router set --disable ' + self.NAME) - opts = self.get_opts(['name', 'admin_state_up']) - raw_output = self.openstack('router show ' + self.NAME + opts) - self.assertEqual("DOWN\n" + self.NAME + "\n", raw_output) - - def test_router_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('router show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + """Test create, list filter""" + # Get project IDs + cmd_output = json.loads(self.openstack('token issue -f json ')) + auth_project_id = cmd_output['project_id'] + + cmd_output = json.loads(self.openstack('project list -f json ')) + admin_project_id = None + demo_project_id = None + for p in cmd_output: + if p['Name'] == 'admin': + admin_project_id = p['ID'] + if p['Name'] == 'demo': + demo_project_id = p['ID'] + + # Verify assumptions: + # * admin and demo projects are present + # * demo and admin are distinct projects + # * tests run as admin + self.assertIsNotNone(admin_project_id) + self.assertIsNotNone(demo_project_id) + self.assertNotEqual(admin_project_id, demo_project_id) + self.assertEqual(admin_project_id, auth_project_id) + + name1 = uuid.uuid4().hex + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'router create -f json ' + + '--disable ' + + name1 + )) + + self.addCleanup(self.openstack, 'router delete ' + name1) + self.assertEqual( + name1, + cmd_output["name"], + ) + self.assertEqual( + "DOWN", + cmd_output["admin_state_up"], + ) + self.assertEqual( + admin_project_id, + cmd_output["project_id"], + ) + cmd_output = json.loads(self.openstack( + 'router create -f json ' + + '--project ' + demo_project_id + + ' ' + name2 + )) + + self.addCleanup(self.openstack, 'router delete ' + name2) + self.assertEqual( + name2, + cmd_output["name"], + ) + self.assertEqual( + "UP", + cmd_output["admin_state_up"], + ) + self.assertEqual( + demo_project_id, + cmd_output["project_id"], + ) + + # Test list --project + cmd_output = json.loads(self.openstack( + 'router list -f json ' + + '--project ' + demo_project_id + )) + names = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertIn(name2, names) + + # Test list --disable + cmd_output = json.loads(self.openstack( + 'router list -f json ' + + '--disable ' + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertNotIn(name2, names) + + # Test list --name + cmd_output = json.loads(self.openstack( + 'router list -f json ' + + '--name ' + name1 + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertNotIn(name2, names) + + # Test list --long + cmd_output = json.loads(self.openstack( + 'router list -f json ' + + '--long ' + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + def test_router_set_show_unset(self): + """Tests create router, set, unset, show""" + + name = uuid.uuid4().hex + new_name = name + "_" + cmd_output = json.loads(self.openstack( + 'router create -f json ' + + '--description aaaa ' + + name + )) + self.addCleanup(self.openstack, 'router delete ' + new_name) + self.assertEqual( + name, + cmd_output["name"], + ) + self.assertEqual( + 'aaaa', + cmd_output["description"], + ) + + # Test set --disable + cmd_output = self.openstack( + 'router set ' + + '--name ' + new_name + + ' --description bbbb ' + + '--disable ' + + name + ) + self.assertOutput('', cmd_output) + + cmd_output = json.loads(self.openstack( + 'router show -f json ' + + new_name + )) + self.assertEqual( + new_name, + cmd_output["name"], + ) + self.assertEqual( + 'bbbb', + cmd_output["description"], + ) + self.assertEqual( + 'DOWN', + cmd_output["admin_state_up"], + ) + + # Test set --ha --distributed + cmd_output = self.openstack( + 'router set ' + + '--distributed ' + + '--external-gateway public ' + + new_name + ) + self.assertOutput('', cmd_output) + + cmd_output = json.loads(self.openstack( + 'router show -f json ' + + new_name + )) + self.assertEqual( + True, + cmd_output["distributed"], + ) + self.assertIsNotNone(cmd_output["external_gateway_info"]) + + # Test unset + cmd_output = self.openstack( + 'router unset ' + + '--external-gateway ' + + new_name + ) + cmd_output = json.loads(self.openstack( + 'router show -f json ' + + new_name + )) + self.assertIsNone(cmd_output["external_gateway_info"]) diff --git a/openstackclient/tests/functional/network/v2/test_subnet.py b/openstackclient/tests/functional/network/v2/test_subnet.py index 231671f3..61cffcde 100644 --- a/openstackclient/tests/functional/network/v2/test_subnet.py +++ b/openstackclient/tests/functional/network/v2/test_subnet.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import json +import random import uuid from openstackclient.tests.functional import base @@ -17,50 +19,253 @@ from openstackclient.tests.functional import base class SubnetTests(base.TestCase): """Functional tests for subnet. """ - NAME = uuid.uuid4().hex - NETWORK_NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['name'] @classmethod def setUpClass(cls): - # Create a network for the subnet. - cls.openstack('network create ' + cls.NETWORK_NAME) - opts = cls.get_opts(cls.FIELDS) - raw_output = cls.openstack( - 'subnet create --network ' + cls.NETWORK_NAME + - ' --subnet-range 10.10.10.0/24 ' + - cls.NAME + opts - ) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) + # Create a network for the all subnet tests. + cls.NETWORK_NAME = uuid.uuid4().hex + cmd_output = json.loads(cls.openstack( + 'network create -f json ' + + cls.NETWORK_NAME + )) + # Get network_id for assertEqual + cls.NETWORK_ID = cmd_output["id"] @classmethod def tearDownClass(cls): - raw_output = cls.openstack('subnet delete ' + cls.NAME) - cls.assertOutput('', raw_output) raw_output = cls.openstack('network delete ' + cls.NETWORK_NAME) cls.assertOutput('', raw_output) + def test_subnet_create_and_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd = ('subnet create -f json --network ' + + self.NETWORK_NAME + + ' --subnet-range') + cmd_output = self._subnet_create(cmd, name1) + self.assertEqual( + name1, + cmd_output["name"], + ) + self.assertEqual( + self.NETWORK_ID, + cmd_output["network_id"], + ) + name2 = uuid.uuid4().hex + cmd = ('subnet create -f json --network ' + + self.NETWORK_NAME + + ' --subnet-range') + cmd_output = self._subnet_create(cmd, name2) + self.assertEqual( + name2, + cmd_output["name"], + ) + self.assertEqual( + self.NETWORK_ID, + cmd_output["network_id"], + ) + + del_output = self.openstack( + 'subnet delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) + def test_subnet_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('subnet list' + opts) - self.assertIn(self.NAME, raw_output) - - def test_subnet_set(self): - self.openstack('subnet set --no-dhcp ' + self.NAME) - opts = self.get_opts(['name', 'enable_dhcp']) - raw_output = self.openstack('subnet show ' + self.NAME + opts) - self.assertEqual("False\n" + self.NAME + "\n", raw_output) - - def test_subnet_set_service_type(self): - TYPE = 'network:floatingip_agent_gateway' - self.openstack('subnet set --service-type ' + TYPE + ' ' + self.NAME) - opts = self.get_opts(['name', 'service_types']) - raw_output = self.openstack('subnet show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n" + TYPE + "\n", raw_output) - - def test_subnet_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('subnet show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + """Test create, list filter""" + name1 = uuid.uuid4().hex + name2 = uuid.uuid4().hex + cmd = ('subnet create -f json ' + + '--network ' + self.NETWORK_NAME + + ' --dhcp --subnet-range') + cmd_output = self._subnet_create(cmd, name1) + + self.addCleanup(self.openstack, 'subnet delete ' + name1) + self.assertEqual( + name1, + cmd_output["name"], + ) + self.assertEqual( + True, + cmd_output["enable_dhcp"], + ) + self.assertEqual( + self.NETWORK_ID, + cmd_output["network_id"], + ) + self.assertEqual( + 4, + cmd_output["ip_version"], + ) + + cmd = ('subnet create -f json ' + + '--network ' + self.NETWORK_NAME + + ' --ip-version 6 --no-dhcp ' + + '--subnet-range') + cmd_output = self._subnet_create(cmd, name2, is_type_ipv4=False) + + self.addCleanup(self.openstack, 'subnet delete ' + name2) + self.assertEqual( + name2, + cmd_output["name"], + ) + self.assertEqual( + False, + cmd_output["enable_dhcp"], + ) + self.assertEqual( + self.NETWORK_ID, + cmd_output["network_id"], + ) + self.assertEqual( + 6, + cmd_output["ip_version"], + ) + + # Test list --long + cmd_output = json.loads(self.openstack( + 'subnet list -f json ' + + '--long ' + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test list --name + cmd_output = json.loads(self.openstack( + 'subnet list -f json ' + + '--name ' + name1 + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertNotIn(name2, names) + + # Test list --ip-version + cmd_output = json.loads(self.openstack( + 'subnet list -f json ' + + '--ip-version 6' + )) + names = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertIn(name2, names) + + # Test list --network + cmd_output = json.loads(self.openstack( + 'subnet list -f json ' + + '--network ' + self.NETWORK_ID + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test list --no-dhcp + cmd_output = json.loads(self.openstack( + 'subnet list -f json ' + + '--no-dhcp ' + )) + names = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertIn(name2, names) + + def test_subnet_set_show_unset(self): + """Test create subnet, set, unset, show""" + + name = uuid.uuid4().hex + new_name = name + "_" + cmd = ('subnet create -f json ' + + '--network ' + self.NETWORK_NAME + + ' --description aaaa --subnet-range') + cmd_output = self._subnet_create(cmd, name) + + self.addCleanup(self.openstack, 'subnet delete ' + new_name) + self.assertEqual( + name, + cmd_output["name"], + ) + self.assertEqual( + 'aaaa', + cmd_output["description"], + ) + + # Test set --no-dhcp --name --gateway --description + cmd_output = self.openstack( + 'subnet set ' + + '--name ' + new_name + + ' --description bbbb ' + + '--no-dhcp ' + + '--gateway 10.10.11.1 ' + + '--service-type network:floatingip_agent_gateway ' + + name + ) + self.assertOutput('', cmd_output) + + cmd_output = json.loads(self.openstack( + 'subnet show -f json ' + + new_name + )) + self.assertEqual( + new_name, + cmd_output["name"], + ) + self.assertEqual( + 'bbbb', + cmd_output["description"], + ) + self.assertEqual( + False, + cmd_output["enable_dhcp"], + ) + self.assertEqual( + '10.10.11.1', + cmd_output["gateway_ip"], + ) + self.assertEqual( + 'network:floatingip_agent_gateway', + cmd_output["service_types"], + ) + + # Test unset + cmd_output = self.openstack( + 'subnet unset ' + + '--service-type network:floatingip_agent_gateway ' + + new_name + ) + self.assertOutput('', cmd_output) + + cmd_output = json.loads(self.openstack( + 'subnet show -f json ' + + new_name + )) + self.assertEqual( + '', + cmd_output["service_types"], + ) + + def _subnet_create(self, cmd, name, is_type_ipv4=True): + # Try random subnet range for subnet creating + # Because we can not determine ahead of time what subnets are already + # in use, possibly by another test running in parallel, try 4 times + for i in range(4): + # Make a random subnet + if is_type_ipv4: + subnet = ".".join(map( + str, + (random.randint(0, 223) for _ in range(3)) + )) + ".0/26" + else: + subnet = ":".join(map( + str, + (hex(random.randint(0, 65535))[2:] for _ in range(7)) + )) + ":0/112" + try: + cmd_output = json.loads(self.openstack( + cmd + ' ' + subnet + ' ' + + name + )) + except Exception: + if (i == 3): + # raise the exception at the last time + raise + pass + else: + # break and no longer retry if create sucessfully + break + return cmd_output diff --git a/openstackclient/tests/functional/post_test_hook_tips.sh b/openstackclient/tests/functional/post_test_hook_tips.sh new file mode 100755 index 00000000..28ab9580 --- /dev/null +++ b/openstackclient/tests/functional/post_test_hook_tips.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# This is a script that kicks off a series of functional tests against an +# OpenStack cloud. It will attempt to create an instance if one is not +# available. Do not run this script unless you know what you're doing. +# For more information refer to: +# http://docs.openstack.org/developer/python-openstackclient/ + +# This particular script differs from the normal post_test_hook because +# it installs the master (tip) version of osc-lib, os-client-config +# and openstacksdk, OSCs most important dependencies. + +function generate_testr_results { + if [ -f .testrepository/0 ]; then + sudo .tox/functional-tips/bin/testr last --subunit > $WORKSPACE/testrepository.subunit + sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit + sudo .tox/functional-tips/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html + sudo gzip -9 $BASE/logs/testrepository.subunit + sudo gzip -9 $BASE/logs/testr_results.html + sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz + fi +} + +export OPENSTACKCLIENT_DIR="$BASE/new/python-openstackclient" +sudo chown -R jenkins:stack $OPENSTACKCLIENT_DIR + +# Go to the openstackclient dir +cd $OPENSTACKCLIENT_DIR + +# Run tests +echo "Running openstackclient functional-tips test suite" +set +e + +# Source environment variables to kick things off +source ~stack/devstack/openrc admin admin +echo 'Running tests with:' +env | grep OS + +# Preserve env for OS_ credentials +sudo -E -H -u jenkins tox -e functional-tips +EXIT_CODE=$? +set -e + +# Collect and parse result +generate_testr_results +exit $EXIT_CODE diff --git a/openstackclient/tests/functional/volume/v1/test_qos.py b/openstackclient/tests/functional/volume/v1/test_qos.py index 770d5acb..434840f6 100644 --- a/openstackclient/tests/functional/volume/v1/test_qos.py +++ b/openstackclient/tests/functional/volume/v1/test_qos.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from openstackclient.tests.functional.volume.v1 import common @@ -18,38 +19,103 @@ from openstackclient.tests.functional.volume.v1 import common class QosTests(common.BaseVolumeTests): """Functional tests for volume qos. """ - NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['id', 'name'] - ID = None - - @classmethod - def setUpClass(cls): - super(QosTests, cls).setUpClass() - opts = cls.get_opts(cls.FIELDS) - raw_output = cls.openstack('volume qos create ' + cls.NAME + opts) - cls.ID, name, rol = raw_output.split('\n') - cls.assertOutput(cls.NAME, name) - - @classmethod - def tearDownClass(cls): - raw_output = cls.openstack('volume qos delete ' + cls.ID) - cls.assertOutput('', raw_output) - - def test_volume_qos_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('volume qos list' + opts) - self.assertIn(self.NAME, raw_output) - - def test_volume_qos_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('volume qos show ' + self.ID + opts) - self.assertEqual(self.ID + "\n" + self.NAME + "\n", raw_output) - - def test_volume_qos_metadata(self): + def test_volume_qos_create_list(self): + """Test create, list, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume qos create -f json ' + + name1 + )) + self.assertEqual( + name1, + cmd_output['name'] + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume qos create -f json ' + + name2 + )) + self.assertEqual( + name2, + cmd_output['name'] + ) + + # Test list + cmd_output = json.loads(self.openstack( + 'volume qos list -f json' + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test delete multiple + del_output = self.openstack('volume qos delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) + + def test_volume_qos_set_show_unset(self): + """Tests create volume qos, set, unset, show, delete""" + + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume qos create -f json ' + + '--consumer front-end ' + '--property Alpha=a ' + + name + )) + self.addCleanup(self.openstack, 'volume qos delete ' + name) + self.assertEqual( + name, + cmd_output['name'] + ) + + self.assertEqual( + "front-end", + cmd_output['consumer'] + ) + + # Test volume qos set + raw_output = self.openstack( + 'volume qos set ' + + '--property Alpha=c ' + + '--property Beta=b ' + + name, + ) + self.assertOutput('', raw_output) + + # Test volume qos show + cmd_output = json.loads(self.openstack( + 'volume qos show -f json ' + + name + )) + self.assertEqual( + name, + cmd_output['name'] + ) + self.assertEqual( + "Alpha='c', Beta='b'", + cmd_output['properties'] + ) + + # Test volume qos unset raw_output = self.openstack( - 'volume qos set --property a=b --property c=d ' + self.ID) - self.assertEqual("", raw_output) - opts = self.get_opts(['name', 'specs']) - raw_output = self.openstack('volume qos show ' + self.ID + opts) - self.assertEqual(self.NAME + "\na='b', c='d'\n", raw_output) + 'volume qos unset ' + + '--property Alpha ' + + name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume qos show -f json ' + + name + )) + self.assertEqual( + name, + cmd_output['name'] + ) + self.assertEqual( + "Beta='b'", + cmd_output['properties'] + ) + + # TODO(qiangjiahui): Add tests for associate and disassociate volume type diff --git a/openstackclient/tests/functional/volume/v1/test_snapshot.py b/openstackclient/tests/functional/volume/v1/test_snapshot.py index c6d65ccc..89a98661 100644 --- a/openstackclient/tests/functional/volume/v1/test_snapshot.py +++ b/openstackclient/tests/functional/volume/v1/test_snapshot.py @@ -10,19 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +import json import time import uuid from openstackclient.tests.functional.volume.v1 import common -class SnapshotTests(common.BaseVolumeTests): - """Functional tests for snapshot. """ +class VolumeSnapshotTests(common.BaseVolumeTests): + """Functional tests for volume snapshot. """ VOLLY = uuid.uuid4().hex - NAME = uuid.uuid4().hex - OTHER_NAME = uuid.uuid4().hex - HEADERS = ['"Name"'] @classmethod def wait_for_status(cls, command, status, tries): @@ -30,57 +28,223 @@ class SnapshotTests(common.BaseVolumeTests): for attempt in range(tries): time.sleep(1) raw_output = cls.openstack(command + opts) - if (raw_output == status): + if (raw_output.rstrip() == status): return cls.assertOutput(status, raw_output) @classmethod def setUpClass(cls): - super(SnapshotTests, cls).setUpClass() - cls.openstack('volume create --size 1 ' + cls.VOLLY) - cls.wait_for_status('volume show ' + cls.VOLLY, 'available\n', 3) - opts = cls.get_opts(['status']) - raw_output = cls.openstack('snapshot create --name ' + cls.NAME + - ' ' + cls.VOLLY + opts) - cls.assertOutput('creating\n', raw_output) - cls.wait_for_status('snapshot show ' + cls.NAME, 'available\n', 3) + super(VolumeSnapshotTests, cls).setUpClass() + # create a volume for all tests to create snapshot + cmd_output = json.loads(cls.openstack( + 'volume create -f json ' + + '--size 1 ' + + cls.VOLLY + )) + cls.wait_for_status('volume show ' + cls.VOLLY, 'available', 6) + cls.VOLUME_ID = cmd_output['id'] @classmethod def tearDownClass(cls): - # Rename test - raw_output = cls.openstack( - 'snapshot set --name ' + cls.OTHER_NAME + ' ' + cls.NAME) + cls.wait_for_status('volume show ' + cls.VOLLY, 'available', 6) + raw_output = cls.openstack('volume delete --force ' + cls.VOLLY) cls.assertOutput('', raw_output) - # Delete test - raw_output_snapshot = cls.openstack( - 'snapshot delete ' + cls.OTHER_NAME) - cls.wait_for_status('volume show ' + cls.VOLLY, 'available\n', 6) - raw_output_volume = cls.openstack('volume delete --force ' + cls.VOLLY) - cls.assertOutput('', raw_output_snapshot) - cls.assertOutput('', raw_output_volume) - - def test_snapshot_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('snapshot list' + opts) - self.assertIn(self.NAME, raw_output) - - def test_snapshot_set_unset_properties(self): + + def test_volume_snapshot__delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name1 + + ' --volume ' + self.VOLLY + )) + self.assertEqual( + name1, + cmd_output["display_name"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name2 + + ' --volume ' + self.VOLLY + )) + self.assertEqual( + name2, + cmd_output["display_name"], + ) + + self.wait_for_status( + 'volume snapshot show ' + name1, 'available', 6) + self.wait_for_status( + 'volume snapshot show ' + name2, 'available', 6) + + del_output = self.openstack( + 'volume snapshot delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) + + def test_volume_snapshot_list(self): + """Test create, list filter""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name1 + + ' --volume ' + self.VOLLY + )) + self.addCleanup(self.openstack, 'volume snapshot delete ' + name1) + self.assertEqual( + name1, + cmd_output["display_name"], + ) + self.assertEqual( + self.VOLUME_ID, + cmd_output["volume_id"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.wait_for_status( + 'volume snapshot show ' + name1, 'available', 6) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name2 + + ' --volume ' + self.VOLLY + )) + self.addCleanup(self.openstack, 'volume snapshot delete ' + name2) + self.assertEqual( + name2, + cmd_output["display_name"], + ) + self.assertEqual( + self.VOLUME_ID, + cmd_output["volume_id"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.wait_for_status( + 'volume snapshot show ' + name2, 'available', 6) + + # Test list --long, --status + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--long ' + + '--status error' + )) + names = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertNotIn(name2, names) + + # Test list --volume + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--volume ' + self.VOLLY + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test list --name + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--name ' + name1 + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertNotIn(name2, names) + + def test_snapshot_set(self): + """Test create, set, unset, show, delete volume snapshot""" + name = uuid.uuid4().hex + new_name = name + "_" + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + '--volume ' + self.VOLLY + + ' --description aaaa ' + + name + )) + self.addCleanup(self.openstack, 'volume snapshot delete ' + new_name) + self.assertEqual( + name, + cmd_output["display_name"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.assertEqual( + 'aaaa', + cmd_output["display_description"], + ) + self.wait_for_status( + 'volume snapshot show ' + name, 'available', 6) + + # Test volume snapshot set + raw_output = self.openstack( + 'volume snapshot set ' + + '--name ' + new_name + + ' --description bbbb ' + + '--property Alpha=a ' + + '--property Beta=b ' + + name, + ) + self.assertOutput('', raw_output) + + # Show snapshot set result + cmd_output = json.loads(self.openstack( + 'volume snapshot show -f json ' + + new_name + )) + self.assertEqual( + new_name, + cmd_output["display_name"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.assertEqual( + 'bbbb', + cmd_output["display_description"], + ) + self.assertEqual( + "Alpha='a', Beta='b'", + cmd_output["properties"], + ) + + # Test volume unset raw_output = self.openstack( - 'snapshot set --property a=b --property c=d ' + self.NAME) - self.assertEqual("", raw_output) - opts = self.get_opts(["properties"]) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) - self.assertEqual("a='b', c='d'\n", raw_output) - - raw_output = self.openstack('snapshot unset --property a ' + self.NAME) - self.assertEqual("", raw_output) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) - self.assertEqual("c='d'\n", raw_output) - - def test_snapshot_set_description(self): + 'volume snapshot unset ' + + '--property Alpha ' + + new_name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume snapshot show -f json ' + + new_name + )) + self.assertEqual( + "Beta='b'", + cmd_output["properties"], + ) + + # Test volume snapshot set --no-property raw_output = self.openstack( - 'snapshot set --description backup ' + self.NAME) - self.assertEqual("", raw_output) - opts = self.get_opts(["display_description", "display_name"]) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) - self.assertEqual("backup\n" + self.NAME + "\n", raw_output) + 'volume snapshot set ' + + '--no-property ' + + new_name, + ) + self.assertOutput('', raw_output) + cmd_output = json.loads(self.openstack( + 'volume snapshot show -f json ' + + new_name + )) + self.assertNotIn( + "Beta='b'", + cmd_output["properties"], + ) diff --git a/openstackclient/tests/functional/volume/v1/test_volume.py b/openstackclient/tests/functional/volume/v1/test_volume.py index 5e4bcbea..3f04e071 100644 --- a/openstackclient/tests/functional/volume/v1/test_volume.py +++ b/openstackclient/tests/functional/volume/v1/test_volume.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import json +import time import uuid from openstackclient.tests.functional.volume.v1 import common @@ -18,71 +20,190 @@ from openstackclient.tests.functional.volume.v1 import common class VolumeTests(common.BaseVolumeTests): """Functional tests for volume. """ - NAME = uuid.uuid4().hex - OTHER_NAME = uuid.uuid4().hex - HEADERS = ['"Display Name"'] - FIELDS = ['display_name'] - - @classmethod - def setUpClass(cls): - super(VolumeTests, cls).setUpClass() - opts = cls.get_opts(cls.FIELDS) - raw_output = cls.openstack('volume create --size 1 ' + cls.NAME + opts) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) - - @classmethod - def tearDownClass(cls): - # Rename test - raw_output = cls.openstack( - 'volume set --name ' + cls.OTHER_NAME + ' ' + cls.NAME) - cls.assertOutput('', raw_output) - # Delete test - raw_output = cls.openstack('volume delete ' + cls.OTHER_NAME) - cls.assertOutput('', raw_output) + def test_volume_create_and_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + name1 + )) + self.assertEqual( + 1, + cmd_output["size"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 2 ' + + name2 + )) + self.assertEqual( + 2, + cmd_output["size"], + ) + + self.wait_for("volume", name1, "available") + self.wait_for("volume", name2, "available") + del_output = self.openstack('volume delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) def test_volume_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('volume list' + opts) - self.assertIn(self.NAME, raw_output) + """Test create, list filter""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + name1 + )) + self.addCleanup(self.openstack, 'volume delete ' + name1) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.wait_for("volume", name1, "available") + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 2 ' + + name2 + )) + self.addCleanup(self.openstack, 'volume delete ' + name2) + self.assertEqual( + 2, + cmd_output["size"], + ) + self.wait_for("volume", name2, "available") + + # Test list + cmd_output = json.loads(self.openstack( + 'volume list -f json ' + )) + names = [x["Display Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test list --long + cmd_output = json.loads(self.openstack( + 'volume list -f json --long' + )) + bootable = [x["Bootable"] for x in cmd_output] + self.assertIn('false', bootable) + + # Test list --name + cmd_output = json.loads(self.openstack( + 'volume list -f json ' + + '--name ' + name1 + )) + names = [x["Display Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertNotIn(name2, names) - def test_volume_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + def test_volume_set_and_unset(self): + """Tests create volume, set, unset, show, delete""" + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + '--description aaaa ' + + '--property Alpha=a ' + + name + )) + self.assertEqual( + name, + cmd_output["display_name"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.assertEqual( + 'aaaa', + cmd_output["display_description"], + ) + self.assertEqual( + "Alpha='a'", + cmd_output["properties"], + ) + self.assertEqual( + 'false', + cmd_output["bootable"], + ) + self.wait_for("volume", name, "available") - def test_volume_properties(self): + # Test volume set + new_name = uuid.uuid4().hex + self.addCleanup(self.openstack, 'volume delete ' + new_name) raw_output = self.openstack( - 'volume set --property a=b --property c=d ' + self.NAME) - self.assertEqual("", raw_output) - opts = self.get_opts(["properties"]) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual("a='b', c='d'\n", raw_output) - - raw_output = self.openstack('volume unset --property a ' + self.NAME) - self.assertEqual("", raw_output) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual("c='d'\n", raw_output) - - def test_volume_set(self): - self.openstack('volume set --description RAMAC ' + self.NAME) - opts = self.get_opts(["display_description", "display_name"]) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual("RAMAC\n" + self.NAME + "\n", raw_output) - - def test_volume_set_size(self): - self.openstack('volume set --size 2 ' + self.NAME) - opts = self.get_opts(["display_name", "size"]) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n2\n", raw_output) - - def test_volume_set_bootable(self): - self.openstack('volume set --bootable ' + self.NAME) - opts = self.get_opts(["bootable"]) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual("true\n", raw_output) - - self.openstack('volume set --non-bootable ' + self.NAME) - opts = self.get_opts(["bootable"]) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual("false\n", raw_output) + 'volume set ' + + '--name ' + new_name + + ' --size 2 ' + + '--description bbbb ' + + '--no-property ' + + '--property Beta=b ' + + '--property Gamma=c ' + + '--bootable ' + + name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + new_name + )) + self.assertEqual( + new_name, + cmd_output["display_name"], + ) + self.assertEqual( + 2, + cmd_output["size"], + ) + self.assertEqual( + 'bbbb', + cmd_output["display_description"], + ) + self.assertEqual( + "Beta='b', Gamma='c'", + cmd_output["properties"], + ) + self.assertEqual( + 'true', + cmd_output["bootable"], + ) + + # Test volume unset + raw_output = self.openstack( + 'volume unset ' + + '--property Beta ' + + new_name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + new_name + )) + self.assertEqual( + "Gamma='c'", + cmd_output["properties"], + ) + + def wait_for(self, check_type, check_name, desired_status, wait=120, + interval=5, failures=['ERROR']): + status = "notset" + total_sleep = 0 + opts = self.get_opts(['status']) + while total_sleep < wait: + status = self.openstack(check_type + ' show ' + check_name + opts) + status = status.rstrip() + print('Checking {} {} Waiting for {} current status: {}' + .format(check_type, check_name, desired_status, status)) + if status == desired_status: + break + self.assertNotIn(status, failures) + time.sleep(interval) + total_sleep += interval + self.assertEqual(desired_status, status) diff --git a/openstackclient/tests/functional/volume/v1/test_volume_type.py b/openstackclient/tests/functional/volume/v1/test_volume_type.py index 955759b6..d1842795 100644 --- a/openstackclient/tests/functional/volume/v1/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v1/test_volume_type.py @@ -87,3 +87,74 @@ class VolumeTypeTests(common.BaseVolumeTests): time.sleep(5) raw_output = self.openstack(cmd) self.assertOutput('', raw_output) + + # NOTE: Add some basic funtional tests with the old format to + # make sure the command works properly, need to change + # these to new test format when beef up all tests for + # volume tye commands. + def test_encryption_type(self): + encryption_type = uuid.uuid4().hex + # test create new encryption type + opts = self.get_opts(['encryption']) + raw_output = self.openstack( + 'volume type create ' + '--encryption-provider LuksEncryptor ' + '--encryption-cipher aes-xts-plain64 ' + '--encryption-key-size 128 ' + '--encryption-control-location front-end ' + + encryption_type + opts) + expected = ["provider='LuksEncryptor'", + "cipher='aes-xts-plain64'", + "key_size='128'", + "control_location='front-end'"] + for attr in expected: + self.assertIn(attr, raw_output) + # test show encryption type + opts = self.get_opts(['encryption']) + raw_output = self.openstack( + 'volume type show --encryption-type ' + encryption_type + opts) + expected = ["provider='LuksEncryptor'", + "cipher='aes-xts-plain64'", + "key_size='128'", + "control_location='front-end'"] + for attr in expected: + self.assertIn(attr, raw_output) + # test list encryption type + opts = self.get_opts(['Encryption']) + raw_output = self.openstack( + 'volume type list --encryption-type ' + opts) + expected = ["provider='LuksEncryptor'", + "cipher='aes-xts-plain64'", + "key_size='128'", + "control_location='front-end'"] + for attr in expected: + self.assertIn(attr, raw_output) + # test set new encryption type + raw_output = self.openstack( + 'volume type set ' + '--encryption-provider LuksEncryptor ' + '--encryption-cipher aes-xts-plain64 ' + '--encryption-key-size 128 ' + '--encryption-control-location front-end ' + + self.NAME) + self.assertEqual('', raw_output) + opts = self.get_opts(['encryption']) + raw_output = self.openstack( + 'volume type show --encryption-type ' + self.NAME + opts) + expected = ["provider='LuksEncryptor'", + "cipher='aes-xts-plain64'", + "key_size='128'", + "control_location='front-end'"] + for attr in expected: + self.assertIn(attr, raw_output) + # test unset encryption type + raw_output = self.openstack( + 'volume type unset --encryption-type ' + self.NAME) + self.assertEqual('', raw_output) + opts = self.get_opts(['encryption']) + raw_output = self.openstack( + 'volume type show --encryption-type ' + self.NAME + opts) + self.assertEqual('\n', raw_output) + # test delete encryption type + raw_output = self.openstack('volume type delete ' + encryption_type) + self.assertEqual('', raw_output) diff --git a/openstackclient/tests/functional/volume/v2/test_qos.py b/openstackclient/tests/functional/volume/v2/test_qos.py index a54acbfd..aee10dca 100644 --- a/openstackclient/tests/functional/volume/v2/test_qos.py +++ b/openstackclient/tests/functional/volume/v2/test_qos.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from openstackclient.tests.functional.volume.v2 import common @@ -18,45 +19,107 @@ from openstackclient.tests.functional.volume.v2 import common class QosTests(common.BaseVolumeTests): """Functional tests for volume qos. """ - NAME = uuid.uuid4().hex - HEADERS = ['Name'] - FIELDS = ['id', 'name'] - ID = None - - @classmethod - def setUpClass(cls): - super(QosTests, cls).setUpClass() - opts = cls.get_opts(cls.FIELDS) - raw_output = cls.openstack('volume qos create ' + cls.NAME + opts) - cls.ID, name, rol = raw_output.split('\n') - cls.assertOutput(cls.NAME, name) - - @classmethod - def tearDownClass(cls): - raw_output = cls.openstack('volume qos delete ' + cls.ID) - cls.assertOutput('', raw_output) - - def test_volume_qos_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('volume qos list' + opts) - self.assertIn(self.NAME, raw_output) - - def test_volume_qos_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('volume qos show ' + self.ID + opts) - self.assertEqual(self.ID + "\n" + self.NAME + "\n", raw_output) - - def test_volume_qos_metadata(self): + def test_volume_qos_create_delete_list(self): + """Test create, list, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume qos create -f json ' + + name1 + )) + self.assertEqual( + name1, + cmd_output['name'] + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume qos create -f json ' + + name2 + )) + self.assertEqual( + name2, + cmd_output['name'] + ) + + # Test list + cmd_output = json.loads(self.openstack( + 'volume qos list -f json' + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test delete multiple + del_output = self.openstack('volume qos delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) + + def test_volume_qos_set_show_unset(self): + """Tests create volume qos, set, unset, show, delete""" + + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume qos create -f json ' + + '--consumer front-end ' + '--property Alpha=a ' + + name + )) + self.addCleanup(self.openstack, 'volume qos delete ' + name) + self.assertEqual( + name, + cmd_output['name'] + ) + + self.assertEqual( + "front-end", + cmd_output['consumer'] + ) + self.assertEqual( + "Alpha='a'", + cmd_output['properties'] + ) + + # Test volume qos set raw_output = self.openstack( - 'volume qos set --property a=b --property c=d ' + self.ID) - self.assertEqual("", raw_output) - opts = self.get_opts(['name', 'specs']) - raw_output = self.openstack('volume qos show ' + self.ID + opts) - self.assertEqual(self.NAME + "\na='b', c='d'\n", raw_output) + 'volume qos set ' + + '--property Alpha=c ' + + '--property Beta=b ' + + name, + ) + self.assertOutput('', raw_output) + + # Test volume qos show + cmd_output = json.loads(self.openstack( + 'volume qos show -f json ' + + name + )) + self.assertEqual( + name, + cmd_output['name'] + ) + self.assertEqual( + "Alpha='c', Beta='b'", + cmd_output['properties'] + ) + # Test volume qos unset raw_output = self.openstack( - 'volume qos unset --property a ' + self.ID) - self.assertEqual("", raw_output) - opts = self.get_opts(['name', 'specs']) - raw_output = self.openstack('volume qos show ' + self.ID + opts) - self.assertEqual(self.NAME + "\nc='d'\n", raw_output) + 'volume qos unset ' + + '--property Alpha ' + + name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume qos show -f json ' + + name + )) + self.assertEqual( + name, + cmd_output['name'] + ) + self.assertEqual( + "Beta='b'", + cmd_output['properties'] + ) + + # TODO(qiangjiahui): Add tests for associate and disassociate volume type diff --git a/openstackclient/tests/functional/volume/v2/test_snapshot.py b/openstackclient/tests/functional/volume/v2/test_snapshot.py index fcbc31cb..422e5b7c 100644 --- a/openstackclient/tests/functional/volume/v2/test_snapshot.py +++ b/openstackclient/tests/functional/volume/v2/test_snapshot.py @@ -10,19 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +import json import time import uuid from openstackclient.tests.functional.volume.v2 import common -class SnapshotTests(common.BaseVolumeTests): - """Functional tests for snapshot. """ +class VolumeSnapshotTests(common.BaseVolumeTests): + """Functional tests for volume snapshot. """ VOLLY = uuid.uuid4().hex - NAME = uuid.uuid4().hex - OTHER_NAME = uuid.uuid4().hex - HEADERS = ['"Name"'] @classmethod def wait_for_status(cls, command, status, tries): @@ -30,57 +28,234 @@ class SnapshotTests(common.BaseVolumeTests): for attempt in range(tries): time.sleep(1) raw_output = cls.openstack(command + opts) - if (raw_output == status): + if (raw_output.rstrip() == status): return cls.assertOutput(status, raw_output) @classmethod def setUpClass(cls): - super(SnapshotTests, cls).setUpClass() - cls.openstack('volume create --size 1 ' + cls.VOLLY) - cls.wait_for_status('volume show ' + cls.VOLLY, 'available\n', 3) - opts = cls.get_opts(['status']) - raw_output = cls.openstack('snapshot create --name ' + cls.NAME + - ' ' + cls.VOLLY + opts) - cls.assertOutput('creating\n', raw_output) - cls.wait_for_status('snapshot show ' + cls.NAME, 'available\n', 3) + super(VolumeSnapshotTests, cls).setUpClass() + # create a volume for all tests to create snapshot + cmd_output = json.loads(cls.openstack( + 'volume create -f json ' + + '--size 1 ' + + cls.VOLLY + )) + cls.wait_for_status('volume show ' + cls.VOLLY, 'available', 6) + cls.VOLUME_ID = cmd_output['id'] @classmethod def tearDownClass(cls): - # Rename test - raw_output = cls.openstack( - 'snapshot set --name ' + cls.OTHER_NAME + ' ' + cls.NAME) + cls.wait_for_status('volume show ' + cls.VOLLY, 'available', 6) + raw_output = cls.openstack('volume delete --force ' + cls.VOLLY) cls.assertOutput('', raw_output) - # Delete test - raw_output_snapshot = cls.openstack( - 'snapshot delete ' + cls.OTHER_NAME) - cls.wait_for_status('volume show ' + cls.VOLLY, 'available\n', 6) - raw_output_volume = cls.openstack('volume delete --force ' + cls.VOLLY) - cls.assertOutput('', raw_output_snapshot) - cls.assertOutput('', raw_output_volume) - - def test_snapshot_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('snapshot list' + opts) - self.assertIn(self.NAME, raw_output) - - def test_snapshot_properties(self): + + def test_volume_snapshot__delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name1 + + ' --volume ' + self.VOLLY + )) + self.assertEqual( + name1, + cmd_output["name"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name2 + + ' --volume ' + self.VOLLY + )) + self.assertEqual( + name2, + cmd_output["name"], + ) + + self.wait_for_status( + 'volume snapshot show ' + name1, 'available', 6) + self.wait_for_status( + 'volume snapshot show ' + name2, 'available', 6) + + del_output = self.openstack( + 'volume snapshot delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) + + def test_volume_snapshot_list(self): + """Test create, list filter""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name1 + + ' --volume ' + self.VOLLY + )) + self.addCleanup(self.openstack, 'volume snapshot delete ' + name1) + self.assertEqual( + name1, + cmd_output["name"], + ) + self.assertEqual( + self.VOLUME_ID, + cmd_output["volume_id"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.wait_for_status( + 'volume snapshot show ' + name1, 'available', 6) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + name2 + + ' --volume ' + self.VOLLY + )) + self.addCleanup(self.openstack, 'volume snapshot delete ' + name2) + self.assertEqual( + name2, + cmd_output["name"], + ) + self.assertEqual( + self.VOLUME_ID, + cmd_output["volume_id"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.wait_for_status( + 'volume snapshot show ' + name2, 'available', 6) + raw_output = self.openstack( + 'volume snapshot set ' + + '--state error ' + + name2 + ) + self.assertOutput('', raw_output) + + # Test list --long, --status + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--long ' + + '--status error' + )) + names = [x["Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertIn(name2, names) + + # Test list --volume + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--volume ' + self.VOLLY + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test list --name + cmd_output = json.loads(self.openstack( + 'volume snapshot list -f json ' + + '--name ' + name1 + )) + names = [x["Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertNotIn(name2, names) + + def test_volume_snapshot_set(self): + """Test create, set, unset, show, delete volume snapshot""" + name = uuid.uuid4().hex + new_name = name + "_" + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + '--volume ' + self.VOLLY + + ' --description aaaa ' + + '--property Alpha=a ' + + name + )) + self.addCleanup(self.openstack, 'volume snapshot delete ' + new_name) + self.assertEqual( + name, + cmd_output["name"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.assertEqual( + 'aaaa', + cmd_output["description"], + ) + self.assertEqual( + "Alpha='a'", + cmd_output["properties"], + ) + self.wait_for_status( + 'volume snapshot show ' + name, 'available', 6) + + # Test volume snapshot set raw_output = self.openstack( - 'snapshot set --property a=b --property c=d ' + self.NAME) - self.assertEqual("", raw_output) - opts = self.get_opts(["properties"]) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) - self.assertEqual("a='b', c='d'\n", raw_output) - - raw_output = self.openstack('snapshot unset --property a ' + self.NAME) - self.assertEqual("", raw_output) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) - self.assertEqual("c='d'\n", raw_output) - - def test_snapshot_set(self): + 'volume snapshot set ' + + '--name ' + new_name + + ' --description bbbb ' + + '--property Alpha=c ' + + '--property Beta=b ' + + name, + ) + self.assertOutput('', raw_output) + + # Show snapshot set result + cmd_output = json.loads(self.openstack( + 'volume snapshot show -f json ' + + new_name + )) + self.assertEqual( + new_name, + cmd_output["name"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.assertEqual( + 'bbbb', + cmd_output["description"], + ) + self.assertEqual( + "Alpha='c', Beta='b'", + cmd_output["properties"], + ) + + # Test volume snapshot unset + raw_output = self.openstack( + 'volume snapshot unset ' + + '--property Alpha ' + + new_name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume snapshot show -f json ' + + new_name + )) + self.assertEqual( + "Beta='b'", + cmd_output["properties"], + ) + + # Test volume snapshot set --no-property raw_output = self.openstack( - 'snapshot set --description backup ' + self.NAME) - self.assertEqual("", raw_output) - opts = self.get_opts(["description", "name"]) - raw_output = self.openstack('snapshot show ' + self.NAME + opts) - self.assertEqual("backup\n" + self.NAME + "\n", raw_output) + 'volume snapshot set ' + + '--no-property ' + + new_name, + ) + self.assertOutput('', raw_output) + cmd_output = json.loads(self.openstack( + 'volume snapshot show -f json ' + + new_name + )) + self.assertNotIn( + "Beta='b'", + cmd_output["properties"], + ) diff --git a/openstackclient/tests/functional/volume/v2/test_volume.py b/openstackclient/tests/functional/volume/v2/test_volume.py index fb880578..ce98236f 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume.py +++ b/openstackclient/tests/functional/volume/v2/test_volume.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import time import uuid @@ -19,116 +20,232 @@ from openstackclient.tests.functional.volume.v2 import common class VolumeTests(common.BaseVolumeTests): """Functional tests for volume. """ - NAME = uuid.uuid4().hex - SNAPSHOT_NAME = uuid.uuid4().hex - VOLUME_FROM_SNAPSHOT_NAME = uuid.uuid4().hex - OTHER_NAME = uuid.uuid4().hex - HEADERS = ['"Display Name"'] - FIELDS = ['name'] - - @classmethod - def setUpClass(cls): - super(VolumeTests, cls).setUpClass() - opts = cls.get_opts(cls.FIELDS) - - # Create test volume - raw_output = cls.openstack('volume create --size 1 ' + cls.NAME + opts) - expected = cls.NAME + '\n' - cls.assertOutput(expected, raw_output) - - @classmethod - def tearDownClass(cls): - # Rename test volume - raw_output = cls.openstack( - 'volume set --name ' + cls.OTHER_NAME + ' ' + cls.NAME) - cls.assertOutput('', raw_output) - - # Set volume state - cls.openstack('volume set --state error ' + cls.OTHER_NAME) - opts = cls.get_opts(["status"]) - raw_output_status = cls.openstack( - 'volume show ' + cls.OTHER_NAME + opts) - - # Delete test volume - raw_output = cls.openstack('volume delete ' + cls.OTHER_NAME) - cls.assertOutput('', raw_output) - cls.assertOutput('error\n', raw_output_status) + def test_volume_delete(self): + """Test create, delete multiple""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + name1 + )) + self.assertEqual( + 1, + cmd_output["size"], + ) + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 2 ' + + name2 + )) + self.assertEqual( + 2, + cmd_output["size"], + ) + + self.wait_for("volume", name1, "available") + self.wait_for("volume", name2, "available") + del_output = self.openstack('volume delete ' + name1 + ' ' + name2) + self.assertOutput('', del_output) def test_volume_list(self): - opts = self.get_opts(self.HEADERS) - raw_output = self.openstack('volume list' + opts) - self.assertIn(self.NAME, raw_output) + """Test create, list filter""" + name1 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + name1 + )) + self.addCleanup(self.openstack, 'volume delete ' + name1) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.wait_for("volume", name1, "available") + + name2 = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 2 ' + + name2 + )) + self.addCleanup(self.openstack, 'volume delete ' + name2) + self.assertEqual( + 2, + cmd_output["size"], + ) + self.wait_for("volume", name2, "available") + raw_output = self.openstack( + 'volume set ' + + '--state error ' + + name2 + ) + self.assertOutput('', raw_output) + + # Test list --long + cmd_output = json.loads(self.openstack( + 'volume list -f json ' + + '--long' + )) + names = [x["Display Name"] for x in cmd_output] + self.assertIn(name1, names) + self.assertIn(name2, names) + + # Test list --status + cmd_output = json.loads(self.openstack( + 'volume list -f json ' + + '--status error' + )) + names = [x["Display Name"] for x in cmd_output] + self.assertNotIn(name1, names) + self.assertIn(name2, names) + + # TODO(qiangjiahui): Add project option to filter tests when we can + # specify volume with project + + def test_volume_set_and_unset(self): + """Tests create volume, set, unset, show, delete""" + name = uuid.uuid4().hex + new_name = name + "_" + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + '--description aaaa ' + + '--property Alpha=a ' + + name + )) + self.addCleanup(self.openstack, 'volume delete ' + new_name) + self.assertEqual( + name, + cmd_output["name"], + ) + self.assertEqual( + 1, + cmd_output["size"], + ) + self.assertEqual( + 'aaaa', + cmd_output["description"], + ) + self.assertEqual( + "Alpha='a'", + cmd_output["properties"], + ) + self.assertEqual( + 'false', + cmd_output["bootable"], + ) + self.wait_for("volume", name, "available") - def test_volume_show(self): - opts = self.get_opts(self.FIELDS) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n", raw_output) + # Test volume set + raw_output = self.openstack( + 'volume set ' + + '--name ' + new_name + + ' --size 2 ' + + '--description bbbb ' + + '--no-property ' + + '--property Beta=b ' + + '--property Gamma=c ' + + '--image-property a=b ' + + '--image-property c=d ' + + '--bootable ' + + name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + new_name + )) + self.assertEqual( + new_name, + cmd_output["name"], + ) + self.assertEqual( + 2, + cmd_output["size"], + ) + self.assertEqual( + 'bbbb', + cmd_output["description"], + ) + self.assertEqual( + "Beta='b', Gamma='c'", + cmd_output["properties"], + ) + self.assertEqual( + {'a': 'b', 'c': 'd'}, + cmd_output["volume_image_metadata"], + ) + self.assertEqual( + 'true', + cmd_output["bootable"], + ) - def test_volume_properties(self): + # Test volume unset raw_output = self.openstack( - 'volume set --property a=b --property c=d ' + self.NAME) - self.assertEqual("", raw_output) - opts = self.get_opts(["properties"]) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual("a='b', c='d'\n", raw_output) - - raw_output = self.openstack('volume unset --property a ' + self.NAME) - self.assertEqual("", raw_output) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual("c='d'\n", raw_output) - - def test_volume_set(self): - discription = uuid.uuid4().hex - self.openstack('volume set --description ' + discription + ' ' + - self.NAME) - opts = self.get_opts(["description", "name"]) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual(discription + "\n" + self.NAME + "\n", raw_output) - - def test_volume_set_size(self): - self.openstack('volume set --size 2 ' + self.NAME) - opts = self.get_opts(["name", "size"]) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual(self.NAME + "\n2\n", raw_output) - - def test_volume_set_bootable(self): - self.openstack('volume set --bootable ' + self.NAME) - opts = self.get_opts(["bootable"]) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual("true\n", raw_output) - - self.openstack('volume set --non-bootable ' + self.NAME) - opts = self.get_opts(["bootable"]) - raw_output = self.openstack('volume show ' + self.NAME + opts) - self.assertEqual("false\n", raw_output) + 'volume unset ' + + '--property Beta ' + + '--image-property a ' + + new_name, + ) + self.assertOutput('', raw_output) + + cmd_output = json.loads(self.openstack( + 'volume show -f json ' + + new_name + )) + self.assertEqual( + "Gamma='c'", + cmd_output["properties"], + ) + self.assertEqual( + {'c': 'd'}, + cmd_output["volume_image_metadata"], + ) def test_volume_snapshot(self): - opts = self.get_opts(self.FIELDS) - - # Create snapshot from test volume - raw_output = self.openstack('snapshot create ' + self.NAME + - ' --name ' + self.SNAPSHOT_NAME + opts) - expected = self.SNAPSHOT_NAME + '\n' - self.assertOutput(expected, raw_output) - self.wait_for("snapshot", self.SNAPSHOT_NAME, "available") - - # Create volume from snapshot - raw_output = self.openstack('volume create --size 2 --snapshot ' + - self.SNAPSHOT_NAME + ' ' + - self.VOLUME_FROM_SNAPSHOT_NAME + opts) - expected = self.VOLUME_FROM_SNAPSHOT_NAME + '\n' - self.assertOutput(expected, raw_output) - self.wait_for("volume", self.VOLUME_FROM_SNAPSHOT_NAME, "available") - - # Delete volume that create from snapshot - raw_output = self.openstack('volume delete ' + - self.VOLUME_FROM_SNAPSHOT_NAME) - self.assertOutput('', raw_output) + """Tests volume create from snapshot""" + + volume_name = uuid.uuid4().hex + snapshot_name = uuid.uuid4().hex + # Make a snapshot + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--size 1 ' + + volume_name + )) + self.wait_for("volume", volume_name, "available") + self.assertEqual( + volume_name, + cmd_output["name"], + ) + cmd_output = json.loads(self.openstack( + 'volume snapshot create -f json ' + + snapshot_name + + ' --volume ' + volume_name + )) + self.wait_for("volume snapshot", snapshot_name, "available") - # Delete test snapshot - raw_output = self.openstack('snapshot delete ' + self.SNAPSHOT_NAME) + name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'volume create -f json ' + + '--snapshot ' + snapshot_name + + ' ' + name + )) + self.addCleanup(self.openstack, 'volume delete ' + name) + self.addCleanup(self.openstack, 'volume delete ' + volume_name) + self.assertEqual( + name, + cmd_output["name"], + ) + self.wait_for("volume", name, "available") + + # Delete snapshot + raw_output = self.openstack( + 'volume snapshot delete ' + snapshot_name) self.assertOutput('', raw_output) - self.wait_for("volume", self.NAME, "available") def wait_for(self, check_type, check_name, desired_status, wait=120, interval=5, failures=['ERROR']): diff --git a/openstackclient/tests/functional/volume/v2/test_volume_type.py b/openstackclient/tests/functional/volume/v2/test_volume_type.py index d8bd3a96..a5d0a767 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_type.py @@ -42,6 +42,11 @@ class VolumeTypeTests(common.BaseVolumeTests): raw_output = self.openstack('volume type list' + opts) self.assertIn(self.NAME, raw_output) + def test_volume_type_list_default(self): + opts = self.get_opts(self.HEADERS) + raw_output = self.openstack('volume type list --default' + opts) + self.assertEqual("lvmdriver-1\n", raw_output) + def test_volume_type_show(self): opts = self.get_opts(self.FIELDS) raw_output = self.openstack('volume type show ' + self.NAME + opts) @@ -97,3 +102,90 @@ class VolumeTypeTests(common.BaseVolumeTests): time.sleep(5) raw_output = self.openstack(cmd) self.assertOutput('', raw_output) + + # NOTE: Add some basic funtional tests with the old format to + # make sure the command works properly, need to change + # these to new test format when beef up all tests for + # volume tye commands. + def test_encryption_type(self): + encryption_type = uuid.uuid4().hex + # test create new encryption type + opts = self.get_opts(['encryption']) + raw_output = self.openstack( + 'volume type create ' + '--encryption-provider LuksEncryptor ' + '--encryption-cipher aes-xts-plain64 ' + '--encryption-key-size 128 ' + '--encryption-control-location front-end ' + + encryption_type + opts) + expected = ["provider='LuksEncryptor'", + "cipher='aes-xts-plain64'", + "key_size='128'", + "control_location='front-end'"] + for attr in expected: + self.assertIn(attr, raw_output) + # test show encryption type + opts = self.get_opts(['encryption']) + raw_output = self.openstack( + 'volume type show --encryption-type ' + encryption_type + opts) + expected = ["provider='LuksEncryptor'", + "cipher='aes-xts-plain64'", + "key_size='128'", + "control_location='front-end'"] + for attr in expected: + self.assertIn(attr, raw_output) + # test list encryption type + opts = self.get_opts(['Encryption']) + raw_output = self.openstack( + 'volume type list --encryption-type ' + opts) + expected = ["provider='LuksEncryptor'", + "cipher='aes-xts-plain64'", + "key_size='128'", + "control_location='front-end'"] + for attr in expected: + self.assertIn(attr, raw_output) + # test set existing encryption type + raw_output = self.openstack( + 'volume type set ' + '--encryption-key-size 256 ' + '--encryption-control-location back-end ' + + encryption_type) + self.assertEqual('', raw_output) + opts = self.get_opts(['encryption']) + raw_output = self.openstack( + 'volume type show --encryption-type ' + encryption_type + opts) + expected = ["provider='LuksEncryptor'", + "cipher='aes-xts-plain64'", + "key_size='256'", + "control_location='back-end'"] + for attr in expected: + self.assertIn(attr, raw_output) + # test set new encryption type + raw_output = self.openstack( + 'volume type set ' + '--encryption-provider LuksEncryptor ' + '--encryption-cipher aes-xts-plain64 ' + '--encryption-key-size 128 ' + '--encryption-control-location front-end ' + + self.NAME) + self.assertEqual('', raw_output) + opts = self.get_opts(['encryption']) + raw_output = self.openstack( + 'volume type show --encryption-type ' + self.NAME + opts) + expected = ["provider='LuksEncryptor'", + "cipher='aes-xts-plain64'", + "key_size='128'", + "control_location='front-end'"] + for attr in expected: + self.assertIn(attr, raw_output) + # test unset encryption type + raw_output = self.openstack( + 'volume type unset --encryption-type ' + self.NAME) + self.assertEqual('', raw_output) + opts = self.get_opts(['encryption']) + raw_output = self.openstack( + 'volume type show --encryption-type ' + self.NAME + opts) + self.assertEqual('\n', raw_output) + # test delete encryption type + raw_output = self.openstack('volume type delete ' + encryption_type) + self.assertEqual('', raw_output) diff --git a/openstackclient/tests/functional/volume/v3/__init__.py b/openstackclient/tests/functional/volume/v3/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/openstackclient/tests/functional/volume/v3/__init__.py diff --git a/openstackclient/tests/functional/volume/v3/common.py b/openstackclient/tests/functional/volume/v3/common.py new file mode 100644 index 00000000..57a62df6 --- /dev/null +++ b/openstackclient/tests/functional/volume/v3/common.py @@ -0,0 +1,23 @@ +# 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 os + +from openstackclient.tests.functional import base + + +class BaseVolumeTests(base.TestCase): + """Base class for Volume functional tests. """ + + @classmethod + def setUpClass(cls): + os.environ['OS_VOLUME_API_VERSION'] = '3' diff --git a/openstackclient/tests/functional/volume/v3/test_qos.py b/openstackclient/tests/functional/volume/v3/test_qos.py new file mode 100644 index 00000000..46965ced --- /dev/null +++ b/openstackclient/tests/functional/volume/v3/test_qos.py @@ -0,0 +1,23 @@ +# 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.tests.functional.volume.v2 import test_qos as v2 +import os + + +class QosTests(v2.QosTests): + """Functional tests for volume qos. """ + + @classmethod + def setUpClass(cls): + super(QosTests, cls).setUpClass() + os.environ['OS_VOLUME_API_VERSION'] = '3' diff --git a/openstackclient/tests/functional/volume/v3/test_snapshot.py b/openstackclient/tests/functional/volume/v3/test_snapshot.py new file mode 100644 index 00000000..bf05b9de --- /dev/null +++ b/openstackclient/tests/functional/volume/v3/test_snapshot.py @@ -0,0 +1,23 @@ +# 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.tests.functional.volume.v2 import test_snapshot as v2 +import os + + +class VolumeSnapshotTests(v2.VolumeSnapshotTests): + """Functional tests for volume snapshot. """ + + @classmethod + def setUpClass(cls): + super(VolumeSnapshotTests, cls).setUpClass() + os.environ['OS_VOLUME_API_VERSION'] = '3' diff --git a/openstackclient/tests/functional/volume/v3/test_transfer_request.py b/openstackclient/tests/functional/volume/v3/test_transfer_request.py new file mode 100644 index 00000000..7b54dd20 --- /dev/null +++ b/openstackclient/tests/functional/volume/v3/test_transfer_request.py @@ -0,0 +1,24 @@ +# 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.tests.functional.volume.v2 import test_transfer_request \ + as v2 +import os + + +class TransferRequestTests(v2.TransferRequestTests): + """Functional tests for transfer request. """ + + @classmethod + def setUpClass(cls): + super(TransferRequestTests, cls).setUpClass() + os.environ['OS_VOLUME_API_VERSION'] = '3' diff --git a/openstackclient/tests/functional/volume/v3/test_volume.py b/openstackclient/tests/functional/volume/v3/test_volume.py new file mode 100644 index 00000000..333826d8 --- /dev/null +++ b/openstackclient/tests/functional/volume/v3/test_volume.py @@ -0,0 +1,23 @@ +# 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.tests.functional.volume.v2 import test_volume as v2 +import os + + +class VolumeTests(v2.VolumeTests): + """Functional tests for volume. """ + + @classmethod + def setUpClass(cls): + super(VolumeTests, cls).setUpClass() + os.environ['OS_VOLUME_API_VERSION'] = '3' diff --git a/openstackclient/tests/functional/volume/v3/test_volume_type.py b/openstackclient/tests/functional/volume/v3/test_volume_type.py new file mode 100644 index 00000000..f10e64b4 --- /dev/null +++ b/openstackclient/tests/functional/volume/v3/test_volume_type.py @@ -0,0 +1,23 @@ +# 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.tests.functional.volume.v2 import test_volume_type as v2 +import os + + +class VolumeTypeTests(v2.VolumeTypeTests): + """Functional tests for volume type. """ + + @classmethod + def setUpClass(cls): + super(VolumeTypeTests, cls).setUpClass() + os.environ['OS_VOLUME_API_VERSION'] = '3' diff --git a/openstackclient/tests/unit/api/test_image_v1.py b/openstackclient/tests/unit/api/test_image_v1.py index e02ef381..6ce3ddea 100644 --- a/openstackclient/tests/unit/api/test_image_v1.py +++ b/openstackclient/tests/unit/api/test_image_v1.py @@ -21,7 +21,7 @@ from openstackclient.tests.unit import utils FAKE_PROJECT = 'xyzpdq' -FAKE_URL = 'http://gopher.com' +FAKE_URL = 'http://gopher.dev10.com' class TestImageAPIv1(utils.TestCase): diff --git a/openstackclient/tests/unit/api/test_image_v2.py b/openstackclient/tests/unit/api/test_image_v2.py index 5dbb51e0..22490e46 100644 --- a/openstackclient/tests/unit/api/test_image_v2.py +++ b/openstackclient/tests/unit/api/test_image_v2.py @@ -21,7 +21,7 @@ from openstackclient.tests.unit import utils FAKE_PROJECT = 'xyzpdq' -FAKE_URL = 'http://gopher.com' +FAKE_URL = 'http://gopher.dev20.com' class TestImageAPIv2(utils.TestCase): diff --git a/openstackclient/tests/unit/common/test_module.py b/openstackclient/tests/unit/common/test_module.py index eb54dbe0..2491d639 100644 --- a/openstackclient/tests/unit/common/test_module.py +++ b/openstackclient/tests/unit/common/test_module.py @@ -26,19 +26,28 @@ from openstackclient.tests.unit import utils # currently == '*client*' module_name_1 = 'fakeclient' module_version_1 = '0.1.2' -MODULE_1 = { - '__version__': module_version_1, -} module_name_2 = 'zlib' module_version_2 = '1.1' -MODULE_2 = { - '__version__': module_version_2, -} + +# module_3 match openstacksdk +module_name_3 = 'openstack' +module_version_3 = '0.9.13' + +# module_4 match sub module of fakeclient +module_name_4 = 'fakeclient.submodule' +module_version_4 = '0.2.2' + +# module_5 match private module +module_name_5 = '_private_module.lib' +module_version_5 = '0.0.1' MODULES = { module_name_1: fakes.FakeModule(module_name_1, module_version_1), module_name_2: fakes.FakeModule(module_name_2, module_version_2), + module_name_3: fakes.FakeModule(module_name_3, module_version_3), + module_name_4: fakes.FakeModule(module_name_4, module_version_4), + module_name_5: fakes.FakeModule(module_name_5, module_version_5), } @@ -79,6 +88,41 @@ class TestCommandList(utils.TestCommand): self.assertEqual(datalist, tuple(data)) + def test_command_list_with_group_not_found(self): + arglist = [ + '--group', 'not_exist', + ] + verifylist = [ + ('group', 'not_exist'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Command Group', 'Commands') + self.assertEqual(collist, columns) + self.assertEqual([], data) + + def test_command_list_with_group(self): + arglist = [ + '--group', 'common', + ] + verifylist = [ + ('group', 'common'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Command Group', 'Commands') + self.assertEqual(collist, columns) + datalist = (( + 'openstack.common', + 'limits show\nextension list' + ),) + + self.assertEqual(datalist, tuple(data)) + @mock.patch.dict( 'openstackclient.common.module.sys.modules', @@ -105,9 +149,18 @@ class TestModuleList(utils.TestCommand): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Additional modules may be present, just check our additions + # Output xxxclient and openstacksdk, but not regular module, like: zlib self.assertIn(module_name_1, columns) self.assertIn(module_version_1, data) + self.assertNotIn(module_name_2, columns) + self.assertNotIn(module_version_2, data) + self.assertIn(module_name_3, columns) + self.assertIn(module_version_3, data) + # Filter sub and private modules + self.assertNotIn(module_name_4, columns) + self.assertNotIn(module_version_4, data) + self.assertNotIn(module_name_5, columns) + self.assertNotIn(module_version_5, data) def test_module_list_all(self): arglist = [ @@ -123,8 +176,15 @@ class TestModuleList(utils.TestCommand): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Additional modules may be present, just check our additions + # Output xxxclient, openstacksdk and regular module, like: zlib self.assertIn(module_name_1, columns) - self.assertIn(module_name_2, columns) self.assertIn(module_version_1, data) + self.assertIn(module_name_2, columns) self.assertIn(module_version_2, data) + self.assertIn(module_name_3, columns) + self.assertIn(module_version_3, data) + # Filter sub and private modules + self.assertNotIn(module_name_4, columns) + self.assertNotIn(module_version_4, data) + self.assertNotIn(module_name_5, columns) + self.assertNotIn(module_version_5, data) diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 7dd23373..63f6435f 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -17,6 +17,7 @@ from openstackclient.common import quota from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes @@ -518,3 +519,161 @@ class TestQuotaShow(TestQuota): self.network.get_quota.assert_called_once_with( identity_fakes.project_id) self.assertNotCalled(self.network.get_quota_default) + + +class TestQuotaList(TestQuota): + """Test cases for quota list command""" + + project = identity_fakes_v3.FakeProject.create_one_project() + + quota_list = network_fakes.FakeQuota.create_one_net_quota() + quota_list1 = compute_fakes.FakeQuota.create_one_comp_quota() + quota_list2 = volume_fakes.FakeQuota.create_one_vol_quota() + + default_quota = network_fakes.FakeQuota.create_one_default_net_quota() + default_quota1 = compute_fakes.FakeQuota.create_one_default_comp_quota() + default_quota2 = volume_fakes.FakeQuota.create_one_default_vol_quota() + + reference_data = (project.id, + quota_list.floating_ips, + quota_list.networks, + quota_list.ports, + quota_list.rbac_policies, + quota_list.routers, + quota_list.security_groups, + quota_list.security_group_rules, + quota_list.subnets, + quota_list.subnet_pools) + + comp_reference_data = (project.id, + quota_list1.cores, + quota_list1.fixed_ips, + quota_list1.injected_files, + quota_list1.injected_file_content_bytes, + quota_list1.injected_file_path_bytes, + quota_list1.instances, + quota_list1.key_pairs, + quota_list1.metadata_items, + quota_list1.ram, + quota_list1.server_groups, + quota_list1.server_group_members) + + vol_reference_data = (project.id, + quota_list2.backups, + quota_list2.backup_gigabytes, + quota_list2.gigabytes, + quota_list2.per_volume_gigabytes, + quota_list2.snapshots, + quota_list2.volumes) + + net_column_header = ( + 'Project ID', + 'Floating IPs', + 'Networks', + 'Ports', + 'RBAC Policies', + 'Routers', + 'Security Groups', + 'Security Group Rules', + 'Subnets', + 'Subnet Pools' + ) + + comp_column_header = ( + 'Project ID', + 'Cores', + 'Fixed IPs', + 'Injected Files', + 'Injected File Content Bytes', + 'Injected File Path Bytes', + 'Instances', + 'Key Pairs', + 'Metadata Items', + 'Ram', + 'Server Groups', + 'Server Group Members', + ) + + vol_column_header = ( + 'Project ID', + 'Backups', + 'Backup Gigabytes', + 'Gigabytes', + 'Per Volume Gigabytes', + 'Snapshots', + 'Volumes', + ) + + def setUp(self): + super(TestQuotaList, self).setUp() + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + self.identity = self.app.client_manager.identity + self.identity.tenants.list = mock.Mock(return_value=[self.project]) + + self.network = self.app.client_manager.network + self.compute = self.app.client_manager.compute + self.volume = self.app.client_manager.volume + + self.network.get_quota = mock.Mock(return_value=self.quota_list) + self.compute.quotas.get = mock.Mock(return_value=self.quota_list1) + self.volume.quotas.get = mock.Mock(return_value=self.quota_list2) + + self.network.get_quota_default = mock.Mock( + return_value=self.default_quota) + self.compute.quotas.defaults = mock.Mock( + return_value=self.default_quota1) + self.volume.quotas.defaults = mock.Mock( + return_value=self.default_quota2) + + self.cmd = quota.ListQuota(self.app, None) + + def test_quota_list_network(self): + arglist = [ + '--network' + ] + verifylist = [ + ('network', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.net_column_header, columns) + + self.assertEqual(self.reference_data, list(data)[0]) + + def test_quota_list_compute(self): + arglist = [ + '--compute' + ] + verifylist = [ + ('compute', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.comp_column_header, columns) + + self.assertEqual(self.comp_reference_data, list(data)[0]) + + def test_quota_list_volume(self): + arglist = [ + '--volume' + ] + verifylist = [ + ('volume', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.vol_column_header, columns) + + self.assertEqual(self.vol_reference_data, list(data)[0]) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 985ce5e2..4a194859 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -168,6 +168,9 @@ class FakeComputev2Client(object): self.quota_classes = mock.Mock() self.quota_classes.resource_class = fakes.FakeResource(None, {}) + self.usage = mock.Mock() + self.usage.resource_class = fakes.FakeResource(None, {}) + self.volumes = mock.Mock() self.volumes.resource_class = fakes.FakeResource(None, {}) @@ -201,6 +204,9 @@ class FakeComputev2Client(object): self.server_groups = mock.Mock() self.server_groups.resource_class = fakes.FakeResource(None, {}) + self.instance_action = mock.Mock() + self.instance_action.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -653,6 +659,47 @@ class FakeServer(object): return mock.Mock(side_effect=servers) +class FakeServerEvent(object): + """Fake one or more server event.""" + + @staticmethod + def create_one_server_event(attrs=None): + """Create a fake server event. + + :param attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id and other attributes + """ + attrs = attrs or {} + + # Set default attributes + server_event_info = { + "instance_uuid": "server-event-" + uuid.uuid4().hex, + "user_id": "user-id-" + uuid.uuid4().hex, + "start_time": "2017-02-27T07:47:13.000000", + "request_id": "req-" + uuid.uuid4().hex, + "action": "create", + "message": None, + "project_id": "project-id-" + uuid.uuid4().hex, + "events": [{ + "finish_time": "2017-02-27T07:47:25.000000", + "start_time": "2017-02-27T07:47:15.000000", + "traceback": None, + "event": "compute__do_build_and_run_instance", + "result": "Success" + }] + } + # Overwrite default attributes + server_event_info.update(attrs) + + server_event = fakes.FakeResource( + info=copy.deepcopy(server_event_info), + loaded=True, + ) + return server_event + + class FakeService(object): """Fake one or more services.""" @@ -1248,3 +1295,129 @@ class FakeServerGroup(object): info=copy.deepcopy(server_group_info), loaded=True) return server_group + + +class FakeUsage(object): + """Fake one or more usage.""" + + @staticmethod + def create_one_usage(attrs=None): + """Create a fake usage. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with tenant_id and other attributes + """ + if attrs is None: + attrs = {} + + # Set default attributes. + usage_info = { + 'tenant_id': 'usage-tenant-id-' + uuid.uuid4().hex, + 'total_memory_mb_usage': 512.0, + 'total_vcpus_usage': 1.0, + 'total_local_gb_usage': 1.0, + 'server_usages': [ + { + 'ended_at': None, + 'flavor': 'usage-flavor-' + uuid.uuid4().hex, + 'hours': 1.0, + 'local_gb': 1, + 'memory_mb': 512, + 'name': 'usage-name-' + uuid.uuid4().hex, + 'state': 'active', + 'uptime': 3600, + 'vcpus': 1 + } + ] + } + + # Overwrite default attributes. + usage_info.update(attrs) + + usage = fakes.FakeResource(info=copy.deepcopy(usage_info), + loaded=True) + + return usage + + @staticmethod + def create_usages(attrs=None, count=2): + """Create multiple fake services. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of services to fake + :return: + A list of FakeResource objects faking the services + """ + usages = [] + for i in range(0, count): + usages.append(FakeUsage.create_one_usage(attrs)) + + return usages + + +class FakeQuota(object): + """Fake quota""" + + @staticmethod + def create_one_comp_quota(attrs=None): + """Create one quota""" + + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'cores': 20, + 'fixed_ips': 30, + 'injected_files': 100, + 'injected_file_content_bytes': 10240, + 'injected_file_path_bytes': 255, + 'instances': 50, + 'key_pairs': 20, + 'metadata_items': 10, + 'ram': 51200, + 'server_groups': 10, + 'server_group_members': 10 + } + + quota_attrs.update(attrs) + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + + quota.project_id = quota_attrs['id'] + + return quota + + @staticmethod + def create_one_default_comp_quota(attrs=None): + """Crate one quota""" + + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'cores': 10, + 'fixed_ips': 10, + 'injected_files': 100, + 'injected_file_content_bytes': 10240, + 'injected_file_path_bytes': 255, + 'instances': 20, + 'key_pairs': 20, + 'metadata_items': 10, + 'ram': 51200, + 'server_groups': 10, + 'server_group_members': 10 + } + + quota_attrs.update(attrs) + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + + quota.project_id = quota_attrs['id'] + + return quota diff --git a/openstackclient/tests/unit/compute/v2/test_console.py b/openstackclient/tests/unit/compute/v2/test_console.py index d53d241e..3c708aae 100644 --- a/openstackclient/tests/unit/compute/v2/test_console.py +++ b/openstackclient/tests/unit/compute/v2/test_console.py @@ -35,11 +35,7 @@ class TestConsoleUrlShow(TestConsole): 'protocol': 'fake_protocol', 'type': 'fake_type'}} methods = { - 'get_vnc_console': fake_console_data, - 'get_spice_console': fake_console_data, - 'get_serial_console': fake_console_data, - 'get_rdp_console': fake_console_data, - 'get_mks_console': fake_console_data, + 'get_console_url': fake_console_data } self.fake_server = compute_fakes.FakeServer.create_one_server( methods=methods) @@ -68,7 +64,7 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_vnc_console.assert_called_once_with('novnc') + self.fake_server.get_console_url.assert_called_once_with('novnc') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -83,7 +79,7 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_vnc_console.assert_called_once_with('novnc') + self.fake_server.get_console_url.assert_called_once_with('novnc') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -98,7 +94,7 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_vnc_console.assert_called_once_with('xvpvnc') + self.fake_server.get_console_url.assert_called_once_with('xvpvnc') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -113,14 +109,14 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_spice_console.assert_called_once_with( + self.fake_server.get_console_url.assert_called_once_with( 'spice-html5') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) def test_console_url_show_compatible(self): methods = { - 'get_vnc_console': {'console': {'url': 'http://localhost', + 'get_console_url': {'console': {'url': 'http://localhost', 'type': 'fake_type'}}, } old_fake_server = compute_fakes.FakeServer.create_one_server( @@ -130,8 +126,8 @@ class TestConsoleUrlShow(TestConsole): 'url', ) old_data = ( - methods['get_vnc_console']['console']['type'], - methods['get_vnc_console']['console']['url'] + methods['get_console_url']['console']['type'], + methods['get_console_url']['console']['url'] ) arglist = [ 'foo_vm', @@ -144,7 +140,7 @@ class TestConsoleUrlShow(TestConsole): with mock.patch.object(self.servers_mock, 'get', return_value=old_fake_server): columns, data = self.cmd.take_action(parsed_args) - old_fake_server.get_vnc_console.assert_called_once_with('novnc') + old_fake_server.get_console_url.assert_called_once_with('novnc') self.assertEqual(old_columns, columns) self.assertEqual(old_data, data) @@ -159,7 +155,7 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_rdp_console.assert_called_once_with( + self.fake_server.get_console_url.assert_called_once_with( 'rdp-html5') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -175,7 +171,7 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_serial_console.assert_called_once_with( + self.fake_server.get_console_url.assert_called_once_with( 'serial') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -191,6 +187,6 @@ class TestConsoleUrlShow(TestConsole): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.fake_server.get_mks_console.assert_called_once_with() + self.fake_server.get_console_url.assert_called_once_with('webmks') self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index 93ad9d14..4cdbb25b 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -160,7 +160,7 @@ class TestFlavorCreate(TestFlavor): self.flavor.is_public = False arglist = [ - '--id', self.flavor.id, + '--id', 'auto', '--ram', str(self.flavor.ram), '--disk', str(self.flavor.disk), '--ephemeral', str(self.flavor.ephemeral), @@ -174,7 +174,6 @@ class TestFlavorCreate(TestFlavor): self.flavor.name, ] verifylist = [ - ('id', self.flavor.id), ('ram', self.flavor.ram), ('disk', self.flavor.disk), ('ephemeral', self.flavor.ephemeral), @@ -193,7 +192,7 @@ class TestFlavorCreate(TestFlavor): self.flavor.ram, self.flavor.vcpus, self.flavor.disk, - self.flavor.id, + 'auto', self.flavor.ephemeral, self.flavor.swap, self.flavor.rxtx_factor, @@ -529,6 +528,23 @@ class TestFlavorSet(TestFlavor): self.flavor.set_keys.assert_called_with({'FOO': '"B A R"'}) self.assertIsNone(result) + def test_flavor_set_no_property(self): + arglist = [ + '--no-property', + 'baremetal' + ] + verifylist = [ + ('no_property', True), + ('flavor', 'baremetal') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.flavors_mock.find.assert_called_with(name=parsed_args.flavor, + is_public=None) + self.flavor.unset_keys.assert_called_with(['property']) + self.assertIsNone(result) + def test_flavor_set_project(self): arglist = [ '--project', self.project.id, diff --git a/openstackclient/tests/unit/compute/v2/test_hypervisor.py b/openstackclient/tests/unit/compute/v2/test_hypervisor.py index e39570af..7200d04e 100644 --- a/openstackclient/tests/unit/compute/v2/test_hypervisor.py +++ b/openstackclient/tests/unit/compute/v2/test_hypervisor.py @@ -48,19 +48,63 @@ class TestHypervisorList(TestHypervisor): self.columns = ( "ID", - "Hypervisor Hostname" + "Hypervisor Hostname", + "Hypervisor Type", + "Host IP", + "State" + ) + self.columns_long = ( + "ID", + "Hypervisor Hostname", + "Hypervisor Type", + "Host IP", + "State", + "vCPUs Used", + "vCPUs", + "Memory MB Used", + "Memory MB" ) self.data = ( ( self.hypervisors[0].id, self.hypervisors[0].hypervisor_hostname, + self.hypervisors[0].hypervisor_type, + self.hypervisors[0].host_ip, + self.hypervisors[0].state ), ( self.hypervisors[1].id, self.hypervisors[1].hypervisor_hostname, + self.hypervisors[1].hypervisor_type, + self.hypervisors[1].host_ip, + self.hypervisors[1].state ), ) + self.data_long = ( + ( + self.hypervisors[0].id, + self.hypervisors[0].hypervisor_hostname, + self.hypervisors[0].hypervisor_type, + self.hypervisors[0].host_ip, + self.hypervisors[0].state, + self.hypervisors[0].vcpus_used, + self.hypervisors[0].vcpus, + self.hypervisors[0].memory_mb_used, + self.hypervisors[0].memory_mb + ), + ( + self.hypervisors[1].id, + self.hypervisors[1].hypervisor_hostname, + self.hypervisors[1].hypervisor_type, + self.hypervisors[1].host_ip, + self.hypervisors[1].state, + self.hypervisors[1].vcpus_used, + self.hypervisors[1].vcpus, + self.hypervisors[1].memory_mb_used, + self.hypervisors[1].memory_mb + ), + ) # Get the command object to test self.cmd = hypervisor.ListHypervisor(self.app, None) @@ -93,6 +137,9 @@ class TestHypervisorList(TestHypervisor): ( self.hypervisors[0].id, self.hypervisors[0].hypervisor_hostname, + self.hypervisors[1].hypervisor_type, + self.hypervisors[1].host_ip, + self.hypervisors[1].state, ), ) @@ -123,6 +170,24 @@ class TestHypervisorList(TestHypervisor): self.cmd.take_action, parsed_args) + def test_hypervisor_list_long_option(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + self.hypervisors_mock.list.assert_called_with() + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, tuple(data)) + class TestHypervisorShow(TestHypervisor): diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py index cb008545..d6f5ecf4 100644 --- a/openstackclient/tests/unit/compute/v2/test_keypair.py +++ b/openstackclient/tests/unit/compute/v2/test_keypair.py @@ -15,6 +15,7 @@ import mock from mock import call +import uuid from osc_lib import exceptions from osc_lib import utils @@ -115,6 +116,36 @@ class TestKeypairCreate(TestKeypair): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_keypair_create_private_key(self): + tmp_pk_file = '/tmp/kp-file-' + uuid.uuid4().hex + arglist = [ + '--private-key', tmp_pk_file, + self.keypair.name, + ] + verifylist = [ + ('private_key', tmp_pk_file), + ('name', self.keypair.name) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('io.open') as mock_open: + mock_open.return_value = mock.MagicMock() + m_file = mock_open.return_value.__enter__.return_value + + columns, data = self.cmd.take_action(parsed_args) + + self.keypairs_mock.create.assert_called_with( + self.keypair.name, + public_key=None + ) + + mock_open.assert_called_once_with(tmp_pk_file, 'w+') + m_file.write.assert_called_once_with(self.keypair.private_key) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + class TestKeypairDelete(TestKeypair): @@ -179,8 +210,7 @@ class TestKeypairDelete(TestKeypair): self.cmd.take_action(parsed_args) self.fail('CommandError should be raised.') except exceptions.CommandError as e: - self.assertEqual('1 of 2 public keys failed to delete.', - str(e)) + self.assertEqual('1 of 2 keys failed to delete.', str(e)) find_mock.assert_any_call( self.keypairs_mock, self.keypairs[0].name) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 1081b9a3..7691ef59 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -19,6 +19,7 @@ from mock import call from osc_lib import exceptions from osc_lib import utils as common_utils +from oslo_utils import timeutils from openstackclient.compute.v2 import server from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -36,10 +37,6 @@ class TestServer(compute_fakes.TestComputev2): self.servers_mock = self.app.client_manager.compute.servers self.servers_mock.reset_mock() - # Get a shortcut to the compute client ImageManager Mock - self.cimages_mock = self.app.client_manager.compute.images - self.cimages_mock.reset_mock() - # Get a shortcut to the compute client FlavorManager Mock self.flavors_mock = self.app.client_manager.compute.flavors self.flavors_mock.reset_mock() @@ -149,24 +146,33 @@ class TestServerAddFloatingIP(TestServer): 'add_floating_ip': None, } - def test_server_add_floating_ip(self): + def _test_server_add_floating_ip(self, extralist, fixed_ip_address): servers = self.setup_servers_mock(count=1) arglist = [ servers[0].id, '1.2.3.4', - ] + ] + extralist verifylist = [ ('server', servers[0].id), ('ip_address', '1.2.3.4'), + ('fixed_ip_address', fixed_ip_address), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - servers[0].add_floating_ip.assert_called_once_with('1.2.3.4') + servers[0].add_floating_ip.assert_called_once_with('1.2.3.4', + fixed_ip_address) self.assertIsNone(result) + def test_server_add_floating_ip(self): + self._test_server_add_floating_ip([], None) + + def test_server_add_floating_ip_to_fixed_ip(self): + extralist = ['--fixed-ip-address', '5.6.7.8'] + self._test_server_add_floating_ip(extralist, '5.6.7.8') + class TestServerAddSecurityGroup(TestServer): @@ -259,7 +265,7 @@ class TestServerCreate(TestServer): self.servers_mock.create.return_value = self.new_server self.image = image_fakes.FakeImage.create_one_image() - self.cimages_mock.get.return_value = self.image + self.images_mock.get.return_value = self.image self.flavor = compute_fakes.FakeFlavor.create_one_flavor() self.flavors_mock.get.return_value = self.flavor @@ -312,7 +318,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, - block_device_mapping={}, + block_device_mapping_v2=[], nics=[], scheduler_hints={}, config_drive=None, @@ -328,6 +334,61 @@ class TestServerCreate(TestServer): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + def test_server_create_with_options(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--key-name', 'keyname', + '--property', 'Beta=b', + '--security-group', 'securitygroup', + '--hint', 'a=b', + '--hint', 'a=c', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('key_name', 'keyname'), + ('property', {'Beta': 'b'}), + ('security_group', ['securitygroup']), + ('hint', ['a=b', 'a=c']), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + meta={'Beta': 'b'}, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=['securitygroup'], + userdata=None, + key_name='keyname', + availability_zone=None, + block_device_mapping_v2=[], + nics=[], + scheduler_hints={'a': ['b', 'c']}, + config_drive=None, + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + def test_server_create_with_network(self): arglist = [ '--image', 'image1', @@ -392,7 +453,7 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, - block_device_mapping={}, + block_device_mapping_v2=[], nics=[{'net-id': 'net1_uuid', 'v4-fixed-ip': '', 'v6-fixed-ip': '', @@ -415,6 +476,248 @@ class TestServerCreate(TestServer): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + def test_server_create_with_auto_network(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--nic', 'auto', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('nic', ['auto']), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # 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_v2=[], + nics='auto', + scheduler_hints={}, + config_drive=None, + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + + def test_server_create_with_none_network(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--nic', 'none', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('nic', ['none']), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # 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_v2=[], + nics='none', + scheduler_hints={}, + config_drive=None, + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + + def test_server_create_with_conflict_network_options(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--nic', 'none', + '--nic', 'auto', + '--nic', 'port-id=port1', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('nic', ['none', 'auto', 'port-id=port1']), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + get_endpoints = mock.Mock() + get_endpoints.return_value = {'network': []} + self.app.client_manager.auth_ref = mock.Mock() + self.app.client_manager.auth_ref.service_catalog = mock.Mock() + self.app.client_manager.auth_ref.service_catalog.get_endpoints = ( + get_endpoints) + + find_port = mock.Mock() + network_client = self.app.client_manager.network + network_client.find_port = find_port + port_resource = mock.Mock() + port_resource.id = 'port1_uuid' + find_port.return_value = port_resource + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + self.assertNotCalled(self.servers_mock.create) + + def test_server_create_with_invalid_network_options(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--nic', 'abcdefgh', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('nic', ['abcdefgh']), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + self.assertNotCalled(self.servers_mock.create) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) + def test_server_create_with_wait_ok(self, mock_wait_for_status): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--wait', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('config_drive', False), + ('wait', True), + ('server_name', self.new_server.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + self.new_server.id, + callback=server._show_progress, + ) + + 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_v2=[], + nics=[], + scheduler_hints={}, + config_drive=None, + ) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=False) + def test_server_create_with_wait_fails(self, mock_wait_for_status): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--wait', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('config_drive', False), + ('wait', True), + ('server_name', self.new_server.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(SystemExit, self.cmd.take_action, parsed_args) + + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + self.new_server.id, + callback=server._show_progress, + ) + + 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_v2=[], + nics=[], + scheduler_hints={}, + config_drive=None, + ) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + @mock.patch('openstackclient.compute.v2.server.io.open') def test_server_create_userdata(self, mock_open): mock_file = mock.Mock(name='File') @@ -458,7 +761,7 @@ class TestServerCreate(TestServer): userdata=mock_file, key_name=None, availability_zone=None, - block_device_mapping={}, + block_device_mapping_v2=[], nics=[], scheduler_hints={}, config_drive=None, @@ -509,9 +812,14 @@ class TestServerCreate(TestServer): userdata=None, key_name=None, availability_zone=None, - block_device_mapping={ - 'vda': real_volume_mapping - }, + block_device_mapping_v2=[{ + 'device_name': 'vda', + 'uuid': real_volume_mapping.split(':', 1)[0], + 'destination_type': 'volume', + 'source_type': 'volume', + 'delete_on_termination': '0', + 'volume_size': '' + }], nics=[], scheduler_hints={}, config_drive=None, @@ -658,6 +966,8 @@ class TestServerList(TestServer): 'Networks', 'Image Name', 'Image ID', + 'Flavor Name', + 'Flavor ID', 'Availability Zone', 'Host', 'Properties', @@ -679,6 +989,8 @@ class TestServerList(TestServer): 'tenant_id': None, 'all_tenants': False, 'user_id': None, + 'deleted': False, + 'changes_since': None, } # Default params of the core function of the command in the case of no @@ -708,7 +1020,7 @@ class TestServerList(TestServer): self.servers_mock.list.return_value = self.servers self.image = image_fakes.FakeImage.create_one_image() - self.cimages_mock.get.return_value = self.image + self.images_mock.get.return_value = self.image self.flavor = compute_fakes.FakeFlavor.create_one_flavor() self.flavors_mock.get.return_value = self.flavor @@ -726,6 +1038,12 @@ class TestServerList(TestServer): for s in self.servers ] + Flavor = collections.namedtuple('Flavor', 'id name') + self.flavors_mock.list.return_value = [ + Flavor(id=s.flavor['id'], name=self.flavor.name) + for s in self.servers + ] + for s in self.servers: self.data.append(( s.id, @@ -745,6 +1063,8 @@ class TestServerList(TestServer): server._format_servers_list_networks(s.networks), self.image.name, s.image['id'], + self.flavor.name, + s.flavor['id'], getattr(s, 'OS-EXT-AZ:availability_zone'), getattr(s, 'OS-EXT-SRV-ATTR:host'), s.Metadata, @@ -755,6 +1075,7 @@ class TestServerList(TestServer): verifylist = [ ('all_projects', False), ('long', False), + ('deleted', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -792,7 +1113,7 @@ class TestServerList(TestServer): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.cimages_mock.get.assert_any_call(self.image.id) + self.images_mock.get.assert_any_call(self.image.id) self.search_opts['image'] = self.image.id self.servers_mock.list.assert_called_with(**self.kwargs) @@ -820,6 +1141,48 @@ class TestServerList(TestServer): self.assertEqual(self.columns, columns) self.assertEqual(tuple(self.data), tuple(data)) + def test_server_list_with_changes_since(self): + + arglist = [ + '--changes-since', '2016-03-04T06:27:59Z', + '--deleted' + ] + verifylist = [ + ('changes_since', '2016-03-04T06:27:59Z'), + ('deleted', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['changes_since'] = '2016-03-04T06:27:59Z' + self.search_opts['deleted'] = True + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError) + def test_server_list_with_invalid_changes_since(self, mock_parse_isotime): + + arglist = [ + '--changes-since', 'Invalid time value', + ] + verifylist = [ + ('changes_since', 'Invalid time value'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('Invalid changes-since value: Invalid time ' + 'value', str(e)) + mock_parse_isotime.assert_called_once_with( + 'Invalid time value' + ) + class TestServerLock(TestServer): @@ -841,6 +1204,209 @@ class TestServerLock(TestServer): self.run_method_with_servers('lock', 3) +class TestServerMigrate(TestServer): + + def setUp(self): + super(TestServerMigrate, self).setUp() + + methods = { + 'migrate': None, + 'live_migrate': None, + } + self.server = compute_fakes.FakeServer.create_one_server( + methods=methods) + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = self.server + + self.servers_mock.migrate.return_value = None + self.servers_mock.live_migrate.return_value = None + + # Get the command object to test + self.cmd = server.MigrateServer(self.app, None) + + def test_server_migrate_no_options(self): + arglist = [ + self.server.id, + ] + verifylist = [ + ('live', None), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.migrate.assert_called_with() + self.assertNotCalled(self.servers_mock.live_migrate) + self.assertIsNone(result) + + def test_server_migrate_with_block_migration(self): + arglist = [ + '--block-migration', self.server.id, + ] + verifylist = [ + ('live', None), + ('block_migration', True), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.assertNotCalled(self.servers_mock.live_migrate) + self.assertNotCalled(self.servers_mock.migrate) + + def test_server_migrate_with_disk_overcommit(self): + arglist = [ + '--disk-overcommit', self.server.id, + ] + verifylist = [ + ('live', None), + ('block_migration', False), + ('disk_overcommit', True), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.assertNotCalled(self.servers_mock.live_migrate) + self.assertNotCalled(self.servers_mock.migrate) + + def test_server_live_migrate(self): + arglist = [ + '--live', 'fakehost', self.server.id, + ] + verifylist = [ + ('live', 'fakehost'), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.live_migrate.assert_called_with(block_migration=False, + disk_over_commit=False, + host='fakehost') + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + + def test_server_block_live_migrate(self): + arglist = [ + '--live', 'fakehost', '--block-migration', self.server.id, + ] + verifylist = [ + ('live', 'fakehost'), + ('block_migration', True), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.live_migrate.assert_called_with(block_migration=True, + disk_over_commit=False, + host='fakehost') + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + + def test_server_live_migrate_with_disk_overcommit(self): + arglist = [ + '--live', 'fakehost', '--disk-overcommit', self.server.id, + ] + verifylist = [ + ('live', 'fakehost'), + ('block_migration', False), + ('disk_overcommit', True), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.live_migrate.assert_called_with(block_migration=False, + disk_over_commit=True, + host='fakehost') + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + + def test_server_live_migrate_with_false_value_options(self): + arglist = [ + '--live', 'fakehost', '--no-disk-overcommit', + '--shared-migration', self.server.id, + ] + verifylist = [ + ('live', 'fakehost'), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.live_migrate.assert_called_with(block_migration=False, + disk_over_commit=False, + host='fakehost') + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) + def test_server_migrate_with_wait(self, mock_wait_for_status): + arglist = [ + '--wait', self.server.id, + ] + verifylist = [ + ('live', None), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.migrate.assert_called_with() + self.assertNotCalled(self.servers_mock.live_migrate) + self.assertIsNone(result) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=False) + def test_server_migrate_with_wait_fails(self, mock_wait_for_status): + arglist = [ + '--wait', self.server.id, + ] + verifylist = [ + ('live', None), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(SystemExit, self.cmd.take_action, parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.migrate.assert_called_with() + self.assertNotCalled(self.servers_mock.live_migrate) + + class TestServerPause(TestServer): def setUp(self): @@ -868,7 +1434,7 @@ class TestServerRebuild(TestServer): # Return value for utils.find_resource for image self.image = image_fakes.FakeImage.create_one_image() - self.cimages_mock.get.return_value = self.image + self.images_mock.get.return_value = self.image # Fake the rebuilt new server. new_server = compute_fakes.FakeServer.create_one_server() @@ -908,7 +1474,7 @@ class TestServerRebuild(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.cimages_mock.get.assert_called_with(self.image.id) + self.images_mock.get.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) def test_rebuild_with_current_image_and_password(self): @@ -927,7 +1493,7 @@ class TestServerRebuild(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) - self.cimages_mock.get.assert_called_with(self.image.id) + self.images_mock.get.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, password) @mock.patch.object(common_utils, 'wait_for_status', return_value=True) @@ -955,7 +1521,7 @@ class TestServerRebuild(TestServer): ) self.servers_mock.get.assert_called_with(self.server.id) - self.cimages_mock.get.assert_called_with(self.image.id) + self.images_mock.get.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) @mock.patch.object(common_utils, 'wait_for_status', return_value=False) @@ -979,7 +1545,7 @@ class TestServerRebuild(TestServer): ) self.servers_mock.get.assert_called_with(self.server.id) - self.cimages_mock.get.assert_called_with(self.image.id) + self.images_mock.get.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, None) @@ -1477,7 +2043,7 @@ class TestServerShow(TestServer): # This is the return value for utils.find_resource() self.servers_mock.get.return_value = self.server - self.cimages_mock.get.return_value = self.image + self.images_mock.get.return_value = self.image self.flavors_mock.get.return_value = self.flavor # Get the command object to test @@ -1835,6 +2401,7 @@ class TestServerGeneral(TestServer): # Call _prep_server_detail(). server_detail = server._prep_server_detail( self.app.client_manager.compute, + self.app.client_manager.image, _server ) # 'networks' is used to create _server. Remove it. diff --git a/openstackclient/tests/unit/compute/v2/test_server_event.py b/openstackclient/tests/unit/compute/v2/test_server_event.py new file mode 100644 index 00000000..5c94891a --- /dev/null +++ b/openstackclient/tests/unit/compute/v2/test_server_event.py @@ -0,0 +1,167 @@ +# Copyright 2017 Huawei, Inc. All rights reserved. +# +# 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 server_event +from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes + + +class TestServerEvent(compute_fakes.TestComputev2): + + fake_server = compute_fakes.FakeServer.create_one_server() + + def setUp(self): + super(TestServerEvent, self).setUp() + + self.servers_mock = self.app.client_manager.compute.servers + self.servers_mock.reset_mock() + self.events_mock = self.app.client_manager.compute.instance_action + self.events_mock.reset_mock() + + self.servers_mock.get.return_value = self.fake_server + + +class TestListServerEvent(TestServerEvent): + + fake_event = compute_fakes.FakeServerEvent.create_one_server_event() + + columns = ( + 'Request ID', + 'Server ID', + 'Action', + 'Start Time', + ) + data = (( + fake_event.request_id, + fake_event.instance_uuid, + fake_event.action, + fake_event.start_time, + ), ) + + long_columns = ( + 'Request ID', + 'Server ID', + 'Action', + 'Start Time', + 'Message', + 'Project ID', + 'User ID', + ) + long_data = (( + fake_event.request_id, + fake_event.instance_uuid, + fake_event.action, + fake_event.start_time, + fake_event.message, + fake_event.project_id, + fake_event.user_id, + ), ) + + def setUp(self): + super(TestListServerEvent, self).setUp() + + self.events_mock.list.return_value = [self.fake_event, ] + self.cmd = server_event.ListServerEvent(self.app, None) + + def test_server_event_list(self): + arglist = [ + self.fake_server.name, + ] + verifylist = [ + ('server', self.fake_server.name), + ('long', False), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_once_with(self.fake_server.name) + self.events_mock.list.assert_called_once_with(self.fake_server.id) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, tuple(data)) + + def test_server_event_list_long(self): + arglist = [ + '--long', + self.fake_server.name, + ] + verifylist = [ + ('server', self.fake_server.name), + ('long', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_once_with(self.fake_server.name) + self.events_mock.list.assert_called_once_with(self.fake_server.id) + + self.assertEqual(self.long_columns, columns) + self.assertEqual(self.long_data, tuple(data)) + + +class TestShowServerEvent(TestServerEvent): + + fake_event = compute_fakes.FakeServerEvent.create_one_server_event() + + columns = ( + 'action', + 'events', + 'instance_uuid', + 'message', + 'project_id', + 'request_id', + 'start_time', + 'user_id', + ) + data = ( + fake_event.action, + fake_event.events, + fake_event.instance_uuid, + fake_event.message, + fake_event.project_id, + fake_event.request_id, + fake_event.start_time, + fake_event.user_id, + ) + + def setUp(self): + super(TestShowServerEvent, self).setUp() + + self.events_mock.get.return_value = self.fake_event + self.cmd = server_event.ShowServerEvent(self.app, None) + + def test_server_event_show(self): + arglist = [ + self.fake_server.name, + self.fake_event.request_id, + ] + verifylist = [ + ('server', self.fake_server.name), + ('request_id', self.fake_event.request_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_once_with(self.fake_server.name) + self.events_mock.get.assert_called_once_with( + self.fake_server.id, self.fake_event.request_id) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index d474f41d..088497da 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -63,54 +63,23 @@ class TestServerGroupCreate(TestServerGroup): def test_server_group_create(self): arglist = [ - '--policy', 'affinity', + '--policy', 'anti-affinity', 'affinity_group', ] verifylist = [ - ('policy', ['affinity']), + ('policy', 'anti-affinity'), ('name', 'affinity_group'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.server_groups_mock.create.assert_called_once_with( name=parsed_args.name, - policies=parsed_args.policy, + policies=[parsed_args.policy], ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_server_group_create_with_multiple_policies(self): - arglist = [ - '--policy', 'affinity', - '--policy', 'soft-affinity', - 'affinity_group', - ] - verifylist = [ - ('policy', ['affinity', 'soft-affinity']), - ('name', 'affinity_group'), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - self.server_groups_mock.create.assert_called_once_with( - name=parsed_args.name, - policies=parsed_args.policy, - ) - - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - def test_server_group_create_no_policy(self): - arglist = [ - 'affinity_group', - ] - verifylist = None - self.assertRaises(tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - verifylist) - class TestServerGroupDelete(TestServerGroup): diff --git a/openstackclient/tests/unit/compute/v2/test_usage.py b/openstackclient/tests/unit/compute/v2/test_usage.py new file mode 100644 index 00000000..a383e903 --- /dev/null +++ b/openstackclient/tests/unit/compute/v2/test_usage.py @@ -0,0 +1,179 @@ +# 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 datetime +import mock + +from openstackclient.compute.v2 import usage +from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes + + +class TestUsage(compute_fakes.TestComputev2): + + def setUp(self): + super(TestUsage, self).setUp() + + self.usage_mock = self.app.client_manager.compute.usage + self.usage_mock.reset_mock() + + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + +class TestUsageList(TestUsage): + + project = identity_fakes.FakeProject.create_one_project() + # Return value of self.usage_mock.list(). + usages = compute_fakes.FakeUsage.create_usages( + attrs={'tenant_id': project.name}, count=1) + + columns = ( + "Project", + "Servers", + "RAM MB-Hours", + "CPU Hours", + "Disk GB-Hours" + ) + + data = [( + usages[0].tenant_id, + len(usages[0].server_usages), + float("%.2f" % usages[0].total_memory_mb_usage), + float("%.2f" % usages[0].total_vcpus_usage), + float("%.2f" % usages[0].total_local_gb_usage), + )] + + def setUp(self): + super(TestUsageList, self).setUp() + + self.usage_mock.list.return_value = self.usages + + self.projects_mock.list.return_value = [self.project] + # Get the command object to test + self.cmd = usage.ListUsage(self.app, None) + + def test_usage_list_no_options(self): + + arglist = [] + verifylist = [ + ('start', None), + ('end', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.projects_mock.list.assert_called_with() + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + def test_usage_list_with_options(self): + arglist = [ + '--start', '2016-11-11', + '--end', '2016-12-20', + ] + verifylist = [ + ('start', '2016-11-11'), + ('end', '2016-12-20'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.projects_mock.list.assert_called_with() + self.usage_mock.list.assert_called_with( + datetime.datetime(2016, 11, 11, 0, 0), + datetime.datetime(2016, 12, 20, 0, 0), + detailed=True) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + +class TestUsageShow(TestUsage): + + project = identity_fakes.FakeProject.create_one_project() + # Return value of self.usage_mock.list(). + usage = compute_fakes.FakeUsage.create_one_usage( + attrs={'tenant_id': project.name}) + + columns = ( + 'CPU Hours', + 'Disk GB-Hours', + 'RAM MB-Hours', + 'Servers', + ) + + data = ( + float("%.2f" % usage.total_vcpus_usage), + float("%.2f" % usage.total_local_gb_usage), + float("%.2f" % usage.total_memory_mb_usage), + len(usage.server_usages), + ) + + def setUp(self): + super(TestUsageShow, self).setUp() + + self.usage_mock.get.return_value = self.usage + + self.projects_mock.get.return_value = self.project + # Get the command object to test + self.cmd = usage.ShowUsage(self.app, None) + + def test_usage_show_no_options(self): + + self.app.client_manager.auth_ref = mock.Mock() + self.app.client_manager.auth_ref.project_id = self.project.id + + arglist = [] + verifylist = [ + ('project', None), + ('start', None), + ('end', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_usage_show_with_options(self): + + arglist = [ + '--project', self.project.id, + '--start', '2016-11-11', + '--end', '2016-12-20', + ] + verifylist = [ + ('project', self.project.id), + ('start', '2016-11-11'), + ('end', '2016-12-20'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.usage_mock.get.assert_called_with( + self.project.id, + datetime.datetime(2016, 11, 11, 0, 0), + datetime.datetime(2016, 12, 20, 0, 0)) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/fakes.py b/openstackclient/tests/unit/fakes.py index f7cb5676..f28f9103 100644 --- a/openstackclient/tests/unit/fakes.py +++ b/openstackclient/tests/unit/fakes.py @@ -161,6 +161,9 @@ class FakeModule(object): def __init__(self, name, version): self.name = name self.__version__ = version + # Workaround for openstacksdk case + self.version = mock.Mock() + self.version.__version__ = version class FakeResource(object): @@ -212,10 +215,19 @@ class FakeResource(object): def keys(self): return self._info.keys() + def to_dict(self): + return self._info + @property def info(self): return self._info + def __getitem__(self, item): + return self._info.get(item) + + def get(self, item, default=None): + return self._info.get(item, default) + class FakeResponse(requests.Response): diff --git a/openstackclient/tests/unit/identity/v2_0/test_project.py b/openstackclient/tests/unit/identity/v2_0/test_project.py index c1f00762..c726f2a6 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_project.py +++ b/openstackclient/tests/unit/identity/v2_0/test_project.py @@ -13,8 +13,11 @@ # under the License. # +import mock + from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v2_0 import project from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes @@ -23,6 +26,7 @@ from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes class TestProject(identity_fakes.TestIdentityv2): fake_project = identity_fakes.FakeProject.create_one_project() + fake_projects = identity_fakes.FakeProject.create_projects() columns = ( 'description', @@ -36,6 +40,12 @@ class TestProject(identity_fakes.TestIdentityv2): fake_project.id, fake_project.name, ) + datalists = ( + (fake_projects[0].description, True, + fake_projects[0].id, fake_projects[0].name,), + (fake_projects[1].description, True, + fake_projects[1].id, fake_projects[1].name,), + ) def setUp(self): super(TestProject, self).setUp() @@ -302,6 +312,32 @@ class TestProjectDelete(TestProject): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_projects_with_exception(self, find_mock): + find_mock.side_effect = [self.fake_project, + exceptions.CommandError] + arglist = [ + self.fake_project.id, + 'unexist_project', + ] + verifylist = [ + ('projects', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 projects failed to delete.', + str(e)) + + find_mock.assert_any_call(self.projects_mock, self.fake_project.id) + find_mock.assert_any_call(self.projects_mock, 'unexist_project') + + self.assertEqual(2, find_mock.call_count) + self.projects_mock.delete.assert_called_once_with(self.fake_project.id) + class TestProjectList(TestProject): @@ -357,6 +393,35 @@ class TestProjectList(TestProject): ), ) self.assertEqual(datalist, tuple(data)) + def test_project_list_sort(self): + self.projects_mock.list.return_value = self.fake_projects + + arglist = ['--sort', 'name:asc', ] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + (columns, data) = self.cmd.take_action(parsed_args) + self.projects_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(collist, columns) + + if self.fake_projects[0].name > self.fake_projects[1].name: + datalists = ( + (self.fake_projects[1].id, self.fake_projects[1].name), + (self.fake_projects[0].id, self.fake_projects[0].name), + ) + else: + datalists = ( + (self.fake_projects[0].id, self.fake_projects[0].name), + (self.fake_projects[1].id, self.fake_projects[1].name), + ) + + self.assertEqual(datalists, tuple(data)) + class TestProjectSet(TestProject): diff --git a/openstackclient/tests/unit/identity/v2_0/test_role.py b/openstackclient/tests/unit/identity/v2_0/test_role.py index 68ebf141..684ce803 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_role.py +++ b/openstackclient/tests/unit/identity/v2_0/test_role.py @@ -17,6 +17,7 @@ import mock from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v2_0 import role from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes @@ -240,6 +241,32 @@ class TestRoleDelete(TestRole): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_roles_with_exception(self, find_mock): + find_mock.side_effect = [self.fake_role, + exceptions.CommandError] + arglist = [ + self.fake_role.id, + 'unexist_role', + ] + verifylist = [ + ('roles', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 roles failed to delete.', + str(e)) + + find_mock.assert_any_call(self.roles_mock, self.fake_role.id) + find_mock.assert_any_call(self.roles_mock, 'unexist_role') + + self.assertEqual(2, find_mock.call_count) + self.roles_mock.delete.assert_called_once_with(self.fake_role.id) + class TestRoleList(TestRole): diff --git a/openstackclient/tests/unit/identity/v2_0/test_user.py b/openstackclient/tests/unit/identity/v2_0/test_user.py index 765f8559..a8b9497e 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_user.py +++ b/openstackclient/tests/unit/identity/v2_0/test_user.py @@ -17,6 +17,7 @@ import mock from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v2_0 import user from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes @@ -411,6 +412,32 @@ class TestUserDelete(TestUser): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_users_with_exception(self, find_mock): + find_mock.side_effect = [self.fake_user, + exceptions.CommandError] + arglist = [ + self.fake_user.id, + 'unexist_user', + ] + verifylist = [ + ('users', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 users failed to delete.', + str(e)) + + find_mock.assert_any_call(self.users_mock, self.fake_user.id) + find_mock.assert_any_call(self.users_mock, 'unexist_user') + + self.assertEqual(2, find_mock.call_count) + self.users_mock.delete.assert_called_once_with(self.fake_user.id) + class TestUserList(TestUser): diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index 75065e65..139d90d5 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -622,6 +622,23 @@ class FakeProject(object): loaded=True) return project + @staticmethod + def create_projects(attrs=None, count=2): + """Create multiple fake projects. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of projects to fake + :return: + A list of FakeResource objects faking the projects + """ + + projects = [] + for i in range(0, count): + projects.append(FakeProject.create_one_project(attrs)) + return projects + class FakeDomain(object): """Fake one or more domain.""" @@ -753,6 +770,44 @@ class FakeUser(object): loaded=True) return user + @staticmethod + def create_users(attrs=None, count=2): + """Create multiple fake users. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of users to fake + :return: + A list of FakeResource objects faking the users + """ + users = [] + for i in range(0, count): + user = FakeUser.create_one_user(attrs) + users.append(user) + + return users + + @staticmethod + def get_users(users=None, count=2): + """Get an iterable MagicMock object with a list of faked users. + + If users list is provided, then initialize the Mock object with + the list. Otherwise create one. + + :param List users: + A list of FakeResource objects faking users + :param Integer count: + The number of users to be faked + :return + An iterable Mock object with side_effect set to a list of faked + users + """ + if users is None: + users = FakeUser.create_users(count) + + return mock.Mock(side_effect=users) + class FakeGroup(object): """Fake one or more group.""" diff --git a/openstackclient/tests/unit/identity/v3/test_group.py b/openstackclient/tests/unit/identity/v3/test_group.py index eb50adb5..81722631 100644 --- a/openstackclient/tests/unit/identity/v3/test_group.py +++ b/openstackclient/tests/unit/identity/v3/test_group.py @@ -16,6 +16,7 @@ from mock import call from keystoneauth1 import exceptions as ks_exc from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import group from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -41,34 +42,79 @@ class TestGroup(identity_fakes.TestIdentityv3): class TestGroupAddUser(TestGroup): - group = identity_fakes.FakeGroup.create_one_group() - user = identity_fakes.FakeUser.create_one_user() + _group = identity_fakes.FakeGroup.create_one_group() + users = identity_fakes.FakeUser.create_users(count=2) def setUp(self): super(TestGroupAddUser, self).setUp() - self.groups_mock.get.return_value = self.group - self.users_mock.get.return_value = self.user + self.groups_mock.get.return_value = self._group + self.users_mock.get = ( + identity_fakes.FakeUser.get_users(self.users)) self.users_mock.add_to_group.return_value = None self.cmd = group.AddUserToGroup(self.app, None) def test_group_add_user(self): arglist = [ - self.group.name, - self.user.name, + self._group.name, + self.users[0].name, ] verifylist = [ - ('group', self.group.name), - ('user', self.user.name), + ('group', self._group.name), + ('user', [self.users[0].name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.users_mock.add_to_group.assert_called_once_with( - self.user.id, self.group.id) + self.users[0].id, self._group.id) + self.assertIsNone(result) + + def test_group_add_multi_users(self): + arglist = [ + self._group.name, + self.users[0].name, + self.users[1].name, + ] + verifylist = [ + ('group', self._group.name), + ('user', [self.users[0].name, self.users[1].name]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + calls = [call(self.users[0].id, self._group.id), + call(self.users[1].id, self._group.id)] + self.users_mock.add_to_group.assert_has_calls(calls) self.assertIsNone(result) + @mock.patch.object(group.LOG, 'error') + def test_group_add_user_with_error(self, mock_error): + self.users_mock.add_to_group.side_effect = [ + exceptions.CommandError(), None] + arglist = [ + self._group.name, + self.users[0].name, + self.users[1].name, + ] + verifylist = [ + ('group', self._group.name), + ('user', [self.users[0].name, self.users[1].name]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + msg = "1 of 2 users not added to group %s." % self._group.name + self.assertEqual(msg, str(e)) + msg = ("%(user)s not added to group %(group)s: ") % { + 'user': self.users[0].name, + 'group': self._group.name, + } + mock_error.assert_called_once_with(msg) + class TestGroupCheckUser(TestGroup): @@ -100,6 +146,23 @@ class TestGroupCheckUser(TestGroup): self.user.id, self.group.id) self.assertIsNone(result) + def test_group_check_user_server_error(self): + def server_error(*args): + raise ks_exc.http.InternalServerError + self.users_mock.check_in_group.side_effect = server_error + arglist = [ + self.group.name, + self.user.name, + ] + verifylist = [ + ('group', self.group.name), + ('user', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(ks_exc.http.InternalServerError, + self.cmd.take_action, parsed_args) + class TestGroupCreate(TestGroup): @@ -257,6 +320,32 @@ class TestGroupDelete(TestGroup): self.groups_mock.delete.assert_called_once_with(self.groups[0].id) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_groups_with_exception(self, find_mock): + find_mock.side_effect = [self.groups[0], + exceptions.CommandError] + arglist = [ + self.groups[0].id, + 'unexist_group', + ] + verifylist = [ + ('groups', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 groups failed to delete.', + str(e)) + + find_mock.assert_any_call(self.groups_mock, self.groups[0].id) + find_mock.assert_any_call(self.groups_mock, 'unexist_group') + + self.assertEqual(2, find_mock.call_count) + self.groups_mock.delete.assert_called_once_with(self.groups[0].id) + class TestGroupList(TestGroup): @@ -405,34 +494,79 @@ class TestGroupList(TestGroup): class TestGroupRemoveUser(TestGroup): - group = identity_fakes.FakeGroup.create_one_group() - user = identity_fakes.FakeUser.create_one_user() + _group = identity_fakes.FakeGroup.create_one_group() + users = identity_fakes.FakeUser.create_users(count=2) def setUp(self): super(TestGroupRemoveUser, self).setUp() - self.groups_mock.get.return_value = self.group - self.users_mock.get.return_value = self.user + self.groups_mock.get.return_value = self._group + self.users_mock.get = ( + identity_fakes.FakeUser.get_users(self.users)) self.users_mock.remove_from_group.return_value = None self.cmd = group.RemoveUserFromGroup(self.app, None) def test_group_remove_user(self): arglist = [ - self.group.id, - self.user.id, + self._group.id, + self.users[0].id, ] verifylist = [ - ('group', self.group.id), - ('user', self.user.id), + ('group', self._group.id), + ('user', [self.users[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.users_mock.remove_from_group.assert_called_once_with( - self.user.id, self.group.id) + self.users[0].id, self._group.id) self.assertIsNone(result) + def test_group_remove_multi_users(self): + arglist = [ + self._group.name, + self.users[0].name, + self.users[1].name, + ] + verifylist = [ + ('group', self._group.name), + ('user', [self.users[0].name, self.users[1].name]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + calls = [call(self.users[0].id, self._group.id), + call(self.users[1].id, self._group.id)] + self.users_mock.remove_from_group.assert_has_calls(calls) + self.assertIsNone(result) + + @mock.patch.object(group.LOG, 'error') + def test_group_remove_user_with_error(self, mock_error): + self.users_mock.remove_from_group.side_effect = [ + exceptions.CommandError(), None] + arglist = [ + self._group.id, + self.users[0].id, + self.users[1].id, + ] + verifylist = [ + ('group', self._group.id), + ('user', [self.users[0].id, self.users[1].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + msg = "1 of 2 users not removed from group %s." % self._group.id + self.assertEqual(msg, str(e)) + msg = ("%(user)s not removed from group %(group)s: ") % { + 'user': self.users[0].id, + 'group': self._group.id, + } + mock_error.assert_called_once_with(msg) + class TestGroupSet(TestGroup): diff --git a/openstackclient/tests/unit/identity/v3/test_mappings.py b/openstackclient/tests/unit/identity/v3/test_mappings.py index 5086724c..93fe1196 100644 --- a/openstackclient/tests/unit/identity/v3/test_mappings.py +++ b/openstackclient/tests/unit/identity/v3/test_mappings.py @@ -181,16 +181,12 @@ class TestMappingSet(TestMapping): mocker.return_value = identity_fakes.MAPPING_RULES_2 with mock.patch("openstackclient.identity.v3.mapping." "SetMapping._read_rules", mocker): - columns, data = self.cmd.take_action(parsed_args) + result = self.cmd.take_action(parsed_args) self.mapping_mock.update.assert_called_with( mapping=identity_fakes.mapping_id, rules=identity_fakes.MAPPING_RULES_2) - collist = ('id', 'rules') - self.assertEqual(collist, columns) - datalist = (identity_fakes.mapping_id, - identity_fakes.MAPPING_RULES_2) - self.assertEqual(datalist, data) + self.assertIsNone(result) def test_set_rules_wrong_file_path(self): arglist = [ diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index 702d9209..7be81153 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -14,8 +14,10 @@ # import mock +from mock import call from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import project from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -445,12 +447,39 @@ class TestProjectDelete(TestProject): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_projects_with_exception(self, find_mock): + find_mock.side_effect = [self.project, + exceptions.CommandError] + arglist = [ + self.project.id, + 'unexist_project', + ] + verifylist = [ + ('projects', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 projects failed to delete.', + str(e)) + + find_mock.assert_any_call(self.projects_mock, self.project.id) + find_mock.assert_any_call(self.projects_mock, 'unexist_project') + + self.assertEqual(2, find_mock.call_count) + self.projects_mock.delete.assert_called_once_with(self.project.id) + class TestProjectList(TestProject): domain = identity_fakes.FakeDomain.create_one_domain() project = identity_fakes.FakeProject.create_one_project( attrs={'domain_id': domain.id}) + projects = identity_fakes.FakeProject.create_projects() columns = ( 'ID', @@ -462,6 +491,12 @@ class TestProjectList(TestProject): project.name, ), ) + datalists = ( + (projects[0].description, True, + projects[0].id, projects[0].name,), + (projects[1].description, True, + projects[1].id, projects[1].name,), + ) def setUp(self): super(TestProjectList, self).setUp() @@ -552,6 +587,66 @@ class TestProjectList(TestProject): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) + def test_project_list_sort(self): + self.projects_mock.list.return_value = self.projects + + arglist = ['--sort', 'name:asc', ] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + (columns, data) = self.cmd.take_action(parsed_args) + self.projects_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(collist, columns) + + if self.projects[0].name > self.projects[1].name: + datalists = ( + (self.projects[1].id, self.projects[1].name), + (self.projects[0].id, self.projects[0].name), + ) + else: + datalists = ( + (self.projects[0].id, self.projects[0].name), + (self.projects[1].id, self.projects[1].name), + ) + + self.assertEqual(datalists, tuple(data)) + + def test_project_list_my_projects(self): + auth_ref = identity_fakes.fake_auth_ref( + identity_fakes.TOKEN_WITH_PROJECT_ID, + ) + ar_mock = mock.PropertyMock(return_value=auth_ref) + type(self.app.client_manager).auth_ref = ar_mock + + arglist = [ + '--my-projects', + ] + verifylist = [ + ('my_projects', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + self.projects_mock.list.assert_called_with( + user=self.app.client_manager.auth_ref.user_id) + + collist = ('ID', 'Name') + self.assertEqual(collist, columns) + datalist = (( + self.project.id, + self.project.name, + ), ) + self.assertEqual(datalist, tuple(data)) + class TestProjectSet(TestProject): @@ -736,8 +831,6 @@ class TestProjectShow(TestProject): def test_project_show(self): - self.projects_mock.get.side_effect = [Exception("Not found"), - self.project] self.projects_mock.get.return_value = self.project arglist = [ @@ -763,11 +856,7 @@ class TestProjectShow(TestProject): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_called_with( - self.project.id, - parents_as_list=False, - subtree_as_list=False, - ) + self.projects_mock.get.assert_called_once_with(self.project.id) collist = ( 'description', @@ -797,8 +886,6 @@ class TestProjectShow(TestProject): 'parents': [{'project': {'id': self.project.parent_id}}] } ) - self.projects_mock.get.side_effect = [Exception("Not found"), - self.project] self.projects_mock.get.return_value = self.project arglist = [ @@ -822,11 +909,12 @@ class TestProjectShow(TestProject): } columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_called_with( - self.project.id, - parents_as_list=True, - subtree_as_list=False, - ) + + self.projects_mock.get.assert_has_calls([call(self.project.id), + call(self.project.id, + parents_as_list=True, + subtree_as_list=False, + )]) collist = ( 'description', @@ -858,8 +946,6 @@ class TestProjectShow(TestProject): 'subtree': [{'project': {'id': 'children-id'}}] } ) - self.projects_mock.get.side_effect = [Exception("Not found"), - self.project] self.projects_mock.get.return_value = self.project arglist = [ @@ -883,11 +969,11 @@ class TestProjectShow(TestProject): } columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_called_with( - self.project.id, - parents_as_list=False, - subtree_as_list=True, - ) + self.projects_mock.get.assert_has_calls([call(self.project.id), + call(self.project.id, + parents_as_list=False, + subtree_as_list=True, + )]) collist = ( 'description', @@ -920,8 +1006,6 @@ class TestProjectShow(TestProject): 'subtree': [{'project': {'id': 'children-id'}}] } ) - self.projects_mock.get.side_effect = [Exception("Not found"), - self.project] self.projects_mock.get.return_value = self.project arglist = [ @@ -946,11 +1030,11 @@ class TestProjectShow(TestProject): } columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_called_with( - self.project.id, - parents_as_list=True, - subtree_as_list=True, - ) + self.projects_mock.get.assert_has_calls([call(self.project.id), + call(self.project.id, + parents_as_list=True, + subtree_as_list=True, + )]) collist = ( 'description', diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index 448e18d3..39dbd244 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -14,6 +14,10 @@ # import copy +import mock + +from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import role from openstackclient.tests.unit import fakes @@ -269,6 +273,22 @@ class TestRoleAdd(TestRole): ) self.assertIsNone(result) + def test_role_add_with_error(self): + arglist = [ + identity_fakes.role_name, + ] + verifylist = [ + ('user', None), + ('group', None), + ('domain', None), + ('project', None), + ('role', identity_fakes.role_name), + ('inherited', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + class TestRoleAddInherited(TestRoleAdd, TestRoleInherited): pass @@ -428,6 +448,36 @@ class TestRoleDelete(TestRole): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_roles_with_exception(self, find_mock): + find_mock.side_effect = [self.roles_mock.get.return_value, + exceptions.CommandError] + arglist = [ + identity_fakes.role_name, + 'unexist_role', + ] + verifylist = [ + ('roles', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 roles failed to delete.', + str(e)) + + find_mock.assert_any_call(self.roles_mock, + identity_fakes.role_name, + domain_id=None) + find_mock.assert_any_call(self.roles_mock, + 'unexist_role', + domain_id=None) + + self.assertEqual(2, find_mock.call_count) + self.roles_mock.delete.assert_called_once_with(identity_fakes.role_id) + class TestRoleList(TestRole): @@ -737,6 +787,17 @@ class TestRoleList(TestRole): ), ) self.assertEqual(datalist, tuple(data)) + def test_role_list_group_with_error(self): + arglist = [ + '--group', identity_fakes.group_id, + ] + verifylist = [ + ('group', identity_fakes.group_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + class TestRoleRemove(TestRole): @@ -948,6 +1009,22 @@ class TestRoleRemove(TestRole): ) self.assertIsNone(result) + def test_role_remove_with_error(self): + arglist = [ + identity_fakes.role_name, + ] + verifylist = [ + ('user', None), + ('group', None), + ('domain', None), + ('project', None), + ('role', identity_fakes.role_name), + ('inherited', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + class TestRoleSet(TestRole): diff --git a/openstackclient/tests/unit/identity/v3/test_trust.py b/openstackclient/tests/unit/identity/v3/test_trust.py index 4eeb8bfe..93e8f63d 100644 --- a/openstackclient/tests/unit/identity/v3/test_trust.py +++ b/openstackclient/tests/unit/identity/v3/test_trust.py @@ -12,6 +12,10 @@ # import copy +import mock + +from osc_lib import exceptions +from osc_lib import utils from openstackclient.identity.v3 import trust from openstackclient.tests.unit import fakes @@ -148,6 +152,33 @@ class TestTrustDelete(TestTrust): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_trusts_with_exception(self, find_mock): + find_mock.side_effect = [self.trusts_mock.get.return_value, + exceptions.CommandError] + arglist = [ + identity_fakes.trust_id, + 'unexist_trust', + ] + verifylist = [ + ('trust', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 trusts failed to delete.', + str(e)) + + find_mock.assert_any_call(self.trusts_mock, identity_fakes.trust_id) + find_mock.assert_any_call(self.trusts_mock, 'unexist_trust') + + self.assertEqual(2, find_mock.call_count) + self.trusts_mock.delete.assert_called_once_with( + identity_fakes.trust_id) + class TestTrustList(TestTrust): diff --git a/openstackclient/tests/unit/identity/v3/test_unscoped_saml.py b/openstackclient/tests/unit/identity/v3/test_unscoped_saml.py index 9e4e1876..34655263 100644 --- a/openstackclient/tests/unit/identity/v3/test_unscoped_saml.py +++ b/openstackclient/tests/unit/identity/v3/test_unscoped_saml.py @@ -12,8 +12,6 @@ import copy -from osc_lib import exceptions - from openstackclient.identity.v3 import unscoped_saml from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -48,7 +46,6 @@ class TestDomainList(TestUnscopedSAML): self.cmd = unscoped_saml.ListAccessibleDomains(self.app, None) def test_accessible_domains_list(self): - self.app.client_manager.auth_plugin_name = 'v3unscopedsaml' arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -70,17 +67,6 @@ class TestDomainList(TestUnscopedSAML): ), ) self.assertEqual(datalist, tuple(data)) - def test_accessible_domains_list_wrong_auth(self): - auth = identity_fakes.FakeAuth("wrong auth") - self.app.client_manager.identity.session.auth = auth - arglist = [] - verifylist = [] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, - parsed_args) - class TestProjectList(TestUnscopedSAML): @@ -99,7 +85,6 @@ class TestProjectList(TestUnscopedSAML): self.cmd = unscoped_saml.ListAccessibleProjects(self.app, None) def test_accessible_projects_list(self): - self.app.client_manager.auth_plugin_name = 'v3unscopedsaml' arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -120,14 +105,3 @@ class TestProjectList(TestUnscopedSAML): identity_fakes.project_name, ), ) self.assertEqual(datalist, tuple(data)) - - def test_accessible_projects_list_wrong_auth(self): - auth = identity_fakes.FakeAuth("wrong auth") - self.app.client_manager.identity.session.auth = auth - arglist = [] - verifylist = [] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, - parsed_args) diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index 6150a5f3..2ce66e94 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -16,6 +16,9 @@ import contextlib import mock +from osc_lib import exceptions +from osc_lib import utils + from openstackclient.identity.v3 import user from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -465,6 +468,32 @@ class TestUserDelete(TestUser): ) self.assertIsNone(result) + @mock.patch.object(utils, 'find_resource') + def test_delete_multi_users_with_exception(self, find_mock): + find_mock.side_effect = [self.user, + exceptions.CommandError] + arglist = [ + self.user.id, + 'unexist_user', + ] + verifylist = [ + ('users', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 users failed to delete.', + str(e)) + + find_mock.assert_any_call(self.users_mock, self.user.id) + find_mock.assert_any_call(self.users_mock, 'unexist_user') + + self.assertEqual(2, find_mock.call_count) + self.users_mock.delete.assert_called_once_with(self.user.id) + class TestUserList(TestUser): @@ -655,9 +684,14 @@ class TestUserList(TestUser): class TestUserSet(TestUser): project = identity_fakes.FakeProject.create_one_project() + domain = identity_fakes.FakeDomain.create_one_domain() user = identity_fakes.FakeUser.create_one_user( attrs={'default_project_id': project.id} ) + user2 = identity_fakes.FakeUser.create_one_user( + attrs={'default_project_id': project.id, + 'domain_id': domain.id} + ) def setUp(self): super(TestUserSet, self).setUp() @@ -719,6 +753,37 @@ class TestUserSet(TestUser): ) self.assertIsNone(result) + def test_user_set_specify_domain(self): + arglist = [ + '--name', 'qwerty', + '--domain', self.domain.id, + self.user2.name + ] + verifylist = [ + ('name', 'qwerty'), + ('password', None), + ('domain', self.domain.id), + ('email', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', self.user2.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + kwargs = { + 'enabled': True, + 'name': 'qwerty' + } + + self.users_mock.update.assert_called_with( + self.user.id, + **kwargs + ) + self.assertIsNone(result) + def test_user_set_password(self): arglist = [ '--password', 'secret', diff --git a/openstackclient/tests/unit/image/v1/fakes.py b/openstackclient/tests/unit/image/v1/fakes.py index a8e52fa3..080356ee 100644 --- a/openstackclient/tests/unit/image/v1/fakes.py +++ b/openstackclient/tests/unit/image/v1/fakes.py @@ -13,7 +13,9 @@ # under the License. # +import copy import mock +import uuid from openstackclient.tests.unit import fakes from openstackclient.tests.unit import utils @@ -74,3 +76,45 @@ class TestImagev1(utils.TestCommand): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) + + +class FakeImage(object): + """Fake one or more images.""" + + @staticmethod + def create_one_image(attrs=None): + """Create a fake image. + + :param Dictionary attrs: + A dictionary with all attrbutes of image + :return: + A FakeResource object with id, name, owner, protected, + visibility and tags attrs + """ + attrs = attrs or {} + + # Set default attribute + image_info = { + 'id': str(uuid.uuid4()), + 'name': 'image-name' + uuid.uuid4().hex, + 'owner': 'image-owner' + uuid.uuid4().hex, + 'container_format': '', + 'disk_format': '', + 'min_disk': 0, + 'min_ram': 0, + 'is_public': True, + 'protected': False, + 'properties': { + 'Alpha': 'a', + 'Beta': 'b', + 'Gamma': 'g'}, + } + + # Overwrite default attributes if there are some attributes set + image_info.update(attrs) + + image = fakes.FakeResource( + info=copy.deepcopy(image_info), + loaded=True) + + return image diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index a6bc80a0..036c8336 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -17,6 +17,7 @@ import copy import mock from osc_lib import exceptions +from osc_lib import utils from openstackclient.image.v1 import image from openstackclient.tests.unit import fakes @@ -35,25 +36,39 @@ class TestImage(image_fakes.TestImagev1): class TestImageCreate(TestImage): + new_image = image_fakes.FakeImage.create_one_image() + columns = ( + 'container_format', + 'disk_format', + 'id', + 'is_public', + 'min_disk', + 'min_ram', + 'name', + 'owner', + 'properties', + 'protected', + ) + data = ( + new_image.container_format, + new_image.disk_format, + new_image.id, + new_image.is_public, + new_image.min_disk, + new_image.min_ram, + new_image.name, + new_image.owner, + utils.format_dict(new_image.properties), + new_image.protected, + ) + def setUp(self): super(TestImageCreate, self).setUp() - self.images_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) + self.images_mock.create.return_value = self.new_image # This is the return value for utils.find_resource() - self.images_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) - self.images_mock.update.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) + self.images_mock.get.return_value = self.new_image + self.images_mock.update.return_value = self.new_image # Get the command object to test self.cmd = image.CreateImage(self.app, None) @@ -65,12 +80,12 @@ class TestImageCreate(TestImage): } self.images_mock.configure_mock(**mock_exception) arglist = [ - image_fakes.image_name, + self.new_image.name, ] verifylist = [ ('container_format', image.DEFAULT_CONTAINER_FORMAT), ('disk_format', image.DEFAULT_DISK_FORMAT), - ('name', image_fakes.image_name), + ('name', self.new_image.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -81,7 +96,7 @@ class TestImageCreate(TestImage): # ImageManager.create(name=, **) self.images_mock.create.assert_called_with( - name=image_fakes.image_name, + name=self.new_image.name, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, data=mock.ANY, @@ -90,8 +105,8 @@ class TestImageCreate(TestImage): # Verify update() was not called, if it was show the args self.assertEqual(self.images_mock.update.call_args_list, []) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_image_reserve_options(self): mock_exception = { @@ -101,17 +116,17 @@ class TestImageCreate(TestImage): self.images_mock.configure_mock(**mock_exception) arglist = [ '--container-format', 'ovf', - '--disk-format', 'fs', + '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', '--protected', '--private', '--project', 'q', - image_fakes.image_name, + self.new_image.name, ] verifylist = [ ('container_format', 'ovf'), - ('disk_format', 'fs'), + ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), ('protected', True), @@ -119,7 +134,7 @@ class TestImageCreate(TestImage): ('public', False), ('private', True), ('project', 'q'), - ('name', image_fakes.image_name), + ('name', self.new_image.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -130,9 +145,9 @@ class TestImageCreate(TestImage): # ImageManager.create(name=, **) self.images_mock.create.assert_called_with( - name=image_fakes.image_name, + name=self.new_image.name, container_format='ovf', - disk_format='fs', + disk_format='ami', min_disk=10, min_ram=4, protected=True, @@ -144,14 +159,14 @@ class TestImageCreate(TestImage): # Verify update() was not called, if it was show the args self.assertEqual(self.images_mock.update.call_args_list, []) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) @mock.patch('openstackclient.image.v1.image.io.open', name='Open') def test_image_create_file(self, mock_open): mock_file = mock.Mock(name='File') mock_open.return_value = mock_file - mock_open.read.return_value = image_fakes.image_data + mock_open.read.return_value = self.data mock_exception = { 'find.side_effect': exceptions.CommandError('x'), 'get.side_effect': exceptions.CommandError('x'), @@ -164,7 +179,7 @@ class TestImageCreate(TestImage): '--public', '--property', 'Alpha=1', '--property', 'Beta=2', - image_fakes.image_name, + self.new_image.name, ] verifylist = [ ('file', 'filer'), @@ -173,7 +188,7 @@ class TestImageCreate(TestImage): ('public', True), ('private', False), ('properties', {'Alpha': '1', 'Beta': '2'}), - ('name', image_fakes.image_name), + ('name', self.new_image.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -193,7 +208,7 @@ class TestImageCreate(TestImage): # ImageManager.create(name=, **) self.images_mock.create.assert_called_with( - name=image_fakes.image_name, + name=self.new_image.name, container_format=image.DEFAULT_CONTAINER_FORMAT, disk_format=image.DEFAULT_DISK_FORMAT, protected=False, @@ -208,21 +223,19 @@ class TestImageCreate(TestImage): # Verify update() was not called, if it was show the args self.assertEqual(self.images_mock.update.call_args_list, []) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) class TestImageDelete(TestImage): + _image = image_fakes.FakeImage.create_one_image() + def setUp(self): super(TestImageDelete, self).setUp() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) + self.images_mock.get.return_value = self._image self.images_mock.delete.return_value = None # Get the command object to test @@ -230,21 +243,23 @@ class TestImageDelete(TestImage): def test_image_delete_no_options(self): arglist = [ - image_fakes.image_id, + self._image.id, ] verifylist = [ - ('images', [image_fakes.image_id]), + ('images', [self._image.id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.images_mock.delete.assert_called_with(image_fakes.image_id) + self.images_mock.delete.assert_called_with(self._image.id) self.assertIsNone(result) class TestImageList(TestImage): + _image = image_fakes.FakeImage.create_one_image() + columns = ( 'ID', 'Name', @@ -252,18 +267,33 @@ class TestImageList(TestImage): ) datalist = ( ( - image_fakes.image_id, - image_fakes.image_name, + _image.id, + _image.name, '', ), ) + # create a image_info as the side_effect of the fake image_list() + info = { + 'id': _image.id, + 'name': _image.name, + 'owner': _image.owner, + 'container_format': _image.container_format, + 'disk_format': _image.disk_format, + 'min_disk': _image.min_disk, + 'min_ram': _image.min_ram, + 'is_public': _image.is_public, + 'protected': _image.protected, + 'properties': _image.properties, + } + image_info = copy.deepcopy(info) + def setUp(self): super(TestImageList, self).setUp() self.api_mock = mock.Mock() self.api_mock.image_list.side_effect = [ - [copy.deepcopy(image_fakes.IMAGE)], [], + [self.image_info], [], ] self.app.client_manager.image.api = self.api_mock @@ -285,7 +315,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( detailed=True, - marker=image_fakes.image_id, + marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -309,7 +339,7 @@ class TestImageList(TestImage): self.api_mock.image_list.assert_called_with( detailed=True, public=True, - marker=image_fakes.image_id, + marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -333,7 +363,7 @@ class TestImageList(TestImage): self.api_mock.image_list.assert_called_with( detailed=True, private=True, - marker=image_fakes.image_id, + marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -354,7 +384,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( detailed=True, - marker=image_fakes.image_id, + marker=self._image.id, ) collist = ( @@ -373,8 +403,8 @@ class TestImageList(TestImage): self.assertEqual(collist, columns) datalist = (( - image_fakes.image_id, - image_fakes.image_name, + self._image.id, + self._image.name, '', '', '', @@ -382,7 +412,7 @@ class TestImageList(TestImage): '', 'public', False, - image_fakes.image_owner, + self._image.owner, "Alpha='a', Beta='b', Gamma='g'", ), ) self.assertEqual(datalist, tuple(data)) @@ -390,7 +420,7 @@ class TestImageList(TestImage): @mock.patch('openstackclient.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): sf_mock.side_effect = [ - [copy.deepcopy(image_fakes.IMAGE)], [], + [self.image_info], [], ] arglist = [ @@ -407,10 +437,10 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( detailed=True, - marker=image_fakes.image_id, + marker=self._image.id, ) sf_mock.assert_called_with( - [image_fakes.IMAGE], + [self.image_info], attr='a', value='1', property_field='properties', @@ -422,7 +452,7 @@ class TestImageList(TestImage): @mock.patch('osc_lib.utils.sort_items') def test_image_list_sort_option(self, si_mock): si_mock.side_effect = [ - [copy.deepcopy(image_fakes.IMAGE)], [], + [self.image_info], [], ] arglist = ['--sort', 'name:asc'] @@ -435,10 +465,10 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( detailed=True, - marker=image_fakes.image_id, + marker=self._image.id, ) si_mock.assert_called_with( - [image_fakes.IMAGE], + [self.image_info], 'name:asc' ) @@ -448,36 +478,30 @@ class TestImageList(TestImage): class TestImageSet(TestImage): + _image = image_fakes.FakeImage.create_one_image() + def setUp(self): super(TestImageSet, self).setUp() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) - self.images_mock.update.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) + self.images_mock.get.return_value = self._image + self.images_mock.update.return_value = self._image # Get the command object to test self.cmd = image.SetImage(self.app, None) def test_image_set_no_options(self): arglist = [ - image_fakes.image_name, + self._image.name, ] verifylist = [ - ('image', image_fakes.image_name), + ('image', self._image.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.images_mock.update.assert_called_with(image_fakes.image_id, + self.images_mock.update.assert_called_with(self._image.id, **{}) self.assertIsNone(result) @@ -490,7 +514,7 @@ class TestImageSet(TestImage): '--disk-format', 'vmdk', '--size', '35165824', '--project', 'new-owner', - image_fakes.image_name, + self._image.name, ] verifylist = [ ('name', 'new-name'), @@ -500,7 +524,7 @@ class TestImageSet(TestImage): ('disk_format', 'vmdk'), ('size', 35165824), ('project', 'new-owner'), - ('image', image_fakes.image_name), + ('image', self._image.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -517,7 +541,7 @@ class TestImageSet(TestImage): } # ImageManager.update(image, **kwargs) self.images_mock.update.assert_called_with( - image_fakes.image_id, + self._image.id, **kwargs ) self.assertIsNone(result) @@ -526,14 +550,14 @@ class TestImageSet(TestImage): arglist = [ '--protected', '--private', - image_fakes.image_name, + self._image.name, ] verifylist = [ ('protected', True), ('unprotected', False), ('public', False), ('private', True), - ('image', image_fakes.image_name), + ('image', self._image.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -545,7 +569,7 @@ class TestImageSet(TestImage): } # ImageManager.update(image, **kwargs) self.images_mock.update.assert_called_with( - image_fakes.image_id, + self._image.id, **kwargs ) self.assertIsNone(result) @@ -554,14 +578,14 @@ class TestImageSet(TestImage): arglist = [ '--unprotected', '--public', - image_fakes.image_name, + self._image.name, ] verifylist = [ ('protected', False), ('unprotected', True), ('public', True), ('private', False), - ('image', image_fakes.image_name), + ('image', self._image.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -573,7 +597,7 @@ class TestImageSet(TestImage): } # ImageManager.update(image, **kwargs) self.images_mock.update.assert_called_with( - image_fakes.image_id, + self._image.id, **kwargs ) self.assertIsNone(result) @@ -582,11 +606,11 @@ class TestImageSet(TestImage): arglist = [ '--property', 'Alpha=1', '--property', 'Beta=2', - image_fakes.image_name, + self._image.name, ] verifylist = [ ('properties', {'Alpha': '1', 'Beta': '2'}), - ('image', image_fakes.image_name), + ('image', self._image.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -601,7 +625,7 @@ class TestImageSet(TestImage): } # ImageManager.update(image, **kwargs) self.images_mock.update.assert_called_with( - image_fakes.image_id, + self._image.id, **kwargs ) self.assertIsNone(result) @@ -624,7 +648,7 @@ class TestImageSet(TestImage): "volume_type": 'volume_type', "container_format": image.DEFAULT_CONTAINER_FORMAT, "disk_format": image.DEFAULT_DISK_FORMAT, - "image": image_fakes.image_name, + "image": self._image.name, } full_response = {"os-volume_upload_image": response} volumes_mock.upload_to_image.return_value = (201, full_response) @@ -632,7 +656,7 @@ class TestImageSet(TestImage): arglist = [ '--volume', 'volly', '--name', 'updated_image', - image_fakes.image_name, + self._image.name, ] verifylist = [ ('private', False), @@ -642,7 +666,7 @@ class TestImageSet(TestImage): ('volume', 'volly'), ('force', False), ('name', 'updated_image'), - ('image', image_fakes.image_name), + ('image', self._image.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -653,13 +677,13 @@ class TestImageSet(TestImage): volumes_mock.upload_to_image.assert_called_with( 'vol1', False, - image_fakes.image_name, + self._image.name, '', '', ) # ImageManager.update(image_id, remove_props=, **) self.images_mock.update.assert_called_with( - image_fakes.image_id, + self._image.id, name='updated_image', volume='volly', ) @@ -668,24 +692,46 @@ class TestImageSet(TestImage): class TestImageShow(TestImage): + _image = image_fakes.FakeImage.create_one_image() + columns = ( + 'container_format', + 'disk_format', + 'id', + 'is_public', + 'min_disk', + 'min_ram', + 'name', + 'owner', + 'properties', + 'protected', + ) + data = ( + _image.container_format, + _image.disk_format, + _image.id, + _image.is_public, + _image.min_disk, + _image.min_ram, + _image.name, + _image.owner, + utils.format_dict(_image.properties), + _image.protected, + ) + def setUp(self): super(TestImageShow, self).setUp() - self.images_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) + self.images_mock.get.return_value = self._image # Get the command object to test self.cmd = image.ShowImage(self.app, None) def test_image_show(self): arglist = [ - image_fakes.image_id, + self._image.id, ] verifylist = [ - ('image', image_fakes.image_id), + ('image', self._image.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -694,8 +740,8 @@ class TestImageShow(TestImage): # data to be shown. columns, data = self.cmd.take_action(parsed_args) self.images_mock.get.assert_called_with( - image_fakes.image_id, + self._image.id, ) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index ebc9c3a7..164185df 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -130,7 +130,7 @@ class TestImageCreate(TestImage): self.images_mock.configure_mock(**mock_exception) arglist = [ '--container-format', 'ovf', - '--disk-format', 'fs', + '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', ('--protected' @@ -143,7 +143,7 @@ class TestImageCreate(TestImage): ] verifylist = [ ('container_format', 'ovf'), - ('disk_format', 'fs'), + ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), ('protected', self.new_image.protected), @@ -165,7 +165,7 @@ class TestImageCreate(TestImage): self.images_mock.create.assert_called_with( name=self.new_image.name, container_format='ovf', - disk_format='fs', + disk_format='ami', min_disk=10, min_ram=4, owner=self.project.id, @@ -193,7 +193,7 @@ class TestImageCreate(TestImage): arglist = [ '--container-format', 'ovf', - '--disk-format', 'fs', + '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', '--owner', 'unexist_owner', @@ -203,7 +203,7 @@ class TestImageCreate(TestImage): ] verifylist = [ ('container_format', 'ovf'), - ('disk_format', 'fs'), + ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), ('owner', 'unexist_owner'), @@ -227,7 +227,7 @@ class TestImageCreate(TestImage): arglist = [ '--container-format', 'ovf', - '--disk-format', 'fs', + '--disk-format', 'ami', '--min-disk', '10', '--min-ram', '4', '--protected', @@ -237,7 +237,7 @@ class TestImageCreate(TestImage): ] verifylist = [ ('container_format', 'ovf'), - ('disk_format', 'fs'), + ('disk_format', 'ami'), ('min_disk', 10), ('min_ram', 4), ('protected', True), @@ -535,7 +535,9 @@ class TestImageList(TestImage): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with() + self.api_mock.image_list.assert_called_with( + marker=self._image.id, + ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) @@ -558,6 +560,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( public=True, + marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -581,6 +584,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( private=True, + marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -604,6 +608,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( shared=True, + marker=self._image.id, ) self.assertEqual(self.columns, columns) @@ -622,7 +627,9 @@ class TestImageList(TestImage): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with() + self.api_mock.image_list.assert_called_with( + marker=self._image.id, + ) collist = ( 'ID', @@ -670,7 +677,9 @@ class TestImageList(TestImage): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with() + self.api_mock.image_list.assert_called_with( + marker=self._image.id, + ) sf_mock.assert_called_with( [self._image], attr='a', @@ -693,7 +702,9 @@ class TestImageList(TestImage): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.api_mock.image_list.assert_called_with() + self.api_mock.image_list.assert_called_with( + marker=self._image.id, + ) si_mock.assert_called_with( [self._image], 'name:asc' @@ -712,7 +723,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( - limit=1, + limit=1, marker=self._image.id ) self.assertEqual(self.columns, columns) @@ -818,6 +829,11 @@ 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) + + self.app.client_manager.auth_ref = mock.Mock( + project_id=self.project.id, + ) + # Get the command object to test self.cmd = image.SetImage(self.app, None) @@ -834,6 +850,101 @@ class TestImageSet(TestImage): self.assertIsNone(result) + self.image_members_mock.update.assert_not_called() + + def test_image_set_membership_option_accept(self): + membership = image_fakes.FakeImage.create_one_image_member( + attrs={'image_id': image_fakes.image_id, + 'member_id': self.project.id} + ) + self.image_members_mock.update.return_value = membership + + arglist = [ + '--accept', + image_fakes.image_id, + ] + verifylist = [ + ('accept', True), + ('reject', False), + ('pending', False), + ('image', image_fakes.image_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.image_members_mock.update.assert_called_once_with( + image_fakes.image_id, + self.app.client_manager.auth_ref.project_id, + 'accepted', + ) + + # Assert that the 'update image" route is also called, in addition to + # the 'update membership' route. + self.images_mock.update.assert_called_with(image_fakes.image_id) + + def test_image_set_membership_option_reject(self): + membership = image_fakes.FakeImage.create_one_image_member( + attrs={'image_id': image_fakes.image_id, + 'member_id': self.project.id} + ) + self.image_members_mock.update.return_value = membership + + arglist = [ + '--reject', + image_fakes.image_id, + ] + verifylist = [ + ('accept', False), + ('reject', True), + ('pending', False), + ('image', image_fakes.image_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.image_members_mock.update.assert_called_once_with( + image_fakes.image_id, + self.app.client_manager.auth_ref.project_id, + 'rejected', + ) + + # Assert that the 'update image" route is also called, in addition to + # the 'update membership' route. + self.images_mock.update.assert_called_with(image_fakes.image_id) + + def test_image_set_membership_option_pending(self): + membership = image_fakes.FakeImage.create_one_image_member( + attrs={'image_id': image_fakes.image_id, + 'member_id': self.project.id} + ) + self.image_members_mock.update.return_value = membership + + arglist = [ + '--pending', + image_fakes.image_id, + ] + verifylist = [ + ('accept', False), + ('reject', False), + ('pending', True), + ('image', image_fakes.image_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.image_members_mock.update.assert_called_once_with( + image_fakes.image_id, + self.app.client_manager.auth_ref.project_id, + 'pending', + ) + + # Assert that the 'update image" route is also called, in addition to + # the 'update membership' route. + self.images_mock.update.assert_called_with(image_fakes.image_id) + def test_image_set_options(self): arglist = [ '--name', 'new-name', diff --git a/openstackclient/tests/unit/network/test_common.py b/openstackclient/tests/unit/network/test_common.py index 325aad2a..4b9a754b 100644 --- a/openstackclient/tests/unit/network/test_common.py +++ b/openstackclient/tests/unit/network/test_common.py @@ -14,6 +14,8 @@ import argparse import mock +import openstack +from openstackclient.common import exceptions from openstackclient.network import common from openstackclient.tests.unit import utils @@ -172,3 +174,15 @@ class TestNetworkAndComputeShowOne(TestNetworkAndCompute): def setUp(self): super(TestNetworkAndComputeShowOne, self).setUp() self.cmd = FakeNetworkAndComputeShowOne(self.app, self.namespace) + + def test_take_action_with_http_exception(self): + with mock.patch.object(self.cmd, 'take_action_network') as m_action: + m_action.side_effect = openstack.exceptions.HttpException("bar") + self.assertRaisesRegex(exceptions.CommandError, "bar", + self.cmd.take_action, mock.Mock()) + + self.app.client_manager.network_endpoint_enabled = False + with mock.patch.object(self.cmd, 'take_action_compute') as m_action: + m_action.side_effect = openstack.exceptions.HttpException("bar") + self.assertRaisesRegex(exceptions.CommandError, "bar", + self.cmd.take_action, mock.Mock()) diff --git a/openstackclient/tests/unit/network/test_sdk_utils.py b/openstackclient/tests/unit/network/test_sdk_utils.py new file mode 100644 index 00000000..d1efa7e4 --- /dev/null +++ b/openstackclient/tests/unit/network/test_sdk_utils.py @@ -0,0 +1,59 @@ +# 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.network import sdk_utils +from openstackclient.tests.unit import utils as tests_utils + + +class TestSDKUtils(tests_utils.TestCase): + + def setUp(self): + super(TestSDKUtils, self).setUp() + + def _test_get_osc_show_columns_for_sdk_resource( + self, sdk_resource, column_map, + expected_display_columns, expected_attr_columns): + display_columns, attr_columns = \ + sdk_utils.get_osc_show_columns_for_sdk_resource( + sdk_resource, column_map) + self.assertEqual(expected_display_columns, display_columns) + self.assertEqual(expected_attr_columns, attr_columns) + + def test_get_osc_show_columns_for_sdk_resource_empty(self): + self._test_get_osc_show_columns_for_sdk_resource( + {}, {}, tuple(), tuple()) + + def test_get_osc_show_columns_for_sdk_resource_empty_map(self): + self._test_get_osc_show_columns_for_sdk_resource( + {'foo': 'foo1'}, {}, + ('foo',), ('foo',)) + + def test_get_osc_show_columns_for_sdk_resource_empty_data(self): + self._test_get_osc_show_columns_for_sdk_resource( + {}, {'foo': 'foo_map'}, + ('foo_map',), ('foo_map',)) + + def test_get_osc_show_columns_for_sdk_resource_map(self): + self._test_get_osc_show_columns_for_sdk_resource( + {'foo': 'foo1'}, {'foo': 'foo_map'}, + ('foo_map',), ('foo',)) + + def test_get_osc_show_columns_for_sdk_resource_map_dup(self): + self._test_get_osc_show_columns_for_sdk_resource( + {'foo': 'foo1', 'foo_map': 'foo1'}, {'foo': 'foo_map'}, + ('foo_map',), ('foo',)) + + def test_get_osc_show_columns_for_sdk_resource_map_full(self): + self._test_get_osc_show_columns_for_sdk_resource( + {'foo': 'foo1', 'bar': 'bar1'}, + {'foo': 'foo_map', 'new': 'bar'}, + ('bar', 'foo_map'), ('bar', 'foo')) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index cea00282..d3685409 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -14,6 +14,8 @@ import argparse import copy import mock +from random import choice +from random import randint import uuid from openstackclient.tests.unit import fakes @@ -37,10 +39,20 @@ QUOTA = { "l7policy": 5, } +RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth-limit' +RULE_TYPE_DSCP_MARKING = 'dscp-marking' +RULE_TYPE_MINIMUM_BANDWIDTH = 'minimum-bandwidth' +VALID_QOS_RULES = [RULE_TYPE_BANDWIDTH_LIMIT, + RULE_TYPE_DSCP_MARKING, + RULE_TYPE_MINIMUM_BANDWIDTH] +VALID_DSCP_MARKS = [0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, + 34, 36, 38, 40, 46, 48, 56] + class FakeNetworkV2Client(object): def __init__(self, **kwargs): + self.session = mock.Mock() self.extensions = mock.Mock() self.extensions.resource_class = fakes.FakeResource(None, {}) @@ -59,6 +71,10 @@ class TestNetworkV2(utils.TestCommand): token=fakes.AUTH_TOKEN, ) + self.app.client_manager.sdk_connection = mock.Mock() + self.app.client_manager.sdk_connection.network = \ + self.app.client_manager.network + self.app.client_manager.identity = ( identity_fakes_v3.FakeIdentityv3Client( endpoint=fakes.AUTH_URL, @@ -98,6 +114,7 @@ class FakeAddressScope(object): loaded=True) # Set attributes with special mapping in OpenStack SDK. + address_scope.is_shared = address_scope_attrs['shared'] address_scope.project_id = address_scope_attrs['tenant_id'] return address_scope @@ -140,6 +157,31 @@ class FakeAddressScope(object): return mock.Mock(side_effect=address_scopes) +class FakeAutoAllocatedTopology(object): + """Fake Auto Allocated Topology""" + + @staticmethod + def create_one_topology(attrs=None): + attrs = attrs or {} + + auto_allocated_topology_attrs = { + 'id': 'network-id-' + uuid.uuid4().hex, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + } + + auto_allocated_topology_attrs.update(attrs) + + auto_allocated_topology = fakes.FakeResource( + info=copy.deepcopy(auto_allocated_topology_attrs), + loaded=True) + + auto_allocated_topology.project_id = auto_allocated_topology_attrs[ + 'tenant_id' + ] + + return auto_allocated_topology + + class FakeAvailabilityZone(object): """Fake one or more network availability zones (AZs).""" @@ -193,15 +235,18 @@ class FakeIPAvailability(object): """Fake one or more network ip availabilities.""" @staticmethod - def create_one_ip_availability(): + def create_one_ip_availability(attrs=None): """Create a fake list with ip availability stats of a network. + :param Dictionary attrs: + A dictionary with all attributes :return: A FakeResource object with network_name, network_id, etc. """ + attrs = attrs or {} # Set default attributes. - network_ip_availability = { + network_ip_attrs = { 'network_id': 'network-id-' + uuid.uuid4().hex, 'network_name': 'network-name-' + uuid.uuid4().hex, 'tenant_id': '', @@ -209,10 +254,13 @@ class FakeIPAvailability(object): 'total_ips': 254, 'used_ips': 6, } + network_ip_attrs.update(attrs) network_ip_availability = fakes.FakeResource( - info=copy.deepcopy(network_ip_availability), + info=copy.deepcopy(network_ip_attrs), loaded=True) + network_ip_availability.project_id = network_ip_attrs['tenant_id'] + return network_ip_availability @staticmethod @@ -290,12 +338,17 @@ class FakeNetwork(object): 'admin_state_up': True, 'shared': False, 'subnets': ['a', 'b'], - 'provider_network_type': 'vlan', + 'provider:network_type': 'vlan', + 'provider:physical_network': 'physnet1', + 'provider:segmentation_id': "400", 'router:external': True, 'availability_zones': [], 'availability_zone_hints': [], 'is_default': False, 'port_security_enabled': True, + 'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, + 'ipv4_address_scope': 'ipv4' + uuid.uuid4().hex, + 'ipv6_address_scope': 'ipv6' + uuid.uuid4().hex, } # Overwrite default attributes. @@ -307,8 +360,21 @@ class FakeNetwork(object): # Set attributes with special mapping in OpenStack SDK. network.project_id = network_attrs['tenant_id'] network.is_router_external = network_attrs['router:external'] + network.is_admin_state_up = network_attrs['admin_state_up'] network.is_port_security_enabled = \ network_attrs['port_security_enabled'] + network.subnet_ids = network_attrs['subnets'] + network.is_shared = network_attrs['shared'] + network.provider_network_type = \ + network_attrs['provider:network_type'] + network.provider_physical_network = \ + network_attrs['provider:physical_network'] + network.provider_segmentation_id = \ + network_attrs['provider:segmentation_id'] + network.ipv4_address_scope_id = \ + network_attrs['ipv4_address_scope'] + network.ipv6_address_scope_id = \ + network_attrs['ipv6_address_scope'] return network @@ -349,6 +415,69 @@ class FakeNetwork(object): return mock.Mock(side_effect=networks) +class FakeNetworkFlavor(object): + """Fake Network Flavor.""" + + @staticmethod + def create_one_network_flavor(attrs=None): + """Create a fake network flavor. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the network flavor + """ + attrs = attrs or {} + + fake_uuid = uuid.uuid4().hex + network_flavor_attrs = { + 'description': 'network-flavor-description-' + fake_uuid, + 'enabled': True, + 'id': 'network-flavor-id-' + fake_uuid, + 'name': 'network-flavor-name-' + fake_uuid, + 'service_type': 'vpn', + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + network_flavor_attrs.update(attrs) + + network_flavor = fakes.FakeResource( + info=copy.deepcopy(network_flavor_attrs), + loaded=True + ) + + network_flavor.project_id = network_flavor_attrs['tenant_id'] + network_flavor.is_enabled = network_flavor_attrs['enabled'] + + return network_flavor + + @staticmethod + def create_flavor(attrs=None, count=2): + """Create multiple fake network flavors. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of network flavors to fake + :return: + A list of FakeResource objects faking the network falvors + """ + network_flavors = [] + for i in range(0, count): + network_flavors.append( + FakeNetworkFlavor.create_one_network_flavor(attrs) + ) + return network_flavors + + @staticmethod + def get_flavor(network_flavors=None, count=2): + """Get a list of flavors.""" + if network_flavors is None: + network_flavors = (FakeNetworkFlavor.create_flavor(count)) + return mock.Mock(side_effect=network_flavors) + + class FakeNetworkSegment(object): """Fake one or more network segments.""" @@ -427,18 +556,20 @@ class FakePort(object): 'binding:vif_details': {}, 'binding:vif_type': 'ovs', 'binding:vnic_type': 'normal', + 'description': 'description-' + uuid.uuid4().hex, 'device_id': 'device-id-' + uuid.uuid4().hex, 'device_owner': 'compute:nova', 'dns_assignment': [{}], 'dns_name': 'dns-name-' + uuid.uuid4().hex, 'extra_dhcp_opts': [{}], - 'fixed_ips': [{}], + 'fixed_ips': [{'ip_address': '10.0.0.3', + 'subnet_id': 'subnet-id-' + uuid.uuid4().hex}], 'id': 'port-id-' + uuid.uuid4().hex, 'mac_address': 'fa:16:3e:a9:4e:72', 'name': 'port-name-' + uuid.uuid4().hex, 'network_id': 'network-id-' + uuid.uuid4().hex, 'port_security_enabled': True, - 'security_groups': [], + 'security_group_ids': [], 'status': 'ACTIVE', 'tenant_id': 'project-id-' + uuid.uuid4().hex, } @@ -450,12 +581,15 @@ class FakePort(object): loaded=True) # Set attributes with special mappings in OpenStack SDK. - port.project_id = port_attrs['tenant_id'] port.binding_host_id = port_attrs['binding:host_id'] port.binding_profile = port_attrs['binding:profile'] port.binding_vif_details = port_attrs['binding:vif_details'] port.binding_vif_type = port_attrs['binding:vif_type'] port.binding_vnic_type = port_attrs['binding:vnic_type'] + port.is_admin_state_up = port_attrs['admin_state_up'] + port.is_port_security_enabled = port_attrs['port_security_enabled'] + port.project_id = port_attrs['tenant_id'] + port.security_group_ids = port_attrs['security_group_ids'] return port @@ -524,6 +658,8 @@ class FakeNetworkAgent(object): agent_attrs.update(attrs) agent = fakes.FakeResource(info=copy.deepcopy(agent_attrs), loaded=True) + agent.is_admin_state_up = agent_attrs['admin_state_up'] + agent.is_alive = agent_attrs['alive'] return agent @staticmethod @@ -633,6 +769,254 @@ class FakeNetworkRBAC(object): return mock.Mock(side_effect=rbac_policies) +class FakeNetworkFlavorProfile(object): + """Fake network flavor profile.""" + + @staticmethod + def create_one_service_profile(attrs=None): + """Create flavor profile.""" + attrs = attrs or {} + + flavor_profile_attrs = { + 'id': 'flavor-profile-id' + uuid.uuid4().hex, + 'description': 'flavor-profile-description-' + uuid.uuid4().hex, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + 'driver': 'driver-' + uuid.uuid4().hex, + 'metainfo': 'metainfo-' + uuid.uuid4().hex, + 'enabled': True + } + + flavor_profile_attrs.update(attrs) + + flavor_profile = fakes.FakeResource( + info=copy.deepcopy(flavor_profile_attrs), + loaded=True) + + flavor_profile.project_id = flavor_profile_attrs['tenant_id'] + flavor_profile.is_enabled = flavor_profile_attrs['enabled'] + + return flavor_profile + + @staticmethod + def create_service_profile(attrs=None, count=2): + """Create multiple flavor profiles.""" + + flavor_profiles = [] + for i in range(0, count): + flavor_profiles.append(FakeNetworkFlavorProfile. + create_one_service_profile(attrs)) + return flavor_profiles + + @staticmethod + def get_service_profile(flavor_profile=None, count=2): + """Get a list of flavor profiles.""" + if flavor_profile is None: + flavor_profile = (FakeNetworkFlavorProfile. + create_service_profile(count)) + return mock.Mock(side_effect=flavor_profile) + + +class FakeNetworkQosPolicy(object): + """Fake one or more QoS policies.""" + + @staticmethod + def create_one_qos_policy(attrs=None): + """Create a fake QoS policy. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with name, id, etc. + """ + attrs = attrs or {} + qos_id = attrs.get('id') or 'qos-policy-id-' + uuid.uuid4().hex + rule_attrs = {'qos_policy_id': qos_id} + rules = [FakeNetworkQosRule.create_one_qos_rule(rule_attrs)] + + # Set default attributes. + qos_policy_attrs = { + 'name': 'qos-policy-name-' + uuid.uuid4().hex, + 'id': qos_id, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + 'shared': False, + 'description': 'qos-policy-description-' + uuid.uuid4().hex, + 'rules': rules, + } + + # Overwrite default attributes. + qos_policy_attrs.update(attrs) + + qos_policy = fakes.FakeResource( + info=copy.deepcopy(qos_policy_attrs), + loaded=True) + + # Set attributes with special mapping in OpenStack SDK. + qos_policy.is_shared = qos_policy_attrs['shared'] + qos_policy.project_id = qos_policy_attrs['tenant_id'] + + return qos_policy + + @staticmethod + def create_qos_policies(attrs=None, count=2): + """Create multiple fake QoS policies. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of QoS policies to fake + :return: + A list of FakeResource objects faking the QoS policies + """ + qos_policies = [] + for i in range(0, count): + qos_policies.append( + FakeNetworkQosPolicy.create_one_qos_policy(attrs)) + + return qos_policies + + @staticmethod + def get_qos_policies(qos_policies=None, count=2): + """Get an iterable MagicMock object with a list of faked QoS policies. + + If qos policies list is provided, then initialize the Mock object + with the list. Otherwise create one. + + :param List address scopes: + A list of FakeResource objects faking qos policies + :param int count: + The number of QoS policies to fake + :return: + An iterable Mock object with side_effect set to a list of faked + QoS policies + """ + if qos_policies is None: + qos_policies = FakeNetworkQosPolicy.create_qos_policies(count) + return mock.Mock(side_effect=qos_policies) + + +class FakeNetworkQosRule(object): + """Fake one or more Network QoS rules.""" + + @staticmethod + def create_one_qos_rule(attrs=None): + """Create a fake Network QoS rule. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with name, id, etc. + """ + attrs = attrs or {} + + # Set default attributes. + type = attrs.get('type') or choice(VALID_QOS_RULES) + qos_rule_attrs = { + 'id': 'qos-rule-id-' + uuid.uuid4().hex, + 'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + 'type': type, + } + if type == RULE_TYPE_BANDWIDTH_LIMIT: + qos_rule_attrs['max_kbps'] = randint(1, 10000) + qos_rule_attrs['max_burst_kbits'] = randint(1, 10000) + elif type == RULE_TYPE_DSCP_MARKING: + qos_rule_attrs['dscp_mark'] = choice(VALID_DSCP_MARKS) + elif type == RULE_TYPE_MINIMUM_BANDWIDTH: + qos_rule_attrs['min_kbps'] = randint(1, 10000) + qos_rule_attrs['direction'] = 'egress' + + # Overwrite default attributes. + qos_rule_attrs.update(attrs) + + qos_rule = fakes.FakeResource(info=copy.deepcopy(qos_rule_attrs), + loaded=True) + + # Set attributes with special mapping in OpenStack SDK. + qos_rule.project_id = qos_rule['tenant_id'] + + return qos_rule + + @staticmethod + def create_qos_rules(attrs=None, count=2): + """Create multiple fake Network QoS rules. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of Network QoS rule to fake + :return: + A list of FakeResource objects faking the Network QoS rules + """ + qos_rules = [] + for i in range(0, count): + qos_rules.append(FakeNetworkQosRule.create_one_qos_rule(attrs)) + return qos_rules + + @staticmethod + def get_qos_rules(qos_rules=None, count=2): + """Get a list of faked Network QoS rules. + + If Network QoS rules list is provided, then initialize the Mock + object with the list. Otherwise create one. + + :param List address scopes: + A list of FakeResource objects faking Network QoS rules + :param int count: + The number of QoS minimum bandwidth rules to fake + :return: + An iterable Mock object with side_effect set to a list of faked + qos minimum bandwidth rules + """ + if qos_rules is None: + qos_rules = (FakeNetworkQosRule.create_qos_rules(count)) + return mock.Mock(side_effect=qos_rules) + + +class FakeNetworkQosRuleType(object): + """Fake one or more Network QoS rule types.""" + + @staticmethod + def create_one_qos_rule_type(attrs=None): + """Create a fake Network QoS rule type. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with name, id, etc. + """ + attrs = attrs or {} + + # Set default attributes. + qos_rule_type_attrs = { + 'type': 'rule-type-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + qos_rule_type_attrs.update(attrs) + + return fakes.FakeResource( + info=copy.deepcopy(qos_rule_type_attrs), + loaded=True) + + @staticmethod + def create_qos_rule_types(attrs=None, count=2): + """Create multiple fake Network QoS rule types. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of QoS rule types to fake + :return: + A list of FakeResource objects faking the QoS rule types + """ + qos_rule_types = [] + for i in range(0, count): + qos_rule_types.append( + FakeNetworkQosRuleType.create_one_qos_rule_type(attrs)) + + return qos_rule_types + + class FakeRouter(object): """Fake one or more routers.""" @@ -672,6 +1056,9 @@ class FakeRouter(object): # Set attributes with special mapping in OpenStack SDK. router.project_id = router_attrs['tenant_id'] + router.is_admin_state_up = router_attrs['admin_state_up'] + router.is_distributed = router_attrs['distributed'] + router.is_ha = router_attrs['ha'] return router @@ -731,7 +1118,7 @@ class FakeSecurityGroup(object): 'id': 'security-group-id-' + uuid.uuid4().hex, 'name': 'security-group-name-' + uuid.uuid4().hex, 'description': 'security-group-description-' + uuid.uuid4().hex, - 'tenant_id': 'project-id-' + uuid.uuid4().hex, + 'project_id': 'project-id-' + uuid.uuid4().hex, 'security_group_rules': [], } @@ -743,7 +1130,7 @@ class FakeSecurityGroup(object): loaded=True) # Set attributes with special mapping in OpenStack SDK. - security_group.project_id = security_group_attrs['tenant_id'] + security_group.project_id = security_group_attrs['project_id'] return security_group @@ -801,8 +1188,10 @@ class FakeSecurityGroupRule(object): # Set default attributes. security_group_rule_attrs = { + 'description': 'security-group-rule-description-' + + uuid.uuid4().hex, 'direction': 'ingress', - 'ethertype': 'IPv4', + 'ether_type': 'IPv4', 'id': 'security-group-rule-id-' + uuid.uuid4().hex, 'port_range_max': None, 'port_range_min': None, @@ -907,6 +1296,8 @@ class FakeSubnet(object): loaded=True) # Set attributes with special mappings in OpenStack SDK. + subnet.is_dhcp_enabled = subnet_attrs['enable_dhcp'] + subnet.subnet_pool_id = subnet_attrs['subnetpool_id'] subnet.project_id = subnet_attrs['tenant_id'] return subnet @@ -1026,6 +1417,97 @@ class FakeFloatingIP(object): return mock.Mock(side_effect=floating_ips) +class FakeNetworkMeter(object): + """Fake network meter""" + + @staticmethod + def create_one_meter(attrs=None): + """Create metering pool""" + attrs = attrs or {} + + meter_attrs = { + 'id': 'meter-id-' + uuid.uuid4().hex, + 'name': 'meter-name-' + uuid.uuid4().hex, + 'description': 'meter-description-' + uuid.uuid4().hex, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + 'shared': False + } + + meter_attrs.update(attrs) + + meter = fakes.FakeResource( + info=copy.deepcopy(meter_attrs), + loaded=True) + + meter.project_id = meter_attrs['tenant_id'] + + return meter + + @staticmethod + def create_meter(attrs=None, count=2): + """Create multiple meters""" + + meters = [] + for i in range(0, count): + meters.append(FakeNetworkMeter. + create_one_meter(attrs)) + return meters + + @staticmethod + def get_meter(meter=None, count=2): + """Get a list of meters""" + if meter is None: + meter = (FakeNetworkMeter. + create_meter(count)) + return mock.Mock(side_effect=meter) + + +class FakeNetworkMeterRule(object): + """Fake metering rule""" + + @staticmethod + def create_one_rule(attrs=None): + """Create one meter rule""" + attrs = attrs or {} + + meter_rule_attrs = { + 'id': 'meter-label-rule-id-' + uuid.uuid4().hex, + 'direction': 'ingress', + 'excluded': False, + 'metering_label_id': 'meter-label-id-' + uuid.uuid4().hex, + 'remote_ip_prefix': '10.0.0.0/24', + 'tenant_id': 'project-id-' + uuid.uuid4().hex, + } + + meter_rule_attrs.update(attrs) + + meter_rule = fakes.FakeResource( + info=copy.deepcopy(meter_rule_attrs), + loaded=True) + + meter_rule.project_id = meter_rule_attrs['tenant_id'] + + return meter_rule + + @staticmethod + def create_meter_rule(attrs=None, count=2): + """Create multiple meter rules""" + + meter_rules = [] + for i in range(0, count): + meter_rules.append(FakeNetworkMeterRule. + create_one_rule(attrs)) + return meter_rules + + @staticmethod + def get_meter_rule(meter_rule=None, count=2): + """Get a list of meter rules""" + if meter_rule is None: + meter_rule = (FakeNetworkMeterRule. + create_meter_rule(count)) + return mock.Mock(side_effect=meter_rule) + + class FakeSubnetPool(object): """Fake one or more subnet pools.""" @@ -1066,6 +1548,11 @@ class FakeSubnetPool(object): ) # Set attributes with special mapping in OpenStack SDK. + subnet_pool.default_prefix_length = \ + subnet_pool_attrs['default_prefixlen'] + subnet_pool.is_shared = subnet_pool_attrs['shared'] + subnet_pool.maximum_prefix_length = subnet_pool_attrs['max_prefixlen'] + subnet_pool.minimum_prefix_length = subnet_pool_attrs['min_prefixlen'] subnet_pool.project_id = subnet_pool_attrs['tenant_id'] return subnet_pool @@ -1107,3 +1594,88 @@ class FakeSubnetPool(object): if subnet_pools is None: subnet_pools = FakeSubnetPool.create_subnet_pools(count) return mock.Mock(side_effect=subnet_pools) + + +class FakeNetworkServiceProvider(object): + """Fake Network Service Providers""" + + @staticmethod + def create_one_network_service_provider(attrs=None): + """Create service provider""" + attrs = attrs or {} + + service_provider = { + 'name': 'provider-name-' + uuid.uuid4().hex, + 'service_type': 'service-type-' + uuid.uuid4().hex, + 'default': False, + } + + service_provider.update(attrs) + + provider = fakes.FakeResource( + info=copy.deepcopy(service_provider), + loaded=True) + provider.is_default = service_provider['default'] + + return provider + + @staticmethod + def create_network_service_providers(attrs=None, count=2): + """Create multiple service providers""" + + service_providers = [] + for i in range(0, count): + service_providers.append(FakeNetworkServiceProvider. + create_one_network_service_provider( + attrs)) + return service_providers + + +class FakeQuota(object): + """Fake quota""" + + @staticmethod + def create_one_net_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'floating_ips': 20, + 'networks': 25, + 'ports': 11, + 'rbac_policies': 15, + 'routers': 40, + 'security_groups': 10, + 'security_group_rules': 100, + 'subnets': 20, + 'subnet_pools': 30} + + quota_attrs.update(attrs) + + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + return quota + + @staticmethod + def create_one_default_net_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'floatingip': 30, + 'network': 20, + 'port': 10, + 'rbac_policy': 25, + 'router': 30, + 'security_group': 30, + 'security_group_rule': 200, + 'subnet': 10, + 'subnetpool': 20} + + quota_attrs.update(attrs) + + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + return quota diff --git a/openstackclient/tests/unit/network/v2/test_address_scope.py b/openstackclient/tests/unit/network/v2/test_address_scope.py index 12c3f1d6..40067188 100644 --- a/openstackclient/tests/unit/network/v2/test_address_scope.py +++ b/openstackclient/tests/unit/network/v2/test_address_scope.py @@ -275,6 +275,104 @@ class TestListAddressScope(TestAddressScope): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_address_scope_list_name(self): + arglist = [ + '--name', self.address_scopes[0].name, + ] + verifylist = [ + ('name', self.address_scopes[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.address_scopes.assert_called_once_with( + **{'name': self.address_scopes[0].name}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_address_scope_list_ip_version(self): + arglist = [ + '--ip-version', str(4), + ] + verifylist = [ + ('ip_version', 4), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.address_scopes.assert_called_once_with( + **{'ip_version': 4}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_address_scope_list_project(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.address_scopes.assert_called_once_with( + **{'tenant_id': project.id, 'project_id': project.id}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_address_scope_project_domain(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + '--project-domain', project.domain_id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id, 'project_id': project.id} + + self.network.address_scopes.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_address_scope_list_share(self): + arglist = [ + '--share', + ] + verifylist = [ + ('share', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.address_scopes.assert_called_once_with( + **{'is_shared': True} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_address_scope_list_no_share(self): + arglist = [ + '--no-share', + ] + verifylist = [ + ('no_share', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.address_scopes.assert_called_once_with( + **{'is_shared': False} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestSetAddressScope(TestAddressScope): diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip.py b/openstackclient/tests/unit/network/v2/test_floating_ip.py index 1f30f2e9..e395300d 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip.py @@ -18,6 +18,7 @@ from osc_lib import exceptions from openstackclient.network.v2 import floating_ip from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -31,6 +32,10 @@ class TestFloatingIPNetwork(network_fakes.TestNetworkV2): # Get a shortcut to the network client self.network = self.app.client_manager.network + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains class TestCreateFloatingIPNetwork(TestFloatingIPNetwork): @@ -145,6 +150,54 @@ class TestCreateFloatingIPNetwork(TestFloatingIPNetwork): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_floating_ip_create_project(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + self.floating_ip.floating_network_id, + ] + verifylist = [ + ('network', self.floating_ip.floating_network_id), + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_ip.assert_called_once_with(**{ + 'floating_network_id': self.floating_ip.floating_network_id, + 'tenant_id': project.id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_floating_ip_create_project_domain(self): + project = identity_fakes_v3.FakeProject.create_one_project() + domain = identity_fakes_v3.FakeDomain.create_one_domain() + self.projects_mock.get.return_value = project + arglist = [ + "--project", project.name, + "--project-domain", domain.name, + self.floating_ip.floating_network_id, + ] + verifylist = [ + ('network', self.floating_ip.floating_network_id), + ('project', project.name), + ('project_domain', domain.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_ip.assert_called_once_with(**{ + 'floating_network_id': self.floating_ip.floating_network_id, + 'tenant_id': project.id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): @@ -155,13 +208,19 @@ class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): super(TestDeleteFloatingIPNetwork, self).setUp() self.network.delete_ip = mock.Mock(return_value=None) - self.network.find_ip = ( - network_fakes.FakeFloatingIP.get_floating_ips(self.floating_ips)) # Get the command object to test self.cmd = floating_ip.DeleteFloatingIP(self.app, self.namespace) - def test_floating_ip_delete(self): + @mock.patch( + "openstackclient.tests.unit.network.v2.test_floating_ip." + + "floating_ip._find_floating_ip" + ) + def test_floating_ip_delete(self, find_floating_ip_mock): + find_floating_ip_mock.side_effect = [ + (self.floating_ips[0], []), + (self.floating_ips[1], []), + ] arglist = [ self.floating_ips[0].id, ] @@ -172,12 +231,24 @@ class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): result = self.cmd.take_action(parsed_args) - self.network.find_ip.assert_called_once_with( - self.floating_ips[0].id, ignore_missing=False) + find_floating_ip_mock.assert_called_once_with( + mock.ANY, + [], + self.floating_ips[0].id, + ignore_missing=False, + ) self.network.delete_ip.assert_called_once_with(self.floating_ips[0]) self.assertIsNone(result) - def test_multi_floating_ips_delete(self): + @mock.patch( + "openstackclient.tests.unit.network.v2.test_floating_ip." + + "floating_ip._find_floating_ip" + ) + def test_floating_ip_delete_multi(self, find_floating_ip_mock): + find_floating_ip_mock.side_effect = [ + (self.floating_ips[0], []), + (self.floating_ips[1], []), + ] arglist = [] verifylist = [] @@ -190,13 +261,37 @@ class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): result = self.cmd.take_action(parsed_args) + calls = [ + call( + mock.ANY, + [], + self.floating_ips[0].id, + ignore_missing=False, + ), + call( + mock.ANY, + [], + self.floating_ips[1].id, + ignore_missing=False, + ), + ] + find_floating_ip_mock.assert_has_calls(calls) + calls = [] for f in self.floating_ips: calls.append(call(f)) self.network.delete_ip.assert_has_calls(calls) self.assertIsNone(result) - def test_multi_floating_ips_delete_with_exception(self): + @mock.patch( + "openstackclient.tests.unit.network.v2.test_floating_ip." + + "floating_ip._find_floating_ip" + ) + def test_floating_ip_delete_multi_exception(self, find_floating_ip_mock): + find_floating_ip_mock.side_effect = [ + (self.floating_ips[0], []), + exceptions.CommandError, + ] arglist = [ self.floating_ips[0].id, 'unexist_floating_ip', @@ -207,21 +302,24 @@ class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.floating_ips[0], exceptions.CommandError] - self.network.find_ip = ( - mock.Mock(side_effect=find_mock_result) - ) - try: self.cmd.take_action(parsed_args) self.fail('CommandError should be raised.') except exceptions.CommandError as e: self.assertEqual('1 of 2 floating_ips failed to delete.', str(e)) - self.network.find_ip.assert_any_call( - self.floating_ips[0].id, ignore_missing=False) - self.network.find_ip.assert_any_call( - 'unexist_floating_ip', ignore_missing=False) + find_floating_ip_mock.assert_any_call( + mock.ANY, + [], + self.floating_ips[0].id, + ignore_missing=False, + ) + find_floating_ip_mock.assert_any_call( + mock.ANY, + [], + 'unexist_floating_ip', + ignore_missing=False, + ) self.network.delete_ip.assert_called_once_with( self.floating_ips[0] ) @@ -231,27 +329,60 @@ class TestListFloatingIPNetwork(TestFloatingIPNetwork): # The floating ips to list up floating_ips = network_fakes.FakeFloatingIP.create_floating_ips(count=3) + fake_network = network_fakes.FakeNetwork.create_one_network({ + 'id': 'fake_network_id', + }) + fake_port = network_fakes.FakePort.create_one_port({ + 'id': 'fake_port_id', + }) + fake_router = network_fakes.FakeRouter.create_one_router({ + 'id': 'fake_router_id', + }) columns = ( 'ID', 'Floating IP Address', 'Fixed IP Address', 'Port', + 'Floating Network', + 'Project', + ) + columns_long = columns + ( + 'Router', + 'Status', + 'Description', ) data = [] + data_long = [] for ip in floating_ips: data.append(( ip.id, ip.floating_ip_address, ip.fixed_ip_address, ip.port_id, + ip.floating_network_id, + ip.tenant_id, + )) + data_long.append(( + ip.id, + ip.floating_ip_address, + ip.fixed_ip_address, + ip.port_id, + ip.floating_network_id, + ip.tenant_id, + ip.router_id, + ip.status, + ip.description, )) def setUp(self): super(TestListFloatingIPNetwork, self).setUp() self.network.ips = mock.Mock(return_value=self.floating_ips) + self.network.find_network = mock.Mock(return_value=self.fake_network) + self.network.find_port = mock.Mock(return_value=self.fake_port) + self.network.find_router = mock.Mock(return_value=self.fake_router) # Get the command object to test self.cmd = floating_ip.ListFloatingIP(self.app, self.namespace) @@ -263,10 +394,149 @@ class TestListFloatingIPNetwork(TestFloatingIPNetwork): columns, data = self.cmd.take_action(parsed_args) - self.network.ips.assert_called_once_with(**{}) + self.network.ips.assert_called_once_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_floating_ip_list_network(self): + arglist = [ + '--network', 'fake_network_id', + ] + verifylist = [ + ('network', 'fake_network_id'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ips.assert_called_once_with(**{ + 'floating_network_id': 'fake_network_id', + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_floating_ip_list_port(self): + arglist = [ + '--port', 'fake_port_id', + ] + verifylist = [ + ('port', 'fake_port_id'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ips.assert_called_once_with(**{ + 'port_id': 'fake_port_id', + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_floating_ip_list_fixed_ip_address(self): + arglist = [ + '--fixed-ip-address', self.floating_ips[0].fixed_ip_address, + ] + verifylist = [ + ('fixed_ip_address', self.floating_ips[0].fixed_ip_address), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ips.assert_called_once_with(**{ + 'fixed_ip_address': self.floating_ips[0].fixed_ip_address, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_floating_ip_list_long(self): + arglist = ['--long', ] + verifylist = [('long', True), ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ips.assert_called_once_with() + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + + def test_floating_ip_list_status(self): + arglist = [ + '--status', 'ACTIVE', + '--long', + ] + verifylist = [ + ('status', 'ACTIVE'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ips.assert_called_once_with(**{ + 'status': 'ACTIVE', + }) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + + def test_floating_ip_list_project(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id, + 'project_id': project.id, } + + self.network.ips.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_floating_ip_list_project_domain(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + '--project-domain', project.domain_id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id, + 'project_id': project.id, } + + self.network.ips.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_floating_ip_list_router(self): + arglist = [ + '--router', 'fake_router_id', + '--long', + ] + verifylist = [ + ('router', 'fake_router_id'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ips.assert_called_once_with(**{ + 'router_id': 'fake_router_id', + }) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + class TestShowFloatingIPNetwork(TestFloatingIPNetwork): @@ -296,7 +566,7 @@ class TestShowFloatingIPNetwork(TestFloatingIPNetwork): floating_ip.floating_network_id, floating_ip.id, floating_ip.port_id, - floating_ip.tenant_id, + floating_ip.project_id, floating_ip.router_id, floating_ip.status, ) @@ -309,7 +579,12 @@ class TestShowFloatingIPNetwork(TestFloatingIPNetwork): # Get the command object to test self.cmd = floating_ip.ShowFloatingIP(self.app, self.namespace) - def test_floating_ip_show(self): + @mock.patch( + "openstackclient.tests.unit.network.v2.test_floating_ip." + + "floating_ip._find_floating_ip" + ) + def test_floating_ip_show(self, find_floating_ip_mock): + find_floating_ip_mock.return_value = (self.floating_ip, []) arglist = [ self.floating_ip.id, ] @@ -320,9 +595,11 @@ class TestShowFloatingIPNetwork(TestFloatingIPNetwork): columns, data = self.cmd.take_action(parsed_args) - self.network.find_ip.assert_called_once_with( + find_floating_ip_mock.assert_called_once_with( + mock.ANY, + [], self.floating_ip.id, - ignore_missing=False + ignore_missing=False, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_ip_availability.py b/openstackclient/tests/unit/network/v2/test_ip_availability.py index c929ab82..c7c5a9b4 100644 --- a/openstackclient/tests/unit/network/v2/test_ip_availability.py +++ b/openstackclient/tests/unit/network/v2/test_ip_availability.py @@ -107,6 +107,7 @@ class TestListIPAvailability(TestIPAvailability): columns, data = self.cmd.take_action(parsed_args) filters = {'tenant_id': self.project.id, + 'project_id': self.project.id, 'ip_version': 4} self.network.network_ip_availabilities.assert_called_once_with( @@ -117,8 +118,10 @@ class TestListIPAvailability(TestIPAvailability): class TestShowIPAvailability(TestIPAvailability): + _network = network_fakes.FakeNetwork.create_one_network() _ip_availability = \ - network_fakes.FakeIPAvailability.create_one_ip_availability() + network_fakes.FakeIPAvailability.create_one_ip_availability( + attrs={'network_id': _network.id}) columns = ( 'network_id', @@ -143,6 +146,8 @@ class TestShowIPAvailability(TestIPAvailability): self.network.find_network_ip_availability = mock.Mock( return_value=self._ip_availability) + self.network.find_network = mock.Mock( + return_value=self._network) # Get the command object to test self.cmd = ip_availability.ShowIPAvailability( @@ -165,8 +170,10 @@ class TestShowIPAvailability(TestIPAvailability): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.network.find_network_ip_availability.assert_called_once_with( + self._ip_availability.network_id, + ignore_missing=False) + self.network.find_network.assert_called_once_with( self._ip_availability.network_name, ignore_missing=False) - self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index 50a60c2d..bc1279ec 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -53,6 +53,8 @@ class TestCreateNetworkIdentityV3(TestNetwork): 'availability_zone_hints': ["nova"], } ) + qos_policy = (network_fakes.FakeNetworkQosPolicy. + create_one_qos_policy(attrs={'id': _network.qos_policy_id})) columns = ( 'admin_state_up', @@ -60,11 +62,16 @@ class TestCreateNetworkIdentityV3(TestNetwork): 'availability_zones', 'description', 'id', + 'ipv4_address_scope', + 'ipv6_address_scope', 'is_default', 'name', 'port_security_enabled', 'project_id', - 'provider_network_type', + 'provider:network_type', + 'provider:physical_network', + 'provider:segmentation_id', + 'qos_policy_id', 'router:external', 'shared', 'status', @@ -77,11 +84,16 @@ class TestCreateNetworkIdentityV3(TestNetwork): utils.format_list(_network.availability_zones), _network.description, _network.id, + _network.ipv4_address_scope_id, + _network.ipv6_address_scope_id, _network.is_default, _network.name, _network.is_port_security_enabled, _network.project_id, _network.provider_network_type, + _network.provider_physical_network, + _network.provider_segmentation_id, + _network.qos_policy_id, network._format_router_external(_network.is_router_external), _network.shared, _network.status, @@ -98,6 +110,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): self.projects_mock.get.return_value = self.project self.domains_mock.get.return_value = self.domain + self.network.find_qos_policy = mock.Mock(return_value=self.qos_policy) def test_create_no_options(self): arglist = [] @@ -140,6 +153,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): "--provider-network-type", "vlan", "--provider-physical-network", "physnet1", "--provider-segment", "400", + "--qos-policy", self.qos_policy.id, "--transparent-vlan", "--enable-port-security", self._network.name, @@ -156,6 +170,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): ('provider_network_type', 'vlan'), ('physical_network', 'physnet1'), ('segmentation_id', '400'), + ('qos_policy', self.qos_policy.id), ('transparent_vlan', True), ('enable_port_security', True), ('name', self._network.name), @@ -170,12 +185,15 @@ class TestCreateNetworkIdentityV3(TestNetwork): 'name': self._network.name, 'shared': True, 'description': self._network.description, + # TODO(dtroyer): Remove tenant_id when we clean up the SDK refactor 'tenant_id': self.project.id, + 'project_id': self.project.id, 'is_default': True, 'router:external': True, 'provider:network_type': 'vlan', 'provider:physical_network': 'physnet1', 'provider:segmentation_id': '400', + 'qos_policy_id': self.qos_policy.id, 'vlan_transparent': True, 'port_security_enabled': True, }) @@ -224,11 +242,16 @@ class TestCreateNetworkIdentityV2(TestNetwork): 'availability_zones', 'description', 'id', + 'ipv4_address_scope', + 'ipv6_address_scope', 'is_default', 'name', 'port_security_enabled', 'project_id', - 'provider_network_type', + 'provider:network_type', + 'provider:physical_network', + 'provider:segmentation_id', + 'qos_policy_id', 'router:external', 'shared', 'status', @@ -241,11 +264,16 @@ class TestCreateNetworkIdentityV2(TestNetwork): utils.format_list(_network.availability_zones), _network.description, _network.id, + _network.ipv4_address_scope_id, + _network.ipv6_address_scope_id, _network.is_default, _network.name, _network.is_port_security_enabled, _network.project_id, _network.provider_network_type, + _network.provider_physical_network, + _network.provider_segmentation_id, + _network.qos_policy_id, network._format_router_external(_network.is_router_external), _network.shared, _network.status, @@ -293,7 +321,9 @@ class TestCreateNetworkIdentityV2(TestNetwork): self.network.create_network.assert_called_once_with(**{ 'admin_state_up': True, 'name': self._network.name, + # TODO(dtroyer): Remove tenant_id when we clean up the SDK refactor 'tenant_id': self.project.id, + 'project_id': self.project.id, }) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -461,6 +491,13 @@ class TestListNetwork(TestNetwork): self.network.networks = mock.Mock(return_value=self._network) + self._agent = \ + network_fakes.FakeNetworkAgent.create_one_network_agent() + self.network.get_agent = mock.Mock(return_value=self._agent) + + self.network.dhcp_agent_hosting_networks = mock.Mock( + return_value=self._network) + def test_network_list_no_options(self): arglist = [] verifylist = [ @@ -494,7 +531,7 @@ class TestListNetwork(TestNetwork): columns, data = self.cmd.take_action(parsed_args) self.network.networks.assert_called_once_with( - **{'router:external': True} + **{'router:external': True, 'is_router_external': True} ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -511,7 +548,7 @@ class TestListNetwork(TestNetwork): columns, data = self.cmd.take_action(parsed_args) self.network.networks.assert_called_once_with( - **{'router:external': False} + **{'router:external': False, 'is_router_external': False} ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -567,7 +604,7 @@ class TestListNetwork(TestNetwork): columns, data = self.cmd.take_action(parsed_args) self.network.networks.assert_called_once_with( - **{'admin_state_up': True} + **{'admin_state_up': True, 'is_admin_state_up': True} ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -585,7 +622,7 @@ class TestListNetwork(TestNetwork): columns, data = self.cmd.take_action(parsed_args) self.network.networks.assert_called_once_with( - **{'admin_state_up': False} + **{'admin_state_up': False, 'is_admin_state_up': False} ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -603,13 +640,13 @@ class TestListNetwork(TestNetwork): columns, data = self.cmd.take_action(parsed_args) self.network.networks.assert_called_once_with( - **{'tenant_id': project.id} + **{'tenant_id': project.id, 'project_id': project.id} ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) - def test_networ_list_project_domain(self): + def test_network_list_project_domain(self): project = identity_fakes_v3.FakeProject.create_one_project() self.projects_mock.get.return_value = project arglist = [ @@ -622,9 +659,11 @@ class TestListNetwork(TestNetwork): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id} + filters = {'tenant_id': project.id, 'project_id': project.id} self.network.networks.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) def test_network_list_share(self): arglist = [ @@ -638,7 +677,7 @@ class TestListNetwork(TestNetwork): columns, data = self.cmd.take_action(parsed_args) self.network.networks.assert_called_once_with( - **{'shared': True} + **{'shared': True, 'is_shared': True} ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -655,7 +694,7 @@ class TestListNetwork(TestNetwork): columns, data = self.cmd.take_action(parsed_args) self.network.networks.assert_called_once_with( - **{'shared': False} + **{'shared': False, 'is_shared': False} ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -679,11 +718,86 @@ class TestListNetwork(TestNetwork): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_network_list_provider_network_type(self): + network_type = self._network[0].provider_network_type + arglist = [ + '--provider-network-type', network_type, + ] + verifylist = [ + ('provider_network_type', network_type), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.networks.assert_called_once_with( + **{'provider:network_type': network_type, + 'provider_network_type': network_type} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_network_list_provider_physical_network(self): + physical_network = self._network[0].provider_physical_network + arglist = [ + '--provider-physical-network', physical_network, + ] + verifylist = [ + ('physical_network', physical_network), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.networks.assert_called_once_with( + **{'provider:physical_network': physical_network, + 'provider_physical_network': physical_network} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_network_list_provider_segment(self): + segmentation_id = self._network[0].provider_segmentation_id + arglist = [ + '--provider-segment', segmentation_id, + ] + verifylist = [ + ('segmentation_id', segmentation_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.networks.assert_called_once_with( + **{'provider:segmentation_id': segmentation_id, + 'provider_segmentation_id': segmentation_id} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_network_list_dhcp_agent(self): + arglist = [ + '--agent', self._agent.id + ] + verifylist = [ + ('agent_id', self._agent.id), + ] + + attrs = {self._agent, } + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.dhcp_agent_hosting_networks.assert_called_once_with( + *attrs) + + self.assertEqual(self.columns, columns) + self.assertEqual(list(data), list(self.data)) + class TestSetNetwork(TestNetwork): # The network to set. _network = network_fakes.FakeNetwork.create_one_network() + qos_policy = (network_fakes.FakeNetworkQosPolicy. + create_one_qos_policy(attrs={'id': _network.qos_policy_id})) def setUp(self): super(TestSetNetwork, self).setUp() @@ -691,6 +805,7 @@ class TestSetNetwork(TestNetwork): self.network.update_network = mock.Mock(return_value=None) self.network.find_network = mock.Mock(return_value=self._network) + self.network.find_qos_policy = mock.Mock(return_value=self.qos_policy) # Get the command object to test self.cmd = network.SetNetwork(self.app, self.namespace) @@ -709,6 +824,7 @@ class TestSetNetwork(TestNetwork): '--provider-segment', '400', '--no-transparent-vlan', '--enable-port-security', + '--qos-policy', self.qos_policy.name, ] verifylist = [ ('network', self._network.name), @@ -723,6 +839,7 @@ class TestSetNetwork(TestNetwork): ('segmentation_id', '400'), ('no_transparent_vlan', True), ('enable_port_security', True), + ('qos_policy', self.qos_policy.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -740,6 +857,7 @@ class TestSetNetwork(TestNetwork): 'provider:segmentation_id': '400', 'vlan_transparent': False, 'port_security_enabled': True, + 'qos_policy_id': self.qos_policy.id, } self.network.update_network.assert_called_once_with( self._network, **attrs) @@ -752,6 +870,7 @@ class TestSetNetwork(TestNetwork): '--no-share', '--internal', '--disable-port-security', + '--no-qos-policy', ] verifylist = [ ('network', self._network.name), @@ -759,6 +878,7 @@ class TestSetNetwork(TestNetwork): ('no_share', True), ('internal', True), ('disable_port_security', True), + ('no_qos_policy', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -769,6 +889,7 @@ class TestSetNetwork(TestNetwork): 'shared': False, 'router:external': False, 'port_security_enabled': False, + 'qos_policy_id': None, } self.network.update_network.assert_called_once_with( self._network, **attrs) @@ -798,11 +919,16 @@ class TestShowNetwork(TestNetwork): 'availability_zones', 'description', 'id', + 'ipv4_address_scope', + 'ipv6_address_scope', 'is_default', 'name', 'port_security_enabled', 'project_id', - 'provider_network_type', + 'provider:network_type', + 'provider:physical_network', + 'provider:segmentation_id', + 'qos_policy_id', 'router:external', 'shared', 'status', @@ -815,11 +941,16 @@ class TestShowNetwork(TestNetwork): utils.format_list(_network.availability_zones), _network.description, _network.id, + _network.ipv4_address_scope_id, + _network.ipv6_address_scope_id, _network.is_default, _network.name, _network.is_port_security_enabled, _network.project_id, _network.provider_network_type, + _network.provider_physical_network, + _network.provider_segmentation_id, + _network.qos_policy_id, network._format_router_external(_network.is_router_external), _network.shared, _network.status, @@ -1109,10 +1240,7 @@ class TestListNetworkCompute(TestNetworkCompute): def test_network_list_no_options(self): arglist = [] - verifylist = [ - ('external', False), - ('long', False), - ] + verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # In base command class Lister in cliff, abstract method take_action() diff --git a/openstackclient/tests/unit/network/v2/test_network_agent.py b/openstackclient/tests/unit/network/v2/test_network_agent.py index 9f5b442a..0d741e06 100644 --- a/openstackclient/tests/unit/network/v2/test_network_agent.py +++ b/openstackclient/tests/unit/network/v2/test_network_agent.py @@ -31,6 +31,48 @@ class TestNetworkAgent(network_fakes.TestNetworkV2): self.network = self.app.client_manager.network +class TestAddNetworkToAgent(TestNetworkAgent): + + net = network_fakes.FakeNetwork.create_one_network() + agent = network_fakes.FakeNetworkAgent.create_one_network_agent() + + def setUp(self): + super(TestAddNetworkToAgent, self).setUp() + + self.network.get_agent = mock.Mock(return_value=self.agent) + self.network.find_network = mock.Mock(return_value=self.net) + self.network.name = self.network.find_network.name + self.network.add_dhcp_agent_to_network = mock.Mock() + self.cmd = network_agent.AddNetworkToAgent( + self.app, self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_add_network_to_dhcp_agent(self): + arglist = [ + '--dhcp', + self.agent.id, + self.net.id + ] + verifylist = [ + ('dhcp', True), + ('agent_id', self.agent.id), + ('network', self.net.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.network.add_dhcp_agent_to_network.assert_called_once_with( + self.agent, self.net) + + class TestDeleteNetworkAgent(TestNetworkAgent): network_agents = ( @@ -66,7 +108,6 @@ class TestDeleteNetworkAgent(TestNetworkAgent): def test_multi_network_agents_delete(self): arglist = [] - verifylist = [] for n in self.network_agents: arglist.append(n.id) @@ -130,6 +171,7 @@ class TestListNetworkAgent(TestNetworkAgent): ) data = [] for agent in network_agents: + agent.agent_type = 'DHCP agent' data.append(( agent.id, agent.agent_type, @@ -140,11 +182,37 @@ class TestListNetworkAgent(TestNetworkAgent): agent.binary, )) + network_agent_columns = ( + 'ID', + 'Host', + 'Admin State Up', + 'Alive', + ) + + network_agent_data = [] + + for agent in network_agents: + network_agent_data.append(( + agent.id, + agent.host, + network_agent._format_admin_state(agent.admin_state_up), + agent.alive, + )) + def setUp(self): super(TestListNetworkAgent, self).setUp() self.network.agents = mock.Mock( return_value=self.network_agents) + _testagent = \ + network_fakes.FakeNetworkAgent.create_one_network_agent() + self.network.get_agent = mock.Mock(return_value=_testagent) + + self._testnetwork = network_fakes.FakeNetwork.create_one_network() + self.network.find_network = mock.Mock(return_value=self._testnetwork) + self.network.network_hosting_dhcp_agents = mock.Mock( + return_value=self.network_agents) + # Get the command object to test self.cmd = network_agent.ListNetworkAgent(self.app, self.namespace) @@ -159,7 +227,103 @@ class TestListNetworkAgent(TestNetworkAgent): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_network_agents_list_agent_type(self): + arglist = [ + '--agent-type', 'dhcp', + ] + verifylist = [ + ('agent_type', 'dhcp'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.agents.assert_called_once_with(**{ + 'agent_type': self.network_agents[0].agent_type, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_network_agents_list_host(self): + arglist = [ + '--host', self.network_agents[0].host, + ] + verifylist = [ + ('host', self.network_agents[0].host), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.agents.assert_called_once_with(**{ + 'host': self.network_agents[0].host, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_network_agents_list_networks(self): + arglist = [ + '--network', self._testnetwork.id, + ] + verifylist = [ + ('network', self._testnetwork.id), + ] + + attrs = {self._testnetwork, } + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.network_hosting_dhcp_agents.assert_called_once_with( + *attrs) + self.assertEqual(self.network_agent_columns, columns) + self.assertEqual(list(self.network_agent_data), list(data)) + + +class TestRemoveNetworkFromAgent(TestNetworkAgent): + + net = network_fakes.FakeNetwork.create_one_network() + agent = network_fakes.FakeNetworkAgent.create_one_network_agent() + + def setUp(self): + super(TestRemoveNetworkFromAgent, self).setUp() + + self.network.get_agent = mock.Mock(return_value=self.agent) + self.network.find_network = mock.Mock(return_value=self.net) + self.network.name = self.network.find_network.name + self.network.remove_dhcp_agent_from_network = mock.Mock() + self.cmd = network_agent.RemoveNetworkFromAgent( + self.app, self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_network_from_dhcp_agent(self): + arglist = [ + '--dhcp', + self.agent.id, + self.net.id + ] + verifylist = [ + ('dhcp', True), + ('agent_id', self.agent.id), + ('network', self.net.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.network.remove_dhcp_agent_from_network.assert_called_once_with( + self.agent, self.net) + +# TODO(huanxuan): Also update by the new attribute name +# "is_admin_state_up" after sdk 0.9.12 class TestSetNetworkAgent(TestNetworkAgent): _network_agent = ( @@ -289,6 +453,6 @@ class TestShowNetworkAgent(TestNetworkAgent): columns, data = self.cmd.take_action(parsed_args) self.network.get_agent.assert_called_once_with( - self._network_agent.id, ignore_missing=False) + self._network_agent.id) self.assertEqual(self.columns, columns) self.assertEqual(list(self.data), list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py b/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py new file mode 100644 index 00000000..1a231160 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py @@ -0,0 +1,267 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 mock + +from openstackclient.network.v2 import network_auto_allocated_topology +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.tests.unit.network.v2 import fakes as network_fakes + + +class TestAutoAllocatedTopology(network_fakes.TestNetworkV2): + def setUp(self): + super(TestAutoAllocatedTopology, self).setUp() + self.network = self.app.client_manager.network + self.projects_mock = self.app.client_manager.identity.projects + + +class TestCreateAutoAllocatedTopology(TestAutoAllocatedTopology): + project = identity_fakes.FakeProject.create_one_project() + network_object = network_fakes.FakeNetwork.create_one_network() + + topology = network_fakes.FakeAutoAllocatedTopology.create_one_topology( + attrs={'id': network_object.id, + 'tenant_id': project.id} + ) + + columns = ( + 'id', + 'project_id', + ) + + data = ( + network_object.id, + project.id, + ) + + def setUp(self): + super(TestCreateAutoAllocatedTopology, self).setUp() + + self.cmd = network_auto_allocated_topology.CreateAutoAllocatedTopology( + self.app, + self.namespace) + self.network.get_auto_allocated_topology = mock.Mock( + return_value=self.topology) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.network.get_auto_allocated_topology.assert_called_with(None) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_project_option(self): + arglist = [ + '--project', self.project.id, + ] + + verifylist = [ + ('project', self.project.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.network.get_auto_allocated_topology.assert_called_with( + self.project.id + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_project_domain_option(self): + arglist = [ + '--project', self.project.id, + '--project-domain', self.project.domain_id, + ] + + verifylist = [ + ('project', self.project.id), + ('project_domain', self.project.domain_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.network.get_auto_allocated_topology.assert_called_with( + self.project.id + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_or_show_option(self): + arglist = [ + '--or-show', + ] + + verifylist = [ + ('or_show', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.network.get_auto_allocated_topology.assert_called_with(None) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestValidateAutoAllocatedTopology(TestAutoAllocatedTopology): + project = identity_fakes.FakeProject.create_one_project() + network_object = network_fakes.FakeNetwork.create_one_network() + + topology = network_fakes.FakeAutoAllocatedTopology.create_one_topology( + attrs={'id': network_object.id, + 'tenant_id': project.id} + ) + + columns = ( + 'id', + 'project_id', + ) + + data = ( + network_object.id, + project.id, + ) + + def setUp(self): + super(TestValidateAutoAllocatedTopology, self).setUp() + + self.cmd = network_auto_allocated_topology.CreateAutoAllocatedTopology( + self.app, + self.namespace) + self.network.validate_auto_allocated_topology = mock.Mock( + return_value=self.topology) + + def test_show_dry_run_no_project(self): + arglist = [ + '--check-resources', + ] + verifylist = [ + ('check_resources', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.validate_auto_allocated_topology.assert_called_with( + None) + + def test_show_dry_run_project_option(self): + arglist = [ + '--check-resources', + '--project', self.project.id, + ] + verifylist = [ + ('check_resources', True), + ('project', self.project.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.validate_auto_allocated_topology.assert_called_with( + self.project.id) + + def test_show_dry_run_project_domain_option(self): + arglist = [ + '--check-resources', + '--project', self.project.id, + '--project-domain', self.project.domain_id, + ] + verifylist = [ + ('check_resources', True), + ('project', self.project.id), + ('project_domain', self.project.domain_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.validate_auto_allocated_topology.assert_called_with( + self.project.id) + + +class TestDeleteAutoAllocatedTopology(TestAutoAllocatedTopology): + project = identity_fakes.FakeProject.create_one_project() + network_object = network_fakes.FakeNetwork.create_one_network() + + topology = network_fakes.FakeAutoAllocatedTopology.create_one_topology( + attrs={'id': network_object.id, + 'tenant_id': project.id} + ) + + def setUp(self): + super(TestDeleteAutoAllocatedTopology, self).setUp() + + self.cmd = network_auto_allocated_topology.DeleteAutoAllocatedTopology( + self.app, + self.namespace) + self.network.delete_auto_allocated_topology = mock.Mock( + return_value=None) + + def test_delete_no_project(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.delete_auto_allocated_topology.assert_called_once_with( + None) + + self.assertIsNone(result) + + def test_delete_project_arg(self): + arglist = [ + '--project', self.project.id, + ] + verifylist = [ + ('project', self.project.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.delete_auto_allocated_topology.assert_called_once_with( + self.project.id) + + self.assertIsNone(result) + + def test_delete_project_domain_arg(self): + arglist = [ + '--project', self.project.id, + '--project-domain', self.project.domain_id, + ] + verifylist = [ + ('project', self.project.id), + ('project_domain', self.project.domain_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.delete_auto_allocated_topology.assert_called_once_with( + self.project.id) + + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/test_network_flavor.py b/openstackclient/tests/unit/network/v2/test_network_flavor.py new file mode 100644 index 00000000..11e27841 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_flavor.py @@ -0,0 +1,407 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 mock + +from osc_lib import exceptions + +from openstackclient.network.v2 import network_flavor +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestNetworkFlavor(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestNetworkFlavor, self).setUp() + + # Get a shortcut to the network client + self.network = self.app.client_manager.network + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + + +class TestCreateNetworkFlavor(TestNetworkFlavor): + + project = identity_fakes_v3.FakeProject.create_one_project() + domain = identity_fakes_v3.FakeDomain.create_one_domain() + # The new network flavor created. + new_network_flavor = ( + network_fakes.FakeNetworkFlavor.create_one_network_flavor()) + columns = ( + 'description', + 'enabled', + 'id', + 'name', + 'project_id', + 'service_type' + ) + data = ( + new_network_flavor.description, + new_network_flavor.enabled, + new_network_flavor.id, + new_network_flavor.name, + new_network_flavor.project_id, + new_network_flavor.service_type, + ) + + def setUp(self): + super(TestCreateNetworkFlavor, self).setUp() + self.network.create_flavor = mock.Mock( + return_value=self.new_network_flavor) + + # Get the command object to test + self.cmd = network_flavor.CreateNetworkFlavor(self.app, self.namespace) + + self.projects_mock.get.return_value = self.project + self.domains_mock.get.return_value = self.domain + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + '--service-type', self.new_network_flavor.service_type, + self.new_network_flavor.name, + ] + verifylist = [ + ('service_type', self.new_network_flavor.service_type), + ('name', self.new_network_flavor.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_flavor.assert_called_once_with(**{ + 'service_type': self.new_network_flavor.service_type, + 'name': self.new_network_flavor.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + '--description', self.new_network_flavor.description, + '--enable', + '--project', self.new_network_flavor.project_id, + '--project-domain', self.domain.name, + '--service-type', self.new_network_flavor.service_type, + self.new_network_flavor.name, + ] + verifylist = [ + ('description', self.new_network_flavor.description), + ('enable', True), + ('project', self.new_network_flavor.project_id), + ('project_domain', self.domain.name), + ('service_type', self.new_network_flavor.service_type), + ('name', self.new_network_flavor.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_flavor.assert_called_once_with(**{ + 'description': self.new_network_flavor.description, + 'enabled': True, + 'tenant_id': self.project.id, + 'service_type': self.new_network_flavor.service_type, + 'name': self.new_network_flavor.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_disable(self): + arglist = [ + '--disable', + '--service-type', self.new_network_flavor.service_type, + self.new_network_flavor.name, + ] + verifylist = [ + ('disable', True), + ('service_type', self.new_network_flavor.service_type), + ('name', self.new_network_flavor.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_flavor.assert_called_once_with(**{ + 'enabled': False, + 'service_type': self.new_network_flavor.service_type, + 'name': self.new_network_flavor.name, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteNetworkFlavor(TestNetworkFlavor): + + # The network flavor to delete. + _network_flavors = ( + network_fakes.FakeNetworkFlavor.create_flavor(count=2)) + + def setUp(self): + super(TestDeleteNetworkFlavor, self).setUp() + self.network.delete_flavor = mock.Mock(return_value=None) + self.network.find_flavor = ( + network_fakes.FakeNetworkFlavor.get_flavor( + network_flavors=self._network_flavors) + ) + + # Get the command object to test + self.cmd = network_flavor.DeleteNetworkFlavor(self.app, self.namespace) + + def test_network_flavor_delete(self): + arglist = [ + self._network_flavors[0].name, + ] + verifylist = [ + ('flavor', [self._network_flavors[0].name]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.find_flavor.assert_called_once_with( + self._network_flavors[0].name, ignore_missing=False) + self.network.delete_flavor.assert_called_once_with( + self._network_flavors[0]) + self.assertIsNone(result) + + def test_multi_network_flavors_delete(self): + arglist = [] + verifylist = [] + + for a in self._network_flavors: + arglist.append(a.name) + verifylist = [ + ('flavor', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [] + for a in self._network_flavors: + calls.append(mock.call(a)) + self.network.delete_flavor.assert_has_calls(calls) + self.assertIsNone(result) + + def test_multi_network_flavors_delete_with_exception(self): + arglist = [ + self._network_flavors[0].name, + 'unexist_network_flavor', + ] + verifylist = [ + ('flavor', + [self._network_flavors[0].name, 'unexist_network_flavor']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self._network_flavors[0], exceptions.CommandError] + self.network.find_flavor = ( + mock.Mock(side_effect=find_mock_result) + ) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 flavors failed to delete.', str(e)) + + self.network.find_flavor.assert_any_call( + self._network_flavors[0].name, ignore_missing=False) + self.network.find_flavor.assert_any_call( + 'unexist_network_flavor', ignore_missing=False) + self.network.delete_flavor.assert_called_once_with( + self._network_flavors[0] + ) + + +class TestListNetworkFlavor(TestNetworkFlavor): + + # The network flavors to list up. + _network_flavors = ( + network_fakes.FakeNetworkFlavor.create_flavor(count=2)) + columns = ( + 'ID', + 'Name', + 'Enabled', + 'Service Type', + 'Description', + ) + data = [] + for flavor in _network_flavors: + data.append(( + flavor.id, + flavor.name, + flavor.enabled, + flavor.service_type, + flavor.description, + )) + + def setUp(self): + super(TestListNetworkFlavor, self).setUp() + self.network.flavors = mock.Mock( + return_value=self._network_flavors) + + # Get the command object to test + self.cmd = network_flavor.ListNetworkFlavor(self.app, self.namespace) + + def test_network_flavor_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.flavors.assert_called_once_with(**{}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestShowNetworkFlavor(TestNetworkFlavor): + + # The network flavor to show. + new_network_flavor = ( + network_fakes.FakeNetworkFlavor.create_one_network_flavor()) + columns = ( + 'description', + 'enabled', + 'id', + 'name', + 'project_id', + 'service_type' + ) + data = ( + new_network_flavor.description, + new_network_flavor.enabled, + new_network_flavor.id, + new_network_flavor.name, + new_network_flavor.project_id, + new_network_flavor.service_type, + ) + + def setUp(self): + super(TestShowNetworkFlavor, self).setUp() + self.network.find_flavor = mock.Mock( + return_value=self.new_network_flavor) + + # Get the command object to test + self.cmd = network_flavor.ShowNetworkFlavor(self.app, self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_all_options(self): + arglist = [ + self.new_network_flavor.name, + ] + verifylist = [ + ('flavor', self.new_network_flavor.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_flavor.assert_called_once_with( + self.new_network_flavor.name, ignore_missing=False) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestSetNetworkFlavor(TestNetworkFlavor): + + # The network flavor to set. + new_network_flavor = ( + network_fakes.FakeNetworkFlavor.create_one_network_flavor()) + + def setUp(self): + super(TestSetNetworkFlavor, self).setUp() + self.network.update_flavor = mock.Mock(return_value=None) + self.network.find_flavor = mock.Mock( + return_value=self.new_network_flavor) + + # Get the command object to test + self.cmd = network_flavor.SetNetworkFlavor(self.app, self.namespace) + + def test_set_nothing(self): + arglist = [self.new_network_flavor.name, ] + verifylist = [ + ('flavor', self.new_network_flavor.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = {} + self.network.update_flavor.assert_called_with( + self.new_network_flavor, **attrs) + self.assertIsNone(result) + + def test_set_name_and_enable(self): + arglist = [ + '--name', 'new_network_flavor', + '--enable', + self.new_network_flavor.name, + ] + verifylist = [ + ('name', 'new_network_flavor'), + ('enable', True), + ('flavor', self.new_network_flavor.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'name': "new_network_flavor", + 'enabled': True, + } + self.network.update_flavor.assert_called_with( + self.new_network_flavor, **attrs) + self.assertIsNone(result) + + def test_set_disable(self): + arglist = [ + '--disable', + self.new_network_flavor.name, + ] + verifylist = [ + ('disable', True), + ('flavor', self.new_network_flavor.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'enabled': False, + } + self.network.update_flavor.assert_called_with( + self.new_network_flavor, **attrs) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py b/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py new file mode 100644 index 00000000..91683241 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py @@ -0,0 +1,448 @@ +# 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 mock + +from osc_lib import exceptions + +from openstackclient.network.v2 import network_flavor_profile +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.unit.network.v2 import fakes as network_fakes + + +class TestFlavorProfile(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestFlavorProfile, self).setUp() + # Get the network client + self.network = self.app.client_manager.network + # Get the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + # Get the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + + +class TestCreateFlavorProfile(TestFlavorProfile): + project = identity_fakes_v3.FakeProject.create_one_project() + domain = identity_fakes_v3.FakeDomain.create_one_domain() + new_flavor_profile = ( + network_fakes.FakeNetworkFlavorProfile. + create_one_service_profile() + ) + columns = ( + 'description', + 'driver', + 'enabled', + 'id', + 'metainfo', + 'project_id', + ) + + data = ( + new_flavor_profile.description, + new_flavor_profile.driver, + new_flavor_profile.enabled, + new_flavor_profile.id, + new_flavor_profile.metainfo, + new_flavor_profile.project_id, + ) + + def setUp(self): + super(TestCreateFlavorProfile, self).setUp() + self.network.create_service_profile = mock.Mock( + return_value=self.new_flavor_profile) + self.projects_mock.get.return_value = self.project + # Get the command object to test + self.cmd = (network_flavor_profile.CreateNetworkFlavorProfile( + self.app, self.namespace)) + + def test_create_all_options(self): + arglist = [ + "--description", self.new_flavor_profile.description, + "--project", self.new_flavor_profile.project_id, + '--project-domain', self.domain.name, + "--enable", + "--driver", self.new_flavor_profile.driver, + "--metainfo", self.new_flavor_profile.metainfo, + ] + + verifylist = [ + ('description', self.new_flavor_profile.description), + ('project', self.new_flavor_profile.project_id), + ('project_domain', self.domain.name), + ('enable', True), + ('driver', self.new_flavor_profile.driver), + ('metainfo', self.new_flavor_profile.metainfo) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_service_profile.assert_called_once_with( + **{'description': self.new_flavor_profile.description, + 'tenant_id': self.project.id, + 'enabled': self.new_flavor_profile.enabled, + 'driver': self.new_flavor_profile.driver, + 'metainfo': self.new_flavor_profile.metainfo} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_with_metainfo(self): + arglist = [ + "--description", self.new_flavor_profile.description, + "--project", self.new_flavor_profile.project_id, + '--project-domain', self.domain.name, + "--enable", + "--metainfo", self.new_flavor_profile.metainfo, + ] + + verifylist = [ + ('description', self.new_flavor_profile.description), + ('project', self.new_flavor_profile.project_id), + ('project_domain', self.domain.name), + ('enable', True), + ('metainfo', self.new_flavor_profile.metainfo) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_service_profile.assert_called_once_with( + **{'description': self.new_flavor_profile.description, + 'tenant_id': self.project.id, + 'enabled': self.new_flavor_profile.enabled, + 'metainfo': self.new_flavor_profile.metainfo} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_with_driver(self): + arglist = [ + "--description", self.new_flavor_profile.description, + "--project", self.new_flavor_profile.project_id, + '--project-domain', self.domain.name, + "--enable", + "--driver", self.new_flavor_profile.driver, + ] + + verifylist = [ + ('description', self.new_flavor_profile.description), + ('project', self.new_flavor_profile.project_id), + ('project_domain', self.domain.name), + ('enable', True), + ('driver', self.new_flavor_profile.driver), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_service_profile.assert_called_once_with( + **{'description': self.new_flavor_profile.description, + 'tenant_id': self.project.id, + 'enabled': self.new_flavor_profile.enabled, + 'driver': self.new_flavor_profile.driver, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_without_driver_and_metainfo(self): + arglist = [ + "--description", self.new_flavor_profile.description, + "--project", self.new_flavor_profile.project_id, + '--project-domain', self.domain.name, + "--enable", + ] + + verifylist = [ + ('description', self.new_flavor_profile.description), + ('project', self.new_flavor_profile.project_id), + ('project_domain', self.domain.name), + ('enable', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_create_disable(self): + arglist = [ + '--disable', + '--driver', self.new_flavor_profile.driver, + ] + verifylist = [ + ('disable', True), + ('driver', self.new_flavor_profile.driver) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_service_profile.assert_called_once_with(**{ + 'enabled': False, + 'driver': self.new_flavor_profile.driver, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteFlavorProfile(TestFlavorProfile): + + # The network flavor_profiles to delete. + _network_flavor_profiles = ( + network_fakes.FakeNetworkFlavorProfile.create_service_profile(count=2)) + + def setUp(self): + super(TestDeleteFlavorProfile, self).setUp() + self.network.delete_service_profile = mock.Mock(return_value=None) + self.network.find_service_profile = ( + network_fakes.FakeNetworkFlavorProfile.get_service_profile( + flavor_profile=self._network_flavor_profiles) + ) + + # Get the command object to test + self.cmd = network_flavor_profile.DeleteNetworkFlavorProfile( + self.app, self.namespace) + + def test_network_flavor_profile_delete(self): + arglist = [ + self._network_flavor_profiles[0].id, + ] + verifylist = [ + ('flavor_profile', [self._network_flavor_profiles[0].id]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.find_service_profile.assert_called_once_with( + self._network_flavor_profiles[0].id, ignore_missing=False) + self.network.delete_service_profile.assert_called_once_with( + self._network_flavor_profiles[0]) + self.assertIsNone(result) + + def test_multi_network_flavor_profiles_delete(self): + arglist = [] + + for a in self._network_flavor_profiles: + arglist.append(a.id) + verifylist = [ + ('flavor_profile', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [] + for a in self._network_flavor_profiles: + calls.append(mock.call(a)) + self.network.delete_service_profile.assert_has_calls(calls) + self.assertIsNone(result) + + def test_multi_network_flavor_profiles_delete_with_exception(self): + arglist = [ + self._network_flavor_profiles[0].id, + 'unexist_network_flavor_profile', + ] + verifylist = [ + ('flavor_profile', + [self._network_flavor_profiles[0].id, + 'unexist_network_flavor_profile']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self._network_flavor_profiles[0], + exceptions.CommandError] + self.network.find_service_profile = ( + mock.Mock(side_effect=find_mock_result) + ) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 flavor_profiles failed to delete.', + str(e)) + + self.network.find_service_profile.assert_any_call( + self._network_flavor_profiles[0].id, ignore_missing=False) + self.network.find_service_profile.assert_any_call( + 'unexist_network_flavor_profile', ignore_missing=False) + self.network.delete_service_profile.assert_called_once_with( + self._network_flavor_profiles[0] + ) + + +class TestListFlavorProfile(TestFlavorProfile): + + # The network flavor profiles list + _network_flavor_profiles = ( + network_fakes.FakeNetworkFlavorProfile.create_service_profile(count=2)) + + columns = ( + 'ID', + 'Driver', + 'Enabled', + 'Metainfo', + 'Description', + ) + data = [] + for flavor_profile in _network_flavor_profiles: + data.append(( + flavor_profile.id, + flavor_profile.driver, + flavor_profile.enabled, + flavor_profile.metainfo, + flavor_profile.description, + )) + + def setUp(self): + super(TestListFlavorProfile, self).setUp() + self.network.service_profiles = mock.Mock( + return_value=self._network_flavor_profiles) + + # Get the command object to test + self.cmd = network_flavor_profile.ListNetworkFlavorProfile( + self.app, self.namespace) + + def test_network_flavor_profile_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.service_profiles.assert_called_once_with(**{}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestShowFlavorProfile(TestFlavorProfile): + + # The network flavor profile to show. + network_flavor_profile = ( + network_fakes.FakeNetworkFlavorProfile.create_one_service_profile()) + columns = ( + 'description', + 'driver', + 'enabled', + 'id', + 'metainfo', + 'project_id', + ) + data = ( + network_flavor_profile.description, + network_flavor_profile.driver, + network_flavor_profile.enabled, + network_flavor_profile.id, + network_flavor_profile.metainfo, + network_flavor_profile.project_id, + ) + + def setUp(self): + super(TestShowFlavorProfile, self).setUp() + self.network.find_service_profile = mock.Mock( + return_value=self.network_flavor_profile) + + # Get the command object to test + self.cmd = network_flavor_profile.ShowNetworkFlavorProfile( + self.app, self.namespace) + + def test_show_all_options(self): + arglist = [ + self.network_flavor_profile.id, + ] + verifylist = [ + ('flavor_profile', self.network_flavor_profile.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_service_profile.assert_called_once_with( + self.network_flavor_profile.id, ignore_missing=False) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestSetFlavorProfile(TestFlavorProfile): + + # The network flavor profile to set. + network_flavor_profile = ( + network_fakes.FakeNetworkFlavorProfile.create_one_service_profile()) + + def setUp(self): + super(TestSetFlavorProfile, self).setUp() + self.network.update_service_profile = mock.Mock(return_value=None) + self.network.find_service_profile = mock.Mock( + return_value=self.network_flavor_profile) + + # Get the command object to test + self.cmd = network_flavor_profile.SetNetworkFlavorProfile( + self.app, self.namespace) + + def test_set_nothing(self): + arglist = [self.network_flavor_profile.id] + verifylist = [ + ('flavor_profile', self.network_flavor_profile.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = {} + self.network.update_service_profile.assert_called_with( + self.network_flavor_profile, **attrs) + self.assertIsNone(result) + + def test_set_enable(self): + arglist = [ + '--enable', + self.network_flavor_profile.id, + ] + verifylist = [ + ('enable', True), + ('flavor_profile', self.network_flavor_profile.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'enabled': True, + } + self.network.update_service_profile.assert_called_with( + self.network_flavor_profile, **attrs) + self.assertIsNone(result) + + def test_set_disable(self): + arglist = [ + '--disable', + self.network_flavor_profile.id, + ] + verifylist = [ + ('disable', True), + ('flavor_profile', self.network_flavor_profile.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'enabled': False, + } + self.network.update_service_profile.assert_called_with( + self.network_flavor_profile, **attrs) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/test_network_meter.py b/openstackclient/tests/unit/network/v2/test_network_meter.py new file mode 100644 index 00000000..2b96f7a6 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_meter.py @@ -0,0 +1,304 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 mock +from mock import call + +from osc_lib import exceptions + +from openstackclient.network.v2 import network_meter +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestMeter(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestMeter, self).setUp() + self.network = self.app.client_manager.network + self.projects_mock = self.app.client_manager.identity.projects + self.domains_mock = self.app.client_manager.identity.domains + + +class TestCreateMeter(TestMeter): + project = identity_fakes_v3.FakeProject.create_one_project() + domain = identity_fakes_v3.FakeDomain.create_one_domain() + + new_meter = ( + network_fakes.FakeNetworkMeter. + create_one_meter() + ) + columns = ( + 'description', + 'id', + 'name', + 'project_id', + 'shared', + ) + + data = ( + new_meter.description, + new_meter.id, + new_meter.name, + new_meter.project_id, + new_meter.shared, + ) + + def setUp(self): + super(TestCreateMeter, self).setUp() + self.network.create_metering_label = mock.Mock( + return_value=self.new_meter) + self.projects_mock.get.return_value = self.project + self.cmd = network_meter.CreateMeter(self.app, self.namespace) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + self.new_meter.name, + ] + + verifylist = [ + ('name', self.new_meter.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_metering_label.assert_called_once_with( + **{'name': self.new_meter.name} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + "--description", self.new_meter.description, + "--project", self.new_meter.project_id, + "--project-domain", self.domain.name, + "--share", + self.new_meter.name, + ] + + verifylist = [ + ('description', self.new_meter.description), + ('name', self.new_meter.name), + ('project', self.new_meter.project_id), + ('project_domain', self.domain.name), + ('share', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_metering_label.assert_called_once_with( + **{'description': self.new_meter.description, + 'name': self.new_meter.name, + 'tenant_id': self.project.id, + 'shared': True, } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteMeter(TestMeter): + + def setUp(self): + super(TestDeleteMeter, self).setUp() + + self.meter_list = \ + network_fakes.FakeNetworkMeter.create_meter(count=2) + + self.network.delete_metering_label = mock.Mock(return_value=None) + + self.network.find_metering_label = network_fakes \ + .FakeNetworkMeter.get_meter( + meter=self.meter_list + ) + + self.cmd = network_meter.DeleteMeter(self.app, self.namespace) + + def test_delete_one_meter(self): + arglist = [ + self.meter_list[0].name, + ] + verifylist = [ + ('meter', [self.meter_list[0].name]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.delete_metering_label.assert_called_once_with( + self.meter_list[0] + ) + self.assertIsNone(result) + + def test_delete_multiple_meters(self): + arglist = [] + for n in self.meter_list: + arglist.append(n.id) + verifylist = [ + ('meter', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [] + for n in self.meter_list: + calls.append(call(n)) + self.network.delete_metering_label.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_meter_exception(self): + arglist = [ + self.meter_list[0].id, + 'xxxx-yyyy-zzzz', + self.meter_list[1].id, + ] + verifylist = [ + ('meter', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + return_find = [ + self.meter_list[0], + exceptions.NotFound('404'), + self.meter_list[1], + ] + self.network.find_meter = mock.Mock(side_effect=return_find) + + ret_delete = [ + None, + exceptions.NotFound('404'), + ] + self.network.delete_metering_label = mock.Mock(side_effect=ret_delete) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + calls = [ + call(self.meter_list[0]), + call(self.meter_list[1]), + ] + self.network.delete_metering_label.assert_has_calls(calls) + + +class TestListMeter(TestMeter): + + meter_list = \ + network_fakes.FakeNetworkMeter.create_meter(count=2) + + columns = ( + 'ID', + 'Name', + 'Description', + 'Shared', + ) + + data = [] + + for meters in meter_list: + data.append(( + meters.id, + meters.name, + meters.description, + meters.shared, + )) + + def setUp(self): + super(TestListMeter, self).setUp() + + self.network.metering_labels = mock.Mock( + return_value=self.meter_list + ) + + self.cmd = network_meter.ListMeter(self.app, self.namespace) + + def test_meter_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.metering_labels.assert_called_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestShowMeter(TestMeter): + new_meter = ( + network_fakes.FakeNetworkMeter. + create_one_meter() + ) + columns = ( + 'description', + 'id', + 'name', + 'project_id', + 'shared', + ) + + data = ( + new_meter.description, + new_meter.id, + new_meter.name, + new_meter.project_id, + new_meter.shared, + ) + + def setUp(self): + super(TestShowMeter, self).setUp() + + self.cmd = network_meter.ShowMeter(self.app, self.namespace) + + self.network.find_metering_label = \ + mock.Mock(return_value=self.new_meter) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_meter_show_option(self): + arglist = [ + self.new_meter.name, + ] + verifylist = [ + ('meter', self.new_meter.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_metering_label.assert_called_with( + self.new_meter.name, ignore_missing=False + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_network_meter_rule.py b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py new file mode 100644 index 00000000..af481793 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py @@ -0,0 +1,321 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 mock +from mock import call + +from osc_lib import exceptions + +from openstackclient.network.v2 import network_meter_rule +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestMeterRule(network_fakes.TestNetworkV2): + def setUp(self): + super(TestMeterRule, self).setUp() + self.network = self.app.client_manager.network + self.projects_mock = self.app.client_manager.identity.projects + self.domains_mock = self.app.client_manager.identity.domains + + +class TestCreateMeterRule(TestMeterRule): + project = identity_fakes_v3.FakeProject.create_one_project() + domain = identity_fakes_v3.FakeDomain.create_one_domain() + + new_rule = ( + network_fakes.FakeNetworkMeterRule. + create_one_rule() + ) + + columns = ( + 'direction', + 'excluded', + 'id', + 'metering_label_id', + 'project_id', + 'remote_ip_prefix', + ) + data = ( + new_rule.direction, + new_rule.excluded, + new_rule.id, + new_rule.metering_label_id, + new_rule.project_id, + new_rule.remote_ip_prefix, + ) + + def setUp(self): + super(TestCreateMeterRule, self).setUp() + fake_meter = network_fakes.FakeNetworkMeter.create_one_meter({ + 'id': self.new_rule.metering_label_id}) + + self.network.create_metering_label_rule = mock.Mock( + return_value=self.new_rule) + self.projects_mock.get.return_value = self.project + self.cmd = network_meter_rule.CreateMeterRule(self.app, + self.namespace) + self.network.find_metering_label = mock.Mock( + return_value=fake_meter) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + self.new_rule.metering_label_id, + "--remote-ip-prefix", self.new_rule.remote_ip_prefix, + ] + verifylist = [ + ('meter', self.new_rule.metering_label_id), + ('remote_ip_prefix', self.new_rule.remote_ip_prefix), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_metering_label_rule.assert_called_once_with( + **{'direction': 'ingress', + 'metering_label_id': self.new_rule.metering_label_id, + 'remote_ip_prefix': self.new_rule.remote_ip_prefix, } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + "--ingress", + "--include", + self.new_rule.metering_label_id, + "--remote-ip-prefix", self.new_rule.remote_ip_prefix, + ] + verifylist = [ + ('ingress', True), + ('include', True), + ('meter', self.new_rule.metering_label_id), + ('remote_ip_prefix', self.new_rule.remote_ip_prefix), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_metering_label_rule.assert_called_once_with( + **{'direction': self.new_rule.direction, + 'excluded': self.new_rule.excluded, + 'metering_label_id': self.new_rule.metering_label_id, + 'remote_ip_prefix': self.new_rule.remote_ip_prefix, } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteMeterRule(TestMeterRule): + def setUp(self): + super(TestDeleteMeterRule, self).setUp() + self.rule_list = \ + network_fakes.FakeNetworkMeterRule.create_meter_rule( + count=2 + ) + self.network.delete_metering_label_rule = mock.Mock(return_value=None) + + self.network.find_metering_label_rule = network_fakes \ + .FakeNetworkMeterRule.get_meter_rule( + meter_rule=self.rule_list + ) + + self.cmd = network_meter_rule.DeleteMeterRule(self.app, + self.namespace) + + def test_delete_one_rule(self): + arglist = [ + self.rule_list[0].id, + ] + verifylist = [ + ('meter_rule_id', [self.rule_list[0].id]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.delete_metering_label_rule.assert_called_once_with( + self.rule_list[0] + ) + self.assertIsNone(result) + + def test_delete_multiple_rules(self): + arglist = [] + for rule in self.rule_list: + arglist.append(rule.id) + verifylist = [ + ('meter_rule_id', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [] + for rule in self.rule_list: + calls.append(call(rule)) + self.network.delete_metering_label_rule.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_rules_exception(self): + arglist = [ + self.rule_list[0].id, + 'xxxx-yyyy-zzzz', + self.rule_list[1].id, + ] + verifylist = [ + ('meter_rule_id', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + return_find = [ + self.rule_list[0], + exceptions.NotFound('404'), + self.rule_list[1], + ] + self.network.find_metering_label_rule = mock.Mock( + side_effect=return_find + ) + + ret_delete = [ + None, + exceptions.NotFound('404'), + ] + self.network.delete_metering_label_rule = mock.Mock( + side_effect=ret_delete + ) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + calls = [ + call(self.rule_list[0]), + call(self.rule_list[1]), + ] + self.network.delete_metering_label_rule.assert_has_calls(calls) + + +class TestListMeterRule(TestMeterRule): + rule_list = \ + network_fakes.FakeNetworkMeterRule.create_meter_rule( + count=2 + ) + + columns = ( + 'ID', + 'Excluded', + 'Direction', + 'Remote IP Prefix', + ) + + data = [] + + for rule in rule_list: + data.append(( + rule.id, + rule.excluded, + rule.direction, + rule.remote_ip_prefix, + )) + + def setUp(self): + super(TestListMeterRule, self).setUp() + + self.network.metering_label_rules = mock.Mock( + return_value=self.rule_list + ) + + self.cmd = network_meter_rule.ListMeterRule(self.app, + self.namespace) + + def test_rule_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.metering_label_rules.assert_called_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestShowMeterRule(TestMeterRule): + new_rule = ( + network_fakes.FakeNetworkMeterRule. + create_one_rule() + ) + + columns = ( + 'direction', + 'excluded', + 'id', + 'metering_label_id', + 'project_id', + 'remote_ip_prefix', + ) + + data = ( + new_rule.direction, + new_rule.excluded, + new_rule.id, + new_rule.metering_label_id, + new_rule.project_id, + new_rule.remote_ip_prefix, + ) + + def setUp(self): + super(TestShowMeterRule, self).setUp() + + self.cmd = network_meter_rule.ShowMeterRule(self.app, + self.namespace) + + self.network.find_metering_label_rule = \ + mock.Mock(return_value=self.new_rule) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_label_rule_show_option(self): + arglist = [ + self.new_rule.id, + ] + verifylist = [ + ('meter_rule_id', self.new_rule.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_metering_label_rule.assert_called_with( + self.new_rule.id, ignore_missing=False + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_policy.py b/openstackclient/tests/unit/network/v2/test_network_qos_policy.py new file mode 100644 index 00000000..667f5015 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_qos_policy.py @@ -0,0 +1,433 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 mock +from mock import call + +from osc_lib import exceptions + +from openstackclient.network.v2 import network_qos_policy +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestQosPolicy(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestQosPolicy, self).setUp() + # Get a shortcut to the network client + self.network = self.app.client_manager.network + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + + +class TestCreateNetworkQosPolicy(TestQosPolicy): + + project = identity_fakes_v3.FakeProject.create_one_project() + + # The new qos policy created. + new_qos_policy = ( + network_fakes.FakeNetworkQosPolicy.create_one_qos_policy( + attrs={ + 'tenant_id': project.id, + } + )) + columns = ( + 'description', + 'id', + 'name', + 'project_id', + 'rules', + 'shared', + ) + + data = ( + new_qos_policy.description, + new_qos_policy.id, + new_qos_policy.name, + new_qos_policy.project_id, + new_qos_policy.rules, + new_qos_policy.shared, + ) + + def setUp(self): + super(TestCreateNetworkQosPolicy, self).setUp() + self.network.create_qos_policy = mock.Mock( + return_value=self.new_qos_policy) + + # Get the command object to test + self.cmd = network_qos_policy.CreateNetworkQosPolicy( + self.app, self.namespace) + + self.projects_mock.get.return_value = self.project + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + self.new_qos_policy.name, + ] + verifylist = [ + ('project', None), + ('name', self.new_qos_policy.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_qos_policy.assert_called_once_with(**{ + 'name': self.new_qos_policy.name + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + '--share', + '--project', self.project.name, + self.new_qos_policy.name, + '--description', 'QoS policy description', + ] + verifylist = [ + ('share', True), + ('project', self.project.name), + ('name', self.new_qos_policy.name), + ('description', 'QoS policy description'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_qos_policy.assert_called_once_with(**{ + 'shared': True, + 'tenant_id': self.project.id, + 'name': self.new_qos_policy.name, + 'description': 'QoS policy description', + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteNetworkQosPolicy(TestQosPolicy): + + # The address scope to delete. + _qos_policies = ( + network_fakes.FakeNetworkQosPolicy.create_qos_policies(count=2)) + + def setUp(self): + super(TestDeleteNetworkQosPolicy, self).setUp() + self.network.delete_qos_policy = mock.Mock(return_value=None) + self.network.find_qos_policy = ( + network_fakes.FakeNetworkQosPolicy.get_qos_policies( + qos_policies=self._qos_policies) + ) + + # Get the command object to test + self.cmd = network_qos_policy.DeleteNetworkQosPolicy( + self.app, self.namespace) + + def test_qos_policy_delete(self): + arglist = [ + self._qos_policies[0].name, + ] + verifylist = [ + ('policy', [self._qos_policies[0].name]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.find_qos_policy.assert_called_once_with( + self._qos_policies[0].name, ignore_missing=False) + self.network.delete_qos_policy.assert_called_once_with( + self._qos_policies[0]) + self.assertIsNone(result) + + def test_multi_qos_policies_delete(self): + arglist = [] + + for a in self._qos_policies: + arglist.append(a.name) + verifylist = [ + ('policy', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [] + for a in self._qos_policies: + calls.append(call(a)) + self.network.delete_qos_policy.assert_has_calls(calls) + self.assertIsNone(result) + + def test_multi_qos_policies_delete_with_exception(self): + arglist = [ + self._qos_policies[0].name, + 'unexist_qos_policy', + ] + verifylist = [ + ('policy', + [self._qos_policies[0].name, 'unexist_qos_policy']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self._qos_policies[0], exceptions.CommandError] + self.network.find_qos_policy = ( + mock.MagicMock(side_effect=find_mock_result) + ) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 QoS policies failed to delete.', str(e)) + + self.network.find_qos_policy.assert_any_call( + self._qos_policies[0].name, ignore_missing=False) + self.network.find_qos_policy.assert_any_call( + 'unexist_qos_policy', ignore_missing=False) + self.network.delete_qos_policy.assert_called_once_with( + self._qos_policies[0] + ) + + +class TestListNetworkQosPolicy(TestQosPolicy): + + # The QoS policies to list up. + qos_policies = ( + network_fakes.FakeNetworkQosPolicy.create_qos_policies(count=3)) + columns = ( + 'ID', + 'Name', + 'Shared', + 'Project', + ) + data = [] + for qos_policy in qos_policies: + data.append(( + qos_policy.id, + qos_policy.name, + qos_policy.shared, + qos_policy.project_id, + )) + + def setUp(self): + super(TestListNetworkQosPolicy, self).setUp() + self.network.qos_policies = mock.Mock(return_value=self.qos_policies) + + # Get the command object to test + self.cmd = network_qos_policy.ListNetworkQosPolicy(self.app, + self.namespace) + + def test_qos_policy_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.qos_policies.assert_called_once_with(**{}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_qos_policy_list_share(self): + arglist = [ + '--share', + ] + verifylist = [ + ('share', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.qos_policies.assert_called_once_with( + **{'shared': True} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_qos_policy_list_no_share(self): + arglist = [ + '--no-share', + ] + verifylist = [ + ('no_share', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.qos_policies.assert_called_once_with( + **{'shared': False} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_network_qos_list_project(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + '--project-domain', project.domain_id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.network.qos_policies.assert_called_once_with( + **{'tenant_id': project.id} + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestSetNetworkQosPolicy(TestQosPolicy): + + # The QoS policy to set. + _qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() + + def setUp(self): + super(TestSetNetworkQosPolicy, self).setUp() + self.network.update_qos_policy = mock.Mock(return_value=None) + self.network.find_qos_policy = mock.Mock( + return_value=self._qos_policy) + + # Get the command object to test + self.cmd = network_qos_policy.SetNetworkQosPolicy(self.app, + self.namespace) + + def test_set_nothing(self): + arglist = [self._qos_policy.name, ] + verifylist = [ + ('policy', self._qos_policy.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = {} + self.network.update_qos_policy.assert_called_with( + self._qos_policy, **attrs) + self.assertIsNone(result) + + def test_set_name_share_description(self): + arglist = [ + '--name', 'new_qos_policy', + '--share', + '--description', 'QoS policy description', + self._qos_policy.name, + ] + verifylist = [ + ('name', 'new_qos_policy'), + ('share', True), + ('description', 'QoS policy description'), + ('policy', self._qos_policy.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'name': 'new_qos_policy', + 'description': 'QoS policy description', + 'shared': True, + } + self.network.update_qos_policy.assert_called_with( + self._qos_policy, **attrs) + self.assertIsNone(result) + + def test_set_no_share(self): + arglist = [ + '--no-share', + self._qos_policy.name, + ] + verifylist = [ + ('no_share', True), + ('policy', self._qos_policy.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'shared': False + } + self.network.update_qos_policy.assert_called_with( + self._qos_policy, **attrs) + self.assertIsNone(result) + + +class TestShowNetworkQosPolicy(TestQosPolicy): + + # The QoS policy to show. + _qos_policy = ( + network_fakes.FakeNetworkQosPolicy.create_one_qos_policy()) + columns = ( + 'description', + 'id', + 'name', + 'project_id', + 'rules', + 'shared', + ) + data = ( + _qos_policy.description, + _qos_policy.id, + _qos_policy.name, + _qos_policy.project_id, + _qos_policy.rules, + _qos_policy.shared, + ) + + def setUp(self): + super(TestShowNetworkQosPolicy, self).setUp() + self.network.find_qos_policy = mock.Mock(return_value=self._qos_policy) + + # Get the command object to test + self.cmd = network_qos_policy.ShowNetworkQosPolicy(self.app, + self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_all_options(self): + arglist = [ + self._qos_policy.name, + ] + verifylist = [ + ('policy', self._qos_policy.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_qos_policy.assert_called_once_with( + self._qos_policy.name, ignore_missing=False) + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.data), list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_rule.py b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py new file mode 100644 index 00000000..41ccae32 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py @@ -0,0 +1,1049 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 mock + +from osc_lib import exceptions + +from openstackclient.network.v2 import network_qos_rule +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth-limit' +RULE_TYPE_DSCP_MARKING = 'dscp-marking' +RULE_TYPE_MINIMUM_BANDWIDTH = 'minimum-bandwidth' +DSCP_VALID_MARKS = [0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, + 34, 36, 38, 40, 46, 48, 56] + + +class TestNetworkQosRule(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestNetworkQosRule, self).setUp() + # Get a shortcut to the network client + self.network = self.app.client_manager.network + self.qos_policy = (network_fakes.FakeNetworkQosPolicy. + create_one_qos_policy()) + self.network.find_qos_policy = mock.Mock(return_value=self.qos_policy) + + +class TestCreateNetworkQosRuleMinimumBandwidth(TestNetworkQosRule): + + def test_check_type_parameters(self): + pass + + def setUp(self): + super(TestCreateNetworkQosRuleMinimumBandwidth, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_MINIMUM_BANDWIDTH} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.columns = ( + 'direction', + 'id', + 'min_kbps', + 'project_id', + 'qos_policy_id', + 'type' + ) + + self.data = ( + self.new_rule.direction, + self.new_rule.id, + self.new_rule.min_kbps, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + self.network.create_qos_minimum_bandwidth_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.CreateNetworkQosRule(self.app, + self.namespace) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + '--type', RULE_TYPE_MINIMUM_BANDWIDTH, + '--min-kbps', str(self.new_rule.min_kbps), + '--egress', + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_MINIMUM_BANDWIDTH), + ('min_kbps', self.new_rule.min_kbps), + ('egress', True), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_qos_minimum_bandwidth_rule.assert_called_once_with( + self.qos_policy.id, + **{'min_kbps': self.new_rule.min_kbps, + 'direction': self.new_rule.direction} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_wrong_options(self): + arglist = [ + '--type', RULE_TYPE_MINIMUM_BANDWIDTH, + '--max-kbps', '10000', + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_MINIMUM_BANDWIDTH), + ('max_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('"Create" rule command for type "minimum-bandwidth" ' + 'requires arguments min_kbps, direction') + self.assertEqual(msg, str(e)) + + +class TestCreateNetworkQosRuleDSCPMarking(TestNetworkQosRule): + + def test_check_type_parameters(self): + pass + + def setUp(self): + super(TestCreateNetworkQosRuleDSCPMarking, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_DSCP_MARKING} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.columns = ( + 'dscp_mark', + 'id', + 'project_id', + 'qos_policy_id', + 'type' + ) + + self.data = ( + self.new_rule.dscp_mark, + self.new_rule.id, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + self.network.create_qos_dscp_marking_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.CreateNetworkQosRule(self.app, + self.namespace) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + '--type', RULE_TYPE_DSCP_MARKING, + '--dscp-mark', str(self.new_rule.dscp_mark), + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_DSCP_MARKING), + ('dscp_mark', self.new_rule.dscp_mark), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_qos_dscp_marking_rule.assert_called_once_with( + self.qos_policy.id, + **{'dscp_mark': self.new_rule.dscp_mark} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_wrong_options(self): + arglist = [ + '--type', RULE_TYPE_DSCP_MARKING, + '--max-kbps', '10000', + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_DSCP_MARKING), + ('max_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('"Create" rule command for type "dscp-marking" ' + 'requires arguments dscp_mark') + self.assertEqual(msg, str(e)) + + +class TestCreateNetworkQosRuleBandwidtLimit(TestNetworkQosRule): + + def test_check_type_parameters(self): + pass + + def setUp(self): + super(TestCreateNetworkQosRuleBandwidtLimit, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_BANDWIDTH_LIMIT} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.columns = ( + 'id', + 'max_burst_kbits', + 'max_kbps', + 'project_id', + 'qos_policy_id', + 'type' + ) + + self.data = ( + self.new_rule.id, + self.new_rule.max_burst_kbits, + self.new_rule.max_kbps, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + self.network.create_qos_bandwidth_limit_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.CreateNetworkQosRule(self.app, + self.namespace) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_create_default_options(self): + arglist = [ + '--type', RULE_TYPE_BANDWIDTH_LIMIT, + '--max-kbps', str(self.new_rule.max_kbps), + '--max-burst-kbits', str(self.new_rule.max_burst_kbits), + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_BANDWIDTH_LIMIT), + ('max_kbps', self.new_rule.max_kbps), + ('max_burst_kbits', self.new_rule.max_burst_kbits), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_qos_bandwidth_limit_rule.assert_called_once_with( + self.qos_policy.id, + **{'max_kbps': self.new_rule.max_kbps, + 'max_burst_kbps': self.new_rule.max_burst_kbits} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_wrong_options(self): + arglist = [ + '--type', RULE_TYPE_BANDWIDTH_LIMIT, + '--min-kbps', '10000', + self.new_rule.qos_policy_id, + ] + + verifylist = [ + ('type', RULE_TYPE_BANDWIDTH_LIMIT), + ('min_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('"Create" rule command for type "bandwidth-limit" ' + 'requires arguments max_kbps, max_burst_kbps') + self.assertEqual(msg, str(e)) + + +class TestDeleteNetworkQosRuleMinimumBandwidth(TestNetworkQosRule): + + def setUp(self): + super(TestDeleteNetworkQosRuleMinimumBandwidth, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_MINIMUM_BANDWIDTH} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.network.delete_qos_minimum_bandwidth_rule = mock.Mock( + return_value=None) + self.network.find_qos_minimum_bandwidth_rule = ( + network_fakes.FakeNetworkQosRule.get_qos_rules( + qos_rules=self.new_rule) + ) + + # Get the command object to test + self.cmd = network_qos_rule.DeleteNetworkQosRule(self.app, + self.namespace) + + def test_qos_policy_delete(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.find_qos_policy.assert_called_once_with( + self.qos_policy.id, ignore_missing=False) + self.network.delete_qos_minimum_bandwidth_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertIsNone(result) + + def test_qos_policy_delete_error(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + self.network.delete_qos_minimum_bandwidth_rule.side_effect = \ + Exception('Error message') + try: + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to delete Network QoS rule ID "%(rule)s": %(e)s' % + {'rule': self.new_rule.id, 'e': 'Error message'}) + self.assertEqual(msg, str(e)) + + +class TestDeleteNetworkQosRuleDSCPMarking(TestNetworkQosRule): + + def setUp(self): + super(TestDeleteNetworkQosRuleDSCPMarking, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_DSCP_MARKING} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.network.delete_qos_dscp_marking_rule = mock.Mock( + return_value=None) + self.network.find_qos_dscp_marking_rule = ( + network_fakes.FakeNetworkQosRule.get_qos_rules( + qos_rules=self.new_rule) + ) + + # Get the command object to test + self.cmd = network_qos_rule.DeleteNetworkQosRule(self.app, + self.namespace) + + def test_qos_policy_delete(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.find_qos_policy.assert_called_once_with( + self.qos_policy.id, ignore_missing=False) + self.network.delete_qos_dscp_marking_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertIsNone(result) + + def test_qos_policy_delete_error(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + self.network.delete_qos_dscp_marking_rule.side_effect = \ + Exception('Error message') + try: + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to delete Network QoS rule ID "%(rule)s": %(e)s' % + {'rule': self.new_rule.id, 'e': 'Error message'}) + self.assertEqual(msg, str(e)) + + +class TestDeleteNetworkQosRuleBandwidthLimit(TestNetworkQosRule): + + def setUp(self): + super(TestDeleteNetworkQosRuleBandwidthLimit, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_BANDWIDTH_LIMIT} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.network.delete_qos_bandwidth_limit_rule = mock.Mock( + return_value=None) + self.network.find_qos_bandwidth_limit_rule = ( + network_fakes.FakeNetworkQosRule.get_qos_rules( + qos_rules=self.new_rule) + ) + + # Get the command object to test + self.cmd = network_qos_rule.DeleteNetworkQosRule(self.app, + self.namespace) + + def test_qos_policy_delete(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.network.find_qos_policy.assert_called_once_with( + self.qos_policy.id, ignore_missing=False) + self.network.delete_qos_bandwidth_limit_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertIsNone(result) + + def test_qos_policy_delete_error(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + self.network.delete_qos_bandwidth_limit_rule.side_effect = \ + Exception('Error message') + try: + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to delete Network QoS rule ID "%(rule)s": %(e)s' % + {'rule': self.new_rule.id, 'e': 'Error message'}) + self.assertEqual(msg, str(e)) + + +class TestSetNetworkQosRuleMinimumBandwidth(TestNetworkQosRule): + + def setUp(self): + super(TestSetNetworkQosRuleMinimumBandwidth, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_MINIMUM_BANDWIDTH} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs=attrs) + self.qos_policy.rules = [self.new_rule] + self.network.update_qos_minimum_bandwidth_rule = mock.Mock( + return_value=None) + self.network.find_qos_minimum_bandwidth_rule = mock.Mock( + return_value=self.new_rule) + self.network.find_qos_policy = mock.Mock( + return_value=self.qos_policy) + + # Get the command object to test + self.cmd = (network_qos_rule.SetNetworkQosRule(self.app, + self.namespace)) + + def test_set_nothing(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.update_qos_minimum_bandwidth_rule.assert_called_with( + self.new_rule, self.qos_policy.id) + self.assertIsNone(result) + + def test_set_min_kbps(self): + self._set_min_kbps() + + def test_set_min_kbps_to_zero(self): + self._set_min_kbps(min_kbps=0) + + def _set_min_kbps(self, min_kbps=None): + if min_kbps: + previous_min_kbps = self.new_rule.min_kbps + self.new_rule.min_kbps = min_kbps + + arglist = [ + '--min-kbps', str(self.new_rule.min_kbps), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('min_kbps', self.new_rule.min_kbps), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'min_kbps': self.new_rule.min_kbps, + } + self.network.update_qos_minimum_bandwidth_rule.assert_called_with( + self.new_rule, self.qos_policy.id, **attrs) + self.assertIsNone(result) + + if min_kbps: + self.new_rule.min_kbps = previous_min_kbps + + def test_set_wrong_options(self): + arglist = [ + '--max-kbps', str(10000), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('max_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to set Network QoS rule ID "%(rule)s": Rule type ' + '"minimum-bandwidth" only requires arguments min_kbps, ' + 'direction' % {'rule': self.new_rule.id}) + self.assertEqual(msg, str(e)) + + +class TestSetNetworkQosRuleDSCPMarking(TestNetworkQosRule): + + def setUp(self): + super(TestSetNetworkQosRuleDSCPMarking, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_DSCP_MARKING} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs=attrs) + self.qos_policy.rules = [self.new_rule] + self.network.update_qos_dscp_marking_rule = mock.Mock( + return_value=None) + self.network.find_qos_dscp_marking_rule = mock.Mock( + return_value=self.new_rule) + self.network.find_qos_policy = mock.Mock( + return_value=self.qos_policy) + + # Get the command object to test + self.cmd = (network_qos_rule.SetNetworkQosRule(self.app, + self.namespace)) + + def test_set_nothing(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.update_qos_dscp_marking_rule.assert_called_with( + self.new_rule, self.qos_policy.id) + self.assertIsNone(result) + + def test_set_dscp_mark(self): + self._set_dscp_mark() + + def test_set_dscp_mark_to_zero(self): + self._set_dscp_mark(dscp_mark=0) + + def _set_dscp_mark(self, dscp_mark=None): + if dscp_mark: + previous_dscp_mark = self.new_rule.dscp_mark + self.new_rule.dscp_mark = dscp_mark + + arglist = [ + '--dscp-mark', str(self.new_rule.dscp_mark), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('dscp_mark', self.new_rule.dscp_mark), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'dscp_mark': self.new_rule.dscp_mark, + } + self.network.update_qos_dscp_marking_rule.assert_called_with( + self.new_rule, self.qos_policy.id, **attrs) + self.assertIsNone(result) + + if dscp_mark: + self.new_rule.dscp_mark = previous_dscp_mark + + def test_set_wrong_options(self): + arglist = [ + '--max-kbps', str(10000), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('max_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to set Network QoS rule ID "%(rule)s": Rule type ' + '"dscp-marking" only requires arguments dscp_mark' % + {'rule': self.new_rule.id}) + self.assertEqual(msg, str(e)) + + +class TestSetNetworkQosRuleBandwidthLimit(TestNetworkQosRule): + + def setUp(self): + super(TestSetNetworkQosRuleBandwidthLimit, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_BANDWIDTH_LIMIT} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs=attrs) + self.qos_policy.rules = [self.new_rule] + self.network.update_qos_bandwidth_limit_rule = mock.Mock( + return_value=None) + self.network.find_qos_bandwidth_limit_rule = mock.Mock( + return_value=self.new_rule) + self.network.find_qos_policy = mock.Mock( + return_value=self.qos_policy) + + # Get the command object to test + self.cmd = (network_qos_rule.SetNetworkQosRule(self.app, + self.namespace)) + + def test_set_nothing(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network.update_qos_bandwidth_limit_rule.assert_called_with( + self.new_rule, self.qos_policy.id) + self.assertIsNone(result) + + def test_set_max_kbps(self): + self._set_max_kbps() + + def test_set_max_kbps_to_zero(self): + self._set_max_kbps(max_kbps=0) + + def _set_max_kbps(self, max_kbps=None): + if max_kbps: + previous_max_kbps = self.new_rule.max_kbps + self.new_rule.max_kbps = max_kbps + + arglist = [ + '--max-kbps', str(self.new_rule.max_kbps), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('max_kbps', self.new_rule.max_kbps), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'max_kbps': self.new_rule.max_kbps, + } + self.network.update_qos_bandwidth_limit_rule.assert_called_with( + self.new_rule, self.qos_policy.id, **attrs) + self.assertIsNone(result) + + if max_kbps: + self.new_rule.max_kbps = previous_max_kbps + + def test_set_max_burst_kbits(self): + self._set_max_burst_kbits() + + def test_set_max_burst_kbits_to_zero(self): + self._set_max_burst_kbits(max_burst_kbits=0) + + def _set_max_burst_kbits(self, max_burst_kbits=None): + if max_burst_kbits: + previous_max_burst_kbits = self.new_rule.max_burst_kbits + self.new_rule.max_burst_kbits = max_burst_kbits + + arglist = [ + '--max-burst-kbits', str(self.new_rule.max_burst_kbits), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('max_burst_kbits', self.new_rule.max_burst_kbits), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'max_burst_kbps': self.new_rule.max_burst_kbits, + } + self.network.update_qos_bandwidth_limit_rule.assert_called_with( + self.new_rule, self.qos_policy.id, **attrs) + self.assertIsNone(result) + + if max_burst_kbits: + self.new_rule.max_burst_kbits = previous_max_burst_kbits + + def test_set_wrong_options(self): + arglist = [ + '--min-kbps', str(10000), + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('min_kbps', 10000), + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + except exceptions.CommandError as e: + msg = ('Failed to set Network QoS rule ID "%(rule)s": Rule type ' + '"bandwidth-limit" only requires arguments max_kbps, ' + 'max_burst_kbps' % {'rule': self.new_rule.id}) + self.assertEqual(msg, str(e)) + + +class TestListNetworkQosRule(TestNetworkQosRule): + + def setUp(self): + super(TestListNetworkQosRule, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_MINIMUM_BANDWIDTH} + self.new_rule_min_bw = (network_fakes.FakeNetworkQosRule. + create_one_qos_rule(attrs=attrs)) + attrs['type'] = RULE_TYPE_DSCP_MARKING + self.new_rule_dscp_mark = (network_fakes.FakeNetworkQosRule. + create_one_qos_rule(attrs=attrs)) + attrs['type'] = RULE_TYPE_BANDWIDTH_LIMIT + self.new_rule_max_bw = (network_fakes.FakeNetworkQosRule. + create_one_qos_rule(attrs=attrs)) + self.qos_policy.rules = [self.new_rule_min_bw, + self.new_rule_dscp_mark, + self.new_rule_max_bw] + self.network.find_qos_minimum_bandwidth_rule = mock.Mock( + return_value=self.new_rule_min_bw) + self.network.find_qos_dscp_marking_rule = mock.Mock( + return_value=self.new_rule_dscp_mark) + self.network.find_qos_bandwidth_limit_rule = mock.Mock( + return_value=self.new_rule_max_bw) + self.columns = ( + 'ID', + 'QoS Policy ID', + 'Type', + 'Max Kbps', + 'Max Burst Kbits', + 'Min Kbps', + 'DSCP mark', + 'Direction', + ) + self.data = [] + for index in range(len(self.qos_policy.rules)): + self.data.append(( + self.qos_policy.rules[index].id, + self.qos_policy.rules[index].qos_policy_id, + self.qos_policy.rules[index].type, + getattr(self.qos_policy.rules[index], 'max_kbps', ''), + getattr(self.qos_policy.rules[index], 'max_burst_kbps', ''), + getattr(self.qos_policy.rules[index], 'min_kbps', ''), + getattr(self.qos_policy.rules[index], 'dscp_mark', ''), + getattr(self.qos_policy.rules[index], 'direction', ''), + )) + # Get the command object to test + self.cmd = network_qos_rule.ListNetworkQosRule(self.app, + self.namespace) + + def test_qos_rule_list(self): + arglist = [ + self.qos_policy.id + ] + verifylist = [ + ('qos_policy', self.qos_policy.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_qos_policy.assert_called_once_with( + self.qos_policy.id, ignore_missing=False) + self.assertEqual(self.columns, columns) + list_data = list(data) + self.assertEqual(len(self.data), len(list_data)) + for index in range(len(list_data)): + self.assertEqual(self.data[index], list_data[index]) + + +class TestShowNetworkQosRuleMinimumBandwidth(TestNetworkQosRule): + + def setUp(self): + super(TestShowNetworkQosRuleMinimumBandwidth, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_MINIMUM_BANDWIDTH} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.columns = ( + 'direction', + 'id', + 'min_kbps', + 'project_id', + 'qos_policy_id', + 'type' + ) + self.data = ( + self.new_rule.direction, + self.new_rule.id, + self.new_rule.min_kbps, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + + self.network.get_qos_minimum_bandwidth_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.ShowNetworkQosRule(self.app, + self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_all_options(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.get_qos_minimum_bandwidth_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.data), list(data)) + + +class TestShowNetworkQosDSCPMarking(TestNetworkQosRule): + + def setUp(self): + super(TestShowNetworkQosDSCPMarking, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_DSCP_MARKING} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.columns = ( + 'dscp_mark', + 'id', + 'project_id', + 'qos_policy_id', + 'type' + ) + self.data = ( + self.new_rule.dscp_mark, + self.new_rule.id, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + + self.network.get_qos_dscp_marking_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.ShowNetworkQosRule(self.app, + self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_all_options(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.get_qos_dscp_marking_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.data), list(data)) + + +class TestShowNetworkQosBandwidthLimit(TestNetworkQosRule): + + def setUp(self): + super(TestShowNetworkQosBandwidthLimit, self).setUp() + attrs = {'qos_policy_id': self.qos_policy.id, + 'type': RULE_TYPE_BANDWIDTH_LIMIT} + self.new_rule = network_fakes.FakeNetworkQosRule.create_one_qos_rule( + attrs) + self.qos_policy.rules = [self.new_rule] + self.columns = ( + 'id', + 'max_burst_kbits', + 'max_kbps', + 'project_id', + 'qos_policy_id', + 'type' + ) + self.data = ( + self.new_rule.id, + self.new_rule.max_burst_kbits, + self.new_rule.max_kbps, + self.new_rule.project_id, + self.new_rule.qos_policy_id, + self.new_rule.type, + ) + + self.network.get_qos_bandwidth_limit_rule = mock.Mock( + return_value=self.new_rule) + + # Get the command object to test + self.cmd = network_qos_rule.ShowNetworkQosRule(self.app, + self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_show_all_options(self): + arglist = [ + self.new_rule.qos_policy_id, + self.new_rule.id, + ] + verifylist = [ + ('qos_policy', self.new_rule.qos_policy_id), + ('id', self.new_rule.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.get_qos_bandwidth_limit_rule.assert_called_once_with( + self.new_rule.id, self.qos_policy.id) + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.data), list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py b/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py new file mode 100644 index 00000000..b93abe80 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py @@ -0,0 +1,62 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 mock + +from openstackclient.network.v2 import network_qos_rule_type as _qos_rule_type +from openstackclient.tests.unit.network.v2 import fakes as network_fakes + + +class TestNetworkQosRuleType(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestNetworkQosRuleType, self).setUp() + # Get a shortcut to the network client + self.network = self.app.client_manager.network + + +class TestListNetworkQosRuleType(TestNetworkQosRuleType): + + # The QoS policies to list up. + qos_rule_types = ( + network_fakes.FakeNetworkQosRuleType.create_qos_rule_types(count=3)) + columns = ( + 'Type', + ) + data = [] + for qos_rule_type in qos_rule_types: + data.append(( + qos_rule_type.type, + )) + + def setUp(self): + super(TestListNetworkQosRuleType, self).setUp() + self.network.qos_rule_types = mock.Mock( + return_value=self.qos_rule_types) + + # Get the command object to test + self.cmd = _qos_rule_type.ListNetworkQosRuleType(self.app, + self.namespace) + + def test_qos_rule_type_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.qos_rule_types.assert_called_once_with(**{}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py index c526ae4e..935ce075 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -36,6 +36,7 @@ class TestNetworkRBAC(network_fakes.TestNetworkV2): class TestCreateNetworkRBAC(TestNetworkRBAC): network_object = network_fakes.FakeNetwork.create_one_network() + qos_object = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() project = identity_fakes_v3.FakeProject.create_one_project() rbac_policy = network_fakes.FakeNetworkRBAC.create_one_network_rbac( attrs={'tenant_id': project.id, @@ -71,6 +72,8 @@ class TestCreateNetworkRBAC(TestNetworkRBAC): return_value=self.rbac_policy) self.network.find_network = mock.Mock( return_value=self.network_object) + self.network.find_qos_policy = mock.Mock( + return_value=self.qos_object) self.projects_mock.get.return_value = self.project def test_network_rbac_create_no_type(self): @@ -194,6 +197,43 @@ class TestCreateNetworkRBAC(TestNetworkRBAC): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_network_rbac_create_qos_object(self): + self.rbac_policy.object_type = 'qos_policy' + self.rbac_policy.object_id = self.qos_object.id + arglist = [ + '--type', 'qos_policy', + '--action', self.rbac_policy.action, + '--target-project', self.rbac_policy.target_tenant, + self.qos_object.name, + ] + verifylist = [ + ('type', 'qos_policy'), + ('action', self.rbac_policy.action), + ('target_project', self.rbac_policy.target_tenant), + ('rbac_object', self.qos_object.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_rbac_policy.assert_called_with(**{ + 'object_id': self.qos_object.id, + 'object_type': 'qos_policy', + 'action': self.rbac_policy.action, + 'target_tenant': self.rbac_policy.target_tenant, + }) + self.data = [ + self.rbac_policy.action, + self.rbac_policy.id, + self.qos_object.id, + 'qos_policy', + self.rbac_policy.tenant_id, + self.rbac_policy.target_tenant, + ] + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestDeleteNetworkRBAC(TestNetworkRBAC): @@ -287,7 +327,12 @@ class TestListNetworkRABC(TestNetworkRBAC): 'Object Type', 'Object ID', ) - + columns_long = ( + 'ID', + 'Object Type', + 'Object ID', + 'Action', + ) data = [] for r in rbac_policies: data.append(( @@ -295,6 +340,14 @@ class TestListNetworkRABC(TestNetworkRBAC): r.object_type, r.object_id, )) + data_long = [] + for r in rbac_policies: + data_long.append(( + r.id, + r.object_type, + r.object_id, + r.action, + )) def setUp(self): super(TestListNetworkRABC, self).setUp() @@ -316,6 +369,55 @@ class TestListNetworkRABC(TestNetworkRBAC): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_network_rbac_list_type_opt(self): + arglist = [ + '--type', self.rbac_policies[0].object_type, ] + verifylist = [ + ('type', self.rbac_policies[0].object_type)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.network.rbac_policies.assert_called_with(**{ + 'object_type': self.rbac_policies[0].object_type + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_network_rbac_list_action_opt(self): + arglist = [ + '--action', self.rbac_policies[0].action, ] + verifylist = [ + ('action', self.rbac_policies[0].action)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.network.rbac_policies.assert_called_with(**{ + 'action': self.rbac_policies[0].action + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_network_rbac_list_with_long(self): + arglist = [ + '--long', + ] + + verifylist = [ + ('long', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.rbac_policies.assert_called_with() + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + class TestSetNetworkRBAC(TestNetworkRBAC): diff --git a/openstackclient/tests/unit/network/v2/test_network_service_provider.py b/openstackclient/tests/unit/network/v2/test_network_service_provider.py new file mode 100644 index 00000000..5ba85ddb --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_service_provider.py @@ -0,0 +1,71 @@ +# Copyright (c) 2016, Intel Corporation. +# All Rights Reserved. +# +# 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 mock + +from openstackclient.network.v2 import network_service_provider \ + as service_provider +from openstackclient.tests.unit.network.v2 import fakes + + +class TestNetworkServiceProvider(fakes.TestNetworkV2): + + def setUp(self): + super(TestNetworkServiceProvider, self).setUp() + self.network = self.app.client_manager.network + + +class TestListNetworkServiceProvider(TestNetworkServiceProvider): + provider_list = \ + fakes.FakeNetworkServiceProvider.create_network_service_providers( + count=2 + ) + + columns = ( + 'Service Type', + 'Name', + 'Default', + ) + + data = [] + + for provider in provider_list: + data.append(( + provider.service_type, + provider.name, + provider.is_default, + )) + + def setUp(self): + super(TestListNetworkServiceProvider, self).setUp() + self.network.service_providers = mock.Mock( + return_value=self.provider_list + ) + + self.cmd = \ + service_provider.ListNetworkServiceProvider(self.app, + self.namespace) + + def test_network_service_provider_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.service_providers.assert_called_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index a2aceab1..d2df5841 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -20,6 +20,7 @@ from osc_lib import utils from openstackclient.network.v2 import port from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -31,6 +32,8 @@ class TestPort(network_fakes.TestNetworkV2): # Get a shortcut to the network client self.network = self.app.client_manager.network + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects def _get_common_cols_data(self, fake_port): columns = ( @@ -41,6 +44,7 @@ class TestPort(network_fakes.TestNetworkV2): 'binding_vif_details', 'binding_vif_type', 'binding_vnic_type', + 'description', 'device_id', 'device_owner', 'dns_assignment', @@ -53,7 +57,7 @@ class TestPort(network_fakes.TestNetworkV2): 'network_id', 'port_security_enabled', 'project_id', - 'security_groups', + 'security_group_ids', 'status', ) @@ -65,6 +69,7 @@ class TestPort(network_fakes.TestNetworkV2): utils.format_dict(fake_port.binding_vif_details), fake_port.binding_vif_type, fake_port.binding_vnic_type, + fake_port.description, fake_port.device_id, fake_port.device_owner, utils.format_list_of_dicts(fake_port.dns_assignment), @@ -77,7 +82,7 @@ class TestPort(network_fakes.TestNetworkV2): fake_port.network_id, fake_port.port_security_enabled, fake_port.project_id, - utils.format_list(fake_port.security_groups), + utils.format_list(fake_port.security_group_ids), fake_port.status, ) @@ -130,6 +135,7 @@ class TestCreatePort(TestPort): '--mac-address', 'aa:aa:aa:aa:aa:aa', '--fixed-ip', 'subnet=%s,ip-address=10.0.0.2' % self.fake_subnet.id, + '--description', self._port.description, '--device', 'deviceid', '--device-owner', 'fakeowner', '--disable', @@ -137,6 +143,7 @@ class TestCreatePort(TestPort): '--binding-profile', 'foo=bar', '--binding-profile', 'foo2=bar2', '--network', self._port.network_id, + '--dns-name', '8.8.8.8', 'test-port', ] @@ -146,12 +153,14 @@ class TestCreatePort(TestPort): 'fixed_ip', [{'subnet': self.fake_subnet.id, 'ip-address': '10.0.0.2'}] ), + ('description', self._port.description), ('device', 'deviceid'), ('device_owner', 'fakeowner'), ('disable', True), ('vnic_type', 'macvtap'), ('binding_profile', {'foo': 'bar', 'foo2': 'bar2'}), ('network', self._port.network_id), + ('dns_name', '8.8.8.8'), ('name', 'test-port'), ] @@ -163,12 +172,14 @@ class TestCreatePort(TestPort): 'mac_address': 'aa:aa:aa:aa:aa:aa', 'fixed_ips': [{'subnet_id': self.fake_subnet.id, 'ip_address': '10.0.0.2'}], + 'description': self._port.description, 'device_id': 'deviceid', 'device_owner': 'fakeowner', 'admin_state_up': False, 'binding:vnic_type': 'macvtap', 'binding:profile': {'foo': 'bar', 'foo2': 'bar2'}, 'network_id': self._port.network_id, + 'dns_name': '8.8.8.8', 'name': 'test-port', }) @@ -208,7 +219,7 @@ class TestCreatePort(TestPort): 'test-port', ] verifylist = [ - ('network', self._port.network_id,), + ('network', self._port.network_id), ('enable', True), ('binding_profile', {'parent_name': 'fake_parent', 'tag': 42}), ('name', 'test-port'), @@ -228,6 +239,237 @@ class TestCreatePort(TestPort): self.assertEqual(ref_columns, columns) self.assertEqual(ref_data, data) + def test_create_with_security_group(self): + secgroup = network_fakes.FakeSecurityGroup.create_one_security_group() + self.network.find_security_group = mock.Mock(return_value=secgroup) + arglist = [ + '--network', self._port.network_id, + '--security-group', secgroup.id, + 'test-port', + ] + + verifylist = [ + ('network', self._port.network_id,), + ('enable', True), + ('security_group', [secgroup.id]), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'security_group_ids': [secgroup.id], + 'name': 'test-port', + }) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) + + def test_create_port_with_dns_name(self): + arglist = [ + '--network', self._port.network_id, + '--dns-name', '8.8.8.8', + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id,), + ('enable', True), + ('dns_name', '8.8.8.8'), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'dns_name': '8.8.8.8', + 'name': 'test-port', + }) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) + + def test_create_with_security_groups(self): + sg_1 = network_fakes.FakeSecurityGroup.create_one_security_group() + sg_2 = network_fakes.FakeSecurityGroup.create_one_security_group() + self.network.find_security_group = mock.Mock(side_effect=[sg_1, sg_2]) + arglist = [ + '--network', self._port.network_id, + '--security-group', sg_1.id, + '--security-group', sg_2.id, + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id,), + ('enable', True), + ('security_group', [sg_1.id, sg_2.id]), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'security_group_ids': [sg_1.id, sg_2.id], + 'name': 'test-port', + }) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) + + def test_create_with_no_security_groups(self): + arglist = [ + '--network', self._port.network_id, + '--no-security-group', + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id), + ('enable', True), + ('no_security_group', True), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'security_group_ids': [], + 'name': 'test-port', + }) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) + + def test_create_port_with_allowed_address_pair_ipaddr(self): + pairs = [{'ip_address': '192.168.1.123'}, + {'ip_address': '192.168.1.45'}] + arglist = [ + '--network', self._port.network_id, + '--allowed-address', 'ip-address=192.168.1.123', + '--allowed-address', 'ip-address=192.168.1.45', + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id), + ('enable', True), + ('allowed_address_pairs', [{'ip-address': '192.168.1.123'}, + {'ip-address': '192.168.1.45'}]), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'allowed_address_pairs': pairs, + 'name': 'test-port', + }) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) + + def test_create_port_with_allowed_address_pair(self): + pairs = [{'ip_address': '192.168.1.123', + 'mac_address': 'aa:aa:aa:aa:aa:aa'}, + {'ip_address': '192.168.1.45', + 'mac_address': 'aa:aa:aa:aa:aa:b1'}] + arglist = [ + '--network', self._port.network_id, + '--allowed-address', + 'ip-address=192.168.1.123,mac-address=aa:aa:aa:aa:aa:aa', + '--allowed-address', + 'ip-address=192.168.1.45,mac-address=aa:aa:aa:aa:aa:b1', + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id), + ('enable', True), + ('allowed_address_pairs', [{'ip-address': '192.168.1.123', + 'mac-address': 'aa:aa:aa:aa:aa:aa'}, + {'ip-address': '192.168.1.45', + 'mac-address': 'aa:aa:aa:aa:aa:b1'}]), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'allowed_address_pairs': pairs, + 'name': 'test-port', + }) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) + + def test_create_port_security_enabled(self): + arglist = [ + '--network', self._port.network_id, + '--enable-port-security', + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id,), + ('enable', True), + ('enable_port_security', True), + ('name', 'test-port'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'port_security_enabled': True, + 'name': 'test-port', + }) + + def test_create_port_security_disabled(self): + arglist = [ + '--network', self._port.network_id, + '--disable-port-security', + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id,), + ('enable', True), + ('disable_port_security', True), + ('name', 'test-port'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.network.create_port.assert_called_once_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'port_security_enabled': False, + 'name': 'test-port', + }) + class TestDeletePort(TestPort): @@ -317,6 +559,17 @@ class TestListPort(TestPort): 'Name', 'MAC Address', 'Fixed IP Addresses', + 'Status', + ) + + columns_long = ( + 'ID', + 'Name', + 'MAC Address', + 'Fixed IP Addresses', + 'Status', + 'Security Groups', + 'Device Owner', ) data = [] @@ -326,6 +579,19 @@ class TestListPort(TestPort): prt.name, prt.mac_address, utils.format_list_of_dicts(prt.fixed_ips), + prt.status, + )) + + data_long = [] + for prt in _ports: + data_long.append(( + prt.id, + prt.name, + prt.mac_address, + utils.format_list_of_dicts(prt.fixed_ips), + prt.status, + utils.format_list(prt.security_group_ids), + prt.device_owner, )) def setUp(self): @@ -419,12 +685,14 @@ class TestListPort(TestPort): '--device-owner', self._ports[0].device_owner, '--router', 'fake-router-name', '--network', 'fake-network-name', + '--mac-address', self._ports[0].mac_address, ] verifylist = [ ('device_owner', self._ports[0].device_owner), ('router', 'fake-router-name'), - ('network', 'fake-network-name') + ('network', 'fake-network-name'), + ('mac_address', self._ports[0].mac_address) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -434,11 +702,172 @@ class TestListPort(TestPort): self.network.ports.assert_called_once_with(**{ 'device_owner': self._ports[0].device_owner, 'device_id': 'fake-router-id', - 'network_id': 'fake-network-id' + 'network_id': 'fake-network-id', + 'mac_address': self._ports[0].mac_address }) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_port_list_mac_address_opt(self): + arglist = [ + '--mac-address', self._ports[0].mac_address, + ] + + verifylist = [ + ('mac_address', self._ports[0].mac_address) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ports.assert_called_once_with(**{ + 'mac_address': self._ports[0].mac_address + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_port_list_fixed_ip_opt_ip_address(self): + ip_address = self._ports[0].fixed_ips[0]['ip_address'] + arglist = [ + '--fixed-ip', "ip-address=%s" % ip_address, + ] + verifylist = [ + ('fixed_ip', [{'ip-address': ip_address}]) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ports.assert_called_once_with(**{ + 'fixed_ips': ['ip_address=%s' % ip_address]}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_port_list_fixed_ip_opt_subnet_id(self): + subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] + arglist = [ + '--fixed-ip', "subnet=%s" % subnet_id, + ] + verifylist = [ + ('fixed_ip', [{'subnet': subnet_id}]) + ] + + self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet( + {'id': subnet_id}) + self.network.find_subnet = mock.Mock(return_value=self.fake_subnet) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.ports.assert_called_once_with(**{ + 'fixed_ips': ['subnet_id=%s' % subnet_id]}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_port_list_fixed_ip_opts(self): + subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] + ip_address = self._ports[0].fixed_ips[0]['ip_address'] + arglist = [ + '--fixed-ip', "subnet=%s,ip-address=%s" % (subnet_id, + ip_address) + ] + verifylist = [ + ('fixed_ip', [{'subnet': subnet_id, + 'ip-address': ip_address}]) + ] + + self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet( + {'id': subnet_id}) + self.network.find_subnet = mock.Mock(return_value=self.fake_subnet) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.ports.assert_called_once_with(**{ + 'fixed_ips': ['subnet_id=%s' % subnet_id, + 'ip_address=%s' % ip_address]}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_port_list_fixed_ips(self): + subnet_id = self._ports[0].fixed_ips[0]['subnet_id'] + ip_address = self._ports[0].fixed_ips[0]['ip_address'] + arglist = [ + '--fixed-ip', "subnet=%s" % subnet_id, + '--fixed-ip', "ip-address=%s" % ip_address, + ] + verifylist = [ + ('fixed_ip', [{'subnet': subnet_id}, + {'ip-address': ip_address}]) + ] + + self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet( + {'id': subnet_id}) + self.network.find_subnet = mock.Mock(return_value=self.fake_subnet) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.ports.assert_called_once_with(**{ + 'fixed_ips': ['subnet_id=%s' % subnet_id, + 'ip_address=%s' % ip_address]}) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_list_port_with_long(self): + arglist = [ + '--long', + ] + + verifylist = [ + ('long', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ports.assert_called_once_with() + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + + def test_port_list_project(self): + project = identity_fakes.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id, 'project_id': project.id} + + self.network.ports.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_port_list_project_domain(self): + project = identity_fakes.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + '--project-domain', project.domain_id, + ] + verifylist = [ + ('project', project.id), + ('project_domain', project.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id, 'project_id': project.id} + + self.network.ports.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestSetPort(TestPort): @@ -458,6 +887,7 @@ class TestSetPort(TestPort): arglist = [ '--fixed-ip', 'ip-address=10.0.0.11', self._port.name, + '--no-fixed-ip', ] verifylist = [ ('fixed_ip', [{'ip-address': '10.0.0.11'}]), @@ -473,6 +903,25 @@ class TestSetPort(TestPort): self.network.update_port.assert_called_once_with(self._port, **attrs) self.assertIsNone(result) + def test_set_dns_name(self): + arglist = [ + '--dns-name', '8.8.8.8', + self._port.name, + ] + verifylist = [ + ('dns_name', '8.8.8.8'), + ('port', self._port.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'dns_name': '8.8.8.8', + } + self.network.update_port.assert_called_once_with(self._port, **attrs) + self.assertIsNone(result) + def test_append_fixed_ip(self): _testport = network_fakes.FakePort.create_one_port( {'fixed_ips': [{'ip_address': '0.0.0.1'}]}) @@ -538,6 +987,25 @@ class TestSetPort(TestPort): self.network.update_port.assert_called_once_with(_testport, **attrs) self.assertIsNone(result) + def test_overwrite_mac_address(self): + _testport = network_fakes.FakePort.create_one_port( + {'mac_address': '11:22:33:44:55:66'}) + self.network.find_port = mock.Mock(return_value=_testport) + arglist = [ + '--mac-address', '66:55:44:33:22:11', + _testport.name, + ] + verifylist = [ + ('mac_address', '66:55:44:33:22:11'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'mac_address': '66:55:44:33:22:11', + } + self.network.update_port.assert_called_once_with(_testport, **attrs) + self.assertIsNone(result) + def test_set_this(self): arglist = [ '--disable', @@ -565,6 +1033,7 @@ class TestSetPort(TestPort): def test_set_that(self): arglist = [ + '--description', 'newDescription', '--enable', '--vnic-type', 'macvtap', '--binding-profile', 'foo=bar', @@ -573,6 +1042,7 @@ class TestSetPort(TestPort): self._port.name, ] verifylist = [ + ('description', 'newDescription'), ('enable', True), ('vnic_type', 'macvtap'), ('binding_profile', {'foo': 'bar'}), @@ -589,6 +1059,7 @@ class TestSetPort(TestPort): 'binding:vnic_type': 'macvtap', 'binding:profile': {'foo': 'bar'}, 'binding:host_id': 'binding-host-id-xxxx', + 'description': 'newDescription', 'name': 'newName', } self.network.update_port.assert_called_once_with(self._port, **attrs) @@ -651,6 +1122,216 @@ class TestSetPort(TestPort): self.network.update_port.assert_called_once_with(self._port, **attrs) self.assertIsNone(result) + def test_set_security_group(self): + sg = network_fakes.FakeSecurityGroup.create_one_security_group() + self.network.find_security_group = mock.Mock(return_value=sg) + arglist = [ + '--security-group', sg.id, + self._port.name, + ] + verifylist = [ + ('security_group', [sg.id]), + ('port', self._port.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'security_group_ids': [sg.id], + } + self.network.update_port.assert_called_once_with(self._port, **attrs) + self.assertIsNone(result) + + def test_append_security_group(self): + sg_1 = network_fakes.FakeSecurityGroup.create_one_security_group() + sg_2 = network_fakes.FakeSecurityGroup.create_one_security_group() + sg_3 = network_fakes.FakeSecurityGroup.create_one_security_group() + self.network.find_security_group = mock.Mock(side_effect=[sg_2, sg_3]) + _testport = network_fakes.FakePort.create_one_port( + {'security_group_ids': [sg_1.id]}) + self.network.find_port = mock.Mock(return_value=_testport) + arglist = [ + '--security-group', sg_2.id, + '--security-group', sg_3.id, + _testport.name, + ] + verifylist = [ + ('security_group', [sg_2.id, sg_3.id]), + ('port', _testport.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'security_group_ids': [sg_2.id, sg_3.id, sg_1.id], + } + self.network.update_port.assert_called_once_with(_testport, **attrs) + self.assertIsNone(result) + + def test_set_no_security_groups(self): + arglist = [ + '--no-security-group', + self._port.name, + ] + verifylist = [ + ('no_security_group', True), + ('port', self._port.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'security_group_ids': [], + } + self.network.update_port.assert_called_once_with(self._port, **attrs) + self.assertIsNone(result) + + def test_overwrite_security_group(self): + sg1 = network_fakes.FakeSecurityGroup.create_one_security_group() + sg2 = network_fakes.FakeSecurityGroup.create_one_security_group() + _testport = network_fakes.FakePort.create_one_port( + {'security_group_ids': [sg1.id]}) + self.network.find_port = mock.Mock(return_value=_testport) + self.network.find_security_group = mock.Mock(return_value=sg2) + arglist = [ + '--security-group', sg2.id, + '--no-security-group', + _testport.name, + ] + verifylist = [ + ('security_group', [sg2.id]), + ('no_security_group', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'security_group_ids': [sg2.id], + } + self.network.update_port.assert_called_once_with(_testport, **attrs) + self.assertIsNone(result) + + def test_set_allowed_address_pair(self): + arglist = [ + '--allowed-address', 'ip-address=192.168.1.123', + self._port.name, + ] + verifylist = [ + ('allowed_address_pairs', [{'ip-address': '192.168.1.123'}]), + ('port', self._port.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'allowed_address_pairs': [{'ip_address': '192.168.1.123'}], + } + self.network.update_port.assert_called_once_with(self._port, **attrs) + self.assertIsNone(result) + + def test_append_allowed_address_pair(self): + _testport = network_fakes.FakePort.create_one_port( + {'allowed_address_pairs': [{'ip_address': '192.168.1.123'}]}) + self.network.find_port = mock.Mock(return_value=_testport) + arglist = [ + '--allowed-address', 'ip-address=192.168.1.45', + _testport.name, + ] + verifylist = [ + ('allowed_address_pairs', [{'ip-address': '192.168.1.45'}]), + ('port', _testport.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'allowed_address_pairs': [{'ip_address': '192.168.1.123'}, + {'ip_address': '192.168.1.45'}], + } + self.network.update_port.assert_called_once_with(_testport, **attrs) + self.assertIsNone(result) + + def test_overwrite_allowed_address_pair(self): + _testport = network_fakes.FakePort.create_one_port( + {'allowed_address_pairs': [{'ip_address': '192.168.1.123'}]}) + self.network.find_port = mock.Mock(return_value=_testport) + arglist = [ + '--allowed-address', 'ip-address=192.168.1.45', + '--no-allowed-address', + _testport.name, + ] + verifylist = [ + ('allowed_address_pairs', [{'ip-address': '192.168.1.45'}]), + ('no_allowed_address_pair', True), + ('port', _testport.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'allowed_address_pairs': [{'ip_address': '192.168.1.45'}], + } + self.network.update_port.assert_called_once_with(_testport, **attrs) + self.assertIsNone(result) + + def test_set_no_allowed_address_pairs(self): + arglist = [ + '--no-allowed-address', + self._port.name, + ] + verifylist = [ + ('no_allowed_address_pair', True), + ('port', self._port.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'allowed_address_pairs': [], + } + self.network.update_port.assert_called_once_with(self._port, **attrs) + self.assertIsNone(result) + + def test_port_security_enabled(self): + arglist = [ + '--enable-port-security', + self._port.id, + ] + verifylist = [ + ('enable_port_security', True), + ('port', self._port.id,) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.network.update_port.assert_called_once_with(self._port, **{ + 'port_security_enabled': True, + }) + + def test_port_security_disabled(self): + arglist = [ + '--disable-port-security', + self._port.id, + ] + verifylist = [ + ('disable_port_security', True), + ('port', self._port.id,) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.network.update_port.assert_called_once_with(self._port, **{ + 'port_security_enabled': False, + }) + class TestShowPort(TestPort): @@ -767,3 +1448,86 @@ class TestUnsetPort(TestPort): self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + + def test_unset_security_group(self): + _fake_sg1 = network_fakes.FakeSecurityGroup.create_one_security_group() + _fake_sg2 = network_fakes.FakeSecurityGroup.create_one_security_group() + _fake_port = network_fakes.FakePort.create_one_port( + {'security_group_ids': [_fake_sg1.id, _fake_sg2.id]}) + self.network.find_port = mock.Mock(return_value=_fake_port) + self.network.find_security_group = mock.Mock(return_value=_fake_sg2) + arglist = [ + '--security-group', _fake_sg2.id, + _fake_port.name, + ] + verifylist = [ + ('security_group_ids', [_fake_sg2.id]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'security_group_ids': [_fake_sg1.id] + } + self.network.update_port.assert_called_once_with( + _fake_port, **attrs) + self.assertIsNone(result) + + def test_unset_port_security_group_not_existent(self): + _fake_sg1 = network_fakes.FakeSecurityGroup.create_one_security_group() + _fake_sg2 = network_fakes.FakeSecurityGroup.create_one_security_group() + _fake_port = network_fakes.FakePort.create_one_port( + {'security_group_ids': [_fake_sg1.id]}) + self.network.find_security_group = mock.Mock(return_value=_fake_sg2) + arglist = [ + '--security-group', _fake_sg2.id, + _fake_port.name, + ] + verifylist = [ + ('security_group_ids', [_fake_sg2.id]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_unset_port_allowed_address_pair(self): + _fake_port = network_fakes.FakePort.create_one_port( + {'allowed_address_pairs': [{'ip_address': '192.168.1.123'}]}) + self.network.find_port = mock.Mock(return_value=_fake_port) + arglist = [ + '--allowed-address', 'ip-address=192.168.1.123', + _fake_port.name, + ] + verifylist = [ + ('allowed_address_pairs', [{'ip-address': '192.168.1.123'}]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'allowed_address_pairs': [], + } + + self.network.update_port.assert_called_once_with(_fake_port, **attrs) + self.assertIsNone(result) + + def test_unset_port_allowed_address_pair_not_existent(self): + _fake_port = network_fakes.FakePort.create_one_port( + {'allowed_address_pairs': [{'ip_address': '192.168.1.123'}]}) + self.network.find_port = mock.Mock(return_value=_fake_port) + arglist = [ + '--allowed-address', 'ip-address=192.168.1.45', + _fake_port.name, + ] + verifylist = [ + ('allowed_address_pairs', [{'ip-address': '192.168.1.45'}]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 6a445862..b837afd1 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -18,6 +18,7 @@ from osc_lib import exceptions from osc_lib import utils as osc_utils from openstackclient.network.v2 import router +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -29,6 +30,7 @@ class TestRouter(network_fakes.TestNetworkV2): # Get a shortcut to the network client self.network = self.app.client_manager.network + self.projects_mock = self.app.client_manager.identity.projects class TestAddPortToRouter(TestRouter): @@ -427,6 +429,97 @@ class TestListRouter(TestRouter): self.assertEqual(self.columns_long_no_az, columns) self.assertEqual(self.data_long_no_az, list(data)) + def test_list_name(self): + test_name = "fakename" + arglist = [ + '--name', test_name, + ] + verifylist = [ + ('long', False), + ('name', test_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.routers.assert_called_once_with( + **{'name': test_name} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_router_list_enable(self): + arglist = [ + '--enable', + ] + verifylist = [ + ('long', False), + ('enable', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.routers.assert_called_once_with( + **{'admin_state_up': True, 'is_admin_state_up': True} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_router_list_disable(self): + arglist = [ + '--disable', + ] + verifylist = [ + ('long', False), + ('disable', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.routers.assert_called_once_with( + **{'admin_state_up': False, 'is_admin_state_up': False} + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_router_list_project(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id, 'project_id': project.id} + + self.network.routers.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_router_list_project_domain(self): + project = identity_fakes_v3.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + '--project-domain', project.domain_id, + ] + verifylist = [ + ('project', project.id), + ('project_domain', project.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id, 'project_id': project.id} + + self.network.routers.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestRemovePortFromRouter(TestRouter): '''Remove port from a Router ''' @@ -509,17 +602,19 @@ class TestSetRouter(TestRouter): # The router to set. _default_route = {'destination': '10.20.20.0/24', 'nexthop': '10.20.30.1'} + _network = network_fakes.FakeNetwork.create_one_network() + _subnet = network_fakes.FakeSubnet.create_one_subnet() _router = network_fakes.FakeRouter.create_one_router( attrs={'routes': [_default_route]} ) def setUp(self): super(TestSetRouter, self).setUp() - + self.network.router_add_gateway = mock.Mock() self.network.update_router = mock.Mock(return_value=None) - self.network.find_router = mock.Mock(return_value=self._router) - + self.network.find_network = mock.Mock(return_value=self._network) + self.network.find_subnet = mock.Mock(return_value=self._subnet) # Get the command object to test self.cmd = router.SetRouter(self.app, self.namespace) @@ -529,6 +624,7 @@ class TestSetRouter(TestRouter): '--enable', '--distributed', '--name', 'noob', + '--no-ha', '--description', 'router', ] verifylist = [ @@ -537,6 +633,7 @@ class TestSetRouter(TestRouter): ('distributed', True), ('name', 'noob'), ('description', 'router'), + ('no_ha', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -546,6 +643,7 @@ class TestSetRouter(TestRouter): 'admin_state_up': True, 'distributed': True, 'name': 'noob', + 'ha': False, 'description': 'router', } self.network.update_router.assert_called_once_with( @@ -557,11 +655,13 @@ class TestSetRouter(TestRouter): self._router.name, '--disable', '--centralized', + '--ha', ] verifylist = [ ('router', self._router.name), ('disable', True), ('centralized', True), + ('ha', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -570,6 +670,7 @@ class TestSetRouter(TestRouter): attrs = { 'admin_state_up': False, 'distributed': False, + 'ha': True, } self.network.update_router.assert_called_once_with( self._router, **attrs) @@ -603,10 +704,10 @@ class TestSetRouter(TestRouter): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - + routes = [{'destination': '10.20.30.0/24', + 'nexthop': '10.20.30.1'}] attrs = { - 'routes': self._router.routes + [{'destination': '10.20.30.0/24', - 'nexthop': '10.20.30.1'}], + 'routes': routes + self._router.routes } self.network.update_router.assert_called_once_with( self._router, **attrs) @@ -632,21 +733,31 @@ class TestSetRouter(TestRouter): self._router, **attrs) self.assertIsNone(result) - def test_set_route_no_route(self): + def test_set_route_overwrite_route(self): + _testrouter = network_fakes.FakeRouter.create_one_router( + {'routes': [{"destination": "10.0.0.2", + "nexthop": "1.1.1.1"}]}) + self.network.find_router = mock.Mock(return_value=_testrouter) arglist = [ - self._router.name, + _testrouter.name, '--route', 'destination=10.20.30.0/24,gateway=10.20.30.1', '--no-route', ] verifylist = [ - ('router', self._router.name), + ('router', _testrouter.name), ('routes', [{'destination': '10.20.30.0/24', 'gateway': '10.20.30.1'}]), ('no_route', True), ] - - self.assertRaises(tests_utils.ParserException, self.check_parser, - self.cmd, arglist, verifylist) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'routes': [{'destination': '10.20.30.0/24', + 'nexthop': '10.20.30.1'}] + } + self.network.update_router.assert_called_once_with( + _testrouter, **attrs) + self.assertIsNone(result) def test_set_clear_routes(self): arglist = [ @@ -668,21 +779,31 @@ class TestSetRouter(TestRouter): self._router, **attrs) self.assertIsNone(result) - def test_set_route_clear_routes(self): + def test_overwrite_route_clear_routes(self): + _testrouter = network_fakes.FakeRouter.create_one_router( + {'routes': [{"destination": "10.0.0.2", + "nexthop": "1.1.1.1"}]}) + self.network.find_router = mock.Mock(return_value=_testrouter) arglist = [ - self._router.name, + _testrouter.name, '--route', 'destination=10.20.30.0/24,gateway=10.20.30.1', '--clear-routes', ] verifylist = [ - ('router', self._router.name), + ('router', _testrouter.name), ('routes', [{'destination': '10.20.30.0/24', 'gateway': '10.20.30.1'}]), ('clear_routes', True), ] - - self.assertRaises(tests_utils.ParserException, self.check_parser, - self.cmd, arglist, verifylist) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = { + 'routes': [{'destination': '10.20.30.0/24', + 'nexthop': '10.20.30.1'}] + } + self.network.update_router.assert_called_once_with( + _testrouter, **attrs) + self.assertIsNone(result) def test_set_nothing(self): arglist = [ @@ -700,6 +821,110 @@ class TestSetRouter(TestRouter): self._router, **attrs) self.assertIsNone(result) + def test_wrong_gateway_params(self): + arglist = [ + "--fixed-ip", "subnet='abc'", + self._router.id, + ] + verifylist = [ + ('fixed_ip', [{'subnet': "'abc'"}]), + ('router', self._router.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + + def test_set_gateway_network_only(self): + arglist = [ + "--external-gateway", self._network.id, + self._router.id, + ] + verifylist = [ + ('external_gateway', self._network.id), + ('router', self._router.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.update_router.assert_called_with( + self._router, **{'external_gateway_info': { + 'network_id': self._network.id}}) + self.assertIsNone(result) + + def test_set_gateway_options_subnet_only(self): + arglist = [ + "--external-gateway", self._network.id, + "--fixed-ip", "subnet='abc'", + self._router.id, + '--enable-snat', + ] + verifylist = [ + ('router', self._router.id), + ('external_gateway', self._network.id), + ('fixed_ip', [{'subnet': "'abc'"}]), + ('enable_snat', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.update_router.assert_called_with( + self._router, **{'external_gateway_info': { + 'network_id': self._network.id, + 'external_fixed_ips': [{ + 'subnet_id': self._subnet.id, }], + 'enable_snat': True, }}) + self.assertIsNone(result) + + def test_set_gateway_option_ipaddress_only(self): + arglist = [ + "--external-gateway", self._network.id, + "--fixed-ip", "ip-address=10.0.1.1", + self._router.id, + '--enable-snat', + ] + verifylist = [ + ('router', self._router.id), + ('external_gateway', self._network.id), + ('fixed_ip', [{'ip-address': "10.0.1.1"}]), + ('enable_snat', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.update_router.assert_called_with( + self._router, **{'external_gateway_info': { + 'network_id': self._network.id, + 'external_fixed_ips': [{ + 'ip_address': "10.0.1.1", }], + 'enable_snat': True, }}) + self.assertIsNone(result) + + def test_set_gateway_options_subnet_ipaddress(self): + arglist = [ + "--external-gateway", self._network.id, + "--fixed-ip", "subnet='abc',ip-address=10.0.1.1", + self._router.id, + '--enable-snat', + ] + verifylist = [ + ('router', self._router.id), + ('external_gateway', self._network.id), + ('fixed_ip', [{'subnet': "'abc'", + 'ip-address': "10.0.1.1"}]), + ('enable_snat', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.update_router.assert_called_with( + self._router, **{'external_gateway_info': { + 'network_id': self._network.id, + 'external_fixed_ips': [{ + 'subnet_id': self._subnet.id, + 'ip_address': "10.0.1.1", }], + 'enable_snat': True, }}) + self.assertIsNone(result) + class TestShowRouter(TestRouter): @@ -773,9 +998,9 @@ class TestUnsetRouter(TestRouter): super(TestUnsetRouter, self).setUp() self._testrouter = network_fakes.FakeRouter.create_one_router( {'routes': [{"destination": "192.168.101.1/24", - "gateway": "172.24.4.3"}, + "nexthop": "172.24.4.3"}, {"destination": "192.168.101.2/24", - "gateway": "172.24.4.3"}], }) + "nexthop": "172.24.4.3"}], }) self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet() self.network.find_router = mock.Mock(return_value=self._testrouter) self.network.update_router = mock.Mock(return_value=None) @@ -816,3 +1041,16 @@ class TestUnsetRouter(TestRouter): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + + def test_unset_router_external_gateway(self): + arglist = [ + '--external-gateway', + self._testrouter.name, + ] + verifylist = [('external_gateway', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + attrs = {'external_gateway_info': {}} + self.network.update_router.assert_called_once_with( + self._testrouter, **attrs) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/test_security_group.py b/openstackclient/tests/unit/network/v2/test_security_group.py index 2615b77a..66d357f9 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group.py +++ b/openstackclient/tests/unit/network/v2/test_security_group.py @@ -404,7 +404,7 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): grp.id, grp.name, grp.description, - grp.tenant_id, + grp.project_id, )) def setUp(self): @@ -444,6 +444,44 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_security_group_list_project(self): + project = identity_fakes.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id, 'project_id': project.id} + + self.network.security_groups.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_security_group_list_project_domain(self): + project = identity_fakes.FakeProject.create_one_project() + self.projects_mock.get.return_value = project + arglist = [ + '--project', project.id, + '--project-domain', project.domain_id, + ] + verifylist = [ + ('project', project.id), + ('project_domain', project.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'tenant_id': project.id, 'project_id': project.id} + + self.network.security_groups.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestListSecurityGroupCompute(TestSecurityGroupCompute): diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule.py b/openstackclient/tests/unit/network/v2/test_security_group_rule.py index 96d58e5c..e3538d5f 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule.py @@ -60,8 +60,9 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): network_fakes.FakeSecurityGroup.create_one_security_group() expected_columns = ( + 'description', 'direction', - 'ethertype', + 'ether_type', 'id', 'port_range_max', 'port_range_min', @@ -81,8 +82,9 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.network.create_security_group_rule = mock.Mock( return_value=self._security_group_rule) self.expected_data = ( + self._security_group_rule.description, self._security_group_rule.direction, - self._security_group_rule.ethertype, + self._security_group_rule.ether_type, self._security_group_rule.id, self._security_group_rule.port_range_max, self._security_group_rule.port_range_min, @@ -119,6 +121,15 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, []) + def test_create_all_remote_options(self): + arglist = [ + '--remote-ip', '10.10.0.0/24', + '--remote-group', self._security_group.id, + self._security_group.id, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + def test_create_bad_ethertype(self): arglist = [ '--ethertype', 'foo', @@ -173,7 +184,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.network.create_security_group_rule.assert_called_once_with(**{ 'direction': self._security_group_rule.direction, - 'ethertype': self._security_group_rule.ethertype, + 'ethertype': self._security_group_rule.ether_type, 'port_range_max': self._security_group_rule.port_range_max, 'port_range_min': self._security_group_rule.port_range_min, 'protocol': self._security_group_rule.protocol, @@ -205,7 +216,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.network.create_security_group_rule.assert_called_once_with(**{ 'direction': self._security_group_rule.direction, - 'ethertype': self._security_group_rule.ethertype, + 'ethertype': self._security_group_rule.ether_type, 'protocol': self._security_group_rule.protocol, 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, 'security_group_id': self._security_group.id, @@ -213,7 +224,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.assertEqual(self.expected_columns, columns) self.assertEqual(self.expected_data, data) - def test_create_source_group(self): + def test_create_remote_group(self): self._setup_security_group_rule({ 'port_range_max': 22, 'port_range_min': 22, @@ -238,7 +249,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.network.create_security_group_rule.assert_called_once_with(**{ 'direction': self._security_group_rule.direction, - 'ethertype': self._security_group_rule.ethertype, + 'ethertype': self._security_group_rule.ether_type, 'port_range_max': self._security_group_rule.port_range_max, 'port_range_min': self._security_group_rule.port_range_min, 'protocol': self._security_group_rule.protocol, @@ -248,6 +259,34 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.assertEqual(self.expected_columns, columns) self.assertEqual(self.expected_data, data) + def test_create_source_group(self): + self._setup_security_group_rule({ + 'remote_group_id': self._security_group.id, + }) + arglist = [ + '--ingress', + '--src-group', self._security_group.name, + self._security_group.id, + ] + verifylist = [ + ('ingress', True), + ('src_group', self._security_group.name), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ether_type, + 'protocol': self._security_group_rule.protocol, + 'remote_group_id': self._security_group_rule.remote_group_id, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + def test_create_source_ip(self): self._setup_security_group_rule({ 'protocol': 'icmp', @@ -269,7 +308,36 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.network.create_security_group_rule.assert_called_once_with(**{ 'direction': self._security_group_rule.direction, - 'ethertype': self._security_group_rule.ethertype, + 'ethertype': self._security_group_rule.ether_type, + 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + def test_create_remote_ip(self): + self._setup_security_group_rule({ + 'protocol': 'icmp', + 'remote_ip_prefix': '10.0.2.0/24', + }) + arglist = [ + '--protocol', self._security_group_rule.protocol, + '--remote-ip', self._security_group_rule.remote_ip_prefix, + self._security_group.id, + ] + verifylist = [ + ('protocol', self._security_group_rule.protocol), + ('remote_ip', self._security_group_rule.remote_ip_prefix), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ether_type, 'protocol': self._security_group_rule.protocol, 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, 'security_group_id': self._security_group.id, @@ -280,7 +348,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): def test_create_network_options(self): self._setup_security_group_rule({ 'direction': 'egress', - 'ethertype': 'IPv6', + 'ether_type': 'IPv6', 'port_range_max': 443, 'port_range_min': 443, 'protocol': '6', @@ -290,7 +358,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): arglist = [ '--dst-port', str(self._security_group_rule.port_range_min), '--egress', - '--ethertype', self._security_group_rule.ethertype, + '--ethertype', self._security_group_rule.ether_type, '--project', self.project.name, '--project-domain', self.domain.name, '--protocol', self._security_group_rule.protocol, @@ -300,7 +368,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): ('dst_port', (self._security_group_rule.port_range_min, self._security_group_rule.port_range_max)), ('egress', True), - ('ethertype', self._security_group_rule.ethertype), + ('ethertype', self._security_group_rule.ether_type), ('project', self.project.name), ('project_domain', self.domain.name), ('protocol', self._security_group_rule.protocol), @@ -312,7 +380,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.network.create_security_group_rule.assert_called_once_with(**{ 'direction': self._security_group_rule.direction, - 'ethertype': self._security_group_rule.ethertype, + 'ethertype': self._security_group_rule.ether_type, 'port_range_max': self._security_group_rule.port_range_max, 'port_range_min': self._security_group_rule.port_range_min, 'protocol': self._security_group_rule.protocol, @@ -376,7 +444,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.network.create_security_group_rule.assert_called_once_with(**{ 'direction': self._security_group_rule.direction, - 'ethertype': self._security_group_rule.ethertype, + 'ethertype': self._security_group_rule.ether_type, 'port_range_min': self._security_group_rule.port_range_min, 'protocol': self._security_group_rule.protocol, 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, @@ -387,7 +455,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): def test_create_ipv6_icmp_type_code(self): self._setup_security_group_rule({ - 'ethertype': 'IPv6', + 'ether_type': 'IPv6', 'port_range_min': 139, 'port_range_max': 2, 'protocol': 'ipv6-icmp', @@ -411,7 +479,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.network.create_security_group_rule.assert_called_once_with(**{ 'direction': self._security_group_rule.direction, - 'ethertype': self._security_group_rule.ethertype, + 'ethertype': self._security_group_rule.ether_type, 'port_range_min': self._security_group_rule.port_range_min, 'port_range_max': self._security_group_rule.port_range_max, 'protocol': self._security_group_rule.protocol, @@ -422,7 +490,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): def test_create_icmpv6_type(self): self._setup_security_group_rule({ - 'ethertype': 'IPv6', + 'ether_type': 'IPv6', 'port_range_min': 139, 'protocol': 'icmpv6', }) @@ -444,7 +512,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.network.create_security_group_rule.assert_called_once_with(**{ 'direction': self._security_group_rule.direction, - 'ethertype': self._security_group_rule.ethertype, + 'ethertype': self._security_group_rule.ether_type, 'port_range_min': self._security_group_rule.port_range_min, 'protocol': self._security_group_rule.protocol, 'security_group_id': self._security_group.id, @@ -452,6 +520,33 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.assertEqual(self.expected_columns, columns) self.assertEqual(self.expected_data, data) + def test_create_with_description(self): + self._setup_security_group_rule({ + 'description': 'Setting SGR', + }) + arglist = [ + '--description', self._security_group_rule.description, + self._security_group.id, + ] + verifylist = [ + ('description', self._security_group_rule.description), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'description': self._security_group_rule.description, + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ether_type, + 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + class TestCreateSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): @@ -498,6 +593,15 @@ class TestCreateSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, []) + def test_create_all_remote_options(self): + arglist = [ + '--remote-ip', '10.10.0.0/24', + '--remote-group', self._security_group.id, + self._security_group.id, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + def test_create_bad_protocol(self): arglist = [ '--protocol', 'foo', @@ -588,6 +692,38 @@ class TestCreateSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, data) + def test_create_remote_group(self): + expected_columns, expected_data = self._setup_security_group_rule({ + 'from_port': 22, + 'to_port': 22, + 'group': {'name': self._security_group.name}, + }) + arglist = [ + '--dst-port', str(self._security_group_rule.from_port), + '--remote-group', self._security_group.name, + self._security_group.id, + ] + verifylist = [ + ('dst_port', (self._security_group_rule.from_port, + self._security_group_rule.to_port)), + ('remote_group', self._security_group.name), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.security_group_rules.create.assert_called_once_with( + self._security_group.id, + self._security_group_rule.ip_protocol, + self._security_group_rule.from_port, + self._security_group_rule.to_port, + self._security_group_rule.ip_range['cidr'], + self._security_group.id, + ) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) + def test_create_source_ip(self): expected_columns, expected_data = self._setup_security_group_rule({ 'ip_protocol': 'icmp', @@ -620,6 +756,38 @@ class TestCreateSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, data) + def test_create_remote_ip(self): + expected_columns, expected_data = self._setup_security_group_rule({ + 'ip_protocol': 'icmp', + 'from_port': -1, + 'to_port': -1, + 'ip_range': {'cidr': '10.0.2.0/24'}, + }) + arglist = [ + '--protocol', self._security_group_rule.ip_protocol, + '--remote-ip', self._security_group_rule.ip_range['cidr'], + self._security_group.id, + ] + verifylist = [ + ('protocol', self._security_group_rule.ip_protocol), + ('remote_ip', self._security_group_rule.ip_range['cidr']), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.security_group_rules.create.assert_called_once_with( + self._security_group.id, + self._security_group_rule.ip_protocol, + self._security_group_rule.from_port, + self._security_group_rule.to_port, + self._security_group_rule.ip_range['cidr'], + None, + ) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) + def test_create_proto_option(self): expected_columns, expected_data = self._setup_security_group_rule({ 'ip_protocol': 'icmp', @@ -871,7 +1039,7 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): security_group_rule._format_network_port_range( _security_group_rule), _security_group_rule.direction, - _security_group_rule.ethertype, + _security_group_rule.ether_type, _security_group_rule.remote_group_id, )) expected_data_no_group.append(( @@ -942,6 +1110,60 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.assertEqual(self.expected_columns_no_group, columns) self.assertEqual(self.expected_data_no_group, list(data)) + def test_list_with_protocol(self): + self._security_group_rule_tcp.port_range_min = 80 + arglist = [ + '--protocol', 'tcp', + ] + verifylist = [ + ('protocol', 'tcp'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.security_group_rules.assert_called_once_with(**{ + 'protocol': 'tcp', + }) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) + + def test_list_with_ingress(self): + self._security_group_rule_tcp.port_range_min = 80 + arglist = [ + '--ingress', + ] + verifylist = [ + ('ingress', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.security_group_rules.assert_called_once_with(**{ + 'direction': 'ingress', + }) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) + + def test_list_with_wrong_egress(self): + self._security_group_rule_tcp.port_range_min = 80 + arglist = [ + '--egress', + ] + verifylist = [ + ('egress', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.security_group_rules.assert_called_once_with(**{ + 'direction': 'egress', + }) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) + class TestListSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): @@ -1075,8 +1297,9 @@ class TestShowSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): network_fakes.FakeSecurityGroupRule.create_one_security_group_rule() columns = ( + 'description', 'direction', - 'ethertype', + 'ether_type', 'id', 'port_range_max', 'port_range_min', @@ -1088,8 +1311,9 @@ class TestShowSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): ) data = ( + _security_group_rule.description, _security_group_rule.direction, - _security_group_rule.ethertype, + _security_group_rule.ether_type, _security_group_rule.id, _security_group_rule.port_range_max, _security_group_rule.port_range_min, diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 2d51aa4a..47de5616 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -636,7 +636,7 @@ class TestListSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'enable_dhcp': True} + filters = {'enable_dhcp': True, 'is_dhcp_enabled': True} self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -652,7 +652,7 @@ class TestListSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'enable_dhcp': False} + filters = {'enable_dhcp': False, 'is_dhcp_enabled': False} self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -685,7 +685,7 @@ class TestListSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id} + filters = {'tenant_id': project.id, 'project_id': project.id} self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -723,7 +723,7 @@ class TestListSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id} + filters = {'tenant_id': project.id, 'project_id': project.id} self.network.subnets.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -925,13 +925,16 @@ class TestSetSubnet(TestSubnet): {'host_routes': [{'destination': '10.20.20.0/24', 'nexthop': '10.20.20.1'}], 'allocation_pools': [{'start': '8.8.8.200', - 'end': '8.8.8.250'}], }) + 'end': '8.8.8.250'}], + 'dns_nameservers': ["10.0.0.1"], }) self.network.find_subnet = mock.Mock(return_value=_testsubnet) arglist = [ '--host-route', 'destination=10.30.30.30/24,gateway=10.30.30.1', '--no-host-route', '--allocation-pool', 'start=8.8.8.100,end=8.8.8.150', '--no-allocation-pool', + '--dns-nameserver', '10.1.10.1', + '--no-dns-nameservers', _testsubnet.name, ] verifylist = [ @@ -939,6 +942,8 @@ class TestSetSubnet(TestSubnet): "destination": "10.30.30.30/24", "gateway": "10.30.30.1"}]), ('allocation_pools', [{ 'start': '8.8.8.100', 'end': '8.8.8.150'}]), + ('dns_nameservers', ['10.1.10.1']), + ('no_dns_nameservers', True), ('no_host_route', True), ('no_allocation_pool', True), ] @@ -948,6 +953,7 @@ class TestSetSubnet(TestSubnet): 'host_routes': [{ "destination": "10.30.30.30/24", "nexthop": "10.30.30.1"}], 'allocation_pools': [{'start': '8.8.8.100', 'end': '8.8.8.150'}], + 'dns_nameservers': ["10.1.10.1"], } self.network.update_subnet.assert_called_once_with( _testsubnet, **attrs) diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py index fa6ffff3..f12537e7 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py @@ -435,7 +435,7 @@ class TestListSubnetPool(TestSubnetPool): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'shared': False} + filters = {'shared': False, 'is_shared': False} self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -451,7 +451,7 @@ class TestListSubnetPool(TestSubnetPool): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'shared': True} + filters = {'shared': True, 'is_shared': True} self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -501,7 +501,7 @@ class TestListSubnetPool(TestSubnetPool): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id} + filters = {'tenant_id': project.id, 'project_id': project.id} self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) @@ -521,7 +521,7 @@ class TestListSubnetPool(TestSubnetPool): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id} + filters = {'tenant_id': project.id, 'project_id': project.id} self.network.subnet_pools.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) diff --git a/openstackclient/tests/unit/test_shell.py b/openstackclient/tests/unit/test_shell.py index 3d91da9b..b9fac684 100644 --- a/openstackclient/tests/unit/test_shell.py +++ b/openstackclient/tests/unit/test_shell.py @@ -422,7 +422,7 @@ class TestShellArgV(TestShell): Use the argv supplied by the test runner so we get actual Python runtime behaviour; we only need to check the type of argv[0] - which will alwyas be present. + which will always be present. """ with mock.patch( diff --git a/openstackclient/tests/unit/volume/v1/fakes.py b/openstackclient/tests/unit/volume/v1/fakes.py index 3999543c..fff5181d 100644 --- a/openstackclient/tests/unit/volume/v1/fakes.py +++ b/openstackclient/tests/unit/volume/v1/fakes.py @@ -23,115 +23,6 @@ from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes from openstackclient.tests.unit import utils -volume_id = 'vvvvvvvv-vvvv-vvvv-vvvvvvvv' -volume_name = 'nigel' -volume_description = 'Nigel Tufnel' -volume_status = 'available' -volume_size = 120 -volume_type = 'to-eleven' -volume_zone = 'stonehenge' -volume_metadata = { - 'Alpha': 'a', - 'Beta': 'b', - 'Gamma': 'g', -} -volume_metadata_str = "Alpha='a', Beta='b', Gamma='g'" - -VOLUME = { - 'id': volume_id, - 'display_name': volume_name, - 'display_description': volume_description, - 'size': volume_size, - 'status': volume_status, - 'attach_status': 'detached', - 'availability_zone': volume_zone, - 'volume_type': volume_type, - 'metadata': volume_metadata, -} - -extension_name = 'SchedulerHints' -extension_namespace = 'http://docs.openstack.org/'\ - 'block-service/ext/scheduler-hints/api/v2' -extension_description = 'Pass arbitrary key/value'\ - 'pairs to the scheduler.' -extension_updated = '2014-02-07T12:00:0-00:00' -extension_alias = 'OS-SCH-HNT' -extension_links = '[{"href":'\ - '"https://github.com/openstack/block-api", "type":'\ - ' "text/html", "rel": "describedby"}]' - -EXTENSION = { - 'name': extension_name, - 'namespace': extension_namespace, - 'description': extension_description, - 'updated': extension_updated, - 'alias': extension_alias, - 'links': extension_links, -} - -# NOTE(dtroyer): duplicating here the minimum image info needed to test -# volume create --image until circular references can be -# avoided by refactoring the test fakes. - -image_id = 'im1' -image_name = 'graven' - - -IMAGE = { - 'id': image_id, - 'name': image_name, -} - -type_id = "5520dc9e-6f9b-4378-a719-729911c0f407" -type_name = "fake-lvmdriver-1" - -TYPE = { - 'id': type_id, - 'name': type_name -} - -qos_id = '6f2be1de-997b-4230-b76c-a3633b59e8fb' -qos_consumer = 'front-end' -qos_default_consumer = 'both' -qos_name = "fake-qos-specs" -qos_specs = { - 'foo': 'bar', - 'iops': '9001' -} -qos_association = { - 'association_type': 'volume_type', - 'name': type_name, - 'id': type_id -} - -QOS = { - 'id': qos_id, - 'consumer': qos_consumer, - 'name': qos_name -} - -QOS_DEFAULT_CONSUMER = { - 'id': qos_id, - 'consumer': qos_default_consumer, - 'name': qos_name -} - -QOS_WITH_SPECS = { - 'id': qos_id, - 'consumer': qos_consumer, - 'name': qos_name, - 'specs': qos_specs -} - -QOS_WITH_ASSOCIATIONS = { - 'id': qos_id, - 'consumer': qos_consumer, - 'name': qos_name, - 'specs': qos_specs, - 'associations': [qos_association] -} - - class FakeTransfer(object): """Fake one or more Transfer.""" @@ -306,6 +197,32 @@ class FakeQos(object): return qos @staticmethod + def create_one_qos_association(attrs=None): + """Create a fake Qos specification association. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, association_type, etc. + """ + attrs = attrs or {} + + # Set default attributes. + qos_association_info = { + "id": 'type-id-' + uuid.uuid4().hex, + "name": 'type-name-' + uuid.uuid4().hex, + "association_type": 'volume_type', + } + + # Overwrite default attributes. + qos_association_info.update(attrs) + + qos_association = fakes.FakeResource( + info=copy.deepcopy(qos_association_info), + loaded=True) + return qos_association + + @staticmethod def create_qoses(attrs=None, count=2): """Create multiple fake Qos specifications. @@ -447,6 +364,9 @@ class FakeVolumev1Client(object): self.qos_specs.resource_class = fakes.FakeResource(None, {}) self.volume_types = mock.Mock() self.volume_types.resource_class = fakes.FakeResource(None, {}) + self.volume_encryption_types = mock.Mock() + self.volume_encryption_types.resource_class = ( + fakes.FakeResource(None, {})) self.transfers = mock.Mock() self.transfers.resource_class = fakes.FakeResource(None, {}) self.volume_snapshots = mock.Mock() @@ -553,6 +473,34 @@ class FakeType(object): return mock.Mock(side_effect=types) + @staticmethod + def create_one_encryption_type(attrs=None): + """Create a fake encryption type. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with volume_type_id etc. + """ + attrs = attrs or {} + + # Set default attributes. + encryption_info = { + "volume_type_id": 'type-id-' + uuid.uuid4().hex, + 'provider': 'LuksEncryptor', + 'cipher': None, + 'key_size': None, + 'control_location': 'front-end', + } + + # Overwrite default attributes. + encryption_info.update(attrs) + + encryption_type = fakes.FakeResource( + info=copy.deepcopy(encryption_info), + loaded=True) + return encryption_type + class FakeSnapshot(object): """Fake one or more snapshot.""" diff --git a/openstackclient/tests/unit/volume/v1/test_backup.py b/openstackclient/tests/unit/volume/v1/test_backup.py index 32c2fd22..1097d3f1 100644 --- a/openstackclient/tests/unit/volume/v1/test_backup.py +++ b/openstackclient/tests/unit/volume/v1/test_backup.py @@ -249,26 +249,65 @@ class TestBackupList(TestBackup): self.volumes_mock.list.return_value = [self.volume] self.backups_mock.list.return_value = self.backups + self.volumes_mock.get.return_value = self.volume # Get the command to test self.cmd = backup.ListVolumeBackup(self.app, None) def test_backup_list_without_options(self): arglist = [] - verifylist = [("long", False)] + verifylist = [ + ("long", False), + ("name", None), + ("status", None), + ("volume", None), + ('all_projects', False), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) + search_opts = { + "name": None, + "status": None, + "volume_id": None, + "all_tenants": False, + } + self.volumes_mock.get.assert_not_called() + self.backups_mock.list.assert_called_with( + search_opts=search_opts, + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) def test_backup_list_with_options(self): - arglist = ["--long"] - verifylist = [("long", True)] + arglist = [ + "--long", + "--name", self.backups[0].name, + "--status", "error", + "--volume", self.volume.id, + "--all-projects" + ] + verifylist = [ + ("long", True), + ("name", self.backups[0].name), + ("status", "error"), + ("volume", self.volume.id), + ('all_projects', True), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) + search_opts = { + "name": self.backups[0].name, + "status": "error", + "volume_id": self.volume.id, + "all_tenants": True, + } + self.volumes_mock.get.assert_called_once_with(self.volume.id) + self.backups_mock.list.assert_called_with( + search_opts=search_opts, + ) self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) diff --git a/openstackclient/tests/unit/volume/v1/test_qos_specs.py b/openstackclient/tests/unit/volume/v1/test_qos_specs.py index 1982980a..e3dc1e78 100644 --- a/openstackclient/tests/unit/volume/v1/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v1/test_qos_specs.py @@ -13,14 +13,12 @@ # under the License. # -import copy import mock from mock import call from osc_lib import exceptions from osc_lib import utils -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes from openstackclient.volume.v1 import qos_specs @@ -39,38 +37,33 @@ class TestQos(volume_fakes.TestVolumev1): class TestQosAssociate(TestQos): + volume_type = volume_fakes.FakeType.create_one_type() + qos_spec = volume_fakes.FakeQos.create_one_qos() + def setUp(self): super(TestQosAssociate, self).setUp() + self.qos_mock.get.return_value = self.qos_spec + self.types_mock.get.return_value = self.volume_type # Get the command object to test self.cmd = qos_specs.AssociateQos(self.app, None) def test_qos_associate(self): - self.qos_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.QOS), - loaded=True - ) - self.types_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.TYPE), - loaded=True - ) arglist = [ - volume_fakes.qos_id, - volume_fakes.type_id + self.qos_spec.id, + self.volume_type.id ] verifylist = [ - ('qos_spec', volume_fakes.qos_id), - ('volume_type', volume_fakes.type_id) + ('qos_spec', self.qos_spec.id), + ('volume_type', self.volume_type.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.qos_mock.associate.assert_called_with( - volume_fakes.qos_id, - volume_fakes.type_id + self.qos_spec.id, + self.volume_type.id ) self.assertIsNone(result) @@ -81,112 +74,86 @@ class TestQosCreate(TestQos): 'consumer', 'id', 'name', - ) - datalist = ( - volume_fakes.qos_consumer, - volume_fakes.qos_id, - volume_fakes.qos_name + 'properties' ) def setUp(self): super(TestQosCreate, self).setUp() - + self.new_qos_spec = volume_fakes.FakeQos.create_one_qos() + self.datalist = ( + self.new_qos_spec.consumer, + self.new_qos_spec.id, + self.new_qos_spec.name, + utils.format_dict(self.new_qos_spec.specs) + ) + self.qos_mock.create.return_value = self.new_qos_spec # Get the command object to test self.cmd = qos_specs.CreateQos(self.app, None) def test_qos_create_without_properties(self): - self.qos_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.QOS_DEFAULT_CONSUMER), - loaded=True - ) - arglist = [ - volume_fakes.qos_name, + self.new_qos_spec.name, ] verifylist = [ - ('name', volume_fakes.qos_name), + ('name', self.new_qos_spec.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.qos_mock.create.assert_called_with( - volume_fakes.qos_name, - {'consumer': volume_fakes.qos_default_consumer} + self.new_qos_spec.name, + {'consumer': 'both'} ) self.assertEqual(self.columns, columns) - datalist = ( - volume_fakes.qos_default_consumer, - volume_fakes.qos_id, - volume_fakes.qos_name - ) - self.assertEqual(datalist, data) + self.assertEqual(self.datalist, data) def test_qos_create_with_consumer(self): - self.qos_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.QOS), - loaded=True - ) - arglist = [ - volume_fakes.qos_name, - '--consumer', volume_fakes.qos_consumer + '--consumer', self.new_qos_spec.consumer, + self.new_qos_spec.name, ] verifylist = [ - ('name', volume_fakes.qos_name), - ('consumer', volume_fakes.qos_consumer) + ('consumer', self.new_qos_spec.consumer), + ('name', self.new_qos_spec.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.qos_mock.create.assert_called_with( - volume_fakes.qos_name, - {'consumer': volume_fakes.qos_consumer} + self.new_qos_spec.name, + {'consumer': self.new_qos_spec.consumer} ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) def test_qos_create_with_properties(self): - self.qos_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.QOS_WITH_SPECS), - loaded=True - ) - arglist = [ - volume_fakes.qos_name, - '--consumer', volume_fakes.qos_consumer, + '--consumer', self.new_qos_spec.consumer, '--property', 'foo=bar', - '--property', 'iops=9001' + '--property', 'iops=9001', + self.new_qos_spec.name, ] verifylist = [ - ('name', volume_fakes.qos_name), - ('consumer', volume_fakes.qos_consumer), - ('property', volume_fakes.qos_specs) + ('consumer', self.new_qos_spec.consumer), + ('property', self.new_qos_spec.specs), + ('name', self.new_qos_spec.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - specs = volume_fakes.qos_specs.copy() - specs.update({'consumer': volume_fakes.qos_consumer}) + self.new_qos_spec.specs.update( + {'consumer': self.new_qos_spec.consumer}) self.qos_mock.create.assert_called_with( - volume_fakes.qos_name, - specs + self.new_qos_spec.name, + self.new_qos_spec.specs ) - columns = self.columns + ( - 'specs', - ) - self.assertEqual(columns, columns) - datalist = self.datalist + ( - volume_fakes.qos_specs, - ) - self.assertEqual(datalist, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) class TestQosDelete(TestQos): @@ -294,79 +261,62 @@ class TestQosDelete(TestQos): class TestQosDisassociate(TestQos): + volume_type = volume_fakes.FakeType.create_one_type() + qos_spec = volume_fakes.FakeQos.create_one_qos() + def setUp(self): super(TestQosDisassociate, self).setUp() + self.qos_mock.get.return_value = self.qos_spec + self.types_mock.get.return_value = self.volume_type # Get the command object to test self.cmd = qos_specs.DisassociateQos(self.app, None) def test_qos_disassociate_with_volume_type(self): - self.qos_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.QOS), - loaded=True - ) - self.types_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.TYPE), - loaded=True - ) arglist = [ - volume_fakes.qos_id, - '--volume-type', volume_fakes.type_id + '--volume-type', self.volume_type.id, + self.qos_spec.id, ] verifylist = [ - ('qos_spec', volume_fakes.qos_id), - ('volume_type', volume_fakes.type_id) + ('volume_type', self.volume_type.id), + ('qos_spec', self.qos_spec.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.qos_mock.disassociate.assert_called_with( - volume_fakes.qos_id, - volume_fakes.type_id + self.qos_spec.id, + self.volume_type.id ) self.assertIsNone(result) def test_qos_disassociate_with_all_volume_types(self): - self.qos_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.QOS), - loaded=True - ) - arglist = [ - volume_fakes.qos_id, - '--all' + '--all', + self.qos_spec.id, ] verifylist = [ - ('qos_spec', volume_fakes.qos_id) + ('qos_spec', self.qos_spec.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.qos_mock.disassociate_all.assert_called_with(volume_fakes.qos_id) + self.qos_mock.disassociate_all.assert_called_with(self.qos_spec.id) self.assertIsNone(result) class TestQosList(TestQos): + qos_spec = volume_fakes.FakeQos.create_one_qos() + qos_association = volume_fakes.FakeQos.create_one_qos_association() + def setUp(self): super(TestQosList, self).setUp() - self.qos_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.QOS_WITH_ASSOCIATIONS), - loaded=True, - ) - self.qos_mock.list.return_value = [self.qos_mock.get.return_value] - self.qos_mock.get_associations.return_value = [fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.qos_association), - loaded=True, - )] + self.qos_mock.list.return_value = [self.qos_spec] + self.qos_mock.get_associations.return_value = [self.qos_association] # Get the command object to test self.cmd = qos_specs.ListQos(self.app, None) @@ -385,85 +335,76 @@ class TestQosList(TestQos): 'Name', 'Consumer', 'Associations', - 'Specs', + 'Properties', ) self.assertEqual(collist, columns) datalist = (( - volume_fakes.qos_id, - volume_fakes.qos_name, - volume_fakes.qos_consumer, - volume_fakes.type_name, - utils.format_dict(volume_fakes.qos_specs), + self.qos_spec.id, + self.qos_spec.name, + self.qos_spec.consumer, + self.qos_association.name, + utils.format_dict(self.qos_spec.specs), ), ) self.assertEqual(datalist, tuple(data)) class TestQosSet(TestQos): + qos_spec = volume_fakes.FakeQos.create_one_qos() + def setUp(self): super(TestQosSet, self).setUp() + self.qos_mock.get.return_value = self.qos_spec # Get the command object to test self.cmd = qos_specs.SetQos(self.app, None) def test_qos_set_with_properties_with_id(self): - self.qos_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.QOS_WITH_SPECS), - loaded=True - ) arglist = [ - volume_fakes.qos_id, '--property', 'foo=bar', - '--property', 'iops=9001' + '--property', 'iops=9001', + self.qos_spec.id, ] verifylist = [ - ('qos_spec', volume_fakes.qos_id), - ('property', volume_fakes.qos_specs) + ('property', self.qos_spec.specs), + ('qos_spec', self.qos_spec.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.qos_mock.set_keys.assert_called_with( - volume_fakes.qos_id, - volume_fakes.qos_specs + self.qos_spec.id, + self.qos_spec.specs ) self.assertIsNone(result) class TestQosShow(TestQos): + qos_spec = volume_fakes.FakeQos.create_one_qos() + qos_association = volume_fakes.FakeQos.create_one_qos_association() + def setUp(self): super(TestQosShow, self).setUp() - - self.qos_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.QOS_WITH_ASSOCIATIONS), - loaded=True, - ) - self.qos_mock.get_associations.return_value = [fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.qos_association), - loaded=True, - )] - + self.qos_mock.get.return_value = self.qos_spec + self.qos_mock.get_associations.return_value = [self.qos_association] # Get the command object to test self.cmd = qos_specs.ShowQos(self.app, None) def test_qos_show(self): arglist = [ - volume_fakes.qos_id + self.qos_spec.id ] verifylist = [ - ('qos_spec', volume_fakes.qos_id) + ('qos_spec', self.qos_spec.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.qos_mock.get.assert_called_with( - volume_fakes.qos_id + self.qos_spec.id ) collist = ( @@ -471,60 +412,57 @@ class TestQosShow(TestQos): 'consumer', 'id', 'name', - 'specs' + 'properties' ) self.assertEqual(collist, columns) datalist = ( - volume_fakes.type_name, - volume_fakes.qos_consumer, - volume_fakes.qos_id, - volume_fakes.qos_name, - utils.format_dict(volume_fakes.qos_specs), + self.qos_association.name, + self.qos_spec.consumer, + self.qos_spec.id, + self.qos_spec.name, + utils.format_dict(self.qos_spec.specs), ) self.assertEqual(datalist, tuple(data)) class TestQosUnset(TestQos): + qos_spec = volume_fakes.FakeQos.create_one_qos() + def setUp(self): super(TestQosUnset, self).setUp() + self.qos_mock.get.return_value = self.qos_spec # Get the command object to test self.cmd = qos_specs.UnsetQos(self.app, None) def test_qos_unset_with_properties(self): - self.qos_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.QOS), - loaded=True - ) arglist = [ - volume_fakes.qos_id, '--property', 'iops', - '--property', 'foo' + '--property', 'foo', + self.qos_spec.id, ] - verifylist = [ - ('qos_spec', volume_fakes.qos_id), - ('property', ['iops', 'foo']) + ('property', ['iops', 'foo']), + ('qos_spec', self.qos_spec.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.qos_mock.unset_keys.assert_called_with( - volume_fakes.qos_id, + self.qos_spec.id, ['iops', 'foo'] ) self.assertIsNone(result) def test_qos_unset_nothing(self): arglist = [ - volume_fakes.qos_id, + self.qos_spec.id, ] verifylist = [ - ('qos_spec', volume_fakes.qos_id), + ('qos_spec', self.qos_spec.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/unit/volume/v1/test_snapshot.py b/openstackclient/tests/unit/volume/v1/test_snapshot.py index edfbdc19..87a62b0a 100644 --- a/openstackclient/tests/unit/volume/v1/test_snapshot.py +++ b/openstackclient/tests/unit/volume/v1/test_snapshot.py @@ -19,7 +19,7 @@ from osc_lib import exceptions from osc_lib import utils from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes -from openstackclient.volume.v1 import snapshot +from openstackclient.volume.v1 import volume_snapshot class TestSnapshot(volume_fakes.TestVolumev1): @@ -67,20 +67,20 @@ class TestSnapshotCreate(TestSnapshot): self.volumes_mock.get.return_value = self.volume self.snapshots_mock.create.return_value = self.new_snapshot # Get the command object to test - self.cmd = snapshot.CreateSnapshot(self.app, None) + self.cmd = volume_snapshot.CreateVolumeSnapshot(self.app, None) def test_snapshot_create(self): arglist = [ - "--name", self.new_snapshot.display_name, + "--volume", self.new_snapshot.volume_id, "--description", self.new_snapshot.display_description, "--force", - self.new_snapshot.volume_id, + self.new_snapshot.display_name, ] verifylist = [ - ("name", self.new_snapshot.display_name), + ("volume", self.new_snapshot.volume_id), ("description", self.new_snapshot.display_description), ("force", True), - ("volume", self.new_snapshot.volume_id), + ("snapshot_name", self.new_snapshot.display_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -97,7 +97,7 @@ class TestSnapshotCreate(TestSnapshot): def test_snapshot_create_without_name(self): arglist = [ - self.new_snapshot.volume_id, + "--volume", self.new_snapshot.volume_id, "--description", self.new_snapshot.display_description, "--force" ] @@ -119,6 +119,32 @@ class TestSnapshotCreate(TestSnapshot): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_snapshot_create_without_volume(self): + arglist = [ + "--description", self.new_snapshot.display_description, + "--force", + self.new_snapshot.display_name + ] + verifylist = [ + ("description", self.new_snapshot.display_description), + ("force", True), + ("snapshot_name", self.new_snapshot.display_name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.get.assert_called_once_with( + self.new_snapshot.display_name) + self.snapshots_mock.create.assert_called_once_with( + self.new_snapshot.volume_id, + True, + self.new_snapshot.display_name, + self.new_snapshot.display_description, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + class TestSnapshotDelete(TestSnapshot): @@ -132,7 +158,7 @@ class TestSnapshotDelete(TestSnapshot): self.snapshots_mock.delete.return_value = None # Get the command object to mock - self.cmd = snapshot.DeleteSnapshot(self.app, None) + self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) def test_snapshot_delete(self): arglist = [ @@ -242,9 +268,10 @@ class TestSnapshotList(TestSnapshot): super(TestSnapshotList, self).setUp() self.volumes_mock.list.return_value = [self.volume] + self.volumes_mock.get.return_value = self.volume self.snapshots_mock.list.return_value = self.snapshots # Get the command to test - self.cmd = snapshot.ListSnapshot(self.app, None) + self.cmd = volume_snapshot.ListVolumeSnapshot(self.app, None) def test_snapshot_list_without_options(self): arglist = [] @@ -257,7 +284,13 @@ class TestSnapshotList(TestSnapshot): columns, data = self.cmd.take_action(parsed_args) self.snapshots_mock.list.assert_called_once_with( - search_opts={'all_tenants': False}) + search_opts={ + 'all_tenants': False, + 'display_name': None, + 'status': None, + 'volume_id': None + } + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -274,11 +307,88 @@ class TestSnapshotList(TestSnapshot): columns, data = self.cmd.take_action(parsed_args) self.snapshots_mock.list.assert_called_once_with( - search_opts={'all_tenants': False} + search_opts={ + 'all_tenants': False, + 'display_name': None, + 'status': None, + 'volume_id': None + } ) self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) + def test_snapshot_list_name_option(self): + arglist = [ + '--name', self.snapshots[0].display_name, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('name', self.snapshots[0].display_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + search_opts={ + 'all_tenants': False, + 'display_name': self.snapshots[0].display_name, + 'status': None, + 'volume_id': None + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_status_option(self): + arglist = [ + '--status', self.snapshots[0].status, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('status', self.snapshots[0].status), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + search_opts={ + 'all_tenants': False, + 'display_name': None, + 'status': self.snapshots[0].status, + 'volume_id': None + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_volumeid_option(self): + arglist = [ + '--volume', self.volume.id, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('volume', self.volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + search_opts={ + 'all_tenants': False, + 'display_name': None, + 'status': None, + 'volume_id': self.volume.id + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_snapshot_list_all_projects(self): arglist = [ '--all-projects', @@ -292,7 +402,13 @@ class TestSnapshotList(TestSnapshot): columns, data = self.cmd.take_action(parsed_args) self.snapshots_mock.list.assert_called_once_with( - search_opts={'all_tenants': True}) + search_opts={ + 'all_tenants': True, + 'display_name': None, + 'status': None, + 'volume_id': None + } + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -307,21 +423,23 @@ class TestSnapshotSet(TestSnapshot): self.snapshots_mock.get.return_value = self.snapshot self.snapshots_mock.set_metadata.return_value = None # Get the command object to mock - self.cmd = snapshot.SetSnapshot(self.app, None) + self.cmd = volume_snapshot.SetVolumeSnapshot(self.app, None) def test_snapshot_set_all(self): arglist = [ "--name", "new_snapshot", "--description", "new_description", - "--property", "x=y", - "--property", "foo=foo", + "--property", "foo_1=foo_1", + "--property", "foo_2=foo_2", + "--no-property", self.snapshot.id, ] - new_property = {"x": "y", "foo": "foo"} + new_property = {"foo_1": "foo_1", "foo_2": "foo_2"} verifylist = [ ("name", "new_snapshot"), ("description", "new_description"), ("property", new_property), + ("no_property", True), ("snapshot", self.snapshot.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -333,8 +451,11 @@ class TestSnapshotSet(TestSnapshot): "display_description": "new_description", } self.snapshot.update.assert_called_with(**kwargs) + self.snapshots_mock.delete_metadata.assert_called_with( + self.snapshot.id, ["foo"] + ) self.snapshots_mock.set_metadata.assert_called_with( - self.snapshot.id, new_property + self.snapshot.id, {"foo_2": "foo_2", "foo_1": "foo_1"} ) self.assertIsNone(result) @@ -404,7 +525,7 @@ class TestSnapshotShow(TestSnapshot): self.snapshots_mock.get.return_value = self.snapshot # Get the command object to test - self.cmd = snapshot.ShowSnapshot(self.app, None) + self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) def test_snapshot_show(self): arglist = [ @@ -432,7 +553,7 @@ class TestSnapshotUnset(TestSnapshot): self.snapshots_mock.get.return_value = self.snapshot self.snapshots_mock.delete_metadata.return_value = None # Get the command object to mock - self.cmd = snapshot.UnsetSnapshot(self.app, None) + self.cmd = volume_snapshot.UnsetVolumeSnapshot(self.app, None) def test_snapshot_unset(self): arglist = [ diff --git a/openstackclient/tests/unit/volume/v1/test_type.py b/openstackclient/tests/unit/volume/v1/test_type.py index 23a1186d..dcdd3d56 100644 --- a/openstackclient/tests/unit/volume/v1/test_type.py +++ b/openstackclient/tests/unit/volume/v1/test_type.py @@ -31,6 +31,10 @@ class TestType(volume_fakes.TestVolumev1): self.types_mock = self.app.client_manager.volume.volume_types self.types_mock.reset_mock() + self.encryption_types_mock = ( + self.app.client_manager.volume.volume_encryption_types) + self.encryption_types_mock.reset_mock() + class TestTypeCreate(TestType): @@ -75,6 +79,67 @@ class TestTypeCreate(TestType): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_type_create_with_encryption(self): + encryption_info = { + 'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': '128', + 'control_location': 'front-end', + } + encryption_type = volume_fakes.FakeType.create_one_encryption_type( + attrs=encryption_info + ) + self.new_volume_type = volume_fakes.FakeType.create_one_type( + attrs={'encryption': encryption_info}) + self.types_mock.create.return_value = self.new_volume_type + self.encryption_types_mock.create.return_value = encryption_type + encryption_columns = ( + 'description', + 'encryption', + 'id', + 'is_public', + 'name', + ) + encryption_data = ( + self.new_volume_type.description, + utils.format_dict(encryption_info), + self.new_volume_type.id, + True, + self.new_volume_type.name, + ) + arglist = [ + '--encryption-provider', 'LuksEncryptor', + '--encryption-cipher', 'aes-xts-plain64', + '--encryption-key-size', '128', + '--encryption-control-location', 'front-end', + self.new_volume_type.name, + ] + verifylist = [ + ('encryption_provider', 'LuksEncryptor'), + ('encryption_cipher', 'aes-xts-plain64'), + ('encryption_key_size', 128), + ('encryption_control_location', 'front-end'), + ('name', self.new_volume_type.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.types_mock.create.assert_called_with( + self.new_volume_type.name, + ) + body = { + 'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end', + } + self.encryption_types_mock.create.assert_called_with( + self.new_volume_type, + body, + ) + self.assertEqual(encryption_columns, columns) + self.assertEqual(encryption_data, data) + class TestTypeDelete(TestType): @@ -156,27 +221,31 @@ class TestTypeList(TestType): volume_types = volume_fakes.FakeType.create_types() - columns = ( + columns = [ "ID", - "Name" - ) - columns_long = ( + "Name", + "Is Public", + ] + columns_long = [ "ID", "Name", + "Is Public", "Properties" - ) + ] data = [] for t in volume_types: data.append(( t.id, t.name, + t.is_public, )) data_long = [] for t in volume_types: data_long.append(( t.id, t.name, + t.is_public, utils.format_dict(t.extra_specs), )) @@ -184,6 +253,8 @@ class TestTypeList(TestType): super(TestTypeList, self).setUp() self.types_mock.list.return_value = self.volume_types + self.encryption_types_mock.create.return_value = None + self.encryption_types_mock.update.return_value = None # get the command to test self.cmd = volume_type.ListVolumeType(self.app, None) @@ -191,6 +262,7 @@ class TestTypeList(TestType): arglist = [] verifylist = [ ("long", False), + ("encryption_type", False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -213,6 +285,47 @@ class TestTypeList(TestType): self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) + def test_type_list_with_encryption(self): + encryption_type = volume_fakes.FakeType.create_one_encryption_type( + attrs={'volume_type_id': self.volume_types[0].id}) + encryption_info = { + 'provider': 'LuksEncryptor', + 'cipher': None, + 'key_size': None, + 'control_location': 'front-end', + } + encryption_columns = self.columns + [ + "Encryption", + ] + encryption_data = [] + encryption_data.append(( + self.volume_types[0].id, + self.volume_types[0].name, + self.volume_types[0].is_public, + utils.format_dict(encryption_info), + )) + encryption_data.append(( + self.volume_types[1].id, + self.volume_types[1].name, + self.volume_types[1].is_public, + '-', + )) + + self.encryption_types_mock.list.return_value = [encryption_type] + arglist = [ + "--encryption-type", + ] + verifylist = [ + ("encryption_type", True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.encryption_types_mock.list.assert_called_once_with() + self.types_mock.list.assert_called_once_with() + self.assertEqual(encryption_columns, columns) + self.assertEqual(encryption_data, list(data)) + class TestTypeSet(TestType): @@ -256,6 +369,60 @@ class TestTypeSet(TestType): {'myprop': 'myvalue'}) self.assertIsNone(result) + def test_type_set_new_encryption(self): + arglist = [ + '--encryption-provider', 'LuksEncryptor', + '--encryption-cipher', 'aes-xts-plain64', + '--encryption-key-size', '128', + '--encryption-control-location', 'front-end', + self.volume_type.id, + ] + verifylist = [ + ('encryption_provider', 'LuksEncryptor'), + ('encryption_cipher', 'aes-xts-plain64'), + ('encryption_key_size', 128), + ('encryption_control_location', 'front-end'), + ('volume_type', self.volume_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + body = { + 'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end', + } + self.encryption_types_mock.create.assert_called_with( + self.volume_type, + body, + ) + self.assertIsNone(result) + + def test_type_set_new_encryption_without_provider(self): + arglist = [ + '--encryption-cipher', 'aes-xts-plain64', + '--encryption-key-size', '128', + '--encryption-control-location', 'front-end', + self.volume_type.id, + ] + verifylist = [ + ('encryption_cipher', 'aes-xts-plain64'), + ('encryption_key_size', 128), + ('encryption_control_location', 'front-end'), + ('volume_type', self.volume_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual("Command Failed: One or more of" + " the operations failed", + str(e)) + self.encryption_types_mock.create.assert_not_called() + self.encryption_types_mock.update.assert_not_called() + class TestTypeShow(TestType): @@ -289,7 +456,8 @@ class TestTypeShow(TestType): self.volume_type.id ] verifylist = [ - ("volume_type", self.volume_type.id) + ("volume_type", self.volume_type.id), + ("encryption_type", False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -299,6 +467,50 @@ class TestTypeShow(TestType): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_type_show_with_encryption(self): + encryption_type = volume_fakes.FakeType.create_one_encryption_type() + encryption_info = { + 'provider': 'LuksEncryptor', + 'cipher': None, + 'key_size': None, + 'control_location': 'front-end', + } + self.volume_type = volume_fakes.FakeType.create_one_type( + attrs={'encryption': encryption_info}) + self.types_mock.get.return_value = self.volume_type + self.encryption_types_mock.get.return_value = encryption_type + encryption_columns = ( + 'description', + 'encryption', + 'id', + 'is_public', + 'name', + 'properties', + ) + encryption_data = ( + self.volume_type.description, + utils.format_dict(encryption_info), + self.volume_type.id, + True, + self.volume_type.name, + utils.format_dict(self.volume_type.extra_specs) + ) + arglist = [ + '--encryption-type', + self.volume_type.id + ] + verifylist = [ + ('encryption_type', True), + ("volume_type", self.volume_type.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.types_mock.get.assert_called_with(self.volume_type.id) + self.encryption_types_mock.get.assert_called_with(self.volume_type.id) + self.assertEqual(encryption_columns, columns) + self.assertEqual(encryption_data, data) + class TestTypeUnset(TestType): @@ -313,13 +525,14 @@ class TestTypeUnset(TestType): # Get the command object to test self.cmd = volume_type.UnsetVolumeType(self.app, None) - def test_type_unset(self): + def test_type_unset_property(self): arglist = [ '--property', 'property', '--property', 'multi_property', self.volume_type.id, ] verifylist = [ + ('encryption_type', False), ('property', ['property', 'multi_property']), ('volume_type', self.volume_type.id), ] @@ -329,6 +542,7 @@ class TestTypeUnset(TestType): result = self.cmd.take_action(parsed_args) self.volume_type.unset_keys.assert_called_once_with( ['property', 'multi_property']) + self.encryption_types_mock.delete.assert_not_called() self.assertIsNone(result) def test_type_unset_failed_with_missing_volume_type_argument(self): @@ -358,3 +572,18 @@ class TestTypeUnset(TestType): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) + + def test_type_unset_encryption_type(self): + arglist = [ + '--encryption-type', + self.volume_type.id, + ] + verifylist = [ + ('encryption_type', True), + ('volume_type', self.volume_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.encryption_types_mock.delete.assert_called_with(self.volume_type) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v1/test_volume.py b/openstackclient/tests/unit/volume/v1/test_volume.py index 73c00844..d46a7ba9 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume.py +++ b/openstackclient/tests/unit/volume/v1/test_volume.py @@ -14,15 +14,14 @@ # import argparse -import copy import mock from mock import call from osc_lib import exceptions from osc_lib import utils -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.unit.image.v1 import fakes as image_fakes from openstackclient.tests.unit import utils as tests_utils from openstackclient.tests.unit.volume.v1 import fakes as volume_fakes from openstackclient.volume.v1 import volume @@ -58,10 +57,6 @@ class TestVolume(volume_fakes.TestVolumev1): return volumes -# TODO(dtroyer): The volume create tests are incomplete, only the minimal -# options and the options that require additional processing -# are implemented at this time. - class TestVolumeCreate(TestVolume): project = identity_fakes.FakeProject.create_one_project() @@ -321,19 +316,16 @@ class TestVolumeCreate(TestVolume): self.assertEqual(self.datalist, data) def test_volume_create_image_id(self): - self.images_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.IMAGE), - loaded=True, - ) + image = image_fakes.FakeImage.create_one_image() + self.images_mock.get.return_value = image arglist = [ - '--image', volume_fakes.image_id, + '--image', image.id, '--size', str(self.new_volume.size), self.new_volume.display_name, ] verifylist = [ - ('image', volume_fakes.image_id), + ('image', image.id), ('size', self.new_volume.size), ('name', self.new_volume.display_name), ] @@ -360,26 +352,23 @@ class TestVolumeCreate(TestVolume): None, None, None, - volume_fakes.image_id, + image.id, ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) def test_volume_create_image_name(self): - self.images_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.IMAGE), - loaded=True, - ) + image = image_fakes.FakeImage.create_one_image() + self.images_mock.get.return_value = image arglist = [ - '--image', volume_fakes.image_name, + '--image', image.name, '--size', str(self.new_volume.size), self.new_volume.display_name, ] verifylist = [ - ('image', volume_fakes.image_name), + ('image', image.name), ('size', self.new_volume.size), ('name', self.new_volume.display_name), ] @@ -406,7 +395,7 @@ class TestVolumeCreate(TestVolume): None, None, None, - volume_fakes.image_id, + image.id, ) self.assertEqual(self.columns, columns) @@ -442,6 +431,142 @@ class TestVolumeCreate(TestVolume): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_volume_create_with_bootable_and_readonly(self): + arglist = [ + '--bootable', + '--read-only', + '--size', str(self.new_volume.size), + self.new_volume.display_name, + ] + verifylist = [ + ('bootable', True), + ('non_bootable', False), + ('read_only', True), + ('read_write', False), + ('size', self.new_volume.size), + ('name', self.new_volume.display_name), + ] + + parsed_args = self.check_parser( + self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + self.new_volume.size, + None, + None, + self.new_volume.display_name, + None, + None, + None, + None, + None, + None, + None, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + self.volumes_mock.set_bootable.assert_called_with( + self.new_volume.id, True) + self.volumes_mock.update_readonly_flag.assert_called_with( + self.new_volume.id, True) + + def test_volume_create_with_nonbootable_and_readwrite(self): + arglist = [ + '--non-bootable', + '--read-write', + '--size', str(self.new_volume.size), + self.new_volume.display_name, + ] + verifylist = [ + ('bootable', False), + ('non_bootable', True), + ('read_only', False), + ('read_write', True), + ('size', self.new_volume.size), + ('name', self.new_volume.display_name), + ] + + parsed_args = self.check_parser( + self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + self.new_volume.size, + None, + None, + self.new_volume.display_name, + None, + None, + None, + None, + None, + None, + None, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + self.volumes_mock.set_bootable.assert_called_with( + self.new_volume.id, False) + self.volumes_mock.update_readonly_flag.assert_called_with( + self.new_volume.id, False) + + @mock.patch.object(volume.LOG, 'error') + def test_volume_create_with_bootable_and_readonly_fail( + self, mock_error): + + self.volumes_mock.set_bootable.side_effect = ( + exceptions.CommandError()) + + self.volumes_mock.update_readonly_flag.side_effect = ( + exceptions.CommandError()) + + arglist = [ + '--bootable', + '--read-only', + '--size', str(self.new_volume.size), + self.new_volume.display_name, + ] + verifylist = [ + ('bootable', True), + ('non_bootable', False), + ('read_only', True), + ('read_write', False), + ('size', self.new_volume.size), + ('name', self.new_volume.display_name), + ] + + parsed_args = self.check_parser( + self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + self.new_volume.size, + None, + None, + self.new_volume.display_name, + None, + None, + None, + None, + None, + None, + None, + ) + + self.assertEqual(2, mock_error.call_count) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + self.volumes_mock.set_bootable.assert_called_with( + self.new_volume.id, True) + self.volumes_mock.update_readonly_flag.assert_called_with( + self.new_volume.id, True) + def test_volume_create_without_size(self): arglist = [ self.new_volume.display_name, @@ -739,6 +864,68 @@ class TestVolumeList(TestVolume): self.cmd, arglist, verifylist) +class TestVolumeMigrate(TestVolume): + + _volume = volume_fakes.FakeVolume.create_one_volume() + + def setUp(self): + super(TestVolumeMigrate, self).setUp() + + self.volumes_mock.get.return_value = self._volume + self.volumes_mock.migrate_volume.return_value = None + # Get the command object to test + self.cmd = volume.MigrateVolume(self.app, None) + + def test_volume_migrate(self): + arglist = [ + "--host", "host@backend-name#pool", + self._volume.id, + ] + verifylist = [ + ("force_host_copy", False), + ("host", "host@backend-name#pool"), + ("volume", self._volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.get.assert_called_once_with(self._volume.id) + self.volumes_mock.migrate_volume.assert_called_once_with( + self._volume.id, "host@backend-name#pool", False) + self.assertIsNone(result) + + def test_volume_migrate_with_option(self): + arglist = [ + "--force-host-copy", + "--host", "host@backend-name#pool", + self._volume.id, + ] + verifylist = [ + ("force_host_copy", True), + ("host", "host@backend-name#pool"), + ("volume", self._volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.get.assert_called_once_with(self._volume.id) + self.volumes_mock.migrate_volume.assert_called_once_with( + self._volume.id, "host@backend-name#pool", True) + self.assertIsNone(result) + + def test_volume_migrate_without_host(self): + arglist = [ + self._volume.id, + ] + verifylist = [ + ("force_host_copy", False), + ("volume", self._volume.id), + ] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + class TestVolumeSet(TestVolume): _volume = volume_fakes.FakeVolume.create_one_volume() @@ -844,8 +1031,7 @@ class TestVolumeSet(TestVolume): ) self.assertIsNone(result) - @mock.patch.object(volume.LOG, 'error') - def test_volume_set_size_smaller(self, mock_log_error): + def test_volume_set_size_smaller(self): self._volume.status = 'available' arglist = [ '--size', '1', @@ -860,15 +1046,11 @@ class TestVolumeSet(TestVolume): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - mock_log_error.assert_called_with("New size must be greater " - "than %s GB", - self._volume.size) - self.assertIsNone(result) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) - @mock.patch.object(volume.LOG, 'error') - def test_volume_set_size_not_available(self, mock_log_error): + def test_volume_set_size_not_available(self): self._volume.status = 'error' arglist = [ '--size', '130', @@ -883,22 +1065,23 @@ class TestVolumeSet(TestVolume): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - mock_log_error.assert_called_with("Volume is in %s state, it must be " - "available before size can be " - "extended", 'error') - self.assertIsNone(result) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) def test_volume_set_property(self): arglist = [ + '--no-property', '--property', 'myprop=myvalue', self._volume.display_name, ] verifylist = [ + ('read_only', False), + ('read_write', False), ('name', None), ('description', None), ('size', None), + ('no_property', True), ('property', {'myprop': 'myvalue'}), ('volume', self._volume.display_name), ('bootable', False), @@ -916,6 +1099,11 @@ class TestVolumeSet(TestVolume): self._volume.id, metadata ) + self.volumes_mock.delete_metadata.assert_called_with( + self._volume.id, + self._volume.metadata.keys() + ) + self.volumes_mock.update_readonly_flag.assert_not_called() self.assertIsNone(result) def test_volume_set_bootable(self): @@ -943,6 +1131,44 @@ class TestVolumeSet(TestVolume): self.volumes_mock.set_bootable.assert_called_with( self._volume.id, verifylist[index][0][1]) + def test_volume_set_readonly(self): + arglist = [ + '--read-only', + self._volume.id + ] + verifylist = [ + ('read_only', True), + ('read_write', False), + ('volume', self._volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.update_readonly_flag.assert_called_once_with( + self._volume.id, + True) + self.assertIsNone(result) + + def test_volume_set_read_write(self): + arglist = [ + '--read-write', + self._volume.id + ] + verifylist = [ + ('read_only', False), + ('read_write', True), + ('volume', self._volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.update_readonly_flag.assert_called_once_with( + self._volume.id, + False) + self.assertIsNone(result) + class TestVolumeShow(TestVolume): diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index 5e1d16e1..d321c71a 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -208,6 +208,9 @@ class FakeVolumeClient(object): self.volume_types.resource_class = fakes.FakeResource(None, {}) self.volume_type_access = mock.Mock() self.volume_type_access.resource_class = fakes.FakeResource(None, {}) + self.volume_encryption_types = mock.Mock() + self.volume_encryption_types.resource_class = ( + fakes.FakeResource(None, {})) self.restores = mock.Mock() self.restores.resource_class = fakes.FakeResource(None, {}) self.qos_specs = mock.Mock() @@ -224,6 +227,8 @@ class FakeVolumeClient(object): self.quota_classes.resource_class = fakes.FakeResource(None, {}) self.consistencygroups = mock.Mock() self.consistencygroups.resource_class = fakes.FakeResource(None, {}) + self.cgsnapshots = mock.Mock() + self.cgsnapshots.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -248,10 +253,7 @@ class TestVolume(utils.TestCommand): class FakeVolume(object): - """Fake one or more volumes. - - TODO(xiexs): Currently, only volume API v2 is supported by this class. - """ + """Fake one or more volumes.""" @staticmethod def create_one_volume(attrs=None): @@ -481,7 +483,7 @@ class FakeBackup(object): If backups list is provided, then initialize the Mock object with the list. Otherwise create one. - :param List volumes: + :param List backups: A list of FakeResource objects faking backups :param Integer count: The number of backups to be faked @@ -547,6 +549,106 @@ class FakeConsistencyGroup(object): return consistency_groups + @staticmethod + def get_consistency_groups(consistency_groups=None, count=2): + """Note: + + Get an iterable MagicMock object with a list of faked + consistency_groups. + + If consistency_groups list is provided, then initialize + the Mock object with the list. Otherwise create one. + + :param List consistency_groups: + A list of FakeResource objects faking consistency_groups + :param Integer count: + The number of consistency_groups to be faked + :return + An iterable Mock object with side_effect set to a list of faked + consistency_groups + """ + if consistency_groups is None: + consistency_groups = (FakeConsistencyGroup. + create_consistency_groups(count)) + + return mock.Mock(side_effect=consistency_groups) + + +class FakeConsistencyGroupSnapshot(object): + """Fake one or more consistency group snapshot.""" + + @staticmethod + def create_one_consistency_group_snapshot(attrs=None): + """Create a fake consistency group snapshot. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + + # Set default attributes. + consistency_group_snapshot_info = { + "id": 'id-' + uuid.uuid4().hex, + "name": 'backup-name-' + uuid.uuid4().hex, + "description": 'description-' + uuid.uuid4().hex, + "status": "error", + "consistencygroup_id": 'consistency-group-id' + uuid.uuid4().hex, + "created_at": 'time-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + consistency_group_snapshot_info.update(attrs) + + consistency_group_snapshot = fakes.FakeResource( + info=copy.deepcopy(consistency_group_snapshot_info), + loaded=True) + return consistency_group_snapshot + + @staticmethod + def create_consistency_group_snapshots(attrs=None, count=2): + """Create multiple fake consistency group snapshots. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of consistency group snapshots to fake + :return: + A list of FakeResource objects faking the + consistency group snapshots + """ + consistency_group_snapshots = [] + for i in range(0, count): + consistency_group_snapshot = ( + FakeConsistencyGroupSnapshot. + create_one_consistency_group_snapshot(attrs) + ) + consistency_group_snapshots.append(consistency_group_snapshot) + + return consistency_group_snapshots + + @staticmethod + def get_consistency_group_snapshots(snapshots=None, count=2): + """Get an iterable MagicMock object with a list of faked cgsnapshots. + + If consistenct group snapshots list is provided, then initialize + the Mock object with the list. Otherwise create one. + + :param List snapshots: + A list of FakeResource objects faking consistency group snapshots + :param Integer count: + The number of consistency group snapshots to be faked + :return + An iterable Mock object with side_effect set to a list of faked + consistency groups + """ + if snapshots is None: + snapshots = (FakeConsistencyGroupSnapshot. + create_consistency_group_snapshots(count)) + + return mock.Mock(side_effect=snapshots) + class FakeExtension(object): """Fake one or more extension.""" @@ -665,7 +767,7 @@ class FakeQos(object): If qoses list is provided, then initialize the Mock object with the list. Otherwise create one. - :param List volumes: + :param List qoses: A list of FakeResource objects faking qoses :param Integer count: The number of qoses to be faked @@ -738,7 +840,7 @@ class FakeSnapshot(object): If snapshots list is provided, then initialize the Mock object with the list. Otherwise create one. - :param List volumes: + :param List snapshots: A list of FakeResource objects faking snapshots :param Integer count: The number of snapshots to be faked @@ -804,3 +906,101 @@ class FakeType(object): volume_types.append(volume_type) return volume_types + + @staticmethod + def get_types(types=None, count=2): + """Get an iterable MagicMock object with a list of faked types. + + If types list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List types: + A list of FakeResource objects faking types + :param Integer count: + The number of types to be faked + :return + An iterable Mock object with side_effect set to a list of faked + types + """ + if types is None: + types = FakeType.create_types(count) + + return mock.Mock(side_effect=types) + + @staticmethod + def create_one_encryption_type(attrs=None): + """Create a fake encryption type. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with volume_type_id etc. + """ + attrs = attrs or {} + + # Set default attributes. + encryption_info = { + "volume_type_id": 'type-id-' + uuid.uuid4().hex, + 'provider': 'LuksEncryptor', + 'cipher': None, + 'key_size': None, + 'control_location': 'front-end', + } + + # Overwrite default attributes. + encryption_info.update(attrs) + + encryption_type = fakes.FakeResource( + info=copy.deepcopy(encryption_info), + loaded=True) + return encryption_type + + +class FakeQuota(object): + """Fake quota""" + + @staticmethod + def create_one_vol_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'backups': 100, + 'backup_gigabytes': 100, + 'gigabytes': 10, + 'per_volume_gigabytes': 10, + 'snapshots': 0, + 'volumes': 10} + + quota_attrs.update(attrs) + + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + quota.project_id = quota_attrs['id'] + + return quota + + @staticmethod + def create_one_default_vol_quota(attrs=None): + """Create one quota""" + attrs = attrs or {} + + quota_attrs = { + 'id': 'project-id-' + uuid.uuid4().hex, + 'backups': 100, + 'backup_gigabytes': 100, + 'gigabytes': 100, + 'per_volume_gigabytes': 100, + 'snapshots': 100, + 'volumes': 100} + + quota_attrs.update(attrs) + + quota = fakes.FakeResource( + info=copy.deepcopy(quota_attrs), + loaded=True) + quota.project_id = quota_attrs['id'] + + return quota diff --git a/openstackclient/tests/unit/volume/v2/test_backup.py b/openstackclient/tests/unit/volume/v2/test_backup.py index 306c9eb3..a8e81c7e 100644 --- a/openstackclient/tests/unit/volume/v2/test_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_backup.py @@ -280,26 +280,78 @@ class TestBackupList(TestBackup): self.volumes_mock.list.return_value = [self.volume] self.backups_mock.list.return_value = self.backups + self.volumes_mock.get.return_value = self.volume + self.backups_mock.get.return_value = self.backups[0] # Get the command to test self.cmd = backup.ListVolumeBackup(self.app, None) def test_backup_list_without_options(self): arglist = [] - verifylist = [("long", False)] + verifylist = [ + ("long", False), + ("name", None), + ("status", None), + ("volume", None), + ("marker", None), + ("limit", None), + ('all_projects', False), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) + search_opts = { + "name": None, + "status": None, + "volume_id": None, + 'all_tenants': False, + } + self.volumes_mock.get.assert_not_called() + self.backups_mock.get.assert_not_called() + self.backups_mock.list.assert_called_with( + search_opts=search_opts, + marker=None, + limit=None, + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) def test_backup_list_with_options(self): - arglist = ["--long"] - verifylist = [("long", True)] + arglist = [ + "--long", + "--name", self.backups[0].name, + "--status", "error", + "--volume", self.volume.id, + "--marker", self.backups[0].id, + "--all-projects", + "--limit", "3", + ] + verifylist = [ + ("long", True), + ("name", self.backups[0].name), + ("status", "error"), + ("volume", self.volume.id), + ("marker", self.backups[0].id), + ('all_projects', True), + ("limit", 3), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) + search_opts = { + "name": self.backups[0].name, + "status": "error", + "volume_id": self.volume.id, + 'all_tenants': True, + } + self.volumes_mock.get.assert_called_once_with(self.volume.id) + self.backups_mock.get.assert_called_once_with(self.backups[0].id) + self.backups_mock.list.assert_called_with( + search_opts=search_opts, + marker=self.backups[0].id, + limit=3, + ) self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) @@ -366,6 +418,30 @@ class TestBackupSet(TestBackup): self.backup.id, **{'name': 'new_name'}) self.assertIsNone(result) + def test_backup_set_description(self): + arglist = [ + '--description', 'new_description', + self.backup.id, + ] + verifylist = [ + ('name', None), + ('description', 'new_description'), + ('backup', self.backup.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': 'new_description' + } + self.backups_mock.update.assert_called_once_with( + self.backup.id, + **kwargs + ) + self.assertIsNone(result) + def test_backup_set_state(self): arglist = [ '--state', 'error', diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index 00e1b60e..6eeeae39 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -12,6 +12,10 @@ # under the License. # +import mock +from mock import call + +from osc_lib import exceptions from osc_lib import utils from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes @@ -28,6 +32,379 @@ class TestConsistencyGroup(volume_fakes.TestVolume): self.app.client_manager.volume.consistencygroups) self.consistencygroups_mock.reset_mock() + self.cgsnapshots_mock = ( + self.app.client_manager.volume.cgsnapshots) + self.cgsnapshots_mock.reset_mock() + + self.volumes_mock = ( + self.app.client_manager.volume.volumes) + self.volumes_mock.reset_mock() + + self.types_mock = self.app.client_manager.volume.volume_types + self.types_mock.reset_mock() + + +class TestConsistencyGroupAddVolume(TestConsistencyGroup): + + _consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + + def setUp(self): + super(TestConsistencyGroupAddVolume, self).setUp() + + self.consistencygroups_mock.get.return_value = ( + self._consistency_group) + # Get the command object to test + self.cmd = \ + consistency_group.AddVolumeToConsistencyGroup(self.app, None) + + def test_add_one_volume_to_consistency_group(self): + volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.get.return_value = volume + arglist = [ + self._consistency_group.id, + volume.id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'add_volumes': volume.id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_add_multiple_volumes_to_consistency_group(self): + volumes = volume_fakes.FakeVolume.create_volumes(count=2) + self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes(volumes) + arglist = [ + self._consistency_group.id, + volumes[0].id, + volumes[1].id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volumes[0].id, volumes[1].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'add_volumes': volumes[0].id + ',' + volumes[1].id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + @mock.patch.object(consistency_group.LOG, 'error') + def test_add_multiple_volumes_to_consistency_group_with_exception( + self, mock_error): + volume = volume_fakes.FakeVolume.create_one_volume() + arglist = [ + self._consistency_group.id, + volume.id, + 'unexist_volume', + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id, 'unexist_volume']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [volume, + exceptions.CommandError, + self._consistency_group] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + result = self.cmd.take_action(parsed_args) + mock_error.assert_called_with("1 of 2 volumes failed to add.") + self.assertIsNone(result) + find_mock.assert_any_call(self.consistencygroups_mock, + self._consistency_group.id) + find_mock.assert_any_call(self.volumes_mock, + volume.id) + find_mock.assert_any_call(self.volumes_mock, + 'unexist_volume') + self.assertEqual(3, find_mock.call_count) + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, add_volumes=volume.id + ) + + +class TestConsistencyGroupCreate(TestConsistencyGroup): + + volume_type = volume_fakes.FakeType.create_one_type() + new_consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + consistency_group_snapshot = ( + volume_fakes. + FakeConsistencyGroupSnapshot. + create_one_consistency_group_snapshot() + ) + + columns = ( + 'availability_zone', + 'created_at', + 'description', + 'id', + 'name', + 'status', + 'volume_types', + ) + data = ( + new_consistency_group.availability_zone, + new_consistency_group.created_at, + new_consistency_group.description, + new_consistency_group.id, + new_consistency_group.name, + new_consistency_group.status, + new_consistency_group.volume_types, + ) + + def setUp(self): + super(TestConsistencyGroupCreate, self).setUp() + self.consistencygroups_mock.create.return_value = ( + self.new_consistency_group) + self.consistencygroups_mock.create_from_src.return_value = ( + self.new_consistency_group) + self.consistencygroups_mock.get.return_value = ( + self.new_consistency_group) + self.types_mock.get.return_value = self.volume_type + self.cgsnapshots_mock.get.return_value = ( + self.consistency_group_snapshot) + + # Get the command object to test + self.cmd = consistency_group.CreateConsistencyGroup(self.app, None) + + def test_consistency_group_create(self): + arglist = [ + '--volume-type', self.volume_type.id, + '--description', self.new_consistency_group.description, + '--availability-zone', + self.new_consistency_group.availability_zone, + self.new_consistency_group.name, + ] + verifylist = [ + ('volume_type', self.volume_type.id), + ('description', self.new_consistency_group.description), + ('availability_zone', + self.new_consistency_group.availability_zone), + ('name', self.new_consistency_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.types_mock.get.assert_called_once_with( + self.volume_type.id) + self.consistencygroups_mock.get.assert_not_called() + self.consistencygroups_mock.create.assert_called_once_with( + self.volume_type.id, + name=self.new_consistency_group.name, + description=self.new_consistency_group.description, + availability_zone=self.new_consistency_group.availability_zone, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_consistency_group_create_without_name(self): + arglist = [ + '--volume-type', self.volume_type.id, + '--description', self.new_consistency_group.description, + '--availability-zone', + self.new_consistency_group.availability_zone, + ] + verifylist = [ + ('volume_type', self.volume_type.id), + ('description', self.new_consistency_group.description), + ('availability_zone', + self.new_consistency_group.availability_zone), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.types_mock.get.assert_called_once_with( + self.volume_type.id) + self.consistencygroups_mock.get.assert_not_called() + self.consistencygroups_mock.create.assert_called_once_with( + self.volume_type.id, + name=None, + description=self.new_consistency_group.description, + availability_zone=self.new_consistency_group.availability_zone, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_consistency_group_create_from_source(self): + arglist = [ + '--consistency-group-source', self.new_consistency_group.id, + '--description', self.new_consistency_group.description, + self.new_consistency_group.name, + ] + verifylist = [ + ('consistency_group_source', self.new_consistency_group.id), + ('description', self.new_consistency_group.description), + ('name', self.new_consistency_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.types_mock.get.assert_not_called() + self.consistencygroups_mock.get.assert_called_once_with( + self.new_consistency_group.id) + self.consistencygroups_mock.create_from_src.assert_called_with( + None, + self.new_consistency_group.id, + name=self.new_consistency_group.name, + description=self.new_consistency_group.description, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_consistency_group_create_from_snapshot(self): + arglist = [ + '--consistency-group-snapshot', self.consistency_group_snapshot.id, + '--description', self.new_consistency_group.description, + self.new_consistency_group.name, + ] + verifylist = [ + ('consistency_group_snapshot', self.consistency_group_snapshot.id), + ('description', self.new_consistency_group.description), + ('name', self.new_consistency_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.types_mock.get.assert_not_called() + self.cgsnapshots_mock.get.assert_called_once_with( + self.consistency_group_snapshot.id) + self.consistencygroups_mock.create_from_src.assert_called_with( + self.consistency_group_snapshot.id, + None, + name=self.new_consistency_group.name, + description=self.new_consistency_group.description, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestConsistencyGroupDelete(TestConsistencyGroup): + + consistency_groups =\ + volume_fakes.FakeConsistencyGroup.create_consistency_groups(count=2) + + def setUp(self): + super(TestConsistencyGroupDelete, self).setUp() + + self.consistencygroups_mock.get = volume_fakes.FakeConsistencyGroup.\ + get_consistency_groups(self.consistency_groups) + self.consistencygroups_mock.delete.return_value = None + + # Get the command object to mock + self.cmd = consistency_group.DeleteConsistencyGroup(self.app, None) + + def test_consistency_group_delete(self): + arglist = [ + self.consistency_groups[0].id + ] + verifylist = [ + ("consistency_groups", [self.consistency_groups[0].id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.consistencygroups_mock.delete.assert_called_with( + self.consistency_groups[0].id, False) + self.assertIsNone(result) + + def test_consistency_group_delete_with_force(self): + arglist = [ + '--force', + self.consistency_groups[0].id, + ] + verifylist = [ + ('force', True), + ("consistency_groups", [self.consistency_groups[0].id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.consistencygroups_mock.delete.assert_called_with( + self.consistency_groups[0].id, True) + self.assertIsNone(result) + + def test_delete_multiple_consistency_groups(self): + arglist = [] + for b in self.consistency_groups: + arglist.append(b.id) + verifylist = [ + ('consistency_groups', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for b in self.consistency_groups: + calls.append(call(b.id, False)) + self.consistencygroups_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_consistency_groups_with_exception(self): + arglist = [ + self.consistency_groups[0].id, + 'unexist_consistency_group', + ] + verifylist = [ + ('consistency_groups', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self.consistency_groups[0], + exceptions.CommandError] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 consistency groups failed to delete.', + str(e)) + + find_mock.assert_any_call(self.consistencygroups_mock, + self.consistency_groups[0].id) + find_mock.assert_any_call(self.consistencygroups_mock, + 'unexist_consistency_group') + + self.assertEqual(2, find_mock.call_count) + self.consistencygroups_mock.delete.assert_called_once_with( + self.consistency_groups[0].id, False + ) + class TestConsistencyGroupList(TestConsistencyGroup): @@ -120,3 +497,211 @@ class TestConsistencyGroupList(TestConsistencyGroup): detailed=True, search_opts={'all_tenants': False}) self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) + + +class TestConsistencyGroupRemoveVolume(TestConsistencyGroup): + + _consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + + def setUp(self): + super(TestConsistencyGroupRemoveVolume, self).setUp() + + self.consistencygroups_mock.get.return_value = ( + self._consistency_group) + # Get the command object to test + self.cmd = \ + consistency_group.RemoveVolumeFromConsistencyGroup(self.app, None) + + def test_remove_one_volume_from_consistency_group(self): + volume = volume_fakes.FakeVolume.create_one_volume() + self.volumes_mock.get.return_value = volume + arglist = [ + self._consistency_group.id, + volume.id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'remove_volumes': volume.id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_remove_multi_volumes_from_consistency_group(self): + volumes = volume_fakes.FakeVolume.create_volumes(count=2) + self.volumes_mock.get = volume_fakes.FakeVolume.get_volumes(volumes) + arglist = [ + self._consistency_group.id, + volumes[0].id, + volumes[1].id, + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volumes[0].id, volumes[1].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'remove_volumes': volumes[0].id + ',' + volumes[1].id, + } + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + @mock.patch.object(consistency_group.LOG, 'error') + def test_remove_multiple_volumes_from_consistency_group_with_exception( + self, mock_error): + volume = volume_fakes.FakeVolume.create_one_volume() + arglist = [ + self._consistency_group.id, + volume.id, + 'unexist_volume', + ] + verifylist = [ + ('consistency_group', self._consistency_group.id), + ('volumes', [volume.id, 'unexist_volume']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [volume, + exceptions.CommandError, + self._consistency_group] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + result = self.cmd.take_action(parsed_args) + mock_error.assert_called_with("1 of 2 volumes failed to remove.") + self.assertIsNone(result) + find_mock.assert_any_call(self.consistencygroups_mock, + self._consistency_group.id) + find_mock.assert_any_call(self.volumes_mock, + volume.id) + find_mock.assert_any_call(self.volumes_mock, + 'unexist_volume') + self.assertEqual(3, find_mock.call_count) + self.consistencygroups_mock.update.assert_called_once_with( + self._consistency_group.id, remove_volumes=volume.id + ) + + +class TestConsistencyGroupSet(TestConsistencyGroup): + + consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + + def setUp(self): + super(TestConsistencyGroupSet, self).setUp() + + self.consistencygroups_mock.get.return_value = ( + self.consistency_group) + # Get the command object to test + self.cmd = consistency_group.SetConsistencyGroup(self.app, None) + + def test_consistency_group_set_name(self): + new_name = 'new_name' + arglist = [ + '--name', new_name, + self.consistency_group.id, + ] + verifylist = [ + ('name', new_name), + ('description', None), + ('consistency_group', self.consistency_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': new_name, + } + self.consistencygroups_mock.update.assert_called_once_with( + self.consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_consistency_group_set_description(self): + new_description = 'new_description' + arglist = [ + '--description', new_description, + self.consistency_group.id, + ] + verifylist = [ + ('name', None), + ('description', new_description), + ('consistency_group', self.consistency_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': new_description, + } + self.consistencygroups_mock.update.assert_called_once_with( + self.consistency_group.id, + **kwargs + ) + self.assertIsNone(result) + + +class TestConsistencyGroupShow(TestConsistencyGroup): + columns = ( + 'availability_zone', + 'created_at', + 'description', + 'id', + 'name', + 'status', + 'volume_types', + ) + + def setUp(self): + super(TestConsistencyGroupShow, self).setUp() + + self.consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + self.data = ( + self.consistency_group.availability_zone, + self.consistency_group.created_at, + self.consistency_group.description, + self.consistency_group.id, + self.consistency_group.name, + self.consistency_group.status, + self.consistency_group.volume_types, + ) + self.consistencygroups_mock.get.return_value = self.consistency_group + self.cmd = consistency_group.ShowConsistencyGroup(self.app, None) + + def test_consistency_group_show(self): + arglist = [ + self.consistency_group.id + ] + verifylist = [ + ("consistency_group", self.consistency_group.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.consistencygroups_mock.get.assert_called_once_with( + self.consistency_group.id) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py b/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py new file mode 100644 index 00000000..3bfe93df --- /dev/null +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py @@ -0,0 +1,351 @@ +# +# 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 mock import call + +from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes +from openstackclient.volume.v2 import consistency_group_snapshot + + +class TestConsistencyGroupSnapshot(volume_fakes.TestVolume): + + def setUp(self): + super(TestConsistencyGroupSnapshot, self).setUp() + + # Get a shortcut to the TransferManager Mock + self.cgsnapshots_mock = ( + self.app.client_manager.volume.cgsnapshots) + self.cgsnapshots_mock.reset_mock() + self.consistencygroups_mock = ( + self.app.client_manager.volume.consistencygroups) + self.consistencygroups_mock.reset_mock() + + +class TestConsistencyGroupSnapshotCreate(TestConsistencyGroupSnapshot): + + _consistency_group_snapshot = ( + volume_fakes. + FakeConsistencyGroupSnapshot. + create_one_consistency_group_snapshot() + ) + consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group()) + + columns = ( + 'consistencygroup_id', + 'created_at', + 'description', + 'id', + 'name', + 'status', + ) + data = ( + _consistency_group_snapshot.consistencygroup_id, + _consistency_group_snapshot.created_at, + _consistency_group_snapshot.description, + _consistency_group_snapshot.id, + _consistency_group_snapshot.name, + _consistency_group_snapshot.status, + ) + + def setUp(self): + super(TestConsistencyGroupSnapshotCreate, self).setUp() + self.cgsnapshots_mock.create.return_value = ( + self._consistency_group_snapshot) + self.consistencygroups_mock.get.return_value = ( + self.consistency_group) + + # Get the command object to test + self.cmd = (consistency_group_snapshot. + CreateConsistencyGroupSnapshot(self.app, None)) + + def test_consistency_group_snapshot_create(self): + arglist = [ + '--consistency-group', self.consistency_group.id, + '--description', self._consistency_group_snapshot.description, + self._consistency_group_snapshot.name, + ] + verifylist = [ + ('consistency_group', self.consistency_group.id), + ('description', self._consistency_group_snapshot.description), + ('snapshot_name', self._consistency_group_snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.consistencygroups_mock.get.assert_called_once_with( + self.consistency_group.id) + self.cgsnapshots_mock.create.assert_called_once_with( + self.consistency_group.id, + name=self._consistency_group_snapshot.name, + description=self._consistency_group_snapshot.description, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_consistency_group_snapshot_create_no_consistency_group(self): + arglist = [ + '--description', self._consistency_group_snapshot.description, + self._consistency_group_snapshot.name, + ] + verifylist = [ + ('description', self._consistency_group_snapshot.description), + ('snapshot_name', self._consistency_group_snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.consistencygroups_mock.get.assert_called_once_with( + self._consistency_group_snapshot.name) + self.cgsnapshots_mock.create.assert_called_once_with( + self.consistency_group.id, + name=self._consistency_group_snapshot.name, + description=self._consistency_group_snapshot.description, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestConsistencyGroupSnapshotDelete(TestConsistencyGroupSnapshot): + + consistency_group_snapshots = ( + volume_fakes.FakeConsistencyGroupSnapshot. + create_consistency_group_snapshots(count=2) + ) + + def setUp(self): + super(TestConsistencyGroupSnapshotDelete, self).setUp() + + self.cgsnapshots_mock.get = ( + volume_fakes.FakeConsistencyGroupSnapshot. + get_consistency_group_snapshots(self.consistency_group_snapshots) + ) + self.cgsnapshots_mock.delete.return_value = None + + # Get the command object to mock + self.cmd = (consistency_group_snapshot. + DeleteConsistencyGroupSnapshot(self.app, None)) + + def test_consistency_group_snapshot_delete(self): + arglist = [ + self.consistency_group_snapshots[0].id + ] + verifylist = [ + ("consistency_group_snapshot", + [self.consistency_group_snapshots[0].id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.cgsnapshots_mock.delete.assert_called_once_with( + self.consistency_group_snapshots[0].id) + self.assertIsNone(result) + + def test_multiple_consistency_group_snapshots_delete(self): + arglist = [] + for c in self.consistency_group_snapshots: + arglist.append(c.id) + verifylist = [ + ('consistency_group_snapshot', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for c in self.consistency_group_snapshots: + calls.append(call(c.id)) + self.cgsnapshots_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + +class TestConsistencyGroupSnapshotList(TestConsistencyGroupSnapshot): + + consistency_group_snapshots = ( + volume_fakes.FakeConsistencyGroupSnapshot. + create_consistency_group_snapshots(count=2) + ) + consistency_group = ( + volume_fakes.FakeConsistencyGroup.create_one_consistency_group() + ) + + columns = [ + 'ID', + 'Status', + 'Name', + ] + columns_long = [ + 'ID', + 'Status', + 'ConsistencyGroup ID', + 'Name', + 'Description', + 'Created At', + ] + data = [] + for c in consistency_group_snapshots: + data.append(( + c.id, + c.status, + c.name, + )) + data_long = [] + for c in consistency_group_snapshots: + data_long.append(( + c.id, + c.status, + c.consistencygroup_id, + c.name, + c.description, + c.created_at, + )) + + def setUp(self): + super(TestConsistencyGroupSnapshotList, self).setUp() + + self.cgsnapshots_mock.list.return_value = ( + self.consistency_group_snapshots) + self.consistencygroups_mock.get.return_value = self.consistency_group + # Get the command to test + self.cmd = ( + consistency_group_snapshot. + ListConsistencyGroupSnapshot(self.app, None) + ) + + def test_consistency_group_snapshot_list_without_options(self): + arglist = [] + verifylist = [ + ("all_projects", False), + ("long", False), + ("status", None), + ("consistency_group", None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'all_tenants': False, + 'status': None, + 'consistencygroup_id': None, + } + self.cgsnapshots_mock.list.assert_called_once_with( + detailed=True, search_opts=search_opts) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_consistency_group_snapshot_list_with_long(self): + arglist = [ + "--long", + ] + verifylist = [ + ("all_projects", False), + ("long", True), + ("status", None), + ("consistency_group", None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'all_tenants': False, + 'status': None, + 'consistencygroup_id': None, + } + self.cgsnapshots_mock.list.assert_called_once_with( + detailed=True, search_opts=search_opts) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + + def test_consistency_group_snapshot_list_with_options(self): + arglist = [ + "--all-project", + "--status", self.consistency_group_snapshots[0].status, + "--consistency-group", self.consistency_group.id, + ] + verifylist = [ + ("all_projects", True), + ("long", False), + ("status", self.consistency_group_snapshots[0].status), + ("consistency_group", self.consistency_group.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'all_tenants': True, + 'status': self.consistency_group_snapshots[0].status, + 'consistencygroup_id': self.consistency_group.id, + } + self.consistencygroups_mock.get.assert_called_once_with( + self.consistency_group.id) + self.cgsnapshots_mock.list.assert_called_once_with( + detailed=True, search_opts=search_opts) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestConsistencyGroupSnapshotShow(TestConsistencyGroupSnapshot): + + _consistency_group_snapshot = ( + volume_fakes. + FakeConsistencyGroupSnapshot. + create_one_consistency_group_snapshot() + ) + + columns = ( + 'consistencygroup_id', + 'created_at', + 'description', + 'id', + 'name', + 'status', + ) + data = ( + _consistency_group_snapshot.consistencygroup_id, + _consistency_group_snapshot.created_at, + _consistency_group_snapshot.description, + _consistency_group_snapshot.id, + _consistency_group_snapshot.name, + _consistency_group_snapshot.status, + ) + + def setUp(self): + super(TestConsistencyGroupSnapshotShow, self).setUp() + + self.cgsnapshots_mock.get.return_value = ( + self._consistency_group_snapshot) + self.cmd = (consistency_group_snapshot. + ShowConsistencyGroupSnapshot(self.app, None)) + + def test_consistency_group_snapshot_show(self): + arglist = [ + self._consistency_group_snapshot.id + ] + verifylist = [ + ("consistency_group_snapshot", self._consistency_group_snapshot.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.cgsnapshots_mock.get.assert_called_once_with( + self._consistency_group_snapshot.id) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_qos_specs.py b/openstackclient/tests/unit/volume/v2/test_qos_specs.py index 7597e852..35d9a345 100644 --- a/openstackclient/tests/unit/volume/v2/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v2/test_qos_specs.py @@ -70,24 +70,26 @@ class TestQosAssociate(TestQos): class TestQosCreate(TestQos): - new_qos_spec = volume_fakes.FakeQos.create_one_qos() columns = ( 'consumer', 'id', 'name', - 'specs' - ) - data = ( - new_qos_spec.consumer, - new_qos_spec.id, - new_qos_spec.name, - new_qos_spec.specs + 'properties' ) def setUp(self): super(TestQosCreate, self).setUp() + self.new_qos_spec = volume_fakes.FakeQos.create_one_qos() self.qos_mock.create.return_value = self.new_qos_spec + + self.data = ( + self.new_qos_spec.consumer, + self.new_qos_spec.id, + self.new_qos_spec.name, + utils.format_dict(self.new_qos_spec.specs) + ) + # Get the command object to test self.cmd = qos_specs.CreateQos(self.app, None) @@ -147,11 +149,11 @@ class TestQosCreate(TestQos): columns, data = self.cmd.take_action(parsed_args) - self.new_qos_spec.specs.update( - {'consumer': self.new_qos_spec.consumer}) self.qos_mock.create.assert_called_with( self.new_qos_spec.name, - self.new_qos_spec.specs + {'consumer': self.new_qos_spec.consumer, + 'foo': 'bar', + 'iops': '9001'} ) self.assertEqual(self.columns, columns) @@ -307,7 +309,7 @@ class TestQosList(TestQos): 'Name', 'Consumer', 'Associations', - 'Specs', + 'Properties', ) data = [] for q in qos_specs: @@ -383,7 +385,7 @@ class TestQosShow(TestQos): 'consumer', 'id', 'name', - 'specs' + 'properties' ) data = ( qos_association.name, diff --git a/openstackclient/tests/unit/volume/v2/test_snapshot.py b/openstackclient/tests/unit/volume/v2/test_snapshot.py index d355662d..1ad97e85 100644 --- a/openstackclient/tests/unit/volume/v2/test_snapshot.py +++ b/openstackclient/tests/unit/volume/v2/test_snapshot.py @@ -19,8 +19,9 @@ from mock import call from osc_lib import exceptions from osc_lib import utils +from openstackclient.tests.unit.identity.v3 import fakes as project_fakes from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes -from openstackclient.volume.v2 import snapshot +from openstackclient.volume.v2 import volume_snapshot class TestSnapshot(volume_fakes.TestVolume): @@ -32,6 +33,8 @@ class TestSnapshot(volume_fakes.TestVolume): self.snapshots_mock.reset_mock() self.volumes_mock = self.app.client_manager.volume.volumes self.volumes_mock.reset_mock() + self.project_mock = self.app.client_manager.identity.projects + self.project_mock.reset_mock() class TestSnapshotCreate(TestSnapshot): @@ -67,24 +70,25 @@ class TestSnapshotCreate(TestSnapshot): self.volumes_mock.get.return_value = self.volume self.snapshots_mock.create.return_value = self.new_snapshot + self.snapshots_mock.manage.return_value = self.new_snapshot # Get the command object to test - self.cmd = snapshot.CreateSnapshot(self.app, None) + self.cmd = volume_snapshot.CreateVolumeSnapshot(self.app, None) def test_snapshot_create(self): arglist = [ - "--name", self.new_snapshot.name, + "--volume", self.new_snapshot.volume_id, "--description", self.new_snapshot.description, "--force", '--property', 'Alpha=a', '--property', 'Beta=b', - self.new_snapshot.volume_id, + self.new_snapshot.name, ] verifylist = [ - ("name", self.new_snapshot.name), + ("volume", self.new_snapshot.volume_id), ("description", self.new_snapshot.description), ("force", True), ('property', {'Alpha': 'a', 'Beta': 'b'}), - ("volume", self.new_snapshot.volume_id), + ("snapshot_name", self.new_snapshot.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -102,7 +106,7 @@ class TestSnapshotCreate(TestSnapshot): def test_snapshot_create_without_name(self): arglist = [ - self.new_snapshot.volume_id, + "--volume", self.new_snapshot.volume_id, "--description", self.new_snapshot.description, "--force" ] @@ -125,6 +129,60 @@ class TestSnapshotCreate(TestSnapshot): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + def test_snapshot_create_without_volume(self): + arglist = [ + "--description", self.new_snapshot.description, + "--force", + self.new_snapshot.name + ] + verifylist = [ + ("description", self.new_snapshot.description), + ("force", True), + ("snapshot_name", self.new_snapshot.name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.get.assert_called_once_with( + self.new_snapshot.name) + self.snapshots_mock.create.assert_called_once_with( + self.new_snapshot.volume_id, + force=True, + name=self.new_snapshot.name, + description=self.new_snapshot.description, + metadata=None, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_snapshot_create_without_remote_source(self): + arglist = [ + '--remote-source', 'source-name=test_source_name', + '--remote-source', 'source-id=test_source_id', + '--volume', self.new_snapshot.volume_id, + ] + ref_dict = {'source-name': 'test_source_name', + 'source-id': 'test_source_id'} + verifylist = [ + ('remote_source', ref_dict), + ('volume', self.new_snapshot.volume_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.manage.assert_called_with( + volume_id=self.new_snapshot.volume_id, + ref=ref_dict, + name=None, + description=None, + metadata=None, + ) + self.snapshots_mock.create.assert_not_called() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + class TestSnapshotDelete(TestSnapshot): @@ -138,7 +196,7 @@ class TestSnapshotDelete(TestSnapshot): self.snapshots_mock.delete.return_value = None # Get the command object to mock - self.cmd = snapshot.DeleteSnapshot(self.app, None) + self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) def test_snapshot_delete(self): arglist = [ @@ -152,7 +210,24 @@ class TestSnapshotDelete(TestSnapshot): result = self.cmd.take_action(parsed_args) self.snapshots_mock.delete.assert_called_with( - self.snapshots[0].id) + self.snapshots[0].id, False) + self.assertIsNone(result) + + def test_snapshot_delete_with_force(self): + arglist = [ + '--force', + self.snapshots[0].id + ] + verifylist = [ + ('force', True), + ("snapshots", [self.snapshots[0].id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.snapshots_mock.delete.assert_called_with( + self.snapshots[0].id, True) self.assertIsNone(result) def test_delete_multiple_snapshots(self): @@ -168,7 +243,7 @@ class TestSnapshotDelete(TestSnapshot): calls = [] for s in self.snapshots: - calls.append(call(s.id)) + calls.append(call(s.id, False)) self.snapshots_mock.delete.assert_has_calls(calls) self.assertIsNone(result) @@ -199,13 +274,14 @@ class TestSnapshotDelete(TestSnapshot): self.assertEqual(2, find_mock.call_count) self.snapshots_mock.delete.assert_called_once_with( - self.snapshots[0].id + self.snapshots[0].id, False ) class TestSnapshotList(TestSnapshot): volume = volume_fakes.FakeVolume.create_one_volume() + project = project_fakes.FakeProject.create_one_project() snapshots = volume_fakes.FakeSnapshot.create_snapshots( attrs={'volume_id': volume.name}, count=3) @@ -248,22 +324,32 @@ class TestSnapshotList(TestSnapshot): super(TestSnapshotList, self).setUp() self.volumes_mock.list.return_value = [self.volume] + self.volumes_mock.get.return_value = self.volume + self.project_mock.get.return_value = self.project self.snapshots_mock.list.return_value = self.snapshots # Get the command to test - self.cmd = snapshot.ListSnapshot(self.app, None) + self.cmd = volume_snapshot.ListVolumeSnapshot(self.app, None) def test_snapshot_list_without_options(self): arglist = [] verifylist = [ ('all_projects', False), - ("long", False) + ('long', False) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.snapshots_mock.list.assert_called_once_with( - limit=None, marker=None, search_opts={'all_tenants': False}) + limit=None, marker=None, + search_opts={ + 'all_tenants': False, + 'name': None, + 'status': None, + 'project_id': None, + 'volume_id': None + } + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -271,11 +357,13 @@ class TestSnapshotList(TestSnapshot): arglist = [ "--long", "--limit", "2", + "--project", self.project.id, "--marker", self.snapshots[0].id, ] verifylist = [ ("long", True), ("limit", 2), + ("project", self.project.id), ("marker", self.snapshots[0].id), ('all_projects', False), ] @@ -286,7 +374,13 @@ class TestSnapshotList(TestSnapshot): self.snapshots_mock.list.assert_called_once_with( limit=2, marker=self.snapshots[0].id, - search_opts={'all_tenants': False} + search_opts={ + 'all_tenants': True, + 'project_id': self.project.id, + 'name': None, + 'status': None, + 'volume_id': None + } ) self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) @@ -304,7 +398,93 @@ class TestSnapshotList(TestSnapshot): columns, data = self.cmd.take_action(parsed_args) self.snapshots_mock.list.assert_called_once_with( - limit=None, marker=None, search_opts={'all_tenants': True}) + limit=None, marker=None, + search_opts={ + 'all_tenants': True, + 'name': None, + 'status': None, + 'project_id': None, + 'volume_id': None + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_name_option(self): + arglist = [ + '--name', self.snapshots[0].name, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('name', self.snapshots[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + limit=None, marker=None, + search_opts={ + 'all_tenants': False, + 'name': self.snapshots[0].name, + 'status': None, + 'project_id': None, + 'volume_id': None + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_status_option(self): + arglist = [ + '--status', self.snapshots[0].status, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('status', self.snapshots[0].status), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + limit=None, marker=None, + search_opts={ + 'all_tenants': False, + 'name': None, + 'status': self.snapshots[0].status, + 'project_id': None, + 'volume_id': None + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_volumeid_option(self): + arglist = [ + '--volume', self.volume.id, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('volume', self.volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.snapshots_mock.list.assert_called_once_with( + limit=None, marker=None, + search_opts={ + 'all_tenants': False, + 'name': None, + 'status': None, + 'project_id': None, + 'volume_id': self.volume.id + } + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -330,9 +510,25 @@ class TestSnapshotSet(TestSnapshot): self.snapshots_mock.set_metadata.return_value = None self.snapshots_mock.update.return_value = None # Get the command object to mock - self.cmd = snapshot.SetSnapshot(self.app, None) + self.cmd = volume_snapshot.SetVolumeSnapshot(self.app, None) + + def test_snapshot_set_no_option(self): + arglist = [ + self.snapshot.id, + ] + verifylist = [ + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - def test_snapshot_set(self): + result = self.cmd.take_action(parsed_args) + self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) + self.assertNotCalled(self.snapshots_mock.reset_state) + self.assertNotCalled(self.snapshots_mock.update) + self.assertNotCalled(self.snapshots_mock.set_metadata) + self.assertIsNone(result) + + def test_snapshot_set_name_and_property(self): arglist = [ "--name", "new_snapshot", "--property", "x=y", @@ -359,6 +555,51 @@ class TestSnapshotSet(TestSnapshot): ) self.assertIsNone(result) + def test_snapshot_set_with_no_property(self): + arglist = [ + "--no-property", + self.snapshot.id, + ] + verifylist = [ + ("no_property", True), + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) + self.assertNotCalled(self.snapshots_mock.reset_state) + self.assertNotCalled(self.snapshots_mock.update) + self.assertNotCalled(self.snapshots_mock.set_metadata) + self.snapshots_mock.delete_metadata.assert_called_with( + self.snapshot.id, ["foo"] + ) + self.assertIsNone(result) + + def test_snapshot_set_with_no_property_and_property(self): + arglist = [ + "--no-property", + "--property", "foo_1=bar_1", + self.snapshot.id, + ] + verifylist = [ + ("no_property", True), + ("property", {"foo_1": "bar_1"}), + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) + self.assertNotCalled(self.snapshots_mock.reset_state) + self.assertNotCalled(self.snapshots_mock.update) + self.snapshots_mock.delete_metadata.assert_called_with( + self.snapshot.id, ["foo"] + ) + self.snapshots_mock.set_metadata.assert_called_once_with( + self.snapshot.id, {"foo_1": "bar_1"}) + self.assertIsNone(result) + def test_snapshot_set_state_to_error(self): arglist = [ "--state", "error", @@ -457,7 +698,7 @@ class TestSnapshotShow(TestSnapshot): self.snapshots_mock.get.return_value = self.snapshot # Get the command object to test - self.cmd = snapshot.ShowSnapshot(self.app, None) + self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) def test_snapshot_show(self): arglist = [ @@ -485,7 +726,7 @@ class TestSnapshotUnset(TestSnapshot): self.snapshots_mock.get.return_value = self.snapshot self.snapshots_mock.delete_metadata.return_value = None # Get the command object to mock - self.cmd = snapshot.UnsetSnapshot(self.app, None) + self.cmd = volume_snapshot.UnsetVolumeSnapshot(self.app, None) def test_snapshot_unset(self): arglist = [ diff --git a/openstackclient/tests/unit/volume/v2/test_type.py b/openstackclient/tests/unit/volume/v2/test_type.py index 84f87e3b..4023d55b 100644 --- a/openstackclient/tests/unit/volume/v2/test_type.py +++ b/openstackclient/tests/unit/volume/v2/test_type.py @@ -13,6 +13,7 @@ # import mock +from mock import call from osc_lib import exceptions from osc_lib import utils @@ -35,6 +36,10 @@ class TestType(volume_fakes.TestVolume): self.app.client_manager.volume.volume_type_access) self.types_access_mock.reset_mock() + self.encryption_types_mock = ( + self.app.client_manager.volume.volume_encryption_types) + self.encryption_types_mock.reset_mock() + self.projects_mock = self.app.client_manager.identity.projects self.projects_mock.reset_mock() @@ -130,15 +135,78 @@ class TestTypeCreate(TestType): self.cmd.take_action, parsed_args) + def test_type_create_with_encryption(self): + encryption_info = { + 'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': '128', + 'control_location': 'front-end', + } + encryption_type = volume_fakes.FakeType.create_one_encryption_type( + attrs=encryption_info + ) + self.new_volume_type = volume_fakes.FakeType.create_one_type( + attrs={'encryption': encryption_info}) + self.types_mock.create.return_value = self.new_volume_type + self.encryption_types_mock.create.return_value = encryption_type + encryption_columns = ( + 'description', + 'encryption', + 'id', + 'is_public', + 'name', + ) + encryption_data = ( + self.new_volume_type.description, + utils.format_dict(encryption_info), + self.new_volume_type.id, + True, + self.new_volume_type.name, + ) + arglist = [ + '--encryption-provider', 'LuksEncryptor', + '--encryption-cipher', 'aes-xts-plain64', + '--encryption-key-size', '128', + '--encryption-control-location', 'front-end', + self.new_volume_type.name, + ] + verifylist = [ + ('encryption_provider', 'LuksEncryptor'), + ('encryption_cipher', 'aes-xts-plain64'), + ('encryption_key_size', 128), + ('encryption_control_location', 'front-end'), + ('name', self.new_volume_type.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.types_mock.create.assert_called_with( + self.new_volume_type.name, + description=None, + ) + body = { + 'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end', + } + self.encryption_types_mock.create.assert_called_with( + self.new_volume_type, + body, + ) + self.assertEqual(encryption_columns, columns) + self.assertEqual(encryption_data, data) + class TestTypeDelete(TestType): - volume_type = volume_fakes.FakeType.create_one_type() + volume_types = volume_fakes.FakeType.create_types(count=2) def setUp(self): super(TestTypeDelete, self).setUp() - self.types_mock.get.return_value = self.volume_type + self.types_mock.get = volume_fakes.FakeType.get_types( + self.volume_types) self.types_mock.delete.return_value = None # Get the command object to mock @@ -146,18 +214,64 @@ class TestTypeDelete(TestType): def test_type_delete(self): arglist = [ - self.volume_type.id + self.volume_types[0].id ] verifylist = [ - ("volume_types", [self.volume_type.id]) + ("volume_types", [self.volume_types[0].id]) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.types_mock.delete.assert_called_with(self.volume_type) + self.types_mock.delete.assert_called_with(self.volume_types[0]) + self.assertIsNone(result) + + def test_delete_multiple_types(self): + arglist = [] + for t in self.volume_types: + arglist.append(t.id) + verifylist = [ + ('volume_types', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for t in self.volume_types: + calls.append(call(t)) + self.types_mock.delete.assert_has_calls(calls) self.assertIsNone(result) + def test_delete_multiple_types_with_exception(self): + arglist = [ + self.volume_types[0].id, + 'unexist_type', + ] + verifylist = [ + ('volume_types', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self.volume_types[0], exceptions.CommandError] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 volume types failed to delete.', + str(e)) + find_mock.assert_any_call( + self.types_mock, self.volume_types[0].id) + find_mock.assert_any_call(self.types_mock, 'unexist_type') + + self.assertEqual(2, find_mock.call_count) + self.types_mock.delete.assert_called_once_with( + self.volume_types[0] + ) + class TestTypeList(TestType): @@ -165,24 +279,31 @@ class TestTypeList(TestType): columns = [ "ID", - "Name" + "Name", + "Is Public", ] columns_long = columns + [ "Description", "Properties" ] - + data_with_default_type = [( + volume_types[0].id, + volume_types[0].name, + True + )] data = [] for t in volume_types: data.append(( t.id, t.name, + t.is_public, )) data_long = [] for t in volume_types: data_long.append(( t.id, t.name, + t.is_public, t.description, utils.format_dict(t.extra_specs), )) @@ -191,6 +312,7 @@ class TestTypeList(TestType): super(TestTypeList, self).setUp() self.types_mock.list.return_value = self.volume_types + self.types_mock.default.return_value = self.volume_types[0] # get the command to test self.cmd = volume_type.ListVolumeType(self.app, None) @@ -200,6 +322,7 @@ class TestTypeList(TestType): ("long", False), ("private", False), ("public", False), + ("default", False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -217,6 +340,7 @@ class TestTypeList(TestType): ("long", True), ("private", False), ("public", True), + ("default", False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -233,6 +357,7 @@ class TestTypeList(TestType): ("long", False), ("private", True), ("public", False), + ("default", False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -241,6 +366,65 @@ class TestTypeList(TestType): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_type_list_with_default_option(self): + arglist = [ + "--default", + ] + verifylist = [ + ("encryption_type", False), + ("long", False), + ("private", False), + ("public", False), + ("default", True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.types_mock.default.assert_called_once_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data_with_default_type, list(data)) + + def test_type_list_with_encryption(self): + encryption_type = volume_fakes.FakeType.create_one_encryption_type( + attrs={'volume_type_id': self.volume_types[0].id}) + encryption_info = { + 'provider': 'LuksEncryptor', + 'cipher': None, + 'key_size': None, + 'control_location': 'front-end', + } + encryption_columns = self.columns + [ + "Encryption", + ] + encryption_data = [] + encryption_data.append(( + self.volume_types[0].id, + self.volume_types[0].name, + self.volume_types[0].is_public, + utils.format_dict(encryption_info), + )) + encryption_data.append(( + self.volume_types[1].id, + self.volume_types[1].name, + self.volume_types[1].is_public, + '-', + )) + + self.encryption_types_mock.list.return_value = [encryption_type] + arglist = [ + "--encryption-type", + ] + verifylist = [ + ("encryption_type", True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.encryption_types_mock.list.assert_called_once_with() + self.types_mock.list.assert_called_once_with(is_public=None) + self.assertEqual(encryption_columns, columns) + self.assertEqual(encryption_data, list(data)) + class TestTypeSet(TestType): @@ -255,6 +439,8 @@ class TestTypeSet(TestType): # Return a project self.projects_mock.get.return_value = self.project + self.encryption_types_mock.create.return_value = None + self.encryption_types_mock.update.return_value = None # Get the command object to test self.cmd = volume_type.SetVolumeType(self.app, None) @@ -378,6 +564,107 @@ class TestTypeSet(TestType): self.project.id, ) + def test_type_set_new_encryption(self): + self.encryption_types_mock.update.side_effect = ( + exceptions.NotFound('NotFound')) + arglist = [ + '--encryption-provider', 'LuksEncryptor', + '--encryption-cipher', 'aes-xts-plain64', + '--encryption-key-size', '128', + '--encryption-control-location', 'front-end', + self.volume_type.id, + ] + verifylist = [ + ('encryption_provider', 'LuksEncryptor'), + ('encryption_cipher', 'aes-xts-plain64'), + ('encryption_key_size', 128), + ('encryption_control_location', 'front-end'), + ('volume_type', self.volume_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + body = { + 'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end', + } + self.encryption_types_mock.update.assert_called_with( + self.volume_type, + body, + ) + self.encryption_types_mock.create.assert_called_with( + self.volume_type, + body, + ) + self.assertIsNone(result) + + @mock.patch.object(utils, 'find_resource') + def test_type_set_existing_encryption(self, mock_find): + mock_find.side_effect = [self.volume_type, + "existing_encryption_type"] + arglist = [ + '--encryption-provider', 'LuksEncryptor', + '--encryption-cipher', 'aes-xts-plain64', + '--encryption-control-location', 'front-end', + self.volume_type.id, + ] + verifylist = [ + ('encryption_provider', 'LuksEncryptor'), + ('encryption_cipher', 'aes-xts-plain64'), + ('encryption_control_location', 'front-end'), + ('volume_type', self.volume_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + body = { + 'provider': 'LuksEncryptor', + 'cipher': 'aes-xts-plain64', + 'control_location': 'front-end', + } + self.encryption_types_mock.update.assert_called_with( + self.volume_type, + body, + ) + self.encryption_types_mock.create.assert_not_called() + self.assertIsNone(result) + + def test_type_set_new_encryption_without_provider(self): + self.encryption_types_mock.update.side_effect = ( + exceptions.NotFound('NotFound')) + arglist = [ + '--encryption-cipher', 'aes-xts-plain64', + '--encryption-key-size', '128', + '--encryption-control-location', 'front-end', + self.volume_type.id, + ] + verifylist = [ + ('encryption_cipher', 'aes-xts-plain64'), + ('encryption_key_size', 128), + ('encryption_control_location', 'front-end'), + ('volume_type', self.volume_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual("Command Failed: One or more of" + " the operations failed", + str(e)) + body = { + 'cipher': 'aes-xts-plain64', + 'key_size': 128, + 'control_location': 'front-end', + } + self.encryption_types_mock.update.assert_called_with( + self.volume_type, + body, + ) + self.encryption_types_mock.create.assert_not_called() + class TestTypeShow(TestType): @@ -413,6 +700,7 @@ class TestTypeShow(TestType): self.volume_type.id ] verifylist = [ + ("encryption_type", False), ("volume_type", self.volume_type.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -488,6 +776,52 @@ class TestTypeShow(TestType): ) self.assertEqual(private_type_data, data) + def test_type_show_with_encryption(self): + encryption_type = volume_fakes.FakeType.create_one_encryption_type() + encryption_info = { + 'provider': 'LuksEncryptor', + 'cipher': None, + 'key_size': None, + 'control_location': 'front-end', + } + self.volume_type = volume_fakes.FakeType.create_one_type( + attrs={'encryption': encryption_info}) + self.types_mock.get.return_value = self.volume_type + self.encryption_types_mock.get.return_value = encryption_type + encryption_columns = ( + 'access_project_ids', + 'description', + 'encryption', + 'id', + 'is_public', + 'name', + 'properties', + ) + encryption_data = ( + None, + self.volume_type.description, + utils.format_dict(encryption_info), + self.volume_type.id, + True, + self.volume_type.name, + utils.format_dict(self.volume_type.extra_specs) + ) + arglist = [ + '--encryption-type', + self.volume_type.id + ] + verifylist = [ + ('encryption_type', True), + ("volume_type", self.volume_type.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.types_mock.get.assert_called_with(self.volume_type.id) + self.encryption_types_mock.get.assert_called_with(self.volume_type.id) + self.assertEqual(encryption_columns, columns) + self.assertEqual(encryption_data, data) + class TestTypeUnset(TestType): @@ -549,6 +883,7 @@ class TestTypeUnset(TestType): self.volume_type.id, ] verifylist = [ + ('encryption_type', False), ('project', ''), ('volume_type', self.volume_type.id), ] @@ -557,7 +892,7 @@ class TestTypeUnset(TestType): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - + self.encryption_types_mock.delete.assert_not_called() self.assertFalse(self.types_access_mock.remove_project_access.called) def test_type_unset_failed_with_missing_volume_type_argument(self): @@ -573,3 +908,18 @@ class TestTypeUnset(TestType): self.cmd, arglist, verifylist) + + def test_type_unset_encryption_type(self): + arglist = [ + '--encryption-type', + self.volume_type.id, + ] + verifylist = [ + ('encryption_type', True), + ('volume_type', self.volume_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.encryption_types_mock.delete.assert_called_with(self.volume_type) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index f4a7c142..fbe719f3 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -46,6 +46,9 @@ class TestVolume(volume_fakes.TestVolume): self.snapshots_mock = self.app.client_manager.volume.volume_snapshots self.snapshots_mock.reset_mock() + self.types_mock = self.app.client_manager.volume.volume_types + self.types_mock.reset_mock() + self.consistencygroups_mock = ( self.app.client_manager.volume.consistencygroups) self.consistencygroups_mock.reset_mock() @@ -447,6 +450,154 @@ class TestVolumeCreate(TestVolume): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_volume_create_with_bootable_and_readonly(self): + arglist = [ + '--bootable', + '--read-only', + '--size', str(self.new_volume.size), + self.new_volume.name, + ] + verifylist = [ + ('bootable', True), + ('non_bootable', False), + ('read_only', True), + ('read_write', False), + ('size', self.new_volume.size), + ('name', self.new_volume.name), + ] + + parsed_args = self.check_parser( + self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + size=self.new_volume.size, + snapshot_id=None, + name=self.new_volume.name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None, + consistencygroup_id=None, + source_replica=None, + multiattach=False, + scheduler_hints=None, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + self.volumes_mock.set_bootable.assert_called_with( + self.new_volume.id, True) + self.volumes_mock.update_readonly_flag.assert_called_with( + self.new_volume.id, True) + + def test_volume_create_with_nonbootable_and_readwrite(self): + arglist = [ + '--non-bootable', + '--read-write', + '--size', str(self.new_volume.size), + self.new_volume.name, + ] + verifylist = [ + ('bootable', False), + ('non_bootable', True), + ('read_only', False), + ('read_write', True), + ('size', self.new_volume.size), + ('name', self.new_volume.name), + ] + + parsed_args = self.check_parser( + self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + size=self.new_volume.size, + snapshot_id=None, + name=self.new_volume.name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None, + consistencygroup_id=None, + source_replica=None, + multiattach=False, + scheduler_hints=None, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + self.volumes_mock.set_bootable.assert_called_with( + self.new_volume.id, False) + self.volumes_mock.update_readonly_flag.assert_called_with( + self.new_volume.id, False) + + @mock.patch.object(volume.LOG, 'error') + def test_volume_create_with_bootable_and_readonly_fail( + self, mock_error): + + self.volumes_mock.set_bootable.side_effect = ( + exceptions.CommandError()) + + self.volumes_mock.update_readonly_flag.side_effect = ( + exceptions.CommandError()) + + arglist = [ + '--bootable', + '--read-only', + '--size', str(self.new_volume.size), + self.new_volume.name, + ] + verifylist = [ + ('bootable', True), + ('non_bootable', False), + ('read_only', True), + ('read_write', False), + ('size', self.new_volume.size), + ('name', self.new_volume.name), + ] + + parsed_args = self.check_parser( + self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volumes_mock.create.assert_called_with( + size=self.new_volume.size, + snapshot_id=None, + name=self.new_volume.name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None, + consistencygroup_id=None, + source_replica=None, + multiattach=False, + scheduler_hints=None, + ) + + self.assertEqual(2, mock_error.call_count) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + self.volumes_mock.set_bootable.assert_called_with( + self.new_volume.id, True) + self.volumes_mock.update_readonly_flag.assert_called_with( + self.new_volume.id, True) + def test_volume_create_with_source_replicated(self): self.volumes_mock.get.return_value = self.new_volume arglist = [ @@ -672,6 +823,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -702,6 +866,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': True, + 'project_id': self.project.id, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -734,6 +911,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': True, + 'project_id': self.project.id, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -764,6 +954,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': self.user.id, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] device = self.mock_volume.attachments[0]['device'] @@ -795,6 +998,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': self.user.id, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -825,6 +1041,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': self.mock_volume.name, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -855,6 +1084,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': self.mock_volume.status, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -885,6 +1127,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': True, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + self.assertEqual(self.columns, columns) server = self.mock_volume.attachments[0]['server_id'] @@ -916,6 +1171,19 @@ class TestVolumeList(TestVolume): columns, data = self.cmd.take_action(parsed_args) + search_opts = { + 'all_tenants': False, + 'project_id': None, + 'user_id': None, + 'display_name': None, + 'status': None, + } + self.volumes_mock.list.assert_called_once_with( + search_opts=search_opts, + marker=None, + limit=None, + ) + collist = [ 'ID', 'Display Name', @@ -996,17 +1264,128 @@ class TestVolumeList(TestVolume): self.cmd, arglist, verifylist) +class TestVolumeMigrate(TestVolume): + + _volume = volume_fakes.FakeVolume.create_one_volume() + + def setUp(self): + super(TestVolumeMigrate, self).setUp() + + self.volumes_mock.get.return_value = self._volume + self.volumes_mock.migrate_volume.return_value = None + # Get the command object to test + self.cmd = volume.MigrateVolume(self.app, None) + + def test_volume_migrate(self): + arglist = [ + "--host", "host@backend-name#pool", + self._volume.id, + ] + verifylist = [ + ("force_host_copy", False), + ("lock_volume", False), + ("unlock_volume", False), + ("host", "host@backend-name#pool"), + ("volume", self._volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.get.assert_called_once_with(self._volume.id) + self.volumes_mock.migrate_volume.assert_called_once_with( + self._volume.id, "host@backend-name#pool", False, False) + self.assertIsNone(result) + + def test_volume_migrate_with_option(self): + arglist = [ + "--force-host-copy", + "--lock-volume", + "--host", "host@backend-name#pool", + self._volume.id, + ] + verifylist = [ + ("force_host_copy", True), + ("lock_volume", True), + ("unlock_volume", False), + ("host", "host@backend-name#pool"), + ("volume", self._volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.get.assert_called_once_with(self._volume.id) + self.volumes_mock.migrate_volume.assert_called_once_with( + self._volume.id, "host@backend-name#pool", True, True) + self.assertIsNone(result) + + def test_volume_migrate_with_unlock_volume(self): + arglist = [ + "--unlock-volume", + "--host", "host@backend-name#pool", + self._volume.id, + ] + verifylist = [ + ("force_host_copy", False), + ("lock_volume", False), + ("unlock_volume", True), + ("host", "host@backend-name#pool"), + ("volume", self._volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.get.assert_called_once_with(self._volume.id) + self.volumes_mock.migrate_volume.assert_called_once_with( + self._volume.id, "host@backend-name#pool", False, False) + self.assertIsNone(result) + + def test_volume_migrate_without_host(self): + arglist = [ + self._volume.id, + ] + verifylist = [ + ("force_host_copy", False), + ("lock_volume", False), + ("unlock_volume", False), + ("volume", self._volume.id), + ] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + class TestVolumeSet(TestVolume): + volume_type = volume_fakes.FakeType.create_one_type() + def setUp(self): super(TestVolumeSet, self).setUp() self.new_volume = volume_fakes.FakeVolume.create_one_volume() self.volumes_mock.get.return_value = self.new_volume + self.types_mock.get.return_value = self.volume_type # Get the command object to test self.cmd = volume.SetVolume(self.app, None) + def test_volume_set_property(self): + arglist = [ + '--property', 'a=b', + '--property', 'c=d', + self.new_volume.id, + ] + verifylist = [ + ('property', {'a': 'b', 'c': 'd'}), + ('volume', self.new_volume.id), + ('bootable', False), + ('non_bootable', False) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.volumes_mock.set_metadata.assert_called_with( + self.new_volume.id, parsed_args.property) + def test_volume_set_image_property(self): arglist = [ '--image-property', 'Alpha=a', @@ -1033,6 +1412,8 @@ class TestVolumeSet(TestVolume): self.new_volume.id ] verifylist = [ + ('read_only', False), + ('read_write', False), ('state', 'error'), ('volume', self.new_volume.id) ] @@ -1042,6 +1423,7 @@ class TestVolumeSet(TestVolume): result = self.cmd.take_action(parsed_args) self.volumes_mock.reset_state.assert_called_with( self.new_volume.id, 'error') + self.volumes_mock.update_readonly_flag.assert_not_called() self.assertIsNone(result) def test_volume_set_state_failed(self): @@ -1090,6 +1472,104 @@ class TestVolumeSet(TestVolume): self.volumes_mock.set_bootable.assert_called_with( self.new_volume.id, verifylist[index][0][1]) + def test_volume_set_readonly(self): + arglist = [ + '--read-only', + self.new_volume.id + ] + verifylist = [ + ('read_only', True), + ('read_write', False), + ('volume', self.new_volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.update_readonly_flag.assert_called_once_with( + self.new_volume.id, + True) + self.assertIsNone(result) + + def test_volume_set_read_write(self): + arglist = [ + '--read-write', + self.new_volume.id + ] + verifylist = [ + ('read_only', False), + ('read_write', True), + ('volume', self.new_volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.update_readonly_flag.assert_called_once_with( + self.new_volume.id, + False) + self.assertIsNone(result) + + def test_volume_set_type(self): + arglist = [ + '--type', self.volume_type.id, + self.new_volume.id + ] + verifylist = [ + ('retype_policy', None), + ('type', self.volume_type.id), + ('volume', self.new_volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.retype.assert_called_once_with( + self.new_volume.id, + self.volume_type.id, + 'never') + self.assertIsNone(result) + + def test_volume_set_type_with_policy(self): + arglist = [ + '--retype-policy', 'on-demand', + '--type', self.volume_type.id, + self.new_volume.id + ] + verifylist = [ + ('retype_policy', 'on-demand'), + ('type', self.volume_type.id), + ('volume', self.new_volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.retype.assert_called_once_with( + self.new_volume.id, + self.volume_type.id, + 'on-demand') + self.assertIsNone(result) + + @mock.patch.object(volume.LOG, 'warning') + def test_volume_set_with_only_retype_policy(self, mock_warning): + arglist = [ + '--retype-policy', 'on-demand', + self.new_volume.id + ] + verifylist = [ + ('retype_policy', 'on-demand'), + ('volume', self.new_volume.id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.retype.assert_not_called() + mock_warning.assert_called_with("'--retype-policy' option will " + "not work without '--type' option") + self.assertIsNone(result) + class TestVolumeShow(TestVolume): diff --git a/openstackclient/tests/unit/volume/v2/test_volume_host.py b/openstackclient/tests/unit/volume/v2/test_volume_host.py new file mode 100644 index 00000000..b024329a --- /dev/null +++ b/openstackclient/tests/unit/volume/v2/test_volume_host.py @@ -0,0 +1,117 @@ +# +# 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.tests.unit.volume.v2 import fakes as host_fakes +from openstackclient.volume.v2 import volume_host + + +class TestVolumeHost(host_fakes.TestVolume): + + def setUp(self): + super(TestVolumeHost, self).setUp() + + self.host_mock = self.app.client_manager.volume.services + self.host_mock.reset_mock() + + +class TestVolumeHostSet(TestVolumeHost): + + service = host_fakes.FakeService.create_one_service() + + def setUp(self): + super(TestVolumeHostSet, self).setUp() + + self.host_mock.freeze_host.return_value = None + self.host_mock.thaw_host.return_value = None + + # Get the command object to mock + self.cmd = volume_host.SetVolumeHost(self.app, None) + + def test_volume_host_set_nothing(self): + arglist = [ + self.service.host, + ] + verifylist = [ + ('host', self.service.host), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.host_mock.freeze_host.assert_not_called() + self.host_mock.thaw_host.assert_not_called() + self.assertIsNone(result) + + def test_volume_host_set_enable(self): + arglist = [ + '--enable', + self.service.host, + ] + verifylist = [ + ('enable', True), + ('host', self.service.host), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.host_mock.thaw_host.assert_called_with(self.service.host) + self.host_mock.freeze_host.assert_not_called() + self.assertIsNone(result) + + def test_volume_host_set_disable(self): + arglist = [ + '--disable', + self.service.host, + ] + verifylist = [ + ('disable', True), + ('host', self.service.host), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.host_mock.freeze_host.assert_called_with(self.service.host) + self.host_mock.thaw_host.assert_not_called() + self.assertIsNone(result) + + +class TestVolumeHostFailover(TestVolumeHost): + + service = host_fakes.FakeService.create_one_service() + + def setUp(self): + super(TestVolumeHostFailover, self).setUp() + + self.host_mock.failover_host.return_value = None + + # Get the command object to mock + self.cmd = volume_host.FailoverVolumeHost(self.app, None) + + def test_volume_host_failover(self): + arglist = [ + '--volume-backend', 'backend_test', + self.service.host, + ] + verifylist = [ + ('volume_backend', 'backend_test'), + ('host', self.service.host), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.host_mock.failover_host.assert_called_with( + self.service.host, 'backend_test') + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v3/__init__.py b/openstackclient/tests/unit/volume/v3/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/__init__.py diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index ade5a95f..c4b0dfca 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -27,7 +27,8 @@ API_VERSION_OPTION = 'os_volume_api_version' API_NAME = "volume" API_VERSIONS = { "1": "cinderclient.v1.client.Client", - "2": "cinderclient.v2.client.Client" + "2": "cinderclient.v2.client.Client", + "3": "cinderclient.v3.client.Client", } diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index c9d0ca0d..9ac1302a 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__) class CreateVolumeBackup(command.ShowOne): - """Create new volume backup""" + _description = _("Create new volume backup") def get_parser(self, prog_name): parser = super(CreateVolumeBackup, self).get_parser(prog_name) @@ -73,7 +73,7 @@ class CreateVolumeBackup(command.ShowOne): class CreateBackup(CreateVolumeBackup): - """Create new backup""" + _description = _("Create new backup") # TODO(Huanxuan Ao): Remove this class and ``backup create`` command # two cycles after Newton. @@ -90,7 +90,7 @@ class CreateBackup(CreateVolumeBackup): class DeleteVolumeBackup(command.Command): - """Delete volume backup(s)""" + _description = _("Delete volume backup(s)") def get_parser(self, prog_name): parser = super(DeleteVolumeBackup, self).get_parser(prog_name) @@ -125,7 +125,7 @@ class DeleteVolumeBackup(command.Command): class DeleteBackup(DeleteVolumeBackup): - """Delete backup(s)""" + _description = _("Delete backup(s)") # TODO(Huanxuan Ao): Remove this class and ``backup delete`` command # two cycles after Newton. @@ -142,7 +142,7 @@ class DeleteBackup(DeleteVolumeBackup): class ListVolumeBackup(command.Lister): - """List volume backups""" + _description = _("List volume backups") def get_parser(self, prog_name): parser = super(ListVolumeBackup, self).get_parser(prog_name) @@ -152,9 +152,36 @@ class ListVolumeBackup(command.Lister): default=False, help=_('List additional fields in output'), ) + parser.add_argument( + "--name", + metavar="<name>", + help=_("Filters results by the backup name") + ) + parser.add_argument( + "--status", + metavar="<status>", + choices=['creating', 'available', 'deleting', + 'error', 'restoring', 'error_restoring'], + help=_("Filters results by the backup status " + "('creating', 'available', 'deleting', " + "'error', 'restoring' or 'error_restoring')") + ) + parser.add_argument( + "--volume", + metavar="<volume>", + help=_("Filters results by the volume which they " + "backup (name or ID)") + ) + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=_('Include all projects (admin only)'), + ) return parser def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume def _format_volume_id(volume_id): """Return a volume name if available @@ -180,13 +207,25 @@ class ListVolumeBackup(command.Lister): # Cache the volume list volume_cache = {} try: - for s in self.app.client_manager.volume.volumes.list(): + for s in volume_client.volumes.list(): volume_cache[s.id] = s except Exception: # Just forget it if there's any trouble pass - data = self.app.client_manager.volume.backups.list() + filter_volume_id = None + if parsed_args.volume: + filter_volume_id = utils.find_resource(volume_client.volumes, + parsed_args.volume).id + search_opts = { + 'name': parsed_args.name, + 'status': parsed_args.status, + 'volume_id': filter_volume_id, + 'all_tenants': parsed_args.all_projects, + } + data = volume_client.backups.list( + search_opts=search_opts, + ) return (column_headers, (utils.get_item_properties( @@ -196,7 +235,7 @@ class ListVolumeBackup(command.Lister): class ListBackup(ListVolumeBackup): - """List backups""" + _description = _("List backups") # TODO(Huanxuan Ao): Remove this class and ``backup list`` command # two cycles after Newton. @@ -213,7 +252,7 @@ class ListBackup(ListVolumeBackup): class RestoreVolumeBackup(command.Command): - """Restore volume backup""" + _description = _("Restore volume backup") def get_parser(self, prog_name): parser = super(RestoreVolumeBackup, self).get_parser(prog_name) @@ -240,7 +279,7 @@ class RestoreVolumeBackup(command.Command): class RestoreBackup(RestoreVolumeBackup): - """Restore backup""" + _description = _("Restore backup") # TODO(Huanxuan Ao): Remove this class and ``backup restore`` command # two cycles after Newton. @@ -257,7 +296,7 @@ class RestoreBackup(RestoreVolumeBackup): class ShowVolumeBackup(command.ShowOne): - """Display volume backup details""" + _description = _("Display volume backup details") def get_parser(self, prog_name): parser = super(ShowVolumeBackup, self).get_parser(prog_name) @@ -277,7 +316,7 @@ class ShowVolumeBackup(command.ShowOne): class ShowBackup(ShowVolumeBackup): - """Display backup details""" + _description = _("Display backup details") # TODO(Huanxuan Ao): Remove this class and ``backup show`` command # two cycles after Newton. diff --git a/openstackclient/volume/v1/qos_specs.py b/openstackclient/volume/v1/qos_specs.py index 93c24a21..bae8c1ab 100644 --- a/openstackclient/volume/v1/qos_specs.py +++ b/openstackclient/volume/v1/qos_specs.py @@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__) class AssociateQos(command.Command): - """Associate a QoS specification to a volume type""" + _description = _("Associate a QoS specification to a volume type") def get_parser(self, prog_name): parser = super(AssociateQos, self).get_parser(prog_name) @@ -57,7 +57,7 @@ class AssociateQos(command.Command): class CreateQos(command.ShowOne): - """Create new QoS specification""" + _description = _("Create new QoS specification") def get_parser(self, prog_name): parser = super(CreateQos, self).get_parser(prog_name) @@ -94,12 +94,14 @@ class CreateQos(command.ShowOne): specs.update(parsed_args.property) qos_spec = volume_client.qos_specs.create(parsed_args.name, specs) - + qos_spec._info.update( + {'properties': utils.format_dict(qos_spec._info.pop('specs'))} + ) return zip(*sorted(six.iteritems(qos_spec._info))) class DeleteQos(command.Command): - """Delete QoS specification""" + _description = _("Delete QoS specification") def get_parser(self, prog_name): parser = super(DeleteQos, self).get_parser(prog_name) @@ -139,7 +141,7 @@ class DeleteQos(command.Command): class DisassociateQos(command.Command): - """Disassociate a QoS specification from a volume type""" + _description = _("Disassociate a QoS specification from a volume type") def get_parser(self, prog_name): parser = super(DisassociateQos, self).get_parser(prog_name) @@ -177,7 +179,7 @@ class DisassociateQos(command.Command): class ListQos(command.Lister): - """List QoS specifications""" + _description = _("List QoS specifications") def take_action(self, parsed_args): volume_client = self.app.client_manager.volume @@ -190,8 +192,10 @@ class ListQos(command.Lister): for association in qos_associations] qos._info.update({'associations': associations}) + display_columns = ( + 'ID', 'Name', 'Consumer', 'Associations', 'Properties') columns = ('ID', 'Name', 'Consumer', 'Associations', 'Specs') - return (columns, + return (display_columns, (utils.get_dict_properties( s._info, columns, formatters={ @@ -202,7 +206,7 @@ class ListQos(command.Lister): class SetQos(command.Command): - """Set QoS specification properties""" + _description = _("Set QoS specification properties") def get_parser(self, prog_name): parser = super(SetQos, self).get_parser(prog_name) @@ -231,7 +235,7 @@ class SetQos(command.Command): class ShowQos(command.ShowOne): - """Display QoS specification details""" + _description = _("Display QoS specification details") def get_parser(self, prog_name): parser = super(ShowQos, self).get_parser(prog_name) @@ -254,13 +258,14 @@ class ShowQos(command.ShowOne): qos_spec._info.update({ 'associations': utils.format_list(associations) }) - qos_spec._info.update({'specs': utils.format_dict(qos_spec.specs)}) + qos_spec._info.update( + {'properties': utils.format_dict(qos_spec._info.pop('specs'))}) return zip(*sorted(six.iteritems(qos_spec._info))) class UnsetQos(command.Command): - """Unset QoS specification properties""" + _description = _("Unset QoS specification properties") def get_parser(self, prog_name): parser = super(UnsetQos, self).get_parser(prog_name) diff --git a/openstackclient/volume/v1/service.py b/openstackclient/volume/v1/service.py index 867c4b9c..d468c6ff 100644 --- a/openstackclient/volume/v1/service.py +++ b/openstackclient/volume/v1/service.py @@ -22,7 +22,7 @@ from openstackclient.i18n import _ class ListService(command.Lister): - """List service command""" + _description = _("List service command") def get_parser(self, prog_name): parser = super(ListService, self).get_parser(prog_name) @@ -76,7 +76,7 @@ class ListService(command.Lister): class SetService(command.Command): - """Set volume service properties""" + _description = _("Set volume service properties") def get_parser(self, prog_name): parser = super(SetService, self).get_parser(prog_name) diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index bc92c0f5..e9e3894b 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -13,6 +13,10 @@ # under the License. # +# TODO(Huanxuan Ao): Remove this file and "snapshot create", "snapshot delete", +# "snapshot set", "snapshot show" and "snapshot unset" +# commands two cycles after Ocata. + """Volume v1 Snapshot action implementations""" import copy @@ -27,11 +31,13 @@ import six from openstackclient.i18n import _ +deprecated = True +LOG_DEP = logging.getLogger('deprecated') LOG = logging.getLogger(__name__) class CreateSnapshot(command.ShowOne): - """Create new snapshot""" + _description = _("Create new snapshot") def get_parser(self, prog_name): parser = super(CreateSnapshot, self).get_parser(prog_name) @@ -61,6 +67,8 @@ class CreateSnapshot(command.ShowOne): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot create" instead.')) volume_client = self.app.client_manager.volume volume_id = utils.find_resource(volume_client.volumes, parsed_args.volume).id @@ -79,7 +87,7 @@ class CreateSnapshot(command.ShowOne): class DeleteSnapshot(command.Command): - """Delete snapshot(s)""" + _description = _("Delete snapshot(s)") def get_parser(self, prog_name): parser = super(DeleteSnapshot, self).get_parser(prog_name) @@ -92,6 +100,8 @@ class DeleteSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot delete" instead.')) volume_client = self.app.client_manager.volume result = 0 @@ -114,7 +124,7 @@ class DeleteSnapshot(command.Command): class ListSnapshot(command.Lister): - """List snapshots""" + _description = _("List snapshots") def get_parser(self, prog_name): parser = super(ListSnapshot, self).get_parser(prog_name) @@ -133,6 +143,8 @@ class ListSnapshot(command.Lister): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot list" instead.')) def _format_volume_id(volume_id): """Return a volume name if available @@ -185,7 +197,7 @@ class ListSnapshot(command.Lister): class SetSnapshot(command.Command): - """Set snapshot properties""" + _description = _("Set snapshot properties") def get_parser(self, prog_name): parser = super(SetSnapshot, self).get_parser(prog_name) @@ -214,6 +226,8 @@ class SetSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot set" instead.')) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) @@ -246,7 +260,7 @@ class SetSnapshot(command.Command): class ShowSnapshot(command.ShowOne): - """Display snapshot details""" + _description = _("Display snapshot details") def get_parser(self, prog_name): parser = super(ShowSnapshot, self).get_parser(prog_name) @@ -258,6 +272,8 @@ class ShowSnapshot(command.ShowOne): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot show" instead.')) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) @@ -270,7 +286,7 @@ class ShowSnapshot(command.ShowOne): class UnsetSnapshot(command.Command): - """Unset snapshot properties""" + _description = _("Unset snapshot properties") def get_parser(self, prog_name): parser = super(UnsetSnapshot, self).get_parser(prog_name) @@ -289,6 +305,8 @@ class UnsetSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot unset" instead.')) 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 cafe8ce6..8e1097f5 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -45,7 +45,7 @@ def _check_size_arg(args): class CreateVolume(command.ShowOne): - """Create new volume""" + _description = _("Create new volume") def get_parser(self, prog_name): parser = super(CreateVolume, self).get_parser(prog_name) @@ -114,6 +114,28 @@ class CreateVolume(command.ShowOne): help=_('Set a property on this volume ' '(repeat option to set multiple properties)'), ) + bootable_group = parser.add_mutually_exclusive_group() + bootable_group.add_argument( + "--bootable", + action="store_true", + help=_("Mark volume as bootable") + ) + bootable_group.add_argument( + "--non-bootable", + action="store_true", + help=_("Mark volume as non-bootable (default)") + ) + readonly_group = parser.add_mutually_exclusive_group() + readonly_group.add_argument( + "--read-only", + action="store_true", + help=_("Set volume to read-only access mode") + ) + readonly_group.add_argument( + "--read-write", + action="store_true", + help=_("Set volume to read-write access mode (default)") + ) return parser @@ -166,6 +188,22 @@ class CreateVolume(command.ShowOne): parsed_args.property, image, ) + + if parsed_args.bootable or parsed_args.non_bootable: + try: + volume_client.volumes.set_bootable( + volume.id, parsed_args.bootable) + except Exception as e: + LOG.error(_("Failed to set volume bootable property: %s"), e) + if parsed_args.read_only or parsed_args.read_write: + try: + volume_client.volumes.update_readonly_flag( + volume.id, + parsed_args.read_only) + except Exception as e: + LOG.error(_("Failed to set volume read-only access " + "mode flag: %s"), e) + # Map 'metadata' column to 'properties' volume._info.update( { @@ -178,7 +216,7 @@ class CreateVolume(command.ShowOne): class DeleteVolume(command.Command): - """Delete volume(s)""" + _description = _("Delete volume(s)") def get_parser(self, prog_name): parser = super(DeleteVolume, self).get_parser(prog_name) @@ -223,7 +261,7 @@ class DeleteVolume(command.Command): class ListVolume(command.Lister): - """List volumes""" + _description = _("List volumes") def get_parser(self, prog_name): parser = super(ListVolume, self).get_parser(prog_name) @@ -344,8 +382,39 @@ class ListVolume(command.Lister): ) for s in data)) +class MigrateVolume(command.Command): + _description = _("Migrate volume to a new host") + + def get_parser(self, prog_name): + parser = super(MigrateVolume, self).get_parser(prog_name) + parser.add_argument( + 'volume', + metavar="<volume>", + help=_("Volume to migrate (name or ID)") + ) + parser.add_argument( + '--host', + metavar="<host>", + required=True, + help=_("Destination host (takes the form: host@backend-name#pool)") + ) + parser.add_argument( + '--force-host-copy', + action="store_true", + help=_("Enable generic host-based force-migration, " + "which bypasses driver optimizations") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + volume_client.volumes.migrate_volume(volume.id, parsed_args.host, + parsed_args.force_host_copy,) + + class SetVolume(command.Command): - """Set volume properties""" + _description = _("Set volume properties") def get_parser(self, prog_name): parser = super(SetVolume, self).get_parser(prog_name) @@ -371,6 +440,15 @@ class SetVolume(command.Command): help=_('Extend volume size in GB'), ) parser.add_argument( + "--no-property", + dest="no_property", + action="store_true", + help=_("Remove all properties from <volume> " + "(specify both --no-property and --property to " + "remove the current properties before setting " + "new properties.)"), + ) + parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, @@ -388,42 +466,91 @@ class SetVolume(command.Command): action="store_true", help=_("Mark volume as non-bootable") ) + readonly_group = parser.add_mutually_exclusive_group() + readonly_group.add_argument( + "--read-only", + action="store_true", + help=_("Set volume to read-only access mode") + ) + readonly_group.add_argument( + "--read-write", + action="store_true", + help=_("Set volume to read-write access mode") + ) return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + result = 0 if parsed_args.size: - if volume.status != 'available': - LOG.error(_("Volume is in %s state, it must be available " - "before size can be extended"), volume.status) - return - if parsed_args.size <= volume.size: - LOG.error(_("New size must be greater than %s GB"), - volume.size) - return - volume_client.volumes.extend(volume.id, parsed_args.size) + try: + if volume.status != 'available': + msg = (_("Volume is in %s state, it must be available " + "before size can be extended") % volume.status) + raise exceptions.CommandError(msg) + if parsed_args.size <= volume.size: + msg = (_("New size must be greater than %s GB") + % volume.size) + raise exceptions.CommandError(msg) + volume_client.volumes.extend(volume.id, parsed_args.size) + except Exception as e: + LOG.error(_("Failed to set volume size: %s"), e) + result += 1 + + if parsed_args.no_property: + try: + volume_client.volumes.delete_metadata( + volume.id, volume.metadata.keys()) + except Exception as e: + LOG.error(_("Failed to clean volume properties: %s"), e) + result += 1 if parsed_args.property: - volume_client.volumes.set_metadata(volume.id, parsed_args.property) + try: + volume_client.volumes.set_metadata( + volume.id, + parsed_args.property) + except Exception as e: + LOG.error(_("Failed to set volume property: %s"), e) + result += 1 if parsed_args.bootable or parsed_args.non_bootable: try: volume_client.volumes.set_bootable( volume.id, parsed_args.bootable) except Exception as e: LOG.error(_("Failed to set volume bootable property: %s"), e) + result += 1 + if parsed_args.read_only or parsed_args.read_write: + try: + volume_client.volumes.update_readonly_flag( + volume.id, + parsed_args.read_only) + except Exception as e: + LOG.error(_("Failed to set volume read-only access " + "mode flag: %s"), e) + result += 1 kwargs = {} if parsed_args.name: kwargs['display_name'] = parsed_args.name if parsed_args.description: kwargs['display_description'] = parsed_args.description if kwargs: - volume_client.volumes.update(volume.id, **kwargs) + try: + volume_client.volumes.update(volume.id, **kwargs) + except Exception as e: + LOG.error(_("Failed to update volume display name " + "or display description: %s"), e) + result += 1 + + if result > 0: + raise exceptions.CommandError(_("One or more of the " + "set operations failed")) class ShowVolume(command.ShowOne): - """Show volume details""" + _description = _("Show volume details") def get_parser(self, prog_name): parser = super(ShowVolume, self).get_parser(prog_name) @@ -453,7 +580,7 @@ class ShowVolume(command.ShowOne): class UnsetVolume(command.Command): - """Unset volume properties""" + _description = _("Unset volume properties") def get_parser(self, prog_name): parser = super(UnsetVolume, self).get_parser(prog_name) diff --git a/openstackclient/volume/v1/volume_snapshot.py b/openstackclient/volume/v1/volume_snapshot.py new file mode 100644 index 00000000..f22c338b --- /dev/null +++ b/openstackclient/volume/v1/volume_snapshot.py @@ -0,0 +1,355 @@ +# Copyright 2012-2013 OpenStack Foundation +# +# 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. +# + +"""Volume v1 Snapshot action implementations""" + +import copy +import logging + +from osc_lib.cli import parseractions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class CreateVolumeSnapshot(command.ShowOne): + _description = _("Create new volume snapshot") + + def get_parser(self, prog_name): + parser = super(CreateVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot_name', + metavar='<snapshot-name>', + nargs="?", + help=_('Name of the snapshot (default to None)'), + ) + parser.add_argument( + '--volume', + metavar='<volume>', + help=_('Volume to snapshot (name or ID) ' + '(default is <snapshot-name>)'), + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('Description of the snapshot'), + ) + parser.add_argument( + '--force', + dest='force', + action='store_true', + default=False, + help=_('Create a snapshot attached to an instance. ' + 'Default is False'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + volume = parsed_args.volume + if not parsed_args.volume: + volume = parsed_args.snapshot_name + volume_id = utils.find_resource(volume_client.volumes, + volume).id + snapshot = volume_client.volume_snapshots.create( + volume_id, + parsed_args.force, + parsed_args.snapshot_name, + parsed_args.description + ) + + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + + return zip(*sorted(six.iteritems(snapshot._info))) + + +class DeleteVolumeSnapshot(command.Command): + _description = _("Delete volume snapshot(s)") + + def get_parser(self, prog_name): + parser = super(DeleteVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshots', + metavar='<snapshot>', + nargs="+", + help=_('Snapshot(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result = 0 + + for i in parsed_args.snapshots: + try: + snapshot_id = utils.find_resource( + volume_client.volume_snapshots, i).id + volume_client.volume_snapshots.delete(snapshot_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete snapshot with " + "name or ID '%(snapshot)s': %(e)s"), + {'snapshot': i, 'e': e}) + + if result > 0: + total = len(parsed_args.snapshots) + msg = (_("%(result)s of %(total)s snapshots failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListVolumeSnapshot(command.Lister): + _description = _("List volume snapshots") + + def get_parser(self, prog_name): + parser = super(ListVolumeSnapshot, self).get_parser(prog_name) + 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'), + ) + parser.add_argument( + '--name', + metavar='<name>', + default=None, + help=_('Filters results by a name.') + ) + parser.add_argument( + '--status', + metavar='<status>', + choices=['available', 'error', 'creating', 'deleting', + 'error-deleting'], + help=_("Filters results by a status. " + "('available', 'error', 'creating', 'deleting'" + " or 'error-deleting')") + ) + parser.add_argument( + '--volume', + metavar='<volume>', + default=None, + help=_('Filters results by a volume (name or ID).') + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + + def _format_volume_id(volume_id): + """Return a volume name if available + + :param volume_id: a volume ID + :rtype: either the volume ID or name + """ + + volume = volume_id + if volume_id in volume_cache.keys(): + volume = volume_cache[volume_id].display_name + return volume + + if parsed_args.long: + columns = ['ID', 'Display Name', 'Display Description', 'Status', + 'Size', 'Created At', 'Volume ID', 'Metadata'] + column_headers = copy.deepcopy(columns) + column_headers[6] = 'Volume' + column_headers[7] = 'Properties' + else: + columns = ['ID', 'Display Name', 'Display Description', 'Status', + 'Size'] + column_headers = copy.deepcopy(columns) + + # Always update Name and Description + column_headers[1] = 'Name' + column_headers[2] = 'Description' + + # Cache the volume list + volume_cache = {} + try: + for s in volume_client.volumes.list(): + volume_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + + volume_id = None + if parsed_args.volume: + volume_id = utils.find_resource( + volume_client.volumes, parsed_args.volume).id + + search_opts = { + 'all_tenants': parsed_args.all_projects, + 'display_name': parsed_args.name, + 'status': parsed_args.status, + 'volume_id': volume_id, + } + + data = volume_client.volume_snapshots.list( + search_opts=search_opts) + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters={'Metadata': utils.format_dict, + 'Volume ID': _format_volume_id}, + ) for s in data)) + + +class SetVolumeSnapshot(command.Command): + _description = _("Set volume snapshot properties") + + def get_parser(self, prog_name): + parser = super(SetVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Snapshot to modify (name or ID)') + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_('New snapshot name') + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('New snapshot description') + ) + parser.add_argument( + "--no-property", + dest="no_property", + action="store_true", + help=_("Remove all properties from <snapshot> " + "(specify both --no-property and --property to " + "remove the current properties before setting " + "new properties.)"), + ) + parser.add_argument( + '--property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + help=_('Property to add/change for this snapshot ' + '(repeat option to set multiple properties)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource(volume_client.volume_snapshots, + parsed_args.snapshot) + + result = 0 + if parsed_args.no_property: + try: + key_list = snapshot.metadata.keys() + volume_client.volume_snapshots.delete_metadata( + snapshot.id, + list(key_list), + ) + except Exception as e: + LOG.error(_("Failed to clean snapshot properties: %s"), e) + result += 1 + + if parsed_args.property: + try: + volume_client.volume_snapshots.set_metadata( + snapshot.id, parsed_args.property) + except Exception as e: + LOG.error(_("Failed to set snapshot property: %s"), e) + result += 1 + + kwargs = {} + if parsed_args.name: + kwargs['display_name'] = parsed_args.name + if parsed_args.description: + kwargs['display_description'] = parsed_args.description + if kwargs: + try: + snapshot.update(**kwargs) + except Exception as e: + LOG.error(_("Failed to update snapshot display name " + "or display description: %s"), e) + result += 1 + + if result > 0: + raise exceptions.CommandError(_("One or more of the " + "set operations failed")) + + +class ShowVolumeSnapshot(command.ShowOne): + _description = _("Display volume snapshot details") + + def get_parser(self, prog_name): + parser = super(ShowVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Snapshot to display (name or ID)') + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource(volume_client.volume_snapshots, + parsed_args.snapshot) + + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + + return zip(*sorted(six.iteritems(snapshot._info))) + + +class UnsetVolumeSnapshot(command.Command): + _description = _("Unset volume snapshot properties") + + def get_parser(self, prog_name): + parser = super(UnsetVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Snapshot to modify (name or ID)'), + ) + parser.add_argument( + '--property', + metavar='<key>', + action='append', + help=_('Property to remove from snapshot ' + '(repeat option to remove multiple properties)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource( + volume_client.volume_snapshots, parsed_args.snapshot) + + if parsed_args.property: + volume_client.volume_snapshots.delete_metadata( + snapshot.id, + parsed_args.property, + ) diff --git a/openstackclient/volume/v1/volume_transfer_request.py b/openstackclient/volume/v1/volume_transfer_request.py index 4d6f2161..f24d5a56 100644 --- a/openstackclient/volume/v1/volume_transfer_request.py +++ b/openstackclient/volume/v1/volume_transfer_request.py @@ -28,7 +28,7 @@ LOG = logging.getLogger(__name__) class AcceptTransferRequest(command.ShowOne): - """Accept volume transfer request.""" + _description = _("Accept volume transfer request.") def get_parser(self, prog_name): parser = super(AcceptTransferRequest, self).get_parser(prog_name) @@ -56,7 +56,7 @@ class AcceptTransferRequest(command.ShowOne): class CreateTransferRequest(command.ShowOne): - """Create volume transfer request.""" + _description = _("Create volume transfer request.") def get_parser(self, prog_name): parser = super(CreateTransferRequest, self).get_parser(prog_name) @@ -85,7 +85,7 @@ class CreateTransferRequest(command.ShowOne): class DeleteTransferRequest(command.Command): - """Delete volume transfer request(s).""" + _description = _("Delete volume transfer request(s).") def get_parser(self, prog_name): parser = super(DeleteTransferRequest, self).get_parser(prog_name) @@ -120,7 +120,7 @@ class DeleteTransferRequest(command.Command): class ListTransferRequest(command.Lister): - """Lists all volume transfer requests.""" + _description = _("Lists all volume transfer requests.") def get_parser(self, prog_name): parser = super(ListTransferRequest, self).get_parser(prog_name) @@ -151,7 +151,7 @@ class ListTransferRequest(command.Lister): class ShowTransferRequest(command.ShowOne): - """Show volume transfer request details.""" + _description = _("Show volume transfer request details.") def get_parser(self, prog_name): parser = super(ShowTransferRequest, self).get_parser(prog_name) diff --git a/openstackclient/volume/v1/volume_type.py b/openstackclient/volume/v1/volume_type.py index 61e9f7fc..f9baa5be 100644 --- a/openstackclient/volume/v1/volume_type.py +++ b/openstackclient/volume/v1/volume_type.py @@ -29,8 +29,28 @@ from openstackclient.i18n import _ LOG = logging.getLogger(__name__) +def _create_encryption_type(volume_client, volume_type, parsed_args): + if not parsed_args.encryption_provider: + msg = _("'--encryption-provider' should be specified while " + "creating a new encryption type") + raise exceptions.CommandError(msg) + # set the default of control location while creating + control_location = 'front-end' + if parsed_args.encryption_control_location: + control_location = parsed_args.encryption_control_location + body = { + 'provider': parsed_args.encryption_provider, + 'cipher': parsed_args.encryption_cipher, + 'key_size': parsed_args.encryption_key_size, + 'control_location': control_location + } + encryption = volume_client.volume_encryption_types.create( + volume_type, body) + return encryption + + class CreateVolumeType(command.ShowOne): - """Create new volume type""" + _description = _("Create new volume type") def get_parser(self, prog_name): parser = super(CreateVolumeType, self).get_parser(prog_name) @@ -46,6 +66,42 @@ class CreateVolumeType(command.ShowOne): help=_('Set a property on this volume type ' '(repeat option to set multiple properties)'), ) + # TODO(Huanxuan Ao): Add choices for each "--encryption-*" option. + parser.add_argument( + '--encryption-provider', + metavar='<provider>', + help=_('Set the class that provides encryption support for ' + 'this volume type (e.g "LuksEncryptor") (admin only) ' + '(This option is required when setting encryption type ' + 'of a volume. Consider using other encryption options ' + 'such as: "--encryption-cipher", "--encryption-key-size" ' + 'and "--encryption-control-location")'), + ) + parser.add_argument( + '--encryption-cipher', + metavar='<cipher>', + help=_('Set the encryption algorithm or mode for this ' + 'volume type (e.g "aes-xts-plain64") (admin only)'), + ) + parser.add_argument( + '--encryption-key-size', + metavar='<key-size>', + type=int, + help=_('Set the size of the encryption key of this ' + 'volume type (e.g "128" or "256") (admin only)'), + ) + parser.add_argument( + '--encryption-control-location', + metavar='<control-location>', + choices=['front-end', 'back-end'], + help=_('Set the notional service where the encryption is ' + 'performed ("front-end" or "back-end") (admin only) ' + '(The default value for this option is "front-end" ' + 'when setting encryption type of a volume. Consider ' + 'using other encryption options such as: ' + '"--encryption-cipher", "--encryption-key-size" and ' + '"--encryption-provider")'), + ) return parser def take_action(self, parsed_args): @@ -55,13 +111,28 @@ class CreateVolumeType(command.ShowOne): if parsed_args.property: result = volume_type.set_keys(parsed_args.property) volume_type._info.update({'properties': utils.format_dict(result)}) + if (parsed_args.encryption_provider or + parsed_args.encryption_cipher or + parsed_args.encryption_key_size or + parsed_args.encryption_control_location): + try: + # create new encryption + encryption = _create_encryption_type( + volume_client, volume_type, parsed_args) + except Exception as e: + LOG.error(_("Failed to set encryption information for this " + "volume type: %s"), e) + # add encryption info in result + encryption._info.pop("volume_type_id", None) + volume_type._info.update( + {'encryption': utils.format_dict(encryption._info)}) volume_type._info.pop("os-volume-type-access:is_public", None) return zip(*sorted(six.iteritems(volume_type._info))) class DeleteVolumeType(command.Command): - """Delete volume type(s)""" + _description = _("Delete volume type(s)") def get_parser(self, prog_name): parser = super(DeleteVolumeType, self).get_parser(prog_name) @@ -97,7 +168,7 @@ class DeleteVolumeType(command.Command): class ListVolumeType(command.Lister): - """List volume types""" + _description = _("List volume types") def get_parser(self, prog_name): parser = super(ListVolumeType, self).get_parser(prog_name) @@ -107,25 +178,63 @@ class ListVolumeType(command.Lister): default=False, help=_('List additional fields in output') ) + parser.add_argument( + "--encryption-type", + action="store_true", + help=_("Display encryption information for each volume type " + "(admin only)"), + ) return parser def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume if parsed_args.long: - columns = ('ID', 'Name', 'Extra Specs') - column_headers = ('ID', 'Name', 'Properties') + columns = ['ID', 'Name', 'Is Public', 'Extra Specs'] + column_headers = ['ID', 'Name', 'Is Public', 'Properties'] else: - columns = ('ID', 'Name') - column_headers = columns - data = self.app.client_manager.volume.volume_types.list() + columns = ['ID', 'Name', 'Is Public'] + column_headers = ['ID', 'Name', 'Is Public'] + data = volume_client.volume_types.list() + + def _format_encryption_info(type_id, encryption_data=None): + encryption_data = encryption + encryption_info = '-' + if type_id in encryption_data.keys(): + encryption_info = encryption_data[type_id] + return encryption_info + + if parsed_args.encryption_type: + encryption = {} + for d in volume_client.volume_encryption_types.list(): + volume_type_id = d._info['volume_type_id'] + # remove some redundant information + del_key = [ + 'deleted', + 'created_at', + 'updated_at', + 'deleted_at', + 'volume_type_id' + ] + for key in del_key: + d._info.pop(key, None) + # save the encryption information with their volume type ID + encryption[volume_type_id] = utils.format_dict(d._info) + # We need to get volume type ID, then show encryption + # information according to the ID, so use "id" to keep + # difference to the real "ID" column. + columns += ['id'] + column_headers += ['Encryption'] + return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Extra Specs': utils.format_dict}, + formatters={'Extra Specs': utils.format_dict, + 'id': _format_encryption_info}, ) for s in data)) class SetVolumeType(command.Command): - """Set volume type properties""" + _description = _("Set volume type properties") def get_parser(self, prog_name): parser = super(SetVolumeType, self).get_parser(prog_name) @@ -141,6 +250,42 @@ class SetVolumeType(command.Command): help=_('Set a property on this volume type ' '(repeat option to set multiple properties)'), ) + # TODO(Huanxuan Ao): Add choices for each "--encryption-*" option. + parser.add_argument( + '--encryption-provider', + metavar='<provider>', + help=_('Set the class that provides encryption support for ' + 'this volume type (e.g "LuksEncryptor") (admin only) ' + '(This option is required when setting encryption type ' + 'of a volume. Consider using other encryption options ' + 'such as: "--encryption-cipher", "--encryption-key-size" ' + 'and "--encryption-control-location")'), + ) + parser.add_argument( + '--encryption-cipher', + metavar='<cipher>', + help=_('Set the encryption algorithm or mode for this ' + 'volume type (e.g "aes-xts-plain64") (admin only)'), + ) + parser.add_argument( + '--encryption-key-size', + metavar='<key-size>', + type=int, + help=_('Set the size of the encryption key of this ' + 'volume type (e.g "128" or "256") (admin only)'), + ) + parser.add_argument( + '--encryption-control-location', + metavar='<control-location>', + choices=['front-end', 'back-end'], + help=_('Set the notional service where the encryption is ' + 'performed ("front-end" or "back-end") (admin only) ' + '(The default value for this option is "front-end" ' + 'when setting encryption type of a volume. Consider ' + 'using other encryption options such as: ' + '"--encryption-cipher", "--encryption-key-size" and ' + '"--encryption-provider")'), + ) return parser def take_action(self, parsed_args): @@ -148,12 +293,33 @@ class SetVolumeType(command.Command): volume_type = utils.find_resource( volume_client.volume_types, parsed_args.volume_type) + result = 0 if parsed_args.property: - volume_type.set_keys(parsed_args.property) + try: + volume_type.set_keys(parsed_args.property) + except Exception as e: + LOG.error(_("Failed to set volume type property: %s"), e) + result += 1 + + if (parsed_args.encryption_provider or + parsed_args.encryption_cipher or + parsed_args.encryption_key_size or + parsed_args.encryption_control_location): + try: + _create_encryption_type( + volume_client, volume_type, parsed_args) + except Exception as e: + LOG.error(_("Failed to set encryption information for this " + "volume type: %s"), e) + result += 1 + + if result > 0: + raise exceptions.CommandError(_("Command Failed: One or more of" + " the operations failed")) class ShowVolumeType(command.ShowOne): - """Display volume type details""" + _description = _("Display volume type details") def get_parser(self, prog_name): parser = super(ShowVolumeType, self).get_parser(prog_name) @@ -162,6 +328,12 @@ class ShowVolumeType(command.ShowOne): metavar="<volume-type>", help=_("Volume type to display (name or ID)") ) + parser.add_argument( + "--encryption-type", + action="store_true", + help=_("Display encryption information of this volume type " + "(admin only)"), + ) return parser def take_action(self, parsed_args): @@ -170,12 +342,23 @@ class ShowVolumeType(command.ShowOne): volume_client.volume_types, parsed_args.volume_type) properties = utils.format_dict(volume_type._info.pop('extra_specs')) volume_type._info.update({'properties': properties}) + if parsed_args.encryption_type: + # show encryption type information for this volume type + try: + encryption = volume_client.volume_encryption_types.get( + volume_type.id) + encryption._info.pop("volume_type_id", None) + volume_type._info.update( + {'encryption': utils.format_dict(encryption._info)}) + except Exception as e: + LOG.error(_("Failed to display the encryption information " + "of this volume type: %s"), e) volume_type._info.pop("os-volume-type-access:is_public", None) return zip(*sorted(six.iteritems(volume_type._info))) class UnsetVolumeType(command.Command): - """Unset volume type properties""" + _description = _("Unset volume type properties") def get_parser(self, prog_name): parser = super(UnsetVolumeType, self).get_parser(prog_name) @@ -191,6 +374,12 @@ class UnsetVolumeType(command.Command): help=_('Remove a property from this volume type ' '(repeat option to remove multiple properties)'), ) + parser.add_argument( + "--encryption-type", + action="store_true", + help=_("Remove the encryption type for this volume type " + "(admin oly)"), + ) return parser def take_action(self, parsed_args): @@ -200,5 +389,21 @@ class UnsetVolumeType(command.Command): parsed_args.volume_type, ) + result = 0 if parsed_args.property: - volume_type.unset_keys(parsed_args.property) + try: + volume_type.unset_keys(parsed_args.property) + except Exception as e: + LOG.error(_("Failed to unset volume type property: %s"), e) + result += 1 + if parsed_args.encryption_type: + try: + volume_client.volume_encryption_types.delete(volume_type) + except Exception as e: + LOG.error(_("Failed to remove the encryption type for this " + "volume type: %s"), e) + result += 1 + + if result > 0: + raise exceptions.CommandError(_("Command Failed: One or more of" + " the operations failed")) diff --git a/openstackclient/volume/v2/backup.py b/openstackclient/volume/v2/backup.py index 2ca35b24..00389fcb 100644 --- a/openstackclient/volume/v2/backup.py +++ b/openstackclient/volume/v2/backup.py @@ -17,6 +17,7 @@ import copy import logging +from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -29,7 +30,7 @@ LOG = logging.getLogger(__name__) class CreateVolumeBackup(command.ShowOne): - """Create new volume backup""" + _description = _("Create new volume backup") def get_parser(self, prog_name): parser = super(CreateVolumeBackup, self).get_parser(prog_name) @@ -94,7 +95,7 @@ class CreateVolumeBackup(command.ShowOne): class CreateBackup(CreateVolumeBackup): - """Create new backup""" + _description = _("Create new backup") # TODO(Huanxuan Ao): Remove this class and ``backup create`` command # two cycles after Newton. @@ -111,7 +112,7 @@ class CreateBackup(CreateVolumeBackup): class DeleteVolumeBackup(command.Command): - """Delete volume backup(s)""" + _description = _("Delete volume backup(s)") def get_parser(self, prog_name): parser = super(DeleteVolumeBackup, self).get_parser(prog_name) @@ -152,7 +153,7 @@ class DeleteVolumeBackup(command.Command): class DeleteBackup(DeleteVolumeBackup): - """Delete backup(s)""" + _description = _("Delete backup(s)") # TODO(Huanxuan Ao): Remove this class and ``backup delete`` command # two cycles after Newton. @@ -169,7 +170,7 @@ class DeleteBackup(DeleteVolumeBackup): class ListVolumeBackup(command.Lister): - """List volume backups""" + _description = _("List volume backups") def get_parser(self, prog_name): parser = super(ListVolumeBackup, self).get_parser(prog_name) @@ -179,9 +180,48 @@ class ListVolumeBackup(command.Lister): default=False, help=_("List additional fields in output") ) + parser.add_argument( + "--name", + metavar="<name>", + help=_("Filters results by the backup name") + ) + parser.add_argument( + "--status", + metavar="<status>", + choices=['creating', 'available', 'deleting', + 'error', 'restoring', 'error_restoring'], + help=_("Filters results by the backup status " + "('creating', 'available', 'deleting', " + "'error', 'restoring' or 'error_restoring')") + ) + parser.add_argument( + "--volume", + metavar="<volume>", + help=_("Filters results by the volume which they " + "backup (name or ID)") + ) + parser.add_argument( + '--marker', + metavar='<marker>', + help=_('The last backup of the previous page (name or ID)'), + ) + parser.add_argument( + '--limit', + type=int, + action=parseractions.NonNegativeAction, + metavar='<limit>', + help=_('Maximum number of backups to display'), + ) + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=_('Include all projects (admin only)'), + ) return parser def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume def _format_volume_id(volume_id): """Return a volume name if available @@ -207,13 +247,31 @@ class ListVolumeBackup(command.Lister): # Cache the volume list volume_cache = {} try: - for s in self.app.client_manager.volume.volumes.list(): + for s in volume_client.volumes.list(): volume_cache[s.id] = s except Exception: # Just forget it if there's any trouble pass - data = self.app.client_manager.volume.backups.list() + filter_volume_id = None + if parsed_args.volume: + filter_volume_id = utils.find_resource(volume_client.volumes, + parsed_args.volume).id + marker_backup_id = None + if parsed_args.marker: + marker_backup_id = utils.find_resource(volume_client.backups, + parsed_args.marker).id + search_opts = { + 'name': parsed_args.name, + 'status': parsed_args.status, + 'volume_id': filter_volume_id, + 'all_tenants': parsed_args.all_projects, + } + data = volume_client.backups.list( + search_opts=search_opts, + marker=marker_backup_id, + limit=parsed_args.limit, + ) return (column_headers, (utils.get_item_properties( @@ -223,7 +281,7 @@ class ListVolumeBackup(command.Lister): class ListBackup(ListVolumeBackup): - """List backups""" + _description = _("List backups") # TODO(Huanxuan Ao): Remove this class and ``backup list`` command # two cycles after Newton. @@ -240,7 +298,7 @@ class ListBackup(ListVolumeBackup): class RestoreVolumeBackup(command.ShowOne): - """Restore volume backup""" + _description = _("Restore volume backup") def get_parser(self, prog_name): parser = super(RestoreVolumeBackup, self).get_parser(prog_name) @@ -265,7 +323,7 @@ class RestoreVolumeBackup(command.ShowOne): class RestoreBackup(RestoreVolumeBackup): - """Restore backup""" + _description = _("Restore backup") # TODO(Huanxuan Ao): Remove this class and ``backup restore`` command # two cycles after Newton. @@ -282,7 +340,7 @@ class RestoreBackup(RestoreVolumeBackup): class SetVolumeBackup(command.Command): - """Set volume backup properties""" + _description = _("Set volume backup properties") def get_parser(self, prog_name): parser = super(SetVolumeBackup, self).get_parser(prog_name) @@ -344,7 +402,7 @@ class SetVolumeBackup(command.Command): class ShowVolumeBackup(command.ShowOne): - """Display volume backup details""" + _description = _("Display volume backup details") def get_parser(self, prog_name): parser = super(ShowVolumeBackup, self).get_parser(prog_name) @@ -364,7 +422,7 @@ class ShowVolumeBackup(command.ShowOne): class ShowBackup(ShowVolumeBackup): - """Display backup details""" + _description = _("Display backup details") # TODO(Huanxuan Ao): Remove this class and ``backup show`` command # two cycles after Newton. diff --git a/openstackclient/volume/v2/consistency_group.py b/openstackclient/volume/v2/consistency_group.py index 39f2d577..0a932f84 100644 --- a/openstackclient/volume/v2/consistency_group.py +++ b/openstackclient/volume/v2/consistency_group.py @@ -14,21 +14,206 @@ """Volume v2 consistency group action implementations""" +import logging + from osc_lib.command import command +from osc_lib import exceptions from osc_lib import utils +import six from openstackclient.i18n import _ +LOG = logging.getLogger(__name__) + + +def _find_volumes(parsed_args_volumes, volume_client): + result = 0 + uuid = '' + for volume in parsed_args_volumes: + try: + volume_id = utils.find_resource( + volume_client.volumes, volume).id + uuid += volume_id + ',' + except Exception as e: + result += 1 + LOG.error(_("Failed to find volume with " + "name or ID '%(volume)s':%(e)s") + % {'volume': volume, 'e': e}) + + return result, uuid + + +class AddVolumeToConsistencyGroup(command.Command): + _description = _("Add volume(s) to consistency group") + + def get_parser(self, prog_name): + parser = super(AddVolumeToConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + 'consistency_group', + metavar="<consistency-group>", + help=_('Consistency group to contain <volume> (name or ID)'), + ) + parser.add_argument( + 'volumes', + metavar='<volume>', + nargs='+', + help=_('Volume(s) to add to <consistency-group> (name or ID) ' + '(repeat option to add multiple volumes)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result, add_uuid = _find_volumes(parsed_args.volumes, volume_client) + + if result > 0: + total = len(parsed_args.volumes) + LOG.error(_("%(result)s of %(total)s volumes failed " + "to add.") % {'result': result, 'total': total}) + + if add_uuid: + add_uuid = add_uuid.rstrip(',') + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group).id + volume_client.consistencygroups.update( + consistency_group_id, add_volumes=add_uuid) + + +class CreateConsistencyGroup(command.ShowOne): + _description = _("Create new consistency group.") + + def get_parser(self, prog_name): + parser = super(CreateConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + "name", + metavar="<name>", + nargs="?", + help=_("Name of new consistency group (default to None)") + ) + exclusive_group = parser.add_mutually_exclusive_group(required=True) + exclusive_group.add_argument( + "--volume-type", + metavar="<volume-type>", + help=_("Volume type of this consistency group (name or ID)") + ) + exclusive_group.add_argument( + "--consistency-group-source", + metavar="<consistency-group>", + help=_("Existing consistency group (name or ID)") + ) + exclusive_group.add_argument( + "--consistency-group-snapshot", + metavar="<consistency-group-snapshot>", + help=_("Existing consistency group snapshot (name or ID)") + ) + parser.add_argument( + "--description", + metavar="<description>", + help=_("Description of this consistency group") + ) + parser.add_argument( + "--availability-zone", + metavar="<availability-zone>", + help=_("Availability zone for this consistency group " + "(not available if creating consistency group " + "from source)"), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + if parsed_args.volume_type: + volume_type_id = utils.find_resource( + volume_client.volume_types, + parsed_args.volume_type).id + consistency_group = volume_client.consistencygroups.create( + volume_type_id, + name=parsed_args.name, + description=parsed_args.description, + availability_zone=parsed_args.availability_zone + ) + else: + if parsed_args.availability_zone: + msg = _("'--availability-zone' option will not work " + "if creating consistency group from source") + LOG.warning(msg) + + consistency_group_id = None + consistency_group_snapshot = None + if parsed_args.consistency_group_source: + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group_source).id + elif parsed_args.consistency_group_snapshot: + consistency_group_snapshot = utils.find_resource( + volume_client.cgsnapshots, + parsed_args.consistency_group_snapshot).id + + consistency_group = ( + volume_client.consistencygroups.create_from_src( + consistency_group_snapshot, + consistency_group_id, + name=parsed_args.name, + description=parsed_args.description + ) + ) + + return zip(*sorted(six.iteritems(consistency_group._info))) + + +class DeleteConsistencyGroup(command.Command): + _description = _("Delete consistency group(s).") + + def get_parser(self, prog_name): + parser = super(DeleteConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + 'consistency_groups', + metavar='<consistency-group>', + nargs="+", + help=_('Consistency group(s) to delete (name or ID)'), + ) + parser.add_argument( + '--force', + action='store_true', + default=False, + help=_("Allow delete in state other than error or available"), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result = 0 + + for i in parsed_args.consistency_groups: + try: + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, i).id + volume_client.consistencygroups.delete( + consistency_group_id, parsed_args.force) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete consistency group with " + "name or ID '%(consistency_group)s':%(e)s") + % {'consistency_group': i, 'e': e}) + + if result > 0: + total = len(parsed_args.consistency_groups) + msg = (_("%(result)s of %(total)s consistency groups failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + class ListConsistencyGroup(command.Lister): - """List consistency groups.""" + _description = _("List consistency groups.") def get_parser(self, prog_name): parser = super(ListConsistencyGroup, self).get_parser(prog_name) parser.add_argument( '--all-projects', action="store_true", - help=_('Show detail for all projects. Admin only. ' + help=_('Show details for all projects. Admin only. ' '(defaults to False)') ) parser.add_argument( @@ -55,3 +240,98 @@ class ListConsistencyGroup(command.Lister): s, columns, formatters={'Volume Types': utils.format_list}) for s in consistency_groups)) + + +class RemoveVolumeFromConsistencyGroup(command.Command): + _description = _("Remove volume(s) from consistency group") + + def get_parser(self, prog_name): + parser = \ + super(RemoveVolumeFromConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + 'consistency_group', + metavar="<consistency-group>", + help=_('Consistency group containing <volume> (name or ID)'), + ) + parser.add_argument( + 'volumes', + metavar='<volume>', + nargs='+', + help=_('Volume(s) to remove from <consistency-group> (name or ID) ' + '(repeat option to remove multiple volumes)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result, remove_uuid = _find_volumes(parsed_args.volumes, volume_client) + + if result > 0: + total = len(parsed_args.volumes) + LOG.error(_("%(result)s of %(total)s volumes failed " + "to remove.") % {'result': result, 'total': total}) + + if remove_uuid: + remove_uuid = remove_uuid.rstrip(',') + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group).id + volume_client.consistencygroups.update( + consistency_group_id, remove_volumes=remove_uuid) + + +class SetConsistencyGroup(command.Command): + _description = _("Set consistency group properties") + + def get_parser(self, prog_name): + parser = super(SetConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + 'consistency_group', + metavar='<consistency-group>', + help=_('Consistency group to modify (name or ID)') + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_('New consistency group name'), + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('New consistency group description'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + kwargs = {} + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description + if kwargs: + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group).id + volume_client.consistencygroups.update( + consistency_group_id, **kwargs) + + +class ShowConsistencyGroup(command.ShowOne): + _description = _("Display consistency group details.") + + def get_parser(self, prog_name): + parser = super(ShowConsistencyGroup, self).get_parser(prog_name) + parser.add_argument( + "consistency_group", + metavar="<consistency-group>", + help=_("Consistency group to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + consistency_group = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group) + return zip(*sorted(six.iteritems(consistency_group._info))) diff --git a/openstackclient/volume/v2/consistency_group_snapshot.py b/openstackclient/volume/v2/consistency_group_snapshot.py new file mode 100644 index 00000000..540deb01 --- /dev/null +++ b/openstackclient/volume/v2/consistency_group_snapshot.py @@ -0,0 +1,190 @@ +# +# 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. +# + +"""Volume v2 consistency group snapshot action implementations""" + +import logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class CreateConsistencyGroupSnapshot(command.ShowOne): + _description = _("Create new consistency group snapshot.") + + def get_parser(self, prog_name): + parser = super( + CreateConsistencyGroupSnapshot, self).get_parser(prog_name) + parser.add_argument( + "snapshot_name", + metavar="<snapshot-name>", + nargs="?", + help=_("Name of new consistency group snapshot (default to None)") + ) + parser.add_argument( + "--consistency-group", + metavar="<consistency-group>", + help=_("Consistency group to snapshot (name or ID) " + "(default to be the same as <snapshot-name>)") + ) + parser.add_argument( + "--description", + metavar="<description>", + help=_("Description of this consistency group snapshot") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + consistency_group = parsed_args.consistency_group + if not parsed_args.consistency_group: + # If "--consistency-group" not specified, then consistency_group + # will be the same as the new consistency group snapshot name + consistency_group = parsed_args.snapshot_name + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + consistency_group).id + consistency_group_snapshot = volume_client.cgsnapshots.create( + consistency_group_id, + name=parsed_args.snapshot_name, + description=parsed_args.description, + ) + + return zip(*sorted(six.iteritems(consistency_group_snapshot._info))) + + +class DeleteConsistencyGroupSnapshot(command.Command): + _description = _("Delete consistency group snapshot(s).") + + def get_parser(self, prog_name): + parser = super( + DeleteConsistencyGroupSnapshot, self).get_parser(prog_name) + parser.add_argument( + "consistency_group_snapshot", + metavar="<consistency-group-snapshot>", + nargs="+", + help=_("Consistency group snapshot(s) to delete (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result = 0 + + for snapshot in parsed_args.consistency_group_snapshot: + try: + snapshot_id = utils.find_resource(volume_client.cgsnapshots, + snapshot).id + + volume_client.cgsnapshots.delete(snapshot_id) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete consistency group snapshot " + "with name or ID '%(snapshot)s': %(e)s") + % {'snapshot': snapshot, 'e': e}) + + if result > 0: + total = len(parsed_args.consistency_group_snapshot) + msg = (_("%(result)s of %(total)s consistency group snapshots " + "failed to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListConsistencyGroupSnapshot(command.Lister): + _description = _("List consistency group snapshots.") + + def get_parser(self, prog_name): + parser = super( + ListConsistencyGroupSnapshot, self).get_parser(prog_name) + parser.add_argument( + '--all-projects', + action="store_true", + help=_('Show detail for all projects (admin only) ' + '(defaults to False)') + ) + parser.add_argument( + '--long', + action="store_true", + help=_('List additional fields in output') + ) + parser.add_argument( + '--status', + metavar="<status>", + choices=['available', 'error', 'creating', 'deleting', + 'error-deleting'], + help=_('Filters results by a status ("available", "error", ' + '"creating", "deleting" or "error_deleting")') + ) + parser.add_argument( + '--consistency-group', + metavar="<consistency-group>", + help=_('Filters results by a consistency group (name or ID)') + ) + return parser + + def take_action(self, parsed_args): + if parsed_args.long: + columns = ['ID', 'Status', 'ConsistencyGroup ID', + 'Name', 'Description', 'Created At'] + else: + columns = ['ID', 'Status', 'Name'] + volume_client = self.app.client_manager.volume + consistency_group_id = None + if parsed_args.consistency_group: + consistency_group_id = utils.find_resource( + volume_client.consistencygroups, + parsed_args.consistency_group, + ).id + search_opts = { + 'all_tenants': parsed_args.all_projects, + 'status': parsed_args.status, + 'consistencygroup_id': consistency_group_id, + } + consistency_group_snapshots = volume_client.cgsnapshots.list( + detailed=True, + search_opts=search_opts, + ) + + return (columns, ( + utils.get_item_properties( + s, columns) + for s in consistency_group_snapshots)) + + +class ShowConsistencyGroupSnapshot(command.ShowOne): + _description = _("Display consistency group snapshot details") + + def get_parser(self, prog_name): + parser = super( + ShowConsistencyGroupSnapshot, self).get_parser(prog_name) + parser.add_argument( + "consistency_group_snapshot", + metavar="<consistency-group-snapshot>", + help=_("Consistency group snapshot to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + consistency_group_snapshot = utils.find_resource( + volume_client.cgsnapshots, + parsed_args.consistency_group_snapshot) + return zip(*sorted(six.iteritems(consistency_group_snapshot._info))) diff --git a/openstackclient/volume/v2/qos_specs.py b/openstackclient/volume/v2/qos_specs.py index 9797f1a6..8e1d67b5 100644 --- a/openstackclient/volume/v2/qos_specs.py +++ b/openstackclient/volume/v2/qos_specs.py @@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__) class AssociateQos(command.Command): - """Associate a QoS specification to a volume type""" + _description = _("Associate a QoS specification to a volume type") def get_parser(self, prog_name): parser = super(AssociateQos, self).get_parser(prog_name) @@ -57,7 +57,7 @@ class AssociateQos(command.Command): class CreateQos(command.ShowOne): - """Create new QoS specification""" + _description = _("Create new QoS specification") def get_parser(self, prog_name): parser = super(CreateQos, self).get_parser(prog_name) @@ -95,11 +95,14 @@ class CreateQos(command.ShowOne): qos_spec = volume_client.qos_specs.create(parsed_args.name, specs) + qos_spec._info.update( + {'properties': utils.format_dict(qos_spec._info.pop('specs'))} + ) return zip(*sorted(six.iteritems(qos_spec._info))) class DeleteQos(command.Command): - """Delete QoS specification""" + _description = _("Delete QoS specification") def get_parser(self, prog_name): parser = super(DeleteQos, self).get_parser(prog_name) @@ -139,7 +142,7 @@ class DeleteQos(command.Command): class DisassociateQos(command.Command): - """Disassociate a QoS specification from a volume type""" + _description = _("Disassociate a QoS specification from a volume type") def get_parser(self, prog_name): parser = super(DisassociateQos, self).get_parser(prog_name) @@ -177,7 +180,7 @@ class DisassociateQos(command.Command): class ListQos(command.Lister): - """List QoS specifications""" + _description = _("List QoS specifications") def take_action(self, parsed_args): volume_client = self.app.client_manager.volume @@ -190,8 +193,11 @@ class ListQos(command.Lister): for association in qos_associations] qos._info.update({'associations': associations}) + display_columns = ( + 'ID', 'Name', 'Consumer', 'Associations', 'Properties') + columns = ('ID', 'Name', 'Consumer', 'Associations', 'Specs') - return (columns, + return (display_columns, (utils.get_dict_properties( s._info, columns, formatters={ @@ -202,7 +208,7 @@ class ListQos(command.Lister): class SetQos(command.Command): - """Set QoS specification properties""" + _description = _("Set QoS specification properties") def get_parser(self, prog_name): parser = super(SetQos, self).get_parser(prog_name) @@ -231,7 +237,7 @@ class SetQos(command.Command): class ShowQos(command.ShowOne): - """Display QoS specification details""" + _description = _("Display QoS specification details") def get_parser(self, prog_name): parser = super(ShowQos, self).get_parser(prog_name) @@ -254,13 +260,14 @@ class ShowQos(command.ShowOne): qos_spec._info.update({ 'associations': utils.format_list(associations) }) - qos_spec._info.update({'specs': utils.format_dict(qos_spec.specs)}) + qos_spec._info.update( + {'properties': utils.format_dict(qos_spec._info.pop('specs'))}) return zip(*sorted(six.iteritems(qos_spec._info))) class UnsetQos(command.Command): - """Unset QoS specification properties""" + _description = _("Unset QoS specification properties") def get_parser(self, prog_name): parser = super(UnsetQos, self).get_parser(prog_name) diff --git a/openstackclient/volume/v2/service.py b/openstackclient/volume/v2/service.py index 867c4b9c..d468c6ff 100644 --- a/openstackclient/volume/v2/service.py +++ b/openstackclient/volume/v2/service.py @@ -22,7 +22,7 @@ from openstackclient.i18n import _ class ListService(command.Lister): - """List service command""" + _description = _("List service command") def get_parser(self, prog_name): parser = super(ListService, self).get_parser(prog_name) @@ -76,7 +76,7 @@ class ListService(command.Lister): class SetService(command.Command): - """Set volume service properties""" + _description = _("Set volume service properties") def get_parser(self, prog_name): parser = super(SetService, self).get_parser(prog_name) diff --git a/openstackclient/volume/v2/snapshot.py b/openstackclient/volume/v2/snapshot.py index 4994e0da..a18887e3 100644 --- a/openstackclient/volume/v2/snapshot.py +++ b/openstackclient/volume/v2/snapshot.py @@ -12,6 +12,10 @@ # under the License. # +# TODO(Huanxuan Ao): Remove this file and "snapshot create", "snapshot delete", +# "snapshot set", "snapshot show" and "snapshot unset" +# commands two cycles after Ocata. + """Volume v2 snapshot action implementations""" import copy @@ -26,11 +30,13 @@ import six from openstackclient.i18n import _ +deprecated = True +LOG_DEP = logging.getLogger('deprecated') LOG = logging.getLogger(__name__) class CreateSnapshot(command.ShowOne): - """Create new snapshot""" + _description = _("Create new snapshot") def get_parser(self, prog_name): parser = super(CreateSnapshot, self).get_parser(prog_name) @@ -66,6 +72,8 @@ class CreateSnapshot(command.ShowOne): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot create" instead.')) volume_client = self.app.client_manager.volume volume_id = utils.find_resource( volume_client.volumes, parsed_args.volume).id @@ -83,7 +91,7 @@ class CreateSnapshot(command.ShowOne): class DeleteSnapshot(command.Command): - """Delete volume snapshot(s)""" + _description = _("Delete volume snapshot(s)") def get_parser(self, prog_name): parser = super(DeleteSnapshot, self).get_parser(prog_name) @@ -96,6 +104,8 @@ class DeleteSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot delete" instead.')) volume_client = self.app.client_manager.volume result = 0 @@ -118,7 +128,7 @@ class DeleteSnapshot(command.Command): class ListSnapshot(command.Lister): - """List snapshots""" + _description = _("List snapshots") def get_parser(self, prog_name): parser = super(ListSnapshot, self).get_parser(prog_name) @@ -149,6 +159,8 @@ class ListSnapshot(command.Lister): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot list" instead.')) def _format_volume_id(volume_id): """Return a volume name if available @@ -199,7 +211,7 @@ class ListSnapshot(command.Lister): class SetSnapshot(command.Command): - """Set snapshot properties""" + _description = _("Set snapshot properties") def get_parser(self, prog_name): parser = super(SetSnapshot, self).get_parser(prog_name) @@ -239,6 +251,8 @@ class SetSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot set" instead.')) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) @@ -280,7 +294,7 @@ class SetSnapshot(command.Command): class ShowSnapshot(command.ShowOne): - """Display snapshot details""" + _description = _("Display snapshot details") def get_parser(self, prog_name): parser = super(ShowSnapshot, self).get_parser(prog_name) @@ -292,6 +306,8 @@ class ShowSnapshot(command.ShowOne): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot show" instead.')) volume_client = self.app.client_manager.volume snapshot = utils.find_resource( volume_client.volume_snapshots, parsed_args.snapshot) @@ -302,7 +318,7 @@ class ShowSnapshot(command.ShowOne): class UnsetSnapshot(command.Command): - """Unset snapshot properties""" + _description = _("Unset snapshot properties") def get_parser(self, prog_name): parser = super(UnsetSnapshot, self).get_parser(prog_name) @@ -322,6 +338,8 @@ class UnsetSnapshot(command.Command): return parser def take_action(self, parsed_args): + LOG_DEP.warning(_('This command has been deprecated. ' + 'Please use "volume snapshot unset" instead.')) 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 e7405114..c361d700 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -45,7 +45,7 @@ def _check_size_arg(args): class CreateVolume(command.ShowOne): - """Create new volume""" + _description = _("Create new volume") def get_parser(self, prog_name): parser = super(CreateVolume, self).get_parser(prog_name) @@ -132,6 +132,28 @@ class CreateVolume(command.ShowOne): help=_("Allow volume to be attached more than once " "(default to False)") ) + bootable_group = parser.add_mutually_exclusive_group() + bootable_group.add_argument( + "--bootable", + action="store_true", + help=_("Mark volume as bootable") + ) + bootable_group.add_argument( + "--non-bootable", + action="store_true", + help=_("Mark volume as non-bootable (default)") + ) + readonly_group = parser.add_mutually_exclusive_group() + readonly_group.add_argument( + "--read-only", + action="store_true", + help=_("Set volume to read-only access mode") + ) + readonly_group.add_argument( + "--read-write", + action="store_true", + help=_("Set volume to read-write access mode (default)") + ) return parser def take_action(self, parsed_args): @@ -199,6 +221,22 @@ class CreateVolume(command.ShowOne): multiattach=parsed_args.multi_attach, scheduler_hints=parsed_args.hint, ) + + if parsed_args.bootable or parsed_args.non_bootable: + try: + volume_client.volumes.set_bootable( + volume.id, parsed_args.bootable) + except Exception as e: + LOG.error(_("Failed to set volume bootable property: %s"), e) + if parsed_args.read_only or parsed_args.read_write: + try: + volume_client.volumes.update_readonly_flag( + volume.id, + parsed_args.read_only) + except Exception as e: + LOG.error(_("Failed to set volume read-only access " + "mode flag: %s"), e) + # Remove key links from being displayed volume._info.update( { @@ -211,7 +249,7 @@ class CreateVolume(command.ShowOne): class DeleteVolume(command.Command): - """Delete volume(s)""" + _description = _("Delete volume(s)") def get_parser(self, prog_name): parser = super(DeleteVolume, self).get_parser(prog_name) @@ -263,7 +301,7 @@ class DeleteVolume(command.Command): class ListVolume(command.Lister): - """List volumes""" + _description = _("List volumes") def get_parser(self, prog_name): parser = super(ListVolume, self).get_parser(prog_name) @@ -379,16 +417,19 @@ class ListVolume(command.Lister): project_id = identity_common.find_project( identity_client, parsed_args.project, - parsed_args.project_domain) + parsed_args.project_domain).id user_id = None if parsed_args.user: user_id = identity_common.find_user(identity_client, parsed_args.user, - parsed_args.user_domain) + parsed_args.user_domain).id + + # set value of 'all_tenants' when using project option + all_projects = bool(parsed_args.project) or parsed_args.all_projects search_opts = { - 'all_tenants': parsed_args.all_projects, + 'all_tenants': all_projects, 'project_id': project_id, 'user_id': user_id, 'display_name': parsed_args.name, @@ -409,8 +450,55 @@ class ListVolume(command.Lister): ) for s in data)) +class MigrateVolume(command.Command): + _description = _("Migrate volume to a new host") + + def get_parser(self, prog_name): + parser = super(MigrateVolume, self).get_parser(prog_name) + parser.add_argument( + 'volume', + metavar="<volume>", + help=_("Volume to migrate (name or ID)") + ) + parser.add_argument( + '--host', + metavar="<host>", + required=True, + help=_("Destination host (takes the form: host@backend-name#pool)") + ) + parser.add_argument( + '--force-host-copy', + action="store_true", + help=_("Enable generic host-based force-migration, " + "which bypasses driver optimizations") + ) + lock_group = parser.add_mutually_exclusive_group() + lock_group.add_argument( + '--lock-volume', + action="store_true", + help=_("If specified, the volume state will be locked " + "and will not allow a migration to be aborted " + "(possibly by another operation)") + ) + lock_group.add_argument( + '--unlock-volume', + action="store_true", + help=_("If specified, the volume state will not be " + "locked and the a migration can be aborted " + "(default) (possibly by another operation)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + volume_client.volumes.migrate_volume(volume.id, parsed_args.host, + parsed_args.force_host_copy, + parsed_args.lock_volume,) + + class SetVolume(command.Command): - """Set volume properties""" + _description = _("Set volume properties") def get_parser(self, prog_name): parser = super(SetVolume, self).get_parser(prog_name) @@ -436,6 +524,15 @@ class SetVolume(command.Command): help=_('New volume description'), ) parser.add_argument( + "--no-property", + dest="no_property", + action="store_true", + help=_("Remove all properties from <volume> " + "(specify both --no-property and --property to " + "remove the current properties before setting " + "new properties.)"), + ) + parser.add_argument( '--property', metavar='<key=value>', action=parseractions.KeyValueAction, @@ -462,6 +559,19 @@ class SetVolume(command.Command): 'in the database with no regard to actual status, ' 'exercise caution when using)'), ) + parser.add_argument( + '--type', + metavar='<volume-type>', + help=_('New volume type (name or ID)'), + ) + parser.add_argument( + '--retype-policy', + metavar='<retype-policy>', + choices=['never', 'on-demand'], + help=_('Migration policy while re-typing volume ' + '("never" or "on-demand", default is "never" ) ' + '(available only when --type option is specified)'), + ) bootable_group = parser.add_mutually_exclusive_group() bootable_group.add_argument( "--bootable", @@ -473,6 +583,17 @@ class SetVolume(command.Command): action="store_true", help=_("Mark volume as non-bootable") ) + readonly_group = parser.add_mutually_exclusive_group() + readonly_group.add_argument( + "--read-only", + action="store_true", + help=_("Set volume to read-only access mode") + ) + readonly_group.add_argument( + "--read-write", + action="store_true", + help=_("Set volume to read-write access mode") + ) return parser def take_action(self, parsed_args): @@ -484,16 +605,25 @@ class SetVolume(command.Command): try: if volume.status != 'available': msg = (_("Volume is in %s state, it must be available " - "before size can be extended"), volume.status) + "before size can be extended") % volume.status) raise exceptions.CommandError(msg) if parsed_args.size <= volume.size: - msg = _("New size must be greater than %s GB"), volume.size + msg = (_("New size must be greater than %s GB") + % volume.size) raise exceptions.CommandError(msg) volume_client.volumes.extend(volume.id, parsed_args.size) except Exception as e: LOG.error(_("Failed to set volume size: %s"), e) result += 1 + if parsed_args.no_property: + try: + volume_client.volumes.delete_metadata( + volume.id, volume.metadata.keys()) + except Exception as e: + LOG.error(_("Failed to clean volume properties: %s"), e) + result += 1 + if parsed_args.property: try: volume_client.volumes.set_metadata( @@ -522,6 +652,37 @@ class SetVolume(command.Command): except Exception as e: LOG.error(_("Failed to set volume bootable property: %s"), e) result += 1 + if parsed_args.read_only or parsed_args.read_write: + try: + volume_client.volumes.update_readonly_flag( + volume.id, + parsed_args.read_only) + except Exception as e: + LOG.error(_("Failed to set volume read-only access " + "mode flag: %s"), e) + result += 1 + if parsed_args.type: + # get the migration policy + migration_policy = 'never' + if parsed_args.retype_policy: + migration_policy = parsed_args.retype_policy + try: + # find the volume type + volume_type = utils.find_resource( + volume_client.volume_types, + parsed_args.type) + # reset to the new volume type + volume_client.volumes.retype( + volume.id, + volume_type.id, + migration_policy) + except Exception as e: + LOG.error(_("Failed to set volume type: %s"), e) + result += 1 + elif parsed_args.retype_policy: + # If the "--retype-policy" is specified without "--type" + LOG.warning(_("'--retype-policy' option will not work " + "without '--type' option")) kwargs = {} if parsed_args.name: @@ -542,7 +703,7 @@ class SetVolume(command.Command): class ShowVolume(command.ShowOne): - """Display volume details""" + _description = _("Display volume details") def get_parser(self, prog_name): parser = super(ShowVolume, self).get_parser(prog_name) @@ -573,7 +734,7 @@ class ShowVolume(command.ShowOne): class UnsetVolume(command.Command): - """Unset volume properties""" + _description = _("Unset volume properties") def get_parser(self, prog_name): parser = super(UnsetVolume, self).get_parser(prog_name) diff --git a/openstackclient/volume/v2/volume_host.py b/openstackclient/volume/v2/volume_host.py new file mode 100644 index 00000000..2fdeb968 --- /dev/null +++ b/openstackclient/volume/v2/volume_host.py @@ -0,0 +1,75 @@ +# +# 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. +# + +"""Volume v2 host action implementations""" + +from osc_lib.command import command + +from openstackclient.i18n import _ + + +class FailoverVolumeHost(command.Command): + _description = _("Failover volume host to different backend") + + def get_parser(self, prog_name): + parser = super(FailoverVolumeHost, self).get_parser(prog_name) + parser.add_argument( + "host", + metavar="<host-name>", + help=_("Name of volume host") + ) + parser.add_argument( + "--volume-backend", + metavar="<backend-id>", + required=True, + help=_("The ID of the volume backend replication " + "target where the host will failover to (required)") + ) + return parser + + def take_action(self, parsed_args): + service_client = self.app.client_manager.volume + service_client.services.failover_host(parsed_args.host, + parsed_args.volume_backend) + + +class SetVolumeHost(command.Command): + _description = _("Set volume host properties") + + def get_parser(self, prog_name): + parser = super(SetVolumeHost, self).get_parser(prog_name) + parser.add_argument( + "host", + metavar="<host-name>", + help=_("Name of volume host") + ) + enabled_group = parser.add_mutually_exclusive_group() + enabled_group.add_argument( + "--disable", + action="store_true", + help=_("Freeze and disable the specified volume host") + ) + enabled_group.add_argument( + "--enable", + action="store_true", + help=_("Thaw and enable the specified volume host") + ) + return parser + + def take_action(self, parsed_args): + service_client = self.app.client_manager.volume + if parsed_args.enable: + service_client.services.thaw_host(parsed_args.host) + if parsed_args.disable: + service_client.services.freeze_host(parsed_args.host) diff --git a/openstackclient/volume/v2/volume_snapshot.py b/openstackclient/volume/v2/volume_snapshot.py new file mode 100644 index 00000000..f12cfed9 --- /dev/null +++ b/openstackclient/volume/v2/volume_snapshot.py @@ -0,0 +1,440 @@ +# +# 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. +# + +"""Volume v2 snapshot action implementations""" + +import copy +import logging + +from osc_lib.cli import parseractions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +LOG = logging.getLogger(__name__) + + +class CreateVolumeSnapshot(command.ShowOne): + _description = _("Create new volume snapshot") + + def get_parser(self, prog_name): + parser = super(CreateVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + "snapshot_name", + metavar="<snapshot-name>", + nargs="?", + help=_("Name of the new snapshot (default to None)") + ) + parser.add_argument( + "--volume", + metavar="<volume>", + help=_("Volume to snapshot (name or ID) " + "(default is <snapshot-name>)") + ) + parser.add_argument( + "--description", + metavar="<description>", + help=_("Description of the snapshot") + ) + parser.add_argument( + "--force", + action="store_true", + default=False, + help=_("Create a snapshot attached to an instance. " + "Default is False") + ) + parser.add_argument( + "--property", + metavar="<key=value>", + action=parseractions.KeyValueAction, + help=_("Set a property to this snapshot " + "(repeat option to set multiple properties)"), + ) + parser.add_argument( + "--remote-source", + metavar="<key=value>", + action=parseractions.KeyValueAction, + help=_("The attribute(s) of the exsiting remote volume snapshot " + "(admin required) (repeat option to specify multiple " + "attributes) e.g.: '--remote-source source-name=test_name " + "--remote-source source-id=test_id'"), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + volume = parsed_args.volume + if not parsed_args.volume: + volume = parsed_args.snapshot_name + volume_id = utils.find_resource( + volume_client.volumes, volume).id + if parsed_args.remote_source: + # Create a new snapshot from an existing remote snapshot source + if parsed_args.force: + msg = (_("'--force' option will not work when you create " + "new volume snapshot from an existing remote " + "volume snapshot")) + LOG.warning(msg) + snapshot = volume_client.volume_snapshots.manage( + volume_id=volume_id, + ref=parsed_args.remote_source, + name=parsed_args.snapshot_name, + description=parsed_args.description, + metadata=parsed_args.property, + ) + else: + # create a new snapshot from scratch + snapshot = volume_client.volume_snapshots.create( + volume_id, + force=parsed_args.force, + name=parsed_args.snapshot_name, + description=parsed_args.description, + metadata=parsed_args.property, + ) + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + return zip(*sorted(six.iteritems(snapshot._info))) + + +class DeleteVolumeSnapshot(command.Command): + _description = _("Delete volume snapshot(s)") + + def get_parser(self, prog_name): + parser = super(DeleteVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + "snapshots", + metavar="<snapshot>", + nargs="+", + help=_("Snapshot(s) to delete (name or ID)") + ) + parser.add_argument( + '--force', + action='store_true', + help=_("Attempt forced removal of snapshot(s), " + "regardless of state (defaults to False)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + result = 0 + + for i in parsed_args.snapshots: + try: + snapshot_id = utils.find_resource( + volume_client.volume_snapshots, i).id + volume_client.volume_snapshots.delete( + snapshot_id, parsed_args.force) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete snapshot with " + "name or ID '%(snapshot)s': %(e)s") + % {'snapshot': i, 'e': e}) + + if result > 0: + total = len(parsed_args.snapshots) + msg = (_("%(result)s of %(total)s snapshots failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) + + +class ListVolumeSnapshot(command.Lister): + _description = _("List volume snapshots") + + def get_parser(self, prog_name): + parser = super(ListVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=_('Include all projects (admin only)'), + ) + parser.add_argument( + '--project', + metavar='<project>', + 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'), + ) + parser.add_argument( + '--marker', + metavar='<marker>', + help=_('The last snapshot ID of the previous page'), + ) + parser.add_argument( + '--limit', + type=int, + action=parseractions.NonNegativeAction, + metavar='<limit>', + help=_('Maximum number of snapshots to display'), + ) + parser.add_argument( + '--name', + metavar='<name>', + default=None, + help=_('Filters results by a name.') + ) + parser.add_argument( + '--status', + metavar='<status>', + choices=['available', 'error', 'creating', 'deleting', + 'error-deleting'], + help=_("Filters results by a status. " + "('available', 'error', 'creating', 'deleting'" + " or 'error-deleting')") + ) + parser.add_argument( + '--volume', + metavar='<volume>', + default=None, + help=_('Filters results by a volume (name or ID).') + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + identity_client = self.app.client_manager.identity + + def _format_volume_id(volume_id): + """Return a volume name if available + + :param volume_id: a volume ID + :rtype: either the volume ID or name + """ + + volume = volume_id + if volume_id in volume_cache.keys(): + volume = volume_cache[volume_id].name + return volume + + if parsed_args.long: + columns = ['ID', 'Name', 'Description', 'Status', + 'Size', 'Created At', 'Volume ID', 'Metadata'] + column_headers = copy.deepcopy(columns) + column_headers[6] = 'Volume' + column_headers[7] = 'Properties' + else: + columns = ['ID', 'Name', 'Description', 'Status', 'Size'] + column_headers = copy.deepcopy(columns) + + # Cache the volume list + volume_cache = {} + try: + for s in volume_client.volumes.list(): + volume_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + + volume_id = None + if parsed_args.volume: + volume_id = utils.find_resource( + volume_client.volumes, parsed_args.volume).id + + project_id = None + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain).id + + # set value of 'all_tenants' when using project option + all_projects = True if parsed_args.project else \ + parsed_args.all_projects + + search_opts = { + 'all_tenants': all_projects, + 'project_id': project_id, + 'name': parsed_args.name, + 'status': parsed_args.status, + 'volume_id': volume_id, + } + + data = volume_client.volume_snapshots.list( + search_opts=search_opts, + marker=parsed_args.marker, + limit=parsed_args.limit, + ) + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters={'Metadata': utils.format_dict, + 'Volume ID': _format_volume_id}, + ) for s in data)) + + +class SetVolumeSnapshot(command.Command): + _description = _("Set volume snapshot properties") + + def get_parser(self, prog_name): + parser = super(SetVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Snapshot to modify (name or ID)') + ) + parser.add_argument( + '--name', + metavar='<name>', + help=_('New snapshot name') + ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('New snapshot description') + ) + parser.add_argument( + "--no-property", + dest="no_property", + action="store_true", + help=_("Remove all properties from <snapshot> " + "(specify both --no-property and --property to " + "remove the current properties before setting " + "new properties.)"), + ) + parser.add_argument( + '--property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + help=_('Property to add/change for this snapshot ' + '(repeat option to set multiple properties)'), + ) + parser.add_argument( + '--state', + metavar='<state>', + choices=['available', 'error', 'creating', 'deleting', + 'error-deleting'], + help=_('New snapshot state. ("available", "error", "creating", ' + '"deleting", or "error_deleting") (admin only) ' + '(This option simply changes the state of the snapshot ' + 'in the database with no regard to actual status, ' + 'exercise caution when using)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource(volume_client.volume_snapshots, + parsed_args.snapshot) + + result = 0 + if parsed_args.no_property: + try: + key_list = snapshot.metadata.keys() + volume_client.volume_snapshots.delete_metadata( + snapshot.id, + list(key_list), + ) + except Exception as e: + LOG.error(_("Failed to clean snapshot properties: %s"), e) + result += 1 + + if parsed_args.property: + try: + volume_client.volume_snapshots.set_metadata( + snapshot.id, parsed_args.property) + except Exception as e: + LOG.error(_("Failed to set snapshot property: %s"), e) + result += 1 + + if parsed_args.state: + try: + volume_client.volume_snapshots.reset_state( + snapshot.id, parsed_args.state) + except Exception as e: + LOG.error(_("Failed to set snapshot state: %s"), e) + result += 1 + + kwargs = {} + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description + if kwargs: + try: + volume_client.volume_snapshots.update( + snapshot.id, **kwargs) + except Exception as e: + LOG.error(_("Failed to update snapshot name " + "or description: %s"), e) + result += 1 + + if result > 0: + raise exceptions.CommandError(_("One or more of the " + "set operations failed")) + + +class ShowVolumeSnapshot(command.ShowOne): + _description = _("Display volume snapshot details") + + def get_parser(self, prog_name): + parser = super(ShowVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + "snapshot", + metavar="<snapshot>", + help=_("Snapshot to display (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource( + volume_client.volume_snapshots, parsed_args.snapshot) + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + return zip(*sorted(six.iteritems(snapshot._info))) + + +class UnsetVolumeSnapshot(command.Command): + _description = _("Unset volume snapshot properties") + + def get_parser(self, prog_name): + parser = super(UnsetVolumeSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='<snapshot>', + help=_('Snapshot to modify (name or ID)'), + ) + parser.add_argument( + '--property', + metavar='<key>', + action='append', + default=[], + help=_('Property to remove from snapshot ' + '(repeat option to remove multiple properties)'), + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource( + volume_client.volume_snapshots, parsed_args.snapshot) + + if parsed_args.property: + volume_client.volume_snapshots.delete_metadata( + snapshot.id, + parsed_args.property, + ) diff --git a/openstackclient/volume/v2/volume_transfer_request.py b/openstackclient/volume/v2/volume_transfer_request.py index 9008fe3c..aefe594a 100644 --- a/openstackclient/volume/v2/volume_transfer_request.py +++ b/openstackclient/volume/v2/volume_transfer_request.py @@ -28,7 +28,7 @@ LOG = logging.getLogger(__name__) class AcceptTransferRequest(command.ShowOne): - """Accept volume transfer request.""" + _description = _("Accept volume transfer request.") def get_parser(self, prog_name): parser = super(AcceptTransferRequest, self).get_parser(prog_name) @@ -56,7 +56,7 @@ class AcceptTransferRequest(command.ShowOne): class CreateTransferRequest(command.ShowOne): - """Create volume transfer request.""" + _description = _("Create volume transfer request.") def get_parser(self, prog_name): parser = super(CreateTransferRequest, self).get_parser(prog_name) @@ -85,7 +85,7 @@ class CreateTransferRequest(command.ShowOne): class DeleteTransferRequest(command.Command): - """Delete volume transfer request(s).""" + _description = _("Delete volume transfer request(s).") def get_parser(self, prog_name): parser = super(DeleteTransferRequest, self).get_parser(prog_name) @@ -120,7 +120,7 @@ class DeleteTransferRequest(command.Command): class ListTransferRequest(command.Lister): - """Lists all volume transfer requests.""" + _description = _("Lists all volume transfer requests.") def get_parser(self, prog_name): parser = super(ListTransferRequest, self).get_parser(prog_name) @@ -151,7 +151,7 @@ class ListTransferRequest(command.Lister): class ShowTransferRequest(command.ShowOne): - """Show volume transfer request details.""" + _description = _("Show volume transfer request details.") def get_parser(self, prog_name): parser = super(ShowTransferRequest, self).get_parser(prog_name) diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index 80a1f21b..8d2901f2 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -29,8 +29,46 @@ from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) +def _create_encryption_type(volume_client, volume_type, parsed_args): + if not parsed_args.encryption_provider: + msg = _("'--encryption-provider' should be specified while " + "creating a new encryption type") + raise exceptions.CommandError(msg) + # set the default of control location while creating + control_location = 'front-end' + if parsed_args.encryption_control_location: + control_location = parsed_args.encryption_control_location + body = { + 'provider': parsed_args.encryption_provider, + 'cipher': parsed_args.encryption_cipher, + 'key_size': parsed_args.encryption_key_size, + 'control_location': control_location + } + encryption = volume_client.volume_encryption_types.create( + volume_type, body) + return encryption + + +def _set_encryption_type(volume_client, volume_type, parsed_args): + # update the existing encryption type + body = {} + for attr in ['provider', 'cipher', 'key_size', 'control_location']: + info = getattr(parsed_args, 'encryption_' + attr, None) + if info is not None: + body[attr] = info + try: + volume_client.volume_encryption_types.update(volume_type, body) + except Exception as e: + if type(e).__name__ == 'NotFound': + # create new encryption type + LOG.warning(_("No existing encryption type found, creating " + "new encryption type for this volume type ...")) + _create_encryption_type( + volume_client, volume_type, parsed_args) + + class CreateVolumeType(command.ShowOne): - """Create new volume type""" + _description = _("Create new volume type") def get_parser(self, prog_name): parser = super(CreateVolumeType, self).get_parser(prog_name) @@ -70,6 +108,42 @@ class CreateVolumeType(command.ShowOne): help=_("Allow <project> to access private type (name or ID) " "(Must be used with --private option)"), ) + # TODO(Huanxuan Ao): Add choices for each "--encryption-*" option. + parser.add_argument( + '--encryption-provider', + metavar='<provider>', + help=_('Set the class that provides encryption support for ' + 'this volume type (e.g "LuksEncryptor") (admin only) ' + '(This option is required when setting encryption type ' + 'of a volume. Consider using other encryption options ' + 'such as: "--encryption-cipher", "--encryption-key-size" ' + 'and "--encryption-control-location")'), + ) + parser.add_argument( + '--encryption-cipher', + metavar='<cipher>', + help=_('Set the encryption algorithm or mode for this ' + 'volume type (e.g "aes-xts-plain64") (admin only)'), + ) + parser.add_argument( + '--encryption-key-size', + metavar='<key-size>', + type=int, + help=_('Set the size of the encryption key of this ' + 'volume type (e.g "128" or "256") (admin only)'), + ) + parser.add_argument( + '--encryption-control-location', + metavar='<control-location>', + choices=['front-end', 'back-end'], + help=_('Set the notional service where the encryption is ' + 'performed ("front-end" or "back-end") (admin only) ' + '(The default value for this option is "front-end" ' + 'when setting encryption type of a volume. Consider ' + 'using other encryption options such as: ' + '"--encryption-cipher", "--encryption-key-size" and ' + '"--encryption-provider")'), + ) identity_common.add_project_domain_option_to_parser(parser) return parser @@ -110,13 +184,28 @@ class CreateVolumeType(command.ShowOne): if parsed_args.property: result = volume_type.set_keys(parsed_args.property) volume_type._info.update({'properties': utils.format_dict(result)}) + if (parsed_args.encryption_provider or + parsed_args.encryption_cipher or + parsed_args.encryption_key_size or + parsed_args.encryption_control_location): + try: + # create new encryption + encryption = _create_encryption_type( + volume_client, volume_type, parsed_args) + except Exception as e: + LOG.error(_("Failed to set encryption information for this " + "volume type: %s"), e) + # add encryption info in result + encryption._info.pop("volume_type_id", None) + volume_type._info.update( + {'encryption': utils.format_dict(encryption._info)}) volume_type._info.pop("os-volume-type-access:is_public", None) return zip(*sorted(six.iteritems(volume_type._info))) class DeleteVolumeType(command.Command): - """Delete volume type(s)""" + _description = _("Delete volume type(s)") def get_parser(self, prog_name): parser = super(DeleteVolumeType, self).get_parser(prog_name) @@ -152,7 +241,7 @@ class DeleteVolumeType(command.Command): class ListVolumeType(command.Lister): - """List volume types""" + _description = _("List volume types") def get_parser(self, prog_name): parser = super(ListVolumeType, self).get_parser(prog_name) @@ -160,9 +249,16 @@ class ListVolumeType(command.Lister): '--long', action='store_true', default=False, - help=_('List additional fields in output')) + help=_('List additional fields in output') + ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( + "--default", + action='store_true', + default=False, + help=_('List the default volume type') + ) + public_group.add_argument( "--public", action="store_true", help=_("List only public types") @@ -172,32 +268,73 @@ class ListVolumeType(command.Lister): action="store_true", help=_("List only private types (admin only)") ) + parser.add_argument( + "--encryption-type", + action="store_true", + help=_("Display encryption information for each volume type " + "(admin only)"), + ) return parser def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume if parsed_args.long: - columns = ['ID', 'Name', 'Description', 'Extra Specs'] - column_headers = ['ID', 'Name', 'Description', 'Properties'] + columns = ['ID', 'Name', 'Is Public', 'Description', 'Extra Specs'] + column_headers = [ + 'ID', 'Name', 'Is Public', 'Description', 'Properties'] + else: + columns = ['ID', 'Name', 'Is Public'] + column_headers = ['ID', 'Name', 'Is Public'] + if parsed_args.default: + data = [volume_client.volume_types.default()] else: - columns = ['ID', 'Name'] - column_headers = columns + is_public = None + if parsed_args.public: + is_public = True + if parsed_args.private: + is_public = False + data = volume_client.volume_types.list( + is_public=is_public) + + def _format_encryption_info(type_id, encryption_data=None): + encryption_data = encryption + encryption_info = '-' + if type_id in encryption_data.keys(): + encryption_info = encryption_data[type_id] + return encryption_info + + if parsed_args.encryption_type: + encryption = {} + for d in volume_client.volume_encryption_types.list(): + volume_type_id = d._info['volume_type_id'] + # remove some redundant information + del_key = [ + 'deleted', + 'created_at', + 'updated_at', + 'deleted_at', + 'volume_type_id' + ] + for key in del_key: + d._info.pop(key, None) + # save the encryption information with their volume type ID + encryption[volume_type_id] = utils.format_dict(d._info) + # We need to get volume type ID, then show encryption + # information according to the ID, so use "id" to keep + # difference to the real "ID" column. + columns += ['id'] + column_headers += ['Encryption'] - is_public = None - if parsed_args.public: - is_public = True - if parsed_args.private: - is_public = False - data = self.app.client_manager.volume.volume_types.list( - is_public=is_public) return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Extra Specs': utils.format_dict}, + formatters={'Extra Specs': utils.format_dict, + 'id': _format_encryption_info}, ) for s in data)) class SetVolumeType(command.Command): - """Set volume type properties""" + _description = _("Set volume type properties") def get_parser(self, prog_name): parser = super(SetVolumeType, self).get_parser(prog_name) @@ -213,7 +350,7 @@ class SetVolumeType(command.Command): ) parser.add_argument( '--description', - metavar='<name>', + metavar='<description>', help=_('Set volume type description'), ) parser.add_argument( @@ -230,7 +367,43 @@ class SetVolumeType(command.Command): '(admin only)'), ) identity_common.add_project_domain_option_to_parser(parser) - + # TODO(Huanxuan Ao): Add choices for each "--encryption-*" option. + parser.add_argument( + '--encryption-provider', + metavar='<provider>', + help=_('Set the class that provides encryption support for ' + 'this volume type (e.g "LuksEncryptor") (admin only) ' + '(This option is required when setting encryption type ' + 'of a volume for the first time. Consider using other ' + 'encryption options such as: "--encryption-cipher", ' + '"--encryption-key-size" and ' + '"--encryption-control-location")'), + ) + parser.add_argument( + '--encryption-cipher', + metavar='<cipher>', + help=_('Set the encryption algorithm or mode for this ' + 'volume type (e.g "aes-xts-plain64") (admin only)'), + ) + parser.add_argument( + '--encryption-key-size', + metavar='<key-size>', + type=int, + help=_('Set the size of the encryption key of this ' + 'volume type (e.g "128" or "256") (admin only)'), + ) + parser.add_argument( + '--encryption-control-location', + metavar='<control-location>', + choices=['front-end', 'back-end'], + help=_('Set the notional service where the encryption is ' + 'performed ("front-end" or "back-end") (admin only) ' + '(The default value for this option is "front-end" ' + 'when setting encryption type of a volume for the ' + 'first time. Consider using other encryption options ' + 'such as: "--encryption-cipher", "--encryption-key-size" ' + 'and "--encryption-provider")'), + ) return parser def take_action(self, parsed_args): @@ -239,7 +412,6 @@ class SetVolumeType(command.Command): volume_type = utils.find_resource( volume_client.volume_types, parsed_args.volume_type) - result = 0 kwargs = {} if parsed_args.name: @@ -280,13 +452,24 @@ class SetVolumeType(command.Command): "project: %s"), e) result += 1 + if (parsed_args.encryption_provider or + parsed_args.encryption_cipher or + parsed_args.encryption_key_size or + parsed_args.encryption_control_location): + try: + _set_encryption_type(volume_client, volume_type, parsed_args) + except Exception as e: + LOG.error(_("Failed to set encryption information for this " + "volume type: %s"), e) + result += 1 + if result > 0: raise exceptions.CommandError(_("Command Failed: One or more of" " the operations failed")) class ShowVolumeType(command.ShowOne): - """Display volume type details""" + _description = _("Display volume type details") def get_parser(self, prog_name): parser = super(ShowVolumeType, self).get_parser(prog_name) @@ -295,6 +478,12 @@ class ShowVolumeType(command.ShowOne): metavar="<volume-type>", help=_("Volume type to display (name or ID)") ) + parser.add_argument( + "--encryption-type", + action="store_true", + help=_("Display encryption information of this volume type " + "(admin only)"), + ) return parser def take_action(self, parsed_args): @@ -319,12 +508,23 @@ class ShowVolumeType(command.ShowOne): '%(type)s: %(e)s') LOG.error(msg % {'type': volume_type.id, 'e': e}) volume_type._info.update({'access_project_ids': access_project_ids}) + if parsed_args.encryption_type: + # show encryption type information for this volume type + try: + encryption = volume_client.volume_encryption_types.get( + volume_type.id) + encryption._info.pop("volume_type_id", None) + volume_type._info.update( + {'encryption': utils.format_dict(encryption._info)}) + except Exception as e: + LOG.error(_("Failed to display the encryption information " + "of this volume type: %s"), e) volume_type._info.pop("os-volume-type-access:is_public", None) return zip(*sorted(six.iteritems(volume_type._info))) class UnsetVolumeType(command.Command): - """Unset volume type properties""" + _description = _("Unset volume type properties") def get_parser(self, prog_name): parser = super(UnsetVolumeType, self).get_parser(prog_name) @@ -347,7 +547,12 @@ class UnsetVolumeType(command.Command): ' (admin only)'), ) identity_common.add_project_domain_option_to_parser(parser) - + parser.add_argument( + "--encryption-type", + action="store_true", + help=_("Remove the encryption type for this volume type " + "(admin only)"), + ) return parser def take_action(self, parsed_args): @@ -381,6 +586,13 @@ class UnsetVolumeType(command.Command): LOG.error(_("Failed to remove volume type access from " "project: %s"), e) result += 1 + if parsed_args.encryption_type: + try: + volume_client.volume_encryption_types.delete(volume_type) + except Exception as e: + LOG.error(_("Failed to remove the encryption type for this " + "volume type: %s"), e) + result += 1 if result > 0: raise exceptions.CommandError(_("Command Failed: One or more of" diff --git a/openstackclient/volume/v3/__init__.py b/openstackclient/volume/v3/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/openstackclient/volume/v3/__init__.py |
