From ae00a173bd0b019975d4bec662d8a78b7ec77c20 Mon Sep 17 00:00:00 2001 From: Ivan Pchelintsev Date: Thu, 11 Feb 2021 12:54:23 +0300 Subject: [PATCH] Add Consistency Groups support in PowerStore driver Implements: blueprint powerstore-cg-support Change-Id: Ibd5f57ec472ed95a85fd608fce9a1a0090f31afe --- .../test_volume_create_from_source.py | 2 +- .../test_volume_group_create_delete_update.py | 144 +++++++++++ .../test_volume_group_create_from_source.py | 112 +++++++++ ...est_volume_group_snapshot_create_delete.py | 101 ++++++++ .../drivers/dell_emc/powerstore/adapter.py | 233 +++++++++++++++++- .../drivers/dell_emc/powerstore/client.py | 148 ++++++++++- .../drivers/dell_emc/powerstore/driver.py | 29 ++- .../drivers/dell_emc/powerstore/utils.py | 16 ++ .../drivers/dell-emc-powerstore-driver.rst | 19 ++ doc/source/reference/support-matrix.ini | 2 +- ...owerstore-cg-support-ac1842d2041dcbfd.yaml | 4 + 11 files changed, 802 insertions(+), 8 deletions(-) create mode 100644 cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_create_delete_update.py create mode 100644 cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_create_from_source.py create mode 100644 cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_snapshot_create_delete.py create mode 100644 releasenotes/notes/bp-powerstore-cg-support-ac1842d2041dcbfd.yaml diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_create_from_source.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_create_from_source.py index 931ce4ae91f..48fa7d8c6bf 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_create_from_source.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_create_from_source.py @@ -100,7 +100,7 @@ class TestVolumeCreateFromSource(powerstore.TestPowerStoreDriver): @mock.patch("requests.request") @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." "PowerStoreClient.clone_volume_or_snapshot") - def test_create_volume_from_source_extende_bad_status( + def test_create_volume_from_source_extended_bad_status( self, mock_create_from_source, mock_extend_request diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_create_delete_update.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_create_delete_update.py new file mode 100644 index 00000000000..9f123b657b9 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_create_delete_update.py @@ -0,0 +1,144 @@ +# Copyright (c) 2021 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# 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 unittest import mock + +from cinder import exception +from cinder.tests.unit import fake_group +from cinder.tests.unit import fake_volume +from cinder.tests.unit.volume.drivers.dell_emc import powerstore + + +class TestVolumeGroupCreateDeleteUpdate(powerstore.TestPowerStoreDriver): + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_chap_config") + def setUp(self, mock_chap): + super(TestVolumeGroupCreateDeleteUpdate, self).setUp() + self.driver.check_for_setup_error() + self.volume1 = fake_volume.fake_volume_obj( + self.context, + host="host@backend", + provider_id="fake_id", + size=8 + ) + self.volume2 = fake_volume.fake_volume_obj( + self.context, + host="host@backend", + provider_id="fake_id", + size=8 + ) + self.group = fake_group.fake_group_obj( + self.context, + ) + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.create_vg") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_create_volume_group(self, mock_is_cg, mock_create): + mock_create.return_value = "fake_id" + mock_is_cg.return_value = True + self.driver.create_group(self.context, self.group) + + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_create_volume_group_fallback_to_generic(self, mock_is_cg): + mock_is_cg.return_value = False + self.assertRaises(NotImplementedError, + self.driver.create_group, + self.context, + self.group) + + @mock.patch("requests.request") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_create_volume_group_bad_status(self, + mock_is_cg, + mock_create_request): + mock_create_request.return_value = powerstore.MockResponse(rc=400) + error = self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_group, + self.context, + self.group) + self.assertIn("Failed to create PowerStore volume group", error.msg) + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.delete_volume_or_snapshot") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_id_by_name") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_delete_volume_group(self, mock_is_cg, mock_get_id, mock_delete): + self.driver.delete_group(self.context, self.group, []) + + @mock.patch("requests.request") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_id_by_name") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_delete_volume_group_bad_status(self, + mock_is_cg, + mock_get_id, + mock_delete): + mock_delete.return_value = powerstore.MockResponse(rc=400) + error = self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.delete_group, + self.context, + self.group, + [] + ) + self.assertIn("Failed to delete PowerStore volume group", error.msg) + + @mock.patch("cinder.objects.volume.Volume.is_replicated") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_update_volume_group_add_replicated_volumes(self, + mock_is_cg, + mock_replicated): + mock_replicated.return_value = True + self.assertRaises(exception.InvalidVolume, + self.driver.update_group, + self.context, + self.group, + [self.volume1], + []) + + @mock.patch("requests.request") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_id_by_name") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_update_volume_group_add_volumes_bad_status(self, + mock_is_cg, + mock_get_vg_id, + mock_add_volumes): + mock_add_volumes.return_value = powerstore.MockResponse(rc=400) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.update_group, + self.context, + self.group, + [self.volume1], + []) + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.remove_volumes_from_vg") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.add_volumes_to_vg") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_id_by_name") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_update_volume_group_add_remove_volumes(self, + mock_is_cg, + mock_get_vg_id, + mock_add_volumes, + mock_remove_volumes): + self.driver.update_group(self.context, + self.group, + add_volumes=[self.volume1], + remove_volumes=[self.volume2]) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_create_from_source.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_create_from_source.py new file mode 100644 index 00000000000..a96fd27b641 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_create_from_source.py @@ -0,0 +1,112 @@ +# Copyright (c) 2021 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# 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 unittest import mock + +from cinder.tests.unit import fake_group +from cinder.tests.unit import fake_group_snapshot +from cinder.tests.unit import fake_snapshot +from cinder.tests.unit import fake_volume +from cinder.tests.unit.volume.drivers.dell_emc import powerstore + + +class TestVolumeGroupCreateFromSource(powerstore.TestPowerStoreDriver): + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_chap_config") + def setUp(self, mock_chap): + super(TestVolumeGroupCreateFromSource, self).setUp() + self.driver.check_for_setup_error() + self.volume = fake_volume.fake_volume_obj( + self.context, + host="host@backend", + provider_id="fake_id", + size=8 + ) + self.source_volume = fake_volume.fake_volume_obj( + self.context, + host="host@backend", + provider_id="fake_id", + size=8 + ) + self.source_volume_snap = fake_snapshot.fake_snapshot_obj( + self.context, + volume=self.source_volume, + volume_size=8 + ) + self.group = fake_group.fake_group_obj( + self.context, + ) + self.source_group = fake_group.fake_group_obj( + self.context, + ) + self.source_group_snap = fake_group_snapshot.fake_group_snapshot_obj( + self.context + ) + self.source_group_snap.group = self.source_group + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.rename_volume") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_volume_id_by_name") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.clone_vg_or_vg_snapshot") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_id_by_name") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_create_volume_group_clone(self, + mock_is_cg, + mock_get_group_id, + mock_clone, + mock_get_volume_id, + mock_rename): + mock_get_volume_id.return_value = "fake_id" + group_updates, volume_updates = self.driver.create_group_from_src( + self.context, + self.group, + volumes=[self.volume], + source_group=self.source_group, + source_vols=[self.source_volume] + ) + self.assertEqual(1, len(volume_updates)) + self.assertEqual("fake_id", volume_updates[0]["provider_id"]) + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.rename_volume") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_volume_id_by_name") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.clone_vg_or_vg_snapshot") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_snapshot_id_by_name") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_id_by_name") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_create_volume_group_from_snapshot(self, + mock_is_cg, + mock_get_group_id, + mock_get_snapshot_id, + mock_clone, + mock_get_volume_id, + mock_rename): + mock_get_volume_id.return_value = "fake_id" + group_updates, volume_updates = self.driver.create_group_from_src( + self.context, + self.group, + volumes=[self.volume], + snapshots=[self.source_volume_snap], + group_snapshot=self.source_group_snap + ) + self.assertEqual(1, len(volume_updates)) + self.assertEqual("fake_id", volume_updates[0]["provider_id"]) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_snapshot_create_delete.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_snapshot_create_delete.py new file mode 100644 index 00000000000..f02091e4d19 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_group_snapshot_create_delete.py @@ -0,0 +1,101 @@ +# Copyright (c) 2021 Dell Inc. or its subsidiaries. +# All Rights Reserved. +# +# 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 unittest import mock + +from cinder import exception +from cinder.tests.unit import fake_group +from cinder.tests.unit import fake_group_snapshot +from cinder.tests.unit.volume.drivers.dell_emc import powerstore + + +class TestVolumeGroupSnapshotCreateDelete(powerstore.TestPowerStoreDriver): + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_chap_config") + def setUp(self, mock_chap): + super(TestVolumeGroupSnapshotCreateDelete, self).setUp() + self.driver.check_for_setup_error() + self.group = fake_group.fake_group_obj( + self.context, + ) + self.group_snapshot = fake_group_snapshot.fake_group_snapshot_obj( + self.context + ) + self.group_snapshot.group = self.group + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.create_vg_snapshot") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_id_by_name") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_create_volume_group_snapshot(self, + mock_is_cg, + mock_get_id, + mock_create): + self.driver.create_group_snapshot(self.context, + self.group_snapshot, + []) + + @mock.patch("requests.request") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_id_by_name") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_create_volume_group_snapshot_bad_status(self, + mock_is_cg, + mock_get_id, + mock_create): + mock_create.return_value = powerstore.MockResponse(rc=400) + error = self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_group_snapshot, + self.context, + self.group_snapshot, + []) + self.assertIn("Failed to create snapshot", error.msg) + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.delete_volume_or_snapshot") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_snapshot_id_by_name") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_id_by_name") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_delete_volume_group_snapshot(self, + mock_is_cg, + mock_get_group_id, + mock_get_snapshot_id, + mock_delete): + self.driver.delete_group_snapshot(self.context, + self.group_snapshot, + []) + + @mock.patch("requests.request") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_snapshot_id_by_name") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_vg_id_by_name") + @mock.patch("cinder.volume.volume_utils.is_group_a_cg_snapshot_type") + def test_delete_volume_group_snapshot_bad_status(self, + mock_is_cg, + mock_get_group_id, + mock_get_snapshot_id, + mock_delete): + mock_delete.return_value = powerstore.MockResponse(rc=400) + error = self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_group_snapshot, + self.context, + self.group_snapshot, + []) + self.assertIn("Failed to delete PowerStore volume group snapshot", + error.msg) diff --git a/cinder/volume/drivers/dell_emc/powerstore/adapter.py b/cinder/volume/drivers/dell_emc/powerstore/adapter.py index 9341dc103c3..343e12a2ea3 100644 --- a/cinder/volume/drivers/dell_emc/powerstore/adapter.py +++ b/cinder/volume/drivers/dell_emc/powerstore/adapter.py @@ -22,10 +22,12 @@ from cinder import coordination from cinder import exception from cinder.i18n import _ from cinder.objects import fields +from cinder.objects.group_snapshot import GroupSnapshot from cinder.objects.snapshot import Snapshot from cinder.volume.drivers.dell_emc.powerstore import client from cinder.volume.drivers.dell_emc.powerstore import utils from cinder.volume import manager +from cinder.volume import volume_utils LOG = logging.getLogger(__name__) @@ -88,6 +90,19 @@ class CommonAdapter(object): }) def create_volume(self, volume): + group_provider_id = None + if ( + volume.group_id and + volume_utils.is_group_a_cg_snapshot_type(volume.group) + ): + if volume.is_replicated(): + msg = _("Volume with enabled replication can not be added to " + "PowerStore volume group.") + LOG.error(msg) + raise exception.InvalidVolume(reason=msg) + group_provider_id = self.client.get_vg_id_by_name( + volume.group_id + ) if volume.is_replicated(): pp_name = utils.get_protection_policy_from_volume(volume) pp_id = self.client.get_protection_policy_id_by_name(pp_name) @@ -98,26 +113,31 @@ class CommonAdapter(object): replication_status = fields.ReplicationStatus.DISABLED LOG.debug("Create PowerStore volume %(volume_name)s of size " "%(volume_size)s GiB with id %(volume_id)s. " - "Protection policy: %(pp_name)s.", + "Protection policy: %(pp_name)s. " + "Volume group id: %(group_id)s. ", { "volume_name": volume.name, "volume_size": volume.size, "volume_id": volume.id, "pp_name": pp_name, + "group_id": volume.group_id, }) size_in_bytes = utils.gib_to_bytes(volume.size) provider_id = self.client.create_volume(volume.name, size_in_bytes, - pp_id) + pp_id, + group_provider_id) LOG.debug("Successfully created PowerStore volume %(volume_name)s of " "size %(volume_size)s GiB with id %(volume_id)s on " - "Protection policy: %(pp_name)s." + "Protection policy: %(pp_name)s. " + "Volume group id: %(group_id)s. " "PowerStore volume id: %(volume_provider_id)s.", { "volume_name": volume.name, "volume_size": volume.size, "volume_id": volume.id, "pp_name": pp_name, + "group_id": volume.group_id, "volume_provider_id": provider_id, }) return { @@ -273,6 +293,7 @@ class CommonAdapter(object): "thin_provisioning_support": True, "compression_support": True, "multiattach": True, + "consistent_group_snapshot_enabled": True, } backend_stats = self.client.get_metrics() backend_total_capacity = utils.bytes_to_gib( @@ -787,6 +808,212 @@ class CommonAdapter(object): }, } + @utils.is_group_a_cg_snapshot_type + def create_group(self, group): + LOG.debug("Create PowerStore volume group %(group_name)s with id " + "%(group_id)s.", + { + "group_name": group.name, + "group_id": group.id, + }) + self.client.create_vg(group.id) + LOG.debug("Successfully created PowerStore volume group " + "%(group_name)s with id %(group_id)s.", + { + "group_name": group.name, + "group_id": group.id, + }) + + @utils.is_group_a_cg_snapshot_type + def delete_group(self, group): + LOG.debug("Delete PowerStore volume group %(group_name)s with id " + "%(group_id)s.", + { + "group_name": group.name, + "group_id": group.id, + }) + try: + group_provider_id = self.client.get_vg_id_by_name( + group.id + ) + except exception.VolumeBackendAPIException: + return None, None + self.client.delete_volume_or_snapshot(group_provider_id, + entity="volume group") + LOG.debug("Successfully deleted PowerStore volume group " + "%(group_name)s with id %(group_id)s.", + { + "group_name": group.name, + "group_id": group.id, + }) + return None, None + + @utils.is_group_a_cg_snapshot_type + def update_group(self, group, add_volumes, remove_volumes): + volumes_to_add = [] + for volume in add_volumes: + if volume.is_replicated(): + msg = _("Volume with enabled replication can not be added to " + "PowerStore volume group.") + LOG.error(msg) + raise exception.InvalidVolume(reason=msg) + volumes_to_add.append(self._get_volume_provider_id(volume)) + volumes_to_remove = [ + self._get_volume_provider_id(volume) for volume in remove_volumes + ] + LOG.debug("Update PowerStore volume group %(group_name)s with id " + "%(group_id)s. Add PowerStore volumes with ids: " + "%(volumes_to_add)s, remove PowerStore volumes with ids: " + "%(volumes_to_remove)s.", + { + "group_name": group.name, + "group_id": group.id, + "volumes_to_add": volumes_to_add, + "volumes_to_remove": volumes_to_remove, + }) + group_provider_id = self.client.get_vg_id_by_name(group.id) + if volumes_to_add: + self.client.add_volumes_to_vg(group_provider_id, + volumes_to_add) + if volumes_to_remove: + self.client.remove_volumes_from_vg(group_provider_id, + volumes_to_remove) + LOG.debug("Successfully updated PowerStore volume group " + "%(group_name)s with id %(group_id)s. " + "Add PowerStore volumes with ids: %(volumes_to_add)s, " + "remove PowerStore volumes with ids: %(volumes_to_remove)s.", + { + "group_name": group.name, + "group_id": group.id, + "volumes_to_add": volumes_to_add, + "volumes_to_remove": volumes_to_remove, + }) + return None, None, None + + @utils.is_group_a_cg_snapshot_type + def create_group_snapshot(self, group_snapshot): + LOG.debug("Create PowerStore snapshot %(snapshot_name)s with id " + "%(snapshot_id)s of volume group %(group_name)s with id " + "%(group_id)s.", + { + "snapshot_name": group_snapshot.name, + "snapshot_id": group_snapshot.id, + "group_name": group_snapshot.group.name, + "group_id": group_snapshot.group.id, + }) + group_provider_id = self.client.get_vg_id_by_name( + group_snapshot.group.id + ) + self.client.create_vg_snapshot( + group_provider_id, + group_snapshot.id + ) + LOG.debug("Successfully created PowerStore snapshot %(snapshot_name)s " + "with id %(snapshot_id)s of volume group %(group_name)s " + "with id %(group_id)s.", + { + "snapshot_name": group_snapshot.name, + "snapshot_id": group_snapshot.id, + "group_name": group_snapshot.group.name, + "group_id": group_snapshot.group.id, + }) + return None, None + + @utils.is_group_a_cg_snapshot_type + def delete_group_snapshot(self, group_snapshot): + LOG.debug("Delete PowerStore snapshot %(snapshot_name)s with id " + "%(snapshot_id)s of volume group %(group_name)s with " + "id %(group_id)s.", + { + "snapshot_name": group_snapshot.name, + "snapshot_id": group_snapshot.id, + "group_name": group_snapshot.group.name, + "group_id": group_snapshot.group.id, + }) + try: + group_provider_id = self.client.get_vg_id_by_name( + group_snapshot.group.id + ) + group_snapshot_provider_id = ( + self.client.get_vg_snapshot_id_by_name( + group_provider_id, + group_snapshot.id + )) + except exception.VolumeBackendAPIException: + return None, None + self.client.delete_volume_or_snapshot(group_snapshot_provider_id, + entity="volume group snapshot") + LOG.debug("Successfully deleted PowerStore snapshot %(snapshot_name)s " + "with id %(snapshot_id)s of volume group %(group_name)s " + "with id %(group_id)s.", + { + "snapshot_name": group_snapshot.name, + "snapshot_id": group_snapshot.id, + "group_name": group_snapshot.group.name, + "group_id": group_snapshot.group.id, + }) + return None, None + + @utils.is_group_a_cg_snapshot_type + def create_group_from_source(self, + group, + volumes, + source, + snapshots, + source_vols): + if isinstance(source, GroupSnapshot): + entity = "volume group snapshot" + group_provider_id = self.client.get_vg_id_by_name( + source.group.id + ) + source_provider_id = self.client.get_vg_snapshot_id_by_name( + group_provider_id, + source.id + ) + source_vols = [snapshot.volume for snapshot in snapshots] + base_clone_name = "%s.%s" % (group.id, source.id) + else: + entity = "volume group" + source_provider_id = self.client.get_vg_id_by_name(source.id) + base_clone_name = group.id + LOG.debug("Create PowerStore volume group %(group_name)s with id " + "%(group_id)s from %(entity)s %(entity_name)s with id " + "%(entity_id)s.", + { + "group_name": group.name, + "group_id": group.id, + "entity": entity, + "entity_name": source.name, + "entity_id": source.id, + }) + self.client.clone_vg_or_vg_snapshot( + group.id, + source_provider_id, + entity + ) + LOG.debug("Successfully created PowerStore volume group " + "%(group_name)s with id %(group_id)s from %(entity)s " + "%(entity_name)s with id %(entity_id)s.", + { + "group_name": group.name, + "group_id": group.id, + "entity": entity, + "entity_name": source.name, + "entity_id": source.id, + }) + updates = [] + for volume, source_vol in zip(volumes, source_vols): + volume_name = "%s.%s" % (base_clone_name, source_vol.name) + volume_provider_id = self.client.get_volume_id_by_name(volume_name) + self.client.rename_volume(volume_provider_id, volume.name) + volume_updates = { + "id": volume.id, + "provider_id": volume_provider_id, + "replication_status": group.replication_status, + } + updates.append(volume_updates) + return None, updates + class FibreChannelAdapter(CommonAdapter): def __init__(self, **kwargs): diff --git a/cinder/volume/drivers/dell_emc/powerstore/client.py b/cinder/volume/drivers/dell_emc/powerstore/client.py index bc0d668036d..5b830f42fe2 100644 --- a/cinder/volume/drivers/dell_emc/powerstore/client.py +++ b/cinder/volume/drivers/dell_emc/powerstore/client.py @@ -158,13 +158,14 @@ class PowerStoreClient(object): LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) - def create_volume(self, name, size, pp_id): + def create_volume(self, name, size, pp_id, group_id): r, response = self._send_post_request( "/volume", payload={ "name": name, "size": size, "protection_policy_id": pp_id, + "volume_group_id": group_id, } ) if r.status_code not in self.ok_codes: @@ -174,7 +175,15 @@ class PowerStoreClient(object): return response["id"] def delete_volume_or_snapshot(self, entity_id, entity="volume"): - r, response = self._send_delete_request("/volume/%s" % entity_id) + if entity in ["volume group", "volume group snapshot"]: + r, response = self._send_delete_request( + "/volume_group/%s" % entity_id, + payload={ + "delete_members": True, + }, + ) + else: + r, response = self._send_delete_request("/volume/%s" % entity_id) if r.status_code not in self.ok_codes: if r.status_code == requests.codes.not_found: LOG.warning("PowerStore %(entity)s with id %(entity_id)s is " @@ -599,3 +608,138 @@ class PowerStoreClient(object): "with id %s.") % rep_session_id) LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) + + def create_vg(self, name): + r, response = self._send_post_request( + "/volume_group", + payload={ + "name": name, + } + ) + if r.status_code not in self.ok_codes: + msg = _("Failed to create PowerStore volume group %s.") % name + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + return response["id"] + + def get_vg_id_by_name(self, name): + r, response = self._send_get_request( + "/volume_group", + params={ + "name": "eq.%s" % name, + } + ) + if r.status_code not in self.ok_codes: + msg = _("Failed to query PowerStore volume groups.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + try: + group_id = response[0].get("id") + return group_id + except IndexError: + msg = _("PowerStore volume group %s is not found.") % name + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def add_volumes_to_vg(self, group_id, volume_ids): + r, response = self._send_post_request( + "/volume_group/%s/add_members" % group_id, + payload={ + "volume_ids": volume_ids, + } + ) + if r.status_code not in self.ok_codes: + msg = (_("Failed to add volumes to PowerStore volume group " + "with id %(group_id)s. Volumes: %(volume_ids)s.") + % {"group_id": group_id, + "volume_ids": volume_ids, }) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def remove_volumes_from_vg(self, group_id, volume_ids): + r, response = self._send_post_request( + "/volume_group/%s/remove_members" % group_id, + payload={ + "volume_ids": volume_ids, + } + ) + if r.status_code not in self.ok_codes: + msg = (_("Failed to remove volumes from PowerStore volume group " + "with id %(group_id)s. Volumes: %(volume_ids)s.") + % {"group_id": group_id, + "volume_ids": volume_ids, }) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def create_vg_snapshot(self, group_id, name): + r, response = self._send_post_request( + "/volume_group/%s/snapshot" % group_id, + payload={ + "name": name, + } + ) + if r.status_code not in self.ok_codes: + msg = (_("Failed to create snapshot %(snapshot_name)s for " + "PowerStore volume group with id %(group_id)s.") + % {"snapshot_name": name, + "group_id": group_id, }) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + return response["id"] + + def get_vg_snapshot_id_by_name(self, group_id, name): + r, response = self._send_get_request( + "/volume_group", + params={ + "name": "eq.%s" % name, + "protection_data->>source_id": "eq.%s" % group_id, + "type": "eq.Snapshot", + } + ) + if r.status_code not in self.ok_codes: + msg = _("Failed to query PowerStore volume groups snapshots.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + try: + vg_snap_id = response[0].get("id") + return vg_snap_id + except IndexError: + msg = (_("PowerStore snapshot %(snapshot_name)s for volume group" + "with id %(group_id)s is not found.") + % {"snapshot_name": name, + "group_id": group_id, }) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def clone_vg_or_vg_snapshot(self, + name, + entity_id, + entity="volume group"): + r, response = self._send_post_request( + "/volume_group/%s/clone" % entity_id, + payload={ + "name": name, + } + ) + if r.status_code not in self.ok_codes: + msg = (_("Failed to create clone %(clone_name)s for " + "PowerStore %(entity)s with id %(entity_id)s.") + % {"clone_name": name, + "entity": entity, + "entity_id": entity_id, }) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + return response["id"] + + def rename_volume(self, volume_id, name): + r, response = self._send_patch_request( + "/volume/%s" % volume_id, + payload={ + "name": name, + } + ) + if r.status_code not in self.ok_codes: + msg = (_("Failed to rename PowerStore volume with id %s.") + % volume_id) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) diff --git a/cinder/volume/drivers/dell_emc/powerstore/driver.py b/cinder/volume/drivers/dell_emc/powerstore/driver.py index b3407cc9c7d..cf787351b24 100644 --- a/cinder/volume/drivers/dell_emc/powerstore/driver.py +++ b/cinder/volume/drivers/dell_emc/powerstore/driver.py @@ -46,9 +46,10 @@ class PowerStoreDriver(driver.VolumeDriver): 1.0.0 - Initial version 1.0.1 - Add CHAP support 1.1.0 - Add volume replication v2.1 support + 1.1.1 - Add Consistency Groups support """ - VERSION = "1.1.0" + VERSION = "1.1.1" VENDOR = "Dell EMC" # ThirdPartySystems wiki page @@ -187,6 +188,32 @@ class PowerStoreDriver(driver.VolumeDriver): ) return secondary_id, volumes_updates, groups_updates + def create_group(self, context, group): + return self.adapter.create_group(group) + + def delete_group(self, context, group, volumes): + return self.adapter.delete_group(group) + + def update_group(self, context, group, + add_volumes=None, remove_volumes=None): + return self.adapter.update_group(group, add_volumes, remove_volumes) + + def create_group_snapshot(self, context, group_snapshot, snapshots): + return self.adapter.create_group_snapshot(group_snapshot) + + def delete_group_snapshot(self, context, group_snapshot, snapshots): + return self.adapter.delete_group_snapshot(group_snapshot) + + def create_group_from_src(self, context, group, volumes, + group_snapshot=None, snapshots=None, + source_group=None, source_vols=None): + source = group_snapshot or source_group + return self.adapter.create_group_from_source(group, + volumes, + source, + snapshots, + source_vols) + @property def adapter(self): return self.adapters.get(self.active_backend_id) diff --git a/cinder/volume/drivers/dell_emc/powerstore/utils.py b/cinder/volume/drivers/dell_emc/powerstore/utils.py index 86a12ab9250..d9e7e1b4b78 100644 --- a/cinder/volume/drivers/dell_emc/powerstore/utils.py +++ b/cinder/volume/drivers/dell_emc/powerstore/utils.py @@ -15,6 +15,7 @@ """Utilities for Dell EMC PowerStore Cinder driver.""" +import functools import re from oslo_log import log as logging @@ -162,3 +163,18 @@ def get_protection_policy_from_volume(volume): """ return volume.volume_type.extra_specs.get(driver.POWERSTORE_PP_KEY) + + +def is_group_a_cg_snapshot_type(func): + """Check if group is a consistent snapshot group. + + Fallback to generic volume group implementation if consistent group + snapshot is not enabled. + """ + + @functools.wraps(func) + def inner(self, *args, **kwargs): + if not volume_utils.is_group_a_cg_snapshot_type(args[0]): + raise NotImplementedError + return func(self, *args, **kwargs) + return inner diff --git a/doc/source/configuration/block-storage/drivers/dell-emc-powerstore-driver.rst b/doc/source/configuration/block-storage/drivers/dell-emc-powerstore-driver.rst index e4bfbc8d76c..21e48289997 100644 --- a/doc/source/configuration/block-storage/drivers/dell-emc-powerstore-driver.rst +++ b/doc/source/configuration/block-storage/drivers/dell-emc-powerstore-driver.rst @@ -19,6 +19,10 @@ Supported operations - Attach a volume to multiple servers simultaneously (multiattach). - Revert a volume to a snapshot. - OpenStack replication v2.1 support. +- Create, delete, update Consistency Groups. +- Create, delete Consistency Groups snapshots. +- Clone a Consistency Group. +- Create a Consistency Group from a Consistency Group snapshot. Driver configuration ~~~~~~~~~~~~~~~~~~~~ @@ -161,3 +165,18 @@ failback operation using ``--backend_id default``: .. code-block:: console $ cinder failover-host cinder_host@powerstore --backend_id default + +Consistency Groups support +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To use PowerStore Volume Groups create Group Type with consistent group +snapshot enabled. + +.. code-block:: console + + $ cinder --os-volume-api-version 3.11 group-type-create powerstore_vg + $ cinder --os-volume-api-version 3.11 group-type-key powerstore_vg set consistent_group_snapshot_enabled=" True" + +.. note:: Currently driver does not support Consistency Groups replication. + Adding volume to Consistency Group and creating volume in Consistency Group + will fail if volume is replicated. diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index 412d0c1bd01..3b746e90b01 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -553,7 +553,7 @@ notes=Vendor drivers that support consistency groups are able to creation of consistent snapshots across a group. driver.datera=missing driver.dell_emc_powermax=complete -driver.dell_emc_powerstore=missing +driver.dell_emc_powerstore=complete driver.dell_emc_powervault=missing driver.dell_emc_sc=complete driver.dell_emc_unity=complete diff --git a/releasenotes/notes/bp-powerstore-cg-support-ac1842d2041dcbfd.yaml b/releasenotes/notes/bp-powerstore-cg-support-ac1842d2041dcbfd.yaml new file mode 100644 index 00000000000..de9436ca942 --- /dev/null +++ b/releasenotes/notes/bp-powerstore-cg-support-ac1842d2041dcbfd.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + PowerStore driver: Add Consistency Groups support. \ No newline at end of file