'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},
class SubnetPoolDeleteError(BadRequest):
message = _("Unable to delete subnet pool: %(reason)s")
+
+
+class SubnetPoolQuotaExceeded(OverQuota):
+ message = _("Per-tenant subnet pool prefix quota exceeded")
'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,
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:
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
--- /dev/null
+# 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))
-268fb5e99aa2
+28a09af858a8
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',
# License for the specific language governing permissions and limitations
# under the License.
+import math
import operator
import netaddr
def __init__(self, subnetpool):
self._subnetpool = subnetpool
+ self._sp_helper = SubnetPoolHelper()
def _get_allocated_cidrs(self, session):
query = session.query(
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)
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:
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)
_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)
'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):
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()
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)
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:
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',
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']
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,
'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):
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)
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'],
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."""