Add QoS Suport for Pure Storage

Add back-end QoS support for Pure Storage.
Two parameters are supported:
maxIOPS - maximum IOPS per volume - range 100 -100M
maxBWS - maximum I/O bandwidth in MB/s - range 1 - 524288

DocImpact
Implements: blueprint pure-backend-qos

Change-Id: I7f548b1aa1285499b5835fc2ebba3b6e55d8fb15
This commit is contained in:
Simon Dodsley 2020-05-12 12:16:46 -04:00
parent 23bb3d332a
commit 79b2a4f461
5 changed files with 384 additions and 14 deletions

View File

@ -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_snapshot
from cinder.tests.unit import fake_volume from cinder.tests.unit import fake_volume
from cinder.tests.unit import test from cinder.tests.unit import test
from cinder.volume import qos_specs
from cinder.volume import volume_types
from cinder.volume import volume_utils from cinder.volume import volume_utils
@ -498,6 +500,14 @@ MANAGEABLE_PURE_SNAP_REFS = [
] ]
MAX_SNAP_LENGTH = 96 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): class FakePureStorageHTTPError(Exception):
def __init__(self, target=None, rest_version=None, code=None, 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' self.async_array2.get_rest_version.return_value = '1.4'
def new_fake_vol(self, set_provider_id=True, fake_context=None, 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: if fake_context is None:
fake_context = mock.MagicMock() fake_context = mock.MagicMock()
if type_extra_specs is None: if type_extra_specs is None:
@ -591,6 +602,8 @@ class PureBaseSharedDriverTestCase(PureDriverTestCase):
voltype = fake_volume.fake_volume_type_obj(fake_context) voltype = fake_volume.fake_volume_type_obj(fake_context)
voltype.extra_specs = type_extra_specs 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) 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 + "._add_to_group_if_needed")
@mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type") @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): mock_add_to_group):
srcvol, _ = self.new_fake_vol() srcvol, _ = self.new_fake_vol()
snap = fake_snapshot.fake_snapshot_obj(mock.MagicMock(), volume=srcvol) snap = fake_snapshot.fake_snapshot_obj(mock.MagicMock(), volume=srcvol)
@ -987,7 +1002,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
mock_get_replicated_type.return_value = None mock_get_replicated_type.return_value = None
vol, vol_name = self.new_fake_vol(set_provider_id=False) vol, vol_name = self.new_fake_vol(set_provider_id=False)
mock_get_volume_type.return_value = vol.volume_type
# Branch where extend unneeded # Branch where extend unneeded
self.driver.create_volume_from_snapshot(vol, snap) self.driver.create_volume_from_snapshot(vol, snap)
self.array.copy_volume.assert_called_with(snap_name, vol_name) 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 + "._add_to_group_if_needed")
@mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type") @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, def test_create_volume_from_snapshot_with_extend(self,
mock_get_volume_type,
mock_get_replicated_type, mock_get_replicated_type,
mock_add_to_group): mock_add_to_group):
srcvol, srcvol_name = self.new_fake_vol(spec={"size": 1}) 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, vol, vol_name = self.new_fake_vol(set_provider_id=False,
spec={"size": 2}) spec={"size": 2})
mock_get_volume_type.return_value = vol.volume_type
self.driver.create_volume_from_snapshot(vol, snap) self.driver.create_volume_from_snapshot(vol, snap)
expected = [mock.call.copy_volume(snap_name, vol_name), expected = [mock.call.copy_volume(snap_name, vol_name),
@ -1017,7 +1035,8 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
self.array.assert_has_calls(expected) self.array.assert_has_calls(expected)
mock_add_to_group.assert_called_once_with(vol, vol_name) 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 = { repl_extra_specs = {
'replication_type': '<in> async', 'replication_type': '<in> async',
'replication_enabled': '<is> true', 'replication_enabled': '<is> true',
@ -1027,6 +1046,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
vol, vol_name = self.new_fake_vol(set_provider_id=False, vol, vol_name = self.new_fake_vol(set_provider_id=False,
type_extra_specs=repl_extra_specs) type_extra_specs=repl_extra_specs)
mock_get_volume_type.return_value = vol.volume_type
self.driver.create_volume_from_snapshot(vol, snap) self.driver.create_volume_from_snapshot(vol, snap)
self.array.copy_volume.assert_called_with(snap_name, vol_name) 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 + "._extend_if_needed", autospec=True)
@mock.patch(BASE_DRIVER_OBJ + "._get_pgroup_snap_name_from_snapshot") @mock.patch(BASE_DRIVER_OBJ + "._get_pgroup_snap_name_from_snapshot")
@mock.patch(BASE_DRIVER_OBJ + "._get_replication_type_from_vol_type") @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_get_snap_name,
mock_extend_if_needed, mock_extend_if_needed,
mock_add_to_group): mock_add_to_group):
@ -1042,6 +1064,7 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
cgsnap = fake_group_snapshot.fake_group_snapshot_obj(mock.MagicMock(), cgsnap = fake_group_snapshot.fake_group_snapshot_obj(mock.MagicMock(),
group=cgroup) group=cgroup)
vol, vol_name = self.new_fake_vol(spec={"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 = fake_snapshot.fake_snapshot_obj(mock.MagicMock(), volume=vol)
snap.group_snapshot_id = cgsnap.id snap.group_snapshot_id = cgsnap.id
snap.group_snapshot = cgsnap snap.group_snapshot = cgsnap
@ -2650,13 +2673,15 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
"some_pgroup", "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 = { repl_extra_specs = {
'replication_type': '<in> async', 'replication_type': '<in> async',
'replication_enabled': '<is> true', 'replication_enabled': '<is> true',
} }
vol, vol_name = self.new_fake_vol(spec={"size": 2}, vol, vol_name = self.new_fake_vol(spec={"size": 2},
type_extra_specs=repl_extra_specs) type_extra_specs=repl_extra_specs)
mock_get_volume_type.return_value = vol.volume_type
self.driver.create_volume(vol) self.driver.create_volume(vol)
@ -2666,7 +2691,8 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
REPLICATION_PROTECTION_GROUP, REPLICATION_PROTECTION_GROUP,
addvollist=[vol["name"] + "-cinder"]) 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 = { repl_extra_specs = {
'replication_type': '<in> sync', 'replication_type': '<in> sync',
'replication_enabled': '<is> true', 'replication_enabled': '<is> true',
@ -2674,6 +2700,8 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
vol, vol_name = self.new_fake_vol(spec={"size": 2}, vol, vol_name = self.new_fake_vol(spec={"size": 2},
type_extra_specs=repl_extra_specs) type_extra_specs=repl_extra_specs)
mock_get_volume_type.return_value = vol.volume_type
self.driver.create_volume(vol) self.driver.create_volume(vol)
self.array.create_volume.assert_called_with( self.array.create_volume.assert_called_with(
@ -3083,6 +3111,191 @@ class PureBaseVolumeDriverTestCase(PureBaseSharedDriverTestCase):
expected_wwn = '3624a93709714b5cb91634c470002b2c8' expected_wwn = '3624a93709714b5cb91634c470002b2c8'
self.assertEqual(expected_wwn, returned_wwn) 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): class PureISCSIDriverTestCase(PureBaseSharedDriverTestCase):
@ -3809,7 +4022,7 @@ class PureVolumeUpdateStatsTestCase(PureBaseSharedDriverTestCase):
'consistencygroup_support': True, 'consistencygroup_support': True,
'thin_provisioning_support': True, 'thin_provisioning_support': True,
'multiattach': True, 'multiattach': True,
'QoS_support': False, 'QoS_support': True,
'total_capacity_gb': TOTAL_CAPACITY, 'total_capacity_gb': TOTAL_CAPACITY,
'free_capacity_gb': TOTAL_CAPACITY - USED_SPACE, 'free_capacity_gb': TOTAL_CAPACITY - USED_SPACE,
'reserved_percentage': reserved_percentage, 'reserved_percentage': reserved_percentage,

View File

@ -35,6 +35,7 @@ try:
except ImportError: except ImportError:
purestorage = None purestorage = None
from cinder import context
from cinder import exception from cinder import exception
from cinder.i18n import _ from cinder.i18n import _
from cinder import interface from cinder import interface
@ -44,6 +45,8 @@ from cinder import utils
from cinder.volume import configuration from cinder.volume import configuration
from cinder.volume import driver from cinder.volume import driver
from cinder.volume.drivers.san import san 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.volume import volume_utils
from cinder.zonemanager import utils as fczm_utils from cinder.zonemanager import utils as fczm_utils
@ -128,7 +131,8 @@ EXTRA_SPECS_REPL_TYPE = "replication_type"
MAX_VOL_LENGTH = 63 MAX_VOL_LENGTH = 63
MAX_SNAP_LENGTH = 96 MAX_SNAP_LENGTH = 96
UNMANAGED_SUFFIX = '-unmanaged' 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 = [ ASYNC_REPLICATION_REQUIRED_API_VERSIONS = [
'1.3', '1.4', '1.5'] + SYNC_REPLICATION_REQUIRED_API_VERSIONS '1.3', '1.4', '1.5'] + SYNC_REPLICATION_REQUIRED_API_VERSIONS
MANAGE_SNAP_REQUIRED_API_VERSIONS = [ MANAGE_SNAP_REQUIRED_API_VERSIONS = [
@ -187,10 +191,11 @@ def pure_driver_debug_trace(f):
class PureBaseVolumeDriver(san.SanDriver): class PureBaseVolumeDriver(san.SanDriver):
"""Performs volume management on Pure Storage FlashArray.""" """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 SUPPORTS_ACTIVE_ACTIVE = True
PURE_QOS_KEYS = ['maxIOPS', 'maxBWS']
# ThirdPartySystems wiki page # ThirdPartySystems wiki page
CI_WIKI_NAME = "Pure_Storage_CI" CI_WIKI_NAME = "Pure_Storage_CI"
@ -315,6 +320,48 @@ class PureBaseVolumeDriver(san.SanDriver):
self._uniform_active_cluster_target_arrays.append( self._uniform_active_cluster_target_arrays.append(
target_array) 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): def do_setup(self, context):
"""Performs driver initialization steps that could raise exceptions.""" """Performs driver initialization steps that could raise exceptions."""
if purestorage is None: if purestorage is None:
@ -433,16 +480,27 @@ class PureBaseVolumeDriver(san.SanDriver):
@pure_driver_debug_trace @pure_driver_debug_trace
def create_volume(self, volume): def create_volume(self, volume):
"""Creates a volume.""" """Creates a volume."""
qos = None
vol_name = self._generate_purity_vol_name(volume) vol_name = self._generate_purity_vol_name(volume)
vol_size = volume["size"] * units.Gi 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 = 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) return self._setup_volume(current_array, volume, vol_name)
@pure_driver_debug_trace @pure_driver_debug_trace
def create_volume_from_snapshot(self, volume, snapshot): def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot.""" """Creates a volume from a snapshot."""
qos = None
vol_name = self._generate_purity_vol_name(volume) vol_name = self._generate_purity_vol_name(volume)
if snapshot['group_snapshot'] or snapshot['cgsnapshot']: if snapshot['group_snapshot'] or snapshot['cgsnapshot']:
snap_name = self._get_pgroup_snap_name_from_snapshot(snapshot) 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) snap_name = self._get_snap_name(snapshot)
current_array = self._get_current_array() 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) current_array.copy_volume(snap_name, vol_name)
self._extend_if_needed(current_array, self._extend_if_needed(current_array,
vol_name, vol_name,
snapshot["volume_size"], snapshot["volume_size"],
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) return self._setup_volume(current_array, volume, vol_name)
def _setup_volume(self, array, volume, purity_vol_name): def _setup_volume(self, array, volume, purity_vol_name):
@ -773,7 +844,7 @@ class PureBaseVolumeDriver(san.SanDriver):
data['consistencygroup_support'] = True data['consistencygroup_support'] = True
data['thin_provisioning_support'] = True data['thin_provisioning_support'] = True
data['multiattach'] = True data['multiattach'] = True
data['QoS_support'] = False data['QoS_support'] = True
# Add capacity info for scheduler # Add capacity info for scheduler
data['total_capacity_gb'] = total_capacity data['total_capacity_gb'] = total_capacity
@ -1224,6 +1295,17 @@ class PureBaseVolumeDriver(san.SanDriver):
self._rename_volume_object(ref_vol_name, self._rename_volume_object(ref_vol_name,
new_vol_name, new_vol_name,
raise_not_exist=True) 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 volume.provider_id = new_vol_name
async_enabled = self._enable_async_replication_if_needed(current_array, async_enabled = self._enable_async_replication_if_needed(current_array,
volume) volume)
@ -1559,6 +1641,47 @@ class PureBaseVolumeDriver(san.SanDriver):
return REPLICATION_TYPE_ASYNC return REPLICATION_TYPE_ASYNC
return None 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): def _generate_purity_vol_name(self, volume):
"""Return the name of the volume Purity will use. """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. 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 # 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 = volume_type.VolumeType.get_by_name_or_id(context,
new_type['id']) 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 # 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 # manager that it should do a migration for this retype
return False, None 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 return True, model_update
@pure_driver_debug_trace @pure_driver_debug_trace

View File

@ -48,6 +48,20 @@ Supported operations
* Replicate volumes to remote Pure Storage array(s). * 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
<https://docs.openstack.org/python-openstackclient/latest/cli/command-objects/volume-qos.html>`_
section in the OpenStack Client command list.
Configure OpenStack and Purity Configure OpenStack and Purity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -470,7 +470,7 @@ driver.nexenta=missing
driver.nfs=missing driver.nfs=missing
driver.nimble=missing driver.nimble=missing
driver.prophetstor=missing driver.prophetstor=missing
driver.pure=missing driver.pure=complete
driver.qnap=missing driver.qnap=missing
driver.quobyte=missing driver.quobyte=missing
driver.rbd=missing driver.rbd=missing

View File

@ -0,0 +1,4 @@
---
features:
- Added support for QoS in the Pure Storage drivers.
QoS support is available from Purity//FA 5.3.0