[Pure Storage] Add capacity based backend QoS options

Add new QoS spec parameters based on the capacity of the volume.

New parameters are maxIOPS_per_GB and maxBWS_per_GB.

If provied in the same QoS spec as the original max values for
IOPS or BWS, the max values will take precedence.

Capacity based QoS settings will be calculated and applied
for the following actions:

- create volume
- create volume from snapshot
- extend volume
- clone volume
- manage volume
- retype volume

QoS settings are not applied to volumes within volume groups
as QoS for these is controlled at the volume group level.

Implements: blueprint pure-per-gb-qos

Change-Id: I79b5180ac48140f747761d0c04f7bd62d9995b43
This commit is contained in:
Simon Dodsley
2025-04-30 11:42:43 -04:00
parent 635bdab2c0
commit 986929bc99
4 changed files with 122 additions and 28 deletions

View File

@@ -1198,10 +1198,14 @@ MPS_REFS = ValidResponse(200, None, 3,
DotNotation(MANAGEABLE_PURE_SNAP_REFS[2])], {})
# unit for maxBWS is MB
QOS_IOPS_BWS = {"maxIOPS": "100", "maxBWS": "1"}
QOS_IOPS_BWS_2 = {"maxIOPS": "1000", "maxBWS": "10"}
QOS_INVALID = {"maxIOPS": "100", "maxBWS": str(512 * 1024 + 1)}
QOS_ZEROS = {"maxIOPS": "0", "maxBWS": "0"}
QOS_IOPS_BWS = {"maxIOPS": "100", "maxBWS": "1",
"maxIOPS_per_GB": "0", "maxBWS_per_GB": "0"}
QOS_IOPS_BWS_2 = {"maxIOPS": "1000", "maxBWS": "10",
"maxIOPS_per_GB": "0", "maxBWS_per_GB": "0"}
QOS_INVALID = {"maxIOPS": "100", "maxBWS": str(512 * 1024 + 1),
"maxIOPS_per_GB": "0", "maxBWS_per_GB": "0"}
QOS_ZEROS = {"maxIOPS": "0", "maxBWS": "0",
"maxIOPS_per_GB": "0", "maxBWS_per_GB": "0"}
QOS_IOPS = {"maxIOPS": "100"}
QOS_BWS = {"maxBWS": "1"}
MAX_IOPS = 100000000
@@ -2126,11 +2130,14 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
@mock.patch(DRIVER_PATH + ".flasharray.VolumePost")
@mock.patch(BASE_DRIVER_OBJ + "._add_to_group_if_needed")
@mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type")
def test_create_cloned_volume(self, mock_get_replication_type,
@mock.patch.object(volume_types, 'get_volume_type')
def test_create_cloned_volume(self, mock_get_volume_type,
mock_get_replication_type,
mock_add_to_group,
mock_fa):
vol, vol_name = self.new_fake_vol(set_provider_id=False)
src_vol, src_name = self.new_fake_vol()
mock_get_volume_type.return_value = vol.volume_type
mock_data = self.array.flasharray.VolumePost(names=[vol_name],
source=
pure.flasharray.
@@ -2172,7 +2179,9 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
@mock.patch(BASE_DRIVER_OBJ + "._get_qos_settings")
@mock.patch(BASE_DRIVER_OBJ + ".set_qos")
@mock.patch(DRIVER_PATH + ".flasharray.VolumePost")
@mock.patch.object(volume_types, 'get_volume_type')
def test_create_cloned_volume_qos(self, qos_info,
mock_get_volume_type,
mock_fa,
mock_qos,
mock_qos_specs):
@@ -2184,6 +2193,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
vol, vol_name = self.new_fake_vol(set_provider_id=False)
src_vol, src_name = self.new_fake_vol(spec={"size": 1},
type_qos_specs_id=qos.id)
mock_get_volume_type.return_value = vol.volume_type
mock_data = self.array.flasharray.VolumePost(names=[vol_name],
source=
pure.flasharray.
@@ -2194,10 +2204,16 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
self.mock_object(self.driver, '_get_volume_type_extra_spec',
return_value={})
self.driver.create_cloned_volume(vol, src_vol)
self.driver.set_qos.assert_called_with(self.array, vol_name, qos)
self.driver.set_qos.assert_called_with(self.array,
vol_name,
vol["size"],
qos)
@mock.patch(DRIVER_PATH + ".flasharray.VolumePost")
def test_create_cloned_volume_sync_rep(self, mock_fa):
@mock.patch.object(volume_types, 'get_volume_type')
def test_create_cloned_volume_sync_rep(self,
mock_get_volume_type,
mock_fa):
repl_extra_specs = {
'replication_type': '<in> sync',
'replication_enabled': '<is> true',
@@ -2206,6 +2222,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
type_extra_specs=repl_extra_specs)
vol, vol_name = self.new_fake_vol(set_provider_id=False,
type_extra_specs=repl_extra_specs)
mock_get_volume_type.return_value = vol.volume_type
mock_data = self.array.flasharray.VolumePost(names=[vol_name],
source=pure.flasharray.
reference(name=src_name))
@@ -2222,11 +2239,14 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
@mock.patch(DRIVER_PATH + ".flasharray.VolumePost")
@mock.patch(BASE_DRIVER_OBJ + "._add_to_group_if_needed")
@mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type")
def test_create_cloned_volume_and_extend(self, mock_get_replication_type,
@mock.patch.object(volume_types, 'get_volume_type')
def test_create_cloned_volume_and_extend(self, mock_get_volume_type,
mock_get_replication_type,
mock_add_to_group,
mock_fa, mock_extend):
vol, vol_name = self.new_fake_vol(set_provider_id=False,
spec={"size": 2})
mock_get_volume_type.return_value = vol.volume_type
src_vol, src_name = self.new_fake_vol()
mock_get_replication_type.return_value = None
mock_data = self.array.flasharray.VolumePost(names=[vol_name],
@@ -2246,9 +2266,12 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
# Tests cloning a volume that is part of a consistency group
@mock.patch(BASE_DRIVER_OBJ + "._add_to_group_if_needed")
@mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type")
def test_create_cloned_volume_with_cgroup(self, mock_get_replication_type,
@mock.patch.object(volume_types, 'get_volume_type')
def test_create_cloned_volume_with_cgroup(self, mock_get_volume_type,
mock_get_replication_type,
mock_add_to_group):
vol, vol_name = self.new_fake_vol(set_provider_id=False)
mock_get_volume_type.return_value = vol.volume_type
group = fake_group.fake_group_obj(mock.MagicMock())
self.driver._get_volume_type_extra_spec = mock.Mock(
return_value={})
@@ -2619,8 +2642,10 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
self.array.delete_connections.assert_not_called()
@mock.patch(DRIVER_PATH + ".flasharray.VolumePatch")
def test_extend_volume(self, mock_fa):
@mock.patch.object(volume_types, 'get_volume_type')
def test_extend_volume(self, mock_get_volume_type, mock_fa):
vol, vol_name = self.new_fake_vol(spec={"size": 1})
mock_get_volume_type.return_value = vol.volume_type
mock_data = self.flasharray.VolumePatch(provisioned=3 * units.Gi)
self.driver.extend_volume(vol, 3)
self.array.patch_volumes.\
@@ -4505,7 +4530,10 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
Reference(name=vol_name),
name=vol_name,
qos={'maxIOPS': 100,
'maxBWS': 1048576})
'maxBWS': 1048576,
'maxBWS': 1048576,
'maxIOPS_per_GB': 0,
'maxBWS_per_GB': 0})
mock_fa.return_value = mock_data
mock_get_volume_type.return_value = vol.volume_type
@@ -4517,9 +4545,11 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
with_default_protection=
False,
volume=mock_data)
self.driver.set_qos.assert_called_with(self.array, vol_name,
self.driver.set_qos.assert_called_with(self.array, vol_name, 1,
{'maxIOPS': 100,
'maxBWS': 1048576})
'maxBWS': 1048576,
'maxIOPS_per_GB': 0,
'maxBWS_per_GB': 0})
self.assertFalse(self.array.extend_volume.called)
mock_add_to_group.assert_called_once_with(vol, vol_name)
self.assert_error_propagates(
@@ -4546,9 +4576,11 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
self.array.get_volumes.return_value = MPV
self.driver.manage_existing(vol, volume_ref)
mock_qos.assert_called_with(self.array, vol_name,
mock_qos.assert_called_with(self.array, vol_name, 3,
{'maxIOPS': 100,
'maxBWS': 1048576})
'maxBWS': 1048576,
'maxIOPS_per_GB': 0,
'maxBWS_per_GB': 0})
@mock.patch(DRIVER_PATH + ".flasharray.VolumePatch")
def test_retype_qos(self, mock_fa):

View File

@@ -228,7 +228,7 @@ class PureBaseVolumeDriver(san.SanDriver):
"""Performs volume management on Pure Storage FlashArray."""
SUPPORTS_ACTIVE_ACTIVE = True
PURE_QOS_KEYS = ['maxIOPS', 'maxBWS']
PURE_QOS_KEYS = ['maxIOPS', 'maxBWS', 'maxIOPS_per_GB', 'maxBWS_per_GB']
# ThirdPartySystems wiki page
CI_WIKI_NAME = "Pure_Storage_CI"
@@ -346,7 +346,19 @@ class PureBaseVolumeDriver(san.SanDriver):
target_array)
@pure_driver_debug_trace
def set_qos(self, array, vol_name, qos):
def set_qos(self, array, vol_name, vol_size, qos):
# max_IOPS and max_BWS override the per GB IOPS and BW values if
# both are provided. If only a per GB value is provided then
# we must ensure, based on volume size, the IOPS or BW values
# do not exceed the maximum limits for these values allowed per
# volume.
if qos['maxIOPS'] == 0 and qos['maxIOPS_per_GB']:
qos['maxIOPS'] = min(MAX_IOPS,
int(qos['maxIOPS_per_GB']) * vol_size)
if qos['maxBWS'] == 0 and qos['maxBWS_per_GB']:
qos['maxBWS'] = min(MAX_BWS,
int(qos['maxBWS_per_GB']) * vol_size)
if qos['maxIOPS'] == 0 and qos['maxBWS'] == 0:
array.patch_volumes(names=[vol_name],
volume=flasharray.VolumePatch(
@@ -444,6 +456,19 @@ class PureBaseVolumeDriver(san.SanDriver):
@pure_driver_debug_trace
def create_with_qos(self, array, vol_name, vol_size, qos):
# max_IOPS and max_BWS override the per GB IOPS and BW values if
# both are provided. Iif only a per GB value is provided then
# we must ensure, based on volume size, the IOPS or BW values
# do not exceed the maximum limits for these values allowed per
# volume.
gb_size = vol_size / units.Gi
if qos['maxIOPS'] == 0 and qos['maxIOPS_per_GB']:
qos['maxIOPS'] = min(MAX_IOPS,
int(qos['maxIOPS_per_GB']) * gb_size)
if qos['maxBWS'] == 0 and qos['maxBWS_per_GB']:
qos['maxBWS'] = min(MAX_BWS,
int(qos['maxBWS_per_GB']) * gb_size)
if self._array.safemode:
if qos['maxIOPS'] == 0 and qos['maxBWS'] == 0:
array.post_volumes(names=[vol_name],
@@ -826,7 +851,7 @@ class PureBaseVolumeDriver(san.SanDriver):
snapshot["volume_size"],
volume["size"])
if qos is not None:
self.set_qos(current_array, vol_name, qos)
self.set_qos(current_array, vol_name, snapshot["volume_size"], qos)
else:
current_array.patch_volumes(names=[vol_name],
volume=flasharray.VolumePatch(
@@ -938,11 +963,15 @@ class PureBaseVolumeDriver(san.SanDriver):
vol_name,
src_vref["size"],
volume["size"])
# Check if the volume_type has QoS settings and if so
# apply them to the newly created volume
qos = self._get_qos_settings(volume.volume_type)
if qos:
self.set_qos(current_array, vol_name, qos)
type_id = volume.get('volume_type_id')
ctxt = context.get_admin_context()
if type_id is not None:
volume_type = volume_types.get_volume_type(ctxt, type_id)
# Check if the volume_type has QoS settings and if so
# apply them to the newly created volume
qos = self._get_qos_settings(volume_type)
if qos is not None:
self.set_qos(current_array, vol_name, volume["size"], qos)
return self._setup_volume(current_array, volume, vol_name)
@@ -1385,17 +1414,25 @@ class PureBaseVolumeDriver(san.SanDriver):
return thin_provisioning
@pure_driver_debug_trace
def extend_volume(self, volume, new_size):
def extend_volume(self, volume, new_size_gb):
"""Extend volume to new_size."""
# Get current array in case we have failed over via replication.
current_array = self._get_current_array()
vol_name = self._get_vol_name(volume)
new_size = new_size * units.Gi
new_size = new_size_gb * units.Gi
current_array.patch_volumes(names=[vol_name],
volume=flasharray.VolumePatch(
provisioned=new_size))
ctxt = context.get_admin_context()
type_id = volume.get('volume_type_id')
if type_id is not None:
volume_type = volume_types.get_volume_type(ctxt, type_id)
LOG.debug("QOS volume type: '%s'", volume_type)
qos = self._get_qos_settings(volume_type)
if qos is not None:
self.set_qos(current_array, vol_name, new_size, qos)
def _add_volume_to_consistency_group(self, group, vol_name):
pgroup_name = self._get_pgroup_name(group)
@@ -1949,7 +1986,8 @@ class PureBaseVolumeDriver(san.SanDriver):
qos = None
qos = self._get_qos_settings(volume.volume_type)
if qos:
self.set_qos(current_array, new_vol_name, qos)
vol_size = int(volume_data.provisioned / units.Gi)
self.set_qos(current_array, new_vol_name, vol_size, qos)
volume.provider_id = new_vol_name
async_enabled = self._enable_async_replication_if_needed(current_array,
volume)
@@ -2403,7 +2441,7 @@ class PureBaseVolumeDriver(san.SanDriver):
if qos == {}:
return None
else:
# Check set vslues are within limits
# Check set values are within limits
iops_qos = int(qos.get('maxIOPS', 0))
bw_qos = int(qos.get('maxBWS', 0)) * MIN_BWS
if iops_qos != 0 and not (MIN_IOPS <= iops_qos <= MAX_IOPS):
@@ -2419,6 +2457,8 @@ class PureBaseVolumeDriver(san.SanDriver):
qos['maxIOPS'] = iops_qos
qos['maxBWS'] = bw_qos
qos['maxIOPS_per_GB'] = int(qos.get('maxIOPS_per_GB', 0))
qos['maxBWS_per_GB'] = int(qos.get('maxBWS_per_GB', 0)) * MIN_BWS
return qos
def _generate_purity_vol_name(self, volume):
@@ -2769,7 +2809,7 @@ class PureBaseVolumeDriver(san.SanDriver):
qos = self._get_qos_settings(new_type)
vol_name = self._generate_purity_vol_name(volume)
if qos is not None:
self.set_qos(current_array, vol_name, qos)
self.set_qos(current_array, vol_name, volume["size"], qos)
else:
current_array.patch_volumes(names=[vol_name],
volume=flasharray.VolumePatch(

View File

@@ -59,12 +59,27 @@ following capabilities in the OpenStack Block Storage API
* **maxBWS** - Maximum bandwidth limit in MB/s. Range: 1 - 524288 (512GB/s)
* **maxIOPS_per_GB** - Maximum number of IOPs allowed for volume based on
capacity. Range: 100 - 100M
* **maxBWS_per_GB** - Maximum bandwidth limit in MB/s based on capacity.
Range: 1 - 524288 (512GB/s)
If both max and per_GB values are provided for a QoS type, the max value will
take precedence.
If the calculated per_GB value for a volume based on capacity is greater
than the maximum allowed value, the maximum allowed values will be applied.
The qos keys above must be created and asscoiated to a volume type. For
information on how to set the key-value pairs and associate them with a
volume type see the `volume qos
<https://docs.openstack.org/python-openstackclient/latest/cli/command-objects/volume-qos.html>`_
section in the OpenStack Client command list.
QoS settings are not applied to any volume in a volume group as these are
controlled at the volume group level.
Configure OpenStack and Purity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -0,0 +1,7 @@
---
features:
- |
[Pure Storage] Added new QoS spec parameters to support QoS per GB.
New spec options are ``maxIOPS_per_GB`` and ``maxBWS_per_GB``. If
either of these are provided with the equivalent ``max`` value, the
``max`` value will take precedence.