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:
parent
fb8ea72240
commit
2fa1fc4bb1
|
@ -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},
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
|
@ -1 +1 @@
|
|||
268fb5e99aa2
|
||||
28a09af858a8
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
Loading…
Reference in New Issue