Add quota features into quantum.
Blueprint quantum-api-quotas We support quota check for creating network, subnet and port. Change-Id: I943335816308767c7eba084d80b969fcb2e5a8fb
This commit is contained in:
parent
50117fb2e8
commit
370bca0507
@ -30,3 +30,16 @@ api_paste_config = api-paste.ini
|
|||||||
|
|
||||||
# Maximum amount of retries to generate a unique MAC address
|
# Maximum amount of retries to generate a unique MAC address
|
||||||
# mac_generation_retries = 16
|
# mac_generation_retries = 16
|
||||||
|
|
||||||
|
[QUOTAS]
|
||||||
|
# number of networks allowed per tenant
|
||||||
|
# quota_network = 10
|
||||||
|
|
||||||
|
# number of subnets allowed per tenant
|
||||||
|
# quota_subnet = 10
|
||||||
|
|
||||||
|
# number of ports allowed per tenant
|
||||||
|
# quota_port = 50
|
||||||
|
|
||||||
|
# default driver to use for quota checks
|
||||||
|
# quota_driver = quantum.quota.ConfDriver
|
||||||
|
@ -22,6 +22,7 @@ from quantum.api.v2 import views
|
|||||||
from quantum.common import exceptions
|
from quantum.common import exceptions
|
||||||
from quantum.common import utils
|
from quantum.common import utils
|
||||||
from quantum import policy
|
from quantum import policy
|
||||||
|
from quantum import quota
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
|
XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
|
||||||
@ -37,6 +38,8 @@ FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
|
|||||||
exceptions.InvalidAllocationPool: webob.exc.HTTPBadRequest,
|
exceptions.InvalidAllocationPool: webob.exc.HTTPBadRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QUOTAS = quota.QUOTAS
|
||||||
|
|
||||||
|
|
||||||
def fields(request):
|
def fields(request):
|
||||||
"""
|
"""
|
||||||
@ -176,10 +179,8 @@ class Controller(object):
|
|||||||
|
|
||||||
def create(self, request, body=None):
|
def create(self, request, body=None):
|
||||||
"""Creates a new instance of the requested entity"""
|
"""Creates a new instance of the requested entity"""
|
||||||
|
|
||||||
body = self._prepare_request_body(request.context, body, True,
|
body = self._prepare_request_body(request.context, body, True,
|
||||||
allow_bulk=True)
|
allow_bulk=True)
|
||||||
|
|
||||||
action = "create_%s" % self._resource
|
action = "create_%s" % self._resource
|
||||||
|
|
||||||
# Check authz
|
# Check authz
|
||||||
@ -196,12 +197,22 @@ class Controller(object):
|
|||||||
action,
|
action,
|
||||||
item[self._resource],
|
item[self._resource],
|
||||||
)
|
)
|
||||||
|
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)
|
||||||
else:
|
else:
|
||||||
self._validate_network_tenant_ownership(
|
self._validate_network_tenant_ownership(
|
||||||
request,
|
request,
|
||||||
body[self._resource]
|
body[self._resource]
|
||||||
)
|
)
|
||||||
policy.enforce(request.context, action, body[self._resource])
|
policy.enforce(request.context, action, body[self._resource])
|
||||||
|
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)
|
||||||
except exceptions.PolicyNotAuthorized:
|
except exceptions.PolicyNotAuthorized:
|
||||||
raise webob.exc.HTTPForbidden()
|
raise webob.exc.HTTPForbidden()
|
||||||
|
|
||||||
|
@ -168,3 +168,16 @@ class PreexistingDeviceFailure(QuantumException):
|
|||||||
|
|
||||||
class SudoRequired(QuantumException):
|
class SudoRequired(QuantumException):
|
||||||
message = _("Sudo priviledge is required to run this command.")
|
message = _("Sudo priviledge is required to run this command.")
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaResourceUnknown(QuantumException):
|
||||||
|
message = _("Unknown quota resources %(unknown)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class OverQuota(QuantumException):
|
||||||
|
message = _("Quota exceeded for resources: %(overs)s")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidQuotaValue(QuantumException):
|
||||||
|
message = _("Change would make usage less than 0 for the following "
|
||||||
|
"resources: %(unders)s")
|
||||||
|
262
quantum/quota.py
Normal file
262
quantum/quota.py
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 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.
|
||||||
|
|
||||||
|
"""Quotas for instances, volumes, and floating ips."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from quantum.common import exceptions
|
||||||
|
from quantum.openstack.common import cfg
|
||||||
|
from quantum.openstack.common import importutils
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
quota_opts = [
|
||||||
|
cfg.IntOpt('quota_network',
|
||||||
|
default=10,
|
||||||
|
help='number of networks allowed per tenant, -1 for unlimited'),
|
||||||
|
cfg.IntOpt('quota_subnet',
|
||||||
|
default=10,
|
||||||
|
help='number of subnets allowed per tenant, -1 for unlimited'),
|
||||||
|
cfg.IntOpt('quota_port',
|
||||||
|
default=50,
|
||||||
|
help='number of ports allowed per tenant, -1 for unlimited'),
|
||||||
|
cfg.StrOpt('quota_driver',
|
||||||
|
default='quantum.quota.ConfDriver',
|
||||||
|
help='default driver to use for quota checks'),
|
||||||
|
]
|
||||||
|
# Register the configuration options
|
||||||
|
cfg.CONF.register_opts(quota_opts, 'QUOTAS')
|
||||||
|
|
||||||
|
|
||||||
|
class ConfDriver(object):
|
||||||
|
"""
|
||||||
|
Driver to perform necessary checks to enforce quotas and obtain
|
||||||
|
quota information. The default driver utilizes the default values
|
||||||
|
in quantum.conf.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _get_quotas(self, context, 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 resources: A dictionary of the registered resources.
|
||||||
|
:param keys: A list of the desired quotas to retrieve.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Filter resources
|
||||||
|
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))
|
||||||
|
quotas = {}
|
||||||
|
for resource in sub_resources.values():
|
||||||
|
quotas[resource.name] = resource.default
|
||||||
|
return quotas
|
||||||
|
|
||||||
|
def limit_check(self, context, 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 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, 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), quotas=quotas,
|
||||||
|
usages={})
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResource(object):
|
||||||
|
"""Describe a single resource for quota checking."""
|
||||||
|
|
||||||
|
def __init__(self, name, flag=None):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
self.flag = flag
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default(self):
|
||||||
|
"""Return the default value of the quota."""
|
||||||
|
|
||||||
|
return cfg.CONF.QUOTAS[self.flag] if self.flag else -1
|
||||||
|
|
||||||
|
|
||||||
|
class CountableResource(BaseResource):
|
||||||
|
"""Describe a resource where the counts are determined by a function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, count, flag=None):
|
||||||
|
"""Initializes a CountableResource.
|
||||||
|
|
||||||
|
Countable resources are those resources which directly
|
||||||
|
correspond to objects in the database, i.e., netowk, subnet,
|
||||||
|
etc.,. A CountableResource must be constructed with a counting
|
||||||
|
function, which will be called to determine the current counts
|
||||||
|
of the resource.
|
||||||
|
|
||||||
|
The counting function will be passed the context, along with
|
||||||
|
the extra positional and keyword arguments that are passed to
|
||||||
|
Quota.count(). It should return an integer specifying the
|
||||||
|
count.
|
||||||
|
|
||||||
|
:param name: The name of the resource, i.e., "instances".
|
||||||
|
:param count: A callable which returns the count of the
|
||||||
|
resource. The arguments passed are as described
|
||||||
|
above.
|
||||||
|
:param flag: The name of the flag or configuration option
|
||||||
|
which specifies the default value of the quota
|
||||||
|
for this resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super(CountableResource, self).__init__(name, flag=flag)
|
||||||
|
self.count = count
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaEngine(object):
|
||||||
|
"""Represent the set of recognized quotas."""
|
||||||
|
|
||||||
|
def __init__(self, quota_driver_class=None):
|
||||||
|
"""Initialize a Quota object."""
|
||||||
|
|
||||||
|
if not quota_driver_class:
|
||||||
|
quota_driver_class = cfg.CONF.QUOTAS.quota_driver
|
||||||
|
|
||||||
|
if isinstance(quota_driver_class, basestring):
|
||||||
|
quota_driver_class = importutils.import_object(quota_driver_class)
|
||||||
|
|
||||||
|
self._resources = {}
|
||||||
|
self._driver = quota_driver_class
|
||||||
|
|
||||||
|
def __contains__(self, resource):
|
||||||
|
return resource in self._resources
|
||||||
|
|
||||||
|
def register_resource(self, resource):
|
||||||
|
"""Register a resource."""
|
||||||
|
|
||||||
|
self._resources[resource.name] = resource
|
||||||
|
|
||||||
|
def register_resources(self, resources):
|
||||||
|
"""Register a list of resources."""
|
||||||
|
|
||||||
|
for resource in resources:
|
||||||
|
self.register_resource(resource)
|
||||||
|
|
||||||
|
def count(self, context, resource, *args, **kwargs):
|
||||||
|
"""Count a resource.
|
||||||
|
|
||||||
|
For countable resources, invokes the count() function and
|
||||||
|
returns its result. Arguments following the context and
|
||||||
|
resource are passed directly to the count function declared by
|
||||||
|
the resource.
|
||||||
|
|
||||||
|
:param context: The request context, for access checks.
|
||||||
|
:param resource: The name of the resource, as a string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get the resource
|
||||||
|
res = self._resources.get(resource)
|
||||||
|
if not res or not hasattr(res, 'count'):
|
||||||
|
raise exceptions.QuotaResourceUnknown(unknown=[resource])
|
||||||
|
|
||||||
|
return res.count(context, *args, **kwargs)
|
||||||
|
|
||||||
|
def limit_check(self, context, **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. The
|
||||||
|
values to check are given as keyword arguments, where the key
|
||||||
|
identifies the specific quota limit to check, and the value is
|
||||||
|
the proposed value.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._driver.limit_check(context, self._resources, values)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def resources(self):
|
||||||
|
return sorted(self._resources.keys())
|
||||||
|
|
||||||
|
|
||||||
|
QUOTAS = QuotaEngine()
|
||||||
|
|
||||||
|
|
||||||
|
def _count_resource(context, plugin, resources, tenant_id):
|
||||||
|
obj_getter = getattr(plugin, "get_%s" % resources)
|
||||||
|
obj_list = obj_getter(context, filters={'tenant_id': [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'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
QUOTAS.register_resources(resources)
|
@ -128,11 +128,7 @@ class ResourceIndexTestCase(unittest.TestCase):
|
|||||||
self.assertTrue(link['rel'] == 'self')
|
self.assertTrue(link['rel'] == 'self')
|
||||||
|
|
||||||
|
|
||||||
class APIv2TestCase(unittest.TestCase):
|
class APIv2TestBase(unittest.TestCase):
|
||||||
# NOTE(jkoelker) This potentially leaks the mock object if the setUp
|
|
||||||
# raises without being caught. Using unittest2
|
|
||||||
# or dropping 2.6 support so we can use addCleanup
|
|
||||||
# will get around this.
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
plugin = 'quantum.quantum_plugin_base_v2.QuantumPluginBaseV2'
|
plugin = 'quantum.quantum_plugin_base_v2.QuantumPluginBaseV2'
|
||||||
# Ensure 'stale' patched copies of the plugin are never returned
|
# Ensure 'stale' patched copies of the plugin are never returned
|
||||||
@ -155,6 +151,12 @@ class APIv2TestCase(unittest.TestCase):
|
|||||||
self.plugin = None
|
self.plugin = None
|
||||||
cfg.CONF.reset()
|
cfg.CONF.reset()
|
||||||
|
|
||||||
|
|
||||||
|
class APIv2TestCase(APIv2TestBase):
|
||||||
|
# NOTE(jkoelker) This potentially leaks the mock object if the setUp
|
||||||
|
# raises without being caught. Using unittest2
|
||||||
|
# or dropping 2.6 support so we can use addCleanup
|
||||||
|
# will get around this.
|
||||||
def test_verbose_attr(self):
|
def test_verbose_attr(self):
|
||||||
instance = self.plugin.return_value
|
instance = self.plugin.return_value
|
||||||
instance.get_networks.return_value = []
|
instance.get_networks.return_value = []
|
||||||
@ -387,7 +389,7 @@ class APIv2TestCase(unittest.TestCase):
|
|||||||
|
|
||||||
# Note: since all resources use the same controller and validation
|
# Note: since all resources use the same controller and validation
|
||||||
# logic, we actually get really good coverage from testing just networks.
|
# logic, we actually get really good coverage from testing just networks.
|
||||||
class JSONV2TestCase(APIv2TestCase):
|
class JSONV2TestCase(APIv2TestBase):
|
||||||
|
|
||||||
def _test_list(self, req_tenant_id, real_tenant_id):
|
def _test_list(self, req_tenant_id, real_tenant_id):
|
||||||
env = {}
|
env = {}
|
||||||
@ -729,3 +731,42 @@ class V2Views(unittest.TestCase):
|
|||||||
keys = ('id', 'network_id', 'tenant_id', 'gateway_ip',
|
keys = ('id', 'network_id', 'tenant_id', 'gateway_ip',
|
||||||
'ip_version', 'cidr')
|
'ip_version', 'cidr')
|
||||||
self._view(keys, views.subnet)
|
self._view(keys, views.subnet)
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaTest(APIv2TestBase):
|
||||||
|
def test_create_network_quota(self):
|
||||||
|
cfg.CONF.set_override('quota_network', 1, group='QUOTAS')
|
||||||
|
net_id = _uuid()
|
||||||
|
initial_input = {'network': {'name': 'net1', 'tenant_id': _uuid()}}
|
||||||
|
full_input = {'network': {'admin_state_up': True, 'subnets': []}}
|
||||||
|
full_input['network'].update(initial_input['network'])
|
||||||
|
|
||||||
|
return_value = {'id': net_id, 'status': "ACTIVE"}
|
||||||
|
return_value.update(full_input['network'])
|
||||||
|
return_networks = {'networks': [return_value]}
|
||||||
|
instance = self.plugin.return_value
|
||||||
|
instance.get_networks.return_value = return_networks
|
||||||
|
res = self.api.post_json(
|
||||||
|
_get_path('networks'), initial_input, expect_errors=True)
|
||||||
|
instance.get_networks.assert_called_with(mock.ANY,
|
||||||
|
filters=mock.ANY)
|
||||||
|
self.assertTrue("Quota exceeded for resources" in
|
||||||
|
res.json['QuantumError'])
|
||||||
|
|
||||||
|
def test_create_network_quota_without_limit(self):
|
||||||
|
cfg.CONF.set_override('quota_network', -1, group='QUOTAS')
|
||||||
|
net_id = _uuid()
|
||||||
|
initial_input = {'network': {'name': 'net1', 'tenant_id': _uuid()}}
|
||||||
|
full_input = {'network': {'admin_state_up': True, 'subnets': []}}
|
||||||
|
full_input['network'].update(initial_input['network'])
|
||||||
|
return_networks = []
|
||||||
|
for i in xrange(0, 3):
|
||||||
|
return_value = {'id': net_id + str(i), 'status': "ACTIVE"}
|
||||||
|
return_value.update(full_input['network'])
|
||||||
|
return_networks.append(return_value)
|
||||||
|
self.assertEquals(3, len(return_networks))
|
||||||
|
instance = self.plugin.return_value
|
||||||
|
instance.get_networks.return_value = return_networks
|
||||||
|
res = self.api.post_json(
|
||||||
|
_get_path('networks'), initial_input)
|
||||||
|
self.assertEqual(res.status_int, exc.HTTPCreated.code)
|
||||||
|
Loading…
Reference in New Issue
Block a user