Dell PowerFlex: Improve secret handling

The scaleio connector required PowerFlex backend credentials
to invoke PowerFlex RESTAPI to map/unmap volumes and set QoS.
The credentials were deployed in the connector configuration
file, which was not suitable for use with bare metal hosts.

This patch removes the need for connector configuration.
The scaleio connector passes SDC GUID in the connector properties.
Dell PowerFlex driver retrieves SDC GUID from the connector
properties and handled the mapping logic and QoS setting instead.

Depends-On: https://review.opendev.org/c/openstack/os-brick/+/952655
Closes-Bug: #2114879
Change-Id: Ia51ecebacb12e5ed21cd0e3236c434edba7fe8bc
Signed-off-by: Yian Zong <yian.zong@dell.com>
This commit is contained in:
Yian Zong
2025-05-29 07:35:15 +00:00
parent c948b22eac
commit e3d2601109
13 changed files with 1356 additions and 346 deletions

View File

@@ -0,0 +1,51 @@
{
"hostOsFullType": null,
"systemId": "2c4a220db6e0520f",
"name": null,
"mdmConnectionState": "Connected",
"softwareVersionInfo": "R5_5.0.0",
"peerMdmId": null,
"sdtId": null,
"sdcApproved": true,
"sdcAgentActive": false,
"mdmIpAddressesCurrent": false,
"sdcIp": "192.168.10.12",
"sdcIps": [
"192.168.10.12"
],
"osType": "Linux",
"perfProfile": "HighPerformance",
"socketAllocationFailure": null,
"memoryAllocationFailure": null,
"versionInfo": "R5_5.0.0",
"nqn": null,
"maxNumPaths": null,
"maxNumSysPorts": null,
"sdcType": "AppSdc",
"sdcGuid": "028888FA-502A-4FAC-A888-1FA3B256358C",
"installedSoftwareVersionInfo": "R5_5.0.0",
"kernelVersion": "5.15.179",
"kernelBuildNumber": null,
"sdcApprovedIps": null,
"hostType": "SdcHost",
"sdrId": null,
"id": "01f7117d0000000b",
"links": [
{
"rel": "self",
"href": "/api/instances/Sdc::01f7117d0000000b"
},
{
"rel": "/api/Sdc/relationship/Statistics",
"href": "/api/instances/Sdc::01f7117d0000000b/relationships/Statistics"
},
{
"rel": "/api/Sdc/relationship/Volume",
"href": "/api/instances/Sdc::01f7117d0000000b/relationships/Volume"
},
{
"rel": "/api/parent/relationship/systemId",
"href": "/api/instances/System::2c4a220db6e0520f"
}
]
}

View File

@@ -0,0 +1,53 @@
[
{
"hostOsFullType": null,
"systemId": "2c4a220db6e0520f",
"name": null,
"mdmConnectionState": "Connected",
"softwareVersionInfo": "R5_5.0.0",
"peerMdmId": null,
"sdtId": null,
"sdcApproved": true,
"sdcAgentActive": false,
"mdmIpAddressesCurrent": false,
"sdcIp": "192.168.10.12",
"sdcIps": [
"192.168.10.12"
],
"osType": "Linux",
"perfProfile": "HighPerformance",
"socketAllocationFailure": null,
"memoryAllocationFailure": null,
"versionInfo": "R5_5.0.0",
"nqn": null,
"maxNumPaths": null,
"maxNumSysPorts": null,
"sdcType": "AppSdc",
"sdcGuid": "028888FA-502A-4FAC-A888-1FA3B256358C",
"installedSoftwareVersionInfo": "R5_5.0.0",
"kernelVersion": "5.15.179",
"kernelBuildNumber": null,
"sdcApprovedIps": null,
"hostType": "SdcHost",
"sdrId": null,
"id": "01f7117d0000000b",
"links": [
{
"rel": "self",
"href": "/api/instances/Sdc::01f7117d0000000b"
},
{
"rel": "/api/Sdc/relationship/Statistics",
"href": "/api/instances/Sdc::01f7117d0000000b/relationships/Statistics"
},
{
"rel": "/api/Sdc/relationship/Volume",
"href": "/api/instances/Sdc::01f7117d0000000b/relationships/Volume"
},
{
"rel": "/api/parent/relationship/systemId",
"href": "/api/instances/System::2c4a220db6e0520f"
}
]
}
]

View File

@@ -0,0 +1,128 @@
[
{
"managedBy": "ScaleIO",
"originalExpiryTime": 0,
"retentionLevels": [],
"snplIdOfSourceVolume": null,
"volumeReplicationState": "UnmarkedForReplication",
"mappedSdcInfo": [
{
"limitIops": 0,
"limitBwInMbps": 0,
"isDirectBufferMapping": false,
"sdcId": "01f7117d0000000b",
"sdcIp": "192.168.10.12",
"sdcName": null,
"accessMode": "ReadWrite",
"nqn": null,
"hostType": "SdcHost"
}
],
"replicationJournalVolume": false,
"replicationTimeStamp": 0,
"name": "yian_sdc_1",
"creationTime": 1746889338,
"storagePoolId": "fa85edfd00000000",
"dataLayout": "MediumGranularity",
"compressionMethod": "NotApplicable",
"vtreeId": "2b713ee600000007",
"sizeInKb": 8388608,
"volumeClass": "defaultclass",
"accessModeLimit": "ReadWrite",
"pairIds": null,
"volumeType": "ThinProvisioned",
"consistencyGroupId": null,
"ancestorVolumeId": null,
"notGenuineSnapshot": false,
"secureSnapshotExpTime": 0,
"useRmcache": false,
"snplIdOfAutoSnapshot": null,
"lockedAutoSnapshot": false,
"lockedAutoSnapshotMarkedForRemoval": false,
"autoSnapshotGroupId": null,
"timeStampIsAccurate": false,
"nsid": 12,
"id": "694a2d140000000b",
"links": [
{
"rel": "self",
"href": "/api/instances/Volume::694a2d140000000b"
},
{
"rel": "/api/Volume/relationship/Statistics",
"href": "/api/instances/Volume::694a2d140000000b/relationships/Statistics"
},
{
"rel": "/api/parent/relationship/vtreeId",
"href": "/api/instances/VTree::2b713ee600000007"
},
{
"rel": "/api/parent/relationship/storagePoolId",
"href": "/api/instances/StoragePool::fa85edfd00000000"
}
]
},
{
"managedBy": "ScaleIO",
"originalExpiryTime": 0,
"retentionLevels": [],
"snplIdOfSourceVolume": null,
"volumeReplicationState": "UnmarkedForReplication",
"mappedSdcInfo": [
{
"limitIops": 0,
"limitBwInMbps": 0,
"isDirectBufferMapping": false,
"sdcId": "01f7117d0000000b",
"sdcIp": "192.168.10.12",
"sdcName": null,
"accessMode": "ReadWrite",
"nqn": null,
"hostType": "SdcHost"
}
],
"replicationJournalVolume": false,
"replicationTimeStamp": 0,
"name": "yian_sdc_0",
"creationTime": 1746886943,
"storagePoolId": "fa85edfd00000000",
"dataLayout": "MediumGranularity",
"compressionMethod": "NotApplicable",
"vtreeId": "2b713ee500000002",
"sizeInKb": 8388608,
"volumeClass": "defaultclass",
"accessModeLimit": "ReadWrite",
"pairIds": null,
"volumeType": "ThinProvisioned",
"consistencyGroupId": null,
"ancestorVolumeId": null,
"notGenuineSnapshot": false,
"secureSnapshotExpTime": 0,
"useRmcache": false,
"snplIdOfAutoSnapshot": null,
"lockedAutoSnapshot": false,
"lockedAutoSnapshotMarkedForRemoval": false,
"autoSnapshotGroupId": null,
"timeStampIsAccurate": false,
"nsid": 10,
"id": "694a2d1300000009",
"links": [
{
"rel": "self",
"href": "/api/instances/Volume::694a2d1300000009"
},
{
"rel": "/api/Volume/relationship/Statistics",
"href": "/api/instances/Volume::694a2d1300000009/relationships/Statistics"
},
{
"rel": "/api/parent/relationship/vtreeId",
"href": "/api/instances/VTree::2b713ee500000002"
},
{
"rel": "/api/parent/relationship/storagePoolId",
"href": "/api/instances/StoragePool::fa85edfd00000000"
}
]
}
]

