Merge "Add test for compute API os-quota-class-sets"
diff --git a/tempest/api/compute/admin/test_quotas.py b/tempest/api/compute/admin/test_quotas.py
index 348666d..9263396 100644
--- a/tempest/api/compute/admin/test_quotas.py
+++ b/tempest/api/compute/admin/test_quotas.py
@@ -13,14 +13,26 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import six
+from testtools import matchers
+
 from tempest.api.compute import base
+from tempest.common import tempest_fixtures as fixtures
 from tempest.common.utils import data_utils
+from tempest.openstack.common import log as logging
 from tempest import test
 
+LOG = logging.getLogger(__name__)
+
 
 class QuotasAdminTestJSON(base.BaseV2ComputeAdminTest):
     force_tenant_isolation = True
 
+    def setUp(self):
+        # NOTE(mriedem): Avoid conflicts with os-quota-class-sets tests.
+        self.useFixture(fixtures.LockFixture('compute_quotas'))
+        super(QuotasAdminTestJSON, self).setUp()
+
     @classmethod
     def setUpClass(cls):
         super(QuotasAdminTestJSON, cls).setUpClass()
@@ -134,3 +146,49 @@
 
 class QuotasAdminTestXML(QuotasAdminTestJSON):
     _interface = 'xml'
+
+
+class QuotaClassesAdminTestJSON(base.BaseV2ComputeAdminTest):
+    """Tests the os-quota-class-sets API to update default quotas.
+    """
+
+    def setUp(self):
+        # All test cases in this class need to externally lock on doing
+        # anything with default quota values.
+        self.useFixture(fixtures.LockFixture('compute_quotas'))
+        super(QuotaClassesAdminTestJSON, self).setUp()
+
+    @classmethod
+    def setUpClass(cls):
+        super(QuotaClassesAdminTestJSON, cls).setUpClass()
+        cls.adm_client = cls.os_adm.quota_classes_client
+
+    def _restore_default_quotas(self, original_defaults):
+        LOG.debug("restoring quota class defaults")
+        resp, body = self.adm_client.update_quota_class_set(
+            'default', **original_defaults)
+        self.assertEqual(200, resp.status)
+
+    @test.attr(type='gate')
+    def test_update_default_quotas(self):
+        LOG.debug("get the current 'default' quota class values")
+        resp, body = self.adm_client.get_quota_class_set('default')
+        self.assertEqual(200, resp.status)
+        self.assertIn('id', body)
+        self.assertEqual('default', body.pop('id'))
+        # restore the defaults when the test is done
+        self.addCleanup(self._restore_default_quotas, body.copy())
+        # increment all of the values for updating the default quota class
+        for quota, default in six.iteritems(body):
+            body[quota] = default + 1
+        LOG.debug("update limits for the default quota class set")
+        resp, update_body = self.adm_client.update_quota_class_set('default',
+                                                                   **body)
+        self.assertEqual(200, resp.status)
+        LOG.debug("assert that the response has all of the changed values")
+        self.assertThat(update_body.items(),
+                        matchers.ContainsAll(body.items()))
+
+
+class QuotaClassesAdminTestXML(QuotaClassesAdminTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index a1aaa95..caf4174 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -67,6 +67,8 @@
             cls.keypairs_client = cls.os.keypairs_client
             cls.security_groups_client = cls.os.security_groups_client
             cls.quotas_client = cls.os.quotas_client
+            # NOTE(mriedem): os-quota-class-sets is v2 API only
+            cls.quota_classes_client = cls.os.quota_classes_client
             cls.limits_client = cls.os.limits_client
             cls.volumes_extensions_client = cls.os.volumes_extensions_client
             cls.volumes_client = cls.os.volumes_client
diff --git a/tempest/api/compute/test_quotas.py b/tempest/api/compute/test_quotas.py
index dc85e76..eeff3ce 100644
--- a/tempest/api/compute/test_quotas.py
+++ b/tempest/api/compute/test_quotas.py
@@ -14,11 +14,17 @@
 #    under the License.
 
 from tempest.api.compute import base
+from tempest.common import tempest_fixtures as fixtures
 from tempest import test
 
 
 class QuotasTestJSON(base.BaseV2ComputeTest):
 
+    def setUp(self):
+        # NOTE(mriedem): Avoid conflicts with os-quota-class-sets tests.
+        self.useFixture(fixtures.LockFixture('compute_quotas'))
+        super(QuotasTestJSON, self).setUp()
+
     @classmethod
     def setUpClass(cls):
         super(QuotasTestJSON, cls).setUpClass()
diff --git a/tempest/api/compute/v3/test_quotas.py b/tempest/api/compute/v3/test_quotas.py
index 62a7556..ecf70cf 100644
--- a/tempest/api/compute/v3/test_quotas.py
+++ b/tempest/api/compute/v3/test_quotas.py
@@ -14,11 +14,17 @@
 #    under the License.
 
 from tempest.api.compute import base
+from tempest.common import tempest_fixtures as fixtures
 from tempest import test
 
 
 class QuotasV3Test(base.BaseV3ComputeTest):
 
+    def setUp(self):
+        # NOTE(mriedem): Avoid conflicts with os-quota-class-sets tests.
+        self.useFixture(fixtures.LockFixture('compute_quotas'))
+        super(QuotasV3Test, self).setUp()
+
     @classmethod
     def setUpClass(cls):
         super(QuotasV3Test, cls).setUpClass()
diff --git a/tempest/api_schema/compute/v2/quota_classes.py b/tempest/api_schema/compute/v2/quota_classes.py
new file mode 100644
index 0000000..3464fb4
--- /dev/null
+++ b/tempest/api_schema/compute/v2/quota_classes.py
@@ -0,0 +1,31 @@
+# Copyright 2014 IBM Corporation.
+# 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 copy
+
+from tempest.api_schema.compute.v2 import quotas
+
+# NOTE(mriedem): os-quota-class-sets responses are the same as os-quota-sets
+# except for the key in the response body is quota_class_set instead of
+# quota_set, so update this copy of the schema from os-quota-sets.
+quota_set = copy.deepcopy(quotas.quota_set)
+quota_set['response_body']['properties']['quota_class_set'] = (
+    quota_set['response_body']['properties'].pop('quota_set'))
+quota_set['response_body']['required'] = ['quota_class_set']
+
+quota_set_update = copy.deepcopy(quotas.quota_set_update)
+quota_set_update['response_body']['properties']['quota_class_set'] = (
+    quota_set_update['response_body']['properties'].pop('quota_set'))
+quota_set_update['response_body']['required'] = ['quota_class_set']
diff --git a/tempest/clients.py b/tempest/clients.py
index 4050a20..4e2205e 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -50,6 +50,7 @@
 from tempest.services.compute.json.limits_client import LimitsClientJSON
 from tempest.services.compute.json.migrations_client import \
     MigrationsClientJSON
+from tempest.services.compute.json.quotas_client import QuotaClassesClientJSON
 from tempest.services.compute.json.quotas_client import QuotasClientJSON
 from tempest.services.compute.json.security_groups_client import \
     SecurityGroupsClientJSON
@@ -105,6 +106,7 @@
     InterfacesClientXML
 from tempest.services.compute.xml.keypairs_client import KeyPairsClientXML
 from tempest.services.compute.xml.limits_client import LimitsClientXML
+from tempest.services.compute.xml.quotas_client import QuotaClassesClientXML
 from tempest.services.compute.xml.quotas_client import QuotasClientXML
 from tempest.services.compute.xml.security_groups_client \
     import SecurityGroupsClientXML
@@ -220,6 +222,8 @@
             self.images_client = ImagesClientXML(self.auth_provider)
             self.keypairs_client = KeyPairsClientXML(self.auth_provider)
             self.quotas_client = QuotasClientXML(self.auth_provider)
+            self.quota_classes_client = QuotaClassesClientXML(
+                self.auth_provider)
             self.flavors_client = FlavorsClientXML(self.auth_provider)
             self.extensions_client = ExtensionsClientXML(self.auth_provider)
             self.volumes_extensions_client = VolumesExtensionsClientXML(
@@ -288,6 +292,8 @@
             self.keypairs_v3_client = KeyPairsV3ClientJSON(
                 self.auth_provider)
             self.quotas_client = QuotasClientJSON(self.auth_provider)
+            self.quota_classes_client = QuotaClassesClientJSON(
+                self.auth_provider)
             self.quotas_v3_client = QuotasV3ClientJSON(self.auth_provider)
             self.flavors_client = FlavorsClientJSON(self.auth_provider)
             self.flavors_v3_client = FlavorsV3ClientJSON(self.auth_provider)
diff --git a/tempest/services/compute/json/quotas_client.py b/tempest/services/compute/json/quotas_client.py
index 7e828d8..14b7100 100644
--- a/tempest/services/compute/json/quotas_client.py
+++ b/tempest/services/compute/json/quotas_client.py
@@ -15,6 +15,7 @@
 
 import json
 
+from tempest.api_schema.compute.v2 import quota_classes as classes_schema
 from tempest.api_schema.compute.v2 import quotas as schema
 from tempest.common import rest_client
 from tempest import config
@@ -118,3 +119,32 @@
         resp, body = self.delete('os-quota-sets/%s' % str(tenant_id))
         self.validate_response(schema.delete_quota, resp, body)
         return resp, body
+
+
+class QuotaClassesClientJSON(rest_client.RestClient):
+
+    def __init__(self, auth_provider):
+        super(QuotaClassesClientJSON, self).__init__(auth_provider)
+        self.service = CONF.compute.catalog_type
+
+    def get_quota_class_set(self, quota_class_id):
+        """List the quota class set for a quota class."""
+
+        url = 'os-quota-class-sets/%s' % str(quota_class_id)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        self.validate_response(classes_schema.quota_set, resp, body)
+        return resp, body['quota_class_set']
+
+    def update_quota_class_set(self, quota_class_id, **kwargs):
+        """
+        Updates the quota class's limits for one or more resources.
+        """
+        post_body = json.dumps({'quota_class_set': kwargs})
+
+        resp, body = self.put('os-quota-class-sets/%s' % str(quota_class_id),
+                              post_body)
+
+        body = json.loads(body)
+        self.validate_response(classes_schema.quota_set_update, resp, body)
+        return resp, body['quota_class_set']
diff --git a/tempest/services/compute/xml/quotas_client.py b/tempest/services/compute/xml/quotas_client.py
index 5502fcc..7f87248 100644
--- a/tempest/services/compute/xml/quotas_client.py
+++ b/tempest/services/compute/xml/quotas_client.py
@@ -22,6 +22,19 @@
 CONF = config.CONF
 
 
+def format_quota(q):
+    quota = {}
+    for k, v in q.items():
+        try:
+            v = int(v)
+        except ValueError:
+            pass
+
+        quota[k] = v
+
+    return quota
+
+
 class QuotasClientXML(rest_client.RestClient):
     TYPE = "xml"
 
@@ -29,18 +42,6 @@
         super(QuotasClientXML, self).__init__(auth_provider)
         self.service = CONF.compute.catalog_type
 
-    def _format_quota(self, q):
-        quota = {}
-        for k, v in q.items():
-            try:
-                v = int(v)
-            except ValueError:
-                pass
-
-            quota[k] = v
-
-        return quota
-
     def get_quota_set(self, tenant_id, user_id=None):
         """List the quota set for a tenant."""
 
@@ -49,7 +50,7 @@
             url += '?user_id=%s' % str(user_id)
         resp, body = self.get(url)
         body = xml_utils.xml_to_json(etree.fromstring(body))
-        body = self._format_quota(body)
+        body = format_quota(body)
         return resp, body
 
     def get_default_quota_set(self, tenant_id):
@@ -58,7 +59,7 @@
         url = 'os-quota-sets/%s/defaults' % str(tenant_id)
         resp, body = self.get(url)
         body = xml_utils.xml_to_json(etree.fromstring(body))
-        body = self._format_quota(body)
+        body = format_quota(body)
         return resp, body
 
     def update_quota_set(self, tenant_id, user_id=None,
@@ -124,9 +125,41 @@
                                   str(xml_utils.Document(post_body)))
 
         body = xml_utils.xml_to_json(etree.fromstring(body))
-        body = self._format_quota(body)
+        body = format_quota(body)
         return resp, body
 
     def delete_quota_set(self, tenant_id):
         """Delete the tenant's quota set."""
         return self.delete('os-quota-sets/%s' % str(tenant_id))
+
+
+class QuotaClassesClientXML(rest_client.RestClient):
+    TYPE = "xml"
+
+    def __init__(self, auth_provider):
+        super(QuotaClassesClientXML, self).__init__(auth_provider)
+        self.service = CONF.compute.catalog_type
+
+    def get_quota_class_set(self, quota_class_id):
+        """List the quota class set for a quota class."""
+
+        url = 'os-quota-class-sets/%s' % str(quota_class_id)
+        resp, body = self.get(url)
+        body = xml_utils.xml_to_json(etree.fromstring(body))
+        body = format_quota(body)
+        return resp, body
+
+    def update_quota_class_set(self, quota_class_id, **kwargs):
+        """
+        Updates the quota class's limits for one or more resources.
+        """
+        post_body = xml_utils.Element("quota_class_set",
+                                      xmlns=xml_utils.XMLNS_11,
+                                      **kwargs)
+
+        resp, body = self.put('os-quota-class-sets/%s' % str(quota_class_id),
+                              str(xml_utils.Document(post_body)))
+
+        body = xml_utils.xml_to_json(etree.fromstring(body))
+        body = format_quota(body)
+        return resp, body