summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStephen Finucane <sfinucan@redhat.com>2021-06-03 10:21:20 +0100
committerStephen Finucane <sfinucan@redhat.com>2021-06-03 17:58:48 +0100
commit4c2e8523a98a0dd33e0203c47e94384420c14f9c (patch)
tree1d854cbedd7b7bdd31323b31dd617c05b7d31304
parent5faa9ef8058a161a0637dceb91f213ac5ac39070 (diff)
downloadpython-openstackclient-4c2e8523a98a0dd33e0203c47e94384420c14f9c.tar.gz
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 <sfinucan@redhat.com>
-rw-r--r--doc/source/cli/command-objects/volume-group.rst23
-rw-r--r--doc/source/cli/commands.rst1
-rw-r--r--doc/source/cli/data/cinder.csv18
-rw-r--r--openstackclient/tests/unit/volume/v3/fakes.py111
-rw-r--r--openstackclient/tests/unit/volume/v3/test_volume_group.py497
-rw-r--r--openstackclient/volume/v3/volume_group.py506
-rw-r--r--releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml8
-rw-r--r--setup.cfg7
8 files changed, 1162 insertions, 9 deletions
diff --git a/doc/source/cli/command-objects/volume-group.rst b/doc/source/cli/command-objects/volume-group.rst
new file mode 100644
index 00000000..50bc830f
--- /dev/null
+++ b/doc/source/cli/command-objects/volume-group.rst
@@ -0,0 +1,23 @@
+============
+volume group
+============
+
+Block Storage v3
+
+.. autoprogram-cliff:: openstack.volume.v3
+ :command: volume group create
+
+.. autoprogram-cliff:: openstack.volume.v3
+ :command: volume group delete
+
+.. autoprogram-cliff:: openstack.volume.v3
+ :command: volume group list
+
+.. autoprogram-cliff:: openstack.volume.v3
+ :command: volume group failover
+
+.. autoprogram-cliff:: openstack.volume.v3
+ :command: volume group set
+
+.. autoprogram-cliff:: openstack.volume.v3
+ :command: volume group show
diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst
index cc95b3d1..b91a896f 100644
--- a/doc/source/cli/commands.rst
+++ b/doc/source/cli/commands.rst
@@ -159,6 +159,7 @@ referring to both Compute and Volume quotas.
* ``volume backend pool``: (**Volume**) volume backend storage pools
* ``volume backup record``: (**Volume**) volume record that can be imported or exported
* ``volume backend``: (**Volume**) volume backend storage
+* ``volume group``: (**Volume**) group of volumes
* ``volume host``: (**Volume**) the physical computer for volumes
* ``volume message``: (**Volume**) volume API internal messages detailing volume failure messages
* ``volume qos``: (**Volume**) quality-of-service (QoS) specification for volumes
diff --git a/doc/source/cli/data/cinder.csv b/doc/source/cli/data/cinder.csv
index 649cd8f5..031aa439 100644
--- a/doc/source/cli/data/cinder.csv
+++ b/doc/source/cli/data/cinder.csv
@@ -44,15 +44,15 @@ force-delete,volume delete --force,"Attempts force-delete of volume regardless o
freeze-host,volume host set --disable,Freeze and disable the specified cinder-volume host.
get-capabilities,volume backend capability show,Show capabilities of a volume backend. Admin only.
get-pools,volume backend pool list,Show pool information for backends. Admin only.
-group-create,,Creates a group. (Supported by API versions 3.13 - 3.latest)
+group-create,volume group create,Creates a group. (Supported by API versions 3.13 - 3.latest)
group-create-from-src,,Creates a group from a group snapshot or a source group. (Supported by API versions 3.14 - 3.latest)
-group-delete,,Removes one or more groups. (Supported by API versions 3.13 - 3.latest)
-group-disable-replication,,Disables replication for group. (Supported by API versions 3.38 - 3.latest)
-group-enable-replication,,Enables replication for group. (Supported by API versions 3.38 - 3.latest)
-group-failover-replication,,Fails over replication for group. (Supported by API versions 3.38 - 3.latest)
-group-list,,Lists all groups. (Supported by API versions 3.13 - 3.latest)
-group-list-replication-targets,,Lists replication targets for group. (Supported by API versions 3.38 - 3.latest)
-group-show,,Shows details of a group. (Supported by API versions 3.13 - 3.latest)
+group-delete,volume group delete,Removes one or more groups. (Supported by API versions 3.13 - 3.latest)
+group-disable-replication,volume group set --disable-replication,Disables replication for group. (Supported by API versions 3.38 - 3.latest)
+group-enable-replication,volume group set --enable-replication,Enables replication for group. (Supported by API versions 3.38 - 3.latest)
+group-failover-replication,volume group failover,Fails over replication for group. (Supported by API versions 3.38 - 3.latest)
+group-list,volume group list,Lists all groups. (Supported by API versions 3.13 - 3.latest)
+group-list-replication-targets,volume group list --replication-targets,Lists replication targets for group. (Supported by API versions 3.38 - 3.latest)
+group-show,volume group show,Shows details of a group. (Supported by API versions 3.13 - 3.latest)
group-snapshot-create,,Creates a group snapshot. (Supported by API versions 3.14 - 3.latest)
group-snapshot-delete,,Removes one or more group snapshots. (Supported by API versions 3.14 - 3.latest)
group-snapshot-list,,Lists all group snapshots. (Supported by API versions 3.14 - 3.latest)
@@ -65,7 +65,7 @@ group-type-key,,Sets or unsets group_spec for a group type. (Supported by API ve
group-type-list,,Lists available 'group types'. (Admin only will see private types) (Supported by API versions 3.11 - 3.latest)
group-type-show,,Show group type details. (Supported by API versions 3.11 - 3.latest)
group-type-update,,Updates group type name description and/or is_public. (Supported by API versions 3.11 - 3.latest)
-group-update,,Updates a group. (Supported by API versions 3.13 - 3.latest)
+group-update,volume group set,Updates a group. (Supported by API versions 3.13 - 3.latest)
image-metadata,volume set --image-property,Sets or deletes volume image metadata.
image-metadata-show,volume show,Shows volume image metadata.
list,volume list,Lists all volumes.
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))
diff --git a/openstackclient/volume/v3/volume_group.py b/openstackclient/volume/v3/volume_group.py
new file mode 100644
index 00000000..db4e9a94
--- /dev/null
+++ b/openstackclient/volume/v3/volume_group.py
@@ -0,0 +1,506 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from cinderclient import api_versions
+from osc_lib.command import command
+from osc_lib import exceptions
+from osc_lib import utils
+
+from openstackclient.i18n import _
+
+LOG = logging.getLogger(__name__)
+
+
+def _format_group(group):
+ columns = (
+ 'id',
+ 'status',
+ 'name',
+ 'description',
+ 'group_type',
+ 'volume_types',
+ 'availability_zone',
+ 'created_at',
+ 'volumes',
+ 'group_snapshot_id',
+ 'source_group_id',
+ )
+ column_headers = (
+ 'ID',
+ 'Status',
+ 'Name',
+ 'Description',
+ 'Group Type',
+ 'Volume Types',
+ 'Availability Zone',
+ 'Created At',
+ 'Volumes',
+ 'Group Snapshot ID',
+ 'Source Group ID',
+ )
+
+ # TODO(stephenfin): Consider using a formatter for volume_types since it's
+ # a list
+ return (
+ column_headers,
+ utils.get_item_properties(
+ group,
+ columns,
+ ),
+ )
+
+
+class CreateVolumeGroup(command.ShowOne):
+ """Create a volume group.
+
+ Generic volume groups enable you to create a group of volumes and manage
+ them together.
+
+ Generic volume groups are more flexible than consistency groups. Currently
+ volume consistency groups only support consistent group snapshot. It
+ cannot be extended easily to serve other purposes. A project may want to
+ put volumes used in the same application together in a group so that it is
+ easier to manage them together, and this group of volumes may or may not
+ support consistent group snapshot. Generic volume group solve this problem.
+ By decoupling the tight relationship between the group construct and the
+ consistency concept, generic volume groups can be extended to support other
+ features in the future.
+
+ This command requires ``--os-volume-api-version`` 3.13 or greater.
+ """
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ 'volume_group_type',
+ metavar='<volume_group_type>',
+ help=_('Name or ID of volume group type to use.'),
+ )
+ parser.add_argument(
+ 'volume_types',
+ metavar='<volume_type>',
+ nargs='+',
+ default=[],
+ help=_('Name or ID of volume type(s) to use.'),
+ )
+ parser.add_argument(
+ '--name',
+ metavar='<name>',
+ help=_('Name of the volume group.'),
+ )
+ parser.add_argument(
+ '--description',
+ metavar='<description>',
+ help=_('Description of a volume group.')
+ )
+ parser.add_argument(
+ '--availability-zone',
+ metavar='<availability-zone>',
+ help=_('Availability zone for volume group.'),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ volume_client = self.app.client_manager.volume
+
+ if volume_client.api_version < api_versions.APIVersion('3.13'):
+ msg = _(
+ "--os-volume-api-version 3.13 or greater is required to "
+ "support the 'volume group create' command"
+ )
+ raise exceptions.CommandError(msg)
+
+ volume_group_type = utils.find_resource(
+ volume_client.group_types,
+ parsed_args.volume_group_type,
+ )
+
+ volume_types = []
+ for volume_type in parsed_args.volume_types:
+ volume_types.append(
+ utils.find_resource(
+ volume_client.volume_types,
+ volume_type,
+ )
+ )
+
+ group = volume_client.groups.create(
+ volume_group_type.id,
+ ','.join(x.id for x in volume_types),
+ parsed_args.name,
+ parsed_args.description,
+ availability_zone=parsed_args.availability_zone)
+
+ group = volume_client.groups.get(group.id)
+
+ return _format_group(group)
+
+
+class DeleteVolumeGroup(command.Command):
+ """Delete a volume group.
+
+ This command requires ``--os-volume-api-version`` 3.13 or greater.
+ """
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ 'group',
+ metavar='<group>',
+ help=_('Name or ID of volume group to delete'),
+ )
+ parser.add_argument(
+ '--force',
+ action='store_true',
+ default=False,
+ help=_(
+ 'Delete the volume group even if it contains volumes. '
+ 'This will delete any remaining volumes in the group.',
+ )
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ volume_client = self.app.client_manager.volume
+
+ if volume_client.api_version < api_versions.APIVersion('3.13'):
+ msg = _(
+ "--os-volume-api-version 3.13 or greater is required to "
+ "support the 'volume group delete' command"
+ )
+ raise exceptions.CommandError(msg)
+
+ group = utils.find_resource(
+ volume_client.groups,
+ parsed_args.group,
+ )
+
+ volume_client.groups.delete(
+ group.id, delete_volumes=parsed_args.force)
+
+
+class SetVolumeGroup(command.ShowOne):
+ """Update a volume group.
+
+ This command requires ``--os-volume-api-version`` 3.13 or greater.
+ """
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ 'group',
+ metavar='<group>',
+ help=_('Name or ID of volume group.'),
+ )
+ parser.add_argument(
+ '--name',
+ metavar='<name>',
+ help=_('New name for group.'),
+ )
+ parser.add_argument(
+ '--description',
+ metavar='<description>',
+ help=_('New description for group.'),
+ )
+ parser.add_argument(
+ '--enable-replication',
+ action='store_true',
+ dest='enable_replication',
+ default=None,
+ help=_(
+ 'Enable replication for group. '
+ '(supported by --os-volume-api-version 3.38 or above)'
+ ),
+ )
+ parser.add_argument(
+ '--disable-replication',
+ action='store_false',
+ dest='enable_replication',
+ help=_(
+ 'Disable replication for group. '
+ '(supported by --os-volume-api-version 3.38 or above)'
+ ),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ volume_client = self.app.client_manager.volume
+
+ if volume_client.api_version < api_versions.APIVersion('3.13'):
+ msg = _(
+ "--os-volume-api-version 3.13 or greater is required to "
+ "support the 'volume group set' command"
+ )
+ raise exceptions.CommandError(msg)
+
+ group = utils.find_resource(
+ volume_client.groups,
+ parsed_args.group,
+ )
+
+ if parsed_args.enable_replication is not None:
+ if volume_client.api_version < api_versions.APIVersion('3.38'):
+ msg = _(
+ "--os-volume-api-version 3.38 or greater is required to "
+ "support the '--enable-replication' or "
+ "'--disable-replication' options"
+ )
+ raise exceptions.CommandError(msg)
+
+ if parsed_args.enable_replication:
+ volume_client.groups.enable_replication(group.id)
+ else:
+ volume_client.groups.disable_replication(group.id)
+
+ kwargs = {}
+
+ if parsed_args.name is not None:
+ kwargs['name'] = parsed_args.name
+
+ if parsed_args.description is not None:
+ kwargs['description'] = parsed_args.description
+
+ if kwargs:
+ group = volume_client.groups.update(group.id, **kwargs)
+
+ return _format_group(group)
+
+
+class ListVolumeGroup(command.Lister):
+ """Lists all volume groups.
+
+ This command requires ``--os-volume-api-version`` 3.13 or greater.
+ """
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ '--all-projects',
+ dest='all_projects',
+ action='store_true',
+ default=utils.env('ALL_PROJECTS', default=False),
+ help=_('Shows details for all projects (admin only).'),
+ )
+ # TODO(stephenfin): Add once we have an equivalent command for
+ # 'cinder list-filters'
+ # parser.add_argument(
+ # '--filter',
+ # metavar='<key=value>',
+ # action=parseractions.KeyValueAction,
+ # dest='filters',
+ # help=_(
+ # "Filter key and value pairs. Use 'foo' to "
+ # "check enabled filters from server. Use 'key~=value' for "
+ # "inexact filtering if the key supports "
+ # "(supported by --os-volume-api-version 3.33 or above)"
+ # ),
+ # )
+ return parser
+
+ def take_action(self, parsed_args):
+ volume_client = self.app.client_manager.volume
+
+ if volume_client.api_version < api_versions.APIVersion('3.13'):
+ msg = _(
+ "--os-volume-api-version 3.13 or greater is required to "
+ "support the 'volume group list' command"
+ )
+ raise exceptions.CommandError(msg)
+
+ search_opts = {
+ 'all_tenants': parsed_args.all_projects,
+ }
+
+ groups = volume_client.groups.list(
+ search_opts=search_opts)
+
+ column_headers = (
+ 'ID',
+ 'Status',
+ 'Name',
+ )
+ columns = (
+ 'id',
+ 'status',
+ 'name',
+ )
+
+ return (
+ column_headers,
+ (
+ utils.get_item_properties(a, columns)
+ for a in groups
+ ),
+ )
+
+
+class ShowVolumeGroup(command.ShowOne):
+ """Show detailed information for a volume group.
+
+ This command requires ``--os-volume-api-version`` 3.13 or greater.
+ """
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ 'group',
+ metavar='<group>',
+ help=_('Name or ID of volume group.'),
+ )
+ parser.add_argument(
+ '--volumes',
+ action='store_true',
+ dest='show_volumes',
+ default=None,
+ help=_(
+ 'Show volumes included in the group. '
+ '(supported by --os-volume-api-version 3.25 or above)'
+ ),
+ )
+ parser.add_argument(
+ '--no-volumes',
+ action='store_false',
+ dest='show_volumes',
+ help=_(
+ 'Do not show volumes included in the group. '
+ '(supported by --os-volume-api-version 3.25 or above)'
+ ),
+ )
+ parser.add_argument(
+ '--replication-targets',
+ action='store_true',
+ dest='show_replication_targets',
+ default=None,
+ help=_(
+ 'Show replication targets for the group. '
+ '(supported by --os-volume-api-version 3.38 or above)'
+ ),
+ )
+ parser.add_argument(
+ '--no-replication-targets',
+ action='store_false',
+ dest='show_replication_targets',
+ help=_(
+ 'Do not show replication targets for the group. '
+ '(supported by --os-volume-api-version 3.38 or above)'
+ ),
+ )
+
+ return parser
+
+ def take_action(self, parsed_args):
+ volume_client = self.app.client_manager.volume
+
+ if volume_client.api_version < api_versions.APIVersion('3.13'):
+ msg = _(
+ "--os-volume-api-version 3.13 or greater is required to "
+ "support the 'volume group show' command"
+ )
+ raise exceptions.CommandError(msg)
+
+ kwargs = {}
+
+ if parsed_args.show_volumes is not None:
+ if volume_client.api_version < api_versions.APIVersion('3.25'):
+ msg = _(
+ "--os-volume-api-version 3.25 or greater is required to "
+ "support the '--(no-)volumes' option"
+ )
+ raise exceptions.CommandError(msg)
+
+ kwargs['list_volume'] = parsed_args.show_volumes
+
+ if parsed_args.show_replication_targets is not None:
+ if volume_client.api_version < api_versions.APIVersion('3.38'):
+ msg = _(
+ "--os-volume-api-version 3.38 or greater is required to "
+ "support the '--(no-)replication-targets' option"
+ )
+ raise exceptions.CommandError(msg)
+
+ group = utils.find_resource(
+ volume_client.groups,
+ parsed_args.group,
+ )
+
+ group = volume_client.groups.show(group.id, **kwargs)
+
+ if parsed_args.show_replication_targets:
+ replication_targets = \
+ volume_client.groups.list_replication_targets(group.id)
+
+ group.replication_targets = replication_targets
+
+ # TODO(stephenfin): Show replication targets
+ return _format_group(group)
+
+
+class FailoverVolumeGroup(command.Command):
+ """Failover replication for a volume group.
+
+ This command requires ``--os-volume-api-version`` 3.38 or greater.
+ """
+
+ def get_parser(self, prog_name):
+ parser = super().get_parser(prog_name)
+ parser.add_argument(
+ 'group',
+ metavar='<group>',
+ help=_('Name or ID of volume group to failover replication for.'),
+ )
+ parser.add_argument(
+ '--allow-attached-volume',
+ action='store_true',
+ dest='allow_attached_volume',
+ default=False,
+ help=_(
+ 'Allow group with attached volumes to be failed over.',
+ )
+ )
+ parser.add_argument(
+ '--disallow-attached-volume',
+ action='store_false',
+ dest='allow_attached_volume',
+ default=False,
+ help=_(
+ 'Disallow group with attached volumes to be failed over.',
+ )
+ )
+ parser.add_argument(
+ '--secondary-backend-id',
+ metavar='<backend_id>',
+ help=_('Secondary backend ID.'),
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ volume_client = self.app.client_manager.volume
+
+ if volume_client.api_version < api_versions.APIVersion('3.38'):
+ msg = _(
+ "--os-volume-api-version 3.38 or greater is required to "
+ "support the 'volume group failover' command"
+ )
+ raise exceptions.CommandError(msg)
+
+ group = utils.find_resource(
+ volume_client.groups,
+ parsed_args.group,
+ )
+
+ volume_client.groups.failover_replication(
+ group.id,
+ allow_attached_volume=parsed_args.allow_attached_volume,
+ secondary_backend_id=parsed_args.secondary_backend_id,
+ )
diff --git a/releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml b/releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml
new file mode 100644
index 00000000..8b3fe7ec
--- /dev/null
+++ b/releasenotes/notes/add-volume-group-commands-b121d6ec7da9779a.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Add ``volume group create``, ``volume group delete``,
+ ``volume group list``, ``volume group failover``,
+ ``volume group set/unset`` and ``volume attachment show``
+ commands to create, delete, list, failover, update and show volume groups,
+ respectively.
diff --git a/setup.cfg b/setup.cfg
index d593762a..1d031a8f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -714,6 +714,13 @@ openstack.volume.v3 =
volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord
volume_backup_record_import = openstackclient.volume.v2.backup_record:ImportBackupRecord
+ volume_group_create = openstackclient.volume.v3.volume_group:CreateVolumeGroup
+ volume_group_delete = openstackclient.volume.v3.volume_group:DeleteVolumeGroup
+ volume_group_list = openstackclient.volume.v3.volume_group:ListVolumeGroup
+ volume_group_failover = openstackclient.volume.v3.volume_group:FailoverVolumeGroup
+ volume_group_set = openstackclient.volume.v3.volume_group:SetVolumeGroup
+ volume_group_show = openstackclient.volume.v3.volume_group:ShowVolumeGroup
+
volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost
volume_message_delete = openstackclient.volume.v3.volume_message:DeleteMessage