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
This commit is contained in:
Ivan Pchelintsev 2021-01-25 11:21:15 +03:00
parent 0fe910f130
commit f328341ed0
15 changed files with 903 additions and 376 deletions

View File

@ -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")

View File

@ -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)

View File

@ -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"])

View File

@ -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")

View File

@ -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)

View File

@ -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
)

View File

@ -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
)

View File

@ -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": {

View File

@ -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)

View File

@ -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

View File

@ -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 "

View File

@ -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)

View File

@ -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 = <Backend name>
# PowerStore appliances
powerstore_appliances = <Appliances names> # Ex. Appliance-1,Appliance-2
# PowerStore allowed ports
powerstore_ports = <Allowed 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: <Replication system San ip>,
san_login: <Replication system San username>,
san_password: <Replication system 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='<is> True' powerstore_replicated
#. Set Protection policy name for volume type.
.. code-block:: console
$ openstack volume type set --property powerstore:protection_policy=<protection policy name> \
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 <server>
$ nova unshelve [--availability-zone <availability_zone>] <server>
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

View File

@ -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

View File

@ -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.