Add Cinder driver for Dell EMC PowerStore
* Supported Protocols - FC - iSCSI * Supported Features - Volume Create/Delete - Volume Attach/Detach - Snapshot Create/Delete - Create Volume from Snapshot - Get Volume Stats - Copy Image to Volume - Copy Volume to Image - Clone Volume - Extend Volume - Revert Volume to Snapshot Implements: blueprint powerstore-cinder-driver Change-Id: Icef5b38ba39eec761c1cfa70e2a66bc28ddf4cd6
This commit is contained in:
parent
be4a682890
commit
517cb6448b
@ -74,6 +74,8 @@ from cinder.volume.drivers.datera import datera_iscsi as \
|
|||||||
cinder_volume_drivers_datera_dateraiscsi
|
cinder_volume_drivers_datera_dateraiscsi
|
||||||
from cinder.volume.drivers.dell_emc.powermax import common as \
|
from cinder.volume.drivers.dell_emc.powermax import common as \
|
||||||
cinder_volume_drivers_dell_emc_powermax_common
|
cinder_volume_drivers_dell_emc_powermax_common
|
||||||
|
from cinder.volume.drivers.dell_emc.powerstore import driver as \
|
||||||
|
cinder_volume_drivers_dell_emc_powerstore_driver
|
||||||
from cinder.volume.drivers.dell_emc.sc import storagecenter_common as \
|
from cinder.volume.drivers.dell_emc.sc import storagecenter_common as \
|
||||||
cinder_volume_drivers_dell_emc_sc_storagecentercommon
|
cinder_volume_drivers_dell_emc_sc_storagecentercommon
|
||||||
from cinder.volume.drivers.dell_emc.unity import driver as \
|
from cinder.volume.drivers.dell_emc.unity import driver as \
|
||||||
@ -296,6 +298,8 @@ def list_opts():
|
|||||||
cinder_volume_driver.image_opts,
|
cinder_volume_driver.image_opts,
|
||||||
cinder_volume_driver.fqdn_opts,
|
cinder_volume_driver.fqdn_opts,
|
||||||
cinder_volume_drivers_dell_emc_powermax_common.powermax_opts,
|
cinder_volume_drivers_dell_emc_powermax_common.powermax_opts,
|
||||||
|
cinder_volume_drivers_dell_emc_powerstore_driver.
|
||||||
|
POWERSTORE_OPTS,
|
||||||
cinder_volume_drivers_dell_emc_sc_storagecentercommon.
|
cinder_volume_drivers_dell_emc_sc_storagecentercommon.
|
||||||
common_opts,
|
common_opts,
|
||||||
cinder_volume_drivers_dell_emc_unity_driver.UNITY_OPTS,
|
cinder_volume_drivers_dell_emc_unity_driver.UNITY_OPTS,
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
# Copyright (c) 2020 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.
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from cinder.tests.unit import test
|
||||||
|
from cinder.volume import configuration
|
||||||
|
from cinder.volume.drivers.dell_emc.powerstore import driver
|
||||||
|
from cinder.volume.drivers.dell_emc.powerstore import options
|
||||||
|
|
||||||
|
|
||||||
|
class MockResponse(requests.Response):
|
||||||
|
def __init__(self, content=None, rc=200):
|
||||||
|
super(MockResponse, self).__init__()
|
||||||
|
|
||||||
|
if content is None:
|
||||||
|
content = []
|
||||||
|
if isinstance(content, str):
|
||||||
|
content = content.encode()
|
||||||
|
self._content = content
|
||||||
|
self.request = mock.MagicMock()
|
||||||
|
self.status_code = rc
|
||||||
|
|
||||||
|
def json(self, **kwargs):
|
||||||
|
if isinstance(self._content, bytes):
|
||||||
|
return super(MockResponse, self).json(**kwargs)
|
||||||
|
return self._content
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
if not isinstance(self._content, bytes):
|
||||||
|
return json.dumps(self._content)
|
||||||
|
return super(MockResponse, self).text
|
||||||
|
|
||||||
|
|
||||||
|
class TestPowerStoreDriver(test.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPowerStoreDriver, self).setUp()
|
||||||
|
self.configuration = configuration.Configuration(
|
||||||
|
options.POWERSTORE_OPTS,
|
||||||
|
configuration.SHARED_CONF_GROUP
|
||||||
|
)
|
||||||
|
self._set_overrides()
|
||||||
|
self.driver = driver.PowerStoreDriver(configuration=self.configuration)
|
||||||
|
self.driver.do_setup({})
|
||||||
|
self.iscsi_driver = self.driver
|
||||||
|
|
||||||
|
self._override_shared_conf("storage_protocol", override="FC")
|
||||||
|
self.fc_driver = driver.PowerStoreDriver(
|
||||||
|
configuration=self.configuration
|
||||||
|
)
|
||||||
|
self.fc_driver.do_setup({})
|
||||||
|
|
||||||
|
def _override_shared_conf(self, *args, **kwargs):
|
||||||
|
return self.override_config(*args,
|
||||||
|
**kwargs,
|
||||||
|
group=configuration.SHARED_CONF_GROUP)
|
||||||
|
|
||||||
|
def _set_overrides(self):
|
||||||
|
# Override the defaults to fake values
|
||||||
|
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")
|
@ -0,0 +1,77 @@
|
|||||||
|
# Copyright (c) 2020 Dell Inc. or its subsidiaries.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.tests.unit.volume.drivers.dell_emc import powerstore
|
||||||
|
|
||||||
|
|
||||||
|
class TestBase(powerstore.TestPowerStoreDriver):
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_appliance_id_by_name")
|
||||||
|
def test_configuration(self, mock_appliance):
|
||||||
|
mock_appliance.return_value = "A1"
|
||||||
|
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.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_appliance_id_by_name")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_appliance_metrics")
|
||||||
|
def test_update_volume_stats(self, mock_metrics, mock_appliance):
|
||||||
|
mock_appliance.return_value = "A1"
|
||||||
|
mock_metrics.return_value = {
|
||||||
|
"physical_total": 2147483648,
|
||||||
|
"physical_used": 1073741824,
|
||||||
|
}
|
||||||
|
self.driver.check_for_setup_error()
|
||||||
|
self.driver._update_volume_stats()
|
||||||
|
|
||||||
|
@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_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)
|
@ -0,0 +1,89 @@
|
|||||||
|
# Copyright (c) 2020 Dell Inc. or its subsidiaries.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.tests.unit import fake_snapshot
|
||||||
|
from cinder.tests.unit import fake_volume
|
||||||
|
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_appliance_id_by_name")
|
||||||
|
def setUp(self, mock_appliance):
|
||||||
|
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",
|
||||||
|
provider_id="fake_id",
|
||||||
|
size=8
|
||||||
|
)
|
||||||
|
self.snapshot = fake_snapshot.fake_snapshot_obj(
|
||||||
|
{},
|
||||||
|
provider_id="fake_id_1",
|
||||||
|
volume=self.volume
|
||||||
|
)
|
||||||
|
|
||||||
|
@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")
|
||||||
|
def test_create_snapshot_bad_status(self, mock_create_request):
|
||||||
|
mock_create_request.return_value = powerstore.MockResponse(rc=400)
|
||||||
|
error = self.assertRaises(
|
||||||
|
exception.VolumeBackendAPIException,
|
||||||
|
self.driver.create_snapshot,
|
||||||
|
self.snapshot
|
||||||
|
)
|
||||||
|
self.assertIn("Failed to create snapshot", error.msg)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.delete_volume_or_snapshot")
|
||||||
|
def test_delete_snapshot(self, mock_delete):
|
||||||
|
self.driver.delete_snapshot(self.snapshot)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_delete_snapshot_bad_status(self, mock_delete):
|
||||||
|
mock_delete.return_value = powerstore.MockResponse(rc=400)
|
||||||
|
error = self.assertRaises(
|
||||||
|
exception.VolumeBackendAPIException,
|
||||||
|
self.driver.delete_snapshot,
|
||||||
|
self.snapshot
|
||||||
|
)
|
||||||
|
self.assertIn("Failed to delete PowerStore snapshot", error.msg)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.restore_from_snapshot")
|
||||||
|
def test_revert_to_snapshot(self, mock_revert):
|
||||||
|
self.driver.revert_to_snapshot({}, self.volume, self.snapshot)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_revert_to_snapshot_bad_status(self, mock_revert):
|
||||||
|
mock_revert.return_value = powerstore.MockResponse(rc=400)
|
||||||
|
error = self.assertRaises(
|
||||||
|
exception.VolumeBackendAPIException,
|
||||||
|
self.driver.revert_to_snapshot,
|
||||||
|
{},
|
||||||
|
self.volume,
|
||||||
|
self.snapshot
|
||||||
|
)
|
||||||
|
self.assertIn("Failed to restore PowerStore volume", error.msg)
|
@ -0,0 +1,157 @@
|
|||||||
|
# Copyright (c) 2020 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.objects import volume_attachment
|
||||||
|
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 utils
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver):
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_appliance_id_by_name")
|
||||||
|
def setUp(self, mock_appliance):
|
||||||
|
super(TestVolumeAttachDetach, self).setUp()
|
||||||
|
mock_appliance.return_value = "A1"
|
||||||
|
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",
|
||||||
|
provider_id="fake_id",
|
||||||
|
size=8
|
||||||
|
)
|
||||||
|
self.volume.volume_attachment = (
|
||||||
|
volume_attachment.VolumeAttachmentList()
|
||||||
|
)
|
||||||
|
self.volume.volume_attachment.objects = [
|
||||||
|
volume_attachment.VolumeAttachment(
|
||||||
|
attach_status=fields.VolumeAttachStatus.ATTACHED,
|
||||||
|
attached_host=self.volume.host
|
||||||
|
),
|
||||||
|
volume_attachment.VolumeAttachment(
|
||||||
|
attach_status=fields.VolumeAttachStatus.ATTACHED,
|
||||||
|
attached_host=self.volume.host
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self.fake_iscsi_targets_response = [
|
||||||
|
{
|
||||||
|
"address": "1.2.3.4",
|
||||||
|
"ip_port": {
|
||||||
|
"target_iqn":
|
||||||
|
"iqn.2020-07.com.dell:dellemc-powerstore-test-iqn-1"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "5.6.7.8",
|
||||||
|
"ip_port": {
|
||||||
|
"target_iqn":
|
||||||
|
"iqn.2020-07.com.dell:dellemc-powerstore-test-iqn-1"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.fake_fc_wwns_response = [
|
||||||
|
{
|
||||||
|
"wwn": "58:cc:f0:98:49:21:07:02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wwn": "58:cc:f0:98:49:23:07:02"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.fake_connector = {
|
||||||
|
"host": self.volume.host,
|
||||||
|
"wwpns": ["58:cc:f0:98:49:21:07:02", "58:cc:f0:98:49:23:07:02"],
|
||||||
|
"initiator": "fake_initiator",
|
||||||
|
}
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_fc_port")
|
||||||
|
def test_get_fc_targets(self, mock_get_ip_pool):
|
||||||
|
mock_get_ip_pool.return_value = self.fake_fc_wwns_response
|
||||||
|
wwns = self.fc_driver.adapter._get_fc_targets("A1")
|
||||||
|
self.assertEqual(2, len(wwns))
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_fc_port")
|
||||||
|
def test_get_fc_targets_filtered(self, mock_get_ip_pool):
|
||||||
|
mock_get_ip_pool.return_value = self.fake_fc_wwns_response
|
||||||
|
self.fc_driver.adapter.allowed_ports = ["58:cc:f0:98:49:23:07:02"]
|
||||||
|
wwns = self.fc_driver.adapter._get_fc_targets("A1")
|
||||||
|
self.assertEqual(1, len(wwns))
|
||||||
|
self.assertFalse(
|
||||||
|
utils.fc_wwn_to_string("58:cc:f0:98:49:21:07:02") in wwns
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_fc_port")
|
||||||
|
def test_get_fc_targets_filtered_no_matched_ports(self, mock_get_ip_pool):
|
||||||
|
mock_get_ip_pool.return_value = self.fake_fc_wwns_response
|
||||||
|
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.assertIn("There are no accessible Fibre Channel targets on the "
|
||||||
|
"system.", error.msg)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_ip_pool_address")
|
||||||
|
def test_get_iscsi_targets(self, mock_get_ip_pool):
|
||||||
|
mock_get_ip_pool.return_value = self.fake_iscsi_targets_response
|
||||||
|
iqns, portals = self.iscsi_driver.adapter._get_iscsi_targets("A1")
|
||||||
|
self.assertTrue(len(iqns) == len(portals))
|
||||||
|
self.assertEqual(2, len(portals))
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_ip_pool_address")
|
||||||
|
def test_get_iscsi_targets_filtered(self, mock_get_ip_pool):
|
||||||
|
mock_get_ip_pool.return_value = self.fake_iscsi_targets_response
|
||||||
|
self.iscsi_driver.adapter.allowed_ports = ["1.2.3.4"]
|
||||||
|
iqns, portals = self.iscsi_driver.adapter._get_iscsi_targets("A1")
|
||||||
|
self.assertTrue(len(iqns) == len(portals))
|
||||||
|
self.assertEqual(1, len(portals))
|
||||||
|
self.assertFalse(
|
||||||
|
"iqn.2020-07.com.dell:dellemc-powerstore-test-iqn-2" in iqns
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_ip_pool_address")
|
||||||
|
def test_get_iscsi_targets_filtered_no_matched_ports(self,
|
||||||
|
mock_get_ip_pool):
|
||||||
|
mock_get_ip_pool.return_value = self.fake_iscsi_targets_response
|
||||||
|
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.assertIn("There are no accessible iSCSI targets on the system.",
|
||||||
|
error.msg)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.adapter."
|
||||||
|
"CommonAdapter._detach_volume_from_hosts")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.adapter."
|
||||||
|
"CommonAdapter._filter_hosts_by_initiators")
|
||||||
|
def test_detach_multiattached_volume(self, mock_filter_hosts, mock_detach):
|
||||||
|
self.iscsi_driver.terminate_connection(self.volume,
|
||||||
|
self.fake_connector)
|
||||||
|
mock_filter_hosts.assert_not_called()
|
||||||
|
mock_detach.assert_not_called()
|
||||||
|
self.volume.volume_attachment.objects.pop()
|
||||||
|
self.iscsi_driver.terminate_connection(self.volume,
|
||||||
|
self.fake_connector)
|
||||||
|
mock_filter_hosts.assert_called_once()
|
||||||
|
mock_detach.assert_called_once()
|
@ -0,0 +1,152 @@
|
|||||||
|
# Copyright (c) 2020 Dell Inc. or its subsidiaries.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.tests.unit import fake_volume
|
||||||
|
from cinder.tests.unit.volume.drivers.dell_emc import powerstore
|
||||||
|
from cinder.volume.drivers.dell_emc.powerstore import client
|
||||||
|
|
||||||
|
|
||||||
|
class TestVolumeCreateDeleteExtend(powerstore.TestPowerStoreDriver):
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_appliance_id_by_name")
|
||||||
|
def setUp(self, mock_appliance):
|
||||||
|
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",
|
||||||
|
provider_id="fake_id",
|
||||||
|
size=8
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.create_volume")
|
||||||
|
def test_create_volume(self, mock_create):
|
||||||
|
mock_create.return_value = "fake_id"
|
||||||
|
self.driver.create_volume(self.volume)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_create_volume_bad_status(self, mock_create_request):
|
||||||
|
mock_create_request.return_value = powerstore.MockResponse(rc=400)
|
||||||
|
error = self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.driver.create_volume,
|
||||||
|
self.volume)
|
||||||
|
self.assertIn("Failed to create PowerStore volume", error.msg)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.adapter."
|
||||||
|
"CommonAdapter._detach_volume_from_hosts")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.delete_volume_or_snapshot")
|
||||||
|
def test_delete_volume(self, mock_delete, mock_detach):
|
||||||
|
self.driver.delete_volume(self.volume)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.adapter."
|
||||||
|
"CommonAdapter._detach_volume_from_hosts")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.delete_volume_or_snapshot")
|
||||||
|
def test_delete_volume_no_provider_id(self, mock_delete, mock_detach):
|
||||||
|
self.volume.provider_id = None
|
||||||
|
self.driver.delete_volume(self.volume)
|
||||||
|
mock_detach.assert_not_called()
|
||||||
|
mock_delete.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.adapter."
|
||||||
|
"CommonAdapter._detach_volume_from_hosts")
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_delete_volume_not_found(self, mock_delete_request, mock_detach):
|
||||||
|
mock_delete_request.return_value = powerstore.MockResponse(rc=404)
|
||||||
|
self.driver.delete_volume(self.volume)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_volume_mapped_hosts")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.delete_volume_or_snapshot")
|
||||||
|
def test_delete_volume_detach_not_found(self,
|
||||||
|
mock_delete,
|
||||||
|
mock_mapped_hosts,
|
||||||
|
mock_detach_request):
|
||||||
|
mock_mapped_hosts.return_value = ["fake_host_id"]
|
||||||
|
mock_detach_request.return_value = powerstore.MockResponse(
|
||||||
|
content={},
|
||||||
|
rc=404
|
||||||
|
)
|
||||||
|
self.driver.delete_volume(self.volume)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_volume_mapped_hosts")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.delete_volume_or_snapshot")
|
||||||
|
def test_delete_volume_detach_not_mapped(self,
|
||||||
|
mock_delete,
|
||||||
|
mock_mapped_hosts,
|
||||||
|
mock_detach_request):
|
||||||
|
mock_mapped_hosts.return_value = ["fake_host_id"]
|
||||||
|
mock_detach_request.return_value = powerstore.MockResponse(
|
||||||
|
content={
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"code": client.VOLUME_NOT_MAPPED_ERROR,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rc=422
|
||||||
|
)
|
||||||
|
self.driver.delete_volume(self.volume)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.adapter."
|
||||||
|
"CommonAdapter._detach_volume_from_hosts")
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_delete_volume_bad_status(self, mock_delete, mock_detach):
|
||||||
|
mock_delete.return_value = powerstore.MockResponse(rc=400)
|
||||||
|
error = self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.driver.delete_volume,
|
||||||
|
self.volume)
|
||||||
|
self.assertIn("Failed to delete PowerStore volume", error.msg)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_volume_mapped_hosts")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.delete_volume_or_snapshot")
|
||||||
|
def test_delete_volume_detach_bad_status(self,
|
||||||
|
mock_delete,
|
||||||
|
mock_mapped_hosts,
|
||||||
|
mock_detach_request):
|
||||||
|
mock_mapped_hosts.return_value = ["fake_host_id"]
|
||||||
|
mock_detach_request.return_value = powerstore.MockResponse(rc=400)
|
||||||
|
error = self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.driver.delete_volume,
|
||||||
|
self.volume)
|
||||||
|
self.assertIn("Failed to detach PowerStore volume", error.msg)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.extend_volume")
|
||||||
|
def test_extend_volume(self, mock_extend):
|
||||||
|
self.driver.extend_volume(self.volume, 16)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_extend_volume_bad_status(self, mock_extend_request):
|
||||||
|
mock_extend_request.return_value = powerstore.MockResponse(rc=400)
|
||||||
|
error = self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.driver.extend_volume,
|
||||||
|
self.volume,
|
||||||
|
16)
|
||||||
|
self.assertIn("Failed to extend PowerStore volume", error.msg)
|
@ -0,0 +1,114 @@
|
|||||||
|
# Copyright (c) 2020 Dell Inc. or its subsidiaries.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.tests.unit import fake_snapshot
|
||||||
|
from cinder.tests.unit import fake_volume
|
||||||
|
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_appliance_id_by_name")
|
||||||
|
def setUp(self, mock_appliance):
|
||||||
|
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",
|
||||||
|
provider_id="fake_id",
|
||||||
|
size=8
|
||||||
|
)
|
||||||
|
self.source_volume = fake_volume.fake_volume_obj(
|
||||||
|
{},
|
||||||
|
host="host@backend#test-appliance",
|
||||||
|
provider_id="fake_id_1",
|
||||||
|
size=8
|
||||||
|
)
|
||||||
|
self.source_snapshot = fake_snapshot.fake_snapshot_obj(
|
||||||
|
{},
|
||||||
|
provider_id="fake_id_2",
|
||||||
|
volume_size=8
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.clone_volume_or_snapshot")
|
||||||
|
def test_create_cloned_volume(self, mock_create_cloned):
|
||||||
|
mock_create_cloned.return_value = self.volume.provider_id
|
||||||
|
self.driver.create_cloned_volume(self.volume, self.source_volume)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.extend_volume")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.clone_volume_or_snapshot")
|
||||||
|
def test_create_cloned_volume_extended(self,
|
||||||
|
mock_create_cloned,
|
||||||
|
mock_extend):
|
||||||
|
mock_create_cloned.return_value = self.volume.provider_id
|
||||||
|
self.volume.size = 16
|
||||||
|
self.driver.create_cloned_volume(self.volume, self.source_volume)
|
||||||
|
mock_extend.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.clone_volume_or_snapshot")
|
||||||
|
def test_create_volume_from_snapshot(self, mock_create_from_snap):
|
||||||
|
mock_create_from_snap.return_value = self.volume.provider_id
|
||||||
|
self.driver.create_volume_from_snapshot(self.volume,
|
||||||
|
self.source_snapshot)
|
||||||
|
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.extend_volume")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.clone_volume_or_snapshot")
|
||||||
|
def test_create_volume_from_snapshot_extended(self,
|
||||||
|
mock_create_from_snap,
|
||||||
|
mock_extend):
|
||||||
|
mock_create_from_snap.return_value = self.volume.provider_id
|
||||||
|
self.volume.size = 16
|
||||||
|
self.driver.create_volume_from_snapshot(self.volume,
|
||||||
|
self.source_snapshot)
|
||||||
|
mock_extend.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_create_volume_from_source_bad_status(self, mock_create_request):
|
||||||
|
mock_create_request.return_value = powerstore.MockResponse(rc=400)
|
||||||
|
error = self.assertRaises(
|
||||||
|
exception.VolumeBackendAPIException,
|
||||||
|
self.driver.adapter._create_volume_from_source,
|
||||||
|
self.volume,
|
||||||
|
self.source_volume
|
||||||
|
)
|
||||||
|
self.assertIn("Failed to create clone", error.msg)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.clone_volume_or_snapshot")
|
||||||
|
def test_create_volume_from_source_extende_bad_status(
|
||||||
|
self,
|
||||||
|
mock_create_from_source,
|
||||||
|
mock_extend_request
|
||||||
|
):
|
||||||
|
mock_extend_request.return_value = powerstore.MockResponse(rc=400)
|
||||||
|
self.volume.size = 16
|
||||||
|
error = self.assertRaises(
|
||||||
|
exception.VolumeBackendAPIException,
|
||||||
|
self.driver.adapter._create_volume_from_source,
|
||||||
|
self.volume,
|
||||||
|
self.source_volume
|
||||||
|
)
|
||||||
|
self.assertIn("Failed to extend PowerStore volume", error.msg)
|
790
cinder/volume/drivers/dell_emc/powerstore/adapter.py
Normal file
790
cinder/volume/drivers/dell_emc/powerstore/adapter.py
Normal file
@ -0,0 +1,790 @@
|
|||||||
|
# Copyright (c) 2020 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.
|
||||||
|
|
||||||
|
"""Adapter for Dell EMC PowerStore Cinder driver."""
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from cinder import coordination
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
PROTOCOL_FC = "FC"
|
||||||
|
PROTOCOL_ISCSI = "iSCSI"
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
self.storage_protocol = None
|
||||||
|
self.allowed_ports = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initiators(connector):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _port_is_allowed(self, port):
|
||||||
|
"""Check if port is in allowed ports list.
|
||||||
|
|
||||||
|
If allowed ports are empty then all ports are allowed.
|
||||||
|
|
||||||
|
:param port: iSCSI IP/FC WWN to check
|
||||||
|
:return: is port allowed
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.allowed_ports:
|
||||||
|
return True
|
||||||
|
return port.lower() in self.allowed_ports
|
||||||
|
|
||||||
|
def _get_connection_properties(self, appliance_id, 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)
|
||||||
|
)
|
||||||
|
LOG.debug("Successfully initialized PowerStore %(protocol)s adapter. "
|
||||||
|
"PowerStore appliances: %(appliances)s. "
|
||||||
|
"Allowed ports: %(allowed_ports)s.",
|
||||||
|
{
|
||||||
|
"protocol": self.storage_protocol,
|
||||||
|
"appliances": self.appliances,
|
||||||
|
"allowed_ports": self.allowed_ports,
|
||||||
|
})
|
||||||
|
|
||||||
|
def create_volume(self, volume):
|
||||||
|
appliance_name = volume_utils.extract_host(volume.host, "pool")
|
||||||
|
appliance_id = self.appliances_to_ids_map[appliance_name]
|
||||||
|
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_name": volume.name,
|
||||||
|
"volume_size": volume.size,
|
||||||
|
"volume_id": volume.id,
|
||||||
|
"appliance_name": appliance_name,
|
||||||
|
})
|
||||||
|
size_in_bytes = utils.gib_to_bytes(volume.size)
|
||||||
|
provider_id = self.client.create_volume(appliance_id,
|
||||||
|
volume.name,
|
||||||
|
size_in_bytes)
|
||||||
|
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. "
|
||||||
|
"PowerStore volume id: %(volume_provider_id)s.",
|
||||||
|
{
|
||||||
|
"volume_name": volume.name,
|
||||||
|
"volume_size": volume.size,
|
||||||
|
"volume_id": volume.id,
|
||||||
|
"appliance_name": appliance_name,
|
||||||
|
"volume_provider_id": provider_id,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"provider_id": provider_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def delete_volume(self, volume):
|
||||||
|
if not volume.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.",
|
||||||
|
{
|
||||||
|
"volume_name": volume.name,
|
||||||
|
"volume_id": volume.id,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
LOG.debug("Delete 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,
|
||||||
|
})
|
||||||
|
self._detach_volume_from_hosts(volume)
|
||||||
|
self.client.delete_volume_or_snapshot(volume.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,
|
||||||
|
})
|
||||||
|
|
||||||
|
def extend_volume(self, volume, new_size):
|
||||||
|
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. "
|
||||||
|
"PowerStore volume id: %(volume_provider_id)s.",
|
||||||
|
{
|
||||||
|
"volume_name": volume.name,
|
||||||
|
"volume_size": volume.size,
|
||||||
|
"volume_id": volume.id,
|
||||||
|
"volume_new_size": new_size,
|
||||||
|
"volume_provider_id": volume.provider_id,
|
||||||
|
})
|
||||||
|
size_in_bytes = utils.gib_to_bytes(new_size)
|
||||||
|
self.client.extend_volume(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. "
|
||||||
|
"PowerStore volume id: %(volume_provider_id)s.",
|
||||||
|
{
|
||||||
|
"volume_name": volume.name,
|
||||||
|
"volume_size": volume.size,
|
||||||
|
"volume_id": volume.id,
|
||||||
|
"volume_new_size": new_size,
|
||||||
|
"volume_provider_id": volume.provider_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def create_snapshot(self, snapshot):
|
||||||
|
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: "
|
||||||
|
"%(volume_provider_id)s.",
|
||||||
|
{
|
||||||
|
"snapshot_name": snapshot.name,
|
||||||
|
"snapshot_id": snapshot.id,
|
||||||
|
"volume_name": snapshot.volume.name,
|
||||||
|
"volume_id": snapshot.volume.id,
|
||||||
|
"volume_provider_id": snapshot.volume.provider_id,
|
||||||
|
})
|
||||||
|
snapshot_provider_id = self.client.create_snapshot(
|
||||||
|
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: "
|
||||||
|
"%(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,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"provider_id": snapshot_provider_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def delete_snapshot(self, snapshot):
|
||||||
|
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: "
|
||||||
|
"%(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,
|
||||||
|
})
|
||||||
|
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: "
|
||||||
|
"%(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)
|
||||||
|
LOG.debug("Connection properties for volume %(volume_name)s with id "
|
||||||
|
"%(volume_id)s: %(connection_properties)s.",
|
||||||
|
{
|
||||||
|
"volume_name": volume.name,
|
||||||
|
"volume_id": volume.id,
|
||||||
|
"connection_properties": connection_properties,
|
||||||
|
})
|
||||||
|
return connection_properties
|
||||||
|
|
||||||
|
def terminate_connection(self, volume, connector, **kwargs):
|
||||||
|
self._disconnect_volume(volume, connector)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def update_volume_stats(self):
|
||||||
|
stats = {
|
||||||
|
"volume_backend_name": (
|
||||||
|
self.configuration.safe_get("volume_backend_name") or
|
||||||
|
"powerstore"
|
||||||
|
),
|
||||||
|
"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)
|
||||||
|
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"],
|
||||||
|
"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
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(source, Snapshot):
|
||||||
|
entity = "snapshot"
|
||||||
|
source_size = source.volume_size
|
||||||
|
else:
|
||||||
|
entity = "volume"
|
||||||
|
source_size = source.size
|
||||||
|
volume_provider_id = self.client.clone_volume_or_snapshot(
|
||||||
|
volume.name,
|
||||||
|
source.provider_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
|
||||||
|
|
||||||
|
def _filter_hosts_by_initiators(self, initiators):
|
||||||
|
"""Filter hosts by given list of initiators.
|
||||||
|
|
||||||
|
If initiators are added to different hosts the exception will be
|
||||||
|
raised. In this case one of the hosts should be deleted.
|
||||||
|
|
||||||
|
:param initiators: list of initiators
|
||||||
|
:return: PowerStore host object
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOG.debug("Query PowerStore %(protocol)s hosts.",
|
||||||
|
{
|
||||||
|
"protocol": self.storage_protocol,
|
||||||
|
})
|
||||||
|
hosts = self.client.get_all_hosts(self.storage_protocol)
|
||||||
|
hosts_found = utils.filter_hosts_by_initiators(hosts, initiators)
|
||||||
|
if hosts_found:
|
||||||
|
if len(hosts_found) > 1:
|
||||||
|
hosts_names_found = [host["name"] for host in hosts_found]
|
||||||
|
msg = (_("Initiators are added to different PowerStore hosts: "
|
||||||
|
"%(hosts_names_found)s. Remove all of the hosts "
|
||||||
|
"except one to proceed. Initiators will be modified "
|
||||||
|
"during the next volume attach procedure.")
|
||||||
|
% {"hosts_names_found": hosts_names_found, })
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
else:
|
||||||
|
return hosts_found[0]
|
||||||
|
|
||||||
|
@coordination.synchronized("powerstore-create-host")
|
||||||
|
def _create_host_if_not_exist(self, connector):
|
||||||
|
"""Create PowerStore host if it does not exist.
|
||||||
|
|
||||||
|
:param connector: connection properties
|
||||||
|
:return: PowerStore host object
|
||||||
|
"""
|
||||||
|
|
||||||
|
initiators = self.initiators(connector)
|
||||||
|
host = self._filter_hosts_by_initiators(initiators)
|
||||||
|
if host:
|
||||||
|
self._modify_host_initiators(host, initiators)
|
||||||
|
else:
|
||||||
|
host_name = utils.powerstore_host_name(
|
||||||
|
connector,
|
||||||
|
self.storage_protocol
|
||||||
|
)
|
||||||
|
LOG.debug("Create PowerStore host %(host_name)s. "
|
||||||
|
"Initiators: %(initiators)s.",
|
||||||
|
{
|
||||||
|
"host_name": host_name,
|
||||||
|
"initiators": initiators,
|
||||||
|
})
|
||||||
|
ports = [
|
||||||
|
{
|
||||||
|
"port_name": initiator,
|
||||||
|
"port_type": self.storage_protocol,
|
||||||
|
} for initiator in initiators
|
||||||
|
]
|
||||||
|
host = self.client.create_host(host_name, ports)
|
||||||
|
host["name"] = host_name
|
||||||
|
LOG.debug("Successfully created PowerStore host %(host_name)s. "
|
||||||
|
"Initiators: %(initiators)s. PowerStore host id: "
|
||||||
|
"%(host_provider_id)s.",
|
||||||
|
{
|
||||||
|
"host_name": host["name"],
|
||||||
|
"initiators": initiators,
|
||||||
|
"host_provider_id": host["id"],
|
||||||
|
})
|
||||||
|
return host
|
||||||
|
|
||||||
|
def _modify_host_initiators(self, host, initiators):
|
||||||
|
"""Update PowerStore host initiators if needed.
|
||||||
|
|
||||||
|
:param host: PowerStore host object
|
||||||
|
:param initiators: list of initiators
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
initiators_added = [
|
||||||
|
initiator["port_name"] for initiator in host["host_initiators"]
|
||||||
|
]
|
||||||
|
initiators_to_remove = [
|
||||||
|
initiator for initiator in initiators_added
|
||||||
|
if initiator not in initiators
|
||||||
|
]
|
||||||
|
initiators_to_add = [
|
||||||
|
{
|
||||||
|
"port_name": initiator,
|
||||||
|
"port_type": self.storage_protocol,
|
||||||
|
} for initiator in initiators
|
||||||
|
if initiator not in initiators_added
|
||||||
|
]
|
||||||
|
if initiators_to_remove:
|
||||||
|
LOG.debug("Remove initiators from PowerStore host %(host_name)s. "
|
||||||
|
"Initiators: %(initiators_to_remove)s. "
|
||||||
|
"PowerStore host id: %(host_provider_id)s.",
|
||||||
|
{
|
||||||
|
"host_name": host["name"],
|
||||||
|
"initiators_to_remove": initiators_to_remove,
|
||||||
|
"host_provider_id": host["id"],
|
||||||
|
})
|
||||||
|
self.client.modify_host_initiators(
|
||||||
|
host["id"],
|
||||||
|
remove_initiators=initiators_to_remove
|
||||||
|
)
|
||||||
|
LOG.debug("Successfully removed initiators from PowerStore host "
|
||||||
|
"%(host_name)s. Initiators: %(initiators_to_remove)s. "
|
||||||
|
"PowerStore host id: %(host_provider_id)s.",
|
||||||
|
{
|
||||||
|
"host_name": host["name"],
|
||||||
|
"initiators_to_remove": initiators_to_remove,
|
||||||
|
"host_provider_id": host["id"],
|
||||||
|
})
|
||||||
|
if initiators_to_add:
|
||||||
|
LOG.debug("Add initiators to PowerStore host %(host_name)s. "
|
||||||
|
"Initiators: %(initiators_to_add)s. PowerStore host id: "
|
||||||
|
"%(host_provider_id)s.",
|
||||||
|
{
|
||||||
|
"host_name": host["name"],
|
||||||
|
"initiators_to_add": initiators_to_add,
|
||||||
|
"host_provider_id": host["id"],
|
||||||
|
})
|
||||||
|
self.client.modify_host_initiators(
|
||||||
|
host["id"],
|
||||||
|
add_initiators=initiators_to_add
|
||||||
|
)
|
||||||
|
LOG.debug("Successfully added initiators to PowerStore host "
|
||||||
|
"%(host_name)s. Initiators: %(initiators_to_add)s. "
|
||||||
|
"PowerStore host id: %(host_provider_id)s.",
|
||||||
|
{
|
||||||
|
"host_name": host["name"],
|
||||||
|
"initiators_to_add": initiators_to_add,
|
||||||
|
"host_provider_id": host["id"],
|
||||||
|
})
|
||||||
|
|
||||||
|
def _attach_volume_to_host(self, host, volume):
|
||||||
|
"""Attach PowerStore volume to host.
|
||||||
|
|
||||||
|
:param host: PowerStore host object
|
||||||
|
:param volume: OpenStack volume object
|
||||||
|
:return: attached volume logical number
|
||||||
|
"""
|
||||||
|
|
||||||
|
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.",
|
||||||
|
{
|
||||||
|
"volume_name": volume.name,
|
||||||
|
"volume_id": volume.id,
|
||||||
|
"host_name": host["name"],
|
||||||
|
"volume_provider_id": volume.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
|
||||||
|
)
|
||||||
|
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, "
|
||||||
|
"host id: %(host_provider_id)s. Volume LUN: "
|
||||||
|
"%(volume_lun)s.",
|
||||||
|
{
|
||||||
|
"volume_name": volume.name,
|
||||||
|
"volume_id": volume.id,
|
||||||
|
"host_name": host["name"],
|
||||||
|
"volume_provider_id": volume.provider_id,
|
||||||
|
"host_provider_id": host["id"],
|
||||||
|
"volume_lun": volume_lun,
|
||||||
|
})
|
||||||
|
return volume_lun
|
||||||
|
|
||||||
|
def _create_host_and_attach(self, connector, volume):
|
||||||
|
"""Create PowerStore host and attach volume.
|
||||||
|
|
||||||
|
:param connector: connection properties
|
||||||
|
:param volume: OpenStack volume object
|
||||||
|
:return: attached volume logical number
|
||||||
|
"""
|
||||||
|
|
||||||
|
host = self._create_host_if_not_exist(connector)
|
||||||
|
return self._attach_volume_to_host(host, volume)
|
||||||
|
|
||||||
|
def _connect_volume(self, volume, connector):
|
||||||
|
"""Attach PowerStore volume and return it's connection properties.
|
||||||
|
|
||||||
|
:param volume: OpenStack volume object
|
||||||
|
:param connector: connection properties
|
||||||
|
:return: volume connection properties
|
||||||
|
"""
|
||||||
|
|
||||||
|
appliance_name = volume_utils.extract_host(volume.host, "pool")
|
||||||
|
appliance_id = self.appliances_to_ids_map[appliance_name]
|
||||||
|
volume_lun = self._create_host_and_attach(
|
||||||
|
connector,
|
||||||
|
volume
|
||||||
|
)
|
||||||
|
return self._get_connection_properties(appliance_id,
|
||||||
|
volume_lun)
|
||||||
|
|
||||||
|
def _detach_volume_from_hosts(self, volume, hosts_to_detach=None):
|
||||||
|
"""Detach volume from PowerStore hosts.
|
||||||
|
|
||||||
|
If hosts_to_detach is None, detach volume from all hosts.
|
||||||
|
|
||||||
|
:param volume: OpenStack volume object
|
||||||
|
:param hosts_to_detach: list of hosts to detach from
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
if not hosts_to_detach:
|
||||||
|
# Volume is not attached to any host.
|
||||||
|
return
|
||||||
|
LOG.debug("Detach PowerStore volume %(volume_name)s with id "
|
||||||
|
"%(volume_id)s from hosts. PowerStore volume id: "
|
||||||
|
"%(volume_provider_id)s, hosts ids: %(hosts_provider_ids)s.",
|
||||||
|
{
|
||||||
|
"volume_name": volume.name,
|
||||||
|
"volume_id": volume.id,
|
||||||
|
"volume_provider_id": volume.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)
|
||||||
|
LOG.debug("Successfully detached PowerStore volume "
|
||||||
|
"%(volume_name)s with id %(volume_id)s from hosts. "
|
||||||
|
"PowerStore volume id: %(volume_provider_id)s, "
|
||||||
|
"hosts ids: %(hosts_provider_ids)s.",
|
||||||
|
{
|
||||||
|
"volume_name": volume.name,
|
||||||
|
"volume_id": volume.id,
|
||||||
|
"volume_provider_id": volume.provider_id,
|
||||||
|
"hosts_provider_ids": hosts_to_detach,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _disconnect_volume(self, volume, connector):
|
||||||
|
"""Detach PowerStore volume.
|
||||||
|
|
||||||
|
:param volume: OpenStack volume object
|
||||||
|
:param connector: connection properties
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if connector is None:
|
||||||
|
self._detach_volume_from_hosts(volume)
|
||||||
|
else:
|
||||||
|
is_multiattached = utils.is_multiattached_to_host(
|
||||||
|
volume.volume_attachment,
|
||||||
|
connector["host"]
|
||||||
|
)
|
||||||
|
if is_multiattached:
|
||||||
|
# Do not detach volume until it is attached to more than one
|
||||||
|
# instance on the same host.
|
||||||
|
return
|
||||||
|
initiators = self.initiators(connector)
|
||||||
|
host = self._filter_hosts_by_initiators(initiators)
|
||||||
|
if host:
|
||||||
|
self._detach_volume_from_hosts(volume, [host["id"]])
|
||||||
|
|
||||||
|
def revert_to_snapshot(self, volume, snapshot):
|
||||||
|
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_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,
|
||||||
|
})
|
||||||
|
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_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,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class FibreChannelAdapter(CommonAdapter):
|
||||||
|
def __init__(self, active_backend_id, configuration):
|
||||||
|
super(FibreChannelAdapter, self).__init__(active_backend_id,
|
||||||
|
configuration)
|
||||||
|
self.storage_protocol = PROTOCOL_FC
|
||||||
|
self.driver_volume_type = "fibre_channel"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initiators(connector):
|
||||||
|
return utils.extract_fc_wwpns(connector)
|
||||||
|
|
||||||
|
def _get_fc_targets(self, appliance_id):
|
||||||
|
"""Get available FC WWNs for PowerStore appliance.
|
||||||
|
|
||||||
|
:param appliance_id: PowerStore appliance id
|
||||||
|
:return: list of FC WWNs
|
||||||
|
"""
|
||||||
|
|
||||||
|
wwns = []
|
||||||
|
fc_ports = self.client.get_fc_port(appliance_id)
|
||||||
|
for port in fc_ports:
|
||||||
|
if self._port_is_allowed(port["wwn"]):
|
||||||
|
wwns.append(utils.fc_wwn_to_string(port["wwn"]))
|
||||||
|
if not wwns:
|
||||||
|
msg = _("There are no accessible Fibre Channel targets on the "
|
||||||
|
"system.")
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return wwns
|
||||||
|
|
||||||
|
def _get_connection_properties(self, appliance_id, 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)
|
||||||
|
return {
|
||||||
|
"driver_volume_type": self.driver_volume_type,
|
||||||
|
"data": {
|
||||||
|
"target_discovered": True,
|
||||||
|
"target_lun": volume_lun,
|
||||||
|
"target_wwn": target_wwns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class iSCSIAdapter(CommonAdapter):
|
||||||
|
def __init__(self, active_backend_id, configuration):
|
||||||
|
super(iSCSIAdapter, self).__init__(active_backend_id, configuration)
|
||||||
|
self.storage_protocol = PROTOCOL_ISCSI
|
||||||
|
self.driver_volume_type = "iscsi"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initiators(connector):
|
||||||
|
return [connector["initiator"]]
|
||||||
|
|
||||||
|
def _get_iscsi_targets(self, appliance_id):
|
||||||
|
"""Get available iSCSI portals and IQNs for PowerStore appliance.
|
||||||
|
|
||||||
|
:param appliance_id: PowerStore appliance id
|
||||||
|
:return: iSCSI portals and IQNs
|
||||||
|
"""
|
||||||
|
|
||||||
|
iqns = []
|
||||||
|
portals = []
|
||||||
|
ip_pool_addresses = self.client.get_ip_pool_address(appliance_id)
|
||||||
|
for address in ip_pool_addresses:
|
||||||
|
if self._port_is_allowed(address["address"]):
|
||||||
|
portals.append(
|
||||||
|
utils.iscsi_portal_with_port(address["address"])
|
||||||
|
)
|
||||||
|
iqns.append(address["ip_port"]["target_iqn"])
|
||||||
|
if not portals:
|
||||||
|
msg = _("There are no accessible iSCSI targets on the "
|
||||||
|
"system.")
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return iqns, portals
|
||||||
|
|
||||||
|
def _get_connection_properties(self, appliance_id, 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)
|
||||||
|
return {
|
||||||
|
"driver_volume_type": self.driver_volume_type,
|
||||||
|
"data": {
|
||||||
|
"target_discovered": True,
|
||||||
|
"target_portals": portals,
|
||||||
|
"target_iqns": iqns,
|
||||||
|
"target_luns": [volume_lun] * len(portals),
|
||||||
|
},
|
||||||
|
}
|
427
cinder/volume/drivers/dell_emc/powerstore/client.py
Normal file
427
cinder/volume/drivers/dell_emc/powerstore/client.py
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
# Copyright (c) 2020 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.
|
||||||
|
|
||||||
|
"""REST client for Dell EMC PowerStore Cinder Driver."""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import json
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
VOLUME_NOT_MAPPED_ERROR = "0xE0A08001000F"
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
self.ok_codes = [
|
||||||
|
requests.codes.ok,
|
||||||
|
requests.codes.created,
|
||||||
|
requests.codes.no_content,
|
||||||
|
requests.codes.partial_content
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _verify_cert(self):
|
||||||
|
verify_cert = self.verify_certificate
|
||||||
|
if self.verify_certificate and self.certificate_path:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# log warning if not using certificates
|
||||||
|
if not self.verify_certificate:
|
||||||
|
LOG.warning("Verify certificate is not set, using default of "
|
||||||
|
"False.")
|
||||||
|
LOG.debug("Successfully initialized PowerStore REST client. "
|
||||||
|
"Server IP: %(ip)s, username: %(username)s. "
|
||||||
|
"Verify server's certificate: %(verify_cert)s.",
|
||||||
|
{
|
||||||
|
"ip": self.rest_ip,
|
||||||
|
"username": self.rest_username,
|
||||||
|
"verify_cert": self._verify_cert,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _send_request(self, method, url, payload=None, params=None):
|
||||||
|
if not payload:
|
||||||
|
payload = {}
|
||||||
|
if not params:
|
||||||
|
params = {}
|
||||||
|
request_params = {
|
||||||
|
"auth": (self.rest_username, self.rest_password),
|
||||||
|
"verify": self._verify_cert,
|
||||||
|
}
|
||||||
|
if method == "GET":
|
||||||
|
request_params["params"] = params
|
||||||
|
else:
|
||||||
|
request_params["data"] = json.dumps(payload)
|
||||||
|
request_url = self.base_url + url
|
||||||
|
r = requests.request(method, request_url, **request_params)
|
||||||
|
|
||||||
|
log_level = logging.DEBUG
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
log_level = logging.ERROR
|
||||||
|
LOG.log(log_level,
|
||||||
|
"REST Request: %s %s with body %s",
|
||||||
|
r.request.method,
|
||||||
|
r.request.url,
|
||||||
|
r.request.body)
|
||||||
|
LOG.log(log_level,
|
||||||
|
"REST Response: %s with data %s",
|
||||||
|
r.status_code,
|
||||||
|
r.text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = r.json()
|
||||||
|
except ValueError:
|
||||||
|
response = None
|
||||||
|
return r, response
|
||||||
|
|
||||||
|
_send_get_request = functools.partialmethod(_send_request, "GET")
|
||||||
|
_send_post_request = functools.partialmethod(_send_request, "POST")
|
||||||
|
_send_patch_request = functools.partialmethod(_send_request, "PATCH")
|
||||||
|
_send_delete_request = functools.partialmethod(_send_request, "DELETE")
|
||||||
|
|
||||||
|
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):
|
||||||
|
r, response = self._send_post_request(
|
||||||
|
"/metrics/generate",
|
||||||
|
payload={
|
||||||
|
"entity": "space_metrics_by_appliance",
|
||||||
|
"entity_id": appliance_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = (_("Failed to query metrics for "
|
||||||
|
"PowerStore appliance with id %s.") % appliance_id)
|
||||||
|
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)
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
def create_volume(self, appliance_id, name, size):
|
||||||
|
r, response = self._send_post_request(
|
||||||
|
"/volume",
|
||||||
|
payload={
|
||||||
|
"appliance_id": appliance_id,
|
||||||
|
"name": name,
|
||||||
|
"size": size,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = _("Failed to create PowerStore volume %s.") % name
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return response["id"]
|
||||||
|
|
||||||
|
def delete_volume_or_snapshot(self, entity_id, entity="volume"):
|
||||||
|
r, response = self._send_delete_request("/volume/%s" % entity_id)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
if r.status_code == requests.codes.not_found:
|
||||||
|
LOG.warning("PowerStore %(entity)s with id %(entity_id)s is "
|
||||||
|
"not found. Ignoring error.",
|
||||||
|
{
|
||||||
|
"entity": entity,
|
||||||
|
"entity_id": entity_id,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
msg = (_("Failed to delete PowerStore %(entity)s with id "
|
||||||
|
"%(entity_id)s.")
|
||||||
|
% {"entity": entity,
|
||||||
|
"entity_id": entity_id, })
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
def extend_volume(self, volume_id, size):
|
||||||
|
r, response = self._send_patch_request(
|
||||||
|
"/volume/%s" % volume_id,
|
||||||
|
payload={
|
||||||
|
"size": size,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = (_("Failed to extend PowerStore volume with id %s.")
|
||||||
|
% volume_id)
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
def create_snapshot(self, volume_id, name):
|
||||||
|
r, response = self._send_post_request(
|
||||||
|
"/volume/%s/snapshot" % volume_id,
|
||||||
|
payload={
|
||||||
|
"name": name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = (_("Failed to create snapshot %(snapshot_name)s for "
|
||||||
|
"PowerStore volume with id %(volume_id)s.")
|
||||||
|
% {"snapshot_name": name,
|
||||||
|
"volume_id": volume_id, })
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return response["id"]
|
||||||
|
|
||||||
|
def clone_volume_or_snapshot(self,
|
||||||
|
name,
|
||||||
|
entity_id,
|
||||||
|
entity="volume"):
|
||||||
|
r, response = self._send_post_request(
|
||||||
|
"/volume/%s/clone" % entity_id,
|
||||||
|
payload={
|
||||||
|
"name": name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = (_("Failed to create clone %(clone_name)s for "
|
||||||
|
"PowerStore %(entity)s with id %(entity_id)s.")
|
||||||
|
% {"clone_name": name,
|
||||||
|
"entity": entity,
|
||||||
|
"entity_id": entity_id, })
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return response["id"]
|
||||||
|
|
||||||
|
def get_all_hosts(self, protocol):
|
||||||
|
r, response = self._send_get_request(
|
||||||
|
"/host",
|
||||||
|
params={
|
||||||
|
"select": "id,name,host_initiators",
|
||||||
|
"host_initiators->0->>port_type": "eq.%s" % protocol,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = _("Failed to query PowerStore hosts.")
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def create_host(self, name, ports):
|
||||||
|
r, response = self._send_post_request(
|
||||||
|
"/host",
|
||||||
|
payload={
|
||||||
|
"name": name,
|
||||||
|
"os_type": "Linux",
|
||||||
|
"initiators": ports
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = _("Failed to create PowerStore host %s.") % name
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def modify_host_initiators(self, host_id, **kwargs):
|
||||||
|
r, response = self._send_patch_request(
|
||||||
|
"/host/%s" % host_id,
|
||||||
|
payload={
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = (_("Failed to modify initiators of PowerStore host "
|
||||||
|
"with id %s.") % host_id)
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
def attach_volume_to_host(self, host_id, volume_id):
|
||||||
|
r, response = self._send_post_request(
|
||||||
|
"/volume/%s/attach" % volume_id,
|
||||||
|
payload={
|
||||||
|
"host_id": host_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = (_("Failed to attach PowerStore volume %(volume_id)s "
|
||||||
|
"to host %(host_id)s.")
|
||||||
|
% {"volume_id": volume_id,
|
||||||
|
"host_id": host_id, })
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
def get_volume_mapped_hosts(self, volume_id):
|
||||||
|
r, response = self._send_get_request(
|
||||||
|
"/host_volume_mapping",
|
||||||
|
params={
|
||||||
|
"volume_id": "eq.%s" % volume_id,
|
||||||
|
"select": "host_id"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = _("Failed to query PowerStore host volume mappings.")
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
mapped_hosts = [mapped_host["host_id"] for mapped_host in response]
|
||||||
|
return mapped_hosts
|
||||||
|
|
||||||
|
def get_volume_lun(self, host_id, volume_id):
|
||||||
|
r, response = self._send_get_request(
|
||||||
|
"/host_volume_mapping",
|
||||||
|
params={
|
||||||
|
"host_id": "eq.%s" % host_id,
|
||||||
|
"volume_id": "eq.%s" % volume_id,
|
||||||
|
"select": "logical_unit_number"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = _("Failed to query PowerStore host volume mappings.")
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
try:
|
||||||
|
logical_unit_number = response[0].get("logical_unit_number")
|
||||||
|
return logical_unit_number
|
||||||
|
except IndexError:
|
||||||
|
msg = (_("PowerStore mapping of volume with id %(volume_id)s "
|
||||||
|
"to host %(host_id)s is not found.")
|
||||||
|
% {"volume_id": volume_id,
|
||||||
|
"host_id": host_id, })
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
def get_fc_port(self, appliance_id):
|
||||||
|
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.")
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_ip_pool_address(self, appliance_id):
|
||||||
|
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)"
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = _("Failed to query PowerStore IP pool addresses.")
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def detach_volume_from_host(self, host_id, volume_id):
|
||||||
|
r, response = self._send_post_request(
|
||||||
|
"/volume/%s/detach" % volume_id,
|
||||||
|
payload={
|
||||||
|
"host_id": host_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
if r.status_code == requests.codes.not_found:
|
||||||
|
LOG.warning("PowerStore volume with id %(volume_id)s is "
|
||||||
|
"not found. Ignoring error.",
|
||||||
|
{
|
||||||
|
"volume_id": volume_id,
|
||||||
|
})
|
||||||
|
elif (
|
||||||
|
r.status_code == requests.codes.unprocessable and
|
||||||
|
any([
|
||||||
|
message["code"] == VOLUME_NOT_MAPPED_ERROR
|
||||||
|
for message in response["messages"]
|
||||||
|
])
|
||||||
|
):
|
||||||
|
LOG.warning("PowerStore volume with id %(volume_id)s is "
|
||||||
|
"not mapped to host with id %(host_id)s. "
|
||||||
|
"Ignoring error.",
|
||||||
|
{
|
||||||
|
"volume_id": volume_id,
|
||||||
|
"host_id": host_id,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
msg = (_("Failed to detach PowerStore volume %(volume_id)s "
|
||||||
|
"to host %(host_id)s.")
|
||||||
|
% {"volume_id": volume_id,
|
||||||
|
"host_id": host_id, })
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
def restore_from_snapshot(self, volume_id, snapshot_id):
|
||||||
|
r, response = self._send_post_request(
|
||||||
|
"/volume/%s/restore" % volume_id,
|
||||||
|
payload={
|
||||||
|
"from_snap_id": snapshot_id,
|
||||||
|
"create_backup_snap": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = (_("Failed to restore PowerStore volume with id "
|
||||||
|
"%(volume_id)s from snapshot with id %(snapshot_id)s.")
|
||||||
|
% {"volume_id": volume_id,
|
||||||
|
"snapshot_id": snapshot_id, })
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
119
cinder/volume/drivers/dell_emc/powerstore/driver.py
Normal file
119
cinder/volume/drivers/dell_emc/powerstore/driver.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# Copyright (c) 2020 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.
|
||||||
|
|
||||||
|
"""Cinder driver for Dell EMC PowerStore."""
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
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.san import san
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_opts(POWERSTORE_OPTS, group=configuration.SHARED_CONF_GROUP)
|
||||||
|
|
||||||
|
|
||||||
|
@interface.volumedriver
|
||||||
|
class PowerStoreDriver(driver.VolumeDriver):
|
||||||
|
"""Dell EMC PowerStore Driver.
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
Version history:
|
||||||
|
1.0.0 - Initial version
|
||||||
|
"""
|
||||||
|
|
||||||
|
VERSION = "1.0.0"
|
||||||
|
VENDOR = "Dell EMC"
|
||||||
|
|
||||||
|
# ThirdPartySystems wiki page
|
||||||
|
CI_WIKI_NAME = "DellEMC_PowerStore_CI"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(PowerStoreDriver, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.active_backend_id = kwargs.get("active_backend_id")
|
||||||
|
self.adapter = None
|
||||||
|
self.configuration.append_config_values(san.san_opts)
|
||||||
|
self.configuration.append_config_values(POWERSTORE_OPTS)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_driver_options():
|
||||||
|
return POWERSTORE_OPTS
|
||||||
|
|
||||||
|
def do_setup(self, context):
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
self.adapter = adapter.iSCSIAdapter(self.active_backend_id,
|
||||||
|
self.configuration)
|
||||||
|
self.adapter.do_setup()
|
||||||
|
|
||||||
|
def check_for_setup_error(self):
|
||||||
|
self.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)
|
||||||
|
|
||||||
|
def extend_volume(self, volume, new_size):
|
||||||
|
return self.adapter.extend_volume(volume, new_size)
|
||||||
|
|
||||||
|
def create_snapshot(self, snapshot):
|
||||||
|
return self.adapter.create_snapshot(snapshot)
|
||||||
|
|
||||||
|
def delete_snapshot(self, snapshot):
|
||||||
|
return self.adapter.delete_snapshot(snapshot)
|
||||||
|
|
||||||
|
def create_cloned_volume(self, volume, src_vref):
|
||||||
|
return self.adapter.create_cloned_volume(volume, src_vref)
|
||||||
|
|
||||||
|
def create_volume_from_snapshot(self, volume, snapshot):
|
||||||
|
return self.adapter.create_volume_from_snapshot(volume, snapshot)
|
||||||
|
|
||||||
|
def initialize_connection(self, volume, connector, **kwargs):
|
||||||
|
return self.adapter.initialize_connection(volume, connector, **kwargs)
|
||||||
|
|
||||||
|
def terminate_connection(self, volume, connector, **kwargs):
|
||||||
|
return self.adapter.terminate_connection(volume, connector, **kwargs)
|
||||||
|
|
||||||
|
def revert_to_snapshot(self, context, volume, snapshot):
|
||||||
|
return self.adapter.revert_to_snapshot(volume, snapshot)
|
||||||
|
|
||||||
|
def _update_volume_stats(self):
|
||||||
|
stats = self.adapter.update_volume_stats()
|
||||||
|
stats["driver_version"] = self.VERSION
|
||||||
|
stats["vendor_name"] = self.VENDOR
|
||||||
|
self._stats = stats
|
||||||
|
|
||||||
|
def create_export(self, context, volume, connector):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ensure_export(self, context, volume):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_export(self, context, volume):
|
||||||
|
pass
|
33
cinder/volume/drivers/dell_emc/powerstore/options.py
Normal file
33
cinder/volume/drivers/dell_emc/powerstore/options.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Copyright (c) 2020 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.
|
||||||
|
|
||||||
|
"""Configuration options for Dell EMC PowerStore Cinder driver."""
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
POWERSTORE_APPLIANCES = "powerstore_appliances"
|
||||||
|
POWERSTORE_PORTS = "powerstore_ports"
|
||||||
|
|
||||||
|
POWERSTORE_OPTS = [
|
||||||
|
cfg.ListOpt(POWERSTORE_APPLIANCES,
|
||||||
|
default=[],
|
||||||
|
help="Appliances names. Comma separated list of PowerStore "
|
||||||
|
"appliances names used to provision volumes. Required."),
|
||||||
|
cfg.ListOpt(POWERSTORE_PORTS,
|
||||||
|
default=[],
|
||||||
|
help="Allowed ports. Comma separated list of PowerStore "
|
||||||
|
"iSCSI IPs or FC WWNs (ex. 58:cc:f0:98:49:22:07:02) "
|
||||||
|
"to be used. If option is not set all ports are allowed.")
|
||||||
|
]
|
136
cinder/volume/drivers/dell_emc/powerstore/utils.py
Normal file
136
cinder/volume/drivers/dell_emc/powerstore/utils.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# Copyright (c) 2020 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.
|
||||||
|
|
||||||
|
"""Utilities for Dell EMC PowerStore Cinder driver."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import units
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _
|
||||||
|
from cinder.objects import fields
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_gib(size_in_bytes):
|
||||||
|
"""Convert size in bytes to GiB.
|
||||||
|
|
||||||
|
:param size_in_bytes: size in bytes
|
||||||
|
:return: size in GiB
|
||||||
|
"""
|
||||||
|
|
||||||
|
return size_in_bytes // units.Gi
|
||||||
|
|
||||||
|
|
||||||
|
def gib_to_bytes(size_in_gb):
|
||||||
|
"""Convert size in GiB to bytes.
|
||||||
|
|
||||||
|
:param size_in_gb: size in GiB
|
||||||
|
:return: size in bytes
|
||||||
|
"""
|
||||||
|
|
||||||
|
return size_in_gb * units.Gi
|
||||||
|
|
||||||
|
|
||||||
|
def extract_fc_wwpns(connector):
|
||||||
|
"""Convert connector FC ports to appropriate format with colons.
|
||||||
|
|
||||||
|
:param connector: connection properties
|
||||||
|
:return: FC ports in appropriate format with colons
|
||||||
|
"""
|
||||||
|
|
||||||
|
if "wwnns" not in connector or "wwpns" not in connector:
|
||||||
|
msg = _("Host %s does not have FC initiators.") % connector["host"]
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return [":".join(re.findall("..", wwpn)) for wwpn in connector["wwpns"]]
|
||||||
|
|
||||||
|
|
||||||
|
def fc_wwn_to_string(wwn):
|
||||||
|
"""Convert FC WWN to string without colons.
|
||||||
|
|
||||||
|
:param wwn: FC WWN
|
||||||
|
:return: FC WWN without colons
|
||||||
|
"""
|
||||||
|
|
||||||
|
return wwn.replace(":", "")
|
||||||
|
|
||||||
|
|
||||||
|
def iscsi_portal_with_port(address):
|
||||||
|
"""Add default port 3260 to iSCSI portal
|
||||||
|
|
||||||
|
:param address: iSCSI portal without port
|
||||||
|
:return: iSCSI portal with default port 3260
|
||||||
|
"""
|
||||||
|
|
||||||
|
return "%(address)s:3260" % {"address": address}
|
||||||
|
|
||||||
|
|
||||||
|
def powerstore_host_name(connector, protocol):
|
||||||
|
"""Generate PowerStore host name for connector.
|
||||||
|
|
||||||
|
:param connector: connection properties
|
||||||
|
:param protocol: storage protocol (FC or iSCSI)
|
||||||
|
:return: unique host name
|
||||||
|
"""
|
||||||
|
|
||||||
|
return ("%(host)s-%(protocol)s" %
|
||||||
|
{"host": connector["host"],
|
||||||
|
"protocol": protocol, })
|
||||||
|
|
||||||
|
|
||||||
|
def filter_hosts_by_initiators(hosts, initiators):
|
||||||
|
"""Filter hosts by given list of initiators.
|
||||||
|
|
||||||
|
:param hosts: list of PowerStore host objects
|
||||||
|
:param initiators: list of initiators
|
||||||
|
:return: PowerStore hosts list
|
||||||
|
"""
|
||||||
|
|
||||||
|
hosts_names_found = set()
|
||||||
|
for host in hosts:
|
||||||
|
for initiator in host["host_initiators"]:
|
||||||
|
if initiator["port_name"] in initiators:
|
||||||
|
hosts_names_found.add(host["name"])
|
||||||
|
return list(filter(lambda host: host["name"] in hosts_names_found, hosts))
|
||||||
|
|
||||||
|
|
||||||
|
def is_multiattached_to_host(volume_attachment, host_name):
|
||||||
|
"""Check if volume is attached to multiple instances on one host.
|
||||||
|
|
||||||
|
When multiattach is enabled, a volume could be attached to two or more
|
||||||
|
instances which are hosted on one nova host.
|
||||||
|
Because PowerStore cannot recognize the volume is attached to two or more
|
||||||
|
instances, we should keep the volume attached to the nova host until
|
||||||
|
the volume is detached from the last instance.
|
||||||
|
|
||||||
|
:param volume_attachment: list of VolumeAttachment objects
|
||||||
|
:param host_name: OpenStack host name
|
||||||
|
:return: multiattach flag
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not volume_attachment:
|
||||||
|
return False
|
||||||
|
|
||||||
|
attachments = [
|
||||||
|
attachment for attachment in volume_attachment
|
||||||
|
if (attachment.attach_status == fields.VolumeAttachStatus.ATTACHED and
|
||||||
|
attachment.attached_host == host_name)
|
||||||
|
]
|
||||||
|
return len(attachments) > 1
|
@ -0,0 +1,79 @@
|
|||||||
|
==========================
|
||||||
|
Dell EMC PowerStore driver
|
||||||
|
==========================
|
||||||
|
|
||||||
|
This section explains how to configure and connect the block
|
||||||
|
storage nodes to an PowerStore storage cluster.
|
||||||
|
|
||||||
|
Supported operations
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Create, delete, attach and detach volumes.
|
||||||
|
- Create, delete volume snapshots.
|
||||||
|
- Create a volume from a snapshot.
|
||||||
|
- Copy an image to a volume.
|
||||||
|
- Copy a volume to an image.
|
||||||
|
- Clone a volume.
|
||||||
|
- Extend a volume.
|
||||||
|
- Get volume statistics.
|
||||||
|
- Attach a volume to multiple servers simultaneously (multiattach).
|
||||||
|
- Revert a volume to a snapshot.
|
||||||
|
|
||||||
|
Driver configuration
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Add the following content into ``/etc/cinder/cinder.conf``:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
enabled_backends = powerstore
|
||||||
|
|
||||||
|
[powerstore]
|
||||||
|
# PowerStore REST IP
|
||||||
|
san_ip = <San IP>
|
||||||
|
# PowerStore REST username and password
|
||||||
|
san_login = <San username>
|
||||||
|
san_password = <San Password>
|
||||||
|
# Storage protocol
|
||||||
|
storage_protocol = <Storage protocol> # FC or iSCSI
|
||||||
|
# Volume driver name
|
||||||
|
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
|
||||||
|
|
||||||
|
Driver options
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The driver supports the following configuration options:
|
||||||
|
|
||||||
|
.. config-table::
|
||||||
|
:config-target: PowerStore
|
||||||
|
|
||||||
|
cinder.volume.drivers.dell_emc.powerstore.driver
|
||||||
|
|
||||||
|
SSL support
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
To enable the SSL certificate verification, modify the following options in the
|
||||||
|
``cinder.conf`` file:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
driver_ssl_cert_verify = True
|
||||||
|
driver_ssl_cert_path = <path to the CA>
|
||||||
|
|
||||||
|
By default, the SSL certificate validation is disabled.
|
||||||
|
|
||||||
|
If the ``driver_ssl_cert_path`` option is omitted, the system default CA will
|
||||||
|
be used.
|
||||||
|
|
||||||
|
Thin provisioning and compression
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The driver creates thin provisioned compressed volumes by default.
|
||||||
|
Thick provisioning is not supported.
|
@ -24,6 +24,9 @@ title=Dell EMC XtremeIO Storage Driver (FC, iSCSI)
|
|||||||
[driver.dell_emc_powermax]
|
[driver.dell_emc_powermax]
|
||||||
title=Dell EMC PowerMax (2000, 8000) Storage Driver (iSCSI, FC)
|
title=Dell EMC PowerMax (2000, 8000) Storage Driver (iSCSI, FC)
|
||||||
|
|
||||||
|
[driver.dell_emc_powerstore]
|
||||||
|
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)
|
||||||
|
|
||||||
@ -198,6 +201,7 @@ notes=A vendor driver is considered supported if the vendor is
|
|||||||
isn't resolved before the end of the subsequent release.
|
isn't resolved before the end of the subsequent release.
|
||||||
driver.datera=complete
|
driver.datera=complete
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
|
driver.dell_emc_powerstore=complete
|
||||||
driver.dell_emc_sc=complete
|
driver.dell_emc_sc=complete
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
driver.dell_emc_vmax_af=complete
|
driver.dell_emc_vmax_af=complete
|
||||||
@ -261,6 +265,7 @@ notes=Cinder supports the ability to extend a volume that is attached to
|
|||||||
an instance, but not all drivers are able to do this.
|
an instance, but not all drivers are able to do this.
|
||||||
driver.datera=complete
|
driver.datera=complete
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
|
driver.dell_emc_powerstore=missing
|
||||||
driver.dell_emc_sc=complete
|
driver.dell_emc_sc=complete
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
driver.dell_emc_vmax_af=complete
|
driver.dell_emc_vmax_af=complete
|
||||||
@ -324,6 +329,7 @@ notes=This is the ability to directly attach a snapshot to an
|
|||||||
instance like a volume.
|
instance like a volume.
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=missing
|
driver.dell_emc_powermax=missing
|
||||||
|
driver.dell_emc_powerstore=missing
|
||||||
driver.dell_emc_sc=missing
|
driver.dell_emc_sc=missing
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
driver.dell_emc_vmax_af=missing
|
driver.dell_emc_vmax_af=missing
|
||||||
@ -390,6 +396,7 @@ notes=Vendor drivers that support Quality of Service (QoS) at the
|
|||||||
utilize frontend QoS via libvirt.
|
utilize frontend QoS via libvirt.
|
||||||
driver.datera=complete
|
driver.datera=complete
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
|
driver.dell_emc_powerstore=missing
|
||||||
driver.dell_emc_sc=complete
|
driver.dell_emc_sc=complete
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
driver.dell_emc_vmax_af=complete
|
driver.dell_emc_vmax_af=complete
|
||||||
@ -455,6 +462,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_sc=complete
|
driver.dell_emc_sc=complete
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
driver.dell_emc_vmax_af=complete
|
driver.dell_emc_vmax_af=complete
|
||||||
@ -521,6 +529,7 @@ notes=Vendor drivers that support consistency groups are able to
|
|||||||
creation of consistent snapshots across a group.
|
creation of consistent snapshots across a group.
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
|
driver.dell_emc_powerstore=missing
|
||||||
driver.dell_emc_sc=complete
|
driver.dell_emc_sc=complete
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
driver.dell_emc_vmax_af=complete
|
driver.dell_emc_vmax_af=complete
|
||||||
@ -586,6 +595,7 @@ notes=If a volume driver supports thin provisioning it means that it
|
|||||||
'oversubscription'.
|
'oversubscription'.
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
|
driver.dell_emc_powerstore=complete
|
||||||
driver.dell_emc_sc=complete
|
driver.dell_emc_sc=complete
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
driver.dell_emc_vmax_af=complete
|
driver.dell_emc_vmax_af=complete
|
||||||
@ -652,6 +662,7 @@ notes=Storage assisted volume migration is like host assisted volume
|
|||||||
functionality.
|
functionality.
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
|
driver.dell_emc_powerstore=missing
|
||||||
driver.dell_emc_sc=missing
|
driver.dell_emc_sc=missing
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
driver.dell_emc_vmax_af=complete
|
driver.dell_emc_vmax_af=complete
|
||||||
@ -718,6 +729,7 @@ notes=Vendor drivers that report multi-attach support are able
|
|||||||
attach functionality otherwise data corruption may occur.
|
attach functionality otherwise data corruption may occur.
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
|
driver.dell_emc_powerstore=complete
|
||||||
driver.dell_emc_sc=complete
|
driver.dell_emc_sc=complete
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
driver.dell_emc_vmax_af=complete
|
driver.dell_emc_vmax_af=complete
|
||||||
@ -781,6 +793,7 @@ notes=Vendor drivers that implement the driver assisted function to revert a
|
|||||||
volume to the last snapshot taken.
|
volume to the last snapshot taken.
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
|
driver.dell_emc_powerstore=complete
|
||||||
driver.dell_emc_sc=missing
|
driver.dell_emc_sc=missing
|
||||||
driver.dell_emc_unity=complete
|
driver.dell_emc_unity=complete
|
||||||
driver.dell_emc_vmax_af=complete
|
driver.dell_emc_vmax_af=complete
|
||||||
@ -848,6 +861,7 @@ notes=Vendor drivers that support running in an active/active
|
|||||||
a configuration.
|
a configuration.
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=missing
|
driver.dell_emc_powermax=missing
|
||||||
|
driver.dell_emc_powerstore=missing
|
||||||
driver.dell_emc_sc=missing
|
driver.dell_emc_sc=missing
|
||||||
driver.dell_emc_unity=missing
|
driver.dell_emc_unity=missing
|
||||||
driver.dell_emc_vmax_af=missing
|
driver.dell_emc_vmax_af=missing
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Add Dell EMC PowerStore Storage Driver (iSCSI, FC).
|
Loading…
x
Reference in New Issue
Block a user