Adds a Quotas client for Nova

* Adds a client for the 'os-quota-sets' extension
* Adds basic Admin and non-admin tests for GET and PUT operations for the
Quotas API
* Adds some tests to check Quota enforcement for create server (bug 1034453)

Fixes LP Bug #1040760
Fixes LP Bug #1034453
Change-Id: I7eb0041dbc80d8733bb2df54e4fc4755cfe9ae9c
diff --git a/tempest/manager.py b/tempest/manager.py
index fda887c..59743e5 100644
--- a/tempest/manager.py
+++ b/tempest/manager.py
@@ -41,6 +41,7 @@
 from tempest.services.compute.json import keypairs_client
 from tempest.services.compute.json import volumes_extensions_client
 from tempest.services.compute.json import console_output_client
+from tempest.services.compute.json import quotas_client
 
 NetworkClient = network_client.NetworkClient
 ImagesClient = images_client.ImagesClientJSON
@@ -54,6 +55,7 @@
 VolumesExtensionsClient = volumes_extensions_client.VolumesExtensionsClientJSON
 VolumesClient = volumes_client.VolumesClientJSON
 ConsoleOutputsClient = console_output_client.ConsoleOutputsClient
+QuotasClient = quotas_client.QuotasClient
 
 LOG = logging.getLogger(__name__)
 
@@ -233,6 +235,7 @@
         self.volumes_extensions_client = VolumesExtensionsClient(*client_args)
         self.volumes_client = VolumesClient(*client_args)
         self.console_outputs_client = ConsoleOutputsClient(*client_args)
+        self.quotas_client = QuotasClient(*client_args)
         self.network_client = NetworkClient(*client_args)
 
 
diff --git a/tempest/openstack.py b/tempest/openstack.py
index dc73bd7..fbd2f00 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -59,7 +59,7 @@
 from tempest.services.object_storage.object_client import ObjectClient
 from tempest.services.boto.clients import APIClientEC2
 from tempest.services.boto.clients import ObjectClientS3
-
+from tempest.services.compute.json.quotas_client import QuotasClient
 
 LOG = logging.getLogger(__name__)
 
@@ -184,6 +184,7 @@
             msg = "Unsupported interface type `%s'" % interface
             raise exceptions.InvalidConfiguration(msg)
         self.console_outputs_client = ConsoleOutputsClient(*client_args)
+        self.quotas_client = QuotasClient(*client_args)
         self.network_client = NetworkClient(*client_args)
         self.account_client = AccountClient(*client_args)
         self.container_client = ContainerClient(*client_args)