View File

@@ -29,13 +29,6 @@ class TestAttachDetachVolume(powerflex.TestPowerFlexDriver):
ctx, **{'provider_id': fake.PROVIDER_ID})
self.driver.connector = FakeConnector()
def test_attach_volume(self):
path = self.driver._sio_attach_volume(self.volume)
self.assertEqual(self.fake_path, path)
def test_detach_volume(self):
self.driver._sio_detach_volume(self.volume)
class FakeConnector(object):
def connect_volume(self, connection_properties):

View File

@@ -18,6 +18,7 @@ from cinder import context
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_volume
from cinder.tests.unit.volume.drivers.dell_emc import powerflex
from cinder.tests.unit.volume.drivers.dell_emc.powerflex import mocks
class TestInitializeConnection(powerflex.TestPowerFlexDriver):
@@ -25,65 +26,71 @@ class TestInitializeConnection(powerflex.TestPowerFlexDriver):
"""Setup a test case environment."""
super(TestInitializeConnection, self).setUp()
self.connector = {}
self.connector = {'sdc_guid': 'fake_guid'}
self.ctx = (
context.RequestContext('fake', 'fake', True, auth_token=True))
self.volume = fake_volume.fake_volume_obj(
self.ctx, **{'provider_id': fake.PROVIDER_ID})
self.sdc = {
"id": "sdc1",
}
self.HTTPS_MOCK_RESPONSES = {
self.RESPONSE_MODE.Valid: {
'types/Sdc/instances':
[{'id': "sdc1", 'sdcGuid': 'fake_guid'}],
'instances/Volume::{}/action/setMappedSdcLimits'.format(
self.volume.provider_id
): mocks.MockHTTPSResponse({}, 200),
},
}
def test_only_qos(self):
qos = {'maxIOPS': 1000, 'maxBWS': 2048}
extraspecs = {}
connection_properties = (
self._initialize_connection(qos, extraspecs)['data'])
self.assertEqual(1000, int(connection_properties['iopsLimit']))
self.assertEqual(2048, int(connection_properties['bandwidthLimit']))
self._initialize_connection(qos, extraspecs)['data']
self.driver.primary_client.set_sdc_limits.assert_called_once_with(
self.volume.provider_id, self.sdc["id"], '2048', '1000')
def test_no_qos(self):
qos = {}
extraspecs = {}
connection_properties = (
self._initialize_connection(qos, extraspecs)['data'])
self.assertIsNone(connection_properties['iopsLimit'])
self.assertIsNone(connection_properties['bandwidthLimit'])
self._initialize_connection(qos, extraspecs)['data']
self.driver.primary_client.set_sdc_limits.assert_not_called
def test_qos_scaling_and_max(self):
qos = {'maxIOPS': 100, 'maxBWS': 2048, 'maxIOPSperGB': 10,
'maxBWSperGB': 128}
extraspecs = {}
self.volume.size = 8
connection_properties = (
self._initialize_connection(qos, extraspecs)['data'])
self.assertEqual(80, int(connection_properties['iopsLimit']))
self.assertEqual(1024, int(connection_properties['bandwidthLimit']))
self._initialize_connection(qos, extraspecs)['data']
self.driver.primary_client.set_sdc_limits.assert_called_once_with(
self.volume.provider_id, self.sdc["id"], '1024', '80')
self.volume.size = 24
connection_properties = (
self._initialize_connection(qos, extraspecs)['data'])
self.assertEqual(100, int(connection_properties['iopsLimit']))
self.assertEqual(2048, int(connection_properties['bandwidthLimit']))
self._initialize_connection(qos, extraspecs)['data']
self.driver.primary_client.set_sdc_limits.assert_called_once_with(
self.volume.provider_id, self.sdc["id"], '2048', '100')
def test_qos_scaling_no_max(self):
qos = {'maxIOPSperGB': 10, 'maxBWSperGB': 128}
extraspecs = {}
self.volume.size = 8
connection_properties = (
self._initialize_connection(qos, extraspecs)['data'])
self.assertEqual(80, int(connection_properties['iopsLimit']))
self.assertEqual(1024, int(connection_properties['bandwidthLimit']))
self._initialize_connection(qos, extraspecs)['data']
self.driver.primary_client.set_sdc_limits.assert_called_once_with(
self.volume.provider_id, self.sdc["id"], '1024', '80')
def test_qos_round_up(self):
qos = {'maxBWS': 2000, 'maxBWSperGB': 100}
extraspecs = {}
self.volume.size = 8
connection_properties = (
self._initialize_connection(qos, extraspecs)['data'])
self.assertEqual(1024, int(connection_properties['bandwidthLimit']))
self._initialize_connection(qos, extraspecs)['data']
self.driver.primary_client.set_sdc_limits.assert_called_once_with(
self.volume.provider_id, self.sdc["id"], '1024', None)
self.volume.size = 24
connection_properties = (
self._initialize_connection(qos, extraspecs)['data'])
self.assertEqual(2048, int(connection_properties['bandwidthLimit']))
self._initialize_connection(qos, extraspecs)['data']
self.driver.primary_client.set_sdc_limits.assert_called_once_with(
self.volume.provider_id, self.sdc["id"], '2048', None)
def test_vol_id(self):
extraspecs = qos = {}
@@ -97,4 +104,18 @@ class TestInitializeConnection(powerflex.TestPowerFlexDriver):
self.driver._get_volumetype_qos.return_value = qos
self.driver._get_volumetype_extraspecs = mock.MagicMock()
self.driver._get_volumetype_extraspecs.return_value = extraspecs
return self.driver.initialize_connection(self.volume, self.connector)
self.driver._attach_volume_to_host = mock.MagicMock(
return_value=None
)
self.driver._check_volume_mapped = mock.MagicMock(
return_value=None
)
self.driver.primary_client.set_sdc_limits = mock.MagicMock()
res = self.driver.initialize_connection(self.volume, self.connector)
self.driver._get_volumetype_extraspecs.assert_called_once_with(
self.volume)
self.driver._attach_volume_to_host.assert_called_once_with(
self.volume, self.sdc['id'])
self.driver._check_volume_mapped.assert_called_once_with(
self.sdc['id'], self.volume.provider_id)
return res

View File

