]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add quota features into quantum.
authorYong Sheng Gong <gongysh@cn.ibm.com>
Wed, 11 Jul 2012 04:01:04 +0000 (12:01 +0800)
committerYong Sheng Gong <gongysh@cn.ibm.com>
Fri, 20 Jul 2012 01:40:35 +0000 (09:40 +0800)
Blueprint quantum-api-quotas

We support quota check for creating network, subnet and port.
Change-Id: I943335816308767c7eba084d80b969fcb2e5a8fb

etc/quantum.conf
quantum/api/v2/base.py
quantum/common/exceptions.py
quantum/quota.py [new file with mode: 0644]
quantum/tests/unit/test_api_v2.py

index 2b3c47bdd1b8d58e63857b280be38bc9fb71deb2..90f75a5607bb77cd6fcc92492dcc089aaea684fc 100644 (file)
@@ -30,3 +30,16 @@ api_paste_config = api-paste.ini
 
 # Maximum amount of retries to generate a unique MAC address
 # 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
index e1819c9a5b23cad1062978a9480c40d3cb03dbb4..056e8fb8991f2936458a19fe4102c37b2a5c7849 100644 (file)
@@ -22,6 +22,7 @@ from quantum.api.v2 import views
 from quantum.common import exceptions
 from quantum.common import utils
 from quantum import policy
+from quantum import quota
 
 LOG = logging.getLogger(__name__)
 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,
              }
 
+QUOTAS = quota.QUOTAS
+
 
 def fields(request):
     """
@@ -176,10 +179,8 @@ class Controller(object):
 
     def create(self, request, body=None):
         """Creates a new instance of the requested entity"""
-
         body = self._prepare_request_body(request.context, body, True,
                                           allow_bulk=True)
-
         action = "create_%s" % self._resource
 
         # Check authz
@@ -196,12 +197,22 @@ class Controller(object):
                         action,
                         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:
                 self._validate_network_tenant_ownership(
                     request,
                     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:
             raise webob.exc.HTTPForbidden()
 
index a00cf8ee089bbb27995670f93c02eeb5d1bd9953..88e548927adc121b39c410b0d6597632d1beacd5 100644 (file)
@@ -168,3 +168,16 @@ class PreexistingDeviceFailure(QuantumException):
 
 class SudoRequired(QuantumException):
     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")
diff --git a/quantum/quota.py b/quantum/quota.py
new file mode 100644 (file)
index 0000000..3f819dc
--- /dev/null
@@ -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)
index b068108cd17292cae7c0e51ff00ca7827518e3a6..a7972defebaf0007d0a0f67dd875563bb6d20a41 100644 (file)
@@ -128,11 +128,7 @@ class ResourceIndexTestCase(unittest.TestCase):
         self.assertTrue(link['rel'] == 'self')
 
 
-class APIv2TestCase(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.
+class APIv2TestBase(unittest.TestCase):
     def setUp(self):
         plugin = 'quantum.quantum_plugin_base_v2.QuantumPluginBaseV2'
         # Ensure 'stale' patched copies of the plugin are never returned
@@ -155,6 +151,12 @@ class APIv2TestCase(unittest.TestCase):
         self.plugin = None
         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):
         instance = self.plugin.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
 # 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):
         env = {}
@@ -729,3 +731,42 @@ class V2Views(unittest.TestCase):
         keys = ('id', 'network_id', 'tenant_id', 'gateway_ip',
                 'ip_version', 'cidr')
         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)