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
|
||||
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,
|
||||
|
@ -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]
|
||||
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
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add Dell EMC PowerStore Storage Driver (iSCSI, FC).
|
Loading…
Reference in New Issue
Block a user