@@ -20,6 +20,7 @@ from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.tests.unit.volume.drivers.dell_emc import powerflex
from cinder.tests.unit.volume.drivers.dell_emc.powerflex import mocks
class TestInitializeConnectionSnapshot(powerflex.TestPowerFlexDriver):
@@ -31,7 +32,20 @@ class TestInitializeConnectionSnapshot(powerflex.TestPowerFlexDriver):
self.fake_path = '/fake/path/vol-xx'
self.volume = fake_volume.fake_volume_obj(
self.ctx, **{'provider_id': fake.PROVIDER_ID})
self.connector = {}
self.connector = {'sdc_guid': 'fake_guid'}
self.sdc = {
"id": "sdc1",
}
self.HTTPS_MOCK_RESPONSES = {
self.RESPONSE_MODE.Valid: {
'types/Sdc/instances':
[{'id': "sdc1", 'sdcGuid': 'fake_guid'}],
'instances/Volume::{}/action/setMappedSdcLimits'.format(
self.snapshot_id
): mocks.MockHTTPSResponse({}, 200),
},
}
def test_backup_can_use_snapshots(self):
"""Make sure the driver can use snapshots for backup."""
@@ -42,11 +56,17 @@ class TestInitializeConnectionSnapshot(powerflex.TestPowerFlexDriver):
"""Test initializing when we do not know the snapshot size.
ScaleIO can determine QOS specs based upon volume/snapshot size
The QOS keys should always be returned
The QOS keys should not be returned
"""
snapshot = fake_snapshot.fake_snapshot_obj(
self.ctx, **{'volume': self.volume,
'provider_id': self.snapshot_id})
self.driver._attach_volume_to_host = mock.MagicMock(
return_value=None
)
self.driver._check_volume_mapped = mock.MagicMock(
return_value=None
)
props = self.driver.initialize_connection_snapshot(
snapshot,
self.connector)
@@ -56,19 +76,25 @@ class TestInitializeConnectionSnapshot(powerflex.TestPowerFlexDriver):
self.assertIsNotNone(props['data']['scaleIO_volname'])
self.assertEqual(self.snapshot_id,
props['data']['scaleIO_volume_id'])
# make sure QOS properties are set
self.assertIn('iopsLimit', props['data'])
# make sure QOS properties are not set
self.assertNotIn('iopsLimit', props['data'])
def test_initialize_connection_with_size(self):
"""Test initializing when we know the snapshot size.
PowerFlex can determine QOS specs based upon volume/snapshot size
The QOS keys should always be returned
The QOS keys should not be returned
"""
snapshot = fake_snapshot.fake_snapshot_obj(
self.ctx, **{'volume': self.volume,
'provider_id': self.snapshot_id,
'volume_size': 8})
self.driver._attach_volume_to_host = mock.MagicMock(
return_value=None
)
self.driver._check_volume_mapped = mock.MagicMock(
return_value=None
)
props = self.driver.initialize_connection_snapshot(
snapshot,
self.connector)
@@ -78,8 +104,8 @@ class TestInitializeConnectionSnapshot(powerflex.TestPowerFlexDriver):
self.assertIsNotNone(props['data']['scaleIO_volname'])
self.assertEqual(self.snapshot_id,
props['data']['scaleIO_volume_id'])
# make sure QOS properties are set
self.assertIn('iopsLimit', props['data'])
# make sure QOS properties are not set
self.assertNotIn('iopsLimit', props['data'])
def test_qos_specs(self):
"""Ensure QOS specs are honored if present."""
@@ -93,10 +119,16 @@ class TestInitializeConnectionSnapshot(powerflex.TestPowerFlexDriver):
self.driver._get_volumetype_qos.return_value = qos
self.driver._get_volumetype_extraspecs = mock.MagicMock()
self.driver._get_volumetype_extraspecs.return_value = extraspecs
props = self.driver.initialize_connection_snapshot(
self.driver._attach_volume_to_host = mock.MagicMock(
return_value=None
)
self.driver._check_volume_mapped = mock.MagicMock(
return_value=None
)
self.driver.primary_client.set_sdc_limits = mock.MagicMock()
self.driver.initialize_connection_snapshot(
snapshot,
self.connector)
self.assertEqual(1000, int(props['data']['iopsLimit']))
self.assertEqual(2048, int(props['data']['bandwidthLimit']))
self.driver.primary_client.set_sdc_limits.assert_called_once_with(
self.snapshot_id, self.sdc["id"], '2048', '1000')

View File

@@ -1,167 +0,0 @@
# Copyright (c) 2021 Dell Inc. or its subsidiaries.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import http.client as http_client
import json
from unittest import mock
import ddt
import requests.exceptions
from requests.models import Response
from cinder.tests.unit.volume.drivers.dell_emc import powerflex
from cinder.volume import configuration as conf
from cinder.volume.drivers.dell_emc.powerflex import driver
from cinder.volume.drivers.dell_emc.powerflex import rest_client
@ddt.ddt
class TestPowerFlexClient(powerflex.TestPowerFlexDriver):
params = {'protectionDomainId': '1',
'storagePoolId': '1',
'name': 'HlF355XlSg+xcORfS0afag==',
'volumeType': 'ThinProvisioned',
'volumeSizeInKb': '1048576',
'compressionMethod': 'None'}
expected_status_code = 500
def setUp(self):
super(TestPowerFlexClient, self).setUp()
self.configuration = conf.Configuration(driver.powerflex_opts,
conf.SHARED_CONF_GROUP)
self._set_overrides()
self.client = rest_client.RestClient(self.configuration)
self.client.do_setup()
def _set_overrides(self):
# Override the defaults to fake values
self.override_config('san_ip', override='127.0.0.1',
group=conf.SHARED_CONF_GROUP)
self.override_config('powerflex_rest_server_port', override='8888',
group=conf.SHARED_CONF_GROUP)
self.override_config('san_login', override='test',
group=conf.SHARED_CONF_GROUP)
self.override_config('san_password', override='pass',
group=conf.SHARED_CONF_GROUP)
self.override_config('powerflex_storage_pools',
override='PD1:SP1',
group=conf.SHARED_CONF_GROUP)
self.override_config('max_over_subscription_ratio',
override=5.0, group=conf.SHARED_CONF_GROUP)
self.override_config('powerflex_server_api_version',
override='2.0.0', group=conf.SHARED_CONF_GROUP)
self.override_config('rest_api_connect_timeout',
override=120, group=conf.SHARED_CONF_GROUP)
self.override_config('rest_api_read_timeout',
override=120, group=conf.SHARED_CONF_GROUP)
@mock.patch("requests.get")
def test_rest_get_request_connect_timeout_exception(self, mock_request):
mock_request.side_effect = (requests.
exceptions.ConnectTimeout
('Fake Connect Timeout Exception'))
r, res = (self.client.
execute_powerflex_get_request(url="/version", **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /version failed with timeout exception '
'Fake Connect Timeout Exception', res['message']))
@mock.patch("requests.get")
def test_rest_get_request_read_timeout_exception(self, mock_request):
mock_request.side_effect = (requests.exceptions.ReadTimeout
('Fake Read Timeout Exception'))
r, res = (self.client.
execute_powerflex_get_request(url="/version", **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /version failed with timeout exception '
'Fake Read Timeout Exception', res['message']))
@mock.patch("requests.post")
def test_rest_post_request_connect_timeout_exception(self, mock_request):
mock_request.side_effect = (requests.exceptions.ConnectTimeout
('Fake Connect Timeout Exception'))
r, res = (self.client.execute_powerflex_post_request
(url="/types/Volume/instances", params=self.params, **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /types/Volume/instances failed with '
'timeout exception Fake Connect Timeout Exception', res['message']))
@mock.patch("requests.post")
def test_rest_post_request_read_timeout_exception(self, mock_request):
mock_request.side_effect = (requests.exceptions.ReadTimeout
('Fake Read Timeout Exception'))
r, res = (self.client.execute_powerflex_post_request
(url="/types/Volume/instances", params=self.params, **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /types/Volume/instances failed with '
'timeout exception Fake Read Timeout Exception', res['message']))
@mock.patch("requests.get")
def test_response_check_read_timeout_exception_1(self, mock_request):
r = requests.Response
r.status_code = http_client.UNAUTHORIZED
mock_request.side_effect = [r, (requests.exceptions.ReadTimeout
('Fake Read Timeout Exception'))]
r, res = (self.client.
execute_powerflex_get_request(url="/version", **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /version failed with '
'timeout exception Fake Read Timeout Exception', res['message']))
@mock.patch("requests.get")
def test_response_check_read_timeout_exception_2(self, mock_request):
res1 = requests.Response
res1.status_code = http_client.UNAUTHORIZED
res2 = Response()
res2.status_code = 200
res2._content = str.encode(json.dumps('faketoken'))
mock_request.side_effect = [res1, res2,
(requests.exceptions.ReadTimeout
('Fake Read Timeout Exception'))]
r, res = (self.client.
execute_powerflex_get_request(url="/version", **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /version failed with '
'timeout exception Fake Read Timeout Exception', res['message']))
@mock.patch("requests.post")
@mock.patch("requests.get")
def test_response_check_read_timeout_exception_3(self, mock_post_request,
mock_get_request):
r = requests.Response
r.status_code = http_client.UNAUTHORIZED
mock_post_request.side_effect = r
mock_get_request.side_effect = (requests.exceptions.ReadTimeout
('Fake Read Timeout Exception'))
r, res = (self.client.execute_powerflex_post_request
(url="/types/Volume/instances", params=self.params, **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /types/Volume/instances failed with '
'timeout exception Fake Read Timeout Exception', res['message']))

View File

@@ -0,0 +1,380 @@
# Copyright (c) 2024 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 http.client as http_client
import json
import pathlib
from unittest import mock
import requests.exceptions
from requests.models import Response
from cinder import exception
from cinder.tests.unit import test
from cinder.volume import configuration as conf
from cinder.volume.drivers.dell_emc.powerflex import driver
from cinder.volume.drivers.dell_emc.powerflex import rest_client
class TestPowerFlexClient(test.TestCase):
params = {'protectionDomainId': '1',
'storagePoolId': '1',
'name': 'HlF355XlSg+xcORfS0afag==',
'volumeType': 'ThinProvisioned',
'volumeSizeInKb': '1048576',
'compressionMethod': 'None'}
expected_status_code = 500
status_code_ok = mock.Mock(status_code=http_client.OK)
status_code_bad = mock.Mock(status_code=http_client.BAD_REQUEST)
response_error = {"errorCode": "123", "message": "Error message"}
def setUp(self):
super(TestPowerFlexClient, self).setUp()
self.configuration = conf.Configuration(driver.powerflex_opts,
conf.SHARED_CONF_GROUP)
self._set_overrides()
self.client = rest_client.RestClient(self.configuration)
self.client.do_setup()
self.mockup_file_base = (
str(pathlib.Path.cwd())
+ "/cinder/tests/unit/volume/drivers/dell_emc/powerflex/mockup/"
)
self.sdc_id = "01f7117d0000000b"
self.sdc_guid = "028888FA-502A-4FAC-A888-1FA3B256358C"
self.volume_id = "3bd1f78800000019"
def _set_overrides(self):
# Override the defaults to fake values
self.override_config('san_ip', override='127.0.0.1',
group=conf.SHARED_CONF_GROUP)
self.override_config('powerflex_rest_server_port', override='8888',
group=conf.SHARED_CONF_GROUP)
self.override_config('san_login', override='test',
group=conf.SHARED_CONF_GROUP)
self.override_config('san_password', override='pass',
group=conf.SHARED_CONF_GROUP)
self.override_config('powerflex_storage_pools',
override='PD1:SP1',
group=conf.SHARED_CONF_GROUP)
self.override_config('max_over_subscription_ratio',
override=5.0, group=conf.SHARED_CONF_GROUP)
self.override_config('powerflex_server_api_version',
override='2.0.0', group=conf.SHARED_CONF_GROUP)
self.override_config('rest_api_connect_timeout',
override=120, group=conf.SHARED_CONF_GROUP)
self.override_config('rest_api_read_timeout',
override=120, group=conf.SHARED_CONF_GROUP)
@mock.patch("requests.get")
def test_rest_get_request_connect_timeout_exception(self, mock_request):
mock_request.side_effect = (requests.
exceptions.ConnectTimeout
('Fake Connect Timeout Exception'))
r, res = (self.client.
execute_powerflex_get_request(url="/version", **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /version failed with timeout exception '
'Fake Connect Timeout Exception', res['message']))
@mock.patch("requests.get")
def test_rest_get_request_read_timeout_exception(self, mock_request):
mock_request.side_effect = (requests.exceptions.ReadTimeout
('Fake Read Timeout Exception'))
r, res = (self.client.
execute_powerflex_get_request(url="/version", **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /version failed with timeout exception '
'Fake Read Timeout Exception', res['message']))
@mock.patch("requests.post")
def test_rest_post_request_connect_timeout_exception(self, mock_request):
mock_request.side_effect = (requests.exceptions.ConnectTimeout
('Fake Connect Timeout Exception'))
r, res = (self.client.execute_powerflex_post_request
(url="/types/Volume/instances", params=self.params, **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /types/Volume/instances failed with '
'timeout exception Fake Connect Timeout Exception', res['message']))
@mock.patch("requests.post")
def test_rest_post_request_read_timeout_exception(self, mock_request):
mock_request.side_effect = (requests.exceptions.ReadTimeout
('Fake Read Timeout Exception'))
r, res = (self.client.execute_powerflex_post_request
(url="/types/Volume/instances", params=self.params, **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /types/Volume/instances failed with '
'timeout exception Fake Read Timeout Exception', res['message']))
@mock.patch("requests.get")
def test_response_check_read_timeout_exception_1(self, mock_request):
r = requests.Response
r.status_code = http_client.UNAUTHORIZED
mock_request.side_effect = [r, (requests.exceptions.ReadTimeout
('Fake Read Timeout Exception'))]
r, res = (self.client.
execute_powerflex_get_request(url="/version", **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /version failed with '
'timeout exception Fake Read Timeout Exception', res['message']))
@mock.patch("requests.get")
def test_response_check_read_timeout_exception_2(self, mock_request):
res1 = requests.Response
res1.status_code = http_client.UNAUTHORIZED
res2 = Response()
res2.status_code = 200
res2._content = str.encode(json.dumps('faketoken'))
mock_request.side_effect = [res1, res2,
(requests.exceptions.ReadTimeout
('Fake Read Timeout Exception'))]
r, res = (self.client.
execute_powerflex_get_request(url="/version", **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /version failed with '
'timeout exception Fake Read Timeout Exception', res['message']))
@mock.patch("requests.post")
@mock.patch("requests.get")
def test_response_check_read_timeout_exception_3(self, mock_post_request,
mock_get_request):
r = requests.Response
r.status_code = http_client.UNAUTHORIZED
mock_post_request.side_effect = r
mock_get_request.side_effect = (requests.exceptions.ReadTimeout
('Fake Read Timeout Exception'))
r, res = (self.client.execute_powerflex_post_request
(url="/types/Volume/instances", params=self.params, **{}))
self.assertEqual(self.expected_status_code, r.status_code)
self.assertEqual(self.expected_status_code, res['errorCode'])
(self.assertEqual
('The request to URL /types/Volume/instances failed with '
'timeout exception Fake Read Timeout Exception', res['message']))
def _getJsonFile(self, filename):
f = open(self.mockup_file_base + filename)
data = json.load(f)
f.close()
return data
def test_query_sdc_id_by_guid_valid(self):
response = self._getJsonFile("query_sdc_instances_response.json")
with mock.patch.object(self.client,
'execute_powerflex_get_request',
return_value=(self.status_code_ok,
response)):
result = self.client.query_sdc_id_by_guid(self.sdc_guid)
self.assertEqual(result, self.sdc_id)
self.client.execute_powerflex_get_request.assert_called_with(
'/types/Sdc/instances'
)
def test_query_sdc_id_by_guid_invalid(self):
response = self._getJsonFile("query_sdc_instances_response.json")
with mock.patch.object(self.client,
'execute_powerflex_get_request',
return_value=(self.status_code_ok,
response)):
ex = self.assertRaises(exception.VolumeBackendAPIException,
self.client.query_sdc_id_by_guid,
"invalid_guid")
self.assertIn(
"Failed to query SDC by guid invalid_guid: Not Found.",
ex.msg)
def test_query_sdc_id_by_guid_exception(self):
with mock.patch.object(self.client,
'execute_powerflex_get_request',
return_value=(self.status_code_bad,
self.response_error)):
ex = self.assertRaises(exception.VolumeBackendAPIException,
self.client.query_sdc_id_by_guid,
self.sdc_guid)
self.assertIn(
"Failed to query SDC: Error message.", ex.msg)
def test_query_sdc_by_id_success(self):
response = self._getJsonFile("query_sdc_by_id_response.json")
with mock.patch.object(self.client,
'execute_powerflex_get_request',
return_value=(self.status_code_ok,
response)):
result = self.client.query_sdc_by_id(self.sdc_id)
self.assertEqual(result, response)
self.client.execute_powerflex_get_request.assert_called_with(
'/instances/Sdc::%(sdc_id)s',
sdc_id=self.sdc_id
)
def test_query_sdc_by_id_failure(self):
host_id = "invalid_id"
with mock.patch.object(self.client,
'execute_powerflex_get_request',
return_value=(self.status_code_bad,
self.response_error)):
ex = self.assertRaises(exception.VolumeBackendAPIException,
self.client.query_sdc_by_id,
host_id)
self.assertIn(
f"Failed to query SDC id {host_id}: Error message.", ex.msg)
def test_map_volume_success(self):
with mock.patch.object(self.client,
'execute_powerflex_post_request',
return_value=(self.status_code_ok,
{})):
self.client.map_volume(self.volume_id, self.sdc_id)
self.client.execute_powerflex_post_request.assert_called_with(
f"/instances/Volume::{self.volume_id}/action/addMappedSdc",
{"sdcId": self.sdc_id,
"allowMultipleMappings": "True"},
)
def test_map_volume_failure(self):
with mock.patch.object(self.client,
'execute_powerflex_post_request',
return_value=(self.status_code_bad,
self.response_error)):
ex = self.assertRaises(exception.VolumeBackendAPIException,
self.client.map_volume,
self.volume_id, self.sdc_id)
self.assertIn(
("Failed to map volume %(vol_id)s to SDC %(sdc_id)s"
% {"vol_id": self.volume_id, "sdc_id": self.sdc_id}),
ex.msg)
def test_unmap_volume_success(self):
with mock.patch.object(self.client,
'execute_powerflex_post_request',
return_value=(self.status_code_ok,
{})):
self.client.unmap_volume(self.volume_id, self.sdc_id)
self.client.execute_powerflex_post_request.assert_called_with(
f"/instances/Volume::{self.volume_id}/action/removeMappedSdc",
{"sdcId": self.sdc_id}
)
def test_unmap_volume_host_none_success(self):
with mock.patch.object(self.client,
'_unmap_volume_from_all_sdcs',
return_value=None):
self.client.unmap_volume(self.volume_id)
self.client._unmap_volume_from_all_sdcs.assert_called_with(
self.volume_id,
)
def test_unmap_volume_failure(self):
with mock.patch.object(self.client,
'execute_powerflex_post_request',
return_value=(self.status_code_bad,
self.response_error)):
ex = self.assertRaises(exception.VolumeBackendAPIException,
self.client.unmap_volume,
self.volume_id, self.sdc_id)
self.assertIn(
("Failed to unmap volume %(vol_id)s from SDC %(host_id)s"
% {"vol_id": self.volume_id, "host_id": self.sdc_id}),
ex.msg)
def test_query_sdc_volumes_success(self):
response = self._getJsonFile("query_sdc_volumes_response.json")
with mock.patch.object(self.client,
'execute_powerflex_get_request',
return_value=(self.status_code_ok,
response)):
result = self.client.query_sdc_volumes(self.sdc_id)
self.assertEqual(result, ['694a2d140000000b', '694a2d1300000009'])
self.client.execute_powerflex_get_request.assert_called_with(
f'/instances/Sdc::{self.sdc_id}/relationships/Volume'
)
def test_query_sdc_volumes_failure(self):
host_id = "invalid_id"
with mock.patch.object(self.client,
'execute_powerflex_get_request',
return_value=(self.status_code_bad,
self.response_error)):
ex = self.assertRaises(exception.VolumeBackendAPIException,
self.client.query_sdc_volumes,
host_id)
self.assertIn(
"Failed to query SDC volumes: Error message.", ex.msg)
def test_set_sdc_limits_bandwith(self):
bandwidth_limit = 100
with mock.patch.object(self.client,
'execute_powerflex_post_request',
return_value=(self.status_code_ok,
{})):
self.client.set_sdc_limits(
self.volume_id, self.sdc_id, bandwidth_limit=bandwidth_limit)
url = ("/instances/Volume::%(vol_id)s/action/"
"setMappedSdcLimits" % {'vol_id': self.volume_id})
params = {'sdcId': self.sdc_id,
'bandwidthLimitInKbps': bandwidth_limit}
self.client.execute_powerflex_post_request.assert_called_once_with(
url, params)
def test_set_sdc_limits_iops(self):
iops_limit = 10000
with mock.patch.object(self.client,
'execute_powerflex_post_request',
return_value=(self.status_code_ok,
{})):
self.client.set_sdc_limits(
self.volume_id, self.sdc_id, iops_limit=iops_limit)
url = ("/instances/Volume::%(vol_id)s/action/"
"setMappedSdcLimits" % {'vol_id': self.volume_id})
params = {'sdcId': self.sdc_id,
'iopsLimit': iops_limit}
self.client.execute_powerflex_post_request.assert_called_once_with(
url, params)
def test_set_sdc_limits_failure(self):
with mock.patch.object(self.client,
'execute_powerflex_post_request',
return_value=(self.status_code_bad,
self.response_error)):
ex = self.assertRaises(exception.VolumeBackendAPIException,
self.client.set_sdc_limits,
self.volume_id, self.sdc_id)
self.assertIn(
"Failed to set SDC limits: Error message.",
ex.msg)

View File

@@ -0,0 +1,281 @@
# Copyright (c) 2025 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
import ddt
from cinder import context
from cinder import exception
from cinder.tests.unit import fake_volume
from cinder.tests.unit.volume.drivers.dell_emc import powerflex
class DictToObject:
def __init__(self, dictionary):
for key, value in dictionary.items():
setattr(self, key, value)
def get(self, key):
return self.__dict__.get(key)
@ddt.ddt
class TestSDC(powerflex.TestPowerFlexDriver):
def setUp(self):
"""Setup a test case environment."""
super(TestSDC, self).setUp()
self.client_mock = mock.MagicMock()
self.driver._get_client = mock.MagicMock(return_value=self.client_mock)
self.connector = {
"sdc_guid": "028888FA-502A-4FAC-A888-1FA3B256358C",
"host": "hostname"}
self.host_id = "13bf228a00010001"
self.host = {"name": "hostname"}
self.ctx = (
context.RequestContext('fake', 'fake', True, auth_token=True))
self.attachment1 = DictToObject(fake_volume.fake_db_volume_attachment(
**{
'attach_status': 'attached',
'attached_host': self.host['name']
}
))
self.attachment2 = DictToObject(fake_volume.fake_db_volume_attachment(
**{
'attach_status': 'attached',
'attached_host': self.host['name']
}
))
self.volume = fake_volume.fake_volume_obj(
self.ctx, **{'provider_id': '3bd1f78800000019',
'size': 8})
def test_initialize_connection(self):
self.driver._initialize_connection = mock.MagicMock()
self.driver.initialize_connection(self.volume, self.connector)
self.driver._initialize_connection.assert_called_once_with(
self.volume, self.connector, self.volume.size)
def test__initialize_connection(self):
self.client_mock.query_sdc_id_by_guid.return_value = self.host_id
self.driver._attach_volume_to_host = mock.MagicMock()
self.driver._check_volume_mapped = mock.MagicMock()
result = self.driver._initialize_connection(
self.volume, self.connector, self.volume.size)
self.assertEqual(result['driver_volume_type'], "scaleio")
self.driver._attach_volume_to_host.assert_called_with(
self.volume, self.host_id)
self.driver._check_volume_mapped.assert_called_with(
self.host_id, self.volume.provider_id)
def test__initialize_connection_no_connector(self):
self.assertRaises(exception.InvalidHost,
self.driver._initialize_connection,
self.volume,
{},
self.volume.size)
def test__attach_volume_to_host_success(self):
self.client_mock.query_sdc_by_id.return_value = self.host
self.client_mock.query_volume.return_value = {
"mappedSdcInfo": []
}
self.client_mock.map_volume.return_value = None
self.driver._attach_volume_to_host(self.volume, self.host_id)
self.client_mock.query_sdc_by_id.assert_called_once_with(
self.host_id)
self.client_mock.query_volume.assert_called_once_with(
self.volume.provider_id)
self.client_mock.map_volume.assert_called_once_with(
self.volume.provider_id, self.host_id)
def test__attach_volume_to_host_already_attached(self):
self.client_mock.query_sdc_by_id.return_value = self.host
self.client_mock.query_volume.return_value = {
"mappedSdcInfo": [
{
"sdcId": self.host_id
}
]
}
self.driver._attach_volume_to_host(self.volume, self.host_id)
self.client_mock.query_sdc_by_id.assert_called_once_with(
self.host_id)
self.client_mock.map_volume.assert_not_called()
def test__check_volume_mapped_success(self):
self.client_mock.query_sdc_volumes.return_value = [
'vol1', 'vol2', self.volume.id]
self.driver._check_volume_mapped(self.host_id, self.volume.id)
self.client_mock.query_sdc_volumes.assert_called_once_with(
self.host_id)
def test__check_volume_mapped_fail(self):
self.client_mock.query_sdc_volumes.return_value = []
self.assertRaises(exception.VolumeBackendAPIException,
self.driver._check_volume_mapped,
self.host_id, self.volume.id)
def test__check_volume_mapped_with_retry(self):
self.client_mock.query_sdc_volumes.side_effect = [
[],
[self.volume.id]
]
self.driver._check_volume_mapped('sdc_id', self.volume.id)
self.assertEqual(self.client_mock.query_sdc_volumes.call_count, 2)
def test_terminate_connection(self):
self.driver._terminate_connection = mock.MagicMock()
self.driver.terminate_connection(self.volume, self.connector)
self.driver._terminate_connection.assert_called_once_with(
self.volume, self.connector)
def test__terminate_connection_success(self):
self.client_mock.query_sdc_id_by_guid.return_value = self.host_id
self.driver._detach_volume_from_host = mock.MagicMock(
return_valure=None)
self.driver._terminate_connection(self.volume, self.connector)
self.client_mock.query_sdc_id_by_guid.assert_called_once_with(
self.connector["sdc_guid"])
self.driver._detach_volume_from_host.assert_called_once_with(
self.volume, self.host_id)
def test__terminate_connection_no_connector(self):
self.assertRaises(exception.InvalidHost,
self.driver._terminate_connection,
self.volume,
{})
def test__terminate_connection_multiattached(self):
self.driver._is_multiattached_to_host = mock.MagicMock(
return_valure=False)
self.driver._terminate_connection(self.volume, self.connector)
self.client_mock.query_sdc_id_by_guid.assert_not_called()
def test__is_multiattached_to_host_false(self):
result = self.driver._is_multiattached_to_host(
[self.attachment1], self.host['name'])
self.assertFalse(result)
def test__is_multiattached_to_host_true(self):
result = self.driver._is_multiattached_to_host(
[self.attachment1, self.attachment2],
self.host['name'])
self.assertTrue(result)
@ddt.data("13bf228a00010001", None)
def test__detach_volume_from_host_detached_1(self, host_id):
self.client_mock.query_volume.return_value = {
"mappedSdcInfo": []
}
self.driver._detach_volume_from_host(self.volume, host_id)
self.client_mock.unmap_volume.assert_not_called()
def test__detach_volume_from_host_detached_2(self):
self.client_mock.query_volume.return_value = {
"mappedSdcInfo": [
{
"sdcId": "fake_id"
}
]
}
self.client_mock.query_sdc_by_id.return_value = self.host
self.driver._detach_volume_from_host(self.volume, self.host_id)
self.client_mock.query_sdc_by_id.assert_called_once_with(self.host_id)
self.client_mock.unmap_volume.assert_not_called()
def test__detach_volume_from_host_with_hostid(self):
self.client_mock.query_volume.return_value = {
"mappedSdcInfo": [
{
"sdcId": self.host_id
}
]
}
self.client_mock.query_sdc_by_id.return_value = self.host
self.driver._detach_volume_from_host(self.volume, self.host_id)
self.client_mock.query_sdc_by_id.assert_called_once_with(self.host_id)
self.client_mock.unmap_volume.assert_called_once_with(
self.volume.provider_id, self.host_id)
def test__detach_volume_from_host_without_hostid(self):
self.client_mock.query_volume.return_value = {
"mappedSdcInfo": [
{
"sdcId": self.host_id
}
]
}
self.client_mock.query_sdc_by_id.return_value = self.host
self.driver._detach_volume_from_host(self.volume)
self.client_mock.query_sdc_by_id.assert_not_called()
self.client_mock.unmap_volume.assert_called_once_with(
self.volume.provider_id)
def test__check_volume_unmapped_success(self):
self.client_mock.query_sdc_volumes.return_value = []
self.driver._check_volume_unmapped(self.host_id, self.volume.id)
self.client_mock.query_sdc_volumes.assert_called_once_with(
self.host_id)
def test__check_volume_unmapped_fail(self):
self.client_mock.query_sdc_volumes.return_value = [
'vol1', 'vol2', self.volume.id]
self.assertRaises(exception.VolumeBackendAPIException,
self.driver._check_volume_unmapped,
self.host_id, self.volume.id)
def test__check_volume_unmapped_with_retry(self):
self.client_mock.query_sdc_volumes.side_effect = [
[self.volume.id],
[]
]
self.driver._check_volume_unmapped('sdc_id', self.volume.id)
self.assertEqual(self.client_mock.query_sdc_volumes.call_count, 2)

View File

@@ -20,7 +20,6 @@ import http.client as http_client
import math
from operator import xor
from os_brick import initiator
from oslo_config import cfg
from oslo_log import log as logging
from oslo_log import versionutils
@@ -32,10 +31,10 @@ from cinder.common import constants
from cinder import context
from cinder import exception
from cinder.i18n import _
from cinder.image import image_utils
from cinder import interface
from cinder import objects
from cinder.objects import fields
from cinder.objects.volume import Volume
from cinder import utils
from cinder.volume import configuration
from cinder.volume import driver
@@ -65,10 +64,6 @@ QOS_IOPS_PER_GB = "maxIOPSperGB"
QOS_BANDWIDTH_PER_GB = "maxBWSperGB"
BLOCK_SIZE = 8
VOLUME_NOT_FOUND_ERROR = 79
# This code belongs to older versions of PowerFlex
VOLUME_NOT_MAPPED_ERROR = 84
VOLUME_ALREADY_MAPPED_ERROR = 81
MIN_BWS_SCALING_SIZE = 128
POWERFLEX_MAX_OVERSUBSCRIPTION_RATIO = 10.0
@@ -96,9 +91,10 @@ class PowerFlexDriver(driver.VolumeDriver):
conversion of its type.
3.5.7 - Report trim/discard support.
3.5.8 - Added Cinder active/active support.
3.6.0 - Improved secret handling.
"""
VERSION = "3.5.8"
VERSION = "3.6.0"
SUPPORTS_ACTIVE_ACTIVE = True
# ThirdPartySystems wiki
CI_WIKI_NAME = "DellEMC_PowerFlex_CI"
@@ -117,7 +113,6 @@ class PowerFlexDriver(driver.VolumeDriver):
self.statisticProperties = None
self.storage_pools = None
self.provisioning_type = None
self.connector = None
self.replication_enabled = None
self.replication_device = None
self.failover_choices = None
@@ -195,11 +190,6 @@ class PowerFlexDriver(driver.VolumeDriver):
self.configuration.max_over_subscription_ratio = (
self.configuration.powerflex_max_over_subscription_ratio
)
self.connector = initiator.connector.InitiatorConnector.factory(
initiator.SCALEIO,
utils.get_root_helper(),
self.configuration.num_volume_device_scan_tries
)
self.primary_client = rest_client.RestClient(self.configuration)
self.secondary_client = rest_client.RestClient(self.configuration,
is_primary=False)
@@ -891,23 +881,24 @@ class PowerFlexDriver(driver.VolumeDriver):
"""
try:
ip = connector["ip"]
sdc_guid = connector["sdc_guid"]
except Exception:
ip = "unknown"
LOG.info("Initialize connection for %(vol_id)s to SDC at %(sdc)s.",
{"vol_id": vol_or_snap.id, "sdc": ip})
connection_properties = self._get_client().connection_properties
msg = "SDC guid is not configured."
raise exception.InvalidHost(reason=msg)
LOG.info("Initialize connection for %(vol_id)s to SDC %(sdc)s.",
{"vol_id": vol_or_snap.id, "sdc": sdc_guid})
connection_properties = {}
volume_name = flex_utils.id_to_base64(vol_or_snap.id)
connection_properties["scaleIO_volname"] = volume_name
connection_properties["scaleIO_volume_id"] = vol_or_snap.provider_id
connection_properties["config_group"] = self.configuration.config_group
connection_properties["failed_over"] = self._is_failed_over
connection_properties["verify_certificate"] = (
self._get_client().verify_certificate
)
connection_properties["certificate_path"] = (
self._get_client().certificate_path
)
# map volume
sdc_id = self._get_client().query_sdc_id_by_guid(sdc_guid)
self._attach_volume_to_host(vol_or_snap, sdc_id)
# verify volume is mapped
self._check_volume_mapped(sdc_id, vol_or_snap.provider_id)
if vol_size is not None:
extra_specs = self._get_volumetype_extraspecs(vol_or_snap)
@@ -920,14 +911,76 @@ class PowerFlexDriver(driver.VolumeDriver):
storage_type)
LOG.info("IOPS limit: %s.", iops_limit)
LOG.info("Bandwidth limit: %s.", bandwidth_limit)
connection_properties["iopsLimit"] = iops_limit
connection_properties["bandwidthLimit"] = bandwidth_limit
# Set QoS settings after map was performed
if iops_limit is not None or bandwidth_limit is not None:
self._get_client().set_sdc_limits(vol_or_snap.provider_id, sdc_id,
bandwidth_limit, iops_limit)
return {
"driver_volume_type": "scaleio",
"data": connection_properties,
}
def _attach_volume_to_host(self, volume, sdc_id):
"""Attach PowerFlex volume to host.
:param volume: OpenStack volume object
:param sdc_id: PowerFlex SDC id
"""
host = self._get_client().query_sdc_by_id(sdc_id)
provider_id = volume.provider_id
# check if volume is already attached to the host
vol = self._get_client().query_volume(provider_id)
if vol["mappedSdcInfo"]:
ids = [sdc["sdcId"] for sdc in vol["mappedSdcInfo"]]
if sdc_id in ids:
LOG.debug("PowerFlex volume %(volume_name)s "
"with id %(volume_id)s is already attached to "
"host %(host_name)s. "
"PowerFlex 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": provider_id,
"host_provider_id": sdc_id,
})
return
LOG.debug("Attach PowerFlex volume %(volume_name)s with id "
"%(volume_id)s to host %(host_name)s. PowerFlex 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": provider_id,
"host_provider_id": sdc_id,
})
self._get_client().map_volume(provider_id, sdc_id)
LOG.debug("Successfully attached PowerFlex volume %(volume_name)s "
"with id %(volume_id)s to host %(host_name)s. "
"PowerFlex 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": provider_id,
"host_provider_id": sdc_id,
})
@utils.retry(exception.VolumeBackendAPIException, retries=3)
def _check_volume_mapped(self, sdc_id, volume_id):
mappedVols = self._get_client().query_sdc_volumes(sdc_id)
if volume_id not in mappedVols:
msg = f'Volume {volume_id} is not mapped to SDC {sdc_id}.'
raise exception.VolumeBackendAPIException(msg)
LOG.info("Volume %s is mapped to SDC %s.", volume_id, sdc_id)
@staticmethod
def _get_bandwidth_limit(size, storage_type):
try:
@@ -982,19 +1035,161 @@ class PowerFlexDriver(driver.VolumeDriver):
def terminate_connection(self, volume, connector, **kwargs):
self._terminate_connection(volume, connector)
@staticmethod
def _terminate_connection(volume_or_snap, connector):
def _terminate_connection(self, volume_or_snap, connector):
"""Terminate connection to volume or snapshot.
With PowerFlex, snaps and volumes are terminated identically.
"""
if connector is None:
self._detach_volume_from_host(volume_or_snap)
return
try:
ip = connector["ip"]
sdc_guid = connector["sdc_guid"]
except Exception:
ip = "unknown"
LOG.info("Terminate connection for %(vol_id)s to SDC at %(sdc)s.",
{"vol_id": volume_or_snap.id, "sdc": ip})
msg = "Host IP is not configured."
raise exception.InvalidHost(reason=msg)
LOG.info("Terminate connection for %(vol_id)s to SDC %(sdc)s.",
{"vol_id": volume_or_snap.id, "sdc": sdc_guid})
if isinstance(volume_or_snap, Volume):
is_multiattached = self._is_multiattached_to_host(
volume_or_snap.volume_attachment,
connector["host"]
)
if is_multiattached:
# Do not detach volume if it is attached to more than one
# instance on the same host.
LOG.info("Will not terminate connection for "
"%(vol_id)s to initiator at %(sdc)s "
"because it's multiattach.",
{"vol_id": volume_or_snap.id, "sdc": sdc_guid})
return
# unmap volume
host_id = self._get_client().query_sdc_id_by_guid(sdc_guid)
self._detach_volume_from_host(volume_or_snap, host_id)
self._check_volume_unmapped(host_id, volume_or_snap.provider_id)
LOG.info("Terminated connection for %(vol_id)s to SDC %(sdc)s.",
{"vol_id": volume_or_snap.id, "sdc": sdc_guid})
@staticmethod
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.
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
def _detach_volume_from_host(self, volume, sdc_id=None):
"""Detach PowerFlex volume from nvme host.
:param volume: OpenStack volume object
:param sdc_id: PowerFlex SDC id
"""
provider_id = volume.provider_id
vol = self._get_client().query_volume(provider_id)
# check if volume is already detached
if not vol["mappedSdcInfo"]:
LOG.debug("PowerFlex volume %(volume_name)s "
"with id %(volume_id)s is already detached from "
"all hosts. "
"PowerFlex volume id: %(volume_provider_id)s. ",
{
"volume_name": volume.name,
"volume_id": volume.id,
"volume_provider_id": provider_id,
})
return
if sdc_id:
host = self._get_client().query_sdc_by_id(sdc_id)
# check if volume is already detached from the host
ids = [sdc["sdcId"] for sdc in vol["mappedSdcInfo"]]
if sdc_id not in ids:
LOG.debug("PowerFlex volume %(volume_name)s "
"with id %(volume_id)s is already detached from "
"host %(host_name)s. "
"PowerFlex 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": provider_id,
"host_provider_id": sdc_id,
})
return
LOG.debug("Detach PowerFlex volume %(volume_name)s with id "
"%(volume_id)s from host %(host_name)s. "
"PowerFlex 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": provider_id,
"host_provider_id": sdc_id,
})
self._get_client().unmap_volume(provider_id, sdc_id)
LOG.debug("Successfully detached PowerFlex volume %(volume_name)s "
"with id %(volume_id)s from host %(host_name)s. "
"PowerFlex 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": provider_id,
"host_provider_id": sdc_id,
})
else:
LOG.debug("Detach PowerFlex volume %(volume_name)s with id "
"%(volume_id)s from all mapped hosts. "
"PowerFlex volume id: %(volume_provider_id)s.",
{
"volume_name": volume.name,
"volume_id": volume.id,
"volume_provider_id": provider_id,
})
self._get_client().unmap_volume(provider_id)
LOG.debug("Successfully detached PowerFlex volume %(volume_name)s "
"with id %(volume_id)s from all mapped hosts. "
"PowerFlex volume id: %(volume_provider_id)s.",
{
"volume_name": volume.name,
"volume_id": volume.id,
"volume_provider_id": provider_id,
})
@utils.retry(exception.VolumeBackendAPIException, retries=3)
def _check_volume_unmapped(self, sdc_id, volume_id):
mappedVols = self._get_client().query_sdc_volumes(sdc_id)
if volume_id in mappedVols:
msg = f'Volume {volume_id} is still mapped to SDC {sdc_id}.'
raise exception.VolumeBackendAPIException(msg)
LOG.info("Volume %s is unmapped from SDC %s.", volume_id, sdc_id)
def _update_volume_stats(self):
"""Update storage backend driver statistics."""
@@ -1236,87 +1431,6 @@ class PowerFlexDriver(driver.VolumeDriver):
qos[key] = value
return qos
def _sio_attach_volume(self, volume):
"""Call connector.connect_volume() and return the path."""
LOG.info("Call os-brick to attach PowerFlex volume.")
connection_properties = self._get_client().connection_properties
connection_properties["scaleIO_volname"] = flex_utils.id_to_base64(
volume.id
)
connection_properties["scaleIO_volume_id"] = volume.provider_id
connection_properties["config_group"] = self.configuration.config_group
connection_properties["failed_over"] = self._is_failed_over
connection_properties["verify_certificate"] = (
self._get_client().verify_certificate
)
connection_properties["certificate_path"] = (
self._get_client().certificate_path
)
device_info = self.connector.connect_volume(connection_properties)
return device_info["path"]
def _sio_detach_volume(self, volume):
"""Call the connector.disconnect()."""
LOG.info("Call os-brick to detach PowerFlex volume.")
connection_properties = self._get_client().connection_properties
connection_properties["scaleIO_volname"] = flex_utils.id_to_base64(
volume.id
)
connection_properties["scaleIO_volume_id"] = volume.provider_id
connection_properties["config_group"] = self.configuration.config_group
connection_properties["failed_over"] = self._is_failed_over
connection_properties["verify_certificate"] = (
self._get_client().verify_certificate
)
connection_properties["certificate_path"] = (
self._get_client().certificate_path
)
self.connector.disconnect_volume(connection_properties, volume)
def copy_image_to_volume(self, context, volume, image_service, image_id,
disable_sparse=False):
"""Fetch image from image service and write it to volume."""
LOG.info("Copy image %(image_id)s from image service %(service)s "
"to volume %(vol_id)s.",
{
"image_id": image_id,
"service": image_service,
"vol_id": volume.id,
})
try:
image_utils.fetch_to_raw(context,
image_service,
image_id,
self._sio_attach_volume(volume),
BLOCK_SIZE,
size=volume.size,
disable_sparse=disable_sparse)
finally:
self._sio_detach_volume(volume)
def copy_volume_to_image(self, context, volume, image_service, image_meta):
"""Copy volume to image on image service."""
LOG.info("Copy volume %(vol_id)s to image on "
"image service %(service)s. Image meta: %(meta)s.",
{
"vol_id": volume.id,
"service": image_service,
"meta": image_meta,
})
try:
volume_utils.upload_volume(context,
image_service,
image_meta,
self._sio_attach_volume(volume),
volume)
finally:
self._sio_detach_volume(volume)
def migrate_volume(self, ctxt, volume, host):
"""Migrate PowerFlex volume within the same backend."""

