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")