Merge "Dell PowerFlex: Fix image cache vTree size counting"

This commit is contained in:
Zuul authored and Gerrit Code Review committed 2026-09-29 16:07:48 +00:00
commit 803dca69e6
8 files changed
+435 -97

No files matched your search

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