Add quota per-tenant.

blueprint quantum-api-quotas

We implement it as an extension for linux bridge and ovs plugins.
We also expose the /quotas/Xx url to client to operate the quota.
We need admin role to show other tenant's quota, and to update quota data.
Any user can show its own tenant's quota. An DB table is used to save the
quota for each tenant.

To use it, we have in quantum.conf:
quota_driver = quantum.extensions._quotav2_driver.DbQuotaDriver

The default quotas for each tenant are defined in quantum.conf too.

In addition, modify extension framework to allow exposing a new resource and
its controler. The extension can check the environment, such as configuration
in global cfg.CONF to decide if it can be enabled.

Also, we can define enabled extensions for each plugin in extensions.py
New resources can be put into quota framework via quota_items in nova.conf

Change-Id: I54d6107fdb2808cdae1a40b501ed8c7f379dedee
This commit is contained in:
Yong Sheng Gong 2012-07-29 20:59:55 +08:00
parent 0ec3ef5003
commit 46cac19852
12 changed files with 727 additions and 67 deletions

View File

@ -125,13 +125,19 @@ control_exchange = quantum
# rpc_zmq_bind_address = *
[QUOTAS]
# number of networks allowed per tenant
# resource name(s) that are supported in quota features
# quota_items = network,subnet,port
# default number of resource allowed per tenant, minus for unlimited
# default_quota = -1
# number of networks allowed per tenant, and minus means unlimited
# quota_network = 10
# number of subnets allowed per tenant
# number of subnets allowed per tenant, and minus means unlimited
# quota_subnet = 10
# number of ports allowed per tenant
# number of ports allowed per tenant, and minus means unlimited
# quota_port = 50
# default driver to use for quota checks

View File

