]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Simple subnetpool allocation quotas
authorRyan Tidwell <ryan.tidwell@hp.com>
Mon, 16 Mar 2015 18:02:13 +0000 (11:02 -0700)
committerRyan Tidwell <ryan.tidwell@hp.com>
Tue, 31 Mar 2015 20:56:31 +0000 (20:56 +0000)
Enables enforcement of allocation quotas on subnet pools. The quota
is pool-wide, with the value of allocation_quota applied to every
tenant who uses the pool. allocation_quota must be non-negative,
and is an optional attribute. If not supplied, no quotas are
enforced. Quotas are measured in prefix space allocated. For IPv4
subnet pools, the quota is measured in units of /32 ie each tenant
can allocate up to X /32's from the pool. For IPv6 subnet pools, the
quota is measured in units of /64 ie each tenant can allocate up to
X /64's from the pool. For backward-compatibility, allocation quotas
are not applied to the implicit (AKA null) pool. Standard subnet
quotas will continue to be applied to all requests.

ApiImpact
Partially-Implements: blueprint subnet-allocation
Change-Id: I7e4641f47790414c693c7cc9b7a44b1889087801

neutron/api/v2/attributes.py
neutron/common/exceptions.py
neutron/db/db_base_plugin_v2.py
neutron/db/migration/alembic_migrations/versions/28a09af858a8_subnetpool_quotas.py [new file with mode: 0644]
neutron/db/migration/alembic_migrations/versions/HEAD
neutron/db/models_v2.py
neutron/ipam/subnet_alloc.py
neutron/tests/unit/ipam/test_subnet_alloc.py
neutron/tests/unit/test_db_plugin.py

index 1ccbf779d63d600b6f901cd396e32d6d71e161fd..d94a0a9723be3ff7321d6798e69328c0d0428e2e 100644 (file)
@@ -859,6 +859,12 @@ RESOURCE_ATTRIBUTE_MAP = {
                      'allow_put': True,
                      'validate': {'type:subnet_list': None},
                      'is_visible': True},
+        'default_quota': {'allow_post': True,
+                          'allow_put': True,
+                          'validate': {'type:non_negative': None},
+                          'convert_to': convert_to_int,
+                          'default': ATTR_NOT_SPECIFIED,
+                          'is_visible': True},
         'ip_version': {'allow_post': False,
                        'allow_put': False,
                        'is_visible': True},
index 521f9922d6164af24985cedcb545e77905eb112f..d5b64c52eb2f57d2313bea5a7aa113b283947e56 100644 (file)
@@ -449,3 +449,7 @@ class MaxPrefixSubnetAllocationError(BadRequest):
 
 class SubnetPoolDeleteError(BadRequest):
     message = _("Unable to delete subnet pool: %(reason)s")
+
+
+class SubnetPoolQuotaExceeded(OverQuota):
+    message = _("Per-tenant subnet pool prefix quota exceeded")
index ea3461b2a1307627ff6d48bdef336a948aa6233c..1e5fc8936a3c0fde799d4c4ef0a4fd353b0f526d 100644 (file)
@@ -884,7 +884,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
                'shared': subnetpool['shared'],
                'prefixes': [prefix['cidr']
                             for prefix in subnetpool['prefixes']],
-               'ip_version': subnetpool['ip_version']}
+               'ip_version': subnetpool['ip_version'],
+               'default_quota': subnetpool['default_quota']}
         return self._fields(res, fields)
 
     def _make_port_dict(self, port, fields=None,
@@ -1512,7 +1513,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
                          sp_reader.default_prefixlen,
                          'min_prefixlen': sp_reader.min_prefixlen,
                          'max_prefixlen': sp_reader.max_prefixlen,
-                         'shared': sp_reader.shared}
+                         'shared': sp_reader.shared,
+                         'default_quota': sp_reader.default_quota}
             subnetpool = models_v2.SubnetPool(**pool_args)
             context.session.add(subnetpool)
             for prefix in sp_reader.prefixes:
@@ -1548,7 +1550,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
             updated['prefixes'] = orig_prefixes
 
         for key in ['id', 'name', 'ip_version', 'min_prefixlen',
-                    'max_prefixlen', 'default_prefixlen', 'shared']:
+                    'max_prefixlen', 'default_prefixlen', 'shared',
+                    'default_quota']:
             self._write_key(key, updated, model, new_pool)
 
         return updated
