Dell PowerStore driver: QoS support
Added QoS support for Dell PowerStore driver. Implements: blueprint dell-powerstore-qos Change-Id: Id8f9b78671047eb24a4d0fa3a55a4fdb584d71db
This commit is contained in:
@@ -18,8 +18,11 @@ import uuid
|
|||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
from cinder.tests.unit import test
|
from cinder.tests.unit import test
|
||||||
from cinder.tests.unit.volume.drivers.dell_emc.powerstore import MockResponse
|
from cinder.tests.unit.volume.drivers.dell_emc.powerstore import MockResponse
|
||||||
|
from cinder.volume.drivers.dell_emc.powerstore import (
|
||||||
|
exception as powerstore_exception)
|
||||||
from cinder.volume.drivers.dell_emc.powerstore import client
|
from cinder.volume.drivers.dell_emc.powerstore import client
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +60,26 @@ NVME_IP_POOL_RESP = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
QOS_IO_RULE_PARAMS = {
|
||||||
|
"name": "io-rule-6b6e5489-4b5b-4468-a1f7-32cec2ffa3bf",
|
||||||
|
"type": "Absolute",
|
||||||
|
"max_iops": "200",
|
||||||
|
"max_bw": "18000",
|
||||||
|
"burst_percentage": "50"
|
||||||
|
}
|
||||||
|
|
||||||
|
QOS_POLICY_PARAMS = {
|
||||||
|
"name": "qos-policy-6b6e5489-4b5b-4468-a1f7-32cec2ffa3bf",
|
||||||
|
"io_limit_rule_id": "9beb10ff-a00c-4d88-a7d9-692be2b3073f"
|
||||||
|
}
|
||||||
|
|
||||||
|
QOS_UPDATE_IO_RULE_PARAMS = {
|
||||||
|
"type": "Absolute",
|
||||||
|
"max_iops": "500",
|
||||||
|
"max_bw": "225000",
|
||||||
|
"burst_percentage": "89"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
class TestClient(test.TestCase):
|
class TestClient(test.TestCase):
|
||||||
@@ -100,3 +123,157 @@ class TestClient(test.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(self.client.get_array_version(),
|
self.assertEqual(self.client.get_array_version(),
|
||||||
"3.0.0.0")
|
"3.0.0.0")
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_get_qos_policy_id_by_name(self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(
|
||||||
|
content=[
|
||||||
|
{
|
||||||
|
"id": "d69f7131-4617-4bae-89f8-a540a6bda94b",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rc=200
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.client.get_qos_policy_id_by_name("qos-"
|
||||||
|
"policy-6b6e5489"
|
||||||
|
"-4b5b-4468-a1f7-"
|
||||||
|
"32cec2ffa3bf"),
|
||||||
|
"d69f7131-4617-4bae-89f8-a540a6bda94b")
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_get_qos_policy_id_by_name_exception(self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(rc=400)
|
||||||
|
self.assertRaises(
|
||||||
|
exception.VolumeBackendAPIException,
|
||||||
|
self.client.get_qos_policy_id_by_name,
|
||||||
|
"qos-policy-6b6e5489-4b5b-4468-a1f7-32cec2ffa3bf")
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_create_qos_io_rule(self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(
|
||||||
|
content={
|
||||||
|
"id": "9beb10ff-a00c-4d88-a7d9-692be2b3073f"
|
||||||
|
},
|
||||||
|
rc=200
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.client.create_qos_io_rule(QOS_IO_RULE_PARAMS),
|
||||||
|
"9beb10ff-a00c-4d88-a7d9-692be2b3073f")
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_create_duplicate_qos_io_rule(self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(
|
||||||
|
content={
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"code": "0xE0A0E0010009",
|
||||||
|
"severity": "Error",
|
||||||
|
"message_l10n": "The rule name "
|
||||||
|
"io-rule-9899a65f-70fe-46c9-8f6c-22625c7e19df "
|
||||||
|
"is already used by another rule. "
|
||||||
|
"It needs to be unique (case-insensitive). "
|
||||||
|
"Please use a different name.",
|
||||||
|
"arguments": [
|
||||||
|
"io-rule-6b6e5489-4b5b-4468-a1f7-32cec2ffa3bf"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
rc=400
|
||||||
|
)
|
||||||
|
self.assertRaises(
|
||||||
|
powerstore_exception.DellPowerStoreQoSIORuleExists,
|
||||||
|
self.client.create_qos_io_rule,
|
||||||
|
QOS_IO_RULE_PARAMS)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_create_duplicate_qos_io_rule_with_unexpected_error(
|
||||||
|
self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(
|
||||||
|
content={
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"code": "0xE0101001000C",
|
||||||
|
"severity": "Error",
|
||||||
|
"message_l10n": "The system encountered unexpected "
|
||||||
|
"backend errors. "
|
||||||
|
"Please contact support."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
rc=400
|
||||||
|
)
|
||||||
|
self.assertRaises(
|
||||||
|
powerstore_exception.DellPowerStoreQoSIORuleExists,
|
||||||
|
self.client.create_qos_io_rule,
|
||||||
|
QOS_IO_RULE_PARAMS)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_create_qos_policy(self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(
|
||||||
|
content={
|
||||||
|
"id": "d69f7131-4617-4bae-89f8-a540a6bda94b",
|
||||||
|
},
|
||||||
|
rc=200
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.client.create_qos_policy(QOS_POLICY_PARAMS),
|
||||||
|
"d69f7131-4617-4bae-89f8-a540a6bda94b")
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_create_duplicate_qos_policy(self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(
|
||||||
|
content={
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"code": "0xE02020010004",
|
||||||
|
"severity": "Error",
|
||||||
|
"message_l10n": "The new policy name qos-policy-"
|
||||||
|
"6b6e5489-4b5b-4468-a1f7-32cec2ffa3bf "
|
||||||
|
"is in use. It must be unique "
|
||||||
|
"regardless of character cases.",
|
||||||
|
"arguments": [
|
||||||
|
"qos-policy-6b6e5489-4b5b-4468-a1f7-32cec2ffa3bf"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
rc=400
|
||||||
|
)
|
||||||
|
self.assertRaises(
|
||||||
|
powerstore_exception.DellPowerStoreQoSPolicyExists,
|
||||||
|
self.client.create_qos_policy,
|
||||||
|
QOS_POLICY_PARAMS)
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_update_volume_with_qos_policy(self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(rc=200)
|
||||||
|
self.client.update_volume_with_qos_policy(
|
||||||
|
"fake_volume_id",
|
||||||
|
"qos-policy-6b6e5489-4b5b-4468-a1f7-32cec2ffa3bf")
|
||||||
|
mock_request.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_update_volume_with_qos_policy_exception(self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(rc=400)
|
||||||
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.client.update_volume_with_qos_policy,
|
||||||
|
"fake_volume_id",
|
||||||
|
"qos-policy-6b6e5489-4b5b-4468-a1f7-32cec2ffa3bf")
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_update_qos_io_rule(self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(rc=200)
|
||||||
|
self.client.update_qos_io_rule(
|
||||||
|
"io-rule-6b6e5489-4b5b-4468-a1f7-32cec2ffa3bf",
|
||||||
|
QOS_UPDATE_IO_RULE_PARAMS)
|
||||||
|
mock_request.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("requests.request")
|
||||||
|
def test_update_qos_io_rule_exception(self, mock_request):
|
||||||
|
mock_request.return_value = MockResponse(rc=400)
|
||||||
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.client.update_qos_io_rule,
|
||||||
|
"io-rule-6b6e5489-4b5b-4468-a1f7-32cec2ffa3bf",
|
||||||
|
QOS_UPDATE_IO_RULE_PARAMS)
|
||||||
|
@@ -22,8 +22,29 @@ from cinder.tests.unit import fake_volume
|
|||||||
from cinder.tests.unit.volume.drivers.dell_emc import powerstore
|
from cinder.tests.unit.volume.drivers.dell_emc import powerstore
|
||||||
from cinder.volume.drivers.dell_emc.powerstore import utils
|
from cinder.volume.drivers.dell_emc.powerstore import utils
|
||||||
|
|
||||||
|
FAKE_HOST = {
|
||||||
|
"name": "fake_host",
|
||||||
|
"id": "fake_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver):
|
class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver):
|
||||||
|
|
||||||
|
QOS_SPECS = {
|
||||||
|
'qos_specs': {
|
||||||
|
'name': 'powerstore_qos',
|
||||||
|
'id': 'd8c88f5a-4c6f-4f89-97c5-da1ef059006e',
|
||||||
|
'created_at': 'fake_date',
|
||||||
|
'consumer': 'back-end',
|
||||||
|
'specs': {
|
||||||
|
'max_bw': '104857600',
|
||||||
|
'max_iops': '500',
|
||||||
|
'bandwidth_limit_type': 'Absolute',
|
||||||
|
'burst_percentage': '50'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.get_chap_config")
|
"PowerStoreClient.get_chap_config")
|
||||||
def setUp(self, mock_chap):
|
def setUp(self, mock_chap):
|
||||||
@@ -41,7 +62,8 @@ class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver):
|
|||||||
self.context,
|
self.context,
|
||||||
host="host@backend",
|
host="host@backend",
|
||||||
provider_id="fake_id",
|
provider_id="fake_id",
|
||||||
size=8
|
size=8,
|
||||||
|
volume_type_id="fake_volume_type_id"
|
||||||
)
|
)
|
||||||
self.volume.volume_attachment = (
|
self.volume.volume_attachment = (
|
||||||
volume_attachment.VolumeAttachmentList()
|
volume_attachment.VolumeAttachmentList()
|
||||||
@@ -208,3 +230,126 @@ class TestVolumeAttachDetach(powerstore.TestPowerStoreDriver):
|
|||||||
self.fake_connector)
|
self.fake_connector)
|
||||||
mock_filter_hosts.assert_called_once()
|
mock_filter_hosts.assert_called_once()
|
||||||
mock_detach.assert_called_once()
|
mock_detach.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.volume_types.'
|
||||||
|
'get_volume_type_qos_specs',
|
||||||
|
return_value=QOS_SPECS)
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.attach_volume_to_host")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_volume_lun")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_array_version",
|
||||||
|
return_value='4.0')
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_qos_policy_id_by_name",
|
||||||
|
return_value=None)
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.create_qos_io_rule")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.create_qos_policy")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.update_volume_with_qos_policy")
|
||||||
|
def test_volume_qos_policy_create(self,
|
||||||
|
mock_volume_types,
|
||||||
|
mock_attach_volume,
|
||||||
|
mock_get_volume_lun,
|
||||||
|
mock_get_array_version,
|
||||||
|
mock_get_qos_policy,
|
||||||
|
mock_qos_io_rule,
|
||||||
|
mock_qos_policy,
|
||||||
|
mock_volume_qos_update):
|
||||||
|
|
||||||
|
self.iscsi_driver.adapter.use_chap_auth = False
|
||||||
|
self.mock_object(self.iscsi_driver.adapter,
|
||||||
|
"_create_host_if_not_exist",
|
||||||
|
return_value=(
|
||||||
|
FAKE_HOST,
|
||||||
|
utils.get_chap_credentials(),
|
||||||
|
))
|
||||||
|
self.iscsi_driver.initialize_connection(
|
||||||
|
self.volume,
|
||||||
|
self.fake_connector
|
||||||
|
)
|
||||||
|
mock_get_volume_lun.return_value = "fake_volume_identifier"
|
||||||
|
mock_qos_io_rule.return_value = "9beb10ff-a00c-4d88-a7d9-692be2b3073f"
|
||||||
|
mock_qos_policy.return_value = "d69f7131-4617-4bae-89f8-a540a6bda94b"
|
||||||
|
mock_volume_types.assert_called_once()
|
||||||
|
mock_get_array_version.assert_called_once()
|
||||||
|
mock_get_qos_policy.assert_called_once()
|
||||||
|
mock_attach_volume.assert_called_once()
|
||||||
|
mock_qos_policy.assert_called_once()
|
||||||
|
mock_volume_qos_update.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.volume_types.'
|
||||||
|
'get_volume_type_qos_specs',
|
||||||
|
return_value=QOS_SPECS)
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.attach_volume_to_host")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_volume_lun")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_array_version",
|
||||||
|
return_value='4.0')
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_qos_policy_id_by_name")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.update_qos_io_rule")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.update_volume_with_qos_policy")
|
||||||
|
def test_volume_qos_io_rule_update(self,
|
||||||
|
mock_volume_types,
|
||||||
|
mock_attach_volume,
|
||||||
|
mock_get_volume_lun,
|
||||||
|
mock_get_array_version,
|
||||||
|
mock_get_qos_policy,
|
||||||
|
mock_update_qos_io_rule,
|
||||||
|
mock_volume_qos_update):
|
||||||
|
self.iscsi_driver.adapter.use_chap_auth = False
|
||||||
|
self.mock_object(self.iscsi_driver.adapter,
|
||||||
|
"_create_host_if_not_exist",
|
||||||
|
return_value=(
|
||||||
|
FAKE_HOST,
|
||||||
|
utils.get_chap_credentials(),
|
||||||
|
))
|
||||||
|
self.iscsi_driver.initialize_connection(
|
||||||
|
self.volume,
|
||||||
|
self.fake_connector
|
||||||
|
)
|
||||||
|
mock_get_volume_lun.return_value = "fake_volume_identifier"
|
||||||
|
mock_get_qos_policy.return_value = ("d69f7131-"
|
||||||
|
"4617-4bae-89f8-a540a6bda94b")
|
||||||
|
mock_volume_types.assert_called_once()
|
||||||
|
mock_attach_volume.assert_called_once()
|
||||||
|
mock_get_array_version.assert_called_once()
|
||||||
|
mock_update_qos_io_rule.assert_called_once()
|
||||||
|
mock_volume_qos_update.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.volume_types.'
|
||||||
|
'get_volume_type_qos_specs',
|
||||||
|
return_value=QOS_SPECS)
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.utils."
|
||||||
|
"is_multiattached_to_host", return_value=False)
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.detach_volume_from_host")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.get_array_version",
|
||||||
|
return_value='4.0')
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
|
"PowerStoreClient.update_volume_with_qos_policy")
|
||||||
|
def test_volume_qos_policy_update(self,
|
||||||
|
mock_volume_types,
|
||||||
|
mock_multi_attached_host,
|
||||||
|
mock_detach_volume,
|
||||||
|
mock_get_array_version,
|
||||||
|
mock_volume_qos_update):
|
||||||
|
self.mock_object(self.iscsi_driver.adapter,
|
||||||
|
"_filter_hosts_by_initiators",
|
||||||
|
return_value=FAKE_HOST)
|
||||||
|
self.iscsi_driver.terminate_connection(self.volume,
|
||||||
|
self.fake_connector)
|
||||||
|
mock_volume_types.assert_called_once()
|
||||||
|
mock_multi_attached_host.assert_called_once()
|
||||||
|
mock_detach_volume.assert_called_once()
|
||||||
|
mock_get_array_version.assert_called_once()
|
||||||
|
mock_volume_qos_update.assert_called_once()
|
||||||
|
@@ -77,26 +77,33 @@ class TestVolumeCreateDeleteExtend(powerstore.TestPowerStoreDriver):
|
|||||||
"PowerStoreClient.get_volume_mapped_hosts")
|
"PowerStoreClient.get_volume_mapped_hosts")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.delete_volume_or_snapshot")
|
"PowerStoreClient.delete_volume_or_snapshot")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.adapter."
|
||||||
|
"CommonAdapter._create_or_update_volume_qos_policy")
|
||||||
def test_delete_volume_detach_not_found(self,
|
def test_delete_volume_detach_not_found(self,
|
||||||
mock_delete,
|
mock_delete,
|
||||||
mock_mapped_hosts,
|
mock_mapped_hosts,
|
||||||
mock_detach_request):
|
mock_detach_request,
|
||||||
|
mock_qos_policy):
|
||||||
mock_mapped_hosts.return_value = ["fake_host_id"]
|
mock_mapped_hosts.return_value = ["fake_host_id"]
|
||||||
mock_detach_request.return_value = powerstore.MockResponse(
|
mock_detach_request.return_value = powerstore.MockResponse(
|
||||||
content={},
|
content={},
|
||||||
rc=404
|
rc=404
|
||||||
)
|
)
|
||||||
self.driver.delete_volume(self.volume)
|
self.driver.delete_volume(self.volume)
|
||||||
|
mock_qos_policy.assert_not_called()
|
||||||
|
|
||||||
@mock.patch("requests.request")
|
@mock.patch("requests.request")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.get_volume_mapped_hosts")
|
"PowerStoreClient.get_volume_mapped_hosts")
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.client."
|
||||||
"PowerStoreClient.delete_volume_or_snapshot")
|
"PowerStoreClient.delete_volume_or_snapshot")
|
||||||
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.adapter."
|
||||||
|
"CommonAdapter._create_or_update_volume_qos_policy")
|
||||||
def test_delete_volume_detach_not_mapped(self,
|
def test_delete_volume_detach_not_mapped(self,
|
||||||
mock_delete,
|
mock_delete,
|
||||||
mock_mapped_hosts,
|
mock_mapped_hosts,
|
||||||
mock_detach_request):
|
mock_detach_request,
|
||||||
|
mock_qos_policy):
|
||||||
mock_mapped_hosts.return_value = ["fake_host_id"]
|
mock_mapped_hosts.return_value = ["fake_host_id"]
|
||||||
mock_detach_request.return_value = powerstore.MockResponse(
|
mock_detach_request.return_value = powerstore.MockResponse(
|
||||||
content={
|
content={
|
||||||
@@ -109,6 +116,7 @@ class TestVolumeCreateDeleteExtend(powerstore.TestPowerStoreDriver):
|
|||||||
rc=422
|
rc=422
|
||||||
)
|
)
|
||||||
self.driver.delete_volume(self.volume)
|
self.driver.delete_volume(self.volume)
|
||||||
|
mock_qos_policy.assert_not_called()
|
||||||
|
|
||||||
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.adapter."
|
@mock.patch("cinder.volume.drivers.dell_emc.powerstore.adapter."
|
||||||
"CommonAdapter._detach_volume_from_hosts")
|
"CommonAdapter._detach_volume_from_hosts")
|
||||||
|
@@ -24,15 +24,22 @@ from cinder.i18n import _
|
|||||||
from cinder.objects import fields
|
from cinder.objects import fields
|
||||||
from cinder.objects.group_snapshot import GroupSnapshot
|
from cinder.objects.group_snapshot import GroupSnapshot
|
||||||
from cinder.objects.snapshot import Snapshot
|
from cinder.objects.snapshot import Snapshot
|
||||||
|
from cinder.utils import retry
|
||||||
|
from cinder.volume.drivers.dell_emc.powerstore import (
|
||||||
|
exception as powerstore_exception)
|
||||||
from cinder.volume.drivers.dell_emc.powerstore import client
|
from cinder.volume.drivers.dell_emc.powerstore import client
|
||||||
from cinder.volume.drivers.dell_emc.powerstore import utils
|
from cinder.volume.drivers.dell_emc.powerstore import utils
|
||||||
from cinder.volume import manager
|
from cinder.volume import manager
|
||||||
|
from cinder.volume import volume_types
|
||||||
from cinder.volume import volume_utils
|
from cinder.volume import volume_utils
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
CHAP_MODE_SINGLE = "Single"
|
CHAP_MODE_SINGLE = "Single"
|
||||||
POWERSTORE_NVME_VERSION_SUPPORT = "3.0"
|
POWERSTORE_NVME_VERSION_SUPPORT = "3.0"
|
||||||
|
POWERSTORE_QOS_VERSION_SUPPORT = "4.0"
|
||||||
|
retry_exc_tuple = (powerstore_exception.DellPowerStoreQoSIORuleExists,
|
||||||
|
powerstore_exception.DellPowerStoreQoSPolicyExists)
|
||||||
|
|
||||||
|
|
||||||
class CommonAdapter(object):
|
class CommonAdapter(object):
|
||||||
@@ -592,6 +599,8 @@ class CommonAdapter(object):
|
|||||||
"host_provider_id": host["id"],
|
"host_provider_id": host["id"],
|
||||||
"volume_identifier": volume_identifier,
|
"volume_identifier": volume_identifier,
|
||||||
})
|
})
|
||||||
|
self._create_or_update_volume_qos_policy(volume, provider_id,
|
||||||
|
utils.VOLUME_ATTACH_OPERATION)
|
||||||
return volume_identifier
|
return volume_identifier
|
||||||
|
|
||||||
def _create_host_and_attach(self, connector, volume):
|
def _create_host_and_attach(self, connector, volume):
|
||||||
@@ -668,6 +677,8 @@ class CommonAdapter(object):
|
|||||||
"volume_provider_id": provider_id,
|
"volume_provider_id": provider_id,
|
||||||
"hosts_provider_ids": hosts_to_detach,
|
"hosts_provider_ids": hosts_to_detach,
|
||||||
})
|
})
|
||||||
|
self._create_or_update_volume_qos_policy(volume, provider_id,
|
||||||
|
utils.VOLUME_DETACH_OPERATION)
|
||||||
|
|
||||||
def _disconnect_volume(self, volume, connector):
|
def _disconnect_volume(self, volume, connector):
|
||||||
"""Detach PowerStore volume.
|
"""Detach PowerStore volume.
|
||||||
@@ -1020,6 +1031,94 @@ class CommonAdapter(object):
|
|||||||
updates.append(volume_updates)
|
updates.append(volume_updates)
|
||||||
return None, updates
|
return None, updates
|
||||||
|
|
||||||
|
def _create_or_update_volume_qos_policy(self, volume,
|
||||||
|
provider_id, operation):
|
||||||
|
"""Create or update volume QoS policy
|
||||||
|
|
||||||
|
@param volume: OpenStack volume object
|
||||||
|
@param provider_id: Volume provider Id
|
||||||
|
@param operation: QoS create or update operation
|
||||||
|
"""
|
||||||
|
volume_type_id = volume["volume_type_id"]
|
||||||
|
specs = volume_types.get_volume_type_qos_specs(volume_type_id)
|
||||||
|
qos_specs = specs['qos_specs']
|
||||||
|
if (qos_specs is not None and (qos_specs["consumer"] == "back-end" or
|
||||||
|
qos_specs["consumer"] == "both")
|
||||||
|
and self._check_qos_support()):
|
||||||
|
if operation == utils.VOLUME_ATTACH_OPERATION:
|
||||||
|
qos_policy_id = self._get_or_create_qos_policy(qos_specs)
|
||||||
|
self.client.update_volume_with_qos_policy(provider_id,
|
||||||
|
qos_policy_id)
|
||||||
|
else:
|
||||||
|
self.client.update_volume_with_qos_policy(provider_id,
|
||||||
|
None)
|
||||||
|
|
||||||
|
def _check_qos_support(self):
|
||||||
|
"""Check PowerStore array support QoS or not
|
||||||
|
|
||||||
|
@return: Version is supported or not in boolean
|
||||||
|
"""
|
||||||
|
array_version = self.client.get_array_version()
|
||||||
|
if not utils.version_gte(
|
||||||
|
array_version,
|
||||||
|
POWERSTORE_QOS_VERSION_SUPPORT
|
||||||
|
):
|
||||||
|
msg = (_("PowerStore arrays support QoS starting from version "
|
||||||
|
"%(qos_support_version)s. Current PowerStore array "
|
||||||
|
"version: %(current_version)s.")
|
||||||
|
% {"qos_support_version": POWERSTORE_QOS_VERSION_SUPPORT,
|
||||||
|
"current_version": array_version})
|
||||||
|
LOG.error(msg)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@retry(retry_exc_tuple,
|
||||||
|
interval=1,
|
||||||
|
retries=3,
|
||||||
|
backoff_rate=2)
|
||||||
|
def _get_or_create_qos_policy(self, qos_specs):
|
||||||
|
"""Get or create QoS policy
|
||||||
|
|
||||||
|
1. Create operation: It will verify if a QoS policy is created for the
|
||||||
|
volume type. If not, it will create an I/O limit rule, establish
|
||||||
|
a policy with this rule, and attach the policy to the volume.
|
||||||
|
|
||||||
|
2. Update operation: It will verify if a QoS policy is created for the
|
||||||
|
volume type. If it is, it will update the existing I/O limit rule with
|
||||||
|
the specified QoS values.
|
||||||
|
|
||||||
|
@param qos_specs: Volume QoS specs
|
||||||
|
@return: QoS policy id
|
||||||
|
"""
|
||||||
|
qos_id = qos_specs["id"]
|
||||||
|
policy_name = "qos-policy-%s" % qos_id
|
||||||
|
io_rule_name = "io-rule-%s" % qos_id
|
||||||
|
specs = qos_specs["specs"]
|
||||||
|
io_rule_params = {
|
||||||
|
"type": (specs["bandwidth_limit_type"]
|
||||||
|
if "bandwidth_limit_type" in specs else None),
|
||||||
|
"max_iops":
|
||||||
|
int(specs["max_iops"]) if "max_iops" in specs else None,
|
||||||
|
"max_bw":
|
||||||
|
int(specs["max_bw"]) if "max_bw" in specs else None,
|
||||||
|
"burst_percentage":
|
||||||
|
(int(specs["burst_percentage"])
|
||||||
|
if "burst_percentage" in specs else None)
|
||||||
|
}
|
||||||
|
policy_id = self.client.get_qos_policy_id_by_name(policy_name)
|
||||||
|
if policy_id is None:
|
||||||
|
io_rule_params["name"] = io_rule_name
|
||||||
|
io_rule_id = self.client.create_qos_io_rule(io_rule_params)
|
||||||
|
policy_params = {
|
||||||
|
"name": policy_name,
|
||||||
|
"io_limit_rule_id": io_rule_id
|
||||||
|
}
|
||||||
|
return self.client.create_qos_policy(policy_params)
|
||||||
|
else:
|
||||||
|
self.client.update_qos_io_rule(io_rule_name, io_rule_params)
|
||||||
|
return policy_id
|
||||||
|
|
||||||
|
|
||||||
class FibreChannelAdapter(CommonAdapter):
|
class FibreChannelAdapter(CommonAdapter):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
@@ -25,14 +25,18 @@ import requests
|
|||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder import utils as cinder_utils
|
from cinder import utils as cinder_utils
|
||||||
|
from cinder.volume.drivers.dell_emc.powerstore import (
|
||||||
|
exception as powerstore_exception)
|
||||||
from cinder.volume.drivers.dell_emc.powerstore import utils
|
from cinder.volume.drivers.dell_emc.powerstore import utils
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
VOLUME_NOT_MAPPED_ERROR = "0xE0A08001000F"
|
VOLUME_NOT_MAPPED_ERROR = "0xE0A08001000F"
|
||||||
SESSION_ALREADY_FAILED_OVER_ERROR = "0xE0201005000C"
|
SESSION_ALREADY_FAILED_OVER_ERROR = "0xE0201005000C"
|
||||||
TOO_MANY_SNAPS_ERROR = "0xE0A040010003"
|
TOO_MANY_SNAPS_ERROR = "0xE0A040010003"
|
||||||
MAX_SNAPS_IN_VTREE = 32
|
MAX_SNAPS_IN_VTREE = 32
|
||||||
|
QOS_IO_RULE_EXISTS_ERROR = "0xE0A0E0010009"
|
||||||
|
QOS_POLICY_EXISTS_ERROR = "0xE02020010004"
|
||||||
|
QOS_UNEXPECTED_RESPONSE_ERROR = "0xE0101001000C"
|
||||||
|
|
||||||
|
|
||||||
class PowerStoreClient(object):
|
class PowerStoreClient(object):
|
||||||
@@ -803,3 +807,87 @@ class PowerStoreClient(object):
|
|||||||
raise exception.VolumeBackendAPIException(data=msg)
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
nguid = response["nguid"].split('.')[1]
|
nguid = response["nguid"].split('.')[1]
|
||||||
return nguid
|
return nguid
|
||||||
|
|
||||||
|
def get_qos_policy_id_by_name(self, name):
|
||||||
|
r, response = self._send_get_request(
|
||||||
|
"/policy",
|
||||||
|
params={
|
||||||
|
"name": "eq.%s" % name,
|
||||||
|
"type": "eq.QoS",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = _("Failed to query PowerStore QoS policy "
|
||||||
|
"with name %s." % name)
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
if len(response) > 0:
|
||||||
|
qos_policy_id = response[0].get("id")
|
||||||
|
return qos_policy_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_qos_io_rule(self, io_rule_params):
|
||||||
|
r, response = self._send_post_request(
|
||||||
|
"/io_limit_rule",
|
||||||
|
payload=io_rule_params
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = _("Failed to create PowerStore I/O limit "
|
||||||
|
"rule %s." % io_rule_params["name"])
|
||||||
|
LOG.error(msg)
|
||||||
|
if ("messages" in response and
|
||||||
|
(response["messages"][0]["code"] ==
|
||||||
|
QOS_IO_RULE_EXISTS_ERROR or
|
||||||
|
response["messages"][0]["code"] ==
|
||||||
|
QOS_UNEXPECTED_RESPONSE_ERROR)):
|
||||||
|
raise (
|
||||||
|
powerstore_exception.
|
||||||
|
DellPowerStoreQoSIORuleExists(name=io_rule_params["name"]))
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return response["id"]
|
||||||
|
|
||||||
|
def create_qos_policy(self, policy_params):
|
||||||
|
r, response = self._send_post_request(
|
||||||
|
"/policy",
|
||||||
|
payload=policy_params
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = _("Failed to create PowerStore QoS "
|
||||||
|
"policy %s." % policy_params["name"])
|
||||||
|
LOG.error(msg)
|
||||||
|
if ("messages" in response and
|
||||||
|
(response["messages"][0]["code"] ==
|
||||||
|
QOS_POLICY_EXISTS_ERROR or
|
||||||
|
response["messages"][0]["code"] ==
|
||||||
|
QOS_UNEXPECTED_RESPONSE_ERROR)):
|
||||||
|
raise (
|
||||||
|
powerstore_exception.
|
||||||
|
DellPowerStoreQoSPolicyExists(name=policy_params["name"]))
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return response["id"]
|
||||||
|
|
||||||
|
def update_volume_with_qos_policy(self, provider_id, qos_policy_id):
|
||||||
|
r, response = self._send_patch_request(
|
||||||
|
"/volume/%s" % provider_id,
|
||||||
|
payload={
|
||||||
|
"qos_performance_policy_id": qos_policy_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = _("Failed to update PowerStore volume %(volume_id)s with "
|
||||||
|
"QoS policy %(policy_id)s."
|
||||||
|
% {"volume_id": provider_id,
|
||||||
|
"policy_id": qos_policy_id})
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
def update_qos_io_rule(self, io_rule_name, io_rule_params):
|
||||||
|
r, response = self._send_patch_request(
|
||||||
|
"/io_limit_rule/name:%s" % io_rule_name,
|
||||||
|
payload=io_rule_params
|
||||||
|
)
|
||||||
|
if r.status_code not in self.ok_codes:
|
||||||
|
msg = (_("Failed to update PowerStore I/O limit rule %s.")
|
||||||
|
% io_rule_name)
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
@@ -52,9 +52,10 @@ class PowerStoreDriver(driver.VolumeDriver):
|
|||||||
(iSCSI target, Replication target, etc.)
|
(iSCSI target, Replication target, etc.)
|
||||||
1.2.0 - Add NVMe-OF support
|
1.2.0 - Add NVMe-OF support
|
||||||
1.2.1 - Report trim/discard support
|
1.2.1 - Report trim/discard support
|
||||||
|
1.2.2 - QoS (Quality of Service) support
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "1.2.1"
|
VERSION = "1.2.2"
|
||||||
VENDOR = "Dell EMC"
|
VENDOR = "Dell EMC"
|
||||||
|
|
||||||
# ThirdPartySystems wiki page
|
# ThirdPartySystems wiki page
|
||||||
|
21
cinder/volume/drivers/dell_emc/powerstore/exception.py
Normal file
21
cinder/volume/drivers/dell_emc/powerstore/exception.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
class DellPowerStoreQoSIORuleExists(exception.VolumeDriverException):
|
||||||
|
message = _('QoS I/O Rule %(name)s already exists.')
|
||||||
|
|
||||||
|
|
||||||
|
class DellPowerStoreQoSPolicyExists(exception.VolumeDriverException):
|
||||||
|
message = _('QoS policy %(name)s already exists.')
|
@@ -36,6 +36,8 @@ PROTOCOL_FC = constants.FC
|
|||||||
PROTOCOL_ISCSI = constants.ISCSI
|
PROTOCOL_ISCSI = constants.ISCSI
|
||||||
PROTOCOL_NVME = "NVMe"
|
PROTOCOL_NVME = "NVMe"
|
||||||
POWERSTORE_PP_KEY = "powerstore:protection_policy"
|
POWERSTORE_PP_KEY = "powerstore:protection_policy"
|
||||||
|
VOLUME_ATTACH_OPERATION = 1
|
||||||
|
VOLUME_DETACH_OPERATION = 2
|
||||||
|
|
||||||
|
|
||||||
def bytes_to_gib(size_in_bytes):
|
def bytes_to_gib(size_in_bytes):
|
||||||
|
@@ -23,6 +23,7 @@ Supported operations
|
|||||||
- Create, delete Consistency Groups snapshots.
|
- Create, delete Consistency Groups snapshots.
|
||||||
- Clone a Consistency Group.
|
- Clone a Consistency Group.
|
||||||
- Create a Consistency Group from a Consistency Group snapshot.
|
- Create a Consistency Group from a Consistency Group snapshot.
|
||||||
|
- Quality of Service (QoS)
|
||||||
|
|
||||||
Driver configuration
|
Driver configuration
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -221,3 +222,72 @@ snapshot enabled.
|
|||||||
.. note:: Currently driver does not support Consistency Groups replication.
|
.. note:: Currently driver does not support Consistency Groups replication.
|
||||||
Adding volume to Consistency Group and creating volume in Consistency Group
|
Adding volume to Consistency Group and creating volume in Consistency Group
|
||||||
will fail if volume is replicated.
|
will fail if volume is replicated.
|
||||||
|
|
||||||
|
QoS (Quality of Service) support
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. note:: QoS is supported in PowerStore version 4.0 or later.
|
||||||
|
|
||||||
|
The PowerStore driver supports Quality of Service (QoS) by
|
||||||
|
enabling the following capabilities:
|
||||||
|
|
||||||
|
``bandwidth_limit_type``
|
||||||
|
The QoS bandwidth limit type. This type setting determines
|
||||||
|
how the max_iops and max_bw attributes are used.
|
||||||
|
This has the following two values:
|
||||||
|
|
||||||
|
1. ``Absolute`` - Limits are absolute values specified,
|
||||||
|
either I/O operations per second or bandwidth.
|
||||||
|
|
||||||
|
2. ``Density`` - Limits specified are per GB,
|
||||||
|
e.g. I/O operations per second per GB.
|
||||||
|
|
||||||
|
.. note:: This (bandwidth_limit_type) property is mandatory when creating QoS.
|
||||||
|
|
||||||
|
``max_iops``
|
||||||
|
Maximum I/O operations in either I/O operations per second (IOPS) or
|
||||||
|
I/O operations per second per GB. The specification of the type
|
||||||
|
attribute determines which metric is used.
|
||||||
|
If type is set to absolute, max_iops is specified in IOPS.
|
||||||
|
If type is set to density, max_iops is specified in IOPS per GB.
|
||||||
|
If both max_iops and max_bw are specified,
|
||||||
|
the system will limit I/O if either value is exceeded.
|
||||||
|
The value must be within the range of 1 to 2147483646.
|
||||||
|
|
||||||
|
``max_bw``
|
||||||
|
Maximum I/O bandwidth measured in either Kilobytes per second or Kilobytes
|
||||||
|
per second / per GB. The specification of the type attribute determines
|
||||||
|
which measurement is used. If type is set to absolute, max_bw is specified
|
||||||
|
in Kilobytes per second. If type is set to density max_bw is specified
|
||||||
|
in Kilobytes per second / per GB.
|
||||||
|
If both max_iops and max_bw are specified, the system will
|
||||||
|
limit I/O if either value is exceeded.
|
||||||
|
The value must be within the range of 2000 to 2147483646.
|
||||||
|
|
||||||
|
``burst_percentage``
|
||||||
|
Percentage indicating by how much the limit may be exceeded. If I/O
|
||||||
|
normally runs below the specified limit, then the volume or volume_group
|
||||||
|
will accumulate burst credits that can be used to exceed the limit for
|
||||||
|
a short period (a few seconds, but will not exceed the burst limit).
|
||||||
|
This burst percentage applies to both max_iops and max_bw and
|
||||||
|
is independent of the type setting.
|
||||||
|
The value must be within the range of 0 to 100.
|
||||||
|
If this property is not specified during QoS creation,
|
||||||
|
a default value of 0 will be used.
|
||||||
|
|
||||||
|
.. note:: When creating QoS, you must define either ``max_iops`` or ``max_bw``, or you can define both.
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ openstack volume qos create --consumer back-end --property max_iops=100 --property max_bw=50000 --property bandwidth_limit_type=Absolute --property burst_percentage=80 powerstore_qos
|
||||||
|
$ openstack volume type create --property volume_backend_name=powerstore powerstore
|
||||||
|
$ openstack volume qos associate powerstore_qos powerstore
|
||||||
|
|
||||||
|
.. note:: There are two approaches for updating QoS properties in PowerStore:
|
||||||
|
|
||||||
|
#. ``Retype the Volume``:
|
||||||
|
This involves retyping the volume with the different QoS settings and migrating the volume to the new type.
|
||||||
|
#. ``Modify Existing QoS Properties`` (Recommended):
|
||||||
|
This method entails changing the existing QoS properties and creating a new instance or image
|
||||||
|
volume to update the QoS policy in PowerStore. This will also update the QoS properties of existing attached volumes,
|
||||||
|
created with the same volume type.
|
||||||
|
@@ -408,7 +408,7 @@ notes=Vendor drivers that support Quality of Service (QoS) at the
|
|||||||
driver.datacore=missing
|
driver.datacore=missing
|
||||||
driver.datera=complete
|
driver.datera=complete
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_powerstore=missing
|
driver.dell_emc_powerstore=complete
|
||||||
driver.dell_emc_powerstore_nfs=missing
|
driver.dell_emc_powerstore_nfs=missing
|
||||||
driver.dell_emc_powervault=missing
|
driver.dell_emc_powervault=missing
|
||||||
driver.dell_emc_sc=complete
|
driver.dell_emc_sc=complete
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Dell PowerStore Driver: Added QoS (Quality of Service) support for
|
||||||
|
PowerStore 4.0 or later versions.
|
Reference in New Issue
Block a user