Adds the clients and tests for CINDER QoS V1 & V2 APIs
This patch adds the JSON clients and tests for following
CINDER QoS-specs V1 & V2 APIs :
1. qos-create
2. qos-delete
3. qos-list
4. qos-get
5. qos-key
6. qos-associate
7. qos-get-association
8. qos-disassociate
9. qos-disassociate-all
Partially Implements blueprint: client-checks-success
Partially Implements blueprint: cinder-v2-api-tests
Co-authored-by: Abhijeet.Jain <abhijeet.jain@nectechnologies.in>
Change-Id: I7fc424b57e4e8decf61e991b2988e1cead5dda9b
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 3cd0827..43f48ff 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -148,11 +148,11 @@
_api_version = 1
-class BaseVolumeV1AdminTest(BaseVolumeV1Test):
+class BaseVolumeAdminTest(BaseVolumeTest):
"""Base test case class for all Volume Admin API tests."""
@classmethod
def setUpClass(cls):
- super(BaseVolumeV1AdminTest, cls).setUpClass()
+ super(BaseVolumeAdminTest, cls).setUpClass()
cls.adm_user = CONF.identity.admin_username
cls.adm_pass = CONF.identity.admin_password
cls.adm_tenant = CONF.identity.admin_tenant_name
@@ -160,11 +160,62 @@
msg = ("Missing Volume Admin API credentials "
"in configuration.")
raise cls.skipException(msg)
+
if CONF.compute.allow_tenant_isolation:
cls.os_adm = clients.Manager(cls.isolated_creds.get_admin_creds(),
interface=cls._interface)
else:
cls.os_adm = clients.AdminManager(interface=cls._interface)
+
+ cls.qos_specs = []
+
cls.client = cls.os_adm.volume_types_client
cls.hosts_client = cls.os_adm.volume_hosts_client
cls.quotas_client = cls.os_adm.volume_quotas_client
+ cls.volume_types_client = cls.os_adm.volume_types_client
+
+ if cls._api_version == 1:
+ if not CONF.volume_feature_enabled.api_v1:
+ msg = "Volume API v1 is disabled"
+ raise cls.skipException(msg)
+ cls.volume_qos_client = cls.os_adm.volume_qos_client
+ elif cls._api_version == 2:
+ if not CONF.volume_feature_enabled.api_v2:
+ msg = "Volume API v2 is disabled"
+ raise cls.skipException(msg)
+ cls.volume_qos_client = cls.os_adm.volume_qos_v2_client
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.clear_qos_specs()
+ super(BaseVolumeAdminTest, cls).tearDownClass()
+
+ @classmethod
+ def create_test_qos_specs(cls, name=None, consumer=None, **kwargs):
+ """create a test Qos-Specs."""
+ name = name or data_utils.rand_name(cls.__name__ + '-QoS')
+ consumer = consumer or 'front-end'
+ _, qos_specs = cls.volume_qos_client.create_qos(name, consumer,
+ **kwargs)
+ cls.qos_specs.append(qos_specs['id'])
+ return qos_specs
+
+ @classmethod
+ def clear_qos_specs(cls):
+ for qos_id in cls.qos_specs:
+ try:
+ cls.volume_qos_client.delete_qos(qos_id)
+ except exceptions.NotFound:
+ # The qos_specs may have already been deleted which is OK.
+ pass
+
+ for qos_id in cls.qos_specs:
+ try:
+ cls.volume_qos_client.wait_for_resource_deletion(qos_id)
+ except exceptions.NotFound:
+ # The qos_specs may have already been deleted which is OK.
+ pass
+
+
+class BaseVolumeV1AdminTest(BaseVolumeAdminTest):
+ _api_version = 1
diff --git a/tempest/api/volume/test_qos.py b/tempest/api/volume/test_qos.py
new file mode 100644
index 0000000..8b6ba49
--- /dev/null
+++ b/tempest/api/volume/test_qos.py
@@ -0,0 +1,176 @@
+# All Rights Reserved.
+#
+# 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 tempest.api.volume import base
+from tempest.common.utils import data_utils as utils
+from tempest import test
+
+
+class QosSpecsV2TestJSON(base.BaseVolumeAdminTest):
+ """Test the Cinder QoS-specs.
+
+ Tests for create, list, delete, show, associate,
+ disassociate, set/unset key V2 APIs.
+ """
+
+ @classmethod
+ @test.safe_setup
+ def setUpClass(cls):
+ super(QosSpecsV2TestJSON, cls).setUpClass()
+ # Create admin qos client
+ # Create a test shared qos-specs for tests
+ cls.qos_name = utils.rand_name(cls.__name__ + '-QoS')
+ cls.qos_consumer = 'front-end'
+
+ cls.created_qos = cls.create_test_qos_specs(cls.qos_name,
+ cls.qos_consumer,
+ read_iops_sec='2000')
+
+ def _create_delete_test_qos_with_given_consumer(self, consumer):
+ name = utils.rand_name('qos')
+ qos = {'name': name, 'consumer': consumer}
+ body = self.create_test_qos_specs(name, consumer)
+ for key in ['name', 'consumer']:
+ self.assertEqual(qos[key], body[key])
+
+ self.volume_qos_client.delete_qos(body['id'])
+ self.volume_qos_client.wait_for_resource_deletion(body['id'])
+
+ # validate the deletion
+ _, list_qos = self.volume_qos_client.list_qos()
+ self.assertNotIn(body, list_qos)
+
+ def _create_test_volume_type(self):
+ vol_type_name = utils.rand_name("volume-type")
+ _, vol_type = self.volume_types_client.create_volume_type(
+ vol_type_name)
+ self.addCleanup(self.volume_types_client.delete_volume_type,
+ vol_type['id'])
+ return vol_type
+
+ def _test_associate_qos(self, vol_type_id):
+ self.volume_qos_client.associate_qos(
+ self.created_qos['id'], vol_type_id)
+
+ def _test_get_association_qos(self):
+ _, body = self.volume_qos_client.get_association_qos(
+ self.created_qos['id'])
+
+ associations = []
+ for association in body:
+ associations.append(association['id'])
+
+ return associations
+
+ def test_create_delete_qos_with_front_end_consumer(self):
+ """Tests the creation and deletion of QoS specs
+
+ With consumer as front end
+ """
+ self._create_delete_test_qos_with_given_consumer('front-end')
+
+ def test_create_delete_qos_with_back_end_consumer(self):
+ """Tests the creation and deletion of QoS specs
+
+ With consumer as back-end
+ """
+ self._create_delete_test_qos_with_given_consumer('back-end')
+
+ @test.attr(type='smoke')
+ def test_create_delete_qos_with_both_consumer(self):
+ """Tests the creation and deletion of QoS specs
+
+ With consumer as both front end and back end
+ """
+ self._create_delete_test_qos_with_given_consumer('both')
+
+ @test.attr(type='smoke')
+ def test_get_qos(self):
+ """Tests the detail of a given qos-specs"""
+ _, body = self.volume_qos_client.get_qos(self.created_qos['id'])
+ self.assertEqual(self.qos_name, body['name'])
+ self.assertEqual(self.qos_consumer, body['consumer'])
+
+ @test.attr(type='smoke')
+ def test_list_qos(self):
+ """Tests the list of all qos-specs"""
+ _, body = self.volume_qos_client.list_qos()
+ self.assertIn(self.created_qos, body)
+
+ @test.attr(type='smoke')
+ def test_set_unset_qos_key(self):
+ """Test the addition of a specs key to qos-specs"""
+ args = {'iops_bytes': '500'}
+ _, body = self.volume_qos_client.set_qos_key(self.created_qos['id'],
+ iops_bytes='500')
+ self.assertEqual(args, body)
+ _, body = self.volume_qos_client.get_qos(self.created_qos['id'])
+ self.assertEqual(args['iops_bytes'], body['specs']['iops_bytes'])
+
+ # test the deletion of a specs key from qos-specs
+ keys = ['iops_bytes']
+ self.volume_qos_client.unset_qos_key(self.created_qos['id'], keys)
+ operation = 'qos-key-unset'
+ self.volume_qos_client.wait_for_qos_operations(self.created_qos['id'],
+ operation, keys)
+ _, body = self.volume_qos_client.get_qos(self.created_qos['id'])
+ self.assertNotIn(keys[0], body['specs'])
+
+ @test.attr(type='smoke')
+ def test_associate_disassociate_qos(self):
+ """Test the following operations :
+
+ 1. associate_qos
+ 2. get_association_qos
+ 3. disassociate_qos
+ 4. disassociate_all_qos
+ """
+
+ # create a test volume-type
+ vol_type = []
+ for _ in range(0, 3):
+ vol_type.append(self._create_test_volume_type())
+
+ # associate the qos-specs with volume-types
+ for i in range(0, 3):
+ self._test_associate_qos(vol_type[i]['id'])
+
+ # get the association of the qos-specs
+ associations = self._test_get_association_qos()
+
+ for i in range(0, 3):
+ self.assertIn(vol_type[i]['id'], associations)
+
+ # disassociate a volume-type with qos-specs
+ self.volume_qos_client.disassociate_qos(
+ self.created_qos['id'], vol_type[0]['id'])
+ operation = 'disassociate'
+ self.volume_qos_client.wait_for_qos_operations(self.created_qos['id'],
+ operation,
+ vol_type[0]['id'])
+ associations = self._test_get_association_qos()
+ self.assertNotIn(vol_type[0]['id'], associations)
+
+ # disassociate all volume-types from qos-specs
+ self.volume_qos_client.disassociate_all_qos(
+ self.created_qos['id'])
+ operation = 'disassociate-all'
+ self.volume_qos_client.wait_for_qos_operations(self.created_qos['id'],
+ operation)
+ associations = self._test_get_association_qos()
+ self.assertEmpty(associations)
+
+
+class QosSpecsV1TestJSON(QosSpecsV2TestJSON):
+ _api_version = 1
diff --git a/tempest/clients.py b/tempest/clients.py
index 2b8b6fb..eab496e 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -180,12 +180,14 @@
from tempest.services.volume.json.backups_client import BackupsClientJSON
from tempest.services.volume.json.extensions_client import \
ExtensionsClientJSON as VolumeExtensionClientJSON
+from tempest.services.volume.json.qos_client import QosSpecsClientJSON
from tempest.services.volume.json.snapshots_client import SnapshotsClientJSON
from tempest.services.volume.json.volumes_client import VolumesClientJSON
from tempest.services.volume.v2.json.availability_zone_client import \
VolumeV2AvailabilityZoneClientJSON
from tempest.services.volume.v2.json.extensions_client import \
ExtensionsV2ClientJSON as VolumeV2ExtensionClientJSON
+from tempest.services.volume.v2.json.qos_client import QosSpecsV2ClientJSON
from tempest.services.volume.v2.json.volumes_client import VolumesV2ClientJSON
from tempest.services.volume.v2.xml.availability_zone_client import \
VolumeV2AvailabilityZoneClientXML
@@ -428,6 +430,13 @@
self.security_group_default_rules_client = (
SecurityGroupDefaultRulesClientJSON(self.auth_provider))
self.networks_client = NetworksClientJSON(self.auth_provider)
+ # NOTE : As XML clients are not implemented for Qos-specs.
+ # So, setting the qos_client here. Once client are implemented,
+ # qos_client would be moved to its respective if/else.
+ # Bug : 1312553
+ self.volume_qos_client = QosSpecsClientJSON(self.auth_provider)
+ self.volume_qos_v2_client = QosSpecsV2ClientJSON(
+ self.auth_provider)
class AltManager(Manager):
diff --git a/tempest/services/volume/json/qos_client.py b/tempest/services/volume/json/qos_client.py
new file mode 100644
index 0000000..6e0bee9
--- /dev/null
+++ b/tempest/services/volume/json/qos_client.py
@@ -0,0 +1,161 @@
+# All Rights Reserved.
+#
+# 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.
+
+import json
+import time
+
+from tempest.common import rest_client
+from tempest import config
+from tempest import exceptions
+
+CONF = config.CONF
+
+
+class BaseQosSpecsClientJSON(rest_client.RestClient):
+ """Client class to send CRUD QoS API requests"""
+
+ def __init__(self, auth_provider):
+ super(BaseQosSpecsClientJSON, self).__init__(auth_provider)
+ self.service = CONF.volume.catalog_type
+ self.build_interval = CONF.volume.build_interval
+ self.build_timeout = CONF.volume.build_timeout
+
+ def is_resource_deleted(self, qos_id):
+ try:
+ self.get_qos(qos_id)
+ except exceptions.NotFound:
+ return True
+ return False
+
+ def wait_for_qos_operations(self, qos_id, operation, args=None):
+ """Waits for a qos operations to be completed.
+
+ NOTE : operation value is required for wait_for_qos_operations()
+ operation = 'qos-key' / 'disassociate' / 'disassociate-all'
+ args = keys[] when operation = 'qos-key'
+ args = volume-type-id disassociated when operation = 'disassociate'
+ args = None when operation = 'disassociate-all'
+ """
+ start_time = int(time.time())
+ while True:
+ if operation == 'qos-key-unset':
+ resp, body = self.get_qos(qos_id)
+ self.expected_success(200, resp.status)
+ if not any(key in body['specs'] for key in args):
+ return
+ elif operation == 'disassociate':
+ resp, body = self.get_association_qos(qos_id)
+ self.expected_success(200, resp.status)
+ if not any(args in body[i]['id'] for i in range(0, len(body))):
+ return
+ elif operation == 'disassociate-all':
+ resp, body = self.get_association_qos(qos_id)
+ self.expected_success(200, resp.status)
+ if not body:
+ return
+ else:
+ msg = (" operation value is either not defined or incorrect.")
+ raise exceptions.UnprocessableEntity(msg)
+
+ if int(time.time()) - start_time >= self.build_timeout:
+ raise exceptions.TimeoutException
+ time.sleep(self.build_interval)
+
+ def create_qos(self, name, consumer, **kwargs):
+ """Create a QoS Specification.
+
+ name : name of the QoS specifications
+ consumer : conumer of Qos ( front-end / back-end / both )
+ """
+ post_body = {'name': name, 'consumer': consumer}
+ post_body.update(kwargs)
+ post_body = json.dumps({'qos_specs': post_body})
+ resp, body = self.post('qos-specs', post_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return resp, body['qos_specs']
+
+ def delete_qos(self, qos_id, force=False):
+ """Delete the specified QoS specification."""
+ resp, body = self.delete(
+ "qos-specs/%s?force=%s" % (str(qos_id), force))
+ self.expected_success(202, resp.status)
+
+ def list_qos(self):
+ """List all the QoS specifications created."""
+ url = 'qos-specs'
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return resp, body['qos_specs']
+
+ def get_qos(self, qos_id):
+ """Get the specified QoS specification."""
+ url = "qos-specs/%s" % str(qos_id)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return resp, body['qos_specs']
+
+ def set_qos_key(self, qos_id, **kwargs):
+ """Set the specified keys/values of QoS specification.
+
+ kwargs : it is the dictionary of the key=value pairs to set
+ """
+ put_body = json.dumps({"qos_specs": kwargs})
+ resp, body = self.put('qos-specs/%s' % qos_id, put_body)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return resp, body['qos_specs']
+
+ def unset_qos_key(self, qos_id, keys):
+ """Unset the specified keys of QoS specification.
+
+ keys : it is the array of the keys to unset
+ """
+ put_body = json.dumps({'keys': keys})
+ resp, _ = self.put('qos-specs/%s/delete_keys' % qos_id, put_body)
+ self.expected_success(202, resp.status)
+
+ def associate_qos(self, qos_id, vol_type_id):
+ """Associate the specified QoS with specified volume-type."""
+ url = "qos-specs/%s/associate" % str(qos_id)
+ url += "?vol_type_id=%s" % vol_type_id
+ resp, _ = self.get(url)
+ self.expected_success(202, resp.status)
+
+ def get_association_qos(self, qos_id):
+ """Get the association of the specified QoS specification."""
+ url = "qos-specs/%s/associations" % str(qos_id)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return resp, body['qos_associations']
+
+ def disassociate_qos(self, qos_id, vol_type_id):
+ """Disassociate the specified QoS with specified volume-type."""
+ url = "qos-specs/%s/disassociate" % str(qos_id)
+ url += "?vol_type_id=%s" % vol_type_id
+ resp, _ = self.get(url)
+ self.expected_success(202, resp.status)
+
+ def disassociate_all_qos(self, qos_id):
+ """Disassociate the specified QoS with all associations."""
+ url = "qos-specs/%s/disassociate_all" % str(qos_id)
+ resp, _ = self.get(url)
+ self.expected_success(202, resp.status)
+
+
+class QosSpecsClientJSON(BaseQosSpecsClientJSON):
+ """Volume V1 QoS client."""
diff --git a/tempest/services/volume/v2/json/qos_client.py b/tempest/services/volume/v2/json/qos_client.py
new file mode 100644
index 0000000..a734df8
--- /dev/null
+++ b/tempest/services/volume/v2/json/qos_client.py
@@ -0,0 +1,23 @@
+# All Rights Reserved.
+#
+# 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 tempest.services.volume.json import qos_client
+
+
+class QosSpecsV2ClientJSON(qos_client.BaseQosSpecsClientJSON):
+
+ def __init__(self, auth_provider):
+ super(QosSpecsV2ClientJSON, self).__init__(auth_provider)
+
+ self.api_version = "v2"