From 6a38b650c001ec8e6da435856c37a28737401aaf Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Tue, 13 Mar 2012 17:13:02 -0500 Subject: Implement quota classes. Allows entire classes of quotas to be associated with projects, which makes it easier to set specific quotas across multiple projects. TODO: * (?) Adding a mapping between projects and quota classes Change-Id: I6b6477481187d16af225d33c1989430e4071d5a8 --- etc/nova/policy.json | 1 + .../api/openstack/compute/contrib/quota_classes.py | 99 +++++++++++ nova/api/openstack/compute/contrib/quotas.py | 11 +- nova/context.py | 7 +- nova/db/api.py | 33 ++++ nova/db/sqlalchemy/api.py | 83 +++++++++ .../migrate_repo/versions/083_quota_class.py | 61 +++++++ nova/db/sqlalchemy/models.py | 25 ++- nova/exception.py | 4 + nova/quota.py | 29 +++- .../compute/contrib/test_quota_classes.py | 163 ++++++++++++++++++ .../api/openstack/compute/contrib/test_quotas.py | 8 +- .../tests/api/openstack/compute/test_extensions.py | 1 + nova/tests/policy.json | 1 + nova/tests/test_quota.py | 190 +++++++++++++++++++++ 15 files changed, 692 insertions(+), 24 deletions(-) create mode 100644 nova/api/openstack/compute/contrib/quota_classes.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_quota_classes.py 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 2a38d026f..782bbdcdb 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 9eda43941..8b966336c 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 = ("\n" + '' + '10' + '20' + '' + '30' + '40' + '50' + '60' + '70' + '80' + '90' + '') + + 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): -- cgit