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:
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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']))
|
||||
@@ -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)
|
||||
281
cinder/tests/unit/volume/drivers/dell_emc/powerflex/test_sdc.py
Normal file
281
cinder/tests/unit/volume/drivers/dell_emc/powerflex/test_sdc.py
Normal 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)
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user