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:
parent
de6fdec710
commit
6a38b650c0
@ -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"]],
|
||||
|
99
nova/api/openstack/compute/contrib/quota_classes.py
Normal file
99
nova/api/openstack/compute/contrib/quota_classes.py
Normal 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
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
61
nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py
Normal file
61
nova/db/sqlalchemy/migrate_repo/versions/083_quota_class.py
Normal 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
|
@ -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'
|
||||
|
@ -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.")
|
||||
|
||||
|
@ -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):
|
||||
|
163
nova/tests/api/openstack/compute/contrib/test_quota_classes.py
Normal file
163
nova/tests/api/openstack/compute/contrib/test_quota_classes.py
Normal 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)
|
@ -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')
|
||||
|
@ -173,6 +173,7 @@ class ExtensionControllerTest(ExtensionTestCase):
|
||||
"Keypairs",
|
||||
"Multinic",
|
||||
"Networks",
|
||||
"QuotaClasses",
|
||||
"Quotas",
|
||||
"Rescue",
|
||||
"SchedulerHints",
|
||||
|
@ -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": [],
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user