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:
Ivan Pchelintsev 2020-07-02 18:00:54 +03:00
parent be4a682890
commit 517cb6448b
16 changed files with 2275 additions and 0 deletions

View File

@ -74,6 +74,8 @@ from cinder.volume.drivers.datera import datera_iscsi as \
cinder_volume_drivers_datera_dateraiscsi
from cinder.volume.drivers.dell_emc.powermax import common as \
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 \
cinder_volume_drivers_dell_emc_sc_storagecentercommon
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.fqdn_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.
common_opts,
cinder_volume_drivers_dell_emc_unity_driver.UNITY_OPTS,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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),
},
}

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

View 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

View 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.")
]

View 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

View File

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

View File

@ -24,6 +24,9 @@ title=Dell EMC XtremeIO Storage Driver (FC, iSCSI)
[driver.dell_emc_powermax]
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]
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.
driver.datera=complete
driver.dell_emc_powermax=complete
driver.dell_emc_powerstore=complete
driver.dell_emc_sc=complete
driver.dell_emc_unity=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.
driver.datera=complete
driver.dell_emc_powermax=complete
driver.dell_emc_powerstore=missing
driver.dell_emc_sc=complete
driver.dell_emc_unity=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.
driver.datera=missing
driver.dell_emc_powermax=missing
driver.dell_emc_powerstore=missing
driver.dell_emc_sc=missing
driver.dell_emc_unity=complete
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.
driver.datera=complete
driver.dell_emc_powermax=complete
driver.dell_emc_powerstore=missing
driver.dell_emc_sc=complete
driver.dell_emc_unity=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.
driver.datera=missing
driver.dell_emc_powermax=complete
driver.dell_emc_powerstore=missing
driver.dell_emc_sc=complete
driver.dell_emc_unity=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.
driver.datera=missing
driver.dell_emc_powermax=complete
driver.dell_emc_powerstore=missing
driver.dell_emc_sc=complete
driver.dell_emc_unity=complete
driver.dell_emc_vmax_af=complete
@ -586,6 +595,7 @@ notes=If a volume driver supports thin provisioning it means that it
'oversubscription'.
driver.datera=missing
driver.dell_emc_powermax=complete
driver.dell_emc_powerstore=complete
driver.dell_emc_sc=complete
driver.dell_emc_unity=complete
driver.dell_emc_vmax_af=complete
@ -652,6 +662,7 @@ notes=Storage assisted volume migration is like host assisted volume
functionality.
driver.datera=missing
driver.dell_emc_powermax=complete
driver.dell_emc_powerstore=missing
driver.dell_emc_sc=missing
driver.dell_emc_unity=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.
driver.datera=missing
driver.dell_emc_powermax=complete
driver.dell_emc_powerstore=complete
driver.dell_emc_sc=complete
driver.dell_emc_unity=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.
driver.datera=missing
driver.dell_emc_powermax=complete
driver.dell_emc_powerstore=complete
driver.dell_emc_sc=missing
driver.dell_emc_unity=complete
driver.dell_emc_vmax_af=complete
@ -848,6 +861,7 @@ notes=Vendor drivers that support running in an active/active
a configuration.
driver.datera=missing
driver.dell_emc_powermax=missing
driver.dell_emc_powerstore=missing
driver.dell_emc_sc=missing
driver.dell_emc_unity=missing
driver.dell_emc_vmax_af=missing

View File

@ -0,0 +1,4 @@
---
features:
- |
Add Dell EMC PowerStore Storage Driver (iSCSI, FC).