diff --git a/cinder/tests/unit/volume/drivers/test_pure.py b/cinder/tests/unit/volume/drivers/test_pure.py index 3823e5e6eab..2da844a2186 100644 --- a/cinder/tests/unit/volume/drivers/test_pure.py +++ b/cinder/tests/unit/volume/drivers/test_pure.py @@ -30,6 +30,8 @@ from cinder.tests.unit import fake_group_snapshot from cinder.tests.unit import fake_snapshot from cinder.tests.unit import fake_volume from cinder.tests.unit import test +from cinder.volume import qos_specs +from cinder.volume import volume_types from cinder.volume import volume_utils @@ -498,6 +500,14 @@ MANAGEABLE_PURE_SNAP_REFS = [ ] MAX_SNAP_LENGTH = 96 +# 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 = {"maxIOPS": "100"} +QOS_BWS = {"maxBWS": "1"} + class FakePureStorageHTTPError(Exception): def __init__(self, target=None, rest_version=None, code=None, @@ -581,7 +591,8 @@ class PureBaseSharedDriverTestCase(PureDriverTestCase): self.async_array2.get_rest_version.return_value = '1.4' def new_fake_vol(self, set_provider_id=True, fake_context=None, - spec=None, type_extra_specs=None): + spec=None, type_extra_specs=None, type_qos_specs_id=None, + type_qos_specs=None): if fake_context is None: fake_context = mock.MagicMock() if type_extra_specs is None: @@ -591,6 +602,8 @@ class PureBaseSharedDriverTestCase(PureDriverTestCase): voltype = fake_volume.fake_volume_type_obj(fake_context) voltype.extra_specs = type_extra_specs + voltype.qos_specs_id = type_qos_specs_id + voltype.qos_specs = type_qos_specs vol = fake_volume.fake_volume_obj(fake_context, **spec) @@ -979,7 +992,9 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): @mock.patch(BASE_DRIVER_OBJ + "._add_to_group_if_needed") @mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type") - def test_create_volume_from_snapshot(self, mock_get_replicated_type, + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_from_snapshot(self, mock_get_volume_type, + mock_get_replicated_type, mock_add_to_group): srcvol, _ = self.new_fake_vol() snap = fake_snapshot.fake_snapshot_obj(mock.MagicMock(), volume=srcvol) @@ -987,7 +1002,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): mock_get_replicated_type.return_value = None vol, vol_name = self.new_fake_vol(set_provider_id=False) - + mock_get_volume_type.return_value = vol.volume_type # Branch where extend unneeded self.driver.create_volume_from_snapshot(vol, snap) self.array.copy_volume.assert_called_with(snap_name, vol_name) @@ -1000,7 +1015,9 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): @mock.patch(BASE_DRIVER_OBJ + "._add_to_group_if_needed") @mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type") + @mock.patch.object(volume_types, 'get_volume_type') def test_create_volume_from_snapshot_with_extend(self, + mock_get_volume_type, mock_get_replicated_type, mock_add_to_group): srcvol, srcvol_name = self.new_fake_vol(spec={"size": 1}) @@ -1010,6 +1027,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): vol, vol_name = self.new_fake_vol(set_provider_id=False, spec={"size": 2}) + mock_get_volume_type.return_value = vol.volume_type self.driver.create_volume_from_snapshot(vol, snap) expected = [mock.call.copy_volume(snap_name, vol_name), @@ -1017,7 +1035,8 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): self.array.assert_has_calls(expected) mock_add_to_group.assert_called_once_with(vol, vol_name) - def test_create_volume_from_snapshot_sync(self): + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_from_snapshot_sync(self, mock_get_volume_type): repl_extra_specs = { 'replication_type': ' async', 'replication_enabled': ' true', @@ -1027,6 +1046,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): 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 self.driver.create_volume_from_snapshot(vol, snap) self.array.copy_volume.assert_called_with(snap_name, vol_name) @@ -1034,7 +1054,9 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): @mock.patch(BASE_DRIVER_OBJ + "._extend_if_needed", autospec=True) @mock.patch(BASE_DRIVER_OBJ + "._get_pgroup_snap_name_from_snapshot") @mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type") - def test_create_volume_from_cgsnapshot(self, mock_get_replicated_type, + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_from_cgsnapshot(self, mock_get_volume_type, + mock_get_replicated_type, mock_get_snap_name, mock_extend_if_needed, mock_add_to_group): @@ -1042,6 +1064,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): cgsnap = fake_group_snapshot.fake_group_snapshot_obj(mock.MagicMock(), group=cgroup) vol, vol_name = self.new_fake_vol(spec={"group": cgroup}) + mock_get_volume_type.return_value = vol.volume_type snap = fake_snapshot.fake_snapshot_obj(mock.MagicMock(), volume=vol) snap.group_snapshot_id = cgsnap.id snap.group_snapshot = cgsnap @@ -2650,13 +2673,15 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): "some_pgroup", ) - def test_create_volume_replicated_async(self): + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_replicated_async(self, mock_get_volume_type): repl_extra_specs = { 'replication_type': ' async', 'replication_enabled': ' true', } vol, vol_name = self.new_fake_vol(spec={"size": 2}, type_extra_specs=repl_extra_specs) + mock_get_volume_type.return_value = vol.volume_type self.driver.create_volume(vol) @@ -2666,7 +2691,8 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): REPLICATION_PROTECTION_GROUP, addvollist=[vol["name"] + "-cinder"]) - def test_create_volume_replicated_sync(self): + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_replicated_sync(self, mock_get_volume_type): repl_extra_specs = { 'replication_type': ' sync', 'replication_enabled': ' true', @@ -2674,6 +2700,8 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): vol, vol_name = self.new_fake_vol(spec={"size": 2}, type_extra_specs=repl_extra_specs) + mock_get_volume_type.return_value = vol.volume_type + self.driver.create_volume(vol) self.array.create_volume.assert_called_with( @@ -3083,6 +3111,191 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase): expected_wwn = '3624a93709714b5cb91634c470002b2c8' self.assertEqual(expected_wwn, returned_wwn) + @mock.patch.object(qos_specs, "get_qos_specs") + def test_get_qos_settings_from_specs_id(self, mock_get_qos_specs): + qos = qos_specs.create(mock.MagicMock(), "qos-iops-bws", QOS_IOPS_BWS) + mock_get_qos_specs.return_value = qos + + voltype = fake_volume.fake_volume_type_obj(mock.MagicMock()) + voltype.qos_specs_id = qos.id + voltype.extra_specs = QOS_IOPS_BWS_2 # test override extra_specs + + specs = self.driver._get_qos_settings(voltype) + self.assertEqual(specs["maxIOPS"], + int(QOS_IOPS_BWS["maxIOPS"])) + self.assertEqual(specs["maxBWS"], + int(QOS_IOPS_BWS["maxBWS"]) * 1024 * 1024) + + def test_get_qos_settings_from_extra_specs(self): + voltype = fake_volume.fake_volume_type_obj(mock.MagicMock()) + voltype.extra_specs = QOS_IOPS_BWS + + specs = self.driver._get_qos_settings(voltype) + self.assertEqual(specs["maxIOPS"], + int(QOS_IOPS_BWS["maxIOPS"])) + self.assertEqual(specs["maxBWS"], + int(QOS_IOPS_BWS["maxBWS"]) * 1024 * 1024) + + def test_get_qos_settings_set_zeros(self): + voltype = fake_volume.fake_volume_type_obj(mock.MagicMock()) + voltype.extra_specs = QOS_ZEROS + specs = self.driver._get_qos_settings(voltype) + self.assertEqual(specs["maxIOPS"], 0) + self.assertEqual(specs["maxBWS"], 0) + + def test_get_qos_settings_set_one(self): + voltype = fake_volume.fake_volume_type_obj(mock.MagicMock()) + voltype.extra_specs = QOS_IOPS + specs = self.driver._get_qos_settings(voltype) + self.assertEqual(specs["maxIOPS"], int(QOS_IOPS["maxIOPS"])) + self.assertEqual(specs["maxBWS"], 0) + + voltype.extra_specs = QOS_BWS + specs = self.driver._get_qos_settings(voltype) + self.assertEqual(specs["maxIOPS"], 0) + self.assertEqual(specs["maxBWS"], + int(QOS_BWS["maxBWS"]) * 1024 * 1024) + + def test_get_qos_settings_invalid(self): + voltype = fake_volume.fake_volume_type_obj(mock.MagicMock()) + voltype.extra_specs = QOS_INVALID + self.assertRaises(exception.InvalidQoSSpecs, + self.driver._get_qos_settings, + voltype) + + @mock.patch(BASE_DRIVER_OBJ + "._add_to_group_if_needed") + @mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type") + @mock.patch.object(qos_specs, "get_qos_specs") + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_with_qos(self, mock_get_volume_type, + mock_get_qos_specs, + mock_get_repl_type, + mock_add_to_group): + qos = qos_specs.create(mock.MagicMock(), "qos-iops-bws", QOS_IOPS_BWS) + vol, vol_name = self.new_fake_vol(spec={"size": 1}, + type_qos_specs_id=qos.id) + + mock_get_volume_type.return_value = vol.volume_type + self.array.get_rest_version.return_value = '1.17' + mock_get_qos_specs.return_value = qos + mock_get_repl_type.return_value = None + + self.driver.create_volume(vol) + self.array.create_volume.assert_called_with( + vol_name, 1 * units.Gi, + iops_limit=int(QOS_IOPS_BWS["maxIOPS"]), + bandwidth_limit=int(QOS_IOPS_BWS["maxBWS"]) * 1024 * 1024) + mock_add_to_group.assert_called_once_with(vol, + vol_name) + self.assert_error_propagates([self.array.create_volume], + self.driver.create_volume, vol) + + @mock.patch(BASE_DRIVER_OBJ + "._add_to_group_if_needed") + @mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type") + @mock.patch.object(qos_specs, "get_qos_specs") + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_from_snapshot_with_qos(self, mock_get_volume_type, + mock_get_qos_specs, + mock_get_repl_type, + mock_add_to_group): + srcvol, _ = self.new_fake_vol() + snap = fake_snapshot.fake_snapshot_obj(mock.MagicMock(), volume=srcvol) + snap_name = snap["volume_name"] + "-cinder." + snap["name"] + qos = qos_specs.create(mock.MagicMock(), "qos-iops-bws", QOS_IOPS_BWS) + vol, vol_name = self.new_fake_vol(set_provider_id=False, + type_qos_specs_id=qos.id) + + mock_get_volume_type.return_value = vol.volume_type + self.array.get_rest_version.return_value = '1.17' + mock_get_qos_specs.return_value = qos + mock_get_repl_type.return_value = None + + self.driver.create_volume_from_snapshot(vol, snap) + self.array.copy_volume.assert_called_with(snap_name, vol_name) + self.array.set_volume.assert_called_with( + vol_name, + iops_limit=int(QOS_IOPS_BWS["maxIOPS"]), + bandwidth_limit=int(QOS_IOPS_BWS["maxBWS"]) * 1024 * 1024) + self.assertFalse(self.array.extend_volume.called) + mock_add_to_group.assert_called_once_with(vol, vol_name) + self.assert_error_propagates( + [self.array.copy_volume], + self.driver.create_volume_from_snapshot, vol, snap) + self.assertFalse(self.array.extend_volume.called) + + @mock.patch.object(qos_specs, "get_qos_specs") + @mock.patch.object(volume_types, 'get_volume_type') + def test_manage_existing_with_qos(self, mock_get_volume_type, + mock_get_qos_specs): + ref_name = 'vol1' + volume_ref = {'name': ref_name} + qos = qos_specs.create(mock.MagicMock(), "qos-iops-bws", QOS_IOPS_BWS) + vol, vol_name = self.new_fake_vol(set_provider_id=False, + type_qos_specs_id=qos.id) + + mock_get_volume_type.return_value = vol.volume_type + mock_get_qos_specs.return_value = qos + self.array.list_volume_private_connections.return_value = [] + self.array.get_rest_version.return_value = '1.17' + + self.driver.manage_existing(vol, volume_ref) + self.array.list_volume_private_connections.assert_called_with(ref_name) + self.array.rename_volume.assert_called_with(ref_name, vol_name) + self.array.set_volume.assert_called_with( + vol_name, + iops_limit=int(QOS_IOPS_BWS["maxIOPS"]), + bandwidth_limit=int(QOS_IOPS_BWS["maxBWS"]) * 1024 * 1024) + + def test_retype_qos(self): + mock_context = mock.MagicMock() + vol, vol_name = self.new_fake_vol() + qos = qos_specs.create(mock.MagicMock(), "qos-iops-bws", QOS_IOPS_BWS) + new_type = fake_volume.fake_volume_type_obj(mock_context) + new_type.qos_specs_id = qos.id + + self.array.get_rest_version.return_value = '1.17' + get_voltype = "cinder.objects.volume_type.VolumeType.get_by_name_or_id" + with mock.patch(get_voltype) as mock_get_vol_type: + mock_get_vol_type.return_value = new_type + did_retype, model_update = self.driver.retype( + mock_context, + vol, + new_type, + None, # ignored by driver + None, # ignored by driver + ) + + self.array.set_volume.assert_called_with( + vol_name, + iops_limit=int(QOS_IOPS_BWS["maxIOPS"]), + bandwidth_limit=int(QOS_IOPS_BWS["maxBWS"]) * 1024 * 1024) + self.assertTrue(did_retype) + self.assertIsNone(model_update) + + def test_retype_qos_reset_iops(self): + mock_context = mock.MagicMock() + vol, vol_name = self.new_fake_vol() + new_type = fake_volume.fake_volume_type_obj(mock_context) + + self.array.get_rest_version.return_value = '1.17' + get_voltype = "cinder.objects.volume_type.VolumeType.get_by_name_or_id" + with mock.patch(get_voltype) as mock_get_vol_type: + mock_get_vol_type.return_value = new_type + did_retype, model_update = self.driver.retype( + mock_context, + vol, + new_type, + None, # ignored by driver + None, # ignored by driver + ) + + self.array.set_volume.assert_called_with( + vol_name, + iops_limit="", + bandwidth_limit="") + self.assertTrue(did_retype) + self.assertIsNone(model_update) + class PureISCSIDriverTestCase(PureBaseSharedDriverTestCase): @@ -3809,7 +4022,7 @@ class PureVolumeUpdateStatsTestCase(PureBaseSharedDriverTestCase): 'consistencygroup_support': True, 'thin_provisioning_support': True, 'multiattach': True, - 'QoS_support': False, + 'QoS_support': True, 'total_capacity_gb': TOTAL_CAPACITY, 'free_capacity_gb': TOTAL_CAPACITY - USED_SPACE, 'reserved_percentage': reserved_percentage, diff --git a/cinder/volume/drivers/pure.py b/cinder/volume/drivers/pure.py index 757599ac2e0..a5d848e1be3 100644 --- a/cinder/volume/drivers/pure.py +++ b/cinder/volume/drivers/pure.py @@ -35,6 +35,7 @@ try: except ImportError: purestorage = None +from cinder import context from cinder import exception from cinder.i18n import _ from cinder import interface @@ -44,6 +45,8 @@ from cinder import utils from cinder.volume import configuration from cinder.volume import driver from cinder.volume.drivers.san import san +from cinder.volume import qos_specs +from cinder.volume import volume_types from cinder.volume import volume_utils from cinder.zonemanager import utils as fczm_utils @@ -128,7 +131,8 @@ EXTRA_SPECS_REPL_TYPE = "replication_type" MAX_VOL_LENGTH = 63 MAX_SNAP_LENGTH = 96 UNMANAGED_SUFFIX = '-unmanaged' -SYNC_REPLICATION_REQUIRED_API_VERSIONS = ['1.13', '1.14'] +QOS_REQUIRED_API_VERSION = ['1.17'] +SYNC_REPLICATION_REQUIRED_API_VERSIONS = ['1.13', '1.14', '1.17'] ASYNC_REPLICATION_REQUIRED_API_VERSIONS = [ '1.3', '1.4', '1.5'] + SYNC_REPLICATION_REQUIRED_API_VERSIONS MANAGE_SNAP_REQUIRED_API_VERSIONS = [ @@ -187,10 +191,11 @@ def pure_driver_debug_trace(f): class PureBaseVolumeDriver(san.SanDriver): """Performs volume management on Pure Storage FlashArray.""" - SUPPORTED_REST_API_VERSIONS = ['1.2', '1.3', '1.4', '1.5', '1.13', '1.14'] + SUPPORTED_REST_API_VERSIONS = ['1.2', '1.3', '1.4', '1.5', + '1.13', '1.14', '1.17'] SUPPORTS_ACTIVE_ACTIVE = True - + PURE_QOS_KEYS = ['maxIOPS', 'maxBWS'] # ThirdPartySystems wiki page CI_WIKI_NAME = "Pure_Storage_CI" @@ -315,6 +320,48 @@ class PureBaseVolumeDriver(san.SanDriver): self._uniform_active_cluster_target_arrays.append( target_array) + @pure_driver_debug_trace + def set_qos(self, array, vol_name, qos): + LOG.debug('QoS: %(qos)s', {'qos': qos}) + if qos['maxIOPS'] == '0' and qos['maxBWS'] == 0: + array.set_volume(vol_name, + iops_limit='', + bandwidth_limit='') + elif qos['maxIOPS'] == 0: + array.set_volume(vol_name, + iops_limit='', + bandwidth_limit=qos['maxBWS']) + elif qos['maxBWS'] == 0: + array.set_volume(vol_name, + iops_limit=qos['maxIOPS'], + bandwidth_limit='') + else: + array.set_volume(vol_name, + iops_limit=qos['maxIOPS'], + bandwidth_limit=qos['maxBWS']) + return + + @pure_driver_debug_trace + def create_with_qos(self, array, vol_name, vol_size, qos): + LOG.debug('QoS: %(qos)s', {'qos': qos}) + if qos['maxIOPS'] == 0 and qos['maxBWS'] == 0: + array.create_volume(vol_name, vol_size, + iops_limit='', + bandwidth_limit='') + elif qos['maxIOPS'] == 0: + array.create_volume(vol_name, vol_size, + iops_limit='', + bandwidth_limit=qos['maxBWS']) + elif qos['maxBWS'] == 0: + array.create_volume(vol_name, vol_size, + iops_limit=qos['maxIOPS'], + bandwidth_limit='') + else: + array.create_volume(vol_name, vol_size, + iops_limit=qos['maxIOPS'], + bandwidth_limit=qos['maxBWS']) + return + def do_setup(self, context): """Performs driver initialization steps that could raise exceptions.""" if purestorage is None: @@ -433,16 +480,27 @@ class PureBaseVolumeDriver(san.SanDriver): @pure_driver_debug_trace def create_volume(self, volume): """Creates a volume.""" + qos = None vol_name = self._generate_purity_vol_name(volume) vol_size = volume["size"] * units.Gi + ctxt = context.get_admin_context() + type_id = volume.get('volume_type_id') current_array = self._get_current_array() - current_array.create_volume(vol_name, vol_size) + if type_id is not None: + volume_type = volume_types.get_volume_type(ctxt, type_id) + if (current_array.get_rest_version() in QOS_REQUIRED_API_VERSION): + qos = self._get_qos_settings(volume_type) + if qos is not None: + self.create_with_qos(current_array, vol_name, vol_size, qos) + else: + current_array.create_volume(vol_name, vol_size) return self._setup_volume(current_array, volume, vol_name) @pure_driver_debug_trace def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot.""" + qos = None vol_name = self._generate_purity_vol_name(volume) if snapshot['group_snapshot'] or snapshot['cgsnapshot']: snap_name = self._get_pgroup_snap_name_from_snapshot(snapshot) @@ -450,12 +508,25 @@ class PureBaseVolumeDriver(san.SanDriver): snap_name = self._get_snap_name(snapshot) current_array = self._get_current_array() + 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) + if (current_array.get_rest_version() in QOS_REQUIRED_API_VERSION): + qos = self._get_qos_settings(volume_type) current_array.copy_volume(snap_name, vol_name) self._extend_if_needed(current_array, vol_name, snapshot["volume_size"], volume["size"]) + if qos is not None: + self.set_qos(current_array, vol_name, qos) + else: + current_array.set_volume(vol_name, + iops_limit='', + bandwidth_limit='') + return self._setup_volume(current_array, volume, vol_name) def _setup_volume(self, array, volume, purity_vol_name): @@ -773,7 +844,7 @@ class PureBaseVolumeDriver(san.SanDriver): data['consistencygroup_support'] = True data['thin_provisioning_support'] = True data['multiattach'] = True - data['QoS_support'] = False + data['QoS_support'] = True # Add capacity info for scheduler data['total_capacity_gb'] = total_capacity @@ -1224,6 +1295,17 @@ class PureBaseVolumeDriver(san.SanDriver): self._rename_volume_object(ref_vol_name, new_vol_name, raise_not_exist=True) + # Check if the volume_type has QoS settings and if so + # apply them to the newly managed volume + qos = None + if (current_array.get_rest_version() in QOS_REQUIRED_API_VERSION): + qos = self._get_qos_settings(volume.volume_type) + if qos is not None: + self.set_qos(current_array, new_vol_name, qos) + else: + current_array.set_volume(new_vol_name, + iops_limit='', + bandwidth_limit='') volume.provider_id = new_vol_name async_enabled = self._enable_async_replication_if_needed(current_array, volume) @@ -1559,6 +1641,47 @@ class PureBaseVolumeDriver(san.SanDriver): return REPLICATION_TYPE_ASYNC return None + def _get_qos_settings(self, volume_type): + """Get extra_specs and qos_specs of a volume_type. + + This fetches the keys from the volume type. Anything set + from qos_specs will override keys set from extra_specs + """ + + # Deal with volume with no type + qos = {} + qos_specs_id = volume_type.get('qos_specs_id') + specs = volume_type.get('extra_specs') + # We prefer QoS specs associations to override + # any existing extra-specs settings + if qos_specs_id is not None: + ctxt = context.get_admin_context() + kvs = qos_specs.get_qos_specs(ctxt, qos_specs_id)['specs'] + else: + kvs = specs + + for key, value in kvs.items(): + if key in self.PURE_QOS_KEYS: + qos[key] = value + if qos == {}: + return None + else: + # Chack set vslues are within limits + iops_qos = int(qos.get('maxIOPS', 0)) + bw_qos = int(qos.get('maxBWS', 0)) * 1048576 + if iops_qos != 0 and not (100 <= iops_qos <= 100000000): + msg = _('maxIOPS QoS error. Must be more than ' + '100 and less than 100000000') + raise exception.InvalidQoSSpecs(message=msg) + if bw_qos != 0 and not (1048576 <= bw_qos <= 549755813888): + msg = _('maxBWS QoS error. Must be between ' + '1 and 524288') + raise exception.InvalidQoSSpecs(message=msg) + + qos['maxIOPS'] = iops_qos + qos['maxBWS'] = bw_qos + return qos + def _generate_purity_vol_name(self, volume): """Return the name of the volume Purity will use. @@ -1710,6 +1833,7 @@ class PureBaseVolumeDriver(san.SanDriver): replicated for async, or part of a pod for sync replication. """ + qos = None # TODO(patrickeast): Can remove this once new_type is a VolumeType OVO new_type = volume_type.VolumeType.get_by_name_or_id(context, new_type['id']) @@ -1765,6 +1889,21 @@ class PureBaseVolumeDriver(san.SanDriver): # We can't move a volume in or out of a pod, indicate to the # manager that it should do a migration for this retype return False, None + + # If we are moving to a volume type with QoS settings then + # make sure the volume gets the correct new QoS settings. + # This could mean removing existing QoS settings. + current_array = self._get_current_array() + if (current_array.get_rest_version() in QOS_REQUIRED_API_VERSION): + 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) + else: + current_array.set_volume(vol_name, + iops_limit='', + bandwidth_limit='') + return True, model_update @pure_driver_debug_trace diff --git a/doc/source/configuration/block-storage/drivers/pure-storage-driver.rst b/doc/source/configuration/block-storage/drivers/pure-storage-driver.rst index 3aa464f69a6..7153ecd606c 100644 --- a/doc/source/configuration/block-storage/drivers/pure-storage-driver.rst +++ b/doc/source/configuration/block-storage/drivers/pure-storage-driver.rst @@ -48,6 +48,20 @@ Supported operations * Replicate volumes to remote Pure Storage array(s). +QoS support for the Pure Storage drivers include the ability to set the +following capabilities in the OpenStack Block Storage API +``cinder.api.contrib.qos_spec_manage`` qos specs extension module: + +* **maxIOPS** - Maximum number of IOPs allowed for volume. Range: 100 - 100M + +* **maxBWS** - Maximum bandwidth limit in MB/s. Range: 1 - 524288 (512GB/s) + +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 +`_ +section in the OpenStack Client command list. + Configure OpenStack and Purity ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index 1729fb89377..3f5a6b8cd84 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -470,7 +470,7 @@ driver.nexenta=missing driver.nfs=missing driver.nimble=missing driver.prophetstor=missing -driver.pure=missing +driver.pure=complete driver.qnap=missing driver.quobyte=missing driver.rbd=missing diff --git a/releasenotes/notes/pure-storage-add-qos-37958a90beff12d6.yaml b/releasenotes/notes/pure-storage-add-qos-37958a90beff12d6.yaml new file mode 100644 index 00000000000..2a830b2a8fb --- /dev/null +++ b/releasenotes/notes/pure-storage-add-qos-37958a90beff12d6.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added support for QoS in the Pure Storage drivers. + QoS support is available from Purity//FA 5.3.0