diff --git a/tempest/services/compute/admin/__init__.py b/tempest/services/compute/admin/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/compute/admin/__init__.py
diff --git a/tempest/services/compute/admin/json/__init__.py b/tempest/services/compute/admin/json/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/compute/admin/json/__init__.py
diff --git a/tempest/services/compute/admin/json/quotas_client.py b/tempest/services/compute/admin/json/quotas_client.py
new file mode 100644
index 0000000..625d4d4
--- /dev/null
+++ b/tempest/services/compute/admin/json/quotas_client.py
@@ -0,0 +1,79 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 NTT Data
+# 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
+
+from tempest.services.compute.json.quotas_client import QuotasClient
+
+
+class AdminQuotasClient(QuotasClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(AdminQuotasClient, self).__init__(config, username, password,
+                                                auth_url, tenant_name)
+
+    def update_quota_set(self, tenant_id, injected_file_content_bytes=None,
+                         metadata_items=None, ram=None, floating_ips=None,
+                         key_pairs=None, instances=None,
+                         security_group_rules=None, injected_files=None,
+                         cores=None, injected_file_path_bytes=None,
+                         security_groups=None):
+        """
+        Updates the tenant's quota limits for one or more resources
+        """
+        post_body = {}
+
+        if injected_file_content_bytes >= 0:
+            post_body['injected_file_content_bytes'] = \
+                injected_file_content_bytes
+
+        if metadata_items >= 0:
+            post_body['metadata_items'] = metadata_items
+
+        if ram >= 0:
+            post_body['ram'] = ram
+
+        if floating_ips >= 0:
+            post_body['floating_ips'] = floating_ips
+
+        if key_pairs >= 0:
+            post_body['key_pairs'] = key_pairs
+
+        if instances >= 0:
+            post_body['instances'] = instances
+
+        if security_group_rules >= 0:
+            post_body['security_group_rules'] = security_group_rules
+
+        if injected_files >= 0:
+            post_body['injected_files'] = injected_files
+
+        if cores >= 0:
+            post_body['cores'] = cores
+
+        if injected_file_path_bytes >= 0:
+            post_body['injected_file_path_bytes'] = injected_file_path_bytes
+
+        if security_groups >= 0:
+            post_body['security_groups'] = security_groups
+
+        post_body = json.dumps({'quota_set': post_body})
+        resp, body = self.put('os-quota-sets/%s' % str(tenant_id), post_body,
+                              self.headers)
+
+        body = json.loads(body)
+        return resp, body['quota_set']
diff --git a/tempest/services/compute/json/quotas_client.py b/tempest/services/compute/json/quotas_client.py
new file mode 100644
index 0000000..2cc417f
--- /dev/null
+++ b/tempest/services/compute/json/quotas_client.py
@@ -0,0 +1,36 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 NTT Data
+# 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
+
+from tempest.common.rest_client import RestClient
+
+
+class QuotasClient(RestClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(QuotasClient, self).__init__(config, username, password,
+                                           auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def get_quota_set(self, tenant_id):
+        """List the quota set for a tenant"""
+
+        url = 'os-quota-sets/%s' % str(tenant_id)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['quota_set']
diff --git a/tempest/tests/compute/admin/test_quotas.py b/tempest/tests/compute/admin/test_quotas.py
new file mode 100644
index 0000000..98ca169
--- /dev/null
+++ b/tempest/tests/compute/admin/test_quotas.py
@@ -0,0 +1,156 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# 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 nose.plugins.attrib import attr
+
+from tempest.tests.compute.base import BaseComputeTest
+from tempest.services.compute.admin.json import quotas_client as adm_quotas
+from tempest import exceptions
+
+
+class QuotasTest(BaseComputeTest):
+
+    @classmethod
+    def setUpClass(cls):
+        super(QuotasTest, cls).setUpClass()
+        adm_user = cls.config.compute_admin.username
+        adm_pass = cls.config.compute_admin.password
+        adm_tenant = cls.config.compute_admin.tenant_name
+        auth_url = cls.config.identity.auth_url
+
+        cls.adm_client = adm_quotas.AdminQuotasClient(cls.config, adm_user,
+                                                      adm_pass, auth_url,
+                                                      adm_tenant)
+        cls.client = cls.os.quotas_client
+        cls.identity_admin_client = cls._get_identity_admin_client()
+        resp, tenants = cls.identity_admin_client.list_tenants()
+
+        if cls.config.compute.allow_tenant_isolation:
+            cls.demo_tenant_id = cls.isolated_creds[0][0]['tenantId']
+        else:
+            cls.demo_tenant_id = [tnt['id'] for tnt in tenants if tnt['name']
+                                  == cls.config.compute.tenant_name][0]
+
+        cls.adm_tenant_id = [tnt['id'] for tnt in tenants if tnt['name'] ==
+                             cls.config.compute_admin.tenant_name][0]
+
+        cls.default_quota_set = {'injected_file_content_bytes': 10240,
+                                 'metadata_items': 128, 'injected_files': 5,
+                                 'ram': 51200, 'floating_ips': 10,
+                                 'key_pairs': 100,
+                                 'injected_file_path_bytes': 255,
+                                 'instances': 10, 'security_group_rules': 20,
+                                 'cores': 20, 'security_groups': 10}
+
+    @classmethod
+    def tearDown(cls):
+        for server in cls.servers:
+            try:
+                cls.servers_client.delete_server(server['id'])
+            except exceptions.NotFound:
+                continue
+
+    @attr(type='smoke')
+    def test_get_default_quotas(self):
+        """Admin can get the default resource quota set for a tenant"""
+        expected_quota_set = self.default_quota_set.copy()
+        expected_quota_set['id'] = self.demo_tenant_id
+        try:
+            resp, quota_set = self.client.get_quota_set(self.demo_tenant_id)
+            self.assertEqual(200, resp.status)
+            self.assertSequenceEqual(expected_quota_set, quota_set)
+        except:
+            self.fail("Admin could not get the default quota set for a tenant")
+
+    def test_update_all_quota_resources_for_tenant(self):
+        """Admin can update all the resource quota limits for a tenant"""
+        new_quota_set = {'injected_file_content_bytes': 20480,
+                         'metadata_items': 256, 'injected_files': 10,
+                         'ram': 10240, 'floating_ips': 20, 'key_pairs': 200,
+                         'injected_file_path_bytes': 512, 'instances': 20,
+                         'security_group_rules': 20, 'cores': 2,
+                         'security_groups': 20}
+        try:
+            # Update limits for all quota resources
+            resp, quota_set = self.adm_client.update_quota_set(
+                self.demo_tenant_id,
+                **new_quota_set)
+            self.assertEqual(200, resp.status)
+            self.assertSequenceEqual(new_quota_set, quota_set)
+        except:
+            self.fail("Admin could not update quota set for the tenant")
+        finally:
+            # Reset quota resource limits to default values
+            resp, quota_set = self.adm_client.update_quota_set(
+                self.demo_tenant_id,
+                **self.default_quota_set)
+            self.assertEqual(200, resp.status, "Failed to reset quota "
+                             "defaults")
+
+    def test_get_updated_quotas(self):
+        """Verify that GET shows the updated quota set"""
+        self.adm_client.update_quota_set(self.demo_tenant_id,
+                                         ram='5120')
+        try:
+            resp, quota_set = self.client.get_quota_set(self.demo_tenant_id)
+            self.assertEqual(200, resp.status)
+            self.assertEqual(quota_set['ram'], 5120)
+        except:
+            self.fail("Could not get the update quota limit for resource")
+        finally:
+            # Reset quota resource limits to default values
+            resp, quota_set = self.adm_client.update_quota_set(
+                self.demo_tenant_id,
+                **self.default_quota_set)
+            self.assertEqual(200, resp.status, "Failed to reset quota "
+                             "defaults")
+
+    def test_create_server_when_cpu_quota_is_full(self):
+        """Disallow server creation when tenant's vcpu quota is full"""
+        resp, quota_set = self.client.get_quota_set(self.demo_tenant_id)
+        default_vcpu_quota = quota_set['cores']
+        vcpu_quota = 0  # Set the quota to zero to conserve resources
+
+        resp, quota_set = self.adm_client.update_quota_set(self.demo_tenant_id,
+                                                           cores=vcpu_quota)
+        try:
+            self.create_server()
+        except exceptions.OverLimit:
+            pass
+        else:
+            self.fail("Could create servers over the VCPU quota limit")
+        finally:
+            self.adm_client.update_quota_set(self.demo_tenant_id,
+                                             cores=default_vcpu_quota)
+
+    def test_create_server_when_memory_quota_is_full(self):
+        """Disallow server creation when tenant's memory quota is full"""
+        resp, quota_set = self.client.get_quota_set(self.demo_tenant_id)
+        default_mem_quota = quota_set['ram']
+        mem_quota = 0  # Set the quota to zero to conserve resources
+
+        self.adm_client.update_quota_set(self.demo_tenant_id,
+                                         ram=mem_quota)
+        try:
+            self.create_server()
+        except exceptions.OverLimit:
+            pass
+        else:
+            self.fail("Could create servers over the memory quota limit")
+        finally:
+            self.adm_client.update_quota_set(self.demo_tenant_id,
+                                             ram=default_mem_quota)
diff --git a/tempest/tests/compute/base.py b/tempest/tests/compute/base.py
index ebf3b54..bb2ff8b 100644
--- a/tempest/tests/compute/base.py
+++ b/tempest/tests/compute/base.py
@@ -17,14 +17,13 @@
 
 import logging
 import time
-import nose
 
 import unittest2 as unittest
 import nose
 
 from tempest import config
-from tempest import openstack
 from tempest import exceptions
+from tempest import openstack
 from tempest.common.utils.data_utils import rand_name
 
 __all__ = ['BaseComputeTest', 'BaseComputeTestJSON', 'BaseComputeTestXML',
@@ -61,6 +60,7 @@
         cls.keypairs_client = os.keypairs_client
         cls.security_groups_client = os.security_groups_client
         cls.console_outputs_client = os.console_outputs_client
+        cls.quotas_client = os.quotas_client
         cls.limits_client = os.limits_client
         cls.volumes_extensions_client = os.volumes_extensions_client
         cls.volumes_client = os.volumes_client
@@ -178,10 +178,12 @@
         cls.clear_isolated_creds()
 
     @classmethod
-    def create_server(cls, image_id=None):
+    def create_server(cls, image_id=None, flavor=None):
         """Wrapper utility that returns a test server"""
         server_name = rand_name(cls.__name__ + "-instance")
-        flavor = cls.flavor_ref
+
+        if not flavor:
+            flavor = cls.flavor_ref
         if not image_id:
             image_id = cls.image_ref
 
diff --git a/tempest/tests/compute/test_quotas.py b/tempest/tests/compute/test_quotas.py
new file mode 100644
index 0000000..d07064f
--- /dev/null
+++ b/tempest/tests/compute/test_quotas.py
@@ -0,0 +1,49 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# 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 nose.plugins.attrib import attr
+
+from tempest.tests.compute.base import BaseComputeTest
+
+
+class QuotasTest(BaseComputeTest):
+
+    @classmethod
+    def setUpClass(cls):
+        super(QuotasTest, cls).setUpClass()
+        cls.client = cls.quotas_client
+        cls.admin_client = cls._get_identity_admin_client()
+        resp, tenants = cls.admin_client.list_tenants()
+        cls.tenant_id = [tnt['id'] for tnt in tenants if tnt['name'] ==
+                         cls.client.tenant_name][0]
+
+    @attr(type='smoke')
+    def test_get_default_quotas(self):
+        """User can get the default quota set for it's tenant"""
+        expected_quota_set = {'injected_file_content_bytes': 10240,
+                              'metadata_items': 128, 'injected_files': 5,
+                              'ram': 51200, 'floating_ips': 10,
+                              'key_pairs': 100,
+                              'injected_file_path_bytes': 255, 'instances': 10,
+                              'security_group_rules': 20, 'cores': 20,
+                              'id': self.tenant_id, 'security_groups': 10}
+        try:
+            resp, quota_set = self.client.get_quota_set(self.tenant_id)
+            self.assertEqual(200, resp.status)
+            self.assertSequenceEqual(expected_quota_set, quota_set)
+        except:
+            self.fail("Quota set for tenant did not have default limits")