@ -265,7 +265,9 @@ class Controller(object):
self._resource + '.create.start',
notifier_api.INFO,
body)
body = self._prepare_request_body(request.context, body, True)
body = Controller.prepare_request_body(request.context, body, True,
self._resource, self._attr_info,
allow_bulk=self._allow_bulk)
action = "create_%s" % self._resource
# Check authz
try:
@ -280,11 +282,20 @@ class Controller(object):
action,
item[self._resource],
plugin=self._plugin)
count = QUOTAS.count(request.context, self._resource,
self._plugin, self._collection,
item[self._resource]['tenant_id'])
kwargs = {self._resource: count + 1}
QUOTAS.limit_check(request.context, **kwargs)
try:
count = QUOTAS.count(request.context, self._resource,
self._plugin, self._collection,
item[self._resource]['tenant_id'])
kwargs = {self._resource: count + 1}
except exceptions.QuotaResourceUnknown as e:
# We don't want to quota this resource
LOG.debug(e)
except Exception:
raise
else:
QUOTAS.limit_check(request.context,
item[self._resource]['tenant_id'],
**kwargs)
else:
self._validate_network_tenant_ownership(
request,
@ -294,11 +305,20 @@ class Controller(object):
action,
body[self._resource],
plugin=self._plugin)
count = QUOTAS.count(request.context, self._resource,
self._plugin, self._collection,
body[self._resource]['tenant_id'])
kwargs = {self._resource: count + 1}
QUOTAS.limit_check(request.context, **kwargs)
try:
count = QUOTAS.count(request.context, self._resource,
self._plugin, self._collection,
body[self._resource]['tenant_id'])
kwargs = {self._resource: count + 1}
except exceptions.QuotaResourceUnknown as e:
# We don't want to quota this resource
LOG.debug(e)
except Exception:
raise
else:
QUOTAS.limit_check(request.context,
body[self._resource]['tenant_id'],
**kwargs)
except exceptions.PolicyNotAuthorized:
LOG.exception("Create operation not authorized")
raise webob.exc.HTTPForbidden()
@ -366,7 +386,9 @@ class Controller(object):
self._resource + '.update.start',
notifier_api.INFO,
payload)
body = self._prepare_request_body(request.context, body, False)
body = Controller.prepare_request_body(request.context, body, False,
self._resource, self._attr_info,
allow_bulk=self._allow_bulk)
action = "update_%s" % self._resource
# Load object to check authz
# but pass only attributes in the original body and required
@ -399,7 +421,8 @@ class Controller(object):
result)
return result
def _populate_tenant_id(self, context, res_dict, is_create):
@staticmethod
def _populate_tenant_id(context, res_dict, is_create):
if (('tenant_id' in res_dict and
res_dict['tenant_id'] != context.tenant_id and
@ -416,7 +439,9 @@ class Controller(object):
" that tenant_id is specified")
raise webob.exc.HTTPBadRequest(msg)
def _prepare_request_body(self, context, body, is_create):
@staticmethod
def prepare_request_body(context, body, is_create, resource, attr_info,
allow_bulk=False):
""" verifies required attributes are in request body, and that
an attribute is only specified if it is allowed for the given
operation (create/update).
@ -425,35 +450,36 @@ class Controller(object):
body argument must be the deserialized body
"""
collection = resource + "s"
if not body:
raise webob.exc.HTTPBadRequest(_("Resource body required"))
body = body or {self._resource: {}}
if self._collection in body and self._allow_bulk:
bulk_body = [self._prepare_request_body(context,
{self._resource: b},
is_create)
if self._resource not in b
else self._prepare_request_body(context, b, is_create)
for b in body[self._collection]]
body = body or {resource: {}}
if collection in body and allow_bulk:
bulk_body = [Controller.prepare_request_body(
context, {resource: b}, is_create, resource, attr_info,
allow_bulk) if resource not in b
else Controller.prepare_request_body(
context, b, is_create, resource, attr_info, allow_bulk)
for b in body[collection]]
if not bulk_body:
raise webob.exc.HTTPBadRequest(_("Resources required"))
return {self._collection: bulk_body}
return {collection: bulk_body}
elif self._collection in body and not self._allow_bulk:
elif collection in body and not allow_bulk:
raise webob.exc.HTTPBadRequest("Bulk operation not supported")
res_dict = body.get(self._resource)
res_dict = body.get(resource)
if res_dict is None:
msg = _("Unable to find '%s' in request body") % self._resource
msg = _("Unable to find '%s' in request body") % resource
raise webob.exc.HTTPBadRequest(msg)
self._populate_tenant_id(context, res_dict, is_create)
Controller._populate_tenant_id(context, res_dict, is_create)
if is_create: # POST
for attr, attr_vals in self._attr_info.iteritems():
for attr, attr_vals in attr_info.iteritems():
is_required = ('default' not in attr_vals and
attr_vals['allow_post'])
if is_required and attr not in res_dict:
@ -469,12 +495,12 @@ class Controller(object):
res_dict[attr] = res_dict.get(attr,
attr_vals.get('default'))
else: # PUT
for attr, attr_vals in self._attr_info.iteritems():
for attr, attr_vals in attr_info.iteritems():
if attr in res_dict and not attr_vals['allow_put']:
msg = _("Cannot update read-only attribute %s") % attr
raise webob.exc.HTTPUnprocessableEntity(msg)
for attr, attr_vals in self._attr_info.iteritems():
for attr, attr_vals in attr_info.iteritems():
# Convert values if necessary
if ('convert_to' in attr_vals and
attr in res_dict and

View File

@ -213,3 +213,7 @@ class InvalidQuotaValue(QuantumException):
class InvalidSharedSetting(QuantumException):
message = _("Unable to reconfigure sharing settings for network"
"%(network). Multiple tenants are using it")
class InvalidExtenstionEnv(QuantumException):
message = _("Invalid extension environment: %(reason)s")

View File

@ -0,0 +1,158 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from quantum.common import exceptions
from quantum.extensions import _quotav2_model as quotav2_model
class DbQuotaDriver(object):
"""
Driver to perform necessary checks to enforce quotas and obtain
quota information. The default driver utilizes the local
database.
"""
@staticmethod
def get_tenant_quotas(context, resources, tenant_id):
"""
Given a list of resources, retrieve the quotas for the given
tenant.
:param context: The request context, for access checks.
:param resources: A dictionary of the registered resource keys.
:param tenant_id: The ID of the tenant to return quotas for.
:return dict: from resource name to dict of name and limit
"""
quotas = {}
tenant_quotas = context.session.query(
quotav2_model.Quota).filter_by(tenant_id=tenant_id).all()
tenant_quotas_dict = {}
for _quota in tenant_quotas:
tenant_quotas_dict[_quota['resource']] = _quota['limit']
for key, resource in resources.items():
quotas[key] = dict(
name=key,
limit=tenant_quotas_dict.get(key, resource.default))
return quotas
@staticmethod
def delete_tenant_quota(context, tenant_id):
"""Delete the quota entries for a given tenant_id.
Atfer deletion, this tenant will use default quota values in conf.
"""
with context.session.begin():
tenant_quotas = context.session.query(
quotav2_model.Quota).filter_by(tenant_id=tenant_id).all()
for quota in tenant_quotas:
context.session.delete(quota)
@staticmethod
def get_all_quotas(context, resources):
"""
Given a list of resources, retrieve the quotas for the all
tenants.
:param context: The request context, for access checks.
:param resources: A dictionary of the registered resource keys.
:return quotas: list of dict of tenant_id:, resourcekey1:
resourcekey2: ...
"""
_quotas = context.session.query(quotav2_model.Quota).all()
quotas = {}
tenant_quotas_dict = {}
for _quota in _quotas:
tenant_id = _quota['tenant_id']
if tenant_id not in quotas:
quotas[tenant_id] = {'tenant_id': tenant_id}
tenant_quotas_dict = quotas[tenant_id]
tenant_quotas_dict[_quota['resource']] = _quota['limit']
# we complete the quotas according to input resources
for tenant_quotas_dict in quotas.itervalues():
for key, resource in resources.items():
tenant_quotas_dict[key] = tenant_quotas_dict.get(
key, resource.default)
return quotas.itervalues()
def _get_quotas(self, context, tenant_id, resources, keys):
"""
A helper method which retrieves the quotas for the specific
resources identified by keys, and which apply to the current
context.
:param context: The request context, for access checks.
:param tenant_id: the tenant_id to check quota.
:param resources: A dictionary of the registered resources.
:param keys: A list of the desired quotas to retrieve.
"""
desired = set(keys)
sub_resources = dict((k, v) for k, v in resources.items()
if k in desired)
# Make sure we accounted for all of them...
if len(keys) != len(sub_resources):
unknown = desired - set(sub_resources.keys())
raise exceptions.QuotaResourceUnknown(unknown=sorted(unknown))
# Grab and return the quotas (without usages)
quotas = DbQuotaDriver.get_tenant_quotas(
context, sub_resources, context.tenant_id)
return dict((k, v['limit']) for k, v in quotas.items())
def limit_check(self, context, tenant_id, resources, values):
"""Check simple quota limits.
For limits--those quotas for which there is no usage
synchronization function--this method checks that a set of
proposed values are permitted by the limit restriction.
This method will raise a QuotaResourceUnknown exception if a
given resource is unknown or if it is not a simple limit
resource.
If any of the proposed values is over the defined quota, an
OverQuota exception will be raised with the sorted list of the
resources which are too high. Otherwise, the method returns
nothing.
:param context: The request context, for access checks.
:param tenant_id: The tenant_id to check the quota.
:param resources: A dictionary of the registered resources.
:param values: A dictionary of the values to check against the
quota.
"""
# Ensure no value is less than zero
unders = [key for key, val in values.items() if val < 0]
if unders:
raise exceptions.InvalidQuotaValue(unders=sorted(unders))
# Get the applicable quotas
quotas = self._get_quotas(context, tenant_id, resources, values.keys())
# Check the quotas and construct a list of the resources that
# would be put over limit by the desired values
overs = [key for key, val in values.items()
if quotas[key] >= 0 and quotas[key] < val]
if overs:
raise exceptions.OverQuota(overs=sorted(overs))

View File

@ -0,0 +1,30 @@
# Copyright (c) 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.
import sqlalchemy as sa
from quantum.db import model_base
from quantum.db import models_v2
class Quota(model_base.BASEV2, models_v2.HasId):
"""Represent a single quota override for a tenant.
If there is no row for a given tenant id and resource, then the
default for the quota class is used.
"""
tenant_id = sa.Column(sa.String(255), index=True)
resource = sa.Column(sa.String(255))
limit = sa.Column(sa.Integer)

View File

@ -30,11 +30,28 @@ from quantum.common import exceptions
import quantum.extensions
from quantum.manager import QuantumManager
from quantum.openstack.common import cfg
from quantum.openstack.common import importutils
from quantum import wsgi
LOG = logging.getLogger('quantum.api.extensions')
# Besides the supported_extension_aliases in plugin class,
# we also support register enabled extensions here so that we
# can load some mandatory files (such as db models) before initialize plugin
ENABLED_EXTS = {
'quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2':
{
'ext_alias': ["quotas"],
'ext_db_models': ['quantum.extensions._quotav2_model.Quota'],
},
'quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2':
{
'ext_alias': ["quotas"],
'ext_db_models': ['quantum.extensions._quotav2_model.Quota'],
},
}
class PluginInterface(object):
__metaclass__ = ABCMeta
@ -132,8 +149,8 @@ class ExtensionDescriptor(object):
request_exts = []
return request_exts
def get_extended_attributes(self, version):
"""Map describing extended attributes for core resources.
def get_extended_resources(self, version):
"""retrieve extended resources or attributes for core resources.
Extended attributes are implemented by a core plugin similarly
to the attributes defined in the core, and can appear in
@ -143,6 +160,9 @@ class ExtensionDescriptor(object):
map[<resource_name>][<attribute_name>][<attribute_property>]
specifying the extended resource attribute properties required
by that API version.
Extension can add resources and their attr definitions too.
The returned map can be integrated into RESOURCE_ATTRIBUTE_MAP.
"""
return {}
@ -281,7 +301,6 @@ class ExtensionMiddleware(wsgi.Middleware):
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
mapper)
super(ExtensionMiddleware, self).__init__(application)
@classmethod
@ -411,12 +430,22 @@ class ExtensionManager(object):
return request_exts
def extend_resources(self, version, attr_map):
"""Extend resources with additional attributes."""
"""Extend resources with additional resources or attributes.
:param: attr_map, the existing mapping from resource name to
attrs definition.
After this function, we will extend the attr_map if an extension
wants to extend this map.
"""
for ext in self.extensions.itervalues():
try:
extended_attrs = ext.get_extended_attributes(version)
extended_attrs = ext.get_extended_resources(version)
for resource, resource_attrs in extended_attrs.iteritems():
attr_map[resource].update(resource_attrs)
if attr_map.get(resource, None):
attr_map[resource].update(resource_attrs)
else:
attr_map[resource] = resource_attrs
except AttributeError:
# Extensions aren't required to have extended
# attributes
@ -433,6 +462,12 @@ class ExtensionManager(object):
except AttributeError as ex:
LOG.exception(_("Exception loading extension: %s"), unicode(ex))
return False
if hasattr(extension, 'check_env'):
try:
extension.check_env()
except exceptions.InvalidExtenstionEnv as ex:
LOG.warn(_("Exception loading extension: %s"), unicode(ex))
return False
return True
def _load_all_extensions(self):
@ -511,6 +546,10 @@ class PluginAwareExtensionManager(ExtensionManager):
supports_extension = (hasattr(self.plugin,
"supported_extension_aliases") and
alias in self.plugin.supported_extension_aliases)
plugin_provider = cfg.CONF.core_plugin
if not supports_extension and plugin_provider in ENABLED_EXTS:
supports_extension = (alias in
ENABLED_EXTS[plugin_provider]['ext_alias'])
if not supports_extension:
LOG.warn("extension %s not supported by plugin %s",
alias, self.plugin)
@ -531,6 +570,11 @@ class PluginAwareExtensionManager(ExtensionManager):
@classmethod
def get_instance(cls):
if cls._instance is None:
plugin_provider = cfg.CONF.core_plugin
if plugin_provider in ENABLED_EXTS:
for model in ENABLED_EXTS[plugin_provider]['ext_db_models']:
LOG.debug('loading model %s', model)
model_class = importutils.import_class(model)
cls._instance = cls(get_extensions_path(),
QuantumManager.get_plugin())
return cls._instance

View File

@ -72,7 +72,7 @@ class Providernet(object):
def get_updated(cls):
return "2012-07-23T10:00:00-00:00"
def get_extended_attributes(self, version):
def get_extended_resources(self, version):
if version == "2.0":
return EXTENDED_ATTRIBUTES_2_0
else:

View File

@ -0,0 +1,171 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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 quantum.api.v2 import base
from quantum.common import exceptions
from quantum.extensions import extensions
from quantum.extensions import _quotav2_driver as quotav2_driver
from quantum.extensions import _quotav2_model as quotav2_model
from quantum.manager import QuantumManager
from quantum.openstack.common import cfg
from quantum import quota
from quantum import wsgi
RESOURCE_NAME = 'quota'
RESOURCE_COLLECTION = RESOURCE_NAME + "s"
QUOTAS = quota.QUOTAS
DB_QUOTA_DRIVER = 'quantum.extensions._quotav2_driver.DbQuotaDriver'
EXTENDED_ATTRIBUTES_2_0 = {
RESOURCE_COLLECTION: {}
}
for quota_resource in QUOTAS.resources.iterkeys():
attr_dict = EXTENDED_ATTRIBUTES_2_0[RESOURCE_COLLECTION]
attr_dict[quota_resource] = {'allow_post': False,
'allow_put': True,
'convert_to': int,
'is_visible': True}
class QuotaSetsController(wsgi.Controller):
def __init__(self, plugin):
self._resource_name = RESOURCE_NAME
self._plugin = plugin
def _get_body(self, request):
body = self._deserialize(request.body, request.get_content_type())
attr_info = EXTENDED_ATTRIBUTES_2_0[RESOURCE_COLLECTION]
req_body = base.Controller.prepare_request_body(
request.context, body, False, self._resource_name, attr_info)
return req_body
def _get_quotas(self, request, tenant_id):
values = quotav2_driver.DbQuotaDriver.get_tenant_quotas(
request.context, QUOTAS.resources, tenant_id)
return dict((k, v['limit']) for k, v in values.items())
def create(self, request, body=None):
raise NotImplemented()
def index(self, request):
context = request.context
if not context.is_admin:
raise webob.exc.HTTPForbidden()
return {self._resource_name + "s":
quotav2_driver.DbQuotaDriver.get_all_quotas(
context, QUOTAS.resources)}
def tenant(self, request):
"""Retrieve the tenant info in context."""
context = request.context
if not context.tenant_id:
raise webob.exc.HTTPBadRequest('invalid tenant')
return {'tenant': {'tenant_id': context.tenant_id}}
def show(self, request, id):
context = request.context
tenant_id = id
if not tenant_id:
raise webob.exc.HTTPBadRequest('invalid tenant')
if (tenant_id != context.tenant_id and
not context.is_admin):
raise webob.exc.HTTPForbidden()
return {self._resource_name:
self._get_quotas(request, tenant_id)}
def _check_modification_delete_privilege(self, context, tenant_id):
if not tenant_id:
raise webob.exc.HTTPBadRequest('invalid tenant')
if (not context.is_admin):
raise webob.exc.HTTPForbidden()
return tenant_id
def delete(self, request, id):
tenant_id = id
tenant_id = self._check_modification_delete_privilege(request.context,
tenant_id)
quotav2_driver.DbQuotaDriver.delete_tenant_quota(request.context,
tenant_id)
def update(self, request, id):
tenant_id = id
tenant_id = self._check_modification_delete_privilege(request.context,
tenant_id)
req_body = self._get_body(request)
for key in req_body[self._resource_name].keys():
if key in QUOTAS.resources:
value = int(req_body[self._resource_name][key])
with request.context.session.begin():
tenant_quotas = request.context.session.query(
quotav2_model.Quota).filter_by(tenant_id=tenant_id,
resource=key).all()
if not tenant_quotas:
quota = quotav2_model.Quota(tenant_id=tenant_id,
resource=key,
limit=value)
request.context.session.add(quota)
else:
quota = tenant_quotas[0]
quota.update({'limit': value})
return {self._resource_name: self._get_quotas(request, tenant_id)}
class Quotasv2(object):
"""Quotas management support"""
@classmethod
def get_name(cls):
return "Quotas for each tenant"
@classmethod
def get_alias(cls):
return RESOURCE_COLLECTION
@classmethod
def get_description(cls):
return ("Expose functions for cloud admin to update quotas"
"for each tenant")
@classmethod
def get_namespace(cls):
return "http://docs.openstack.org/network/ext/quotas-sets/api/v2.0"
@classmethod
def get_updated(cls):
return "2012-07-29T10:00:00-00:00"
def get_extended_resources(self, version):
if version == "2.0":
return EXTENDED_ATTRIBUTES_2_0
else:
return {}
def check_env(self):
if cfg.CONF.QUOTAS.quota_driver != DB_QUOTA_DRIVER:
msg = _('quota driver %s is needed.') % DB_QUOTA_DRIVER
raise exceptions.InvalidExtenstionEnv(reason=msg)
@classmethod
def get_resources(cls):
""" Returns Ext Resources """
controller = QuotaSetsController(QuantumManager.get_plugin())
return [extensions.ResourceExtension(
Quotasv2.get_alias(),
controller,
collection_actions={'tenant': 'GET'})]

View File

@ -24,15 +24,24 @@ from quantum.openstack.common import importutils
LOG = logging.getLogger(__name__)
quota_opts = [
cfg.ListOpt('quota_items',
default=['network', 'subnet', 'port'],
help='resource name(s) that are supported in quota features'),
cfg.IntOpt('default_quota',
default=-1,
help='default number of resource allowed per tenant, '
'minus for unlimited'),
cfg.IntOpt('quota_network',
default=10,
help='number of networks allowed per tenant, -1 for unlimited'),
help='number of networks allowed per tenant,'
'minus for unlimited'),
cfg.IntOpt('quota_subnet',
default=10,
help='number of subnets allowed per tenant, -1 for unlimited'),
help='number of subnets allowed per tenant, '
'minus for unlimited'),
cfg.IntOpt('quota_port',
default=50,
help='number of ports allowed per tenant, -1 for unlimited'),
help='number of ports allowed per tenant, minus for unlimited'),
cfg.StrOpt('quota_driver',
default='quantum.quota.ConfDriver',
help='default driver to use for quota checks'),
@ -73,7 +82,8 @@ class ConfDriver(object):
quotas[resource.name] = resource.default
return quotas
def limit_check(self, context, resources, values):
def limit_check(self, context, tenant_id,
resources, values):
"""Check simple quota limits.
For limits--those quotas for which there is no usage
@ -90,6 +100,7 @@ class ConfDriver(object):
nothing.
:param context: The request context, for access checks.
:param tennant_id: The tenant_id to check quota.
:param resources: A dictionary of the registered resources.
:param values: A dictionary of the values to check against the
quota.
@ -115,14 +126,12 @@ class ConfDriver(object):
class BaseResource(object):
"""Describe a single resource for quota checking."""
def __init__(self, name, flag=None):
def __init__(self, name, flag):
"""
Initializes a Resource.
:param name: The name of the resource, i.e., "instances".
:param flag: The name of the flag or configuration option
which specifies the default value of the quota
for this resource.
"""
self.name = name
@ -131,8 +140,10 @@ class BaseResource(object):
@property
def default(self):
"""Return the default value of the quota."""
return cfg.CONF.QUOTAS[self.flag] if self.flag else -1
if hasattr(cfg.CONF.QUOTAS, self.flag):
return cfg.CONF.QUOTAS[self.flag]
else:
return cfg.CONF.QUOTAS.default_quota
class CountableResource(BaseResource):
@ -186,9 +197,17 @@ class QuotaEngine(object):
def register_resource(self, resource):
"""Register a resource."""
if resource.name in self._resources:
LOG.warn('%s is already registered.', resource.name)
return
self._resources[resource.name] = resource
def register_resource_by_name(self, resourcename):
"""Register a resource by name."""
resource = CountableResource(resourcename, _count_resource,
'quota_' + resourcename)
self.register_resource(resource)
def register_resources(self, resources):
"""Register a list of resources."""
@ -214,7 +233,7 @@ class QuotaEngine(object):
return res.count(context, *args, **kwargs)
def limit_check(self, context, **values):
def limit_check(self, context, tenant_id, **values):
"""Check simple quota limits.
For limits--those quotas for which there is no usage
@ -236,11 +255,12 @@ class QuotaEngine(object):
:param context: The request context, for access checks.
"""
return self._driver.limit_check(context, self._resources, values)
return self._driver.limit_check(context, tenant_id,
self._resources, values)
@property
def resources(self):
return sorted(self._resources.keys())
return self._resources
QUOTAS = QuotaEngine()
@ -252,11 +272,9 @@ def _count_resource(context, plugin, resources, tenant_id):
return len(obj_list) if obj_list else 0
resources = [
CountableResource('network', _count_resource, 'quota_network'),
CountableResource('subnet', _count_resource, 'quota_subnet'),
CountableResource('port', _count_resource, 'quota_port'),
]
resources = []
for resource_item in cfg.CONF.QUOTAS.quota_items:
resources.append(CountableResource(resource_item, _count_resource,
'quota_' + resource_item))
QUOTAS.register_resources(resources)

View File

@ -41,7 +41,7 @@ class V2attributes(object):
def get_updated(self):
return "2012-07-18T10:00:00-00:00"
def get_extended_attributes(self, version):
def get_extended_resources(self, version):
if version == "2.0":
return EXTENDED_ATTRIBUTES_2_0
else:

View File

@ -0,0 +1,190 @@
import unittest
import webtest
import mock
from quantum.api.v2 import attributes
from quantum.common import config
from quantum import context
from quantum.db import api as db
from quantum.extensions import extensions
from quantum import manager
from quantum.openstack.common import cfg
from quantum.plugins.linuxbridge.db import l2network_db_v2
from quantum import quota
from quantum.tests.unit import test_api_v2
from quantum.tests.unit import test_extensions
TARGET_PLUGIN = ('quantum.plugins.linuxbridge.lb_quantum_plugin'
'.LinuxBridgePluginV2')
_get_path = test_api_v2._get_path
class QuotaExtensionTestCase(unittest.TestCase):
def setUp(self):
if getattr(self, 'testflag', 1) == 1:
self._setUp1()
else:
self._setUp2()
def _setUp1(self):
db._ENGINE = None
db._MAKER = None
# Ensure 'stale' patched copies of the plugin are never returned
manager.QuantumManager._instance = None
# Ensure existing ExtensionManager is not used
extensions.PluginAwareExtensionManager._instance = None
# Save the global RESOURCE_ATTRIBUTE_MAP
self.saved_attr_map = {}
for resource, attrs in attributes.RESOURCE_ATTRIBUTE_MAP.iteritems():
self.saved_attr_map[resource] = attrs.copy()
# Create the default configurations
args = ['--config-file', test_extensions.etcdir('quantum.conf.test')]
config.parse(args=args)
# Update the plugin and extensions path
cfg.CONF.set_override('core_plugin', TARGET_PLUGIN)
cfg.CONF.set_override(
'quota_driver',
'quantum.extensions._quotav2_driver.DbQuotaDriver',
group='QUOTAS')
cfg.CONF.set_override(
'quota_items',
['network', 'subnet', 'port', 'extra1'],
group='QUOTAS')
self._plugin_patcher = mock.patch(TARGET_PLUGIN, autospec=True)
self.plugin = self._plugin_patcher.start()
# QUOTAS will regester the items in conf when starting
# extra1 here is added later, so have to do it manually
quota.QUOTAS.register_resource_by_name('extra1')
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
l2network_db_v2.initialize()
app = config.load_paste_app('extensions_test_app')
ext_middleware = extensions.ExtensionMiddleware(app, ext_mgr=ext_mgr)
self.api = webtest.TestApp(ext_middleware)
def _setUp2(self):
db._ENGINE = None
db._MAKER = None
# Ensure 'stale' patched copies of the plugin are never returned
manager.QuantumManager._instance = None
# Ensure existing ExtensionManager is not used
extensions.PluginAwareExtensionManager._instance = None
# Save the global RESOURCE_ATTRIBUTE_MAP
self.saved_attr_map = {}
for resource, attrs in attributes.RESOURCE_ATTRIBUTE_MAP.iteritems():
self.saved_attr_map[resource] = attrs.copy()
# Create the default configurations
args = ['--config-file', test_extensions.etcdir('quantum.conf.test')]
config.parse(args=args)
# Update the plugin and extensions path
cfg.CONF.set_override('core_plugin', TARGET_PLUGIN)
self._plugin_patcher = mock.patch(TARGET_PLUGIN, autospec=True)
self.plugin = self._plugin_patcher.start()
ext_mgr = extensions.PluginAwareExtensionManager.get_instance()
l2network_db_v2.initialize()
app = config.load_paste_app('extensions_test_app')
ext_middleware = extensions.ExtensionMiddleware(app, ext_mgr=ext_mgr)
self.api = webtest.TestApp(ext_middleware)
def tearDown(self):
self._plugin_patcher.stop()
self.api = None
self.plugin = None
db._ENGINE = None
db._MAKER = None
cfg.CONF.reset()
# Restore the global RESOURCE_ATTRIBUTE_MAP
attributes.RESOURCE_ATTRIBUTE_MAP = self.saved_attr_map
def test_quotas_loaded_right(self):
res = self.api.get(_get_path('quotas'))
self.assertEquals(200, res.status_int)
def test_quotas_defaul_values(self):
tenant_id = 'tenant_id1'
env = {'quantum.context': context.Context('', tenant_id)}
res = self.api.get(_get_path('quotas', id=tenant_id),
extra_environ=env)
self.assertEquals(10, res.json['quota']['network'])
self.assertEquals(10, res.json['quota']['subnet'])
self.assertEquals(50, res.json['quota']['port'])
self.assertEquals(-1, res.json['quota']['extra1'])
def test_show_quotas_with_admin(self):
tenant_id = 'tenant_id1'
env = {'quantum.context': context.Context('', tenant_id + '2',
is_admin=True)}
res = self.api.get(_get_path('quotas', id=tenant_id),
extra_environ=env)
self.assertEquals(200, res.status_int)
def test_show_quotas_without_admin_forbidden(self):
tenant_id = 'tenant_id1'
env = {'quantum.context': context.Context('', tenant_id + '2',
is_admin=False)}
res = self.api.get(_get_path('quotas', id=tenant_id),
extra_environ=env, expect_errors=True)
self.assertEquals(403, res.status_int)
def test_update_quotas_without_admin_forbidden(self):
tenant_id = 'tenant_id1'
env = {'quantum.context': context.Context('', tenant_id,
is_admin=False)}
quotas = {'quota': {'network': 100}}
res = self.api.put_json(_get_path('quotas', id=tenant_id,
fmt='json'),
quotas, extra_environ=env,
expect_errors=True)
self.assertEquals(403, res.status_int)
def test_update_quotas_with_admin(self):
tenant_id = 'tenant_id1'
env = {'quantum.context': context.Context('', tenant_id + '2',
is_admin=True)}
quotas = {'quota': {'network': 100}}
res = self.api.put_json(_get_path('quotas', id=tenant_id, fmt='json'),
quotas, extra_environ=env)
self.assertEquals(200, res.status_int)
env2 = {'quantum.context': context.Context('', tenant_id)}
res = self.api.get(_get_path('quotas', id=tenant_id),
extra_environ=env2).json
self.assertEquals(100, res['quota']['network'])
def test_delete_quotas_with_admin(self):
tenant_id = 'tenant_id1'
env = {'quantum.context': context.Context('', tenant_id + '2',
is_admin=True)}
res = self.api.delete(_get_path('quotas', id=tenant_id, fmt='json'),
extra_environ=env)
self.assertEquals(204, res.status_int)
def test_delete_quotas_without_admin_forbidden(self):
tenant_id = 'tenant_id1'
env = {'quantum.context': context.Context('', tenant_id,
is_admin=False)}
res = self.api.delete(_get_path('quotas', id=tenant_id, fmt='json'),
extra_environ=env, expect_errors=True)
self.assertEquals(403, res.status_int)
def test_quotas_loaded_bad(self):
self.testflag = 2
try:
res = self.api.get(_get_path('quotas'), expect_errors=True)
self.assertEquals(404, res.status_int)
except Exception:
pass
self.testflag = 1

View File

@ -32,6 +32,7 @@ import webob.dec
import webob.exc
from quantum.common import exceptions as exception
from quantum import context
from quantum.openstack.common import jsonutils
@ -180,6 +181,12 @@ class Request(webob.Request):
return type
return None
@property
def context(self):
if 'quantum.context' not in self.environ:
self.environ['quantum.context'] = context.get_admin_context()
return self.environ['quantum.context']
class ActionDispatcher(object):
"""Maps method name to local methods through action name."""
@ -894,14 +901,20 @@ class Controller(object):
arg_dict['request'] = req
result = method(**arg_dict)
if isinstance(result, dict):
content_type = req.best_match_content_type()
default_xmlns = self.get_default_xmlns(req)
body = self._serialize(result, content_type, default_xmlns)
if isinstance(result, dict) or result is None:
if result is None:
status = 204
content_type = ''
body = None
else:
status = 200
content_type = req.best_match_content_type()
default_xmlns = self.get_default_xmlns(req)
body = self._serialize(result, content_type, default_xmlns)
response = webob.Response()
response.headers['Content-Type'] = content_type
response.body = body
response = webob.Response(status=status,
content_type=content_type,
body=body)
msg_dict = dict(url=req.url, status=response.status_int)
msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
LOG.debug(msg)