From 4c2e8523a98a0dd33e0203c47e94384420c14f9c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 3 Jun 2021 10:21:20 +0100 Subject: volume: Add 'volume group *' commands These mirror the 'cinder group-*' commands, with arguments copied across essentially verbatim. The only significant departures are the replacement of "tenant" terminology with "project" and the merging of the various volume group replication action commands into the parent volume group (e.g. 'openstack volume group set --enable-replication' instead of 'cinder group enable-replication') volume group create volume group delete volume group list volume group show volume group set volume group failover Change-Id: I3b2c0cb92b8a53cc1c0cefa3313b80f59c9e5835 Signed-off-by: Stephen Finucane --- openstackclient/tests/unit/volume/v3/fakes.py | 111 +++++ .../tests/unit/volume/v3/test_volume_group.py | 497 +++++++++++++++++++++ 2 files changed, 608 insertions(+) create mode 100644 openstackclient/tests/unit/volume/v3/test_volume_group.py (limited to 'openstackclient/tests/unit') diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py index 45cad8c1..b0c96290 100644 --- a/openstackclient/tests/unit/volume/v3/fakes.py +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -32,10 +32,16 @@ class FakeVolumeClient(object): self.attachments = mock.Mock() self.attachments.resource_class = fakes.FakeResource(None, {}) + self.groups = mock.Mock() + self.groups.resource_class = fakes.FakeResource(None, {}) + self.group_types = mock.Mock() + self.group_types.resource_class = fakes.FakeResource(None, {}) self.messages = mock.Mock() self.messages.resource_class = fakes.FakeResource(None, {}) self.volumes = mock.Mock() self.volumes.resource_class = fakes.FakeResource(None, {}) + self.volume_types = mock.Mock() + self.volume_types.resource_class = fakes.FakeResource(None, {}) class TestVolume(utils.TestCommand): @@ -59,6 +65,111 @@ class TestVolume(utils.TestCommand): # TODO(stephenfin): Check if the responses are actually the same FakeVolume = volume_v2_fakes.FakeVolume +FakeVolumeType = volume_v2_fakes.FakeVolumeType + + +class FakeVolumeGroup: + """Fake one or more volume groups.""" + + @staticmethod + def create_one_volume_group(attrs=None): + """Create a fake group. + + :param attrs: A dictionary with all attributes of group + :return: A FakeResource object with id, name, status, etc. + """ + attrs = attrs or {} + + group_type = attrs.pop('group_type', None) or uuid.uuid4().hex + volume_types = attrs.pop('volume_types', None) or [uuid.uuid4().hex] + + # Set default attribute + group_info = { + 'id': uuid.uuid4().hex, + 'status': random.choice([ + 'available', + ]), + 'availability_zone': f'az-{uuid.uuid4().hex}', + 'created_at': '2015-09-16T09:28:52.000000', + 'name': 'first_group', + 'description': f'description-{uuid.uuid4().hex}', + 'group_type': group_type, + 'volume_types': volume_types, + 'volumes': [f'volume-{uuid.uuid4().hex}'], + 'group_snapshot_id': None, + 'source_group_id': None, + 'project_id': f'project-{uuid.uuid4().hex}', + } + + # Overwrite default attributes if there are some attributes set + group_info.update(attrs) + + group = fakes.FakeResource( + None, + group_info, + loaded=True) + return group + + @staticmethod + def create_volume_groups(attrs=None, count=2): + """Create multiple fake groups. + + :param attrs: A dictionary with all attributes of group + :param count: The number of groups to be faked + :return: A list of FakeResource objects + """ + groups = [] + for n in range(0, count): + groups.append(FakeVolumeGroup.create_one_volume_group(attrs)) + + return groups + + +class FakeVolumeGroupType: + """Fake one or more volume group types.""" + + @staticmethod + def create_one_volume_group_type(attrs=None): + """Create a fake group type. + + :param attrs: A dictionary with all attributes of group type + :return: A FakeResource object with id, name, description, etc. + """ + attrs = attrs or {} + + # Set default attribute + group_type_info = { + 'id': uuid.uuid4().hex, + 'name': f'group-type-{uuid.uuid4().hex}', + 'description': f'description-{uuid.uuid4().hex}', + 'is_public': random.choice([True, False]), + 'group_specs': {}, + } + + # Overwrite default attributes if there are some attributes set + group_type_info.update(attrs) + + group_type = fakes.FakeResource( + None, + group_type_info, + loaded=True) + return group_type + + @staticmethod + def create_volume_group_types(attrs=None, count=2): + """Create multiple fake group types. + + :param attrs: A dictionary with all attributes of group type + :param count: The number of group types to be faked + :return: A list of FakeResource objects + """ + group_types = [] + for n in range(0, count): + group_types.append( + FakeVolumeGroupType.create_one_volume_group_type(attrs) + ) + + return group_types class FakeVolumeMessage: diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group.py b/openstackclient/tests/unit/volume/v3/test_volume_group.py new file mode 100644 index 00000000..13ef38d2 --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_volume_group.py @@ -0,0 +1,497 @@ +# 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 cinderclient import api_versions +from osc_lib import exceptions + +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import volume_group + + +class TestVolumeGroup(volume_fakes.TestVolume): + + def setUp(self): + super().setUp() + + self.volume_groups_mock = self.app.client_manager.volume.groups + self.volume_groups_mock.reset_mock() + + self.volume_group_types_mock = \ + self.app.client_manager.volume.group_types + self.volume_group_types_mock.reset_mock() + + self.volume_types_mock = self.app.client_manager.volume.volume_types + self.volume_types_mock.reset_mock() + + +class TestVolumeGroupCreate(TestVolumeGroup): + + fake_volume_type = volume_fakes.FakeVolumeType.create_one_volume_type() + fake_volume_group_type = \ + volume_fakes.FakeVolumeGroupType.create_one_volume_group_type() + fake_volume_group = volume_fakes.FakeVolumeGroup.create_one_volume_group( + attrs={ + 'group_type': fake_volume_group_type.id, + 'volume_types': [fake_volume_type.id], + }, + ) + + columns = ( + 'ID', + 'Status', + 'Name', + 'Description', + 'Group Type', + 'Volume Types', + 'Availability Zone', + 'Created At', + 'Volumes', + 'Group Snapshot ID', + 'Source Group ID', + ) + data = ( + fake_volume_group.id, + fake_volume_group.status, + fake_volume_group.name, + fake_volume_group.description, + fake_volume_group.group_type, + fake_volume_group.volume_types, + fake_volume_group.availability_zone, + fake_volume_group.created_at, + fake_volume_group.volumes, + fake_volume_group.group_snapshot_id, + fake_volume_group.source_group_id, + ) + + def setUp(self): + super().setUp() + + self.volume_types_mock.get.return_value = self.fake_volume_type + self.volume_group_types_mock.get.return_value = \ + self.fake_volume_group_type + self.volume_groups_mock.create.return_value = self.fake_volume_group + self.volume_groups_mock.get.return_value = self.fake_volume_group + + self.cmd = volume_group.CreateVolumeGroup(self.app, None) + + def test_volume_group_create(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + self.fake_volume_group_type.id, + self.fake_volume_type.id, + ] + verifylist = [ + ('volume_group_type', self.fake_volume_group_type.id), + ('volume_types', [self.fake_volume_type.id]), + ('name', None), + ('description', None), + ('availability_zone', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.get.assert_called_once_with( + self.fake_volume_group_type.id) + self.volume_types_mock.get.assert_called_once_with( + self.fake_volume_type.id) + self.volume_groups_mock.create.assert_called_once_with( + self.fake_volume_group_type.id, + self.fake_volume_type.id, + None, + None, + availability_zone=None, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_create_with_options(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + self.fake_volume_group_type.id, + self.fake_volume_type.id, + '--name', 'foo', + '--description', 'hello, world', + '--availability-zone', 'bar', + ] + verifylist = [ + ('volume_group_type', self.fake_volume_group_type.id), + ('volume_types', [self.fake_volume_type.id]), + ('name', 'foo'), + ('description', 'hello, world'), + ('availability_zone', 'bar'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_group_types_mock.get.assert_called_once_with( + self.fake_volume_group_type.id) + self.volume_types_mock.get.assert_called_once_with( + self.fake_volume_type.id) + self.volume_groups_mock.create.assert_called_once_with( + self.fake_volume_group_type.id, + self.fake_volume_type.id, + 'foo', + 'hello, world', + availability_zone='bar', + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_create_pre_v313(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.12') + + arglist = [ + self.fake_volume_group_type.id, + self.fake_volume_type.id, + ] + verifylist = [ + ('volume_group_type', self.fake_volume_group_type.id), + ('volume_types', [self.fake_volume_type.id]), + ('name', None), + ('description', None), + ('availability_zone', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.13 or greater is required', + str(exc)) + + +class TestVolumeGroupDelete(TestVolumeGroup): + + fake_volume_group = \ + volume_fakes.FakeVolumeGroup.create_one_volume_group() + + def setUp(self): + super().setUp() + + self.volume_groups_mock.get.return_value = self.fake_volume_group + self.volume_groups_mock.delete.return_value = None + + self.cmd = volume_group.DeleteVolumeGroup(self.app, None) + + def test_volume_group_delete(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + self.fake_volume_group.id, + '--force', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('force', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.delete.assert_called_once_with( + self.fake_volume_group.id, delete_volumes=True, + ) + self.assertIsNone(result) + + def test_volume_group_delete_pre_v313(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.12') + + arglist = [ + self.fake_volume_group.id, + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('force', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.13 or greater is required', + str(exc)) + + +class TestVolumeGroupSet(TestVolumeGroup): + + fake_volume_group = \ + volume_fakes.FakeVolumeGroup.create_one_volume_group() + + columns = ( + 'ID', + 'Status', + 'Name', + 'Description', + 'Group Type', + 'Volume Types', + 'Availability Zone', + 'Created At', + 'Volumes', + 'Group Snapshot ID', + 'Source Group ID', + ) + data = ( + fake_volume_group.id, + fake_volume_group.status, + fake_volume_group.name, + fake_volume_group.description, + fake_volume_group.group_type, + fake_volume_group.volume_types, + fake_volume_group.availability_zone, + fake_volume_group.created_at, + fake_volume_group.volumes, + fake_volume_group.group_snapshot_id, + fake_volume_group.source_group_id, + ) + + def setUp(self): + super().setUp() + + self.volume_groups_mock.get.return_value = self.fake_volume_group + self.volume_groups_mock.update.return_value = self.fake_volume_group + + self.cmd = volume_group.SetVolumeGroup(self.app, None) + + def test_volume_group_set(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + self.fake_volume_group.id, + '--name', 'foo', + '--description', 'hello, world', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('name', 'foo'), + ('description', 'hello, world'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.update.assert_called_once_with( + self.fake_volume_group.id, name='foo', description='hello, world', + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_with_enable_replication_option(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.38') + + arglist = [ + self.fake_volume_group.id, + '--enable-replication', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('enable_replication', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.enable_replication.assert_called_once_with( + self.fake_volume_group.id) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_set_pre_v313(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.12') + + arglist = [ + self.fake_volume_group.id, + '--name', 'foo', + '--description', 'hello, world', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('name', 'foo'), + ('description', 'hello, world'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.13 or greater is required', + str(exc)) + + def test_volume_group_with_enable_replication_option_pre_v338(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.37') + + arglist = [ + self.fake_volume_group.id, + '--enable-replication', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('enable_replication', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.38 or greater is required', + str(exc)) + + +class TestVolumeGroupList(TestVolumeGroup): + + fake_volume_groups = \ + volume_fakes.FakeVolumeGroup.create_volume_groups() + + columns = ( + 'ID', + 'Status', + 'Name', + ) + data = [ + ( + fake_volume_group.id, + fake_volume_group.status, + fake_volume_group.name, + ) for fake_volume_group in fake_volume_groups + ] + + def setUp(self): + super().setUp() + + self.volume_groups_mock.list.return_value = self.fake_volume_groups + + self.cmd = volume_group.ListVolumeGroup(self.app, None) + + def test_volume_group_list(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.13') + + arglist = [ + '--all-projects', + ] + verifylist = [ + ('all_projects', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.list.assert_called_once_with( + search_opts={ + 'all_tenants': True, + }, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), data) + + def test_volume_group_list_pre_v313(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.12') + + arglist = [ + '--all-projects', + ] + verifylist = [ + ('all_projects', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.13 or greater is required', + str(exc)) + + +class TestVolumeGroupFailover(TestVolumeGroup): + + fake_volume_group = \ + volume_fakes.FakeVolumeGroup.create_one_volume_group() + + def setUp(self): + super().setUp() + + self.volume_groups_mock.get.return_value = self.fake_volume_group + self.volume_groups_mock.failover_replication.return_value = None + + self.cmd = volume_group.FailoverVolumeGroup(self.app, None) + + def test_volume_group_failover(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.38') + + arglist = [ + self.fake_volume_group.id, + '--allow-attached-volume', + '--secondary-backend-id', 'foo', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('allow_attached_volume', True), + ('secondary_backend_id', 'foo'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_groups_mock.failover_replication.assert_called_once_with( + self.fake_volume_group.id, + allow_attached_volume=True, + secondary_backend_id='foo', + ) + self.assertIsNone(result) + + def test_volume_group_failover_pre_v338(self): + self.app.client_manager.volume.api_version = \ + api_versions.APIVersion('3.37') + + arglist = [ + self.fake_volume_group.id, + '--allow-attached-volume', + '--secondary-backend-id', 'foo', + ] + verifylist = [ + ('group', self.fake_volume_group.id), + ('allow_attached_volume', True), + ('secondary_backend_id', 'foo'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn( + '--os-volume-api-version 3.38 or greater is required', + str(exc)) -- cgit v1.2.1