summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2012-03-21 16:29:34 +0000
committerGerrit Code Review <review@openstack.org>2012-03-21 16:29:34 +0000
commitf25cb41f5ea351bbbc0abbc1b040cfba8ef25186 (patch)
tree7b28bcca90964650372c18e9034f19aa69f14d74
parent41c57e267752d7cf51a5cbd6bfab7332d218382b (diff)
parent6a38b650c001ec8e6da435856c37a28737401aaf (diff)
downloadnova-f25cb41f5ea351bbbc0abbc1b040cfba8ef25186.tar.gz
nova-f25cb41f5ea351bbbc0abbc1b040cfba8ef25186.tar.xz
nova-f25cb41f5ea351bbbc0abbc1b040cfba8ef25186.zip
Merge "Implement quota classes."
-rw-r--r--etc/nova/policy.json1
-rw-r--r--nova/api/openstack/compute/contrib/quota_classes.py99
-rw-r--r--nova/api/openstack/compute/contrib/quotas.py11
-rw-r--r--nova/context.py7
-rw-r--r--nova/db/api.py33
-rw-r--r--nova/db/sqlalchemy/api.py83
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py61
-rw-r--r--nova/db/sqlalchemy/models.py25
-rw-r--r--nova/exception.py4
-rw-r--r--nova/quota.py29
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_quota_classes.py163
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_quotas.py8
-rw-r--r--nova/tests/api/openstack/compute/test_extensions.py1
-rw-r--r--nova/tests/policy.json1
-rw-r--r--nova/tests/test_quota.py190
15 files changed, 692 insertions, 24 deletions
diff --git a/etc/nova/policy.json b/etc/nova/policy.json
index 9d10de8d6..e52518c5c 100644
--- a/etc/nova/policy.json
+++ b/etc/nova/policy.json
@@ -44,6 +44,7 @@
"compute_extension:multinic": [],
"compute_extension:networks": [["rule:admin_api"]],
"compute_extension:quotas": [],
+ "compute_extension:quota_classes": [],
"compute_extension:rescue": [],
"compute_extension:security_groups": [],
"compute_extension:server_action_list": [["rule:admin_api"]],
diff --git a/nova/api/openstack/compute/contrib/quota_classes.py b/nova/api/openstack/compute/contrib/quota_classes.py
new file mode 100644
index 000000000..5c8e07ba9
--- /dev/null
+++ b/nova/api/openstack/compute/contrib/quota_classes.py
@@ -0,0 +1,99 @@
+# 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.
+
+import webob
+
+from nova.api.openstack import wsgi
+from nova.api.openstack import xmlutil
+from nova.api.openstack import extensions
+from nova import db
+from nova import exception
+from nova import quota
+
+
+authorize = extensions.extension_authorizer('compute', 'quota_classes')
+
+
+class QuotaClassTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('quota_class_set',
+ selector='quota_class_set')
+ root.set('id')
+
+ for resource in quota.quota_resources:
+ elem = xmlutil.SubTemplateElement(root, resource)
+ elem.text = resource
+
+ return xmlutil.MasterTemplate(root, 1)
+
+
+class QuotaClassSetsController(object):
+
+ def _format_quota_set(self, quota_class, quota_set):
+ """Convert the quota object to a result dict"""
+
+ result = dict(id=str(quota_class))
+
+ for resource in quota.quota_resources:
+ result[resource] = quota_set[resource]
+
+ return dict(quota_class_set=result)
+
+ @wsgi.serializers(xml=QuotaClassTemplate)
+ def show(self, req, id):
+ context = req.environ['nova.context']
+ authorize(context)
+ try:
+ db.sqlalchemy.api.authorize_quota_class_context(context, id)
+ return self._format_quota_set(id,
+ quota.get_class_quotas(context, id))
+ except exception.NotAuthorized:
+ raise webob.exc.HTTPForbidden()
+
+ @wsgi.serializers(xml=QuotaClassTemplate)
+ def update(self, req, id, body):
+ context = req.environ['nova.context']
+ authorize(context)
+ quota_class = id
+ for key in body['quota_class_set'].keys():
+ if key in quota.quota_resources:
+ value = int(body['quota_class_set'][key])
+ try:
+ db.quota_class_update(context, quota_class, key, value)
+ except exception.QuotaClassNotFound:
+ db.quota_class_create(context, quota_class, key, value)
+ except exception.AdminRequired:
+ raise webob.exc.HTTPForbidden()
+ return {'quota_class_set': quota.get_class_quotas(context,
+ quota_class)}
+
+
+class Quota_classes(extensions.ExtensionDescriptor):
+ """Quota classes management support"""
+
+ name = "QuotaClasses"
+ alias = "os-quota-class-sets"
+ namespace = ("http://docs.openstack.org/compute/ext/"
+ "quota-classes-sets/api/v1.1")
+ updated = "2012-03-12T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = []
+
+ res = extensions.ResourceExtension('os-quota-class-sets',
+ QuotaClassSetsController())
+ resources.append(res)
+
+ return resources
diff --git a/nova/api/openstack/compute/contrib/quotas.py b/nova/api/openstack/compute/contrib/quotas.py
index 196df0ea0..0738fb81b 100644
--- a/nova/api/openstack/compute/contrib/quotas.py
+++ b/nova/api/openstack/compute/contrib/quotas.py
@@ -28,17 +28,12 @@ from nova import quota
authorize = extensions.extension_authorizer('compute', 'quotas')
-quota_resources = ['metadata_items', 'injected_file_content_bytes',
- 'volumes', 'gigabytes', 'ram', 'floating_ips', 'instances',
- 'injected_files', 'cores']
-
-
class QuotaTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('quota_set', selector='quota_set')
root.set('id')
- for resource in quota_resources:
+ for resource in quota.quota_resources:
elem = xmlutil.SubTemplateElement(root, resource)
elem.text = resource
@@ -52,7 +47,7 @@ class QuotaSetsController(object):
result = dict(id=str(project_id))
- for resource in quota_resources:
+ for resource in quota.quota_resources:
result[resource] = quota_set[resource]
return dict(quota_set=result)
@@ -74,7 +69,7 @@ class QuotaSetsController(object):
authorize(context)
project_id = id
for key in body['quota_set'].keys():
- if key in quota_resources:
+ if key in quota.quota_resources:
value = int(body['quota_set'][key])
try:
db.quota_update(context, project_id, key, value)
diff --git a/nova/context.py b/nova/context.py
index cab8b2d4b..78dca3ffc 100644
--- a/nova/context.py
+++ b/nova/context.py
@@ -42,7 +42,8 @@ class RequestContext(object):
def __init__(self, user_id, project_id, is_admin=None, read_deleted="no",
roles=None, remote_address=None, timestamp=None,
- request_id=None, auth_token=None, overwrite=True, **kwargs):
+ request_id=None, auth_token=None, overwrite=True,
+ quota_class=None, **kwargs):
"""
:param read_deleted: 'no' indicates deleted records are hidden, 'yes'
indicates deleted records are visible, 'only' indicates that
@@ -80,6 +81,7 @@ class RequestContext(object):
request_id = generate_request_id()
self.request_id = request_id
self.auth_token = auth_token
+ self.quota_class = quota_class
if overwrite or not hasattr(local.store, 'context'):
self.update_store()
@@ -95,7 +97,8 @@ class RequestContext(object):
'remote_address': self.remote_address,
'timestamp': utils.strtime(self.timestamp),
'request_id': self.request_id,
- 'auth_token': self.auth_token}
+ 'auth_token': self.auth_token,
+ 'quota_class': self.quota_class}
@classmethod
def from_dict(cls, values):
diff --git a/nova/db/api.py b/nova/db/api.py
index 0b06f87fa..02eaa14a3 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -927,6 +927,39 @@ def quota_destroy_all_by_project(context, project_id):
###################
+def quota_class_create(context, class_name, resource, limit):
+ """Create a quota class for the given name and resource."""
+ return IMPL.quota_class_create(context, class_name, resource, limit)
+
+
+def quota_class_get(context, class_name, resource):
+ """Retrieve a quota class or raise if it does not exist."""
+ return IMPL.quota_class_get(context, class_name, resource)
+
+
+def quota_class_get_all_by_name(context, class_name):
+ """Retrieve all quotas associated with a given quota class."""
+ return IMPL.quota_class_get_all_by_name(context, class_name)
+
+
+def quota_class_update(context, class_name, resource, limit):
+ """Update a quota class or raise if it does not exist."""
+ return IMPL.quota_class_update(context, class_name, resource, limit)
+
+
+def quota_class_destroy(context, class_name, resource):
+ """Destroy the quota class or raise if it does not exist."""
+ return IMPL.quota_class_destroy(context, class_name, resource)
+
+
+def quota_class_destroy_all_by_name(context, class_name):
+ """Destroy all quotas associated with a given quota class."""
+ return IMPL.quota_class_destroy_all_by_name(context, class_name)
+
+
+###################
+
+
def volume_allocate_iscsi_target(context, volume_id, host):
"""Atomically allocate a free iscsi_target from the pool."""
return IMPL.volume_allocate_iscsi_target(context, volume_id, host)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index c6af4b85f..d990b970a 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -89,6 +89,15 @@ def authorize_user_context(context, user_id):
raise exception.NotAuthorized()
+def authorize_quota_class_context(context, class_name):
+ """Ensures a request has permission to access the given quota class."""
+ if is_user_context(context):
+ if not context.quota_class:
+ raise exception.NotAuthorized()
+ elif context.quota_class != class_name:
+ raise exception.NotAuthorized()
+
+
def require_admin_context(f):
"""Decorator to require admin request context.
@@ -2291,6 +2300,80 @@ def quota_destroy_all_by_project(context, project_id):
###################
+@require_context
+def quota_class_get(context, class_name, resource, session=None):
+ result = model_query(context, models.QuotaClass, session=session,
+ read_deleted="no").\
+ filter_by(class_name=class_name).\
+ filter_by(resource=resource).\
+ first()
+
+ if not result:
+ raise exception.QuotaClassNotFound(class_name=class_name)
+
+ return result
+
+
+@require_context
+def quota_class_get_all_by_name(context, class_name):
+ authorize_quota_class_context(context, class_name)
+
+ rows = model_query(context, models.QuotaClass, read_deleted="no").\
+ filter_by(class_name=class_name).\
+ all()
+
+ result = {'class_name': class_name}
+ for row in rows:
+ result[row.resource] = row.hard_limit
+
+ return result
+
+
+@require_admin_context
+def quota_class_create(context, class_name, resource, limit):
+ quota_class_ref = models.QuotaClass()
+ quota_class_ref.class_name = class_name
+ quota_class_ref.resource = resource
+ quota_class_ref.hard_limit = limit
+ quota_class_ref.save()
+ return quota_class_ref
+
+
+@require_admin_context
+def quota_class_update(context, class_name, resource, limit):
+ session = get_session()
+ with session.begin():
+ quota_class_ref = quota_class_get(context, class_name, resource,
+ session=session)
+ quota_class_ref.hard_limit = limit
+ quota_class_ref.save(session=session)
+
+
+@require_admin_context
+def quota_class_destroy(context, class_name, resource):
+ session = get_session()
+ with session.begin():
+ quota_class_ref = quota_class_get(context, class_name, resource,
+ session=session)
+ quota_class_ref.delete(session=session)
+
+
+@require_admin_context
+def quota_class_destroy_all_by_name(context, class_name):
+ session = get_session()
+ with session.begin():
+ quota_classes = model_query(context, models.QuotaClass,
+ session=session, read_deleted="no").\
+ filter_by(class_name=class_name).\
+ all()
+
+ for quota_class_ref in quota_classes:
+ quota_class_ref.delete(session=session)
+
+
+###################
+
+
@require_admin_context
def volume_allocate_iscsi_target(context, volume_id, host):
session = get_session()
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py b/nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py
new file mode 100644
index 000000000..37d9695d7
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py
@@ -0,0 +1,61 @@
+# Copyright 2012 OpenStack LLC.
+#
+# 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 sqlalchemy import Boolean, Column, DateTime
+from sqlalchemy import MetaData, Integer, String, Table
+
+from nova import log as logging
+
+LOG = logging.getLogger(__name__)
+
+
+def upgrade(migrate_engine):
+ meta = MetaData()
+ meta.bind = migrate_engine
+
+ # New table
+ quota_classes = Table('quota_classes', meta,
+ Column('created_at', DateTime(timezone=False)),
+ Column('updated_at', DateTime(timezone=False)),
+ Column('deleted_at', DateTime(timezone=False)),
+ Column('deleted', Boolean(create_constraint=True, name=None)),
+ Column('id', Integer(), primary_key=True),
+ Column('class_name',
+ String(length=255, convert_unicode=True,
+ assert_unicode=None, unicode_error=None,
+ _warn_on_bytestring=False), index=True),
+ Column('resource',
+ String(length=255, convert_unicode=True,
+ assert_unicode=None, unicode_error=None,
+ _warn_on_bytestring=False)),
+ Column('hard_limit', Integer(), nullable=True),
+ )
+
+ try:
+ quota_classes.create()
+ except Exception:
+ LOG.error(_("Table |%s| not created!"), repr(quota_classes))
+ raise
+
+
+def downgrade(migrate_engine):
+ meta = MetaData()
+ meta.bind = migrate_engine
+
+ quota_classes = Table('quota_classes', meta, autoload=True)
+ try:
+ quota_classes.drop()
+ except Exception:
+ LOG.error(_("quota_classes table not dropped"))
+ raise
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 3865cd5d0..634f04dbc 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -419,9 +419,11 @@ class VolumeTypeExtraSpecs(BASE, NovaBase):
class Quota(BASE, NovaBase):
"""Represents a single quota override for a project.
- If there is no row for a given project id and resource, then
- the default for the deployment is used. If the row is present
- but the hard limit is Null, then the resource is unlimited.
+ If there is no row for a given project id and resource, then the
+ default for the quota class is used. If there is no row for a
+ given quota class and resource, then the default for the
+ deployment is used. If the row is present but the hard limit is
+ Null, then the resource is unlimited.
"""
__tablename__ = 'quotas'
@@ -433,6 +435,23 @@ class Quota(BASE, NovaBase):
hard_limit = Column(Integer, nullable=True)
+class QuotaClass(BASE, NovaBase):
+ """Represents a single quota override for a quota class.
+
+ If there is no row for a given quota class and resource, then the
+ default for the deployment is used. If the row is present but the
+ hard limit is Null, then the resource is unlimited.
+ """
+
+ __tablename__ = 'quota_classes'
+ id = Column(Integer, primary_key=True)
+
+ class_name = Column(String(255), index=True)
+
+ resource = Column(String(255))
+ hard_limit = Column(Integer, nullable=True)
+
+
class Snapshot(BASE, NovaBase):
"""Represents a block storage device that can be attached to a vm."""
__tablename__ = 'snapshots'
diff --git a/nova/exception.py b/nova/exception.py
index eb0bf382c..75320c2be 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -692,6 +692,10 @@ class ProjectQuotaNotFound(QuotaNotFound):
message = _("Quota for project %(project_id)s could not be found.")
+class QuotaClassNotFound(QuotaNotFound):
+ message = _("Quota class %(class_name)s could not be found.")
+
+
class SecurityGroupNotFound(NotFound):
message = _("Security group %(security_group_id)s not found.")
diff --git a/nova/quota.py b/nova/quota.py
index fc49de067..cc327122d 100644
--- a/nova/quota.py
+++ b/nova/quota.py
@@ -60,6 +60,11 @@ FLAGS = flags.FLAGS
FLAGS.register_opts(quota_opts)
+quota_resources = ['metadata_items', 'injected_file_content_bytes',
+ 'volumes', 'gigabytes', 'ram', 'floating_ips', 'instances',
+ 'injected_files', 'cores']
+
+
def _get_default_quotas():
defaults = {
'instances': FLAGS.quota_instances,
@@ -80,13 +85,29 @@ def _get_default_quotas():
return defaults
+def get_class_quotas(context, quota_class, defaults=None):
+ """Update defaults with the quota class values."""
+
+ if not defaults:
+ defaults = _get_default_quotas()
+
+ quota = db.quota_class_get_all_by_name(context, quota_class)
+ for key in defaults.keys():
+ if key in quota:
+ defaults[key] = quota[key]
+
+ return defaults
+
+
def get_project_quotas(context, project_id):
- rval = _get_default_quotas()
+ defaults = _get_default_quotas()
+ if context.quota_class:
+ get_class_quotas(context, context.quota_class, defaults)
quota = db.quota_get_all_by_project(context, project_id)
- for key in rval.keys():
+ for key in defaults.keys():
if key in quota:
- rval[key] = quota[key]
- return rval
+ defaults[key] = quota[key]
+ return defaults
def _get_request_allotment(requested, used, quota):
diff --git a/nova/tests/api/openstack/compute/contrib/test_quota_classes.py b/nova/tests/api/openstack/compute/contrib/test_quota_classes.py
new file mode 100644
index 000000000..e29d266e3
--- /dev/null
+++ b/nova/tests/api/openstack/compute/contrib/test_quota_classes.py
@@ -0,0 +1,163 @@
+# 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.
+
+import webob
+from lxml import etree
+
+from nova.api.openstack import wsgi
+from nova.api.openstack.compute.contrib import quota_classes
+from nova import test
+from nova.tests.api.openstack import fakes
+
+
+def quota_set(class_name):
+ return {'quota_class_set': {'id': class_name, 'metadata_items': 128,
+ 'volumes': 10, 'gigabytes': 1000, 'ram': 51200,
+ 'floating_ips': 10, 'instances': 10, 'injected_files': 5,
+ 'cores': 20, 'injected_file_content_bytes': 10240}}
+
+
+class QuotaClassSetsTest(test.TestCase):
+
+ def setUp(self):
+ super(QuotaClassSetsTest, self).setUp()
+ self.controller = quota_classes.QuotaClassSetsController()
+
+ def test_format_quota_set(self):
+ raw_quota_set = {
+ 'instances': 10,
+ 'cores': 20,
+ 'ram': 51200,
+ 'volumes': 10,
+ 'floating_ips': 10,
+ 'metadata_items': 128,
+ 'gigabytes': 1000,
+ 'injected_files': 5,
+ 'injected_file_content_bytes': 10240}
+
+ quota_set = self.controller._format_quota_set('test_class',
+ raw_quota_set)
+ qs = quota_set['quota_class_set']
+
+ self.assertEqual(qs['id'], 'test_class')
+ self.assertEqual(qs['instances'], 10)
+ self.assertEqual(qs['cores'], 20)
+ self.assertEqual(qs['ram'], 51200)
+ self.assertEqual(qs['volumes'], 10)
+ self.assertEqual(qs['gigabytes'], 1000)
+ self.assertEqual(qs['floating_ips'], 10)
+ self.assertEqual(qs['metadata_items'], 128)
+ self.assertEqual(qs['injected_files'], 5)
+ self.assertEqual(qs['injected_file_content_bytes'], 10240)
+
+ def test_quotas_show_as_admin(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class',
+ use_admin_context=True)
+ res_dict = self.controller.show(req, 'test_class')
+
+ self.assertEqual(res_dict, quota_set('test_class'))
+
+ def test_quotas_show_as_unauthorized_user(self):
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class')
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.show,
+ req, 'test_class')
+
+ def test_quotas_update_as_admin(self):
+ body = {'quota_class_set': {'instances': 50, 'cores': 50,
+ 'ram': 51200, 'volumes': 10,
+ 'gigabytes': 1000, 'floating_ips': 10,
+ 'metadata_items': 128, 'injected_files': 5,
+ 'injected_file_content_bytes': 10240}}
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class',
+ use_admin_context=True)
+ res_dict = self.controller.update(req, 'test_class', body)
+
+ self.assertEqual(res_dict, body)
+
+ def test_quotas_update_as_user(self):
+ body = {'quota_class_set': {'instances': 50, 'cores': 50,
+ 'ram': 51200, 'volumes': 10,
+ 'gigabytes': 1000, 'floating_ips': 10,
+ 'metadata_items': 128, 'injected_files': 5,
+ 'injected_file_content_bytes': 10240}}
+
+ req = fakes.HTTPRequest.blank(
+ '/v2/fake4/os-quota-class-sets/test_class')
+ self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
+ req, 'test_class', body)
+
+
+class QuotaTemplateXMLSerializerTest(test.TestCase):
+ def setUp(self):
+ super(QuotaTemplateXMLSerializerTest, self).setUp()
+ self.serializer = quota_classes.QuotaClassTemplate()
+ self.deserializer = wsgi.XMLDeserializer()
+
+ def test_serializer(self):
+ exemplar = dict(quota_class_set=dict(
+ id='test_class',
+ metadata_items=10,
+ injected_file_content_bytes=20,
+ volumes=30,
+ gigabytes=40,
+ ram=50,
+ floating_ips=60,
+ instances=70,
+ injected_files=80,
+ cores=90))
+ text = self.serializer.serialize(exemplar)
+
+ print text
+ tree = etree.fromstring(text)
+
+ self.assertEqual('quota_class_set', tree.tag)
+ self.assertEqual('test_class', tree.get('id'))
+ self.assertEqual(len(exemplar['quota_class_set']) - 1, len(tree))
+ for child in tree:
+ self.assertTrue(child.tag in exemplar['quota_class_set'])
+ self.assertEqual(int(child.text),
+ exemplar['quota_class_set'][child.tag])
+
+ def test_deserializer(self):
+ exemplar = dict(quota_class_set=dict(
+ metadata_items='10',
+ injected_file_content_bytes='20',
+ volumes='30',
+ gigabytes='40',
+ ram='50',
+ floating_ips='60',
+ instances='70',
+ injected_files='80',
+ cores='90'))
+ intext = ("<?xml version='1.0' encoding='UTF-8'?>\n"
+ '<quota_class_set>'
+ '<metadata_items>10</metadata_items>'
+ '<injected_file_content_bytes>20'
+ '</injected_file_content_bytes>'
+ '<volumes>30</volumes>'
+ '<gigabytes>40</gigabytes>'
+ '<ram>50</ram>'
+ '<floating_ips>60</floating_ips>'
+ '<instances>70</instances>'
+ '<injected_files>80</injected_files>'
+ '<cores>90</cores>'
+ '</quota_class_set>')
+
+ result = self.deserializer.deserialize(intext)['body']
+ self.assertEqual(result, exemplar)
diff --git a/nova/tests/api/openstack/compute/contrib/test_quotas.py b/nova/tests/api/openstack/compute/contrib/test_quotas.py
index 980871779..46753b883 100644
--- a/nova/tests/api/openstack/compute/contrib/test_quotas.py
+++ b/nova/tests/api/openstack/compute/contrib/test_quotas.py
@@ -31,11 +31,6 @@ def quota_set(id):
'injected_file_content_bytes': 10240}}
-def quota_set_list():
- return {'quota_set_list': [quota_set('1234'), quota_set('5678'),
- quota_set('update_me')]}
-
-
class QuotaSetsTest(test.TestCase):
def setUp(self):
@@ -54,8 +49,7 @@ class QuotaSetsTest(test.TestCase):
'injected_files': 5,
'injected_file_content_bytes': 10240}
- quota_set = quotas.QuotaSetsController()._format_quota_set('1234',
- raw_quota_set)
+ quota_set = self.controller._format_quota_set('1234', raw_quota_set)
qs = quota_set['quota_set']
self.assertEqual(qs['id'], '1234')
diff --git a/nova/tests/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py
index 62136fd5d..d9b5379c0 100644
--- a/nova/tests/api/openstack/compute/test_extensions.py
+++ b/nova/tests/api/openstack/compute/test_extensions.py
@@ -173,6 +173,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"Keypairs",
"Multinic",
"Networks",
+ "QuotaClasses",
"Quotas",
"Rescue",
"SchedulerHints",
diff --git a/nova/tests/policy.json b/nova/tests/policy.json
index d2e647f9f..e726b8bf0 100644
--- a/nova/tests/policy.json
+++ b/nova/tests/policy.json
@@ -101,6 +101,7 @@
"compute_extension:multinic": [],
"compute_extension:networks": [],
"compute_extension:quotas": [],
+ "compute_extension:quota_classes": [],
"compute_extension:rescue": [],
"compute_extension:security_groups": [],
"compute_extension:server_action_list": [],
diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py
index a3ec9727b..ca4fd265c 100644
--- a/nova/tests/test_quota.py
+++ b/nova/tests/test_quota.py
@@ -32,6 +32,196 @@ from nova.scheduler import driver as scheduler_driver
FLAGS = flags.FLAGS
+class GetQuotaTestCase(test.TestCase):
+ def setUp(self):
+ super(GetQuotaTestCase, self).setUp()
+ self.flags(quota_instances=10,
+ quota_cores=20,
+ quota_ram=50 * 1024,
+ quota_volumes=10,
+ quota_gigabytes=1000,
+ quota_floating_ips=10,
+ quota_metadata_items=128,
+ quota_max_injected_files=5,
+ quota_max_injected_file_content_bytes=10 * 1024)
+ self.context = context.RequestContext('admin', 'admin', is_admin=True)
+
+ def _stub_class(self):
+ def fake_quota_class_get_all_by_name(context, quota_class):
+ result = dict(class_name=quota_class)
+ if quota_class == 'test_class':
+ result.update(
+ instances=5,
+ cores=10,
+ ram=25 * 1024,
+ volumes=5,
+ gigabytes=500,
+ floating_ips=5,
+ metadata_items=64,
+ injected_files=2,
+ injected_file_content_bytes=5 * 1024,
+ invalid_quota=100,
+ )
+ return result
+
+ self.stubs.Set(db, 'quota_class_get_all_by_name',
+ fake_quota_class_get_all_by_name)
+
+ def _stub_project(self, override=False):
+ def fake_quota_get_all_by_project(context, project_id):
+ result = dict(project_id=project_id)
+ if override:
+ result.update(
+ instances=2,
+ cores=5,
+ ram=12 * 1024,
+ volumes=2,
+ gigabytes=250,
+ floating_ips=2,
+ metadata_items=32,
+ injected_files=1,
+ injected_file_content_bytes=2 * 1024,
+ invalid_quota=50,
+ )
+ return result
+
+ self.stubs.Set(db, 'quota_get_all_by_project',
+ fake_quota_get_all_by_project)
+
+ def test_default_quotas(self):
+ result = quota._get_default_quotas()
+ self.assertEqual(result, dict(
+ instances=10,
+ cores=20,
+ ram=50 * 1024,
+ volumes=10,
+ gigabytes=1000,
+ floating_ips=10,
+ metadata_items=128,
+ injected_files=5,
+ injected_file_content_bytes=10 * 1024,
+ ))
+
+ def test_default_quotas_unlimited(self):
+ self.flags(quota_instances=-1,
+ quota_cores=-1,
+ quota_ram=-1,
+ quota_volumes=-1,
+ quota_gigabytes=-1,
+ quota_floating_ips=-1,
+ quota_metadata_items=-1,
+ quota_max_injected_files=-1,
+ quota_max_injected_file_content_bytes=-1)
+ result = quota._get_default_quotas()
+ self.assertEqual(result, dict(
+ instances=None,
+ cores=None,
+ ram=None,
+ volumes=None,
+ gigabytes=None,
+ floating_ips=None,
+ metadata_items=None,
+ injected_files=None,
+ injected_file_content_bytes=None,
+ ))
+
+ def test_class_quotas_noclass(self):
+ self._stub_class()
+ result = quota.get_class_quotas(self.context, 'noclass')
+ self.assertEqual(result, dict(
+ instances=10,
+ cores=20,
+ ram=50 * 1024,
+ volumes=10,
+ gigabytes=1000,
+ floating_ips=10,
+ metadata_items=128,
+ injected_files=5,
+ injected_file_content_bytes=10 * 1024,
+ ))
+
+ def test_class_quotas(self):
+ self._stub_class()
+ result = quota.get_class_quotas(self.context, 'test_class')
+ self.assertEqual(result, dict(
+ instances=5,
+ cores=10,
+ ram=25 * 1024,
+ volumes=5,
+ gigabytes=500,
+ floating_ips=5,
+ metadata_items=64,
+ injected_files=2,
+ injected_file_content_bytes=5 * 1024,
+ ))
+
+ def test_project_quotas_defaults_noclass(self):
+ self._stub_class()
+ self._stub_project()
+ result = quota.get_project_quotas(self.context, 'admin')
+ self.assertEqual(result, dict(
+ instances=10,
+ cores=20,
+ ram=50 * 1024,
+ volumes=10,
+ gigabytes=1000,
+ floating_ips=10,
+ metadata_items=128,
+ injected_files=5,
+ injected_file_content_bytes=10 * 1024,
+ ))
+
+ def test_project_quotas_overrides_noclass(self):
+ self._stub_class()
+ self._stub_project(True)
+ result = quota.get_project_quotas(self.context, 'admin')
+ self.assertEqual(result, dict(
+ instances=2,
+ cores=5,
+ ram=12 * 1024,
+ volumes=2,
+ gigabytes=250,
+ floating_ips=2,
+ metadata_items=32,
+ injected_files=1,
+ injected_file_content_bytes=2 * 1024,
+ ))
+
+ def test_project_quotas_defaults_withclass(self):
+ self._stub_class()
+ self._stub_project()
+ self.context.quota_class = 'test_class'
+ result = quota.get_project_quotas(self.context, 'admin')
+ self.assertEqual(result, dict(
+ instances=5,
+ cores=10,
+ ram=25 * 1024,
+ volumes=5,
+ gigabytes=500,
+ floating_ips=5,
+ metadata_items=64,
+ injected_files=2,
+ injected_file_content_bytes=5 * 1024,
+ ))
+
+ def test_project_quotas_overrides_withclass(self):
+ self._stub_class()
+ self._stub_project(True)
+ self.context.quota_class = 'test_class'
+ result = quota.get_project_quotas(self.context, 'admin')
+ self.assertEqual(result, dict(
+ instances=2,
+ cores=5,
+ ram=12 * 1024,
+ volumes=2,
+ gigabytes=250,
+ floating_ips=2,
+ metadata_items=32,
+ injected_files=1,
+ injected_file_content_bytes=2 * 1024,
+ ))
+
+
class QuotaTestCase(test.TestCase):
class StubImageService(object):