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
This commit is contained in:
Kevin L. Mitchell 2012-03-13 17:13:02 -05:00
parent de6fdec710
commit 6a38b650c0
15 changed files with 694 additions and 26 deletions

View File

@ -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"]],

View File

@ -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

View File

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

View File

@ -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):

View File

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

View File

@ -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()

View File

@ -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

View File

@ -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'

View File

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

View File

@ -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_project_quotas(context, project_id):
rval = _get_default_quotas()
quota = db.quota_get_all_by_project(context, project_id)
for key in rval.keys():
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:
rval[key] = quota[key]
return rval
defaults[key] = quota[key]
return defaults
def get_project_quotas(context, project_id):
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 defaults.keys():
if key in quota:
defaults[key] = quota[key]
return defaults
def _get_request_allotment(requested, used, quota):

View File

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

View File

@ -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')

View File

@ -173,6 +173,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"Keypairs",
"Multinic",
"Networks",
"QuotaClasses",
"Quotas",
"Rescue",
"SchedulerHints",

View File

@ -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": [],

View File

@ -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):