diff options
Diffstat (limited to 'openstackclient')
31 files changed, 1459 insertions, 85 deletions
diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 742509e4..55c6fe53 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -15,6 +15,7 @@ """Manage access to the clients, including authenticating when needed.""" +import copy import logging import pkg_resources import sys @@ -48,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): @@ -203,6 +208,9 @@ class ClientManager(object): interface=interface) return endpoint + def get_configuration(self): + return copy.deepcopy(self._cli_options.config) + # Plugin Support diff --git a/openstackclient/common/configuration.py b/openstackclient/common/configuration.py new file mode 100644 index 00000000..83df73e2 --- /dev/null +++ b/openstackclient/common/configuration.py @@ -0,0 +1,58 @@ +# 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. +# + +"""Configuration action implementations""" + +import logging + +from cliff import show +import six + +REDACTED = "<redacted>" + + +class ShowConfiguration(show.ShowOne): + """Display configuration details""" + + log = logging.getLogger(__name__ + '.ShowConfiguration') + + def get_parser(self, prog_name): + parser = super(ShowConfiguration, self).get_parser(prog_name) + mask_group = parser.add_mutually_exclusive_group() + mask_group.add_argument( + "--mask", + dest="mask", + action="store_true", + default=True, + help="Attempt to mask passwords (default)", + ) + mask_group.add_argument( + "--unmask", + dest="mask", + action="store_false", + help="Show password in clear text", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + info = self.app.client_manager.get_configuration() + for key, value in six.iteritems(info.pop('auth', {})): + if parsed_args.mask: + if 'password' in key.lower(): + value = REDACTED + if 'token' in key.lower(): + value = REDACTED + info['auth.' + key] = value + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index be6c36eb..e79fd7ed 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -223,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/common/utils.py b/openstackclient/common/utils.py index 6cd35c05..2f8419f4 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -186,9 +186,9 @@ def sort_items(items, sort_str): :param items: a list or generator object of items :param sort_str: a string defining the sort rules, the format is - '<key1>:[direction1],<key2>:[direction2]...', direction can be 'asc' - for ascending or 'desc' for descending, if direction is not given, - it's ascending by default + '<key1>:[direction1],<key2>:[direction2]...', direction can be 'asc' + for ascending or 'desc' for descending, if direction is not given, + it's ascending by default :return: sorted items """ if not sort_str: diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 6ae87b79..9dda32d6 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -19,7 +19,7 @@ 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 = { @@ -68,10 +68,8 @@ 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 diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index eb18a433..3458cf79 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -295,6 +295,7 @@ class UnsetFlavor(show.ShowOne): action='append', help='Property to remove from flavor ' '(repeat option to unset multiple properties)', + required=True, ) parser.add_argument( "flavor", 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_assignment.py b/openstackclient/identity/v3/role_assignment.py index 24e3a7f7..92168498 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,6 +63,7 @@ class ListRoleAssignment(lister.Lister): metavar='<project>', help='Project to filter (name or ID)', ) + common.add_project_domain_option_to_parser(parser) return parser @@ -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,6 +110,7 @@ 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 diff --git a/openstackclient/identity/v3/trust.py b/openstackclient/identity/v3/trust.py index c8e5c4c7..2c3cf537 100644 --- a/openstackclient/identity/v3/trust.py +++ b/openstackclient/identity/v3/trust.py @@ -88,35 +88,20 @@ class CreateTrust(show.ShowOne): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - project_domain = None - if parsed_args.project_domain: - project_domain = common.find_domain(identity_client, - parsed_args.project_domain).id - - trustor_domain = None - if parsed_args.trustor_domain: - trustor_domain = common.find_domain(identity_client, - parsed_args.trustor_domain).id - - trustee_domain = None - if parsed_args.trustee_domain: - trustee_domain = common.find_domain(identity_client, - parsed_args.trustee_domain).id - # NOTE(stevemar): Find the two users, project and roles that # are necessary for making a trust usable, the API dictates that # trustee, project and role are optional, but that makes the trust # pointless, and trusts are immutable, so let's enforce it at the # client level. - trustor_id = utils.find_resource(identity_client.users, - parsed_args.trustor, - domain_id=trustor_domain).id - trustee_id = utils.find_resource(identity_client.users, - parsed_args.trustee, - domain_id=trustee_domain).id - project_id = utils.find_resource(identity_client.projects, + trustor_id = common.find_user(identity_client, + parsed_args.trustor, + parsed_args.trustor_domain).id + trustee_id = common.find_user(identity_client, + parsed_args.trustee, + parsed_args.trustee_domain).id + project_id = common.find_project(identity_client, parsed_args.project, - domain_id=project_domain).id + parsed_args.project_domain).id role_names = [] for role in parsed_args.role: @@ -144,7 +129,7 @@ class CreateTrust(show.ShowOne): # Format roles into something sensible roles = trust._info.pop('roles') - msg = ''.join([r['name'] + ' ' for r in roles]) + msg = ' '.join(r['name'] for r in roles) trust._info['roles'] = msg return zip(*sorted(six.iteritems(trust._info))) @@ -215,7 +200,7 @@ class ShowTrust(show.ShowOne): # Format roles into something sensible roles = trust._info.pop('roles') - msg = ''.join([r['name'] + ' ' for r in roles]) + msg = ' '.join(r['name'] for r in roles) trust._info['roles'] = msg return zip(*sorted(six.iteritems(trust._info))) diff --git a/openstackclient/identity/v3/unscoped_saml.py b/openstackclient/identity/v3/unscoped_saml.py index 9b158b67..fddac68f 100644 --- a/openstackclient/identity/v3/unscoped_saml.py +++ b/openstackclient/identity/v3/unscoped_saml.py @@ -37,7 +37,7 @@ def auth_with_unscoped_saml(func): else: msg = ('This command requires the use of an unscoped SAML ' 'authentication plugin. Please use argument ' - '--os-auth-plugin with one of the following ' + '--os-auth-type with one of the following ' 'plugins: ' + ', '.join(UNSCOPED_AUTH_PLUGINS)) raise exceptions.CommandError(msg) return _decorated 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/image/v2/image.py b/openstackclient/image/v2/image.py index 3dd98338..4c019db6 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -27,6 +27,49 @@ from glanceclient.common import utils as gc_utils from openstackclient.api import utils as api_utils from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.identity import common + + +class AddProjectToImage(show.ShowOne): + """Associate project with image""" + + log = logging.getLogger(__name__ + ".AddProjectToImage") + + def get_parser(self, prog_name): + parser = super(AddProjectToImage, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="<image>", + help="Image to share (name or ID)", + ) + parser.add_argument( + "project", + metavar="<project>", + help="Project to associate with image (name or ID)", + ) + common.add_project_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + image_client = self.app.client_manager.image + identity_client = self.app.client_manager.identity + + project_id = common.find_project(identity_client, + parsed_args.project, + parsed_args.project_domain).id + + image_id = utils.find_resource( + image_client.images, + parsed_args.image).id + + image_member = image_client.image_members.create( + image_id, + project_id, + ) + + return zip(*sorted(six.iteritems(image_member._info))) class DeleteImage(command.Command): @@ -192,6 +235,43 @@ class ListImage(lister.Lister): ) +class RemoveProjectImage(command.Command): + """Disassociate project with image""" + + log = logging.getLogger(__name__ + ".RemoveProjectImage") + + def get_parser(self, prog_name): + parser = super(RemoveProjectImage, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="<image>", + help="Image to unshare (name or ID)", + ) + parser.add_argument( + "project", + metavar="<project>", + help="Project to disassociate with image (name or ID)", + ) + common.add_project_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + image_client = self.app.client_manager.image + identity_client = self.app.client_manager.identity + + project_id = common.find_project(identity_client, + parsed_args.project, + parsed_args.project_domain).id + + image_id = utils.find_resource( + image_client.images, + parsed_args.image).id + + image_client.image_members.delete(image_id, project_id) + + class SaveImage(command.Command): """Save an image locally""" @@ -219,7 +299,7 @@ class SaveImage(command.Command): image_client.images, parsed_args.image, ) - data = image_client.images.data(image) + data = image_client.images.data(image.id) gc_utils.save_image(data, parsed_args.file) 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/object/v1/lib/__init__.py b/openstackclient/object/v1/lib/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/openstackclient/object/v1/lib/__init__.py +++ /dev/null diff --git a/openstackclient/shell.py b/openstackclient/shell.py index edeffdfb..a8b5ac4c 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -240,9 +240,9 @@ class OpenStackShell(app.App): # Set the default plugin to token_endpoint if url and token are given if (self.options.url and self.options.token): # Use service token authentication - cloud_config.set_default('auth_type', 'token_endpoint') + auth_type = 'token_endpoint' else: - cloud_config.set_default('auth_type', 'osc_password') + auth_type = 'osc_password' self.log.debug("options: %s", self.options) project_id = getattr(self.options, 'project_id', None) @@ -266,7 +266,8 @@ class OpenStackShell(app.App): # Ignore the default value of interface. Only if it is set later # will it be used. cc = cloud_config.OpenStackConfig( - override_defaults={'interface': None, }) + override_defaults={'interface': None, + 'auth_type': auth_type, }) self.log.debug("defaults: %s", cc.defaults) self.cloud = cc.get_one_cloud( @@ -297,7 +298,9 @@ 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 diff --git a/openstackclient/tests/common/test_configuration.py b/openstackclient/tests/common/test_configuration.py new file mode 100644 index 00000000..3b942533 --- /dev/null +++ b/openstackclient/tests/common/test_configuration.py @@ -0,0 +1,79 @@ +# 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.common import configuration +from openstackclient.tests import fakes +from openstackclient.tests import utils + + +class TestConfiguration(utils.TestCommand): + + def test_show(self): + arglist = [] + verifylist = [('mask', True)] + cmd = configuration.ShowConfiguration(self.app, None) + parsed_args = self.check_parser(cmd, arglist, verifylist) + + columns, data = cmd.take_action(parsed_args) + + collist = ('auth.password', 'auth.token', 'auth.username', + 'identity_api_version', 'region') + self.assertEqual(collist, columns) + datalist = ( + configuration.REDACTED, + configuration.REDACTED, + fakes.USERNAME, + fakes.VERSION, + fakes.REGION_NAME, + ) + self.assertEqual(datalist, tuple(data)) + + def test_show_unmask(self): + arglist = ['--unmask'] + verifylist = [('mask', False)] + cmd = configuration.ShowConfiguration(self.app, None) + parsed_args = self.check_parser(cmd, arglist, verifylist) + + columns, data = cmd.take_action(parsed_args) + + collist = ('auth.password', 'auth.token', 'auth.username', + 'identity_api_version', 'region') + self.assertEqual(collist, columns) + datalist = ( + fakes.PASSWORD, + fakes.AUTH_TOKEN, + fakes.USERNAME, + fakes.VERSION, + fakes.REGION_NAME, + ) + self.assertEqual(datalist, tuple(data)) + + def test_show_mask(self): + arglist = ['--mask'] + verifylist = [('mask', True)] + cmd = configuration.ShowConfiguration(self.app, None) + parsed_args = self.check_parser(cmd, arglist, verifylist) + + columns, data = cmd.take_action(parsed_args) + + collist = ('auth.password', 'auth.token', 'auth.username', + 'identity_api_version', 'region') + self.assertEqual(collist, columns) + datalist = ( + configuration.REDACTED, + configuration.REDACTED, + fakes.USERNAME, + fakes.VERSION, + fakes.REGION_NAME, + ) + self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index ff69c190..979f9481 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -28,6 +28,7 @@ PASSWORD = "scratchy" PROJECT_NAME = "poochie" REGION_NAME = "richie" INTERFACE = "catchy" +VERSION = "3" TEST_RESPONSE_DICT = fixture.V2Token(token_id=AUTH_TOKEN, user_name=USERNAME) @@ -102,6 +103,17 @@ class FakeClientManager(object): self.auth_ref = None self.auth_plugin_name = None + def get_configuration(self): + return { + 'auth': { + 'username': USERNAME, + 'password': PASSWORD, + 'token': AUTH_TOKEN, + }, + 'region': REGION_NAME, + 'identity_api_version': VERSION, + } + class FakeModule(object): def __init__(self, name, version): diff --git a/openstackclient/tests/identity/v3/test_trust.py b/openstackclient/tests/identity/v3/test_trust.py index b3fbe7f0..b90e7815 100644 --- a/openstackclient/tests/identity/v3/test_trust.py +++ b/openstackclient/tests/identity/v3/test_trust.py @@ -107,7 +107,7 @@ class TestTrustCreate(TestTrust): identity_fakes.trust_id, identity_fakes.trust_impersonation, identity_fakes.project_id, - identity_fakes.role_name + ' ', + identity_fakes.role_name, identity_fakes.user_id, identity_fakes.user_id ) @@ -222,7 +222,7 @@ class TestTrustShow(TestTrust): identity_fakes.trust_id, identity_fakes.trust_impersonation, identity_fakes.project_id, - identity_fakes.role_name + ' ', + identity_fakes.role_name, identity_fakes.user_id, identity_fakes.user_id ) diff --git a/openstackclient/tests/image/v2/fakes.py b/openstackclient/tests/image/v2/fakes.py index 678291bb..1a9e301a 100644 --- a/openstackclient/tests/image/v2/fakes.py +++ b/openstackclient/tests/image/v2/fakes.py @@ -18,6 +18,7 @@ import mock from openstackclient.tests import fakes from openstackclient.tests import utils +from openstackclient.tests.identity.v3 import fakes as identity_fakes image_id = '0f41529e-7c12-4de8-be2d-181abb825b3c' image_name = 'graven' @@ -36,6 +37,13 @@ IMAGE = { IMAGE_columns = tuple(sorted(IMAGE)) IMAGE_data = tuple((IMAGE[x] for x in sorted(IMAGE))) +member_status = 'pending' +MEMBER = { + 'member_id': identity_fakes.project_id, + 'image_id': image_id, + 'status': member_status, +} + # Just enough v2 schema to do some testing IMAGE_schema = { "additionalProperties": { @@ -125,6 +133,8 @@ class FakeImagev2Client(object): def __init__(self, **kwargs): self.images = mock.Mock() self.images.resource_class = fakes.FakeResource(None, {}) + self.image_members = mock.Mock() + self.image_members.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -137,3 +147,8 @@ class TestImagev2(utils.TestCommand): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) + + self.app.client_manager.identity = identity_fakes.FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index 7cfaf083..bfb94765 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -21,6 +21,7 @@ import warlock from glanceclient.v2 import schemas from openstackclient.image.v2 import image from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes from openstackclient.tests.image.v2 import fakes as image_fakes @@ -32,6 +33,96 @@ class TestImage(image_fakes.TestImagev2): # Get a shortcut to the ServerManager Mock self.images_mock = self.app.client_manager.image.images self.images_mock.reset_mock() + self.image_members_mock = self.app.client_manager.image.image_members + self.image_members_mock.reset_mock() + self.project_mock = self.app.client_manager.identity.projects + self.project_mock.reset_mock() + self.domain_mock = self.app.client_manager.identity.domains + self.domain_mock.reset_mock() + + +class TestAddProjectToImage(TestImage): + + def setUp(self): + super(TestAddProjectToImage, 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.image_members_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.MEMBER), + loaded=True, + ) + self.project_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.domain_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + # Get the command object to test + self.cmd = image.AddProjectToImage(self.app, None) + + def test_add_project_to_image_no_option(self): + arglist = [ + image_fakes.image_id, + identity_fakes.project_id, + ] + verifylist = [ + ('image', image_fakes.image_id), + ('project', identity_fakes.project_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.image_members_mock.create.assert_called_with( + image_fakes.image_id, + identity_fakes.project_id + ) + collist = ('image_id', 'member_id', 'status') + self.assertEqual(collist, columns) + datalist = ( + image_fakes.image_id, + identity_fakes.project_id, + image_fakes.member_status + ) + self.assertEqual(datalist, data) + + def test_add_project_to_image_with_option(self): + arglist = [ + image_fakes.image_id, + identity_fakes.project_id, + '--project-domain', identity_fakes.domain_id, + ] + verifylist = [ + ('image', image_fakes.image_id), + ('project', identity_fakes.project_id), + ('project_domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.image_members_mock.create.assert_called_with( + image_fakes.image_id, + identity_fakes.project_id + ) + collist = ('image_id', 'member_id', 'status') + self.assertEqual(collist, columns) + datalist = ( + image_fakes.image_id, + identity_fakes.project_id, + image_fakes.member_status + ) + self.assertEqual(datalist, data) class TestImageDelete(TestImage): @@ -298,6 +389,70 @@ class TestImageList(TestImage): self.assertEqual(datalist, tuple(data)) +class TestRemoveProjectImage(TestImage): + + def setUp(self): + super(TestRemoveProjectImage, 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.project_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.domain_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + self.image_members_mock.delete.return_value = None + # Get the command object to test + self.cmd = image.RemoveProjectImage(self.app, None) + + def test_remove_project_image_no_options(self): + arglist = [ + image_fakes.image_id, + identity_fakes.project_id, + ] + verifylist = [ + ('image', image_fakes.image_id), + ('project', identity_fakes.project_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + self.image_members_mock.delete.assert_called_with( + image_fakes.image_id, + identity_fakes.project_id, + ) + + def test_remove_project_image_with_options(self): + arglist = [ + image_fakes.image_id, + identity_fakes.project_id, + '--project-domain', identity_fakes.domain_id, + ] + verifylist = [ + ('image', image_fakes.image_id), + ('project', identity_fakes.project_id), + ('project_domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + self.image_members_mock.delete.assert_called_with( + image_fakes.image_id, + identity_fakes.project_id, + ) + + class TestImageShow(TestImage): def setUp(self): diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index e2f0580b..0e0cc50b 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': { @@ -203,7 +203,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 4f5f9cfd..a95bc94b 100644 --- a/openstackclient/tests/volume/v2/fakes.py +++ b/openstackclient/tests/volume/v2/fakes.py @@ -17,6 +17,7 @@ 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_id = "ce26708d-a7f8-4b4b-9861-4a80256615a6" @@ -26,8 +27,11 @@ 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"] @@ -93,6 +97,17 @@ TYPE = { TYPE_columns = tuple(sorted(TYPE)) TYPE_data = tuple((TYPE[x] for x in sorted(TYPE))) +formatted_type_properties = "foo='bar'" +TYPE_FORMATTED = { + 'id': type_id, + 'name': type_name, + 'description': type_description, + 'properties': formatted_type_properties +} +TYPE_FORMATTED_columns = tuple(sorted(TYPE_FORMATTED)) +TYPE_FORMATTED_data = tuple((TYPE_FORMATTED[x] for x in + sorted(TYPE_FORMATTED))) + backup_id = "3c409fe6-4d03-4a06-aeab-18bdcdf3c8f4" backup_volume_id = "bdbae8dc-e6ca-43c0-8076-951cc1b093a4" backup_name = "fake_backup" @@ -158,6 +173,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): @@ -189,3 +211,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 6cc988b2..c5b27fa5 100644 --- a/openstackclient/tests/volume/v2/test_type.py +++ b/openstackclient/tests/volume/v2/test_type.py @@ -28,6 +28,135 @@ class TestType(volume_fakes.TestVolume): self.types_mock.reset_mock() +class TestTypeCreate(TestType): + + def setUp(self): + super(TestTypeCreate, self).setUp() + + self.types_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.TYPE), + loaded=True, + ) + # Get the command object to test + self.cmd = volume_type.CreateVolumeType(self.app, None) + + def test_type_create_public(self): + arglist = [ + volume_fakes.type_name, + "--description", volume_fakes.type_description, + "--public" + ] + verifylist = [ + ("name", volume_fakes.type_name), + ("description", volume_fakes.type_description), + ("public", True), + ("private", False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.types_mock.create.assert_called_with( + volume_fakes.type_name, + description=volume_fakes.type_description, + public=True, + ) + + collist = ( + 'description', + 'id', + 'name', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.type_description, + volume_fakes.type_id, + volume_fakes.type_name, + ) + self.assertEqual(datalist, data) + + def test_type_create_private(self): + arglist = [ + volume_fakes.type_name, + "--description", volume_fakes.type_description, + "--private" + ] + verifylist = [ + ("name", volume_fakes.type_name), + ("description", volume_fakes.type_description), + ("public", False), + ("private", True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.types_mock.create.assert_called_with( + volume_fakes.type_name, + description=volume_fakes.type_description, + private=True, + ) + + collist = ( + 'description', + 'id', + 'name', + ) + self.assertEqual(collist, columns) + datalist = ( + volume_fakes.type_description, + volume_fakes.type_id, + volume_fakes.type_name, + ) + self.assertEqual(datalist, data) + + +class TestTypeList(TestType): + def setUp(self): + super(TestTypeList, self).setUp() + + self.types_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.TYPE), + loaded=True + ) + ] + # get the command to test + self.cmd = volume_type.ListVolumeType(self.app, None) + + def test_type_list_without_options(self): + arglist = [] + verifylist = [ + ("long", False) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + collist = ["ID", "Name"] + self.assertEqual(collist, columns) + datalist = (( + volume_fakes.type_id, + volume_fakes.type_name, + ),) + self.assertEqual(datalist, tuple(data)) + + def test_type_list_with_options(self): + arglist = ["--long"] + verifylist = [("long", True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + collist = ["ID", "Name", "Description", "Properties"] + self.assertEqual(collist, columns) + datalist = (( + volume_fakes.type_id, + volume_fakes.type_name, + volume_fakes.type_description, + "foo='bar'" + ),) + self.assertEqual(datalist, tuple(data)) + + class TestTypeShow(TestType): def setUp(self): super(TestTypeShow, self).setUp() @@ -51,8 +180,8 @@ class TestTypeShow(TestType): columns, data = self.cmd.take_action(parsed_args) self.types_mock.get.assert_called_with(volume_fakes.type_id) - self.assertEqual(volume_fakes.TYPE_columns, columns) - self.assertEqual(volume_fakes.TYPE_data, data) + self.assertEqual(volume_fakes.TYPE_FORMATTED_columns, columns) + self.assertEqual(volume_fakes.TYPE_FORMATTED_data, data) class TestTypeDelete(TestType): diff --git a/openstackclient/tests/volume/v2/test_volume.py b/openstackclient/tests/volume/v2/test_volume.py index 9e991b72..4fffefa4 100644 --- a/openstackclient/tests/volume/v2/test_volume.py +++ b/openstackclient/tests/volume/v2/test_volume.py @@ -15,18 +15,485 @@ 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 TestVolumeShow(TestVolume): def setUp(self): diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 093178e3..d4800b8d 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 = '1' 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/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index 5ec2b3c5..e81efb5a 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -263,6 +263,7 @@ class UnsetSnapshot(command.Command): default=[], help='Property to remove from snapshot ' '(repeat to remove multiple values)', + required=True, ) return parser diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index ad9671e3..884611ec 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -437,6 +437,7 @@ class UnsetVolume(command.Command): default=[], help='Property to remove from volume ' '(repeat option to remove multiple properties)', + required=True, ) return parser diff --git a/openstackclient/volume/v1/type.py b/openstackclient/volume/v1/volume_type.py index 46d1828b..d5c617b2 100644 --- a/openstackclient/volume/v1/type.py +++ b/openstackclient/volume/v1/volume_type.py @@ -166,6 +166,7 @@ class UnsetVolumeType(command.Command): default=[], help='Property to remove from volume type ' '(repeat option to remove multiple properties)', + required=True, ) return parser @@ -182,3 +183,27 @@ class UnsetVolumeType(command.Command): else: self.app.log.error("No changes requested\n") return + + +class ShowVolumeType(show.ShowOne): + """Display volume type details""" + + log = logging.getLogger(__name__ + ".ShowVolumeType") + + def get_parser(self, prog_name): + parser = super(ShowVolumeType, self).get_parser(prog_name) + parser.add_argument( + "volume_type", + metavar="<volume-type>", + help="Volume type to display (name or ID)" + ) + 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) + properties = utils.format_dict(volume_type._info.pop('extra_specs')) + volume_type._info.update({'properties': properties}) + return zip(*sorted(six.iteritems(volume_type._info))) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index e50a6f0c..d4536f51 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -20,9 +20,139 @@ from cliff import command 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 +189,77 @@ class DeleteVolume(command.Command): return +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 +282,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 ae5cc8b8..7f9a1c4b 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -17,12 +17,79 @@ import logging 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 CreateVolumeType(show.ShowOne): + """Create new volume type""" + + log = logging.getLogger(__name__ + ".CreateVolumeType") + + def get_parser(self, prog_name): + parser = super(CreateVolumeType, self).get_parser(prog_name) + parser.add_argument( + "name", + metavar="<name>", + help="New volume type name" + ) + parser.add_argument( + "--description", + metavar="<description>", + help="New volume type description", + ) + public_group = parser.add_mutually_exclusive_group() + public_group.add_argument( + "--public", + dest="public", + action="store_true", + default=False, + help="Volume type is accessible to the public", + ) + public_group.add_argument( + "--private", + dest="private", + action="store_true", + default=False, + help="Volume type is not accessible to the public", + ) + parser.add_argument( + '--property', + metavar='<key=value>', + action=parseractions.KeyValueAction, + help='Property to add 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 + + kwargs = {} + if parsed_args.public: + kwargs['public'] = True + if parsed_args.private: + kwargs['private'] = True + + volume_type = volume_client.volume_types.create( + parsed_args.name, + description=parsed_args.description, + **kwargs + ) + volume_type._info.pop('extra_specs') + if parsed_args.property: + result = volume_type.set_keys(parsed_args.property) + volume_type._info.update({'properties': utils.format_dict(result)}) + + return zip(*sorted(six.iteritems(volume_type._info))) + + class DeleteVolumeType(command.Command): """Delete volume type""" @@ -46,6 +113,36 @@ class DeleteVolumeType(command.Command): return +class ListVolumeType(lister.Lister): + """List volume types""" + + log = logging.getLogger(__name__ + '.ListVolumeType') + + def get_parser(self, prog_name): + parser = super(ListVolumeType, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output') + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + if parsed_args.long: + columns = ['ID', 'Name', 'Description', 'Extra Specs'] + column_headers = ['ID', 'Name', 'Description', 'Properties'] + else: + columns = ['ID', 'Name'] + column_headers = columns + data = self.app.client_manager.volume.volume_types.list() + return (column_headers, + (utils.get_item_properties( + s, columns, + formatters={'Extra Specs': utils.format_dict}, + ) for s in data)) + + class ShowVolumeType(show.ShowOne): """Display volume type details""" @@ -65,4 +162,6 @@ class ShowVolumeType(show.ShowOne): volume_client = self.app.client_manager.volume volume_type = utils.find_resource( volume_client.volume_types, parsed_args.volume_type) + properties = utils.format_dict(volume_type._info.pop('extra_specs')) + volume_type._info.update({'properties': properties}) return zip(*sorted(six.iteritems(volume_type._info))) |
