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:
parent
0fe910f130
commit
f328341ed0
@ -18,6 +18,7 @@ from unittest import mock
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from cinder import context
|
||||||
from cinder.tests.unit import test
|
from cinder.tests.unit import test
|
||||||
from cinder.volume import configuration
|
from cinder.volume import configuration
|
||||||
from cinder.volume.drivers.dell_emc.powerstore import driver
|
from cinder.volume.drivers.dell_emc.powerstore import driver
|
||||||
@ -51,6 +52,7 @@ class MockResponse(requests.Response):
|
|||||||
class TestPowerStoreDriver(test.TestCase):
|
class TestPowerStoreDriver(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestPowerStoreDriver, self).setUp()
|
super(TestPowerStoreDriver, self).setUp()
|
||||||
|
self.context = context.RequestContext('fake', 'fake', auth_token=True)
|
||||||
self.configuration = configuration.Configuration(
|
self.configuration = configuration.Configuration(
|
||||||
options.POWERSTORE_OPTS,
|
options.POWERSTORE_OPTS,
|
||||||
configuration.SHARED_CONF_GROUP
|
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_ip", override="127.0.0.1")
|
||||||
self._override_shared_conf("san_login", override="test")
|
self._override_shared_conf("san_login", override="test")
|
||||||
self._override_shared_conf("san_password", override="test")
|
self._override_shared_conf("san_password", override="test")
|
||||||
self._override_shared_conf("powerstore_appliances",
|
|
||||||
override="test-appliance")
|
|
||||||
|
@ -22,47 +22,21 @@ from cinder.tests.unit.volume.drivers.dell_emc import powerstore
|
|||||||
class TestBase(powerstore.TestPowerStoreDriver):
|
class TestBase(powerstore.TestPowerStoreDriver):
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.get_chap_config")
|
"PowerStoreClient.get_chap_config")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
def test_configuration(self, mock_chap):
|
||||||
"PowerStoreClient.get_appliance_id_by_name")
|
|
||||||
def test_configuration(self, mock_appliance, mock_chap):
|
|
||||||
mock_appliance.return_value = "A1"
|
|
||||||
self.driver.check_for_setup_error()
|
self.driver.check_for_setup_error()
|
||||||
|
|
||||||
def test_configuration_rest_parameters_not_set(self):
|
def test_configuration_rest_parameters_not_set(self):
|
||||||
self.driver.adapter.client.rest_ip = None
|
self.driver.adapter.client.rest_ip = None
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
self.assertRaises(exception.InvalidInput,
|
||||||
self.driver.check_for_setup_error)
|
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."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.get_chap_config")
|
"PowerStoreClient.get_chap_config")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.get_appliance_id_by_name")
|
"PowerStoreClient.get_metrics")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
|
||||||
"PowerStoreClient.get_appliance_metrics")
|
|
||||||
def test_update_volume_stats(self,
|
def test_update_volume_stats(self,
|
||||||
mock_metrics,
|
mock_metrics,
|
||||||
mock_appliance,
|
|
||||||
mock_chap):
|
mock_chap):
|
||||||
mock_appliance.return_value = "A1"
|
|
||||||
mock_metrics.return_value = {
|
mock_metrics.return_value = {
|
||||||
"physical_total": 2147483648,
|
"physical_total": 2147483648,
|
||||||
"physical_used": 1073741824,
|
"physical_used": 1073741824,
|
||||||
@ -72,16 +46,65 @@ class TestBase(powerstore.TestPowerStoreDriver):
|
|||||||
|
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.get_chap_config")
|
"PowerStoreClient.get_chap_config")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
|
||||||
"PowerStoreClient.get_appliance_id_by_name")
|
|
||||||
@mock.patch("requests.request")
|
@mock.patch("requests.request")
|
||||||
def test_update_volume_stats_bad_status(self,
|
def test_update_volume_stats_bad_status(self,
|
||||||
mock_metrics,
|
mock_metrics,
|
||||||
mock_appliance,
|
|
||||||
mock_chap):
|
mock_chap):
|
||||||
mock_appliance.return_value = "A1"
|
|
||||||
mock_metrics.return_value = powerstore.MockResponse(rc=400)
|
mock_metrics.return_value = powerstore.MockResponse(rc=400)
|
||||||
self.driver.check_for_setup_error()
|
self.driver.check_for_setup_error()
|
||||||
error = self.assertRaises(exception.VolumeBackendAPIException,
|
error = self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.driver._update_volume_stats)
|
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)
|
||||||
|
@ -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"])
|
@ -24,28 +24,26 @@ from cinder.tests.unit.volume.drivers.dell_emc import powerstore
|
|||||||
class TestSnapshotCreateDelete(powerstore.TestPowerStoreDriver):
|
class TestSnapshotCreateDelete(powerstore.TestPowerStoreDriver):
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.get_chap_config")
|
"PowerStoreClient.get_chap_config")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
def setUp(self, mock_chap):
|
||||||
"PowerStoreClient.get_appliance_id_by_name")
|
|
||||||
def setUp(self, mock_appliance, mock_chap):
|
|
||||||
super(TestSnapshotCreateDelete, self).setUp()
|
super(TestSnapshotCreateDelete, self).setUp()
|
||||||
mock_appliance.return_value = "A1"
|
|
||||||
self.driver.check_for_setup_error()
|
self.driver.check_for_setup_error()
|
||||||
self.volume = fake_volume.fake_volume_obj(
|
self.volume = fake_volume.fake_volume_obj(
|
||||||
{},
|
self.context,
|
||||||
host="host@backend#test-appliance",
|
host="host@backend",
|
||||||
provider_id="fake_id",
|
provider_id="fake_id",
|
||||||
size=8
|
size=8
|
||||||
)
|
)
|
||||||
self.snapshot = fake_snapshot.fake_snapshot_obj(
|
self.snapshot = fake_snapshot.fake_snapshot_obj(
|
||||||
{},
|
self.context,
|
||||||
provider_id="fake_id_1",
|
|
||||||
volume=self.volume
|
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."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.create_snapshot")
|
"PowerStoreClient.create_snapshot")
|
||||||
def test_create_snapshot(self, mock_create):
|
def test_create_snapshot(self, mock_create):
|
||||||
mock_create.return_value = self.snapshot.provider_id
|
|
||||||
self.driver.create_snapshot(self.snapshot)
|
self.driver.create_snapshot(self.snapshot)
|
||||||
|
|
||||||
@mock.patch("requests.request")
|
@mock.patch("requests.request")
|
||||||
|
@ -26,17 +26,14 @@ from cinder.volume.drivers.dell_emc.powerstore import utils
|
|||||||
class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver):
|
class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver):
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.get_chap_config")
|
"PowerStoreClient.get_chap_config")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
def setUp(self, mock_chap):
|
||||||
"PowerStoreClient.get_appliance_id_by_name")
|
|
||||||
def setUp(self, mock_appliance, mock_chap):
|
|
||||||
super(TestVolumeAttachDetach, self).setUp()
|
super(TestVolumeAttachDetach, self).setUp()
|
||||||
mock_appliance.return_value = "A1"
|
|
||||||
mock_chap.return_value = {"mode": "Single"}
|
mock_chap.return_value = {"mode": "Single"}
|
||||||
self.iscsi_driver.check_for_setup_error()
|
self.iscsi_driver.check_for_setup_error()
|
||||||
self.fc_driver.check_for_setup_error()
|
self.fc_driver.check_for_setup_error()
|
||||||
self.volume = fake_volume.fake_volume_obj(
|
self.volume = fake_volume.fake_volume_obj(
|
||||||
{},
|
self.context,
|
||||||
host="host@backend#test-appliance",
|
host="host@backend",
|
||||||
provider_id="fake_id",
|
provider_id="fake_id",
|
||||||
size=8
|
size=8
|
||||||
)
|
)
|
||||||
@ -124,12 +121,12 @@ class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver):
|
|||||||
self.assertNotIn("auth_password", connection_properties["data"])
|
self.assertNotIn("auth_password", connection_properties["data"])
|
||||||
|
|
||||||
def test_get_fc_targets(self):
|
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))
|
self.assertEqual(2, len(wwns))
|
||||||
|
|
||||||
def test_get_fc_targets_filtered(self):
|
def test_get_fc_targets_filtered(self):
|
||||||
self.fc_driver.adapter.allowed_ports = ["58:cc:f0:98:49:23:07:02"]
|
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.assertEqual(1, len(wwns))
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
utils.fc_wwn_to_string("58:cc:f0:98:49:21:07:02") in wwns
|
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):
|
def test_get_fc_targets_filtered_no_matched_ports(self):
|
||||||
self.fc_driver.adapter.allowed_ports = ["fc_wwn_1", "fc_wwn_2"]
|
self.fc_driver.adapter.allowed_ports = ["fc_wwn_1", "fc_wwn_2"]
|
||||||
error = self.assertRaises(exception.VolumeBackendAPIException,
|
error = self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.fc_driver.adapter._get_fc_targets,
|
self.fc_driver.adapter._get_fc_targets)
|
||||||
"A1")
|
|
||||||
self.assertIn("There are no accessible Fibre Channel targets on the "
|
self.assertIn("There are no accessible Fibre Channel targets on the "
|
||||||
"system.", error.msg)
|
"system.", error.msg)
|
||||||
|
|
||||||
def test_get_iscsi_targets(self):
|
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.assertTrue(len(iqns) == len(portals))
|
||||||
self.assertEqual(2, len(portals))
|
self.assertEqual(2, len(portals))
|
||||||
|
|
||||||
def test_get_iscsi_targets_filtered(self):
|
def test_get_iscsi_targets_filtered(self):
|
||||||
self.iscsi_driver.adapter.allowed_ports = ["1.2.3.4"]
|
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.assertTrue(len(iqns) == len(portals))
|
||||||
self.assertEqual(1, len(portals))
|
self.assertEqual(1, len(portals))
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
@ -160,8 +156,7 @@ class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver):
|
|||||||
def test_get_iscsi_targets_filtered_no_matched_ports(self):
|
def test_get_iscsi_targets_filtered_no_matched_ports(self):
|
||||||
self.iscsi_driver.adapter.allowed_ports = ["1.1.1.1", "2.2.2.2"]
|
self.iscsi_driver.adapter.allowed_ports = ["1.1.1.1", "2.2.2.2"]
|
||||||
error = self.assertRaises(exception.VolumeBackendAPIException,
|
error = self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
self.iscsi_driver.adapter._get_iscsi_targets,
|
self.iscsi_driver.adapter._get_iscsi_targets)
|
||||||
"A1")
|
|
||||||
self.assertIn("There are no accessible iSCSI targets on the system.",
|
self.assertIn("There are no accessible iSCSI targets on the system.",
|
||||||
error.msg)
|
error.msg)
|
||||||
|
|
||||||
|
@ -24,15 +24,12 @@ from cinder.volume.drivers.dell_emc.powerstore import client
|
|||||||
class TestVolumeCreateDeleteExtend(powerstore.TestPowerStoreDriver):
|
class TestVolumeCreateDeleteExtend(powerstore.TestPowerStoreDriver):
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.get_chap_config")
|
"PowerStoreClient.get_chap_config")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
def setUp(self, mock_chap):
|
||||||
"PowerStoreClient.get_appliance_id_by_name")
|
|
||||||
def setUp(self, mock_appliance, mock_chap):
|
|
||||||
super(TestVolumeCreateDeleteExtend, self).setUp()
|
super(TestVolumeCreateDeleteExtend, self).setUp()
|
||||||
mock_appliance.return_value = "A1"
|
|
||||||
self.driver.check_for_setup_error()
|
self.driver.check_for_setup_error()
|
||||||
self.volume = fake_volume.fake_volume_obj(
|
self.volume = fake_volume.fake_volume_obj(
|
||||||
{},
|
self.context,
|
||||||
host="host@backend#test-appliance",
|
host="host@backend",
|
||||||
provider_id="fake_id",
|
provider_id="fake_id",
|
||||||
size=8
|
size=8
|
||||||
)
|
)
|
||||||
|
@ -24,29 +24,29 @@ from cinder.tests.unit.volume.drivers.dell_emc import powerstore
|
|||||||
class TestVolumeCreateFromSource(powerstore.TestPowerStoreDriver):
|
class TestVolumeCreateFromSource(powerstore.TestPowerStoreDriver):
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.get_chap_config")
|
"PowerStoreClient.get_chap_config")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
def setUp(self, mock_chap):
|
||||||
"PowerStoreClient.get_appliance_id_by_name")
|
|
||||||
def setUp(self, mock_appliance, mock_chap):
|
|
||||||
super(TestVolumeCreateFromSource, self).setUp()
|
super(TestVolumeCreateFromSource, self).setUp()
|
||||||
mock_appliance.return_value = "A1"
|
|
||||||
self.driver.check_for_setup_error()
|
self.driver.check_for_setup_error()
|
||||||
self.volume = fake_volume.fake_volume_obj(
|
self.volume = fake_volume.fake_volume_obj(
|
||||||
{},
|
self.context,
|
||||||
host="host@backend#test-appliance",
|
host="host@backend",
|
||||||
provider_id="fake_id",
|
provider_id="fake_id",
|
||||||
size=8
|
size=8
|
||||||
)
|
)
|
||||||
self.source_volume = fake_volume.fake_volume_obj(
|
self.source_volume = fake_volume.fake_volume_obj(
|
||||||
{},
|
self.context,
|
||||||
host="host@backend#test-appliance",
|
host="host@backend",
|
||||||
provider_id="fake_id_1",
|
provider_id="fake_id_1",
|
||||||
size=8
|
size=8
|
||||||
)
|
)
|
||||||
self.source_snapshot = fake_snapshot.fake_snapshot_obj(
|
self.source_snapshot = fake_snapshot.fake_snapshot_obj(
|
||||||
{},
|
self.context,
|
||||||
provider_id="fake_id_2",
|
volume=self.source_volume,
|
||||||
volume_size=8
|
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."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.clone_volume_or_snapshot")
|
"PowerStoreClient.clone_volume_or_snapshot")
|
||||||
@ -91,7 +91,7 @@ class TestVolumeCreateFromSource(powerstore.TestPowerStoreDriver):
|
|||||||
mock_create_request.return_value = powerstore.MockResponse(rc=400)
|
mock_create_request.return_value = powerstore.MockResponse(rc=400)
|
||||||
error = self.assertRaises(
|
error = self.assertRaises(
|
||||||
exception.VolumeBackendAPIException,
|
exception.VolumeBackendAPIException,
|
||||||
self.driver.adapter._create_volume_from_source,
|
self.driver.adapter.create_volume_from_source,
|
||||||
self.volume,
|
self.volume,
|
||||||
self.source_volume
|
self.source_volume
|
||||||
)
|
)
|
||||||
@ -109,7 +109,7 @@ class TestVolumeCreateFromSource(powerstore.TestPowerStoreDriver):
|
|||||||
self.volume.size = 16
|
self.volume.size = 16
|
||||||
error = self.assertRaises(
|
error = self.assertRaises(
|
||||||
exception.VolumeBackendAPIException,
|
exception.VolumeBackendAPIException,
|
||||||
self.driver.adapter._create_volume_from_source,
|
self.driver.adapter.create_volume_from_source,
|
||||||
self.volume,
|
self.volume,
|
||||||
self.source_volume
|
self.source_volume
|
||||||
)
|
)
|
||||||
|
@ -21,11 +21,11 @@ from oslo_utils import strutils
|
|||||||
from cinder import coordination
|
from cinder import coordination
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
|
from cinder.objects import fields
|
||||||
from cinder.objects.snapshot import Snapshot
|
from cinder.objects.snapshot import Snapshot
|
||||||
from cinder.volume.drivers.dell_emc.powerstore import client
|
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.drivers.dell_emc.powerstore import utils
|
||||||
from cinder.volume import volume_utils
|
from cinder.volume import manager
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -35,15 +35,19 @@ CHAP_MODE_SINGLE = "Single"
|
|||||||
|
|
||||||
|
|
||||||
class CommonAdapter(object):
|
class CommonAdapter(object):
|
||||||
def __init__(self, active_backend_id, configuration):
|
def __init__(self,
|
||||||
self.active_backend_id = active_backend_id
|
backend_id,
|
||||||
self.appliances = None
|
backend_name,
|
||||||
self.appliances_to_ids_map = {}
|
ports,
|
||||||
self.client = None
|
**client_config):
|
||||||
self.configuration = configuration
|
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.storage_protocol = None
|
||||||
self.allowed_ports = None
|
self.use_chap_auth = False
|
||||||
self.use_chap_auth = None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def initiators(connector):
|
def initiators(connector):
|
||||||
@ -62,79 +66,71 @@ class CommonAdapter(object):
|
|||||||
return True
|
return True
|
||||||
return port.lower() in self.allowed_ports
|
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
|
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):
|
def check_for_setup_error(self):
|
||||||
self.client.check_for_setup_error()
|
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:
|
if self.storage_protocol == PROTOCOL_ISCSI:
|
||||||
chap_config = self.client.get_chap_config()
|
chap_config = self.client.get_chap_config()
|
||||||
if chap_config.get("mode") == CHAP_MODE_SINGLE:
|
if chap_config.get("mode") == CHAP_MODE_SINGLE:
|
||||||
self.use_chap_auth = True
|
self.use_chap_auth = True
|
||||||
LOG.debug("Successfully initialized PowerStore %(protocol)s adapter. "
|
LOG.debug("Successfully initialized PowerStore %(protocol)s adapter "
|
||||||
"PowerStore appliances: %(appliances)s. "
|
"for %(backend_id)s %(backend_name)s backend. "
|
||||||
"Allowed ports: %(allowed_ports)s. "
|
"Allowed ports: %(allowed_ports)s. "
|
||||||
"Use CHAP authentication: %(use_chap_auth)s.",
|
"Use CHAP authentication: %(use_chap_auth)s.",
|
||||||
{
|
{
|
||||||
"protocol": self.storage_protocol,
|
"protocol": self.storage_protocol,
|
||||||
"appliances": self.appliances,
|
"backend_id": self.backend_id,
|
||||||
|
"backend_name": self.backend_name,
|
||||||
"allowed_ports": self.allowed_ports,
|
"allowed_ports": self.allowed_ports,
|
||||||
"use_chap_auth": self.use_chap_auth,
|
"use_chap_auth": self.use_chap_auth,
|
||||||
})
|
})
|
||||||
|
|
||||||
def create_volume(self, volume):
|
def create_volume(self, volume):
|
||||||
appliance_name = volume_utils.extract_host(volume.host, "pool")
|
if volume.is_replicated():
|
||||||
appliance_id = self.appliances_to_ids_map[appliance_name]
|
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 "
|
LOG.debug("Create PowerStore volume %(volume_name)s of size "
|
||||||
"%(volume_size)s GiB with id %(volume_id)s on appliance "
|
"%(volume_size)s GiB with id %(volume_id)s. "
|
||||||
"%(appliance_name)s.",
|
"Protection policy: %(pp_name)s.",
|
||||||
{
|
{
|
||||||
"volume_name": volume.name,
|
"volume_name": volume.name,
|
||||||
"volume_size": volume.size,
|
"volume_size": volume.size,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"appliance_name": appliance_name,
|
"pp_name": pp_name,
|
||||||
})
|
})
|
||||||
size_in_bytes = utils.gib_to_bytes(volume.size)
|
size_in_bytes = utils.gib_to_bytes(volume.size)
|
||||||
provider_id = self.client.create_volume(appliance_id,
|
provider_id = self.client.create_volume(volume.name,
|
||||||
volume.name,
|
size_in_bytes,
|
||||||
size_in_bytes)
|
pp_id)
|
||||||
LOG.debug("Successfully created PowerStore volume %(volume_name)s of "
|
LOG.debug("Successfully created PowerStore volume %(volume_name)s of "
|
||||||
"size %(volume_size)s GiB with id %(volume_id)s on "
|
"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.",
|
"PowerStore volume id: %(volume_provider_id)s.",
|
||||||
{
|
{
|
||||||
"volume_name": volume.name,
|
"volume_name": volume.name,
|
||||||
"volume_size": volume.size,
|
"volume_size": volume.size,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"appliance_name": appliance_name,
|
"pp_name": pp_name,
|
||||||
"volume_provider_id": provider_id,
|
"volume_provider_id": provider_id,
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
"provider_id": provider_id,
|
"provider_id": provider_id,
|
||||||
|
"replication_status": replication_status,
|
||||||
}
|
}
|
||||||
|
|
||||||
def delete_volume(self, volume):
|
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 "
|
LOG.warning("Volume %(volume_name)s with id %(volume_id)s "
|
||||||
"does not have provider_id thus does not "
|
"does not have provider_id thus does not "
|
||||||
"map to PowerStore volume.",
|
"map to PowerStore volume.",
|
||||||
@ -149,20 +145,21 @@ class CommonAdapter(object):
|
|||||||
{
|
{
|
||||||
"volume_name": volume.name,
|
"volume_name": volume.name,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"volume_provider_id": volume.provider_id,
|
"volume_provider_id": provider_id,
|
||||||
})
|
})
|
||||||
self._detach_volume_from_hosts(volume)
|
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 "
|
LOG.debug("Successfully deleted PowerStore volume %(volume_name)s "
|
||||||
"with id %(volume_id)s. PowerStore volume id: "
|
"with id %(volume_id)s. PowerStore volume id: "
|
||||||
"%(volume_provider_id)s.",
|
"%(volume_provider_id)s.",
|
||||||
{
|
{
|
||||||
"volume_name": volume.name,
|
"volume_name": volume.name,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"volume_provider_id": volume.provider_id,
|
"volume_provider_id": provider_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
def extend_volume(self, volume, new_size):
|
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 "
|
LOG.debug("Extend PowerStore volume %(volume_name)s of size "
|
||||||
"%(volume_size)s GiB with id %(volume_id)s to "
|
"%(volume_size)s GiB with id %(volume_id)s to "
|
||||||
"%(volume_new_size)s GiB. "
|
"%(volume_new_size)s GiB. "
|
||||||
@ -172,10 +169,10 @@ class CommonAdapter(object):
|
|||||||
"volume_size": volume.size,
|
"volume_size": volume.size,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"volume_new_size": new_size,
|
"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)
|
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 "
|
LOG.debug("Successfully extended PowerStore volume %(volume_name)s "
|
||||||
"of size %(volume_size)s GiB with id "
|
"of size %(volume_size)s GiB with id "
|
||||||
"%(volume_id)s to %(volume_new_size)s GiB. "
|
"%(volume_id)s to %(volume_new_size)s GiB. "
|
||||||
@ -185,10 +182,11 @@ class CommonAdapter(object):
|
|||||||
"volume_size": volume.size,
|
"volume_size": volume.size,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"volume_new_size": new_size,
|
"volume_new_size": new_size,
|
||||||
"volume_provider_id": volume.provider_id,
|
"volume_provider_id": provider_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
def create_snapshot(self, snapshot):
|
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 "
|
LOG.debug("Create PowerStore snapshot %(snapshot_name)s with id "
|
||||||
"%(snapshot_id)s of volume %(volume_name)s with id "
|
"%(snapshot_id)s of volume %(volume_name)s with id "
|
||||||
"%(volume_id)s. PowerStore volume id: "
|
"%(volume_id)s. PowerStore volume id: "
|
||||||
@ -198,124 +196,57 @@ class CommonAdapter(object):
|
|||||||
"snapshot_id": snapshot.id,
|
"snapshot_id": snapshot.id,
|
||||||
"volume_name": snapshot.volume.name,
|
"volume_name": snapshot.volume.name,
|
||||||
"volume_id": snapshot.volume.id,
|
"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(
|
self.client.create_snapshot(volume_provider_id, snapshot.name)
|
||||||
snapshot.volume.provider_id,
|
|
||||||
snapshot.name)
|
|
||||||
LOG.debug("Successfully created PowerStore snapshot %(snapshot_name)s "
|
LOG.debug("Successfully created PowerStore snapshot %(snapshot_name)s "
|
||||||
"with id %(snapshot_id)s of volume %(volume_name)s with "
|
"with id %(snapshot_id)s of volume %(volume_name)s with "
|
||||||
"id %(volume_id)s. PowerStore snapshot id: "
|
"id %(volume_id)s. PowerStore volume id: "
|
||||||
"%(snapshot_provider_id)s, volume id: "
|
|
||||||
"%(volume_provider_id)s.",
|
"%(volume_provider_id)s.",
|
||||||
{
|
{
|
||||||
"snapshot_name": snapshot.name,
|
"snapshot_name": snapshot.name,
|
||||||
"snapshot_id": snapshot.id,
|
"snapshot_id": snapshot.id,
|
||||||
"volume_name": snapshot.volume.name,
|
"volume_name": snapshot.volume.name,
|
||||||
"volume_id": snapshot.volume.id,
|
"volume_id": snapshot.volume.id,
|
||||||
"snapshot_provider_id": snapshot_provider_id,
|
"volume_provider_id": volume_provider_id,
|
||||||
"volume_provider_id": snapshot.volume.provider_id,
|
|
||||||
})
|
})
|
||||||
return {
|
|
||||||
"provider_id": snapshot_provider_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
def delete_snapshot(self, snapshot):
|
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 "
|
LOG.debug("Delete PowerStore snapshot %(snapshot_name)s with id "
|
||||||
"%(snapshot_id)s of volume %(volume_name)s with "
|
"%(snapshot_id)s of volume %(volume_name)s with "
|
||||||
"id %(volume_id)s. PowerStore snapshot id: "
|
"id %(volume_id)s. PowerStore volume id: "
|
||||||
"%(snapshot_provider_id)s, volume id: "
|
|
||||||
"%(volume_provider_id)s.",
|
"%(volume_provider_id)s.",
|
||||||
{
|
{
|
||||||
"snapshot_name": snapshot.name,
|
"snapshot_name": snapshot.name,
|
||||||
"snapshot_id": snapshot.id,
|
"snapshot_id": snapshot.id,
|
||||||
"volume_name": snapshot.volume.name,
|
"volume_name": snapshot.volume.name,
|
||||||
"volume_id": snapshot.volume.id,
|
"volume_id": snapshot.volume.id,
|
||||||
"snapshot_provider_id": snapshot.provider_id,
|
"volume_provider_id": volume_provider_id,
|
||||||
"volume_provider_id": snapshot.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")
|
entity="snapshot")
|
||||||
LOG.debug("Successfully deleted PowerStore snapshot %(snapshot_name)s "
|
LOG.debug("Successfully deleted PowerStore snapshot %(snapshot_name)s "
|
||||||
"with id %(snapshot_id)s of volume %(volume_name)s with "
|
"with id %(snapshot_id)s of volume %(volume_name)s with "
|
||||||
"id %(volume_id)s. PowerStore snapshot id: "
|
"id %(volume_id)s. PowerStore volume id: "
|
||||||
"%(snapshot_provider_id)s, volume id: "
|
|
||||||
"%(volume_provider_id)s.",
|
"%(volume_provider_id)s.",
|
||||||
{
|
{
|
||||||
"snapshot_name": snapshot.name,
|
"snapshot_name": snapshot.name,
|
||||||
"snapshot_id": snapshot.id,
|
"snapshot_id": snapshot.id,
|
||||||
"volume_name": snapshot.volume.name,
|
"volume_name": snapshot.volume.name,
|
||||||
"volume_id": snapshot.volume.id,
|
"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,
|
"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):
|
def initialize_connection(self, volume, connector, **kwargs):
|
||||||
connection_properties = self._connect_volume(volume, connector)
|
connection_properties = self._connect_volume(volume, connector)
|
||||||
@ -336,76 +267,96 @@ class CommonAdapter(object):
|
|||||||
|
|
||||||
def update_volume_stats(self):
|
def update_volume_stats(self):
|
||||||
stats = {
|
stats = {
|
||||||
"volume_backend_name": (
|
"volume_backend_name": self.backend_name,
|
||||||
self.configuration.safe_get("volume_backend_name") or
|
|
||||||
"powerstore"
|
|
||||||
),
|
|
||||||
"storage_protocol": self.storage_protocol,
|
"storage_protocol": self.storage_protocol,
|
||||||
"thick_provisioning_support": False,
|
"thick_provisioning_support": False,
|
||||||
"thin_provisioning_support": True,
|
"thin_provisioning_support": True,
|
||||||
"compression_support": True,
|
"compression_support": True,
|
||||||
"multiattach": True,
|
"multiattach": True,
|
||||||
"pools": [],
|
|
||||||
}
|
}
|
||||||
backend_total_capacity = 0
|
backend_stats = self.client.get_metrics()
|
||||||
backend_free_capacity = 0
|
backend_total_capacity = utils.bytes_to_gib(
|
||||||
for appliance_name in self.appliances:
|
backend_stats["physical_total"]
|
||||||
appliance_stats = self.client.get_appliance_metrics(
|
)
|
||||||
self.appliances_to_ids_map[appliance_name]
|
backend_free_capacity = (
|
||||||
)
|
backend_total_capacity -
|
||||||
appliance_total_capacity = utils.bytes_to_gib(
|
utils.bytes_to_gib(backend_stats["physical_used"])
|
||||||
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)
|
|
||||||
stats["total_capacity_gb"] = backend_total_capacity
|
stats["total_capacity_gb"] = backend_total_capacity
|
||||||
stats["free_capacity_gb"] = backend_free_capacity
|
stats["free_capacity_gb"] = backend_free_capacity
|
||||||
LOG.debug("Free capacity for backend '%(backend)s': "
|
LOG.debug("Free capacity for backend '%(backend)s': "
|
||||||
"%(free)s GiB, total capacity: %(total)s GiB.",
|
"%(free)s GiB, total capacity: %(total)s GiB.",
|
||||||
{
|
{
|
||||||
"backend": stats["volume_backend_name"],
|
"backend": self.backend_name,
|
||||||
"free": backend_free_capacity,
|
"free": backend_free_capacity,
|
||||||
"total": backend_total_capacity,
|
"total": backend_total_capacity,
|
||||||
})
|
})
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def _create_volume_from_source(self, volume, source):
|
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
|
|
||||||
"""
|
|
||||||
|
|
||||||
if isinstance(source, Snapshot):
|
if isinstance(source, Snapshot):
|
||||||
entity = "snapshot"
|
entity = "snapshot"
|
||||||
source_size = source.volume_size
|
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:
|
else:
|
||||||
entity = "volume"
|
entity = "volume"
|
||||||
source_size = source.size
|
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_provider_id = self.client.clone_volume_or_snapshot(
|
||||||
volume.name,
|
volume.name,
|
||||||
source.provider_id,
|
source_provider_id,
|
||||||
|
pp_id,
|
||||||
entity
|
entity
|
||||||
)
|
)
|
||||||
if volume.size > source_size:
|
if volume.size > source_size:
|
||||||
size_in_bytes = utils.gib_to_bytes(volume.size)
|
size_in_bytes = utils.gib_to_bytes(volume.size)
|
||||||
self.client.extend_volume(volume_provider_id, size_in_bytes)
|
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):
|
def _filter_hosts_by_initiators(self, initiators):
|
||||||
"""Filter hosts by given list of initiators.
|
"""Filter hosts by given list of initiators.
|
||||||
@ -590,6 +541,7 @@ class CommonAdapter(object):
|
|||||||
:return: attached volume logical number
|
:return: attached volume logical number
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
provider_id = self._get_volume_provider_id(volume)
|
||||||
LOG.debug("Attach PowerStore volume %(volume_name)s with id "
|
LOG.debug("Attach PowerStore volume %(volume_name)s with id "
|
||||||
"%(volume_id)s to host %(host_name)s. PowerStore volume id: "
|
"%(volume_id)s to host %(host_name)s. PowerStore volume id: "
|
||||||
"%(volume_provider_id)s, host id: %(host_provider_id)s.",
|
"%(volume_provider_id)s, host id: %(host_provider_id)s.",
|
||||||
@ -597,13 +549,11 @@ class CommonAdapter(object):
|
|||||||
"volume_name": volume.name,
|
"volume_name": volume.name,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"host_name": host["name"],
|
"host_name": host["name"],
|
||||||
"volume_provider_id": volume.provider_id,
|
"volume_provider_id": provider_id,
|
||||||
"host_provider_id": host["id"],
|
"host_provider_id": host["id"],
|
||||||
})
|
})
|
||||||
self.client.attach_volume_to_host(host["id"], volume.provider_id)
|
self.client.attach_volume_to_host(host["id"], provider_id)
|
||||||
volume_lun = self.client.get_volume_lun(
|
volume_lun = self.client.get_volume_lun(host["id"], provider_id)
|
||||||
host["id"], volume.provider_id
|
|
||||||
)
|
|
||||||
LOG.debug("Successfully attached PowerStore volume %(volume_name)s "
|
LOG.debug("Successfully attached PowerStore volume %(volume_name)s "
|
||||||
"with id %(volume_id)s to host %(host_name)s. "
|
"with id %(volume_id)s to host %(host_name)s. "
|
||||||
"PowerStore volume id: %(volume_provider_id)s, "
|
"PowerStore volume id: %(volume_provider_id)s, "
|
||||||
@ -613,7 +563,7 @@ class CommonAdapter(object):
|
|||||||
"volume_name": volume.name,
|
"volume_name": volume.name,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"host_name": host["name"],
|
"host_name": host["name"],
|
||||||
"volume_provider_id": volume.provider_id,
|
"volume_provider_id": provider_id,
|
||||||
"host_provider_id": host["id"],
|
"host_provider_id": host["id"],
|
||||||
"volume_lun": volume_lun,
|
"volume_lun": volume_lun,
|
||||||
})
|
})
|
||||||
@ -638,14 +588,11 @@ class CommonAdapter(object):
|
|||||||
:return: volume connection properties
|
: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(
|
chap_credentials, volume_lun = self._create_host_and_attach(
|
||||||
connector,
|
connector,
|
||||||
volume
|
volume
|
||||||
)
|
)
|
||||||
connection_properties = self._get_connection_properties(appliance_id,
|
connection_properties = self._get_connection_properties(volume_lun)
|
||||||
volume_lun)
|
|
||||||
if self.use_chap_auth:
|
if self.use_chap_auth:
|
||||||
connection_properties["data"]["auth_method"] = "CHAP"
|
connection_properties["data"]["auth_method"] = "CHAP"
|
||||||
connection_properties["data"]["auth_username"] = (
|
connection_properties["data"]["auth_username"] = (
|
||||||
@ -666,11 +613,10 @@ class CommonAdapter(object):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
provider_id = self._get_volume_provider_id(volume)
|
||||||
if hosts_to_detach is None:
|
if hosts_to_detach is None:
|
||||||
# Force detach. Get all mapped hosts and detach.
|
# Force detach. Get all mapped hosts and detach.
|
||||||
hosts_to_detach = self.client.get_volume_mapped_hosts(
|
hosts_to_detach = self.client.get_volume_mapped_hosts(provider_id)
|
||||||
volume.provider_id
|
|
||||||
)
|
|
||||||
if not hosts_to_detach:
|
if not hosts_to_detach:
|
||||||
# Volume is not attached to any host.
|
# Volume is not attached to any host.
|
||||||
return
|
return
|
||||||
@ -680,11 +626,11 @@ class CommonAdapter(object):
|
|||||||
{
|
{
|
||||||
"volume_name": volume.name,
|
"volume_name": volume.name,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"volume_provider_id": volume.provider_id,
|
"volume_provider_id": provider_id,
|
||||||
"hosts_provider_ids": hosts_to_detach,
|
"hosts_provider_ids": hosts_to_detach,
|
||||||
})
|
})
|
||||||
for host_id in 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 "
|
LOG.debug("Successfully detached PowerStore volume "
|
||||||
"%(volume_name)s with id %(volume_id)s from hosts. "
|
"%(volume_name)s with id %(volume_id)s from hosts. "
|
||||||
"PowerStore volume id: %(volume_provider_id)s, "
|
"PowerStore volume id: %(volume_provider_id)s, "
|
||||||
@ -692,7 +638,7 @@ class CommonAdapter(object):
|
|||||||
{
|
{
|
||||||
"volume_name": volume.name,
|
"volume_name": volume.name,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"volume_provider_id": volume.provider_id,
|
"volume_provider_id": provider_id,
|
||||||
"hosts_provider_ids": hosts_to_detach,
|
"hosts_provider_ids": hosts_to_detach,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -721,40 +667,130 @@ class CommonAdapter(object):
|
|||||||
self._detach_volume_from_hosts(volume, [host["id"]])
|
self._detach_volume_from_hosts(volume, [host["id"]])
|
||||||
|
|
||||||
def revert_to_snapshot(self, volume, snapshot):
|
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 "
|
LOG.debug("Restore PowerStore volume %(volume_name)s with id "
|
||||||
"%(volume_id)s from snapshot %(snapshot_name)s with id "
|
"%(volume_id)s from snapshot %(snapshot_name)s with id "
|
||||||
"%(snapshot_id)s. PowerStore volume id: "
|
"%(snapshot_id)s. PowerStore volume id: "
|
||||||
"%(volume_provider_id)s, snapshot id: "
|
"%(volume_provider_id)s.",
|
||||||
"%(snapshot_provider_id)s.",
|
|
||||||
{
|
{
|
||||||
"volume_name": volume.name,
|
"volume_name": volume.name,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"snapshot_name": snapshot.name,
|
"snapshot_name": snapshot.name,
|
||||||
"snapshot_id": snapshot.id,
|
"snapshot_id": snapshot.id,
|
||||||
"volume_provider_id": volume.provider_id,
|
"volume_provider_id": volume_provider_id,
|
||||||
"snapshot_provider_id": snapshot.provider_id,
|
|
||||||
})
|
})
|
||||||
self.client.restore_from_snapshot(volume.provider_id,
|
snapshot_provider_id = self.client.get_snapshot_id_by_name(
|
||||||
snapshot.provider_id)
|
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 "
|
LOG.debug("Successfully restored PowerStore volume %(volume_name)s "
|
||||||
"with id %(volume_id)s from snapshot %(snapshot_name)s "
|
"with id %(volume_id)s from snapshot %(snapshot_name)s "
|
||||||
"with id %(snapshot_id)s. PowerStore volume id: "
|
"with id %(snapshot_id)s. PowerStore volume id: "
|
||||||
"%(volume_provider_id)s, snapshot id: "
|
"%(volume_provider_id)s.",
|
||||||
"%(snapshot_provider_id)s.",
|
|
||||||
{
|
{
|
||||||
"volume_name": volume.name,
|
"volume_name": volume.name,
|
||||||
"volume_id": volume.id,
|
"volume_id": volume.id,
|
||||||
"snapshot_name": snapshot.name,
|
"snapshot_name": snapshot.name,
|
||||||
"snapshot_id": snapshot.id,
|
"snapshot_id": snapshot.id,
|
||||||
"volume_provider_id": volume.provider_id,
|
"volume_provider_id": volume_provider_id,
|
||||||
"snapshot_provider_id": snapshot.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):
|
class FibreChannelAdapter(CommonAdapter):
|
||||||
def __init__(self, active_backend_id, configuration):
|
def __init__(self, **kwargs):
|
||||||
super(FibreChannelAdapter, self).__init__(active_backend_id,
|
super(FibreChannelAdapter, self).__init__(**kwargs)
|
||||||
configuration)
|
|
||||||
self.storage_protocol = PROTOCOL_FC
|
self.storage_protocol = PROTOCOL_FC
|
||||||
self.driver_volume_type = "fibre_channel"
|
self.driver_volume_type = "fibre_channel"
|
||||||
|
|
||||||
@ -762,15 +798,14 @@ class FibreChannelAdapter(CommonAdapter):
|
|||||||
def initiators(connector):
|
def initiators(connector):
|
||||||
return utils.extract_fc_wwpns(connector)
|
return utils.extract_fc_wwpns(connector)
|
||||||
|
|
||||||
def _get_fc_targets(self, appliance_id):
|
def _get_fc_targets(self):
|
||||||
"""Get available FC WWNs for PowerStore appliance.
|
"""Get available FC WWNs.
|
||||||
|
|
||||||
:param appliance_id: PowerStore appliance id
|
|
||||||
:return: list of FC WWNs
|
:return: list of FC WWNs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
wwns = []
|
wwns = []
|
||||||
fc_ports = self.client.get_fc_port(appliance_id)
|
fc_ports = self.client.get_fc_port()
|
||||||
for port in fc_ports:
|
for port in fc_ports:
|
||||||
if self._port_is_allowed(port["wwn"]):
|
if self._port_is_allowed(port["wwn"]):
|
||||||
wwns.append(utils.fc_wwn_to_string(port["wwn"]))
|
wwns.append(utils.fc_wwn_to_string(port["wwn"]))
|
||||||
@ -780,15 +815,14 @@ class FibreChannelAdapter(CommonAdapter):
|
|||||||
raise exception.VolumeBackendAPIException(data=msg)
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
return wwns
|
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.
|
"""Fill connection properties dict with data to attach volume.
|
||||||
|
|
||||||
:param appliance_id: PowerStore appliance id
|
|
||||||
:param volume_lun: attached volume logical unit number
|
:param volume_lun: attached volume logical unit number
|
||||||
:return: connection properties
|
:return: connection properties
|
||||||
"""
|
"""
|
||||||
|
|
||||||
target_wwns = self._get_fc_targets(appliance_id)
|
target_wwns = self._get_fc_targets()
|
||||||
return {
|
return {
|
||||||
"driver_volume_type": self.driver_volume_type,
|
"driver_volume_type": self.driver_volume_type,
|
||||||
"data": {
|
"data": {
|
||||||
@ -800,8 +834,8 @@ class FibreChannelAdapter(CommonAdapter):
|
|||||||
|
|
||||||
|
|
||||||
class iSCSIAdapter(CommonAdapter):
|
class iSCSIAdapter(CommonAdapter):
|
||||||
def __init__(self, active_backend_id, configuration):
|
def __init__(self, **kwargs):
|
||||||
super(iSCSIAdapter, self).__init__(active_backend_id, configuration)
|
super(iSCSIAdapter, self).__init__(**kwargs)
|
||||||
self.storage_protocol = PROTOCOL_ISCSI
|
self.storage_protocol = PROTOCOL_ISCSI
|
||||||
self.driver_volume_type = "iscsi"
|
self.driver_volume_type = "iscsi"
|
||||||
|
|
||||||
@ -809,16 +843,15 @@ class iSCSIAdapter(CommonAdapter):
|
|||||||
def initiators(connector):
|
def initiators(connector):
|
||||||
return [connector["initiator"]]
|
return [connector["initiator"]]
|
||||||
|
|
||||||
def _get_iscsi_targets(self, appliance_id):
|
def _get_iscsi_targets(self):
|
||||||
"""Get available iSCSI portals and IQNs for PowerStore appliance.
|
"""Get available iSCSI portals and IQNs.
|
||||||
|
|
||||||
:param appliance_id: PowerStore appliance id
|
|
||||||
:return: iSCSI portals and IQNs
|
:return: iSCSI portals and IQNs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
iqns = []
|
iqns = []
|
||||||
portals = []
|
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:
|
for address in ip_pool_addresses:
|
||||||
if self._port_is_allowed(address["address"]):
|
if self._port_is_allowed(address["address"]):
|
||||||
portals.append(
|
portals.append(
|
||||||
@ -831,15 +864,14 @@ class iSCSIAdapter(CommonAdapter):
|
|||||||
raise exception.VolumeBackendAPIException(data=msg)
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
return iqns, portals
|
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.
|
"""Fill connection properties dict with data to attach volume.
|
||||||
|
|
||||||
:param appliance_id: PowerStore appliance id
|
|
||||||
:param volume_lun: attached volume logical unit number
|
:param volume_lun: attached volume logical unit number
|
||||||
:return: connection properties
|
:return: connection properties
|
||||||
"""
|
"""
|
||||||
|
|
||||||
iqns, portals = self._get_iscsi_targets(appliance_id)
|
iqns, portals = self._get_iscsi_targets()
|
||||||
return {
|
return {
|
||||||
"driver_volume_type": self.driver_volume_type,
|
"driver_volume_type": self.driver_volume_type,
|
||||||
"data": {
|
"data": {
|
||||||
|
@ -24,24 +24,31 @@ import requests
|
|||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
|
from cinder import utils as cinder_utils
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
VOLUME_NOT_MAPPED_ERROR = "0xE0A08001000F"
|
VOLUME_NOT_MAPPED_ERROR = "0xE0A08001000F"
|
||||||
|
SESSION_ALREADY_FAILED_OVER_ERROR = "0xE0201005000C"
|
||||||
|
|
||||||
|
|
||||||
class PowerStoreClient(object):
|
class PowerStoreClient(object):
|
||||||
def __init__(self, configuration):
|
def __init__(self,
|
||||||
self.configuration = configuration
|
rest_ip,
|
||||||
self.rest_ip = None
|
rest_username,
|
||||||
self.rest_username = None
|
rest_password,
|
||||||
self.rest_password = None
|
verify_certificate,
|
||||||
self.verify_certificate = None
|
certificate_path):
|
||||||
self.certificate_path = None
|
self.rest_ip = rest_ip
|
||||||
self.base_url = None
|
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 = [
|
self.ok_codes = [
|
||||||
requests.codes.ok,
|
requests.codes.ok,
|
||||||
requests.codes.created,
|
requests.codes.created,
|
||||||
|
requests.codes.accepted,
|
||||||
requests.codes.no_content,
|
requests.codes.no_content,
|
||||||
requests.codes.partial_content
|
requests.codes.partial_content
|
||||||
]
|
]
|
||||||
@ -53,28 +60,16 @@ class PowerStoreClient(object):
|
|||||||
verify_cert = self.certificate_path
|
verify_cert = self.certificate_path
|
||||||
return verify_cert
|
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):
|
def check_for_setup_error(self):
|
||||||
if not all([self.rest_ip, self.rest_username, self.rest_password]):
|
if not all([self.rest_ip, self.rest_username, self.rest_password]):
|
||||||
msg = _("REST server IP, username and password must be set.")
|
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
|
# log warning if not using certificates
|
||||||
if not self.verify_certificate:
|
if not self.verify_certificate:
|
||||||
LOG.warning("Verify certificate is not set, using default of "
|
LOG.warning("Verify certificate is not set, using default of "
|
||||||
"False.")
|
"False.")
|
||||||
|
self.verify_certificate = False
|
||||||
LOG.debug("Successfully initialized PowerStore REST client. "
|
LOG.debug("Successfully initialized PowerStore REST client. "
|
||||||
"Server IP: %(ip)s, username: %(username)s. "
|
"Server IP: %(ip)s, username: %(username)s. "
|
||||||
"Verify server's certificate: %(verify_cert)s.",
|
"Verify server's certificate: %(verify_cert)s.",
|
||||||
@ -97,10 +92,9 @@ class PowerStoreClient(object):
|
|||||||
request_params = {
|
request_params = {
|
||||||
"auth": (self.rest_username, self.rest_password),
|
"auth": (self.rest_username, self.rest_password),
|
||||||
"verify": self._verify_cert,
|
"verify": self._verify_cert,
|
||||||
|
"params": params
|
||||||
}
|
}
|
||||||
if method == "GET":
|
if method != "GET":
|
||||||
request_params["params"] = params
|
|
||||||
else:
|
|
||||||
request_params["data"] = json.dumps(payload)
|
request_params["data"] = json.dumps(payload)
|
||||||
request_url = self.base_url + url
|
request_url = self.base_url + url
|
||||||
r = requests.request(method, request_url, **request_params)
|
r = requests.request(method, request_url, **request_params)
|
||||||
@ -143,55 +137,34 @@ class PowerStoreClient(object):
|
|||||||
raise exception.VolumeBackendAPIException(data=msg)
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_appliance_id_by_name(self, appliance_name):
|
def get_metrics(self):
|
||||||
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):
|
|
||||||
r, response = self._send_post_request(
|
r, response = self._send_post_request(
|
||||||
"/metrics/generate",
|
"/metrics/generate",
|
||||||
payload={
|
payload={
|
||||||
"entity": "space_metrics_by_appliance",
|
"entity": "space_metrics_by_cluster",
|
||||||
"entity_id": appliance_id,
|
"entity_id": "0",
|
||||||
},
|
},
|
||||||
log_response_data=False
|
log_response_data=False
|
||||||
)
|
)
|
||||||
if r.status_code not in self.ok_codes:
|
if r.status_code not in self.ok_codes:
|
||||||
msg = (_("Failed to query metrics for "
|
msg = _("Failed to query PowerStore metrics.")
|
||||||
"PowerStore appliance with id %s.") % appliance_id)
|
|
||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exception.VolumeBackendAPIException(data=msg)
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
try:
|
try:
|
||||||
latest_metrics = response[-1]
|
latest_metrics = response[-1]
|
||||||
return latest_metrics
|
return latest_metrics
|
||||||
except IndexError:
|
except IndexError:
|
||||||
msg = (_("Failed to query metrics for "
|
msg = _("Failed to query PowerStore metrics.")
|
||||||
"PowerStore appliance with id %s.") % appliance_id)
|
|
||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exception.VolumeBackendAPIException(data=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(
|
r, response = self._send_post_request(
|
||||||
"/volume",
|
"/volume",
|
||||||
payload={
|
payload={
|
||||||
"appliance_id": appliance_id,
|
|
||||||
"name": name,
|
"name": name,
|
||||||
"size": size,
|
"size": size,
|
||||||
|
"protection_policy_id": pp_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if r.status_code not in self.ok_codes:
|
if r.status_code not in self.ok_codes:
|
||||||
@ -247,14 +220,40 @@ class PowerStoreClient(object):
|
|||||||
raise exception.VolumeBackendAPIException(data=msg)
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
return response["id"]
|
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,
|
def clone_volume_or_snapshot(self,
|
||||||
name,
|
name,
|
||||||
entity_id,
|
entity_id,
|
||||||
|
pp_id,
|
||||||
entity="volume"):
|
entity="volume"):
|
||||||
r, response = self._send_post_request(
|
r, response = self._send_post_request(
|
||||||
"/volume/%s/clone" % entity_id,
|
"/volume/%s/clone" % entity_id,
|
||||||
payload={
|
payload={
|
||||||
"name": name,
|
"name": name,
|
||||||
|
"protection_policy_id": pp_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if r.status_code not in self.ok_codes:
|
if r.status_code not in self.ok_codes:
|
||||||
@ -363,27 +362,25 @@ class PowerStoreClient(object):
|
|||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exception.VolumeBackendAPIException(data=msg)
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
def get_fc_port(self, appliance_id):
|
def get_fc_port(self):
|
||||||
r, response = self._send_get_request(
|
r, response = self._send_get_request(
|
||||||
"/fc_port",
|
"/fc_port",
|
||||||
params={
|
params={
|
||||||
"appliance_id": "eq.%s" % appliance_id,
|
|
||||||
"is_link_up": "eq.True",
|
"is_link_up": "eq.True",
|
||||||
"select": "wwn"
|
"select": "wwn"
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if r.status_code not in self.ok_codes:
|
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)
|
LOG.error(msg)
|
||||||
raise exception.VolumeBackendAPIException(data=msg)
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_ip_pool_address(self, appliance_id):
|
def get_ip_pool_address(self):
|
||||||
r, response = self._send_get_request(
|
r, response = self._send_get_request(
|
||||||
"/ip_pool_address",
|
"/ip_pool_address",
|
||||||
params={
|
params={
|
||||||
"appliance_id": "eq.%s" % appliance_id,
|
|
||||||
"purposes": "eq.{Storage_Iscsi_Target}",
|
"purposes": "eq.{Storage_Iscsi_Target}",
|
||||||
"select": "address,ip_port(target_iqn)"
|
"select": "address,ip_port(target_iqn)"
|
||||||
|
|
||||||
@ -446,3 +443,159 @@ class PowerStoreClient(object):
|
|||||||
"snapshot_id": snapshot_id, })
|
"snapshot_id": snapshot_id, })
|
||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exception.VolumeBackendAPIException(data=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)
|
||||||
|
@ -16,17 +16,24 @@
|
|||||||
"""Cinder driver for Dell EMC PowerStore."""
|
"""Cinder driver for Dell EMC PowerStore."""
|
||||||
|
|
||||||
from oslo_config import cfg
|
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 import interface
|
||||||
from cinder.volume import configuration
|
from cinder.volume import configuration
|
||||||
from cinder.volume import driver
|
from cinder.volume import driver
|
||||||
from cinder.volume.drivers.dell_emc.powerstore import adapter
|
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.drivers.san import san
|
||||||
|
from cinder.volume import manager
|
||||||
|
|
||||||
|
|
||||||
|
POWERSTORE_OPTS = options.POWERSTORE_OPTS
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.register_opts(POWERSTORE_OPTS, group=configuration.SHARED_CONF_GROUP)
|
CONF.register_opts(POWERSTORE_OPTS, group=configuration.SHARED_CONF_GROUP)
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
POWERSTORE_PP_KEY = "powerstore:protection_policy"
|
||||||
|
|
||||||
|
|
||||||
@interface.volumedriver
|
@interface.volumedriver
|
||||||
@ -38,9 +45,10 @@ class PowerStoreDriver(driver.VolumeDriver):
|
|||||||
Version history:
|
Version history:
|
||||||
1.0.0 - Initial version
|
1.0.0 - Initial version
|
||||||
1.0.1 - Add CHAP support
|
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"
|
VENDOR = "Dell EMC"
|
||||||
|
|
||||||
# ThirdPartySystems wiki page
|
# ThirdPartySystems wiki page
|
||||||
@ -50,35 +58,73 @@ class PowerStoreDriver(driver.VolumeDriver):
|
|||||||
super(PowerStoreDriver, self).__init__(*args, **kwargs)
|
super(PowerStoreDriver, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.active_backend_id = kwargs.get("active_backend_id")
|
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(san.san_opts)
|
||||||
self.configuration.append_config_values(POWERSTORE_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
|
@staticmethod
|
||||||
def get_driver_options():
|
def get_driver_options():
|
||||||
return POWERSTORE_OPTS
|
return POWERSTORE_OPTS
|
||||||
|
|
||||||
def do_setup(self, context):
|
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")
|
storage_protocol = self.configuration.safe_get("storage_protocol")
|
||||||
if (
|
if (
|
||||||
storage_protocol and
|
storage_protocol and
|
||||||
storage_protocol.lower() == adapter.PROTOCOL_FC.lower()
|
storage_protocol.lower() == adapter.PROTOCOL_FC.lower()
|
||||||
):
|
):
|
||||||
self.adapter = adapter.FibreChannelAdapter(self.active_backend_id,
|
adapter_class = adapter.FibreChannelAdapter
|
||||||
self.configuration)
|
|
||||||
else:
|
else:
|
||||||
self.adapter = adapter.iSCSIAdapter(self.active_backend_id,
|
adapter_class = adapter.iSCSIAdapter
|
||||||
self.configuration)
|
self.replication_devices = (
|
||||||
self.adapter.do_setup()
|
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):
|
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):
|
def create_volume(self, volume):
|
||||||
return self.adapter.create_volume(volume)
|
return self.adapter.create_volume(volume)
|
||||||
|
|
||||||
def delete_volume(self, 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):
|
def extend_volume(self, volume, new_size):
|
||||||
return self.adapter.extend_volume(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)
|
return self.adapter.create_snapshot(snapshot)
|
||||||
|
|
||||||
def delete_snapshot(self, 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):
|
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):
|
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):
|
def initialize_connection(self, volume, connector, **kwargs):
|
||||||
return self.adapter.initialize_connection(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 = self.adapter.update_volume_stats()
|
||||||
stats["driver_version"] = self.VERSION
|
stats["driver_version"] = self.VERSION
|
||||||
stats["vendor_name"] = self.VENDOR
|
stats["vendor_name"] = self.VENDOR
|
||||||
|
stats["replication_enabled"] = self.replication_enabled
|
||||||
|
stats["replication_targets"] = self.replication_targets
|
||||||
self._stats = stats
|
self._stats = stats
|
||||||
|
|
||||||
def create_export(self, context, volume, connector):
|
def create_export(self, context, volume, connector):
|
||||||
@ -118,3 +169,66 @@ class PowerStoreDriver(driver.VolumeDriver):
|
|||||||
|
|
||||||
def remove_export(self, context, volume):
|
def remove_export(self, context, volume):
|
||||||
pass
|
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
|
||||||
|
@ -24,7 +24,12 @@ POWERSTORE_OPTS = [
|
|||||||
cfg.ListOpt(POWERSTORE_APPLIANCES,
|
cfg.ListOpt(POWERSTORE_APPLIANCES,
|
||||||
default=[],
|
default=[],
|
||||||
help="Appliances names. Comma separated list of PowerStore "
|
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,
|
cfg.ListOpt(POWERSTORE_PORTS,
|
||||||
default=[],
|
default=[],
|
||||||
help="Allowed ports. Comma separated list of PowerStore "
|
help="Allowed ports. Comma separated list of PowerStore "
|
||||||
|
@ -23,6 +23,7 @@ from oslo_utils import units
|
|||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder.objects import fields
|
from cinder.objects import fields
|
||||||
|
from cinder.volume.drivers.dell_emc.powerstore import driver
|
||||||
from cinder.volume import volume_utils
|
from cinder.volume import volume_utils
|
||||||
|
|
||||||
|
|
||||||
@ -151,3 +152,13 @@ def get_chap_credentials():
|
|||||||
CHAP_DEFAULT_SECRET_LENGTH
|
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)
|
||||||
|
@ -18,6 +18,7 @@ Supported operations
|
|||||||
- Get volume statistics.
|
- Get volume statistics.
|
||||||
- Attach a volume to multiple servers simultaneously (multiattach).
|
- Attach a volume to multiple servers simultaneously (multiattach).
|
||||||
- Revert a volume to a snapshot.
|
- Revert a volume to a snapshot.
|
||||||
|
- OpenStack replication v2.1 support.
|
||||||
|
|
||||||
Driver configuration
|
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
|
volume_driver = cinder.volume.drivers.dell_emc.powerstore.driver.PowerStoreDriver
|
||||||
# Backend name
|
# Backend name
|
||||||
volume_backend_name = <Backend name>
|
volume_backend_name = <Backend name>
|
||||||
# PowerStore appliances
|
|
||||||
powerstore_appliances = <Appliances names> # Ex. Appliance-1,Appliance-2
|
|
||||||
# PowerStore allowed ports
|
# PowerStore allowed ports
|
||||||
powerstore_ports = <Allowed ports> # Ex. 58:cc:f0:98:49:22:07:02,58:cc:f0:98:49:23:07:02
|
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,
|
CHAP configuration is retrieved from the storage during driver initialization,
|
||||||
no additional configuration is needed.
|
no additional configuration is needed.
|
||||||
Secrets are generated automatically.
|
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
|
||||||
|
@ -25,7 +25,7 @@ title=Dell EMC XtremeIO Storage Driver (FC, iSCSI)
|
|||||||
title=Dell EMC PowerMax (2000, 8000) Storage Driver (iSCSI, FC)
|
title=Dell EMC PowerMax (2000, 8000) Storage Driver (iSCSI, FC)
|
||||||
|
|
||||||
[driver.dell_emc_powerstore]
|
[driver.dell_emc_powerstore]
|
||||||
title="Dell EMC PowerStore Storage Driver (iSCSI, FC)"
|
title=Dell EMC PowerStore Storage Driver (iSCSI, FC)
|
||||||
|
|
||||||
[driver.dell_emc_sc]
|
[driver.dell_emc_sc]
|
||||||
title=Dell EMC SC Series Storage Driver (iSCSI, FC)
|
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.
|
to take advantage of Cinder's failover and failback commands.
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_powerstore=missing
|
driver.dell_emc_powerstore=complete
|
||||||
driver.dell_emc_powervault=missing
|
driver.dell_emc_powervault=missing
|
||||||
driver.dell_emc_sc=complete
|
driver.dell_emc_sc=complete
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user