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:
parent
23bb3d332a
commit
79b2a4f461
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added support for QoS in the Pure Storage drivers.
|
||||||
|
QoS support is available from Purity//FA 5.3.0
|
Loading…
Reference in New Issue