Merge "HPE: Improved handling of QOS for Alletra MP"

This commit is contained in:
Zuul
2026-03-25 18:43:03 +00:00
committed by Gerrit Code Review
3 changed files with 187 additions and 37 deletions

View File

@@ -1241,6 +1241,52 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
mock_client.assert_has_calls(expected)
self.assertIsNone(return_model)
def test_qos_alletra_mp_uses_kb_bandwidth(self):
"""Ensure Alletra MP QOS uses kB/s for bandwidth limits.
When the backend API version indicates Alletra MP, maxBWS should be
converted using a 1000-based factor into bwMaxLimitKB, and only the
max limits should be sent to createQoSRules.
"""
# Create a common driver instance with Alletra MP API_VERSION.
conf = self.setup_configuration()
common = hpecommon.HPE3PARCommon(conf)
mock_client = mock.Mock()
common.client = mock_client
common.API_VERSION = hpecommon.API_VERSION_2025
# Use the standard QoS specs defined for these tests.
common._set_qos_rule(self.QOS_SPECS, 'vvs-test')
mock_client.createQoSRules.assert_called_once_with(
'vvs-test',
{'ioMaxLimit': 1000, 'bwMaxLimitKB': 50000})
def test_qos_alletra_mp_invalid_without_max_limits(self):
"""Alletra MP QOS requires at least one max limit.
For Alletra MP backends, _set_qos_rule should raise InvalidInput
if both maxIOPS and maxBWS are omitted from the QOS specs.
"""
conf = self.setup_configuration()
common = hpecommon.HPE3PARCommon(conf)
mock_client = mock.Mock()
common.client = mock_client
# Force Alletra MP behavior based on API version.
common.API_VERSION = hpecommon.API_VERSION_2025
# No maxIOPS or maxBWS provided; invalid for Alletra MP.
invalid_qos = {}
self.assertRaises(exception.InvalidInput,
common._set_qos_rule,
invalid_qos,
'vvs-test')
mock_client.createQoSRules.assert_not_called()
@mock.patch.object(volume_types, 'get_volume_type')
def test_create_volume_replicated_periodic(self, _mock_volume_types):
# setup_mock_client drive with default configuration
@@ -2450,7 +2496,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
mock.call.createQoSRules(
'vvs-0DM4qZEVSKON-DXN-NwVpw',
{'ioMinGoal': 100, 'ioMaxLimit': 1000,
'bwMinGoalKB': 25600, 'bwMaxLimitKB': 51200,
'bwMinGoalKB': 25000, 'bwMaxLimitKB': 50000,
'priority': 3,
'latencyGoal': 25}
),
@@ -2816,6 +2862,52 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
mock_client.assert_has_calls(expected)
@ddt.data('volume', 'volume_name_id')
def test_create_cloned_volume_wsapi_2023_no_comment(self, volume_attr):
src_vref = getattr(self, volume_attr)
vol_name = getattr(self, volume_attr.upper() + '_3PAR_NAME')
mock_client = self.setup_driver(wsapi_version=self.wsapi_version_2023)
mock_client.getVolume.return_value = {'name': mock.ANY}
mock_client.copyVolume.return_value = {'taskid': 1}
mock_client.getStorageSystemInfo.return_value = {
'id': self.CLIENT_ID,
'serialNumber': 'XXXXXXX'}
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
volume = {'name': HPE3PARBaseDriver.VOLUME_NAME,
'id': HPE3PARBaseDriver.CLONE_ID,
'display_name': 'Foo Volume',
'size': 2,
'host': volume_utils.append_host(self.FAKE_HOST,
HPE3PAR_CPG2),
'source_volid': src_vref.id}
common = self.driver._login()
model_update = common.create_cloned_volume(volume, src_vref)
self.assertIsNone(model_update)
snap_name = mock.ANY
optional = mock.ANY
optional_fields = {'tpvv': True,
'tdvv': False,
'online': True}
expected = [
mock.call.createSnapshot(snap_name, vol_name, optional),
mock.call.getVolume(snap_name),
mock.call.copyVolume(
snap_name,
'osv-0DM4qZEVSKON-AAAAAAAAA',
HPE3PAR_CPG2,
optional_fields)]
mock_client.assert_has_calls(expected)
@mock.patch.object(volume_types, 'get_volume_type')
def test_clone_volume_with_vvs(self, _mock_volume_types):
# Setup_mock_client drive with default configuration
@@ -4688,8 +4780,8 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
mock.call.createQoSRules(
vvs_matcher,
{'ioMinGoal': 100, 'ioMaxLimit': 1000,
'bwMinGoalKB': 25600, 'priority': 1, 'latencyGoal': 25,
'bwMaxLimitKB': 51200}),
'bwMinGoalKB': 25000, 'priority': 1, 'latencyGoal': 25,
'bwMaxLimitKB': 50000}),
mock.call.addVolumeToVolumeSet(vvs_matcher, osv_matcher),
mock.call.tuneVolume(
osv_matcher, 1,

View File

@@ -1,4 +1,4 @@
# (c) Copyright 2012-2016 Hewlett Packard Enterprise Development LP
# (c) Copyright 2012-2026 Hewlett Packard Enterprise Development LP
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -316,11 +316,12 @@ class HPE3PARCommon(object):
4.0.25 - Update the calculation of free_capacity
4.0.26 - Added comment for cloned volumes. Bug #2062524
4.0.27 - Skip license check for new WSAPI (of 2025). Bug #2119709
4.0.28 - Improved QOS handling for Alletra MP. Bug #2143385
"""
VERSION = "4.0.27"
VERSION = "4.0.28"
stats = {}
@@ -2021,6 +2022,15 @@ class HPE3PARCommon(object):
else:
return default
def _is_alletra_mp(self):
"""Check if the backend is AlletraMP based on WSAPI version.
AlletraMP uses WSAPI version >= 100500000 (API_VERSION_2025).
:returns: True if AlletraMP, False otherwise
"""
return self.API_VERSION >= API_VERSION_2025
def _get_qos_by_volume_type(self, volume_type):
qos = {}
qos_specs_id = volume_type.get('qos_specs_id')
@@ -2062,37 +2072,76 @@ class HPE3PARCommon(object):
latency = self._get_qos_value(qos, 'latency')
priority = self._get_qos_value(qos, 'priority', 'normal')
# Check if backend is AlletraMP
is_alletra_mp = self._is_alletra_mp()
qosRule = {}
if min_io:
qosRule['ioMinGoal'] = int(min_io)
if max_io is None:
qosRule['ioMaxLimit'] = int(min_io)
if max_io:
qosRule['ioMaxLimit'] = int(max_io)
if min_io is None:
qosRule['ioMinGoal'] = int(max_io)
if min_bw:
qosRule['bwMinGoalKB'] = int(min_bw) * units.Ki
if max_bw is None:
qosRule['bwMaxLimitKB'] = int(min_bw) * units.Ki
if max_bw:
qosRule['bwMaxLimitKB'] = int(max_bw) * units.Ki
if min_bw is None:
qosRule['bwMinGoalKB'] = int(max_bw) * units.Ki
if latency:
# latency could be values like 2, 5, etc or
# small values like 0.1, 0.02, etc.
# we are converting to float so that 0.1 doesn't become 0
latency = float(latency)
if latency >= 1:
# by default, latency in millisecs
qosRule['latencyGoal'] = int(latency)
else:
# latency < 1 Eg. 0.1, 0.02, etc
# convert latency to microsecs
qosRule['latencyGoaluSecs'] = int(latency * 1000)
if priority:
qosRule['priority'] = self.qos_priority_level.get(priority.lower())
# For Alletra MP, ioMinGoal, bwMinGoalKB, latencyGoal, and priority
# are deprecated. Only use max limits.
if is_alletra_mp:
# For Alletra MP, at least one of maxIOPS or maxBWS must be
# provided.
if max_io is None and max_bw is None:
err = _(
"For Alletra MP, at least one of maxIOPS or maxBWS "
"QOS parameters must be provided.")
LOG.error(err)
raise exception.InvalidInput(reason=err)
# Alletra MP: Only set max limits, min goals are deprecated
if max_io:
qosRule['ioMaxLimit'] = int(max_io)
if max_bw:
# Alletra MP expects bandwidth in kB/s
qosRule['bwMaxLimitKB'] = int(max_bw) * units.k
if min_io:
LOG.warning(
"minIOPS QOS parameter is deprecated for "
"Alletra MP and will be ignored.")
if min_bw:
LOG.warning(
"minBWS QOS parameter is deprecated for "
"Alletra MP and will be ignored.")
if latency:
LOG.warning("latency QOS parameter is deprecated for "
"Alletra MP and will be ignored.")
if priority:
LOG.warning("priority QOS parameter is deprecated for "
"Alletra MP and will be ignored.")
else:
# 3PAR/Primera/Alletra 9k: Use traditional QoS parameters
if min_io:
qosRule['ioMinGoal'] = int(min_io)
if max_io is None:
qosRule['ioMaxLimit'] = int(min_io)
if max_io:
qosRule['ioMaxLimit'] = int(max_io)
if min_io is None:
qosRule['ioMinGoal'] = int(max_io)
if min_bw:
# 3PAR/Primera/Alletra 9k expect bandwidth in kB/s
qosRule['bwMinGoalKB'] = int(min_bw) * units.k
if max_bw is None:
qosRule['bwMaxLimitKB'] = int(min_bw) * units.k
if max_bw:
qosRule['bwMaxLimitKB'] = int(max_bw) * units.k
if min_bw is None:
qosRule['bwMinGoalKB'] = int(max_bw) * units.k
if latency:
# latency could be values like 2, 5, etc or
# small values like 0.1, 0.02, etc.
# we are converting to float so that 0.1 doesn't become 0
latency = float(latency)
if latency >= 1:
# by default, latency in millisecs
qosRule['latencyGoal'] = int(latency)
else:
# latency < 1 Eg. 0.1, 0.02, etc
# convert latency to microsecs
qosRule['latencyGoaluSecs'] = int(latency * 1000)
if priority:
qosRule['priority'] = (
self.qos_priority_level.get(priority.lower()))
try:
self.client.createQoSRules(vvs_name, qosRule)
@@ -2659,8 +2708,10 @@ class HPE3PARCommon(object):
LOG.info("array version: %(ver)s",
{'ver': self.API_VERSION})
comment_line = None
if self.API_VERSION >= 40600000:
# comment can be added
if self.API_VERSION >= 40600000 and \
self.API_VERSION < API_VERSION_2023:
# comment added for online copy only for
# Alletra9k version
comments = {'volume_id': volume['id'],
'name': volume['name'],
'type': 'OpenStack'}

View File

@@ -0,0 +1,7 @@
---
fixes:
- |
HPE driver `Bug #2143385 <https://bugs.launchpad.net/cinder/+bug/2143385>`_:
Fixed: Handled deprecation of QOS parameters and comments for
online copy. While passing QOS params use units.k(1000) as multiplier,
as backend is expecting values in KB.