View File

@@ -68,18 +68,6 @@ class RestClient(object):
def _get_headers():
return {"content-type": "application/json"}
@property
def connection_properties(self):
return {
"scaleIO_volname": None,
"hostIP": None,
"serverIP": self.rest_ip,
"serverPort": self.rest_port,
"serverUsername": self.rest_username,
"iopsLimit": None,
"bandwidthLimit": None,
}
def do_setup(self):
if self.is_primary:
get_config_value = self.configuration.safe_get
@@ -590,7 +578,7 @@ class RestClient(object):
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def _unmap_volume_before_delete(self, vol_id):
def _unmap_volume_from_all_sdcs(self, vol_id):
url = "/instances/Volume::%(vol_id)s/action/removeMappedSdc"
volume_is_mapped = False
@@ -603,16 +591,22 @@ class RestClient(object):
vol_id)
if volume_is_mapped:
params = {"allSdcs": ""}
LOG.info("Unmap volume from all sdcs before deletion.")
r, unused = self.execute_powerflex_post_request(url,
params,
vol_id=vol_id)
LOG.info("Unmap volume from all sdcs.")
r, response = self.execute_powerflex_post_request(url,
params,
vol_id=vol_id)
if r.status_code != http_client.OK:
msg = (_("Failed to unmap volume %(vol_id)s from all SDCs: "
"%(err)s.") % {"vol_id": vol_id,
"err": response["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
@retry(exception.VolumeBackendAPIException)
def remove_volume(self, vol_id):
url = "/instances/Volume::%(vol_id)s/action/removeVolume"
self._unmap_volume_before_delete(vol_id)
self._unmap_volume_from_all_sdcs(vol_id)
params = {"removeMode": "ONLY_ME"}
r, response = self.execute_powerflex_post_request(url,
params,
@@ -748,3 +742,95 @@ class RestClient(object):
"err": response["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(msg)
def query_sdc_id_by_guid(self, sdc_guid):
url = "/types/Sdc/instances"
r, response = self.execute_powerflex_get_request(url)
if r.status_code != http_client.OK:
msg = (_("Failed to query SDC: %(err)s.") %
{"err": response["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
for sdc in response:
if (sdc["sdcGuid"] and
sdc["sdcGuid"].lower() == sdc_guid.lower()):
return sdc["id"]
msg = (_("Failed to query SDC by guid %(sdc)s: Not Found.") %
{"sdc": sdc_guid})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def query_sdc_by_id(self, sdc_id):
url = "/instances/Sdc::%(sdc_id)s"
r, response = self.execute_powerflex_get_request(
url,
sdc_id = sdc_id)
if r.status_code != http_client.OK:
msg = (_("Failed to query SDC id %(sdc_id)s: %(err)s.") % {
"sdc_id": sdc_id, "err": response["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return response
def map_volume(self, volume_id, sdc_id):
params = {'sdcId': sdc_id, 'allowMultipleMappings': 'True'}
url = "/instances/Volume::%(vol_id)s/action/addMappedSdc" % {
'vol_id': volume_id
}
r, response = self.execute_powerflex_post_request(url, params)
if r.status_code != http_client.OK:
msg = (_("Failed to map volume %(vol_id)s to SDC "
"%(host_id)s: %(err)s.") % {"vol_id": volume_id,
"host_id": sdc_id,
"err": response["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def unmap_volume(self, volume_id, sdc_id=None):
if sdc_id:
params = {'sdcId': sdc_id}
url = "/instances/Volume::%(vol_id)s/action/removeMappedSdc" % {
'vol_id': volume_id
}
r, response = self.execute_powerflex_post_request(url, params)
if r.status_code != http_client.OK:
msg = (_("Failed to unmap volume %(vol_id)s from SDC "
"%(sdc_id)s: %(err)s.") % {
"vol_id": volume_id,
"sdc_id": sdc_id,
"err": response["message"]})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
else:
self._unmap_volume_from_all_sdcs(volume_id)
def query_sdc_volumes(self, sdc_id):
url = ("/instances/Sdc::%(sdc_id)s/relationships/Volume" %
{'sdc_id': sdc_id})
r, response = self.execute_powerflex_get_request(url)
if r.status_code != http_client.OK:
msg = (_("Failed to query SDC volumes: %s.") % response["message"])
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return [volume["id"] for volume in response]
def set_sdc_limits(self, volume_id, sdc_id,
bandwidth_limit=None, iops_limit=None):
params = {'sdcId': sdc_id}
if bandwidth_limit is not None:
params['bandwidthLimitInKbps'] = bandwidth_limit
if iops_limit is not None:
params['iopsLimit'] = iops_limit
url = (
"/instances/Volume::%(volume_id)s/action/setMappedSdcLimits" %
{'volume_id': volume_id}
)
r, response = self.execute_powerflex_post_request(url, params)
if r.status_code != http_client.OK:
msg = (_("Failed to set SDC limits: %s.") % response["message"])
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)

View File

@@ -226,6 +226,10 @@ parameters as follows:
Connector configuration
~~~~~~~~~~~~~~~~~~~~~~~
.. note::
Since 2025.2 release, users do not need to create connector configuration.
Before using attach/detach volume operations PowerFlex connector must be
properly configured. On each node where PowerFlex SDC is installed do the
following:

View File

@@ -0,0 +1,34 @@
---
security:
- |
Dell PowerFlex driver: This release contains a fix for
`Bug #2114879 <https://bugs.launchpad.net/cinder/+bug/2114879>`_.
It removes the limitation of use with bare metal hosts mentioned in
`OSSN-0086 <https://wiki.openstack.org/wiki/OSSN/OSSN-0086>`_.
upgrade:
- |
Dell PowerFlex driver: The fix for `Bug #2114879
<https://bugs.launchpad.net/cinder/+bug/2114879>`_ requires
``os-brick`` version 6.13.0 or greater. Users do not need to
create the `/opt/emc/scaleio/openstack/connector.conf` file
on the hosts using ``os-brick``.
Follow the steps below to upgrade:
1. Upgrade ``os-brick`` to version 6.13.0 without removing
the configuration file. This version can perform mapping
if the driver has not yet done so, provided the configuration
file remains intact.
2. Then upgrade the PowerFlex driver to version 3.6.0 or later.
Note that driver version 3.6.0 requires ``os-brick`` version 6.13.0
or higher to function correctly and will not operate with
earlier versions of ``os-brick``.
3. The connector configuration file can now be safely removed.
fixes:
- |
Dell PowerFlex driver `Bug #2114879
<https://bugs.launchpad.net/cinder/+bug/2114879>`_:
This release contains an updated Dell PowerFlex driver. It must
be used with ``os-brick`` version 6.13.0 or greater. ``os-brick``
no longer requires access to PowerFlex backend secrets, and
all that is handled by the cinder driver now.