# 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
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:
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,
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()
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
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
" 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).
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:
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
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")
--- /dev/null
+# 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))
--- /dev/null
+# 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)
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
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
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 {}
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
mapper)
-
super(ExtensionMiddleware, self).__init__(application)
@classmethod
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
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):
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)
@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
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:
--- /dev/null
+# 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'})]
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'),
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
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.
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
@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):
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."""
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
: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()
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)
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:
--- /dev/null
+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
import webob.exc
from quantum.common import exceptions as exception
+from quantum import context
from quantum.openstack.common import jsonutils
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."""
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)
-
- response = webob.Response()
- response.headers['Content-Type'] = content_type
- response.body = body
+ 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(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)