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