Merge "Dell PowerFlex: Fix image cache vTree size counting"
This commit is contained in:
commit
803dca69e6
8 files changed
+435
-97
No files matched your search
+104
@@ -0,0 +1,104 @@
|
||||
[
|
||||
{
|
||||
"managedBy": "ScaleIO",
|
||||
"originalExpiryTime": 0,
|
||||
"retentionLevels": [],
|
||||
"snplIdOfSourceVolume": null,
|
||||
"volumeReplicationState": "UnmarkedForReplication",
|
||||
"mappedSdcInfo": [],
|
||||
"replicationJournalVolume": false,
|
||||
"replicationTimeStamp": 0,
|
||||
"name": "test_volume_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": [],
|
||||
"replicationJournalVolume": false,
|
||||
"replicationTimeStamp": 0,
|
||||
"name": "test_volume_2",
|
||||
"creationTime": 1746886943,
|
||||
"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": 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::2b713ee600000007"
|
||||
},
|
||||
{
|
||||
"rel": "/api/parent/relationship/storagePoolId",
|
||||
"href": "/api/instances/StoragePool::fa85edfd00000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -145,83 +145,102 @@ class TestCreateClonedVolume(powerflex.TestPowerFlexDriver):
|
||||
# Should proceed directly to _create_volume_from_source
|
||||
mock_create.assert_called_once_with(self.new_volume, self.src_volume)
|
||||
|
||||
@mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.'
|
||||
'PowerFlexDriver._create_volume_from_source')
|
||||
@mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.'
|
||||
'PowerFlexDriver._get_client')
|
||||
def test_create_cloned_volume_image_cache_within_limit(self, mock_client,
|
||||
mock_create):
|
||||
"""Test cloning image cache volume when within clone limit."""
|
||||
def _test_create_cloned_volume_image_cache(self, vtree_volumes,
|
||||
expect_raises,
|
||||
max_size=20,
|
||||
vtree_id='test_vtree_id'):
|
||||
"""Helper for image cache vTree clone limit tests.
|
||||
|
||||
Sets up mocks for query_volume and query_vtree_volumes, then
|
||||
calls create_cloned_volume and verifies the expected outcome.
|
||||
|
||||
:param vtree_volumes: list of volume dicts for query_vtree_volumes
|
||||
:param expect_raises: True if SnapshotLimitReached should be raised
|
||||
:param max_size: powerflex_max_image_cache_vtree_size config value
|
||||
:param vtree_id: vtree ID returned by query_volume
|
||||
"""
|
||||
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
||||
mock_create.return_value = {}
|
||||
self.override_config(options.POWERFLEX_MAX_IMAGE_CACHE_VTREE_SIZE,
|
||||
max_size, configuration.SHARED_CONF_GROUP)
|
||||
|
||||
# Set clone limit to 20
|
||||
self.override_config(options.POWERFLEX_MAX_IMAGE_CACHE_VTREE_SIZE, 20,
|
||||
configuration.SHARED_CONF_GROUP)
|
||||
with mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.'
|
||||
'PowerFlexDriver._get_client') as mock_client, \
|
||||
mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.'
|
||||
'PowerFlexDriver._create_volume_from_source'
|
||||
) as mock_create:
|
||||
mock_create.return_value = {}
|
||||
mock_rest_client = mock.Mock()
|
||||
mock_client.return_value = mock_rest_client
|
||||
mock_rest_client.query_volume.return_value = {
|
||||
'vtreeId': vtree_id
|
||||
}
|
||||
mock_rest_client.query_vtree_volumes.return_value = vtree_volumes
|
||||
|
||||
# Mock REST client and vtree statistics response
|
||||
mock_rest_client = mock.Mock()
|
||||
mock_client.return_value = mock_rest_client
|
||||
mock_rest_client.query_volume.return_value = {
|
||||
'vtreeId': 'test_vtree_id'
|
||||
}
|
||||
mock_rest_client.query_vtree_statistics.return_value = {
|
||||
'numOfVolumes': '10' # Within limit
|
||||
}
|
||||
with mock.patch(
|
||||
'cinder.volume.volume_utils.is_image_cache_entry',
|
||||
return_value=True):
|
||||
if expect_raises:
|
||||
self.assertRaises(
|
||||
exception.SnapshotLimitReached,
|
||||
self.driver.create_cloned_volume,
|
||||
self.new_volume, self.src_volume)
|
||||
else:
|
||||
self.driver.create_cloned_volume(self.new_volume,
|
||||
self.src_volume)
|
||||
|
||||
# Mock volume_utils to indicate source volume is an image cache entry
|
||||
with mock.patch('cinder.volume.volume_utils.is_image_cache_entry',
|
||||
return_value=True):
|
||||
self.driver.create_cloned_volume(self.new_volume, self.src_volume)
|
||||
mock_rest_client.query_volume.assert_called_once_with(
|
||||
self.src_volume.provider_id)
|
||||
mock_rest_client.query_vtree_volumes.assert_called_once_with(
|
||||
vtree_id)
|
||||
if expect_raises:
|
||||
mock_create.assert_not_called()
|
||||
else:
|
||||
mock_create.assert_called_once_with(self.new_volume,
|
||||
self.src_volume)
|
||||
|
||||
# Should query volume and vtree statistics and proceed to create
|
||||
mock_rest_client.query_volume.assert_called_once_with(
|
||||
self.src_volume.provider_id)
|
||||
mock_rest_client.query_vtree_statistics.assert_called_once_with(
|
||||
'test_vtree_id')
|
||||
mock_create.assert_called_once_with(self.new_volume, self.src_volume)
|
||||
def test_create_cloned_volume_image_cache_within_limit(self):
|
||||
"""Test cloning image cache volume when within clone limit.
|
||||
|
||||
@mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.'
|
||||
'PowerFlexDriver._get_client')
|
||||
def test_create_cloned_volume_image_cache_limit_reached(self, mock_client):
|
||||
"""Test cloning image cache volume when clone limit is reached."""
|
||||
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
||||
10 direct children with max_size=20: 10 < 20, no raise.
|
||||
"""
|
||||
src_provider_id = self.src_volume.provider_id
|
||||
vtree_volumes = [
|
||||
{'id': 'root_vol', 'ancestorVolumeId': None},
|
||||
] + [
|
||||
{'id': 'child_%d' % i, 'ancestorVolumeId': src_provider_id}
|
||||
for i in range(10)
|
||||
]
|
||||
self._test_create_cloned_volume_image_cache(vtree_volumes,
|
||||
expect_raises=False,
|
||||
max_size=20)
|
||||
|
||||
# Set clone limit to 20
|
||||
self.override_config(options.POWERFLEX_MAX_IMAGE_CACHE_VTREE_SIZE, 20,
|
||||
configuration.SHARED_CONF_GROUP)
|
||||
def test_create_cloned_volume_image_cache_limit_reached(self):
|
||||
"""Test cloning image cache volume when clone limit is reached.
|
||||
|
||||
# Mock REST client and vtree statistics response
|
||||
mock_rest_client = mock.Mock()
|
||||
mock_client.return_value = mock_rest_client
|
||||
mock_rest_client.query_volume.return_value = {
|
||||
'vtreeId': 'test_vtree_id'
|
||||
}
|
||||
mock_rest_client.query_vtree_statistics.return_value = {
|
||||
'numOfVolumes': '20' # At limit
|
||||
}
|
||||
|
||||
# Mock volume_utils to indicate source volume is an image cache entry
|
||||
with mock.patch('cinder.volume.volume_utils.is_image_cache_entry',
|
||||
return_value=True):
|
||||
self.assertRaises(exception.SnapshotLimitReached,
|
||||
self.driver.create_cloned_volume,
|
||||
self.new_volume, self.src_volume)
|
||||
|
||||
# Should query volume and vtree statistics but fail before create
|
||||
mock_rest_client.query_volume.assert_called_once_with(
|
||||
self.src_volume.provider_id)
|
||||
mock_rest_client.query_vtree_statistics.assert_called_once_with(
|
||||
'test_vtree_id')
|
||||
20 direct children with max_size=20: 20 >= 20, raises.
|
||||
"""
|
||||
src_provider_id = self.src_volume.provider_id
|
||||
vtree_volumes = [
|
||||
{'id': 'root_vol', 'ancestorVolumeId': None},
|
||||
] + [
|
||||
{'id': 'child_%d' % i, 'ancestorVolumeId': src_provider_id}
|
||||
for i in range(20)
|
||||
]
|
||||
self._test_create_cloned_volume_image_cache(vtree_volumes,
|
||||
expect_raises=True,
|
||||
max_size=20)
|
||||
|
||||
@mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.'
|
||||
'PowerFlexDriver._create_volume_from_source')
|
||||
@mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.'
|
||||
'PowerFlexDriver._get_client')
|
||||
@mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.LOG')
|
||||
def test_create_cloned_volume_image_cache_stats_query_fails(
|
||||
def test_create_cloned_volume_image_cache_query_fails(
|
||||
self, mock_log, mock_client, mock_create):
|
||||
"""Test cloning when volume statistics query fails."""
|
||||
"""Test cloning when vtree volumes query fails.
|
||||
|
||||
On VolumeBackendAPIException, log warning and proceed with clone.
|
||||
"""
|
||||
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
||||
mock_create.return_value = {}
|
||||
|
||||
@@ -246,38 +265,105 @@ class TestCreateClonedVolume(powerflex.TestPowerFlexDriver):
|
||||
mock_log.warning.assert_called_once()
|
||||
mock_create.assert_called_once_with(self.new_volume, self.src_volume)
|
||||
|
||||
def test_create_cloned_volume_image_cache_excludes_grandchildren(self):
|
||||
"""Test that grandchildren of the cache volume are not counted.
|
||||
|
||||
2 direct children + 8 grandchildren with max_size=3:
|
||||
only 2 direct children counted, 2 < 3 so no raise.
|
||||
If grandchildren were incorrectly counted (2+1=3 >= 3),
|
||||
the test would fail, proving they are excluded.
|
||||
"""
|
||||
src_provider_id = self.src_volume.provider_id
|
||||
vtree_volumes = [
|
||||
{'id': 'root_vol', 'ancestorVolumeId': None},
|
||||
{'id': 'child_1', 'ancestorVolumeId': src_provider_id},
|
||||
{'id': 'child_2', 'ancestorVolumeId': src_provider_id},
|
||||
{'id': 'grandchild_1', 'ancestorVolumeId': 'child_1'},
|
||||
{'id': 'grandchild_2', 'ancestorVolumeId': 'child_1'},
|
||||
{'id': 'grandchild_3', 'ancestorVolumeId': 'child_1'},
|
||||
{'id': 'grandchild_4', 'ancestorVolumeId': 'child_1'},
|
||||
{'id': 'grandchild_5', 'ancestorVolumeId': 'child_2'},
|
||||
{'id': 'grandchild_6', 'ancestorVolumeId': 'child_2'},
|
||||
{'id': 'grandchild_7', 'ancestorVolumeId': 'child_2'},
|
||||
{'id': 'grandchild_8', 'ancestorVolumeId': 'child_2'},
|
||||
]
|
||||
self._test_create_cloned_volume_image_cache(vtree_volumes,
|
||||
expect_raises=False,
|
||||
max_size=3)
|
||||
|
||||
def test_create_cloned_volume_image_cache_filtering_edge_cases(self):
|
||||
"""Test comprehensive filtering edge cases.
|
||||
|
||||
6 volumes total, only 2 are direct children, max_size=3:
|
||||
2 < 3 so no raise. If any non-child volume were incorrectly
|
||||
counted (2+1=3 >= 3), the test would fail. This validates:
|
||||
- Root volume with ancestorVolumeId=None is excluded
|
||||
- Volumes missing ancestorVolumeId field are excluded
|
||||
- Volumes with None ancestorVolumeId value are excluded
|
||||
- Orphan volumes (unknown parent) are excluded
|
||||
- Only exact matches to src_provider_id are counted
|
||||
"""
|
||||
src_provider_id = self.src_volume.provider_id
|
||||
vtree_volumes = [
|
||||
{'id': 'root_vol', 'ancestorVolumeId': None},
|
||||
{'id': 'root_no_field'},
|
||||
{'id': 'root_none_value', 'ancestorVolumeId': None},
|
||||
{'id': 'child_1', 'ancestorVolumeId': src_provider_id},
|
||||
{'id': 'child_2', 'ancestorVolumeId': src_provider_id},
|
||||
{'id': 'orphan', 'ancestorVolumeId': 'unknown_parent'},
|
||||
]
|
||||
self._test_create_cloned_volume_image_cache(vtree_volumes,
|
||||
expect_raises=False,
|
||||
max_size=3)
|
||||
|
||||
def test_create_cloned_volume_image_cache_vtree_volumes_api_call(self):
|
||||
"""Integration: verify query_vtree_volumes called with correct ID.
|
||||
|
||||
Validates the full flow from query_volume (to get vtreeId) through
|
||||
query_vtree_volumes (to get volume list for filtering).
|
||||
"""
|
||||
src_provider_id = self.src_volume.provider_id
|
||||
vtree_volumes = [
|
||||
{'id': 'root', 'ancestorVolumeId': None},
|
||||
{'id': 'child_1', 'ancestorVolumeId': src_provider_id},
|
||||
]
|
||||
self._test_create_cloned_volume_image_cache(
|
||||
vtree_volumes, expect_raises=False,
|
||||
max_size=20, vtree_id='specific_vtree_id_123')
|
||||
|
||||
@mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.'
|
||||
'PowerFlexDriver._create_volume_from_source')
|
||||
@mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.'
|
||||
'PowerFlexDriver._get_client')
|
||||
def test_create_cloned_volume_image_cache_missing_stats_field(
|
||||
self, mock_client, mock_create):
|
||||
"""Test cloning when statistics response missing expected field."""
|
||||
@mock.patch('cinder.volume.drivers.dell_emc.powerflex.driver.LOG')
|
||||
def test_create_cloned_volume_image_cache_vtree_volumes_query_fails(
|
||||
self, mock_log, mock_client, mock_create):
|
||||
"""Test cloning when query_vtree_volumes fails.
|
||||
|
||||
query_volume succeeds but query_vtree_volumes raises
|
||||
VolumeBackendAPIException. Should log warning and proceed.
|
||||
"""
|
||||
self.set_https_response_mode(self.RESPONSE_MODE.Valid)
|
||||
mock_create.return_value = {}
|
||||
|
||||
# Set clone limit to 20
|
||||
self.override_config(options.POWERFLEX_MAX_IMAGE_CACHE_VTREE_SIZE, 20,
|
||||
configuration.SHARED_CONF_GROUP)
|
||||
|
||||
# Mock REST client with response missing numOfVolumes
|
||||
mock_rest_client = mock.Mock()
|
||||
mock_client.return_value = mock_rest_client
|
||||
mock_rest_client.query_volume.return_value = {
|
||||
'vtreeId': 'test_vtree_id'
|
||||
}
|
||||
mock_rest_client.query_vtree_statistics.return_value = {
|
||||
'otherField': 'value' # Missing numOfVolumes
|
||||
}
|
||||
mock_rest_client.query_vtree_volumes.side_effect = (
|
||||
exception.VolumeBackendAPIException(data="VTree query failed"))
|
||||
|
||||
# Mock volume_utils to indicate source volume is an image cache entry
|
||||
with mock.patch('cinder.volume.volume_utils.is_image_cache_entry',
|
||||
return_value=True):
|
||||
self.driver.create_cloned_volume(self.new_volume, self.src_volume)
|
||||
|
||||
# Should query volume and vtree statistics, default to 0, proceed
|
||||
mock_rest_client.query_volume.assert_called_once_with(
|
||||
self.src_volume.provider_id)
|
||||
mock_rest_client.query_vtree_statistics.assert_called_once_with(
|
||||
mock_rest_client.query_vtree_volumes.assert_called_once_with(
|
||||
'test_vtree_id')
|
||||
mock_log.warning.assert_called_once()
|
||||
mock_create.assert_called_once_with(self.new_volume, self.src_volume)
|
||||
@@ -25,6 +25,7 @@ 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
|
||||
from cinder.volume import configuration
|
||||
from cinder.volume.drivers.dell_emc.powerflex import options
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -145,6 +146,61 @@ class TestMisc(powerflex.TestPowerFlexDriver):
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.driver.check_for_setup_error)
|
||||
|
||||
def test_vtree_size_valid_pf5(self):
|
||||
"""Valid vtree size within PF5 limit (1024)."""
|
||||
self.driver.storage_pools = self.STORAGE_POOLS
|
||||
self.override_config(options.POWERFLEX_MAX_IMAGE_CACHE_VTREE_SIZE,
|
||||
512, configuration.SHARED_CONF_GROUP)
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
def test_vtree_size_exceeds_pf5_limit(self):
|
||||
"""vtree size exceeds PF5 limit (1024)."""
|
||||
self.driver.storage_pools = self.STORAGE_POOLS
|
||||
self.override_config(options.POWERFLEX_MAX_IMAGE_CACHE_VTREE_SIZE,
|
||||
1025, configuration.SHARED_CONF_GROUP)
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.driver.check_for_setup_error)
|
||||
|
||||
def test_vtree_size_exceeds_pf4_limit(self):
|
||||
"""vtree size exceeds PF4 limit (126)."""
|
||||
self.driver.storage_pools = self.STORAGE_POOLS
|
||||
self.override_config(options.POWERFLEX_MAX_IMAGE_CACHE_VTREE_SIZE,
|
||||
200, configuration.SHARED_CONF_GROUP)
|
||||
with self.custom_response_mode(**{
|
||||
'types/ProtectionDomain/instances/action/queryBySelectedIds':
|
||||
mocks.MockHTTPSResponse([
|
||||
{
|
||||
'id': self.PROT_DOMAIN_ID,
|
||||
'genType': 'Mirroring'
|
||||
}
|
||||
], 200)
|
||||
}):
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.driver.check_for_setup_error)
|
||||
|
||||
def test_vtree_size_valid_pf4(self):
|
||||
"""Valid vtree size within PF4 limit (126)."""
|
||||
self.driver.storage_pools = self.STORAGE_POOLS
|
||||
self.override_config(options.POWERFLEX_MAX_IMAGE_CACHE_VTREE_SIZE,
|
||||
100, configuration.SHARED_CONF_GROUP)
|
||||
with self.custom_response_mode(**{
|
||||
'types/ProtectionDomain/instances/action/queryBySelectedIds':
|
||||
mocks.MockHTTPSResponse([
|
||||
{
|
||||
'id': self.PROT_DOMAIN_ID,
|
||||
'genType': 'Mirroring'
|
||||
}
|
||||
], 200)
|
||||
}):
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
def test_vtree_size_zero_skips_validation(self):
|
||||
"""vtree size of 0 skips the validation."""
|
||||
self.driver.storage_pools = self.STORAGE_POOLS
|
||||
self.override_config(options.POWERFLEX_MAX_IMAGE_CACHE_VTREE_SIZE,
|
||||
0, configuration.SHARED_CONF_GROUP)
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
def test_volume_size_round_true(self):
|
||||
self.driver._check_volume_size(1)
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ class TestPowerFlexClient(test.TestCase):
|
||||
self.host_nqn = "nqn.2014-08.org.nvmexpress:uuid:" \
|
||||
"fc9aaff0-09bb-4825-b590-4897d1a4eade"
|
||||
self.host_name = "hostname"
|
||||
self.vtree_id = "2b713ee600000007"
|
||||
|
||||
def _set_overrides(self):
|
||||
# Override the defaults to fake values
|
||||
@@ -459,6 +460,33 @@ class TestPowerFlexClient(test.TestCase):
|
||||
self.client.query_hosts)
|
||||
self.assertIn("Failed to query hosts: Error message.", ex.msg)
|
||||
|
||||
def test_query_vtree_volumes_success(self):
|
||||
response = self._getJsonFile("query_vtree_volumes_response.json")
|
||||
|
||||
with mock.patch.object(self.client,
|
||||
'execute_powerflex_get_request',
|
||||
return_value=(self.status_code_ok,
|
||||
response)):
|
||||
result = self.client.query_vtree_volumes(self.vtree_id)
|
||||
self.assertEqual(result, response)
|
||||
self.client.execute_powerflex_get_request.assert_called_with(
|
||||
'/instances/VTree::%(vtree_id)s/relationships/Volume',
|
||||
vtree_id=self.vtree_id
|
||||
)
|
||||
|
||||
def test_query_vtree_volumes_failure(self):
|
||||
vtree_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_vtree_volumes,
|
||||
vtree_id)
|
||||
self.assertIn(
|
||||
"Failed to query vTree volumes: Error message.", ex.msg)
|
||||
|
||||
def test_query_host_by_nqn_existing_nqn(self):
|
||||
response = self._getJsonFile("query_hosts_response.json")
|
||||
|
||||
|
||||
@@ -232,6 +232,24 @@ class PowerFlexBaseDriver(driver.VolumeDriver):
|
||||
# enforces a consistent gentype across all pools.
|
||||
first_pd = self.storage_pools[0].split(":")[0]
|
||||
client.init_powerflex_gen_type(first_pd)
|
||||
# validate powerflex_max_image_cache_vtree_size against
|
||||
# the version-specific vTree snapshot limit
|
||||
max_vtree_size = (
|
||||
self.configuration.powerflex_max_image_cache_vtree_size
|
||||
)
|
||||
if max_vtree_size > 0:
|
||||
if client.check_powerflex_ec_version():
|
||||
vtree_limit = rest_client.MAX_SNAPS_IN_VTREE_V5
|
||||
else:
|
||||
vtree_limit = rest_client.MAX_SNAPS_IN_VTREE
|
||||
if max_vtree_size > vtree_limit:
|
||||
msg = (_("powerflex_max_image_cache_vtree_size is set "
|
||||
"to %(configured)d but the maximum vTree "
|
||||
"snapshot limit for this PowerFlex system "
|
||||
"is %(limit)d.") %
|
||||
{"configured": max_vtree_size,
|
||||
"limit": vtree_limit})
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
# validate the storage pools and check if zero padding is enabled
|
||||
for pool in self.storage_pools:
|
||||
try:
|
||||
@@ -876,25 +894,36 @@ class PowerFlexBaseDriver(driver.VolumeDriver):
|
||||
LOG.info("Clone volume %(vol_id)s to %(target_vol_id)s.",
|
||||
{"vol_id": src_vref.id, "target_vol_id": volume.id})
|
||||
|
||||
# Check if source volume is an image cache entry and under the max
|
||||
# vTree size limit
|
||||
max_size = self.configuration.powerflex_max_image_cache_vtree_size
|
||||
if max_size > 0 and volume_utils.is_image_cache_entry(src_vref):
|
||||
client = self._get_client()
|
||||
try:
|
||||
volume_info = client.query_volume(src_vref.provider_id)
|
||||
vtree_id = volume_info["vtreeId"]
|
||||
vtree_stats = client.query_vtree_statistics(vtree_id)
|
||||
num_volumes = int(vtree_stats.get("numOfVolumes", "0"))
|
||||
if num_volumes >= max_size:
|
||||
raise exception.SnapshotLimitReached(set_limit=max_size)
|
||||
except exception.VolumeBackendAPIException:
|
||||
LOG.warning("Failed to query volume statistics for image "
|
||||
"cache volume %(vol_id)s. Proceeding with clone.",
|
||||
{"vol_id": src_vref.id})
|
||||
|
||||
self._check_image_cache_vtree_limit(src_vref)
|
||||
return self._create_volume_from_source(volume, src_vref)
|
||||
|
||||
def _check_image_cache_vtree_limit(self, src_vref):
|
||||
"""Check if image cache volume exceeds the direct child limit.
|
||||
|
||||
Counts only direct child volumes of the cache volume (filtering by
|
||||
ancestorVolumeId) and raises SnapshotLimitReached if the count
|
||||
meets or exceeds the configured max size.
|
||||
"""
|
||||
max_size = self.configuration.powerflex_max_image_cache_vtree_size
|
||||
if max_size <= 0 or not volume_utils.is_image_cache_entry(src_vref):
|
||||
return
|
||||
client = self._get_client()
|
||||
try:
|
||||
volume_info = client.query_volume(src_vref.provider_id)
|
||||
vtree_id = volume_info["vtreeId"]
|
||||
vtree_volumes = client.query_vtree_volumes(vtree_id)
|
||||
count_direct_children = 0
|
||||
for v in vtree_volumes:
|
||||
if v.get("ancestorVolumeId") == src_vref.provider_id:
|
||||
count_direct_children += 1
|
||||
if count_direct_children >= max_size:
|
||||
raise exception.SnapshotLimitReached(
|
||||
set_limit=max_size)
|
||||
except exception.VolumeBackendAPIException:
|
||||
LOG.warning("Failed to query vTree volumes for image "
|
||||
"cache volume %(vol_id)s. Proceeding with clone.",
|
||||
{"vol_id": src_vref.id})
|
||||
|
||||
def delete_volume(self, volume):
|
||||
"""Delete volume from PowerFlex storage backend.
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ named Dell EMC VxFlex OS).
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from cinder.volume.drivers.dell_emc.powerflex import rest_client
|
||||
from cinder.volume.drivers.dell_emc.powerflex import utils as flex_utils
|
||||
|
||||
# deprecated options
|
||||
@@ -160,10 +159,14 @@ actual_opts = [
|
||||
help='Use this value to specify read '
|
||||
'timeout value (in seconds) for rest call.'),
|
||||
cfg.IntOpt(POWERFLEX_MAX_IMAGE_CACHE_VTREE_SIZE,
|
||||
default=0, min=0, max=rest_client.MAX_SNAPS_IN_VTREE,
|
||||
help='Maximum size of the vTree associated with an entry in '
|
||||
'the image volume cache. When the size is exceeded, '
|
||||
'the cache entry will be replaced with one created from '
|
||||
'a new vTree. A value of 0 means the size is limited by '
|
||||
'the PowerFlex vTree snapshot limit.')
|
||||
default=0, min=0,
|
||||
help='Maximum number of direct child volumes allowed for an '
|
||||
'image volume cache entry. Only immediate clones are '
|
||||
'counted; grandchildren and deeper descendants are '
|
||||
'excluded. When the limit is reached, the cache '
|
||||
'entry will be replaced with one created from a new '
|
||||
'vTree. A value of 0 means the size is limited by the '
|
||||
'PowerFlex vTree snapshot limit (126 for PowerFlex 4, '
|
||||
'1024 for PowerFlex 5). Consult Dell support before '
|
||||
'changing this value.')
|
||||
]
|
||||
@@ -41,6 +41,7 @@ TOO_MANY_SNAPS_ERROR = 182
|
||||
ILLEGAL_SYNTAX = 0
|
||||
|
||||
MAX_SNAPS_IN_VTREE = 126
|
||||
MAX_SNAPS_IN_VTREE_V5 = 1024
|
||||
TOKEN_REFRESH_SUCC_INTERVAL = 300
|
||||
TOKEN_REFRESH_FAIL_INTERVAL = 60
|
||||
|
||||
@@ -775,6 +776,24 @@ class RestClient(object):
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
return response
|
||||
|
||||
def query_vtree_volumes(self, vtree_id):
|
||||
"""Query all volumes belonging to a vTree.
|
||||
|
||||
:param vtree_id: PowerFlex vTree ID
|
||||
:return: list of volume dicts
|
||||
:raises VolumeBackendAPIException: if the API call fails
|
||||
"""
|
||||
url = "/instances/VTree::%(vtree_id)s/relationships/Volume"
|
||||
|
||||
r, response = self.execute_powerflex_get_request(url,
|
||||
vtree_id=vtree_id)
|
||||
if r.status_code != http_client.OK:
|
||||
msg = (_("Failed to query vTree volumes: %s.") %
|
||||
response["message"])
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
return response
|
||||
|
||||
def migrate_vtree(self, volume, params):
|
||||
url = "/instances/Volume::%(vol_id)s/action/migrateVTree"
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
fixes:
|
||||
- |
|
||||
Dell PowerFlex driver: Fixed image cache vTree size check to count
|
||||
only direct child volumes of the cache volume.
|
||||
Previously, the driver used ``query_vtree_statistics``, which counted
|
||||
all descendants in the vTree, including snapshots and deeper nested
|
||||
volumes. This could incorrectly block clone creation before the cache
|
||||
volume reached the configured direct-child limit.
|
||||
The driver now uses ``query_vtree_volumes`` with the
|
||||
``ancestorVolumeId`` filter to count only immediate children.
|
||||
The ``powerflex_max_image_cache_vtree_size`` configuration option name
|
||||
and behavior are preserved.
|
||||
Reference in new issue
Block a user