summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2021-06-18 18:05:39 +0000
committerGerrit Code Review <review@openstack.org>2021-06-18 18:05:39 +0000
commit87369984d1afcd4e7bee280621d43b5023f60eda (patch)
tree82c44866f9f5bc6b81d5fd0c5dc23dd5728b162c
parent779c39f6163c6aebb2c6e7523a21f96c9d642044 (diff)
parent4c2e8523a98a0dd33e0203c47e94384420c14f9c (diff)
downloadpython-openstackclient-87369984d1afcd4e7bee280621d43b5023f60eda.tar.gz
Merge "volume: Add 'volume group *' commands"
-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 b3909f3a..a462e0d3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -720,6 +720,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