From f328341ed0eb920a6471f7c65e06b27c3f719fbe Mon Sep 17 00:00:00 2001 From: Ivan Pchelintsev Date: Mon, 25 Jan 2021 11:21:15 +0300 Subject: [PATCH] Add OpenStack volume replication v2.1 support in PowerStore driver Cinder driver for PowerStore supports volumes/snapshots with replication enabled according to OpenStack volume replication specification. Implements: blueprint powerstore-replication-support Change-Id: I94d089374dee76d401dc6cf83a9c594779e7eb3e --- .../drivers/dell_emc/powerstore/__init__.py | 4 +- .../drivers/dell_emc/powerstore/test_base.py | 91 ++-- .../dell_emc/powerstore/test_replication.py | 121 +++++ .../test_snapshot_create_delete_revert.py | 16 +- .../powerstore/test_volume_attach_detach.py | 23 +- .../test_volume_create_delete_extend.py | 9 +- .../test_volume_create_from_source.py | 24 +- .../drivers/dell_emc/powerstore/adapter.py | 478 ++++++++++-------- .../drivers/dell_emc/powerstore/client.py | 269 +++++++--- .../drivers/dell_emc/powerstore/driver.py | 140 ++++- .../drivers/dell_emc/powerstore/options.py | 7 +- .../drivers/dell_emc/powerstore/utils.py | 11 + .../drivers/dell-emc-powerstore-driver.rst | 72 ++- doc/source/reference/support-matrix.ini | 4 +- ...-replication-support-700016b83437602e.yaml | 10 + 15 files changed, 903 insertions(+), 376 deletions(-) create mode 100644 cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_replication.py create mode 100644 releasenotes/notes/bp-powerstore-replication-support-700016b83437602e.yaml diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/__init__.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/__init__.py index 7cb429812c7..66e6c194a2a 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/__init__.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/__init__.py @@ -18,6 +18,7 @@ from unittest import mock import requests +from cinder import context from cinder.tests.unit import test from cinder.volume import configuration from cinder.volume.drivers.dell_emc.powerstore import driver @@ -51,6 +52,7 @@ class MockResponse(requests.Response): class TestPowerStoreDriver(test.TestCase): def setUp(self): super(TestPowerStoreDriver, self).setUp() + self.context = context.RequestContext('fake', 'fake', auth_token=True) self.configuration = configuration.Configuration( options.POWERSTORE_OPTS, configuration.SHARED_CONF_GROUP @@ -76,5 +78,3 @@ class TestPowerStoreDriver(test.TestCase): self._override_shared_conf("san_ip", override="127.0.0.1") self._override_shared_conf("san_login", override="test") self._override_shared_conf("san_password", override="test") - self._override_shared_conf("powerstore_appliances", - override="test-appliance") diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_base.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_base.py index c42dce70d83..c2efbcf81c2 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_base.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_base.py @@ -22,47 +22,21 @@ from cinder.tests.unit.volume.drivers.dell_emc import powerstore class TestBase(powerstore.TestPowerStoreDriver): @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." "PowerStoreClient.get_chap_config") - @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." - "PowerStoreClient.get_appliance_id_by_name") - def test_configuration(self, mock_appliance, mock_chap): - mock_appliance.return_value = "A1" + def test_configuration(self, mock_chap): self.driver.check_for_setup_error() def test_configuration_rest_parameters_not_set(self): self.driver.adapter.client.rest_ip = None - self.assertRaises(exception.VolumeBackendAPIException, + self.assertRaises(exception.InvalidInput, self.driver.check_for_setup_error) - def test_configuration_appliances_not_set(self): - self.driver.adapter.appliances = {} - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - - @mock.patch("requests.request") - def test_configuration_appliance_not_found(self, mock_get_request): - mock_get_request.return_value = powerstore.MockResponse() - error = self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - self.assertIn("not found", error.msg) - - @mock.patch("requests.request") - def test_configuration_appliance_bad_status(self, mock_get_request): - mock_get_request.return_value = powerstore.MockResponse(rc=400) - error = self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - self.assertIn("Failed to query PowerStore appliances.", error.msg) - @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." "PowerStoreClient.get_chap_config") @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." - "PowerStoreClient.get_appliance_id_by_name") - @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." - "PowerStoreClient.get_appliance_metrics") + "PowerStoreClient.get_metrics") def test_update_volume_stats(self, mock_metrics, - mock_appliance, mock_chap): - mock_appliance.return_value = "A1" mock_metrics.return_value = { "physical_total": 2147483648, "physical_used": 1073741824, @@ -72,16 +46,65 @@ class TestBase(powerstore.TestPowerStoreDriver): @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." "PowerStoreClient.get_chap_config") - @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." - "PowerStoreClient.get_appliance_id_by_name") @mock.patch("requests.request") def test_update_volume_stats_bad_status(self, mock_metrics, - mock_appliance, mock_chap): - mock_appliance.return_value = "A1" mock_metrics.return_value = powerstore.MockResponse(rc=400) self.driver.check_for_setup_error() error = self.assertRaises(exception.VolumeBackendAPIException, self.driver._update_volume_stats) - self.assertIn("Failed to query metrics", error.msg) + self.assertIn("Failed to query PowerStore metrics", error.msg) + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_chap_config") + def test_configuration_with_replication(self, mock_chap): + replication_device = [ + { + "backend_id": "repl_1", + "san_ip": "127.0.0.2", + "san_login": "test_1", + "san_password": "test_2" + } + ] + self._override_shared_conf("replication_device", + override=replication_device) + self.driver.do_setup({}) + self.driver.check_for_setup_error() + self.assertEqual(2, len(self.driver.adapters)) + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_chap_config") + def test_configuration_with_replication_2_rep_devices(self, mock_chap): + device = { + "backend_id": "repl_1", + "san_ip": "127.0.0.2", + "san_login": "test_1", + "san_password": "test_2" + } + replication_device = [device] * 2 + self._override_shared_conf("replication_device", + override=replication_device) + self.driver.do_setup({}) + error = self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + self.assertIn("PowerStore driver does not support more than one " + "replication device.", error.msg) + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_chap_config") + def test_configuration_with_replication_failed_over(self, mock_chap): + replication_device = [ + { + "backend_id": "repl_1", + "san_ip": "127.0.0.2", + "san_login": "test_1", + "san_password": "test_2" + } + ] + self._override_shared_conf("replication_device", + override=replication_device) + self.driver.do_setup({}) + self.driver.check_for_setup_error() + self.driver.active_backend_id = "repl_1" + self.assertFalse(self.driver.replication_enabled) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_replication.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_replication.py new file mode 100644 index 00000000000..e805d641b1a --- /dev/null +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_replication.py @@ -0,0 +1,121 @@ +# 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.objects import fields +from cinder.tests.unit import fake_volume +from cinder.tests.unit.volume.drivers.dell_emc import powerstore +from cinder.volume.drivers.dell_emc.powerstore import client + + +class TestReplication(powerstore.TestPowerStoreDriver): + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_chap_config") + def setUp(self, mock_chap): + super(TestReplication, self).setUp() + self.replication_backend_id = "repl_1" + replication_device = [ + { + "backend_id": self.replication_backend_id, + "san_ip": "127.0.0.2", + "san_login": "test_1", + "san_password": "test_2" + } + ] + self._override_shared_conf("replication_device", + override=replication_device) + self.driver.do_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, + replication_status="enabled" + ) + + def test_failover_host_no_volumes(self): + self.driver.failover_host({}, [], self.replication_backend_id) + self.assertEqual(self.replication_backend_id, + self.driver.active_backend_id) + + def test_failover_host_invalid_secondary_id(self): + error = self.assertRaises(exception.InvalidReplicationTarget, + self.driver.failover_host, + {}, [], "invalid_id") + self.assertIn("is not a valid choice", error.msg) + + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.wait_for_failover_completion") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.failover_volume_replication_session") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_volume_replication_session_id") + def test_failover_volume(self, + mock_rep_session, + mock_failover, + mock_wait_failover): + updates = self.driver.adapter.failover_volume(self.volume, + is_failback=False) + self.assertIsNone(updates) + + @mock.patch("requests.request") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.failover_volume_replication_session") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_volume_replication_session_id") + def test_failover_volume_already_failed_over(self, + mock_rep_session, + mock_failover, + mock_wait_failover): + mock_wait_failover.return_value = powerstore.MockResponse( + content={ + "response_body": { + "messages": [ + { + "code": client.SESSION_ALREADY_FAILED_OVER_ERROR, + }, + ], + }, + }, + rc=200 + ) + updates = self.driver.adapter.failover_volume(self.volume, + is_failback=False) + self.assertIsNone(updates) + + @mock.patch("requests.request") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.failover_volume_replication_session") + @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." + "PowerStoreClient.get_volume_replication_session_id") + def test_failover_volume_failover_error(self, + mock_rep_session, + mock_failover, + mock_wait_failover): + mock_wait_failover.return_value = powerstore.MockResponse( + content={ + "state": "FAILED", + "response_body": None, + }, + rc=200 + ) + updates = self.driver.adapter.failover_volume(self.volume, + is_failback=False) + self.assertEqual(self.volume.id, updates["volume_id"]) + self.assertEqual(fields.ReplicationStatus.FAILOVER_ERROR, + updates["updates"]["replication_status"]) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_snapshot_create_delete_revert.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_snapshot_create_delete_revert.py index 4dc44aed6a5..9e9dae4034f 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_snapshot_create_delete_revert.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_snapshot_create_delete_revert.py @@ -24,28 +24,26 @@ from cinder.tests.unit.volume.drivers.dell_emc import powerstore class TestSnapshotCreateDelete(powerstore.TestPowerStoreDriver): @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." "PowerStoreClient.get_chap_config") - @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." - "PowerStoreClient.get_appliance_id_by_name") - def setUp(self, mock_appliance, mock_chap): + def setUp(self, mock_chap): super(TestSnapshotCreateDelete, self).setUp() - mock_appliance.return_value = "A1" self.driver.check_for_setup_error() self.volume = fake_volume.fake_volume_obj( - {}, - host="host@backend#test-appliance", + self.context, + host="host@backend", provider_id="fake_id", size=8 ) self.snapshot = fake_snapshot.fake_snapshot_obj( - {}, - provider_id="fake_id_1", + self.context, volume=self.volume ) + self.mock_object(self.driver.adapter.client, + "get_snapshot_id_by_name", + return_value="fake_id_1") @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." "PowerStoreClient.create_snapshot") def test_create_snapshot(self, mock_create): - mock_create.return_value = self.snapshot.provider_id self.driver.create_snapshot(self.snapshot) @mock.patch("requests.request") diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_attach_detach.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_attach_detach.py index dd5a80eec62..92364adeecd 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_attach_detach.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_attach_detach.py @@ -26,17 +26,14 @@ from cinder.volume.drivers.dell_emc.powerstore import utils class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver): @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." "PowerStoreClient.get_chap_config") - @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." - "PowerStoreClient.get_appliance_id_by_name") - def setUp(self, mock_appliance, mock_chap): + def setUp(self, mock_chap): super(TestVolumeAttachDetach, self).setUp() - mock_appliance.return_value = "A1" mock_chap.return_value = {"mode": "Single"} self.iscsi_driver.check_for_setup_error() self.fc_driver.check_for_setup_error() self.volume = fake_volume.fake_volume_obj( - {}, - host="host@backend#test-appliance", + self.context, + host="host@backend", provider_id="fake_id", size=8 ) @@ -124,12 +121,12 @@ class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver): self.assertNotIn("auth_password", connection_properties["data"]) def test_get_fc_targets(self): - wwns = self.fc_driver.adapter._get_fc_targets("A1") + wwns = self.fc_driver.adapter._get_fc_targets() self.assertEqual(2, len(wwns)) def test_get_fc_targets_filtered(self): self.fc_driver.adapter.allowed_ports = ["58:cc:f0:98:49:23:07:02"] - wwns = self.fc_driver.adapter._get_fc_targets("A1") + wwns = self.fc_driver.adapter._get_fc_targets() self.assertEqual(1, len(wwns)) self.assertFalse( utils.fc_wwn_to_string("58:cc:f0:98:49:21:07:02") in wwns @@ -138,19 +135,18 @@ class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver): def test_get_fc_targets_filtered_no_matched_ports(self): self.fc_driver.adapter.allowed_ports = ["fc_wwn_1", "fc_wwn_2"] error = self.assertRaises(exception.VolumeBackendAPIException, - self.fc_driver.adapter._get_fc_targets, - "A1") + self.fc_driver.adapter._get_fc_targets) self.assertIn("There are no accessible Fibre Channel targets on the " "system.", error.msg) def test_get_iscsi_targets(self): - iqns, portals = self.iscsi_driver.adapter._get_iscsi_targets("A1") + iqns, portals = self.iscsi_driver.adapter._get_iscsi_targets() self.assertTrue(len(iqns) == len(portals)) self.assertEqual(2, len(portals)) def test_get_iscsi_targets_filtered(self): self.iscsi_driver.adapter.allowed_ports = ["1.2.3.4"] - iqns, portals = self.iscsi_driver.adapter._get_iscsi_targets("A1") + iqns, portals = self.iscsi_driver.adapter._get_iscsi_targets() self.assertTrue(len(iqns) == len(portals)) self.assertEqual(1, len(portals)) self.assertFalse( @@ -160,8 +156,7 @@ class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver): def test_get_iscsi_targets_filtered_no_matched_ports(self): self.iscsi_driver.adapter.allowed_ports = ["1.1.1.1", "2.2.2.2"] error = self.assertRaises(exception.VolumeBackendAPIException, - self.iscsi_driver.adapter._get_iscsi_targets, - "A1") + self.iscsi_driver.adapter._get_iscsi_targets) self.assertIn("There are no accessible iSCSI targets on the system.", error.msg) diff --git a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_create_delete_extend.py b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_create_delete_extend.py index 16ac1a3fc07..90ecc4b1632 100644 --- a/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_create_delete_extend.py +++ b/cinder/tests/unit/volume/drivers/dell_emc/powerstore/test_volume_create_delete_extend.py @@ -24,15 +24,12 @@ from cinder.volume.drivers.dell_emc.powerstore import client class TestVolumeCreateDeleteExtend(powerstore.TestPowerStoreDriver): @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." "PowerStoreClient.get_chap_config") - @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." - "PowerStoreClient.get_appliance_id_by_name") - def setUp(self, mock_appliance, mock_chap): + def setUp(self, mock_chap): super(TestVolumeCreateDeleteExtend, self).setUp() - mock_appliance.return_value = "A1" self.driver.check_for_setup_error() self.volume = fake_volume.fake_volume_obj( - {}, - host="host@backend#test-appliance", + self.context, + host="host@backend", provider_id="fake_id", size=8 ) 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 1c36133b096..931ce4ae91f 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 @@ -24,29 +24,29 @@ from cinder.tests.unit.volume.drivers.dell_emc import powerstore class TestVolumeCreateFromSource(powerstore.TestPowerStoreDriver): @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." "PowerStoreClient.get_chap_config") - @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." - "PowerStoreClient.get_appliance_id_by_name") - def setUp(self, mock_appliance, mock_chap): + def setUp(self, mock_chap): super(TestVolumeCreateFromSource, self).setUp() - mock_appliance.return_value = "A1" self.driver.check_for_setup_error() self.volume = fake_volume.fake_volume_obj( - {}, - host="host@backend#test-appliance", + self.context, + host="host@backend", provider_id="fake_id", size=8 ) self.source_volume = fake_volume.fake_volume_obj( - {}, - host="host@backend#test-appliance", + self.context, + host="host@backend", provider_id="fake_id_1", size=8 ) self.source_snapshot = fake_snapshot.fake_snapshot_obj( - {}, - provider_id="fake_id_2", + self.context, + volume=self.source_volume, volume_size=8 ) + self.mock_object(self.driver.adapter.client, + "get_snapshot_id_by_name", + return_value="fake_id_1") @mock.patch("cinder.volume.drivers.dell_emc.powerstore.client." "PowerStoreClient.clone_volume_or_snapshot") @@ -91,7 +91,7 @@ class TestVolumeCreateFromSource(powerstore.TestPowerStoreDriver): mock_create_request.return_value = powerstore.MockResponse(rc=400) error = self.assertRaises( exception.VolumeBackendAPIException, - self.driver.adapter._create_volume_from_source, + self.driver.adapter.create_volume_from_source, self.volume, self.source_volume ) @@ -109,7 +109,7 @@ class TestVolumeCreateFromSource(powerstore.TestPowerStoreDriver): self.volume.size = 16 error = self.assertRaises( exception.VolumeBackendAPIException, - self.driver.adapter._create_volume_from_source, + self.driver.adapter.create_volume_from_source, self.volume, self.source_volume ) diff --git a/cinder/volume/drivers/dell_emc/powerstore/adapter.py b/cinder/volume/drivers/dell_emc/powerstore/adapter.py index 0099ff54e29..9341dc103c3 100644 --- a/cinder/volume/drivers/dell_emc/powerstore/adapter.py +++ b/cinder/volume/drivers/dell_emc/powerstore/adapter.py @@ -21,11 +21,11 @@ from oslo_utils import strutils from cinder import coordination from cinder import exception from cinder.i18n import _ +from cinder.objects import fields from cinder.objects.snapshot import Snapshot from cinder.volume.drivers.dell_emc.powerstore import client -from cinder.volume.drivers.dell_emc.powerstore import options from cinder.volume.drivers.dell_emc.powerstore import utils -from cinder.volume import volume_utils +from cinder.volume import manager LOG = logging.getLogger(__name__) @@ -35,15 +35,19 @@ CHAP_MODE_SINGLE = "Single" class CommonAdapter(object): - def __init__(self, active_backend_id, configuration): - self.active_backend_id = active_backend_id - self.appliances = None - self.appliances_to_ids_map = {} - self.client = None - self.configuration = configuration + def __init__(self, + backend_id, + backend_name, + ports, + **client_config): + if isinstance(ports, str): + ports = ports.split(",") + self.allowed_ports = [port.strip().lower() for port in ports] + self.backend_id = backend_id + self.backend_name = backend_name + self.client = client.PowerStoreClient(**client_config) self.storage_protocol = None - self.allowed_ports = None - self.use_chap_auth = None + self.use_chap_auth = False @staticmethod def initiators(connector): @@ -62,79 +66,71 @@ class CommonAdapter(object): return True return port.lower() in self.allowed_ports - def _get_connection_properties(self, appliance_id, volume_lun): + def _get_connection_properties(self, volume_lun): raise NotImplementedError - def do_setup(self): - self.appliances = ( - self.configuration.safe_get(options.POWERSTORE_APPLIANCES) - ) - self.allowed_ports = [ - port.strip().lower() for port in - self.configuration.safe_get(options.POWERSTORE_PORTS) - ] - self.client = client.PowerStoreClient(configuration=self.configuration) - self.client.do_setup() - def check_for_setup_error(self): self.client.check_for_setup_error() - if not self.appliances: - msg = _("PowerStore appliances must be set.") - raise exception.VolumeBackendAPIException(data=msg) - self.appliances_to_ids_map = {} - for appliance_name in self.appliances: - self.appliances_to_ids_map[appliance_name] = ( - self.client.get_appliance_id_by_name(appliance_name) - ) - self.use_chap_auth = False if self.storage_protocol == PROTOCOL_ISCSI: chap_config = self.client.get_chap_config() if chap_config.get("mode") == CHAP_MODE_SINGLE: self.use_chap_auth = True - LOG.debug("Successfully initialized PowerStore %(protocol)s adapter. " - "PowerStore appliances: %(appliances)s. " + LOG.debug("Successfully initialized PowerStore %(protocol)s adapter " + "for %(backend_id)s %(backend_name)s backend. " "Allowed ports: %(allowed_ports)s. " "Use CHAP authentication: %(use_chap_auth)s.", { "protocol": self.storage_protocol, - "appliances": self.appliances, + "backend_id": self.backend_id, + "backend_name": self.backend_name, "allowed_ports": self.allowed_ports, "use_chap_auth": self.use_chap_auth, }) def create_volume(self, volume): - appliance_name = volume_utils.extract_host(volume.host, "pool") - appliance_id = self.appliances_to_ids_map[appliance_name] + 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) + replication_status = fields.ReplicationStatus.ENABLED + else: + pp_name = None + pp_id = None + replication_status = fields.ReplicationStatus.DISABLED LOG.debug("Create PowerStore volume %(volume_name)s of size " - "%(volume_size)s GiB with id %(volume_id)s on appliance " - "%(appliance_name)s.", + "%(volume_size)s GiB with id %(volume_id)s. " + "Protection policy: %(pp_name)s.", { "volume_name": volume.name, "volume_size": volume.size, "volume_id": volume.id, - "appliance_name": appliance_name, + "pp_name": pp_name, }) size_in_bytes = utils.gib_to_bytes(volume.size) - provider_id = self.client.create_volume(appliance_id, - volume.name, - size_in_bytes) + provider_id = self.client.create_volume(volume.name, + size_in_bytes, + pp_id) LOG.debug("Successfully created PowerStore volume %(volume_name)s of " "size %(volume_size)s GiB with id %(volume_id)s on " - "appliance %(appliance_name)s. " + "Protection policy: %(pp_name)s." "PowerStore volume id: %(volume_provider_id)s.", { "volume_name": volume.name, "volume_size": volume.size, "volume_id": volume.id, - "appliance_name": appliance_name, + "pp_name": pp_name, "volume_provider_id": provider_id, }) return { "provider_id": provider_id, + "replication_status": replication_status, } def delete_volume(self, volume): - if not volume.provider_id: + try: + provider_id = self._get_volume_provider_id(volume) + except exception.VolumeBackendAPIException: + provider_id = None + if not provider_id: LOG.warning("Volume %(volume_name)s with id %(volume_id)s " "does not have provider_id thus does not " "map to PowerStore volume.", @@ -149,20 +145,21 @@ class CommonAdapter(object): { "volume_name": volume.name, "volume_id": volume.id, - "volume_provider_id": volume.provider_id, + "volume_provider_id": provider_id, }) self._detach_volume_from_hosts(volume) - self.client.delete_volume_or_snapshot(volume.provider_id) + self.client.delete_volume_or_snapshot(provider_id) LOG.debug("Successfully deleted PowerStore volume %(volume_name)s " "with id %(volume_id)s. PowerStore volume id: " "%(volume_provider_id)s.", { "volume_name": volume.name, "volume_id": volume.id, - "volume_provider_id": volume.provider_id, + "volume_provider_id": provider_id, }) def extend_volume(self, volume, new_size): + provider_id = self._get_volume_provider_id(volume) LOG.debug("Extend PowerStore volume %(volume_name)s of size " "%(volume_size)s GiB with id %(volume_id)s to " "%(volume_new_size)s GiB. " @@ -172,10 +169,10 @@ class CommonAdapter(object): "volume_size": volume.size, "volume_id": volume.id, "volume_new_size": new_size, - "volume_provider_id": volume.provider_id, + "volume_provider_id": provider_id, }) size_in_bytes = utils.gib_to_bytes(new_size) - self.client.extend_volume(volume.provider_id, size_in_bytes) + self.client.extend_volume(provider_id, size_in_bytes) LOG.debug("Successfully extended PowerStore volume %(volume_name)s " "of size %(volume_size)s GiB with id " "%(volume_id)s to %(volume_new_size)s GiB. " @@ -185,10 +182,11 @@ class CommonAdapter(object): "volume_size": volume.size, "volume_id": volume.id, "volume_new_size": new_size, - "volume_provider_id": volume.provider_id, + "volume_provider_id": provider_id, }) def create_snapshot(self, snapshot): + volume_provider_id = self._get_volume_provider_id(snapshot.volume) LOG.debug("Create PowerStore snapshot %(snapshot_name)s with id " "%(snapshot_id)s of volume %(volume_name)s with id " "%(volume_id)s. PowerStore volume id: " @@ -198,124 +196,57 @@ class CommonAdapter(object): "snapshot_id": snapshot.id, "volume_name": snapshot.volume.name, "volume_id": snapshot.volume.id, - "volume_provider_id": snapshot.volume.provider_id, + "volume_provider_id": volume_provider_id, }) - snapshot_provider_id = self.client.create_snapshot( - snapshot.volume.provider_id, - snapshot.name) + self.client.create_snapshot(volume_provider_id, snapshot.name) LOG.debug("Successfully created PowerStore snapshot %(snapshot_name)s " "with id %(snapshot_id)s of volume %(volume_name)s with " - "id %(volume_id)s. PowerStore snapshot id: " - "%(snapshot_provider_id)s, volume id: " + "id %(volume_id)s. PowerStore volume id: " "%(volume_provider_id)s.", { "snapshot_name": snapshot.name, "snapshot_id": snapshot.id, "volume_name": snapshot.volume.name, "volume_id": snapshot.volume.id, - "snapshot_provider_id": snapshot_provider_id, - "volume_provider_id": snapshot.volume.provider_id, + "volume_provider_id": volume_provider_id, }) - return { - "provider_id": snapshot_provider_id, - } def delete_snapshot(self, snapshot): + try: + volume_provider_id = self._get_volume_provider_id(snapshot.volume) + except exception.VolumeBackendAPIException: + return LOG.debug("Delete PowerStore snapshot %(snapshot_name)s with id " "%(snapshot_id)s of volume %(volume_name)s with " - "id %(volume_id)s. PowerStore snapshot id: " - "%(snapshot_provider_id)s, volume id: " + "id %(volume_id)s. PowerStore volume id: " "%(volume_provider_id)s.", { "snapshot_name": snapshot.name, "snapshot_id": snapshot.id, "volume_name": snapshot.volume.name, "volume_id": snapshot.volume.id, - "snapshot_provider_id": snapshot.provider_id, - "volume_provider_id": snapshot.volume.provider_id, + "volume_provider_id": volume_provider_id, }) - self.client.delete_volume_or_snapshot(snapshot.provider_id, + try: + snapshot_provider_id = self.client.get_snapshot_id_by_name( + volume_provider_id, + snapshot.name + ) + except exception.VolumeBackendAPIException: + return + self.client.delete_volume_or_snapshot(snapshot_provider_id, entity="snapshot") LOG.debug("Successfully deleted PowerStore snapshot %(snapshot_name)s " "with id %(snapshot_id)s of volume %(volume_name)s with " - "id %(volume_id)s. PowerStore snapshot id: " - "%(snapshot_provider_id)s, volume id: " + "id %(volume_id)s. PowerStore volume id: " "%(volume_provider_id)s.", { "snapshot_name": snapshot.name, "snapshot_id": snapshot.id, "volume_name": snapshot.volume.name, "volume_id": snapshot.volume.id, - "snapshot_provider_id": snapshot.provider_id, - "volume_provider_id": snapshot.volume.provider_id, - }) - - def create_cloned_volume(self, volume, src_vref): - LOG.debug("Clone PowerStore volume %(source_volume_name)s with id " - "%(source_volume_id)s to volume %(cloned_volume_name)s of " - "size %(cloned_volume_size)s GiB with id " - "%(cloned_volume_id)s. PowerStore source volume id: " - "%(source_volume_provider_id)s.", - { - "source_volume_name": src_vref.name, - "source_volume_id": src_vref.id, - "cloned_volume_name": volume.name, - "cloned_volume_size": volume.size, - "cloned_volume_id": volume.id, - "source_volume_provider_id": src_vref.provider_id, - }) - cloned_provider_id = self._create_volume_from_source(volume, src_vref) - LOG.debug("Successfully cloned PowerStore volume " - "%(source_volume_name)s with id %(source_volume_id)s to " - "volume %(cloned_volume_name)s of size " - "%(cloned_volume_size)s GiB with id %(cloned_volume_id)s. " - "PowerStore source volume id: " - "%(source_volume_provider_id)s, " - "cloned volume id: %(cloned_volume_provider_id)s.", - { - "source_volume_name": src_vref.name, - "source_volume_id": src_vref.id, - "cloned_volume_name": volume.name, - "cloned_volume_size": volume.size, - "cloned_volume_id": volume.id, - "source_volume_provider_id": src_vref.provider_id, - "cloned_volume_provider_id": cloned_provider_id, - }) - return { - "provider_id": cloned_provider_id, - } - - def create_volume_from_snapshot(self, volume, snapshot): - LOG.debug("Create PowerStore volume %(volume_name)s of size " - "%(volume_size)s GiB with id %(volume_id)s from snapshot " - "%(snapshot_name)s with id %(snapshot_id)s. PowerStore " - "snapshot id: %(snapshot_provider_id)s.", - { - "volume_name": volume.name, - "volume_id": volume.id, - "volume_size": volume.size, - "snapshot_name": snapshot.name, - "snapshot_id": snapshot.id, - "snapshot_provider_id": snapshot.provider_id, - }) - volume_provider_id = self._create_volume_from_source(volume, snapshot) - LOG.debug("Successfully created PowerStore volume %(volume_name)s " - "of size %(volume_size)s GiB with id %(volume_id)s from " - "snapshot %(snapshot_name)s with id %(snapshot_id)s. " - "PowerStore volume id: %(volume_provider_id)s, " - "snapshot id: %(snapshot_provider_id)s.", - { - "volume_name": volume.name, - "volume_id": volume.id, - "volume_size": volume.size, - "snapshot_name": snapshot.name, - "snapshot_id": snapshot.id, "volume_provider_id": volume_provider_id, - "snapshot_provider_id": snapshot.provider_id, }) - return { - "provider_id": volume_provider_id, - } def initialize_connection(self, volume, connector, **kwargs): connection_properties = self._connect_volume(volume, connector) @@ -336,76 +267,96 @@ class CommonAdapter(object): def update_volume_stats(self): stats = { - "volume_backend_name": ( - self.configuration.safe_get("volume_backend_name") or - "powerstore" - ), + "volume_backend_name": self.backend_name, "storage_protocol": self.storage_protocol, "thick_provisioning_support": False, "thin_provisioning_support": True, "compression_support": True, "multiattach": True, - "pools": [], } - backend_total_capacity = 0 - backend_free_capacity = 0 - for appliance_name in self.appliances: - appliance_stats = self.client.get_appliance_metrics( - self.appliances_to_ids_map[appliance_name] - ) - appliance_total_capacity = utils.bytes_to_gib( - appliance_stats["physical_total"] - ) - appliance_free_capacity = ( - appliance_total_capacity - - utils.bytes_to_gib(appliance_stats["physical_used"]) - ) - pool = { - "pool_name": appliance_name, - "total_capacity_gb": appliance_total_capacity, - "free_capacity_gb": appliance_free_capacity, - "thick_provisioning_support": False, - "thin_provisioning_support": True, - "compression_support": True, - "multiattach": True, - } - backend_total_capacity += appliance_total_capacity - backend_free_capacity += appliance_free_capacity - stats["pools"].append(pool) + backend_stats = self.client.get_metrics() + backend_total_capacity = utils.bytes_to_gib( + backend_stats["physical_total"] + ) + backend_free_capacity = ( + backend_total_capacity - + utils.bytes_to_gib(backend_stats["physical_used"]) + ) stats["total_capacity_gb"] = backend_total_capacity stats["free_capacity_gb"] = backend_free_capacity LOG.debug("Free capacity for backend '%(backend)s': " "%(free)s GiB, total capacity: %(total)s GiB.", { - "backend": stats["volume_backend_name"], + "backend": self.backend_name, "free": backend_free_capacity, "total": backend_total_capacity, }) return stats - def _create_volume_from_source(self, volume, source): - """Create PowerStore volume from source (snapshot or another volume). - - :param volume: OpenStack volume object - :param source: OpenStack source snapshot or volume - :return: newly created PowerStore volume id - """ - + def create_volume_from_source(self, volume, source): if isinstance(source, Snapshot): entity = "snapshot" source_size = source.volume_size + source_volume_provider_id = self._get_volume_provider_id( + source.volume + ) + source_provider_id = self.client.get_snapshot_id_by_name( + source_volume_provider_id, + source.name + ) else: entity = "volume" source_size = source.size + source_provider_id = self._get_volume_provider_id(source) + 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) + replication_status = fields.ReplicationStatus.ENABLED + else: + pp_name = None + pp_id = None + replication_status = fields.ReplicationStatus.DISABLED + LOG.debug("Create PowerStore volume %(volume_name)s of size " + "%(volume_size)s GiB with id %(volume_id)s from %(entity)s " + "%(entity_name)s with id %(entity_id)s. " + "Protection policy: %(pp_name)s.", + { + "volume_name": volume.name, + "volume_id": volume.id, + "volume_size": volume.size, + "entity": entity, + "entity_name": source.name, + "entity_id": source.id, + "pp_name": pp_name, + }) volume_provider_id = self.client.clone_volume_or_snapshot( volume.name, - source.provider_id, + source_provider_id, + pp_id, entity ) if volume.size > source_size: size_in_bytes = utils.gib_to_bytes(volume.size) self.client.extend_volume(volume_provider_id, size_in_bytes) - return volume_provider_id + LOG.debug("Successfully created PowerStore volume %(volume_name)s " + "of size %(volume_size)s GiB with id %(volume_id)s from " + "%(entity)s %(entity_name)s with id %(entity_id)s. " + "Protection policy %(pp_name)s. " + "PowerStore volume id: %(volume_provider_id)s.", + { + "volume_name": volume.name, + "volume_id": volume.id, + "volume_size": volume.size, + "entity": entity, + "entity_name": source.name, + "entity_id": source.id, + "pp_name": pp_name, + "volume_provider_id": volume_provider_id, + }) + return { + "provider_id": volume_provider_id, + "replication_status": replication_status, + } def _filter_hosts_by_initiators(self, initiators): """Filter hosts by given list of initiators. @@ -590,6 +541,7 @@ class CommonAdapter(object): :return: attached volume logical number """ + provider_id = self._get_volume_provider_id(volume) LOG.debug("Attach PowerStore volume %(volume_name)s with id " "%(volume_id)s to host %(host_name)s. PowerStore volume id: " "%(volume_provider_id)s, host id: %(host_provider_id)s.", @@ -597,13 +549,11 @@ class CommonAdapter(object): "volume_name": volume.name, "volume_id": volume.id, "host_name": host["name"], - "volume_provider_id": volume.provider_id, + "volume_provider_id": provider_id, "host_provider_id": host["id"], }) - self.client.attach_volume_to_host(host["id"], volume.provider_id) - volume_lun = self.client.get_volume_lun( - host["id"], volume.provider_id - ) + self.client.attach_volume_to_host(host["id"], provider_id) + volume_lun = self.client.get_volume_lun(host["id"], provider_id) LOG.debug("Successfully attached PowerStore volume %(volume_name)s " "with id %(volume_id)s to host %(host_name)s. " "PowerStore volume id: %(volume_provider_id)s, " @@ -613,7 +563,7 @@ class CommonAdapter(object): "volume_name": volume.name, "volume_id": volume.id, "host_name": host["name"], - "volume_provider_id": volume.provider_id, + "volume_provider_id": provider_id, "host_provider_id": host["id"], "volume_lun": volume_lun, }) @@ -638,14 +588,11 @@ class CommonAdapter(object): :return: volume connection properties """ - appliance_name = volume_utils.extract_host(volume.host, "pool") - appliance_id = self.appliances_to_ids_map[appliance_name] chap_credentials, volume_lun = self._create_host_and_attach( connector, volume ) - connection_properties = self._get_connection_properties(appliance_id, - volume_lun) + connection_properties = self._get_connection_properties(volume_lun) if self.use_chap_auth: connection_properties["data"]["auth_method"] = "CHAP" connection_properties["data"]["auth_username"] = ( @@ -666,11 +613,10 @@ class CommonAdapter(object): :return: None """ + provider_id = self._get_volume_provider_id(volume) if hosts_to_detach is None: # Force detach. Get all mapped hosts and detach. - hosts_to_detach = self.client.get_volume_mapped_hosts( - volume.provider_id - ) + hosts_to_detach = self.client.get_volume_mapped_hosts(provider_id) if not hosts_to_detach: # Volume is not attached to any host. return @@ -680,11 +626,11 @@ class CommonAdapter(object): { "volume_name": volume.name, "volume_id": volume.id, - "volume_provider_id": volume.provider_id, + "volume_provider_id": provider_id, "hosts_provider_ids": hosts_to_detach, }) for host_id in hosts_to_detach: - self.client.detach_volume_from_host(host_id, volume.provider_id) + self.client.detach_volume_from_host(host_id, provider_id) LOG.debug("Successfully detached PowerStore volume " "%(volume_name)s with id %(volume_id)s from hosts. " "PowerStore volume id: %(volume_provider_id)s, " @@ -692,7 +638,7 @@ class CommonAdapter(object): { "volume_name": volume.name, "volume_id": volume.id, - "volume_provider_id": volume.provider_id, + "volume_provider_id": provider_id, "hosts_provider_ids": hosts_to_detach, }) @@ -721,40 +667,130 @@ class CommonAdapter(object): self._detach_volume_from_hosts(volume, [host["id"]]) def revert_to_snapshot(self, volume, snapshot): + volume_provider_id = self._get_volume_provider_id(volume) + snapshot_volume_provider_id = self._get_volume_provider_id( + snapshot.volume + ) LOG.debug("Restore PowerStore volume %(volume_name)s with id " "%(volume_id)s from snapshot %(snapshot_name)s with id " "%(snapshot_id)s. PowerStore volume id: " - "%(volume_provider_id)s, snapshot id: " - "%(snapshot_provider_id)s.", + "%(volume_provider_id)s.", { "volume_name": volume.name, "volume_id": volume.id, "snapshot_name": snapshot.name, "snapshot_id": snapshot.id, - "volume_provider_id": volume.provider_id, - "snapshot_provider_id": snapshot.provider_id, + "volume_provider_id": volume_provider_id, }) - self.client.restore_from_snapshot(volume.provider_id, - snapshot.provider_id) + snapshot_provider_id = self.client.get_snapshot_id_by_name( + snapshot_volume_provider_id, + snapshot.name + ) + self.client.restore_from_snapshot(volume_provider_id, + snapshot_provider_id) LOG.debug("Successfully restored PowerStore volume %(volume_name)s " "with id %(volume_id)s from snapshot %(snapshot_name)s " "with id %(snapshot_id)s. PowerStore volume id: " - "%(volume_provider_id)s, snapshot id: " - "%(snapshot_provider_id)s.", + "%(volume_provider_id)s.", { "volume_name": volume.name, "volume_id": volume.id, "snapshot_name": snapshot.name, "snapshot_id": snapshot.id, - "volume_provider_id": volume.provider_id, - "snapshot_provider_id": snapshot.provider_id, + "volume_provider_id": volume_provider_id, }) + def _get_volume_provider_id(self, volume): + """Get provider_id for volume. + + If the secondary backend is used after failover operation try to get + volume provider_id from PowerStore API. + + :param volume: OpenStack volume object + :return: volume provider_id + """ + + if ( + self.backend_id == manager.VolumeManager.FAILBACK_SENTINEL or + not volume.is_replicated() + ): + return volume.provider_id + else: + return self.client.get_volume_id_by_name(volume.name) + + def teardown_volume_replication(self, volume): + """Teardown replication for volume so it can be deleted. + + :param volume: OpenStack volume object + :return: None + """ + + LOG.debug("Teardown replication for volume %(volume_name)s " + "with id %(volume_id)s.", + { + "volume_name": volume.name, + "volume_id": volume.id, + }) + try: + provider_id = self._get_volume_provider_id(volume) + rep_session_id = self.client.get_volume_replication_session_id( + provider_id + ) + except exception.VolumeBackendAPIException: + LOG.warning("Replication session for volume %(volume_name)s with " + "id %(volume_id)s is not found. Replication for " + "volume was not configured or was modified from " + "storage side.", + { + "volume_name": volume.name, + "volume_id": volume.id, + }) + return + self.client.unassign_volume_protection_policy(provider_id) + self.client.wait_for_replication_session_deletion(rep_session_id) + + def failover_host(self, volumes, groups, is_failback): + volumes_updates = [] + groups_updates = [] + for volume in volumes: + updates = self.failover_volume(volume, is_failback) + if updates: + volumes_updates.append(updates) + return volumes_updates, groups_updates + + def failover_volume(self, volume, is_failback): + error_status = (fields.ReplicationStatus.ERROR if is_failback else + fields.ReplicationStatus.FAILOVER_ERROR) + try: + provider_id = self._get_volume_provider_id(volume) + rep_session_id = self.client.get_volume_replication_session_id( + provider_id + ) + failover_job_id = self.client.failover_volume_replication_session( + rep_session_id, + is_failback + ) + failover_success = self.client.wait_for_failover_completion( + failover_job_id + ) + if is_failback: + self.client.reprotect_volume_replication_session( + rep_session_id + ) + except exception.VolumeBackendAPIException: + failover_success = False + if not failover_success: + return { + "volume_id": volume.id, + "updates": { + "replication_status": error_status, + }, + } + class FibreChannelAdapter(CommonAdapter): - def __init__(self, active_backend_id, configuration): - super(FibreChannelAdapter, self).__init__(active_backend_id, - configuration) + def __init__(self, **kwargs): + super(FibreChannelAdapter, self).__init__(**kwargs) self.storage_protocol = PROTOCOL_FC self.driver_volume_type = "fibre_channel" @@ -762,15 +798,14 @@ class FibreChannelAdapter(CommonAdapter): def initiators(connector): return utils.extract_fc_wwpns(connector) - def _get_fc_targets(self, appliance_id): - """Get available FC WWNs for PowerStore appliance. + def _get_fc_targets(self): + """Get available FC WWNs. - :param appliance_id: PowerStore appliance id :return: list of FC WWNs """ wwns = [] - fc_ports = self.client.get_fc_port(appliance_id) + fc_ports = self.client.get_fc_port() for port in fc_ports: if self._port_is_allowed(port["wwn"]): wwns.append(utils.fc_wwn_to_string(port["wwn"])) @@ -780,15 +815,14 @@ class FibreChannelAdapter(CommonAdapter): raise exception.VolumeBackendAPIException(data=msg) return wwns - def _get_connection_properties(self, appliance_id, volume_lun): + def _get_connection_properties(self, volume_lun): """Fill connection properties dict with data to attach volume. - :param appliance_id: PowerStore appliance id :param volume_lun: attached volume logical unit number :return: connection properties """ - target_wwns = self._get_fc_targets(appliance_id) + target_wwns = self._get_fc_targets() return { "driver_volume_type": self.driver_volume_type, "data": { @@ -800,8 +834,8 @@ class FibreChannelAdapter(CommonAdapter): class iSCSIAdapter(CommonAdapter): - def __init__(self, active_backend_id, configuration): - super(iSCSIAdapter, self).__init__(active_backend_id, configuration) + def __init__(self, **kwargs): + super(iSCSIAdapter, self).__init__(**kwargs) self.storage_protocol = PROTOCOL_ISCSI self.driver_volume_type = "iscsi" @@ -809,16 +843,15 @@ class iSCSIAdapter(CommonAdapter): def initiators(connector): return [connector["initiator"]] - def _get_iscsi_targets(self, appliance_id): - """Get available iSCSI portals and IQNs for PowerStore appliance. + def _get_iscsi_targets(self): + """Get available iSCSI portals and IQNs. - :param appliance_id: PowerStore appliance id :return: iSCSI portals and IQNs """ iqns = [] portals = [] - ip_pool_addresses = self.client.get_ip_pool_address(appliance_id) + ip_pool_addresses = self.client.get_ip_pool_address() for address in ip_pool_addresses: if self._port_is_allowed(address["address"]): portals.append( @@ -831,15 +864,14 @@ class iSCSIAdapter(CommonAdapter): raise exception.VolumeBackendAPIException(data=msg) return iqns, portals - def _get_connection_properties(self, appliance_id, volume_lun): + def _get_connection_properties(self, volume_lun): """Fill connection properties dict with data to attach volume. - :param appliance_id: PowerStore appliance id :param volume_lun: attached volume logical unit number :return: connection properties """ - iqns, portals = self._get_iscsi_targets(appliance_id) + iqns, portals = self._get_iscsi_targets() return { "driver_volume_type": self.driver_volume_type, "data": { diff --git a/cinder/volume/drivers/dell_emc/powerstore/client.py b/cinder/volume/drivers/dell_emc/powerstore/client.py index 4299b8f65cd..bc0d668036d 100644 --- a/cinder/volume/drivers/dell_emc/powerstore/client.py +++ b/cinder/volume/drivers/dell_emc/powerstore/client.py @@ -24,24 +24,31 @@ import requests from cinder import exception from cinder.i18n import _ +from cinder import utils as cinder_utils LOG = logging.getLogger(__name__) VOLUME_NOT_MAPPED_ERROR = "0xE0A08001000F" +SESSION_ALREADY_FAILED_OVER_ERROR = "0xE0201005000C" class PowerStoreClient(object): - def __init__(self, configuration): - self.configuration = configuration - self.rest_ip = None - self.rest_username = None - self.rest_password = None - self.verify_certificate = None - self.certificate_path = None - self.base_url = None + def __init__(self, + rest_ip, + rest_username, + rest_password, + verify_certificate, + certificate_path): + self.rest_ip = rest_ip + self.rest_username = rest_username + self.rest_password = rest_password + self.verify_certificate = verify_certificate + self.certificate_path = certificate_path + self.base_url = "https://%s:/api/rest" % self.rest_ip self.ok_codes = [ requests.codes.ok, requests.codes.created, + requests.codes.accepted, requests.codes.no_content, requests.codes.partial_content ] @@ -53,28 +60,16 @@ class PowerStoreClient(object): verify_cert = self.certificate_path return verify_cert - def do_setup(self): - self.rest_ip = self.configuration.safe_get("san_ip") - self.rest_username = self.configuration.safe_get("san_login") - self.rest_password = self.configuration.safe_get("san_password") - self.base_url = "https://%s:/api/rest" % self.rest_ip - self.verify_certificate = self.configuration.safe_get( - "driver_ssl_cert_verify" - ) - if self.verify_certificate: - self.certificate_path = ( - self.configuration.safe_get("driver_ssl_cert_path") - ) - def check_for_setup_error(self): if not all([self.rest_ip, self.rest_username, self.rest_password]): msg = _("REST server IP, username and password must be set.") - raise exception.VolumeBackendAPIException(data=msg) + raise exception.InvalidInput(reason=msg) # log warning if not using certificates if not self.verify_certificate: LOG.warning("Verify certificate is not set, using default of " "False.") + self.verify_certificate = False LOG.debug("Successfully initialized PowerStore REST client. " "Server IP: %(ip)s, username: %(username)s. " "Verify server's certificate: %(verify_cert)s.", @@ -97,10 +92,9 @@ class PowerStoreClient(object): request_params = { "auth": (self.rest_username, self.rest_password), "verify": self._verify_cert, + "params": params } - if method == "GET": - request_params["params"] = params - else: + if method != "GET": request_params["data"] = json.dumps(payload) request_url = self.base_url + url r = requests.request(method, request_url, **request_params) @@ -143,55 +137,34 @@ class PowerStoreClient(object): raise exception.VolumeBackendAPIException(data=msg) return response - def get_appliance_id_by_name(self, appliance_name): - r, response = self._send_get_request( - "/appliance", - params={ - "name": "eq.%s" % appliance_name, - } - ) - if r.status_code not in self.ok_codes: - msg = _("Failed to query PowerStore appliances.") - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - try: - appliance_id = response[0].get("id") - return appliance_id - except IndexError: - msg = _("PowerStore appliance %s is not found.") % appliance_name - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - def get_appliance_metrics(self, appliance_id): + def get_metrics(self): r, response = self._send_post_request( "/metrics/generate", payload={ - "entity": "space_metrics_by_appliance", - "entity_id": appliance_id, + "entity": "space_metrics_by_cluster", + "entity_id": "0", }, log_response_data=False ) if r.status_code not in self.ok_codes: - msg = (_("Failed to query metrics for " - "PowerStore appliance with id %s.") % appliance_id) + msg = _("Failed to query PowerStore metrics.") LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) try: latest_metrics = response[-1] return latest_metrics except IndexError: - msg = (_("Failed to query metrics for " - "PowerStore appliance with id %s.") % appliance_id) + msg = _("Failed to query PowerStore metrics.") LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) - def create_volume(self, appliance_id, name, size): + def create_volume(self, name, size, pp_id): r, response = self._send_post_request( "/volume", payload={ - "appliance_id": appliance_id, "name": name, "size": size, + "protection_policy_id": pp_id, } ) if r.status_code not in self.ok_codes: @@ -247,14 +220,40 @@ class PowerStoreClient(object): raise exception.VolumeBackendAPIException(data=msg) return response["id"] + def get_snapshot_id_by_name(self, volume_id, name): + r, response = self._send_get_request( + "/volume", + params={ + "name": "eq.%s" % name, + "protection_data->>source_id": "eq.%s" % volume_id, + "type": "eq.Snapshot", + } + ) + if r.status_code not in self.ok_codes: + msg = _("Failed to query PowerStore snapshots.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + try: + snap_id = response[0].get("id") + return snap_id + except IndexError: + msg = (_("PowerStore snapshot %(snapshot_name)s for volume " + "with id %(volume_id)s is not found.") + % {"snapshot_name": name, + "volume_id": volume_id, }) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + def clone_volume_or_snapshot(self, name, entity_id, + pp_id, entity="volume"): r, response = self._send_post_request( "/volume/%s/clone" % entity_id, payload={ "name": name, + "protection_policy_id": pp_id, } ) if r.status_code not in self.ok_codes: @@ -363,27 +362,25 @@ class PowerStoreClient(object): LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) - def get_fc_port(self, appliance_id): + def get_fc_port(self): r, response = self._send_get_request( "/fc_port", params={ - "appliance_id": "eq.%s" % appliance_id, "is_link_up": "eq.True", "select": "wwn" } ) if r.status_code not in self.ok_codes: - msg = _("Failed to query PowerStore IP pool addresses.") + msg = _("Failed to query PowerStore FC ports.") LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) return response - def get_ip_pool_address(self, appliance_id): + def get_ip_pool_address(self): r, response = self._send_get_request( "/ip_pool_address", params={ - "appliance_id": "eq.%s" % appliance_id, "purposes": "eq.{Storage_Iscsi_Target}", "select": "address,ip_port(target_iqn)" @@ -446,3 +443,159 @@ class PowerStoreClient(object): "snapshot_id": snapshot_id, }) LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) + + def get_protection_policy_id_by_name(self, name): + r, response = self._send_get_request( + "/policy", + params={ + "name": "eq.%s" % name, + "type": "eq.Protection", + } + ) + if r.status_code not in self.ok_codes: + msg = _("Failed to query PowerStore Protection policies.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + try: + pp_id = response[0].get("id") + return pp_id + except IndexError: + msg = _("PowerStore Protection policy %s is not found.") % name + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def get_volume_replication_session_id(self, volume_id): + r, response = self._send_get_request( + "/replication_session", + params={ + "local_resource_id": "eq.%s" % volume_id, + } + ) + if r.status_code not in self.ok_codes: + msg = _("Failed to query PowerStore Replication sessions.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + try: + return response[0].get("id") + except IndexError: + msg = _("Replication session for PowerStore volume with " + "id %s is not found.") % volume_id + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def get_volume_id_by_name(self, name): + r, response = self._send_get_request( + "/volume", + params={ + "name": "eq.%s" % name, + "type": "in.(Primary,Clone)", + } + ) + if r.status_code not in self.ok_codes: + msg = _("Failed to query PowerStore volumes.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + try: + vol_id = response[0].get("id") + return vol_id + except IndexError: + msg = _("PowerStore volume %s is not found.") % name + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def unassign_volume_protection_policy(self, volume_id): + r, response = self._send_patch_request( + "/volume/%s" % volume_id, + payload={ + "protection_policy_id": "", + } + ) + if r.status_code not in self.ok_codes: + msg = (_("Failed to unassign Protection policy for PowerStore " + "volume with id %s.") % volume_id) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + @cinder_utils.retry(exception.VolumeBackendAPIException, + interval=1, backoff_rate=3, retries=5) + def wait_for_replication_session_deletion(self, rep_session_id): + r, response = self._send_get_request( + "/job", + params={ + "resource_type": "eq.replication_session", + "resource_action": "eq.delete", + "resource_id": "eq.%s" % rep_session_id, + "state": "eq.COMPLETED", + } + ) + if r.status_code not in self.ok_codes: + msg = _("Failed to query PowerStore jobs.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + if not response: + msg = _("PowerStore Replication session with " + "id %s is still exists.") % rep_session_id + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def failover_volume_replication_session(self, rep_session_id, is_failback): + r, response = self._send_post_request( + "/replication_session/%s/failover" % rep_session_id, + payload={ + "is_planned": False, + "force": is_failback, + }, + params={ + "is_async": True, + } + ) + if r.status_code not in self.ok_codes: + msg = (_("Failed to failover PowerStore replication session " + "with id %s.") % rep_session_id) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + return response["id"] + + @cinder_utils.retry(exception.VolumeBackendAPIException, + interval=1, backoff_rate=3, retries=5) + def wait_for_failover_completion(self, job_id): + r, response = self._send_get_request( + "/job/%s" % job_id, + params={ + "select": "resource_action,resource_type," + "resource_id,state,response_body", + } + ) + if r.status_code not in self.ok_codes: + msg = _("Failed to query PowerStore job with id %s.") % job_id + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + elif ( + isinstance(response["response_body"], dict) and + any([ + message["code"] == SESSION_ALREADY_FAILED_OVER_ERROR + for message in + response["response_body"].get("messages", []) + ]) + ): + # Replication session is already in Failed-Over state. + return True + elif response["state"] == "COMPLETED": + return True + elif response["state"] in ["FAILED", "UNRECOVERABLE_FAILED"]: + return False + else: + msg = _("Failover of PowerStore Replication session with id " + "%s is still in progress.") % response["resource_id"] + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def reprotect_volume_replication_session(self, rep_session_id): + r, response = self._send_post_request( + "/replication_session/%s/reprotect" % rep_session_id + ) + if r.status_code not in self.ok_codes: + msg = (_("Failed to reprotect PowerStore replication session " + "with id %s.") % rep_session_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 87d0eaf60ce..b3407cc9c7d 100644 --- a/cinder/volume/drivers/dell_emc/powerstore/driver.py +++ b/cinder/volume/drivers/dell_emc/powerstore/driver.py @@ -16,17 +16,24 @@ """Cinder driver for Dell EMC PowerStore.""" from oslo_config import cfg +from oslo_log import log as logging +from cinder import exception +from cinder.i18n import _ from cinder import interface from cinder.volume import configuration from cinder.volume import driver from cinder.volume.drivers.dell_emc.powerstore import adapter -from cinder.volume.drivers.dell_emc.powerstore.options import POWERSTORE_OPTS +from cinder.volume.drivers.dell_emc.powerstore import options from cinder.volume.drivers.san import san +from cinder.volume import manager +POWERSTORE_OPTS = options.POWERSTORE_OPTS CONF = cfg.CONF CONF.register_opts(POWERSTORE_OPTS, group=configuration.SHARED_CONF_GROUP) +LOG = logging.getLogger(__name__) +POWERSTORE_PP_KEY = "powerstore:protection_policy" @interface.volumedriver @@ -38,9 +45,10 @@ class PowerStoreDriver(driver.VolumeDriver): Version history: 1.0.0 - Initial version 1.0.1 - Add CHAP support + 1.1.0 - Add volume replication v2.1 support """ - VERSION = "1.0.1" + VERSION = "1.1.0" VENDOR = "Dell EMC" # ThirdPartySystems wiki page @@ -50,35 +58,73 @@ class PowerStoreDriver(driver.VolumeDriver): super(PowerStoreDriver, self).__init__(*args, **kwargs) self.active_backend_id = kwargs.get("active_backend_id") - self.adapter = None + self.adapters = {} self.configuration.append_config_values(san.san_opts) self.configuration.append_config_values(POWERSTORE_OPTS) + self.replication_configured = False + self.replication_devices = None + + def _init_vendor_properties(self): + properties = {} + self._set_property( + properties, + POWERSTORE_PP_KEY, + "PowerStore Protection Policy.", + _("Specifies the PowerStore Protection Policy for a " + "volume type. Protection Policy is assigned to a volume during " + "creation."), + "string" + ) + return properties, "powerstore" @staticmethod def get_driver_options(): return POWERSTORE_OPTS def do_setup(self, context): + if not self.active_backend_id: + self.active_backend_id = manager.VolumeManager.FAILBACK_SENTINEL storage_protocol = self.configuration.safe_get("storage_protocol") if ( storage_protocol and storage_protocol.lower() == adapter.PROTOCOL_FC.lower() ): - self.adapter = adapter.FibreChannelAdapter(self.active_backend_id, - self.configuration) + adapter_class = adapter.FibreChannelAdapter else: - self.adapter = adapter.iSCSIAdapter(self.active_backend_id, - self.configuration) - self.adapter.do_setup() + adapter_class = adapter.iSCSIAdapter + self.replication_devices = ( + self.configuration.safe_get("replication_device") or [] + ) + self.adapters[manager.VolumeManager.FAILBACK_SENTINEL] = adapter_class( + **self._get_device_configuration() + ) + for index, device in enumerate(self.replication_devices): + self.adapters[device["backend_id"]] = adapter_class( + **self._get_device_configuration(is_primary=False, + device_index=index) + ) def check_for_setup_error(self): - self.adapter.check_for_setup_error() + if len(self.replication_devices) > 1: + msg = _("PowerStore driver does not support more than one " + "replication device.") + raise exception.InvalidInput(reason=msg) + self.replication_configured = True + for adapter in self.adapters.values(): + adapter.check_for_setup_error() def create_volume(self, volume): return self.adapter.create_volume(volume) def delete_volume(self, volume): - return self.adapter.delete_volume(volume) + if volume.is_replicated(): + self.adapter.teardown_volume_replication(volume) + self.adapter.delete_volume(volume) + if not self.is_failed_over: + for backend_id in self.failover_choices: + self.adapters.get(backend_id).delete_volume(volume) + else: + self.adapter.delete_volume(volume) def extend_volume(self, volume, new_size): return self.adapter.extend_volume(volume, new_size) @@ -87,13 +133,16 @@ class PowerStoreDriver(driver.VolumeDriver): return self.adapter.create_snapshot(snapshot) def delete_snapshot(self, snapshot): - return self.adapter.delete_snapshot(snapshot) + self.adapter.delete_snapshot(snapshot) + if snapshot.volume.is_replicated() and not self.is_failed_over: + for backend_id in self.failover_choices: + self.adapters.get(backend_id).delete_snapshot(snapshot) def create_cloned_volume(self, volume, src_vref): - return self.adapter.create_cloned_volume(volume, src_vref) + return self.adapter.create_volume_from_source(volume, src_vref) def create_volume_from_snapshot(self, volume, snapshot): - return self.adapter.create_volume_from_snapshot(volume, snapshot) + return self.adapter.create_volume_from_source(volume, snapshot) def initialize_connection(self, volume, connector, **kwargs): return self.adapter.initialize_connection(volume, connector, **kwargs) @@ -108,6 +157,8 @@ class PowerStoreDriver(driver.VolumeDriver): stats = self.adapter.update_volume_stats() stats["driver_version"] = self.VERSION stats["vendor_name"] = self.VENDOR + stats["replication_enabled"] = self.replication_enabled + stats["replication_targets"] = self.replication_targets self._stats = stats def create_export(self, context, volume, connector): @@ -118,3 +169,66 @@ class PowerStoreDriver(driver.VolumeDriver): def remove_export(self, context, volume): pass + + def failover_host(self, context, volumes, secondary_id=None, groups=None): + if secondary_id not in self.failover_choices: + msg = (_("Target %(target)s is not a valid choice. " + "Valid choices: %(choices)s.") % + {"target": secondary_id, + "choices": ', '.join(self.failover_choices)}) + LOG.error(msg) + raise exception.InvalidReplicationTarget(reason=msg) + is_failback = secondary_id == manager.VolumeManager.FAILBACK_SENTINEL + self.active_backend_id = secondary_id + volumes_updates, groups_updates = self.adapter.failover_host( + volumes, + groups, + is_failback + ) + return secondary_id, volumes_updates, groups_updates + + @property + def adapter(self): + return self.adapters.get(self.active_backend_id) + + @property + def failover_choices(self): + return ( + set(self.adapters.keys()).difference({self.active_backend_id}) + ) + + @property + def is_failed_over(self): + return ( + self.active_backend_id != manager.VolumeManager.FAILBACK_SENTINEL + ) + + @property + def replication_enabled(self): + return self.replication_configured and not self.is_failed_over + + @property + def replication_targets(self): + if self.replication_enabled: + return list(self.adapters.keys()) + return [] + + def _get_device_configuration(self, is_primary=True, device_index=0): + conf = {} + if is_primary: + get_value = self.configuration.safe_get + backend_id = manager.VolumeManager.FAILBACK_SENTINEL + else: + get_value = self.replication_devices[device_index].get + backend_id = get_value("backend_id") + conf["backend_id"] = backend_id + conf["backend_name"] = ( + self.configuration.safe_get("volume_backend_name") or "powerstore" + ) + conf["ports"] = get_value(options.POWERSTORE_PORTS) or [] + conf["rest_ip"] = get_value("san_ip") + conf["rest_username"] = get_value("san_login") + conf["rest_password"] = get_value("san_password") + conf["verify_certificate"] = get_value("driver_ssl_cert_verify") + conf["certificate_path"] = get_value("driver_ssl_cert_path") + return conf diff --git a/cinder/volume/drivers/dell_emc/powerstore/options.py b/cinder/volume/drivers/dell_emc/powerstore/options.py index 4c0eeeaec6e..b460f0c3070 100644 --- a/cinder/volume/drivers/dell_emc/powerstore/options.py +++ b/cinder/volume/drivers/dell_emc/powerstore/options.py @@ -24,7 +24,12 @@ POWERSTORE_OPTS = [ cfg.ListOpt(POWERSTORE_APPLIANCES, default=[], help="Appliances names. Comma separated list of PowerStore " - "appliances names used to provision volumes. Required."), + "appliances names used to provision volumes.", + deprecated_for_removal=True, + deprecated_reason="Is not used anymore. " + "PowerStore Load Balancer is used to " + "provision volumes instead.", + deprecated_since="Wallaby"), cfg.ListOpt(POWERSTORE_PORTS, default=[], help="Allowed ports. Comma separated list of PowerStore " diff --git a/cinder/volume/drivers/dell_emc/powerstore/utils.py b/cinder/volume/drivers/dell_emc/powerstore/utils.py index d2bc1d333a5..86a12ab9250 100644 --- a/cinder/volume/drivers/dell_emc/powerstore/utils.py +++ b/cinder/volume/drivers/dell_emc/powerstore/utils.py @@ -23,6 +23,7 @@ from oslo_utils import units from cinder import exception from cinder.i18n import _ from cinder.objects import fields +from cinder.volume.drivers.dell_emc.powerstore import driver from cinder.volume import volume_utils @@ -151,3 +152,13 @@ def get_chap_credentials(): CHAP_DEFAULT_SECRET_LENGTH ) } + + +def get_protection_policy_from_volume(volume): + """Get PowerStore Protection policy name from volume type. + + :param volume: OpenStack Volume object + :return: Protection policy name + """ + + return volume.volume_type.extra_specs.get(driver.POWERSTORE_PP_KEY) 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 d6a2cb41b27..e4bfbc8d76c 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 @@ -18,6 +18,7 @@ Supported operations - Get volume statistics. - Attach a volume to multiple servers simultaneously (multiattach). - Revert a volume to a snapshot. +- OpenStack replication v2.1 support. Driver configuration ~~~~~~~~~~~~~~~~~~~~ @@ -41,8 +42,6 @@ Add the following content into ``/etc/cinder/cinder.conf``: volume_driver = cinder.volume.drivers.dell_emc.powerstore.driver.PowerStoreDriver # Backend name volume_backend_name = - # PowerStore appliances - powerstore_appliances = # Ex. Appliance-1,Appliance-2 # PowerStore allowed ports powerstore_ports = # Ex. 58:cc:f0:98:49:22:07:02,58:cc:f0:98:49:23:07:02 @@ -93,3 +92,72 @@ side. CHAP configuration is retrieved from the storage during driver initialization, no additional configuration is needed. Secrets are generated automatically. + +Replication support +~~~~~~~~~~~~~~~~~~~ + +Configure replication +^^^^^^^^^^^^^^^^^^^^^ + +#. Pair source and destination PowerStore systems. + +#. Create Protection policy and Replication rule with desired RPO. + +#. Enable replication in ``cinder.conf`` file. + + To enable replication feature for storage backend set ``replication_device`` + as below: + + .. code-block:: ini + + ... + replication_device = backend_id:powerstore_repl_1, + san_ip: , + san_login: , + san_password: + + * Only one replication device is supported for storage backend. + + * Replication device supports the same options as the main storage backend. + +#. Create volume type for volumes with replication enabled. + + .. code-block:: console + + $ openstack volume type create powerstore_replicated + $ openstack volume type set --property replication_enabled=' True' powerstore_replicated + +#. Set Protection policy name for volume type. + + .. code-block:: console + + $ openstack volume type set --property powerstore:protection_policy= \ + powerstore_replicated + +Failover host +^^^^^^^^^^^^^ + +In the event of a disaster, or where there is a required downtime the +administrator can issue the failover host command: + +.. code-block:: console + + $ cinder failover-host cinder_host@powerstore --backend_id powerstore_repl_1 + +After issuing Cinder failover-host command Cinder will switch to configured +replication device, however to get existing instances to use this target and +new paths to volumes it is necessary to first shelve Nova instances and then +unshelve them, this will effectively restart the Nova instance and +re-establish data paths between Nova instances and the volumes. + +.. code-block:: console + + $ nova shelve + $ nova unshelve [--availability-zone ] + +If the primary system becomes available, the administrator can initiate +failback operation using ``--backend_id default``: + +.. code-block:: console + + $ cinder failover-host cinder_host@powerstore --backend_id default diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index 123f4d067b0..412d0c1bd01 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -25,7 +25,7 @@ title=Dell EMC XtremeIO Storage Driver (FC, iSCSI) title=Dell EMC PowerMax (2000, 8000) Storage Driver (iSCSI, FC) [driver.dell_emc_powerstore] -title="Dell EMC PowerStore Storage Driver (iSCSI, FC)" +title=Dell EMC PowerStore Storage Driver (iSCSI, FC) [driver.dell_emc_sc] title=Dell EMC SC Series Storage Driver (iSCSI, FC) @@ -483,7 +483,7 @@ notes=Vendor drivers that support volume replication can report this to take advantage of Cinder's failover and failback commands. 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-replication-support-700016b83437602e.yaml b/releasenotes/notes/bp-powerstore-replication-support-700016b83437602e.yaml new file mode 100644 index 00000000000..447a558ef8b --- /dev/null +++ b/releasenotes/notes/bp-powerstore-replication-support-700016b83437602e.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + PowerStore driver: Add OpenStack replication v2.1 support. +deprecations: + - | + PowerStore driver: ``powerstore_appliances`` option is deprecated and + will be removed in a future release. Driver does not use this option + to determine which appliances to use. PowerStore uses its own + load balancer instead.