diff --git a/neutron/db/migration/alembic_migrations/versions/28a09af858a8_subnetpool_quotas.py b/neutron/db/migration/alembic_migrations/versions/28a09af858a8_subnetpool_quotas.py
new file mode 100644 (file)
index 0000000..64f3598
--- /dev/null
@@ -0,0 +1,36 @@
+# Copyright 2015 OpenStack Foundation
+#
+#    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.
+#
+
+"""Initial operations to support basic quotas on prefix space in a subnet pool
+
+Revision ID: 28a09af858a8
+Revises: 268fb5e99aa2
+Create Date: 2015-03-16 10:36:48.810741
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '28a09af858a8'
+down_revision = '268fb5e99aa2'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    op.add_column('subnetpools',
+                  sa.Column('default_quota',
+                            sa.Integer(),
+                            nullable=True))
index bcef6fb15d8086118c62eefe7c1c6a3dcec42dc3..619b4734546fb08855949b72c117df731b049efc 100644 (file)
@@ -1 +1 @@
-268fb5e99aa2
+28a09af858a8
index 5bf2541f3b9fc9b705c212b804918743c95be4b4..eb77ede252385895237ffb928d50baadc6ca2c5f 100644 (file)
@@ -235,6 +235,7 @@ class SubnetPool(model_base.BASEV2, HasId, HasTenant):
     min_prefixlen = sa.Column(sa.Integer, nullable=False)
     max_prefixlen = sa.Column(sa.Integer, nullable=False)
     shared = sa.Column(sa.Boolean, nullable=False)
+    default_quota = sa.Column(sa.Integer, nullable=True)
     prefixes = orm.relationship(SubnetPoolPrefix,
                                 backref='subnetpools',
                                 cascade='all, delete, delete-orphan',
index b366e241d7fd825f03e41379141a858804e0684e..d4b933210665be23a7be951c8ff266ef291cdfa1 100644 (file)
@@ -13,6 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import math
 import operator
 
 import netaddr
@@ -34,6 +35,7 @@ class SubnetAllocator(driver.Pool):
 
     def __init__(self, subnetpool):
         self._subnetpool = subnetpool
+        self._sp_helper = SubnetPoolHelper()
 
     def _get_allocated_cidrs(self, session):
         query = session.query(
@@ -42,7 +44,7 @@ class SubnetAllocator(driver.Pool):
         return (x.cidr for x in subnets)
 
     def _get_available_prefix_list(self, session):
-        prefixes = (x.cidr for x in self._subnetpool['prefixes'])
+        prefixes = (x.cidr for x in self._subnetpool.prefixes)
         allocations = self._get_allocated_cidrs(session)
         prefix_set = netaddr.IPSet(iterable=prefixes)
         allocation_set = netaddr.IPSet(iterable=allocations)
@@ -52,8 +54,42 @@ class SubnetAllocator(driver.Pool):
                       key=operator.attrgetter('prefixlen'),
                       reverse=True)
 
+    def _num_quota_units_in_prefixlen(self, prefixlen, quota_unit):
+        return math.pow(2, quota_unit - prefixlen)
+
+    def _allocations_used_by_tenant(self, session, quota_unit):
+        subnetpool_id = self._subnetpool['id']
+        tenant_id = self._subnetpool['tenant_id']
+        with session.begin(subtransactions=True):
+            qry = session.query(
+                 models_v2.Subnet).with_lockmode('update')
+            allocations = qry.filter_by(subnetpool_id=subnetpool_id,
+                                        tenant_id=tenant_id)
+            value = 0
+            for allocation in allocations:
+                prefixlen = netaddr.IPNetwork(allocation.cidr).prefixlen
+                value += self._num_quota_units_in_prefixlen(prefixlen,
+                                                            quota_unit)
+            return value
+
+    def _check_subnetpool_tenant_quota(self, session, tenant_id, prefixlen):
+        quota_unit = self._sp_helper.ip_version_subnetpool_quota_unit(
+                                               self._subnetpool['ip_version'])
+        quota = self._subnetpool.get('default_quota')
+
+        if quota:
+            used = self._allocations_used_by_tenant(session, quota_unit)
+            requested_units = self._num_quota_units_in_prefixlen(prefixlen,
+                                                                 quota_unit)
+
+            if used + requested_units > quota:
+                raise n_exc.SubnetPoolQuotaExceeded()
+
     def _allocate_any_subnet(self, session, request):
         with session.begin(subtransactions=True):
+            self._check_subnetpool_tenant_quota(session,
+                                                request.tenant_id,
+                                                request.prefixlen)
             prefix_pool = self._get_available_prefix_list(session)
             for prefix in prefix_pool:
                 if request.prefixlen >= prefix.prefixlen:
@@ -73,6 +109,9 @@ class SubnetAllocator(driver.Pool):
 
     def _allocate_specific_subnet(self, session, request):
         with session.begin(subtransactions=True):
+            self._check_subnetpool_tenant_quota(session,
+                                                request.tenant_id,
+                                                request.prefixlen)
             subnet = request.subnet
             available = self._get_available_prefix_list(session)
             matched = netaddr.all_matching_cidrs(subnet, available)
@@ -152,7 +191,7 @@ class SubnetPoolReader(object):
     _sp_helper = None
 
     def __init__(self, subnetpool):
-        self._read_prefix_list(subnetpool)
+        self._read_prefix_info(subnetpool)
         self._sp_helper = SubnetPoolHelper()
         self._read_id(subnetpool)
         self._read_prefix_bounds(subnetpool)
@@ -168,6 +207,7 @@ class SubnetPoolReader(object):
                            'max_prefixlen': self.max_prefixlen,
                            'default_prefix': self.default_prefix,
                            'default_prefixlen': self.default_prefixlen,
+                           'default_quota': self.default_quota,
                            'shared': self.shared}
 
     def _read_attrs(self, subnetpool, keys):
@@ -225,7 +265,7 @@ class SubnetPoolReader(object):
             setattr(self, prefix_attr, prefix_cidr)
             setattr(self, prefixlen_attr, prefixlen)
 
-    def _read_prefix_list(self, subnetpool):
+    def _read_prefix_info(self, subnetpool):
         prefix_list = subnetpool['prefixes']
         if not prefix_list:
             raise n_exc.EmptySubnetPoolPrefixList()
@@ -236,6 +276,10 @@ class SubnetPoolReader(object):
                 ip_version = netaddr.IPNetwork(prefix).version
             elif netaddr.IPNetwork(prefix).version != ip_version:
                 raise n_exc.PrefixVersionMismatch()
+        self.default_quota = subnetpool.get('default_quota')
+
+        if self.default_quota is attributes.ATTR_NOT_SPECIFIED:
+            self.default_quota = None
 
         self.ip_version = ip_version
         self.prefixes = self._compact_subnetpool_prefix_list(prefix_list)
@@ -253,12 +297,16 @@ class SubnetPoolReader(object):
 
 class SubnetPoolHelper(object):
 
-    PREFIX_VERSION_INFO = {4: {'max_prefixlen': constants.IPv4_BITS,
+    _PREFIX_VERSION_INFO = {4: {'max_prefixlen': constants.IPv4_BITS,
                                'wildcard': '0.0.0.0',
-                               'default_min_prefixlen': 8},
+                               'default_min_prefixlen': 8,
+                               # IPv4 quota measured in units of /32
+                               'quota_units': 32},
                            6: {'max_prefixlen': constants.IPv6_BITS,
                                'wildcard': '::',
-                               'default_min_prefixlen': 64}}
+                               'default_min_prefixlen': 64,
+                               # IPv6 quota measured in units of /64
+                               'quota_units': 64}}
 
     def validate_min_prefixlen(self, min_prefixlen, max_prefixlen):
         if min_prefixlen < 0:
@@ -272,7 +320,7 @@ class SubnetPoolHelper(object):
                                              base_prefixlen=max_prefixlen)
 
     def validate_max_prefixlen(self, prefixlen, ip_version):
-        max = self.PREFIX_VERSION_INFO[ip_version]['max_prefixlen']
+        max = self._PREFIX_VERSION_INFO[ip_version]['max_prefixlen']
         if prefixlen > max:
             raise n_exc.IllegalSubnetPoolPrefixBounds(
                                             prefix_type='max_prefixlen',
@@ -298,10 +346,13 @@ class SubnetPoolHelper(object):
                                              base_prefixlen=max_prefixlen)
 
     def wildcard(self, ip_version):
-        return self.PREFIX_VERSION_INFO[ip_version]['wildcard']
+        return self._PREFIX_VERSION_INFO[ip_version]['wildcard']
 
     def default_max_prefixlen(self, ip_version):
-        return self.PREFIX_VERSION_INFO[ip_version]['max_prefixlen']
+        return self._PREFIX_VERSION_INFO[ip_version]['max_prefixlen']
 
     def default_min_prefixlen(self, ip_version):
-        return self.PREFIX_VERSION_INFO[ip_version]['default_min_prefixlen']
+        return self._PREFIX_VERSION_INFO[ip_version]['default_min_prefixlen']
+
+    def ip_version_subnetpool_quota_unit(self, ip_version):
+        return self._PREFIX_VERSION_INFO[ip_version]['quota_units']
index 256d3b9479f3649d7fe8fc68f26a5bdc3dc145d7..a0fc85d82b40e74e25e75edfba2f4c7460022a29 100644 (file)
@@ -42,6 +42,7 @@ class TestSubnetAllocation(testlib_api.SqlTestCase):
                             min_prefixlen, ip_version,
                             max_prefixlen=attributes.ATTR_NOT_SPECIFIED,
                             default_prefixlen=attributes.ATTR_NOT_SPECIFIED,
+                            default_quota=attributes.ATTR_NOT_SPECIFIED,
                             shared=False):
         subnetpool = {'subnetpool': {'name': name,
                                      'tenant_id': self._tenant_id,
@@ -49,7 +50,8 @@ class TestSubnetAllocation(testlib_api.SqlTestCase):
                                      'min_prefixlen': min_prefixlen,
                                      'max_prefixlen': max_prefixlen,
                                      'default_prefixlen': default_prefixlen,
-                                     'shared': shared}}
+                                     'shared': shared,
+                                     'default_quota': default_quota}}
         return plugin.create_subnetpool(ctx, subnetpool)
 
     def _get_subnetpool(self, ctx, plugin, id):
@@ -142,3 +144,25 @@ class TestSubnetAllocation(testlib_api.SqlTestCase):
             detail = res.get_details()
             self.assertEqual(detail.gateway_ip,
                              netaddr.IPAddress('10.1.2.254'))
+
+    def test__allocation_value_for_tenant_no_allocations(self):
+        sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp',
+                                      ['10.1.0.0/16', '192.168.1.0/24'],
+                                      21, 4)
+        sa = subnet_alloc.SubnetAllocator(sp)
+        value = sa._allocations_used_by_tenant(self.ctx.session, 32)
+        self.assertEqual(value, 0)
+
+    def test_subnetpool_default_quota_exceeded(self):
+        sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp',
+                                      ['fe80::/48'],
+                                      48, 6, default_quota=1)
+        sp = self.plugin._get_subnetpool(self.ctx, sp['id'])
+        sa = subnet_alloc.SubnetAllocator(sp)
+        req = ipam.SpecificSubnetRequest(self._tenant_id,
+                                         uuidutils.generate_uuid(),
+                                         'fe80::/63')
+        self.assertRaises(n_exc.SubnetPoolQuotaExceeded,
+                          sa.allocate_subnet,
+                          self.ctx.session,
+                          req)
index 077f4e90fb3dc25f741fe2453a28b62db83127af..4b87ef4527e9fcf813efcffc0ca1368712114279 100644 (file)
@@ -4815,6 +4815,21 @@ class TestSubnetPoolsV2(NeutronDbPluginV2TestCase):
         res = req.get_response(self.api)
         self.assertEqual(res.status_int, 400)
 
+    def test_update_subnetpool_default_quota(self):
+        initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'],
+                                         tenant_id=self._tenant_id,
+                                         name=self._POOL_NAME,
+                                         min_prefixlen='24',
+                                         default_quota=10)
+
+        self.assertEqual(initial_subnetpool['subnetpool']['default_quota'],
+                         10)
+        data = {'subnetpool': {'default_quota': '1'}}
+        req = self.new_update_request('subnetpools', data,
+                                      initial_subnetpool['subnetpool']['id'])
+        res = self.deserialize(self.fmt, req.get_response(self.api))
+        self.assertEqual(res['subnetpool']['default_quota'], 1)
+
     def test_allocate_any_subnet_with_prefixlen(self):
         with self.network() as network:
             sp = self._test_create_subnetpool(['10.10.0.0/16'],
@@ -5067,6 +5082,29 @@ class TestSubnetPoolsV2(NeutronDbPluginV2TestCase):
             res = req.get_response(self.api)
             self.assertEqual(res.status_int, 400)
 
+    def test_allocate_subnet_over_quota(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21',
+                                              default_quota=2048)
+
+            # Request a specific subnet allocation
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'ip_version': 4,
+                               'prefixlen': 21,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            # Allocate a subnet to fill the quota
+            res = req.get_response(self.api)
+            self.assertEqual(res.status_int, 201)
+            # Attempt to allocate a /21 again
+            res = req.get_response(self.api)
+            # Assert error
+            self.assertEqual(res.status_int, 409)
+
 
 class DbModelTestCase(base.BaseTestCase):
     """DB model tests."""