diff options
Diffstat (limited to 'openstackclient')
27 files changed, 1998 insertions, 108 deletions
diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index 820b4ecf..66272e42 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -185,7 +185,7 @@ def build_auth_plugins_option_parser(parser): metavar='<auth-type>', dest='auth_type', default=utils.env('OS_AUTH_TYPE'), - help='Select an auhentication type. Available types: ' + + help='Select an authentication type. Available types: ' + ', '.join(available_plugins) + '. Default: selected based on --os-username/--os-token' + ' (Env: OS_AUTH_TYPE)', diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index d8e7e1a9..55c6fe53 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -49,6 +49,10 @@ class ClientCache(object): class ClientManager(object): """Manages access to API clients, including authentication.""" + + # A simple incrementing version for the plugin to know what is available + PLUGIN_INTERFACE_VERSION = "2" + identity = ClientCache(identity_client.make_client) def __getattr__(self, name): diff --git a/openstackclient/common/limits.py b/openstackclient/common/limits.py index 4abcf169..c738f150 100644 --- a/openstackclient/common/limits.py +++ b/openstackclient/common/limits.py @@ -31,7 +31,7 @@ class ShowLimits(lister.Lister): def get_parser(self, prog_name): parser = super(ShowLimits, self).get_parser(prog_name) - type_group = parser.add_mutually_exclusive_group() + type_group = parser.add_mutually_exclusive_group(required=True) type_group.add_argument( "--absolute", dest="is_absolute", diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index a40f6e4d..e79fd7ed 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -97,12 +97,13 @@ class SetQuota(command.Command): compute_kwargs = {} for k, v in COMPUTE_QUOTAS.items(): - value = getattr(parsed_args, v, None) + value = getattr(parsed_args, k, None) if value is not None: compute_kwargs[k] = value volume_kwargs = {} for k, v in VOLUME_QUOTAS.items(): + # TODO(jiaxi): Should use k or v needs discuss value = getattr(parsed_args, v, None) if value is not None: if parsed_args.volume_type: @@ -222,8 +223,7 @@ class ShowQuota(show.ShowOne): info.pop(k) # Handle project ID special as it only appears in output - if info['id']: - info['project'] = info['id'] - info.pop('id') + if 'id' in info: + info['project'] = info.pop('id') return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 6ae87b79..dd40b9a9 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -15,17 +15,21 @@ import logging +from openstackclient.common import exceptions from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_COMPUTE_API_VERSION = '2' +DEFAULT_API_VERSION = '2' API_VERSION_OPTION = 'os_compute_api_version' API_NAME = 'compute' API_VERSIONS = { "2": "novaclient.client", } +# Save the microversion if in use +_compute_api_version = None + def make_client(instance): """Returns a compute service client.""" @@ -51,6 +55,9 @@ def make_client(instance): # Remember interface only if it is set kwargs = utils.build_kwargs_dict('endpoint_type', instance._interface) + if _compute_api_version is not None: + kwargs.update({'api_version': _compute_api_version}) + client = compute_client( session=instance.session, extensions=extensions, @@ -68,10 +75,50 @@ def build_option_parser(parser): parser.add_argument( '--os-compute-api-version', metavar='<compute-api-version>', - default=utils.env( - 'OS_COMPUTE_API_VERSION', - default=DEFAULT_COMPUTE_API_VERSION), + default=utils.env('OS_COMPUTE_API_VERSION'), help='Compute API version, default=' + - DEFAULT_COMPUTE_API_VERSION + + DEFAULT_API_VERSION + ' (Env: OS_COMPUTE_API_VERSION)') return parser + + +def check_api_version(check_version): + """Validate version supplied by user + + Returns: + * True if version is OK + * False if the version has not been checked and the previous plugin + check should be performed + * throws an exception if the version is no good + + TODO(dtroyer): make the exception thrown a version-related one + """ + + # Defer client imports until we actually need them + try: + from novaclient import api_versions + except ImportError: + # Retain previous behaviour + return False + + import novaclient + + global _compute_api_version + + # Copy some logic from novaclient 2.27.0 for basic version detection + # NOTE(dtroyer): This is only enough to resume operations using API + # version 2.0 or any valid version supplied by the user. + _compute_api_version = api_versions.get_api_version(check_version) + + if _compute_api_version > api_versions.APIVersion("2.0"): + if not _compute_api_version.matches( + novaclient.API_MIN_VERSION, + novaclient.API_MAX_VERSION, + ): + raise exceptions.CommandError( + "versions supported by client: %s - %s" % ( + novaclient.API_MIN_VERSION.get_string(), + novaclient.API_MAX_VERSION.get_string(), + ), + ) + return True diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py index 3dc9bae0..25c2ed3f 100644 --- a/openstackclient/compute/v2/security_group.py +++ b/openstackclient/compute/v2/security_group.py @@ -50,10 +50,10 @@ def _xform_security_group_rule(sgroup): info['ip_range'] = info['ip_range']['cidr'] else: info['ip_range'] = '' - if info['ip_protocol'] == 'icmp': - info['port_range'] = '' - elif info['ip_protocol'] is None: + if info['ip_protocol'] is None: info['ip_protocol'] = '' + elif info['ip_protocol'].lower() == 'icmp': + info['port_range'] = '' return info @@ -307,7 +307,10 @@ class CreateSecurityGroupRule(show.ShowOne): compute_client.security_groups, parsed_args.group, ) - from_port, to_port = parsed_args.dst_port + if parsed_args.proto.lower() == 'icmp': + from_port, to_port = -1, -1 + else: + from_port, to_port = parsed_args.dst_port data = compute_client.security_group_rules.create( group.id, parsed_args.proto, diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 4efef975..6d837d9c 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -107,14 +107,20 @@ def _prep_server_detail(compute_client, server): image_info = info.get('image', {}) if image_info: image_id = image_info.get('id', '') - image = utils.find_resource(compute_client.images, image_id) - info['image'] = "%s (%s)" % (image.name, image_id) + try: + image = utils.find_resource(compute_client.images, image_id) + info['image'] = "%s (%s)" % (image.name, image_id) + except Exception: + info['image'] = image_id # Convert the flavor blob to a name flavor_info = info.get('flavor', {}) flavor_id = flavor_info.get('id', '') - flavor = utils.find_resource(compute_client.flavors, flavor_id) - info['flavor'] = "%s (%s)" % (flavor.name, flavor_id) + try: + flavor = utils.find_resource(compute_client.flavors, flavor_id) + info['flavor'] = "%s (%s)" % (flavor.name, flavor_id) + except Exception: + info['flavor'] = flavor_id # NOTE(dtroyer): novaclient splits these into separate entries... # Format addresses in a useful way diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index d7b663dd..b8bb33f4 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -21,7 +21,7 @@ from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_IDENTITY_API_VERSION = '2' +DEFAULT_API_VERSION = '2' API_VERSION_OPTION = 'os_identity_api_version' API_NAME = 'identity' API_VERSIONS = { @@ -63,11 +63,9 @@ def build_option_parser(parser): parser.add_argument( '--os-identity-api-version', metavar='<identity-api-version>', - default=utils.env( - 'OS_IDENTITY_API_VERSION', - default=DEFAULT_IDENTITY_API_VERSION), + default=utils.env('OS_IDENTITY_API_VERSION'), help='Identity API version, default=' + - DEFAULT_IDENTITY_API_VERSION + + DEFAULT_API_VERSION + ' (Env: OS_IDENTITY_API_VERSION)') return auth.build_auth_plugins_option_parser(parser) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 199b7dca..9243639e 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -101,9 +101,9 @@ def _process_identity_and_resource_options(parsed_args, kwargs['project'] = common.find_project( identity_client_manager, parsed_args.project, - parsed_args.group_domain, + parsed_args.project_domain, ).id - kwargs['inherited'] = parsed_args.inherited + kwargs['os_inherit_extension_inherited'] = parsed_args.inherited return kwargs diff --git a/openstackclient/identity/v3/role_assignment.py b/openstackclient/identity/v3/role_assignment.py index 24e3a7f7..169c6cb9 100644 --- a/openstackclient/identity/v3/role_assignment.py +++ b/openstackclient/identity/v3/role_assignment.py @@ -45,11 +45,13 @@ class ListRoleAssignment(lister.Lister): metavar='<user>', help='User to filter (name or ID)', ) + common.add_user_domain_option_to_parser(parser) user_or_group.add_argument( '--group', metavar='<group>', help='Group to filter (name or ID)', ) + common.add_group_domain_option_to_parser(parser) domain_or_project = parser.add_mutually_exclusive_group() domain_or_project.add_argument( '--domain', @@ -61,12 +63,13 @@ class ListRoleAssignment(lister.Lister): metavar='<project>', help='Project to filter (name or ID)', ) - + common.add_project_domain_option_to_parser(parser) + common.add_inherited_option_to_parser(parser) return parser def _as_tuple(self, assignment): return (assignment.role, assignment.user, assignment.group, - assignment.project, assignment.domain) + assignment.project, assignment.domain, assignment.inherited) def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) @@ -84,6 +87,7 @@ class ListRoleAssignment(lister.Lister): user = common.find_user( identity_client, parsed_args.user, + parsed_args.user_domain, ) domain = None @@ -98,6 +102,7 @@ class ListRoleAssignment(lister.Lister): project = common.find_project( identity_client, parsed_args.project, + parsed_args.project_domain, ) group = None @@ -105,18 +110,22 @@ class ListRoleAssignment(lister.Lister): group = common.find_group( identity_client, parsed_args.group, + parsed_args.group_domain, ) effective = True if parsed_args.effective else False self.log.debug('take_action(%s)' % parsed_args) - columns = ('Role', 'User', 'Group', 'Project', 'Domain') + columns = ('Role', 'User', 'Group', 'Project', 'Domain', 'Inherited') + + inherited_to = 'projects' if parsed_args.inherited else None data = identity_client.role_assignments.list( domain=domain, user=user, group=group, project=project, role=role, - effective=effective) + effective=effective, + os_inherit_extension_inherited_to=inherited_to) data_parsed = [] for assignment in data: @@ -133,6 +142,9 @@ class ListRoleAssignment(lister.Lister): assignment.domain = '' assignment.project = '' + inherited = scope.get('OS-INHERIT:inherited_to') == 'projects' + assignment.inherited = inherited + del assignment.scope if hasattr(assignment, 'user'): diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index 8fbf8c0f..8dd146e9 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -20,7 +20,7 @@ from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_IMAGE_API_VERSION = '1' +DEFAULT_API_VERSION = '1' API_VERSION_OPTION = 'os_image_api_version' API_NAME = "image" API_VERSIONS = { @@ -81,10 +81,8 @@ def build_option_parser(parser): parser.add_argument( '--os-image-api-version', metavar='<image-api-version>', - default=utils.env( - 'OS_IMAGE_API_VERSION', - default=DEFAULT_IMAGE_API_VERSION), + default=utils.env('OS_IMAGE_API_VERSION'), help='Image API version, default=' + - DEFAULT_IMAGE_API_VERSION + + DEFAULT_API_VERSION + ' (Env: OS_IMAGE_API_VERSION)') return parser diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index 0ef68852..5f72782b 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -18,7 +18,7 @@ from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_NETWORK_API_VERSION = '2' +DEFAULT_API_VERSION = '2' API_VERSION_OPTION = 'os_network_api_version' API_NAME = "network" API_VERSIONS = { @@ -83,10 +83,8 @@ def build_option_parser(parser): parser.add_argument( '--os-network-api-version', metavar='<network-api-version>', - default=utils.env( - 'OS_NETWORK_API_VERSION', - default=DEFAULT_NETWORK_API_VERSION), + default=utils.env('OS_NETWORK_API_VERSION'), help='Network API version, default=' + - DEFAULT_NETWORK_API_VERSION + + DEFAULT_API_VERSION + ' (Env: OS_NETWORK_API_VERSION)') return parser diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index 0359940d..e7587802 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -22,7 +22,7 @@ from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_OBJECT_API_VERSION = '1' +DEFAULT_API_VERSION = '1' API_VERSION_OPTION = 'os_object_api_version' API_NAME = 'object_store' API_VERSIONS = { @@ -52,10 +52,8 @@ def build_option_parser(parser): parser.add_argument( '--os-object-api-version', metavar='<object-api-version>', - default=utils.env( - 'OS_OBJECT_API_VERSION', - default=DEFAULT_OBJECT_API_VERSION), + default=utils.env('OS_OBJECT_API_VERSION'), help='Object API version, default=' + - DEFAULT_OBJECT_API_VERSION + + DEFAULT_API_VERSION + ' (Env: OS_OBJECT_API_VERSION)') return parser diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 6ba19d19..0623d82d 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -317,15 +317,29 @@ class OpenStackShell(app.App): # Loop through extensions to get API versions for mod in clientmanager.PLUGIN_MODULES: - version_opt = getattr(self.options, mod.API_VERSION_OPTION, None) + default_version = getattr(mod, 'DEFAULT_API_VERSION', None) + option = mod.API_VERSION_OPTION.replace('os_', '') + version_opt = self.cloud.config.get(option, default_version) if version_opt: api = mod.API_NAME self.api_version[api] = version_opt - if version_opt not in mod.API_VERSIONS: - self.log.warning( - "The %s version <%s> is not in supported versions <%s>" - % (api, version_opt, - ', '.join(mod.API_VERSIONS.keys()))) + + # Add a plugin interface to let the module validate the version + # requested by the user + skip_old_check = False + mod_check_api_version = getattr(mod, 'check_api_version', None) + if mod_check_api_version: + # this throws an exception if invalid + skip_old_check = mod_check_api_version(version_opt) + + mod_versions = getattr(mod, 'API_VERSIONS', None) + if not skip_old_check and mod_versions: + if version_opt not in mod_versions: + self.log.warning( + "%s version %s is not in supported versions %s" + % (api, version_opt, + ', '.join(mod.API_VERSIONS.keys()))) + # Command groups deal only with major versions version = '.v' + version_opt.replace('.', '_').split('_')[0] cmd_group = 'openstack.' + api.replace('-', '_') + version diff --git a/openstackclient/tests/common/test_quota.py b/openstackclient/tests/common/test_quota.py new file mode 100644 index 00000000..f0013e48 --- /dev/null +++ b/openstackclient/tests/common/test_quota.py @@ -0,0 +1,89 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from openstackclient.common import quota +from openstackclient.tests.compute.v2 import fakes as compute_fakes +from openstackclient.tests import fakes + + +class FakeQuotaResource(fakes.FakeResource): + + _keys = {'property': 'value'} + + def set_keys(self, args): + self._keys.update(args) + + def unset_keys(self, keys): + for key in keys: + self._keys.pop(key, None) + + def get_keys(self): + return self._keys + + +class TestQuota(compute_fakes.TestComputev2): + + def setUp(self): + super(TestQuota, self).setUp() + self.quotas_mock = self.app.client_manager.compute.quotas + self.quotas_mock.reset_mock() + + +class TestQuotaSet(TestQuota): + + def setUp(self): + super(TestQuotaSet, self).setUp() + + self.quotas_mock.find.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.quotas_mock.update.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.cmd = quota.SetQuota(self.app, None) + + def test_quota_set(self): + arglist = [ + '--floating-ips', str(compute_fakes.floating_ip_num), + '--fixed-ips', str(compute_fakes.fix_ip_num), + '--injected-files', str(compute_fakes.injected_file_num), + '--key-pairs', str(compute_fakes.key_pair_num), + compute_fakes.project_name, + ] + verifylist = [ + ('floating_ips', compute_fakes.floating_ip_num), + ('fixed_ips', compute_fakes.fix_ip_num), + ('injected_files', compute_fakes.injected_file_num), + ('key_pairs', compute_fakes.key_pair_num), + ('project', compute_fakes.project_name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + kwargs = { + 'floating_ips': compute_fakes.floating_ip_num, + 'fixed_ips': compute_fakes.fix_ip_num, + 'injected_files': compute_fakes.injected_file_num, + 'key_pairs': compute_fakes.key_pair_num, + } + + self.quotas_mock.update.assert_called_with('project_test', **kwargs) diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index c18dea7e..e798bd40 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -62,6 +62,22 @@ FLAVOR = { 'vcpus': flavor_vcpus, } +floating_ip_num = 100 +fix_ip_num = 100 +injected_file_num = 100 +key_pair_num = 100 +project_name = 'project_test' +QUOTA = { + 'project': project_name, + 'floating-ips': floating_ip_num, + 'fix-ips': fix_ip_num, + 'injected-files': injected_file_num, + 'key-pairs': key_pair_num, +} + +QUOTA_columns = tuple(sorted(QUOTA)) +QUOTA_data = tuple(QUOTA[x] for x in sorted(QUOTA)) + class FakeComputev2Client(object): def __init__(self, **kwargs): @@ -73,6 +89,8 @@ class FakeComputev2Client(object): self.extensions.resource_class = fakes.FakeResource(None, {}) self.flavors = mock.Mock() self.flavors.resource_class = fakes.FakeResource(None, {}) + self.quotas = mock.Mock() + self.quotas.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] diff --git a/openstackclient/tests/compute/v2/test_security_group_rule.py b/openstackclient/tests/compute/v2/test_security_group_rule.py new file mode 100644 index 00000000..9516f8dd --- /dev/null +++ b/openstackclient/tests/compute/v2/test_security_group_rule.py @@ -0,0 +1,338 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy +import mock + +from openstackclient.compute.v2 import security_group +from openstackclient.tests.compute.v2 import fakes as compute_fakes +from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes + + +security_group_id = '11' +security_group_name = 'wide-open' +security_group_description = 'nothing but net' + +security_group_rule_id = '1' + +SECURITY_GROUP = { + 'id': security_group_id, + 'name': security_group_name, + 'description': security_group_description, + 'tenant_id': identity_fakes.project_id, +} + +SECURITY_GROUP_RULE = { + 'id': security_group_rule_id, + 'group': {}, + 'ip_protocol': 'tcp', + 'ip_range': '0.0.0.0/0', + 'parent_group_id': security_group_id, + 'from_port': 0, + 'to_port': 0, +} + +SECURITY_GROUP_RULE_ICMP = { + 'id': security_group_rule_id, + 'group': {}, + 'ip_protocol': 'icmp', + 'ip_range': '0.0.0.0/0', + 'parent_group_id': security_group_id, + 'from_port': -1, + 'to_port': -1, +} + + +class FakeSecurityGroupRuleResource(fakes.FakeResource): + + def get_keys(self): + return {'property': 'value'} + + +class TestSecurityGroupRule(compute_fakes.TestComputev2): + + def setUp(self): + super(TestSecurityGroupRule, self).setUp() + + self.secgroups_mock = mock.Mock() + self.secgroups_mock.resource_class = fakes.FakeResource(None, {}) + self.app.client_manager.compute.security_groups = self.secgroups_mock + self.secgroups_mock.reset_mock() + + self.sg_rules_mock = mock.Mock() + self.sg_rules_mock.resource_class = fakes.FakeResource(None, {}) + self.app.client_manager.compute.security_group_rules = \ + self.sg_rules_mock + self.sg_rules_mock.reset_mock() + + +class TestSecurityGroupRuleCreate(TestSecurityGroupRule): + + def setUp(self): + super(TestSecurityGroupRuleCreate, self).setUp() + + self.secgroups_mock.get.return_value = FakeSecurityGroupRuleResource( + None, + copy.deepcopy(SECURITY_GROUP), + loaded=True, + ) + + # Get the command object to test + self.cmd = security_group.CreateSecurityGroupRule(self.app, None) + + def test_security_group_rule_create_no_options(self): + self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( + None, + copy.deepcopy(SECURITY_GROUP_RULE), + loaded=True, + ) + + arglist = [ + security_group_name, + ] + verifylist = [ + ('group', security_group_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # SecurityGroupManager.create(name, description) + self.sg_rules_mock.create.assert_called_with( + security_group_id, + 'tcp', + 0, + 0, + '0.0.0.0/0', + ) + + collist = ( + 'group', + 'id', + 'ip_protocol', + 'ip_range', + 'parent_group_id', + 'port_range', + ) + self.assertEqual(collist, columns) + datalist = ( + {}, + security_group_rule_id, + 'tcp', + '', + security_group_id, + '0:0', + ) + self.assertEqual(datalist, data) + + def test_security_group_rule_create_ftp(self): + sg_rule = copy.deepcopy(SECURITY_GROUP_RULE) + sg_rule['from_port'] = 20 + sg_rule['to_port'] = 21 + self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( + None, + sg_rule, + loaded=True, + ) + + arglist = [ + security_group_name, + '--dst-port', '20:21', + ] + verifylist = [ + ('group', security_group_name), + ('dst_port', (20, 21)), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # SecurityGroupManager.create(name, description) + self.sg_rules_mock.create.assert_called_with( + security_group_id, + 'tcp', + 20, + 21, + '0.0.0.0/0', + ) + + collist = ( + 'group', + 'id', + 'ip_protocol', + 'ip_range', + 'parent_group_id', + 'port_range', + ) + self.assertEqual(collist, columns) + datalist = ( + {}, + security_group_rule_id, + 'tcp', + '', + security_group_id, + '20:21', + ) + self.assertEqual(datalist, data) + + def test_security_group_rule_create_ssh(self): + sg_rule = copy.deepcopy(SECURITY_GROUP_RULE) + sg_rule['from_port'] = 22 + sg_rule['to_port'] = 22 + self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( + None, + sg_rule, + loaded=True, + ) + + arglist = [ + security_group_name, + '--dst-port', '22', + ] + verifylist = [ + ('group', security_group_name), + ('dst_port', (22, 22)), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # SecurityGroupManager.create(name, description) + self.sg_rules_mock.create.assert_called_with( + security_group_id, + 'tcp', + 22, + 22, + '0.0.0.0/0', + ) + + collist = ( + 'group', + 'id', + 'ip_protocol', + 'ip_range', + 'parent_group_id', + 'port_range', + ) + self.assertEqual(collist, columns) + datalist = ( + {}, + security_group_rule_id, + 'tcp', + '', + security_group_id, + '22:22', + ) + self.assertEqual(datalist, data) + + def test_security_group_rule_create_udp(self): + sg_rule = copy.deepcopy(SECURITY_GROUP_RULE) + sg_rule['ip_protocol'] = 'udp' + self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( + None, + sg_rule, + loaded=True, + ) + + arglist = [ + security_group_name, + '--proto', 'udp', + ] + verifylist = [ + ('group', security_group_name), + ('proto', 'udp'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # SecurityGroupManager.create(name, description) + self.sg_rules_mock.create.assert_called_with( + security_group_id, + 'udp', + 0, + 0, + '0.0.0.0/0', + ) + + collist = ( + 'group', + 'id', + 'ip_protocol', + 'ip_range', + 'parent_group_id', + 'port_range', + ) + self.assertEqual(collist, columns) + datalist = ( + {}, + security_group_rule_id, + 'udp', + '', + security_group_id, + '0:0', + ) + self.assertEqual(datalist, data) + + def test_security_group_rule_create_icmp(self): + self.sg_rules_mock.create.return_value = FakeSecurityGroupRuleResource( + None, + copy.deepcopy(SECURITY_GROUP_RULE_ICMP), + loaded=True, + ) + + arglist = [ + security_group_name, + '--proto', 'ICMP', + ] + verifylist = [ + ('group', security_group_name), + ('proto', 'ICMP'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # SecurityGroupManager.create(name, description) + self.sg_rules_mock.create.assert_called_with( + security_group_id, + 'ICMP', + -1, + -1, + '0.0.0.0/0', + ) + + collist = ( + 'group', + 'id', + 'ip_protocol', + 'ip_range', + 'parent_group_id', + 'port_range', + ) + self.assertEqual(collist, columns) + datalist = ( + {}, + security_group_rule_id, + 'icmp', + '', + security_group_id, + '', + ) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index ae7a684c..9c4de9cc 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -313,6 +313,13 @@ ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID = { 'role': {'id': role_id}, } +ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID_INHERITED = { + 'scope': {'project': {'id': project_id}, + 'OS-INHERIT:inherited_to': 'projects'}, + 'user': {'id': user_id}, + 'role': {'id': role_id}, +} + ASSIGNMENT_WITH_PROJECT_ID_AND_GROUP_ID = { 'scope': {'project': {'id': project_id}}, 'group': {'id': group_id}, @@ -325,6 +332,13 @@ ASSIGNMENT_WITH_DOMAIN_ID_AND_USER_ID = { 'role': {'id': role_id}, } +ASSIGNMENT_WITH_DOMAIN_ID_AND_USER_ID_INHERITED = { + 'scope': {'domain': {'id': domain_id}, + 'OS-INHERIT:inherited_to': 'projects'}, + 'user': {'id': user_id}, + 'role': {'id': role_id}, +} + ASSIGNMENT_WITH_DOMAIN_ID_AND_GROUP_ID = { 'scope': {'domain': {'id': domain_id}}, 'group': {'id': group_id}, diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py index 4ff3b95f..4a0ba066 100644 --- a/openstackclient/tests/identity/v3/test_role.py +++ b/openstackclient/tests/identity/v3/test_role.py @@ -123,7 +123,7 @@ class TestRoleAdd(TestRole): kwargs = { 'user': identity_fakes.user_id, 'domain': identity_fakes.domain_id, - 'inherited': self._is_inheritance_testcase(), + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), } # RoleManager.grant(role, user=, group=, domain=, project=) self.roles_mock.grant.assert_called_with( @@ -156,7 +156,7 @@ class TestRoleAdd(TestRole): kwargs = { 'user': identity_fakes.user_id, 'project': identity_fakes.project_id, - 'inherited': self._is_inheritance_testcase(), + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), } # RoleManager.grant(role, user=, group=, domain=, project=) self.roles_mock.grant.assert_called_with( @@ -189,7 +189,7 @@ class TestRoleAdd(TestRole): kwargs = { 'group': identity_fakes.group_id, 'domain': identity_fakes.domain_id, - 'inherited': self._is_inheritance_testcase(), + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), } # RoleManager.grant(role, user=, group=, domain=, project=) self.roles_mock.grant.assert_called_with( @@ -222,7 +222,7 @@ class TestRoleAdd(TestRole): kwargs = { 'group': identity_fakes.group_id, 'project': identity_fakes.project_id, - 'inherited': self._is_inheritance_testcase(), + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), } # RoleManager.grant(role, user=, group=, domain=, project=) self.roles_mock.grant.assert_called_with( @@ -598,7 +598,7 @@ class TestRoleRemove(TestRole): kwargs = { 'user': identity_fakes.user_id, 'domain': identity_fakes.domain_id, - 'inherited': self._is_inheritance_testcase(), + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), } # RoleManager.revoke(role, user=, group=, domain=, project=) self.roles_mock.revoke.assert_called_with( @@ -631,7 +631,7 @@ class TestRoleRemove(TestRole): kwargs = { 'user': identity_fakes.user_id, 'project': identity_fakes.project_id, - 'inherited': self._is_inheritance_testcase(), + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), } # RoleManager.revoke(role, user=, group=, domain=, project=) self.roles_mock.revoke.assert_called_with( @@ -665,7 +665,7 @@ class TestRoleRemove(TestRole): kwargs = { 'group': identity_fakes.group_id, 'domain': identity_fakes.domain_id, - 'inherited': self._is_inheritance_testcase(), + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), } # RoleManager.revoke(role, user=, group=, domain=, project=) self.roles_mock.revoke.assert_called_with( @@ -698,7 +698,7 @@ class TestRoleRemove(TestRole): kwargs = { 'group': identity_fakes.group_id, 'project': identity_fakes.project_id, - 'inherited': self._is_inheritance_testcase(), + 'os_inherit_extension_inherited': self._is_inheritance_testcase(), } # RoleManager.revoke(role, user=, group=, domain=, project=) self.roles_mock.revoke.assert_called_with( diff --git a/openstackclient/tests/identity/v3/test_role_assignment.py b/openstackclient/tests/identity/v3/test_role_assignment.py index b1ce8b29..9817f53a 100644 --- a/openstackclient/tests/identity/v3/test_role_assignment.py +++ b/openstackclient/tests/identity/v3/test_role_assignment.py @@ -86,21 +86,24 @@ class TestRoleAssignmentList(TestRoleAssignment): effective=False, role=None, user=None, - project=None) + project=None, + os_inherit_extension_inherited_to=None) - collist = ('Role', 'User', 'Group', 'Project', 'Domain') - self.assertEqual(collist, columns) + collist = ('Role', 'User', 'Group', 'Project', 'Domain', 'Inherited') + self.assertEqual(columns, collist) datalist = (( identity_fakes.role_id, identity_fakes.user_id, '', identity_fakes.project_id, - '' + '', + False ), (identity_fakes.role_id, '', identity_fakes.group_id, identity_fakes.project_id, - '' + '', + False ),) self.assertEqual(datalist, tuple(data)) @@ -131,6 +134,7 @@ class TestRoleAssignmentList(TestRoleAssignment): ('project', None), ('role', None), ('effective', False), + ('inherited', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -143,21 +147,24 @@ class TestRoleAssignmentList(TestRoleAssignment): group=None, project=None, role=None, - effective=False) + effective=False, + os_inherit_extension_inherited_to=None) - collist = ('Role', 'User', 'Group', 'Project', 'Domain') - self.assertEqual(collist, columns) + collist = ('Role', 'User', 'Group', 'Project', 'Domain', 'Inherited') + self.assertEqual(columns, collist) datalist = (( identity_fakes.role_id, identity_fakes.user_id, '', '', - identity_fakes.domain_id + identity_fakes.domain_id, + False ), (identity_fakes.role_id, identity_fakes.user_id, '', identity_fakes.project_id, - '' + '', + False ),) self.assertEqual(datalist, tuple(data)) @@ -188,6 +195,7 @@ class TestRoleAssignmentList(TestRoleAssignment): ('project', None), ('role', None), ('effective', False), + ('inherited', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -200,21 +208,24 @@ class TestRoleAssignmentList(TestRoleAssignment): effective=False, project=None, role=None, - user=None) + user=None, + os_inherit_extension_inherited_to=None) - collist = ('Role', 'User', 'Group', 'Project', 'Domain') - self.assertEqual(collist, columns) + collist = ('Role', 'User', 'Group', 'Project', 'Domain', 'Inherited') + self.assertEqual(columns, collist) datalist = (( identity_fakes.role_id, '', identity_fakes.group_id, '', - identity_fakes.domain_id + identity_fakes.domain_id, + False ), (identity_fakes.role_id, '', identity_fakes.group_id, identity_fakes.project_id, - '' + '', + False ),) self.assertEqual(datalist, tuple(data)) @@ -245,6 +256,7 @@ class TestRoleAssignmentList(TestRoleAssignment): ('project', None), ('role', None), ('effective', False), + ('inherited', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -257,21 +269,24 @@ class TestRoleAssignmentList(TestRoleAssignment): effective=False, project=None, role=None, - user=None) + user=None, + os_inherit_extension_inherited_to=None) - collist = ('Role', 'User', 'Group', 'Project', 'Domain') - self.assertEqual(collist, columns) + collist = ('Role', 'User', 'Group', 'Project', 'Domain', 'Inherited') + self.assertEqual(columns, collist) datalist = (( identity_fakes.role_id, identity_fakes.user_id, '', '', - identity_fakes.domain_id + identity_fakes.domain_id, + False ), (identity_fakes.role_id, '', identity_fakes.group_id, '', - identity_fakes.domain_id + identity_fakes.domain_id, + False ),) self.assertEqual(datalist, tuple(data)) @@ -302,6 +317,7 @@ class TestRoleAssignmentList(TestRoleAssignment): ('project', identity_fakes.project_name), ('role', None), ('effective', False), + ('inherited', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -314,21 +330,24 @@ class TestRoleAssignmentList(TestRoleAssignment): effective=False, project=self.projects_mock.get(), role=None, - user=None) + user=None, + os_inherit_extension_inherited_to=None) - collist = ('Role', 'User', 'Group', 'Project', 'Domain') - self.assertEqual(collist, columns) + collist = ('Role', 'User', 'Group', 'Project', 'Domain', 'Inherited') + self.assertEqual(columns, collist) datalist = (( identity_fakes.role_id, identity_fakes.user_id, '', identity_fakes.project_id, - '' + '', + False ), (identity_fakes.role_id, '', identity_fakes.group_id, identity_fakes.project_id, - '' + '', + False ),) self.assertEqual(datalist, tuple(data)) @@ -357,6 +376,7 @@ class TestRoleAssignmentList(TestRoleAssignment): ('project', None), ('role', None), ('effective', True), + ('inherited', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -369,20 +389,84 @@ class TestRoleAssignmentList(TestRoleAssignment): effective=True, project=None, role=None, - user=None) + user=None, + os_inherit_extension_inherited_to=None) - collist = ('Role', 'User', 'Group', 'Project', 'Domain') - self.assertEqual(collist, columns) + collist = ('Role', 'User', 'Group', 'Project', 'Domain', 'Inherited') + self.assertEqual(columns, collist) datalist = (( identity_fakes.role_id, identity_fakes.user_id, '', identity_fakes.project_id, - '' + '', + False + ), (identity_fakes.role_id, + identity_fakes.user_id, + '', + '', + identity_fakes.domain_id, + False + ),) + self.assertEqual(tuple(data), datalist) + + def test_role_assignment_list_inherited(self): + + self.role_assignments_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + (identity_fakes. + ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID_INHERITED)), + loaded=True, + ), + fakes.FakeResource( + None, + copy.deepcopy( + (identity_fakes. + ASSIGNMENT_WITH_DOMAIN_ID_AND_USER_ID_INHERITED)), + loaded=True, + ), + ] + + arglist = ['--inherited'] + verifylist = [ + ('user', None), + ('group', None), + ('domain', None), + ('project', None), + ('role', None), + ('effective', False), + ('inherited', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.role_assignments_mock.list.assert_called_with( + domain=None, + group=None, + effective=False, + project=None, + role=None, + user=None, + os_inherit_extension_inherited_to='projects') + + collist = ('Role', 'User', 'Group', 'Project', 'Domain', 'Inherited') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.user_id, + '', + identity_fakes.project_id, + '', + True ), (identity_fakes.role_id, identity_fakes.user_id, '', '', identity_fakes.domain_id, + True ),) self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index 5b844753..5db04e7c 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -41,17 +41,17 @@ DEFAULT_SERVICE_URL = "http://127.0.0.1:8771/v3.0/" DEFAULT_AUTH_PLUGIN = "v2password" DEFAULT_INTERFACE = "internal" -DEFAULT_COMPUTE_API_VERSION = "2" -DEFAULT_IDENTITY_API_VERSION = "2" -DEFAULT_IMAGE_API_VERSION = "2" -DEFAULT_VOLUME_API_VERSION = "1" -DEFAULT_NETWORK_API_VERSION = "2" - -LIB_COMPUTE_API_VERSION = "2" -LIB_IDENTITY_API_VERSION = "2" -LIB_IMAGE_API_VERSION = "1" -LIB_VOLUME_API_VERSION = "1" -LIB_NETWORK_API_VERSION = "2" +DEFAULT_COMPUTE_API_VERSION = "" +DEFAULT_IDENTITY_API_VERSION = "" +DEFAULT_IMAGE_API_VERSION = "" +DEFAULT_VOLUME_API_VERSION = "" +DEFAULT_NETWORK_API_VERSION = "" + +LIB_COMPUTE_API_VERSION = "" +LIB_IDENTITY_API_VERSION = "" +LIB_IMAGE_API_VERSION = "" +LIB_VOLUME_API_VERSION = "" +LIB_NETWORK_API_VERSION = "" CLOUD_1 = { 'clouds': { @@ -205,7 +205,9 @@ class TestShell(utils.TestCase): initialize_app(). """ - self.occ_get_one = mock.Mock("Test Shell") + cloud = mock.Mock(name="cloudy") + cloud.config = {} + self.occ_get_one = mock.Mock(return_value=cloud) with mock.patch( "os_client_config.config.OpenStackConfig.get_one_cloud", self.occ_get_one, diff --git a/openstackclient/tests/volume/v2/fakes.py b/openstackclient/tests/volume/v2/fakes.py index c896ed6d..7b7758a3 100644 --- a/openstackclient/tests/volume/v2/fakes.py +++ b/openstackclient/tests/volume/v2/fakes.py @@ -17,8 +17,14 @@ import mock from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.image.v2 import fakes as image_fakes from openstackclient.tests import utils +volume_attachment_server = { + 'device': '/dev/ice', + 'server_id': '1233', +} + volume_id = "ce26708d-a7f8-4b4b-9861-4a80256615a6" volume_name = "fake_volume" volume_description = "fake description" @@ -26,11 +32,14 @@ volume_status = "available" volume_size = 20 volume_type = "fake_lvmdriver-1" volume_metadata = { - "foo": "bar" + 'Alpha': 'a', + 'Beta': 'b', + 'Gamma': 'g', } +volume_metadata_str = "Alpha='a', Beta='b', Gamma='g'" volume_snapshot_id = 1 volume_availability_zone = "nova" -volume_attachments = ["fake_attachments"] +volume_attachments = [volume_attachment_server] VOLUME = { "id": volume_id, @@ -169,6 +178,13 @@ QOS_WITH_ASSOCIATIONS = { 'associations': [qos_association] } +image_id = 'im1' +image_name = 'graven' +IMAGE = { + 'id': image_id, + 'name': image_name +} + class FakeVolumeClient(object): def __init__(self, **kwargs): @@ -200,3 +216,7 @@ class TestVolume(utils.TestCommand): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) + self.app.client_manager.image = image_fakes.FakeImagev2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN + ) diff --git a/openstackclient/tests/volume/v2/test_type.py b/openstackclient/tests/volume/v2/test_type.py index c5b27fa5..9a07263b 100644 --- a/openstackclient/tests/volume/v2/test_type.py +++ b/openstackclient/tests/volume/v2/test_type.py @@ -19,6 +19,20 @@ from openstackclient.tests.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume_type +class FakeTypeResource(fakes.FakeResource): + + _keys = {'property': 'value'} + + def set_keys(self, args): + self._keys.update(args) + + def unset_keys(self, key): + self._keys.pop(key, None) + + def get_keys(self): + return self._keys + + class TestType(volume_fakes.TestVolume): def setUp(self): @@ -184,6 +198,122 @@ class TestTypeShow(TestType): self.assertEqual(volume_fakes.TYPE_FORMATTED_data, data) +class TestTypeSet(TestType): + + def setUp(self): + super(TestTypeSet, self).setUp() + + self.types_mock.get.return_value = FakeTypeResource( + None, + copy.deepcopy(volume_fakes.TYPE), + loaded=True, + ) + + # Get the command object to test + self.cmd = volume_type.SetVolumeType(self.app, None) + + def test_type_set_name(self): + new_name = 'new_name' + arglist = [ + '--name', new_name, + volume_fakes.type_id, + ] + verifylist = [ + ('name', new_name), + ('description', None), + ('property', None), + ('volume_type', volume_fakes.type_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': new_name, + } + self.types_mock.update.assert_called_with( + volume_fakes.type_id, + **kwargs + ) + + def test_type_set_description(self): + new_desc = 'new_desc' + arglist = [ + '--description', new_desc, + volume_fakes.type_id, + ] + verifylist = [ + ('name', None), + ('description', new_desc), + ('property', None), + ('volume_type', volume_fakes.type_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': new_desc, + } + self.types_mock.update.assert_called_with( + volume_fakes.type_id, + **kwargs + ) + + def test_type_set_property(self): + arglist = [ + '--property', 'myprop=myvalue', + volume_fakes.type_id, + ] + verifylist = [ + ('name', None), + ('description', None), + ('property', {'myprop': 'myvalue'}), + ('volume_type', volume_fakes.type_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + result = self.types_mock.get.return_value._keys + self.assertIn('myprop', result) + self.assertEqual('myvalue', result['myprop']) + + +class TestTypeUnset(TestType): + + def setUp(self): + super(TestTypeUnset, self).setUp() + + self.types_mock.get.return_value = FakeTypeResource( + None, + copy.deepcopy(volume_fakes.TYPE), + loaded=True, + ) + + self.cmd = volume_type.UnsetVolumeType(self.app, None) + + def test_type_unset(self): + arglist = [ + '--property', 'property', + volume_fakes.type_id, + ] + verifylist = [ + ('property', 'property'), + ('volume_type', volume_fakes.type_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + result = self.types_mock.get.return_value._keys + + self.assertNotIn('property', result) + + class TestTypeDelete(TestType): def setUp(self): super(TestTypeDelete, self).setUp() diff --git a/openstackclient/tests/volume/v2/test_volume.py b/openstackclient/tests/volume/v2/test_volume.py index 9e991b72..b15fd02f 100644 --- a/openstackclient/tests/volume/v2/test_volume.py +++ b/openstackclient/tests/volume/v2/test_volume.py @@ -15,18 +15,699 @@ import copy from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes from openstackclient.tests.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume class TestVolume(volume_fakes.TestVolume): - def setUp(self): super(TestVolume, self).setUp() self.volumes_mock = self.app.client_manager.volume.volumes self.volumes_mock.reset_mock() + self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock.reset_mock() + + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + self.images_mock = self.app.client_manager.image.images + self.images_mock.reset_mock() + + +class TestVolumeCreate(TestVolume): + def setUp(self): + super(TestVolumeCreate, self).setUp() + + self.volumes_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True, + ) + + # Get the command object to test + self.cmd = volume.CreateVolume(self.app, None) + + def test_volume_create_min_options(self): + arglist = [ + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_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.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_options(self): + arglist = [ + '--size', str(volume_fakes.volume_size), + '--description', volume_fakes.volume_description, + '--type', volume_fakes.volume_type, + '--availability-zone', volume_fakes.volume_availability_zone, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('description', volume_fakes.volume_description), + ('type', volume_fakes.volume_type), + ('availability_zone', volume_fakes.volume_availability_zone), + ('name', volume_fakes.volume_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.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=volume_fakes.volume_description, + volume_type=volume_fakes.volume_type, + user_id=None, + project_id=None, + availability_zone=volume_fakes.volume_availability_zone, + metadata=None, + imageRef=None, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_user_project_id(self): + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + # Return a user + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--size', str(volume_fakes.volume_size), + '--project', identity_fakes.project_id, + '--user', identity_fakes.user_id, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('project', identity_fakes.project_id), + ('user', identity_fakes.user_id), + ('name', volume_fakes.volume_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.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=identity_fakes.user_id, + project_id=identity_fakes.project_id, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_user_project_name(self): + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + # Return a user + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--size', str(volume_fakes.volume_size), + '--project', identity_fakes.project_name, + '--user', identity_fakes.user_name, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('project', identity_fakes.project_name), + ('user', identity_fakes.user_name), + ('name', volume_fakes.volume_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.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=identity_fakes.user_id, + project_id=identity_fakes.project_id, + availability_zone=None, + metadata=None, + imageRef=None, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_properties(self): + arglist = [ + '--property', 'Alpha=a', + '--property', 'Beta=b', + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('property', {'Alpha': 'a', 'Beta': 'b'}), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_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.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata={'Alpha': 'a', 'Beta': 'b'}, + imageRef=None, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(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, + ) + + arglist = [ + '--image', volume_fakes.image_id, + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('image', volume_fakes.image_id), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_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.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata=None, + imageRef=volume_fakes.image_id, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(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, + ) + + arglist = [ + '--image', volume_fakes.image_name, + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('image', volume_fakes.image_name), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_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.volumes_mock.create.assert_called_with( + size=volume_fakes.volume_size, + snapshot_id=None, + name=volume_fakes.volume_name, + description=None, + volume_type=None, + user_id=None, + project_id=None, + availability_zone=None, + metadata=None, + imageRef=volume_fakes.image_id, + source_volid=None + ) + + collist = ( + 'attachments', + 'availability_zone', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'snapshot_id', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.volume_attachments, + volume_fakes.volume_availability_zone, + volume_fakes.volume_description, + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + volume_fakes.volume_snapshot_id, + volume_fakes.volume_status, + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + +class TestVolumeList(TestVolume): + + def setUp(self): + super(TestVolumeList, self).setUp() + + self.volumes_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True, + ), + ] + + self.users_mock.get.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ), + ] + + self.projects_mock.get.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = volume.ListVolume(self.app, None) + + def test_volume_list_no_options(self): + arglist = [] + verifylist = [ + ('long', False), + ('all_projects', False), + ('name', None), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = [ + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ] + self.assertEqual(collist, columns) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_all_projects_option(self): + arglist = [ + '--all-projects', + ] + verifylist = [ + ('long', False), + ('all_projects', True), + ('name', None), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = [ + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ] + self.assertEqual(collist, columns) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_name(self): + arglist = [ + '--name', volume_fakes.volume_name, + ] + verifylist = [ + ('long', False), + ('all_projects', False), + ('name', volume_fakes.volume_name), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_status(self): + arglist = [ + '--status', volume_fakes.volume_status, + ] + verifylist = [ + ('long', False), + ('all_projects', False), + ('name', None), + ('status', volume_fakes.volume_status), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ('all_projects', False), + ('name', None), + ('status', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = [ + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Type', + 'Bootable', + 'Attached to', + 'Properties', + ] + self.assertEqual(collist, columns) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + volume_fakes.volume_type, + '', + msg, + "Alpha='a', Beta='b', Gamma='g'", + ), ) + self.assertEqual(datalist, tuple(data)) + class TestVolumeShow(TestVolume): def setUp(self): diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 093178e3..0973868b 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -19,7 +19,7 @@ from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_VOLUME_API_VERSION = '1' +DEFAULT_API_VERSION = '2' API_VERSION_OPTION = 'os_volume_api_version' API_NAME = "volume" API_VERSIONS = { @@ -72,10 +72,8 @@ def build_option_parser(parser): parser.add_argument( '--os-volume-api-version', metavar='<volume-api-version>', - default=utils.env( - 'OS_VOLUME_API_VERSION', - default=DEFAULT_VOLUME_API_VERSION), + default=utils.env('OS_VOLUME_API_VERSION'), help='Volume API version, default=' + - DEFAULT_VOLUME_API_VERSION + + DEFAULT_API_VERSION + ' (Env: OS_VOLUME_API_VERSION)') return parser diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index e50a6f0c..1d298f46 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -14,15 +14,148 @@ """Volume V2 Volume action implementations""" +import copy import logging +import os from cliff import command +from cliff import lister from cliff import show import six +from openstackclient.common import parseractions from openstackclient.common import utils +class CreateVolume(show.ShowOne): + """Create new volume""" + + log = logging.getLogger(__name__ + ".CreateVolume") + + def get_parser(self, prog_name): + parser = super(CreateVolume, self).get_parser(prog_name) + parser.add_argument( + "name", + metavar="<name>", + help="New volume name" + ) + parser.add_argument( + "--size", + metavar="<size>", + type=int, + required=True, + help="New volume size in GB" + ) + parser.add_argument( + "--snapshot", + metavar="<snapshot>", + help="Use <snapshot> as source of new volume (name or ID)" + ) + parser.add_argument( + "--description", + metavar="<description>", + help="New volume description" + ) + parser.add_argument( + "--type", + metavar="<volume-type>", + help="Use <volume-type> as the new volume type", + ) + parser.add_argument( + '--user', + metavar='<user>', + help='Specify an alternate user (name or ID)', + ) + parser.add_argument( + '--project', + metavar='<project>', + help='Specify an alternate project (name or ID)', + ) + parser.add_argument( + "--availability-zone", + metavar="<availability-zone>", + help="Create new volume in <availability_zone>" + ) + parser.add_argument( + "--image", + metavar="<image>", + help="Use <image> as source of new volume (name or ID)" + ) + parser.add_argument( + "--source", + metavar="<volume>", + help="Volume to clone (name or ID)" + ) + parser.add_argument( + "--property", + metavar="<key=value>", + action=parseractions.KeyValueAction, + help="Set a property to this volume " + "(repeat option to set multiple properties)" + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action: (%s)", parsed_args) + + identity_client = self.app.client_manager.identity + volume_client = self.app.client_manager.volume + image_client = self.app.client_manager.image + + source_volume = None + if parsed_args.source: + source_volume = utils.find_resource( + volume_client.volumes, + parsed_args.source).id + + image = None + if parsed_args.image: + image = utils.find_resource( + image_client.images, + parsed_args.image).id + + snapshot = None + if parsed_args.snapshot: + snapshot = utils.find_resource( + volume_client.snapshots, + parsed_args.snapshot).id + + project = None + if parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project).id + + user = None + if parsed_args.user: + user = utils.find_resource( + identity_client.users, + parsed_args.user).id + + volume = volume_client.volumes.create( + size=parsed_args.size, + snapshot_id=snapshot, + name=parsed_args.name, + description=parsed_args.description, + volume_type=parsed_args.type, + user_id=user, + project_id=project, + availability_zone=parsed_args.availability_zone, + metadata=parsed_args.property, + imageRef=image, + source_volid=source_volume + ) + # Remove key links from being displayed + volume._info.update( + { + 'properties': utils.format_dict(volume._info.pop('metadata')), + 'type': volume._info.pop('volume_type') + } + ) + volume._info.pop("links", None) + return zip(*sorted(six.iteritems(volume._info))) + + class DeleteVolume(command.Command): """Delete volume(s)""" @@ -59,6 +192,183 @@ class DeleteVolume(command.Command): return +class ListVolume(lister.Lister): + """List volumes""" + + log = logging.getLogger(__name__ + '.ListVolume') + + def get_parser(self, prog_name): + parser = super(ListVolume, self).get_parser(prog_name) + parser.add_argument( + '--all-projects', + action='store_true', + default=bool(int(os.environ.get("ALL_PROJECTS", 0))), + help='Include all projects (admin only)', + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) + parser.add_argument( + '--name', + metavar='<name>', + help='Filter results by name', + ) + parser.add_argument( + '--status', + metavar='<status>', + help='Filter results by status', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + volume_client = self.app.client_manager.volume + compute_client = self.app.client_manager.compute + + def _format_attach(attachments): + """Return a formatted string of a volume's attached instances + + :param volume: a volume.attachments field + :rtype: a string of formatted instances + """ + + msg = '' + for attachment in attachments: + server = attachment['server_id'] + if server in server_cache: + server = server_cache[server].name + device = attachment['device'] + msg += 'Attached to %s on %s ' % (server, device) + return msg + + if parsed_args.long: + columns = [ + 'ID', + 'Name', + 'Status', + 'Size', + 'Volume Type', + 'Bootable', + 'Attachments', + 'Metadata', + ] + column_headers = copy.deepcopy(columns) + column_headers[1] = 'Display Name' + column_headers[4] = 'Type' + column_headers[6] = 'Attached to' + column_headers[7] = 'Properties' + else: + columns = [ + 'ID', + 'Name', + 'Status', + 'Size', + 'Attachments', + ] + column_headers = copy.deepcopy(columns) + column_headers[1] = 'Display Name' + column_headers[4] = 'Attached to' + + # Cache the server list + server_cache = {} + try: + for s in compute_client.servers.list(): + server_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + + search_opts = { + 'all_projects': parsed_args.all_projects, + 'display_name': parsed_args.name, + 'status': parsed_args.status, + } + + data = volume_client.volumes.list(search_opts=search_opts) + + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters={'Metadata': utils.format_dict, + 'Attachments': _format_attach}, + ) for s in data)) + + +class SetVolume(show.ShowOne): + """Set volume properties""" + + log = logging.getLogger(__name__ + '.SetVolume') + + def get_parser(self, prog_name): + parser = super(SetVolume, self).get_parser(prog_name) + parser.add_argument( + 'volume', + metavar='<volume>', + help='Volume to change (name or ID)', + ) + parser.add_argument( + '--name', + metavar='<name>', + help='New volume name', + ) + parser.add_argument( + '--description', + metavar='<description>', + help='New volume description', + ) + parser.add_argument( + '--size', + metavar='<size>', + type=int, + help='Extend volume size in GB', + ) + parser.add_argument( + '--property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + help='Property to add or modify for this volume ' + '(repeat option to set multiple properties)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + + if parsed_args.size: + if volume.status != 'available': + self.app.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: + self.app.log.error("New size must be greater than %s GB" % + volume.size) + return + volume_client.volumes.extend(volume.id, parsed_args.size) + + if parsed_args.property: + volume_client.volumes.set_metadata(volume.id, parsed_args.property) + + 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) + + if not kwargs and not parsed_args.property and not parsed_args.size: + self.app.log.error("No changes requested\n") + + return + + class ShowVolume(show.ShowOne): """Display volume details""" @@ -81,3 +391,37 @@ class ShowVolume(show.ShowOne): # Remove key links from being displayed volume._info.pop("links", None) return zip(*sorted(six.iteritems(volume._info))) + + +class UnsetVolume(command.Command): + """Unset volume properties""" + + log = logging.getLogger(__name__ + '.UnsetVolume') + + def get_parser(self, prog_name): + parser = super(UnsetVolume, self).get_parser(prog_name) + parser.add_argument( + 'volume', + metavar='<volume>', + help='Volume to modify (name or ID)', + ) + parser.add_argument( + '--property', + metavar='<key>', + required=True, + action='append', + default=[], + help='Property to remove from volume ' + '(repeat option to remove multiple properties)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + volume = utils.find_resource( + volume_client.volumes, parsed_args.volume) + + volume_client.volumes.delete_metadata( + volume.id, parsed_args.property) + return diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index 7f9a1c4b..fb0342c5 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -143,6 +143,67 @@ class ListVolumeType(lister.Lister): ) for s in data)) +class SetVolumeType(command.Command): + """Set volume type properties""" + + log = logging.getLogger(__name__ + '.SetVolumeType') + + def get_parser(self, prog_name): + parser = super(SetVolumeType, self).get_parser(prog_name) + parser.add_argument( + 'volume_type', + metavar='<volume-type>', + help='Volume type to modify (name or ID)', + ) + parser.add_argument( + '--name', + metavar='<name>', + help='Set volume type name', + ) + parser.add_argument( + '--description', + metavar='<name>', + help='Set volume type description', + ) + parser.add_argument( + '--property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + help='Property to add or modify for this volume type ' + '(repeat option to set multiple properties)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + volume_type = utils.find_resource( + volume_client.volume_types, parsed_args.volume_type) + + if (not parsed_args.name + and not parsed_args.description + and not parsed_args.property): + self.app.log.error("No changes requested\n") + return + + kwargs = {} + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description + + if kwargs: + volume_client.volume_types.update( + volume_type.id, + **kwargs + ) + + if parsed_args.property: + volume_type.set_keys(parsed_args.property) + + return + + class ShowVolumeType(show.ShowOne): """Display volume type details""" @@ -165,3 +226,36 @@ class ShowVolumeType(show.ShowOne): properties = utils.format_dict(volume_type._info.pop('extra_specs')) volume_type._info.update({'properties': properties}) return zip(*sorted(six.iteritems(volume_type._info))) + + +class UnsetVolumeType(command.Command): + """Unset volume type properties""" + + log = logging.getLogger(__name__ + '.UnsetVolumeType') + + def get_parser(self, prog_name): + parser = super(UnsetVolumeType, self).get_parser(prog_name) + parser.add_argument( + 'volume_type', + metavar='<volume-type>', + help='Volume type to modify (name or ID)', + ) + parser.add_argument( + '--property', + metavar='<key>', + default=[], + required=True, + help='Property to remove from volume type ' + '(repeat option to remove multiple properties)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + volume_type = utils.find_resource( + volume_client.volume_types, + parsed_args.volume_type, + ) + volume_type.unset_keys(parsed_args.property) + return |
