Simple subnetpool allocation quotas

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
This commit is contained in:
Ryan Tidwell 2015-03-16 11:02:13 -07:00
parent fb8ea72240
commit 2fa1fc4bb1
9 changed files with 178 additions and 15 deletions

View 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},

View 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")

View 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

View File

@ -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))

View File

@ -1 +1 @@
268fb5e99aa2
28a09af858a8

View 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',

View 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']

View 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)

View 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."""