From 6f610d2d87bbfa3b461a6e0d68bb9e88fc330543 Mon Sep 17 00:00:00 2001 From: Ryan Tidwell Date: Thu, 15 Jan 2015 13:17:17 -0800 Subject: [PATCH] Basic subnetpool CRUD Enable creating, reading, updating, and deleting subnet pools via REST API. Includes required changes to REST, model, alembic migrations, and unit tests. Subnet pools carry a list of IPv4 or IPv6 prefixes from which a subnet can be allocated. This will enable tenants to request a subnet from a pool rather than being forced to explicitly provide their own CIDR's for their subnets. This change simply enables managing the lifecycle of a subnet pool and does not yet enable allocation of subnet prefixes from a pool. Subnet pools can have their prefix bounds (min, max, default), name, and prefix list updated. Changes to prefix bounds do not alter existing allocations and will not be blocked by existing allocations. Prefix lists can only be appended to. Prefixes cannot be removed from the pool once added. ApiImpact Partially-Implements: blueprint subnet-allocation Change-Id: I88c6b15aab258069758f1a9423d6616ceb4a33c4 --- etc/policy.json | 7 + neutron/api/v2/attributes.py | 56 +++ neutron/api/v2/router.py | 1 + neutron/common/exceptions.py | 29 ++ neutron/db/db_base_plugin_v2.py | 142 ++++++ .../versions/51c54792158e_subnetpools.py | 62 +++ .../alembic_migrations/versions/HEAD | 2 +- neutron/db/models_v2.py | 30 ++ neutron/ipam/subnet_alloc.py | 188 ++++++++ neutron/neutron_plugin_base_v2.py | 39 ++ neutron/tests/unit/test_db_plugin.py | 453 +++++++++++++++++- 11 files changed, 991 insertions(+), 18 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/51c54792158e_subnetpools.py create mode 100644 neutron/ipam/subnet_alloc.py diff --git a/etc/policy.json b/etc/policy.json index 4fc6c1c5566..ae46bc2cd48 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -8,6 +8,7 @@ "shared": "field:networks:shared=True", "shared_firewalls": "field:firewalls:shared=True", "shared_firewall_policies": "field:firewall_policies:shared=True", + "shared_subnetpools": "field:subnetpools:shared=True", "external": "field:networks:router:external=True", "default": "rule:admin_or_owner", @@ -16,6 +17,12 @@ "update_subnet": "rule:admin_or_network_owner", "delete_subnet": "rule:admin_or_network_owner", + "create_subnetpool": "", + "create_subnetpool:shared": "rule:admin_only", + "get_subnetpool": "rule:admin_or_owner or rule:shared_subnetpools", + "update_subnetpool": "rule:admin_or_owner", + "delete_subnetpool": "rule:admin_or_owner", + "create_network": "", "get_network": "rule:admin_or_owner or rule:shared or rule:external or rule:context_is_advsvc", "get_network:router:external": "rule:regular_user", diff --git a/neutron/api/v2/attributes.py b/neutron/api/v2/attributes.py index a776ac428f7..93b2fac5699 100644 --- a/neutron/api/v2/attributes.py +++ b/neutron/api/v2/attributes.py @@ -651,6 +651,8 @@ PORT = 'port' PORTS = '%ss' % PORT SUBNET = 'subnet' SUBNETS = '%ss' % SUBNET +SUBNETPOOL = 'subnetpool' +SUBNETPOOLS = '%ss' % SUBNETPOOL # Note: a default of ATTR_NOT_SPECIFIED indicates that an # attribute is not required, but will be generated by the plugin # if it is not specified. Particularly, a value of ATTR_NOT_SPECIFIED @@ -812,6 +814,59 @@ RESOURCE_ATTRIBUTE_MAP = { 'is_visible': False, 'required_by_policy': True, 'enforce_policy': True}, + }, + SUBNETPOOLS: { + 'id': {'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True}, + 'name': {'allow_post': True, + 'allow_put': True, + 'validate': {'type:not_empty_string': None}, + 'is_visible': True}, + 'tenant_id': {'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'required_by_policy': True, + 'is_visible': True}, + 'prefixes': {'allow_post': True, + 'allow_put': True, + 'validate': {'type:subnet_list': None}, + 'is_visible': True}, + 'ip_version': {'allow_post': False, + 'allow_put': False, + 'is_visible': True}, + 'allow_overlap': {'allow_post': True, + 'allow_put': False, + 'default': False, + 'convert_to': convert_to_boolean, + 'is_visible': True}, + 'default_prefixlen': {'allow_post': True, + 'allow_put': True, + 'validate': {'type:non_negative': None}, + 'convert_to': convert_to_int, + 'default': ATTR_NOT_SPECIFIED, + 'is_visible': True}, + 'min_prefixlen': {'allow_post': True, + 'allow_put': True, + 'default': ATTR_NOT_SPECIFIED, + 'validate': {'type:non_negative': None}, + 'convert_to': convert_to_int, + 'is_visible': True}, + 'max_prefixlen': {'allow_post': True, + 'allow_put': True, + 'default': ATTR_NOT_SPECIFIED, + 'validate': {'type:non_negative': None}, + 'convert_to': convert_to_int, + 'is_visible': True}, + SHARED: {'allow_post': True, + 'allow_put': False, + 'default': False, + 'convert_to': convert_to_boolean, + 'is_visible': True, + 'required_by_policy': True, + 'enforce_policy': True}, } } @@ -824,6 +879,7 @@ RESOURCE_FOREIGN_KEYS = { PLURALS = {NETWORKS: NETWORK, PORTS: PORT, SUBNETS: SUBNET, + SUBNETPOOLS: SUBNETPOOL, 'dns_nameservers': 'dns_nameserver', 'host_routes': 'host_route', 'allocation_pools': 'allocation_pool', diff --git a/neutron/api/v2/router.py b/neutron/api/v2/router.py index de0147db410..ee008f759de 100644 --- a/neutron/api/v2/router.py +++ b/neutron/api/v2/router.py @@ -33,6 +33,7 @@ LOG = logging.getLogger(__name__) RESOURCES = {'network': 'networks', 'subnet': 'subnets', + 'subnetpool': 'subnetpools', 'port': 'ports'} SUB_RESOURCES = {} COLLECTION_ACTIONS = ['index', 'create'] diff --git a/neutron/common/exceptions.py b/neutron/common/exceptions.py index 48b909235ba..8e8f8fcc703 100644 --- a/neutron/common/exceptions.py +++ b/neutron/common/exceptions.py @@ -79,6 +79,10 @@ class SubnetNotFound(NotFound): message = _("Subnet %(subnet_id)s could not be found") +class SubnetPoolNotFound(NotFound): + message = _("Subnet pool %(subnetpool_id)s could not be found") + + class PortNotFound(NotFound): message = _("Port %(port_id)s could not be found") @@ -397,3 +401,28 @@ class FirewallInternalDriverError(NeutronException): raise this exception to the agent """ message = _("%(driver)s: Internal driver error.") + + +class MissingMinSubnetPoolPrefix(BadRequest): + message = _("Unspecified minimum subnet pool prefix") + + +class EmptySubnetPoolPrefixList(BadRequest): + message = _("Empty subnet pool prefix list") + + +class PrefixVersionMismatch(BadRequest): + message = _("Cannot mix IPv4 and IPv6 prefixes in a subnet pool") + + +class UnsupportedMinSubnetPoolPrefix(BadRequest): + message = _("Prefix '%(prefix)s' not supported in IPv%(version)s pool") + + +class IllegalSubnetPoolPrefixBounds(BadRequest): + message = _("Illegal prefix bounds: %(prefix_type)s=%(prefixlen)s, " + "%(base_prefix_type)s=%(base_prefixlen)s") + + +class IllegalSubnetPoolPrefixUpdate(BadRequest): + message = _("Illegal update to prefixes: %(msg)s") diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index fad9665e7b7..ee4256c5f76 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -34,6 +34,7 @@ from neutron.db import models_v2 from neutron.db import sqlalchemyutils from neutron.extensions import l3 from neutron.i18n import _LE, _LI +from neutron.ipam import subnet_alloc from neutron import manager from neutron import neutron_plugin_base_v2 from neutron.openstack.common import uuidutils @@ -96,6 +97,16 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, raise n_exc.SubnetNotFound(subnet_id=id) return subnet + def _get_subnetpool(self, context, id): + try: + return self._get_by_id(context, models_v2.SubnetPool, id) + except exc.NoResultFound: + raise n_exc.SubnetPoolNotFound(subnetpool_id=id) + + def _get_all_subnetpools(self, context): + # NOTE(tidwellr): see note in _get_all_subnets() + return context.session.query(models_v2.SubnetPool).all() + def _get_port(self, context, id): try: port = self._get_by_id(context, models_v2.Port, id) @@ -818,6 +829,23 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, self._apply_dict_extend_functions(attributes.SUBNETS, res, subnet) return self._fields(res, fields) + def _make_subnetpool_dict(self, subnetpool, fields=None): + default_prefixlen = str(subnetpool['default_prefixlen']) + min_prefixlen = str(subnetpool['min_prefixlen']) + max_prefixlen = str(subnetpool['max_prefixlen']) + res = {'id': subnetpool['id'], + 'name': subnetpool['name'], + 'tenant_id': subnetpool['tenant_id'], + 'default_prefixlen': default_prefixlen, + 'min_prefixlen': min_prefixlen, + 'max_prefixlen': max_prefixlen, + 'shared': subnetpool['shared'], + 'allow_overlap': subnetpool['allow_overlap'], + 'prefixes': [prefix['cidr'] + for prefix in subnetpool['prefixes']], + 'ip_version': subnetpool['ip_version']} + return self._fields(res, fields) + def _make_port_dict(self, port, fields=None, process_extensions=True): res = {"id": port["id"], @@ -1298,6 +1326,120 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, return self._get_collection_count(context, models_v2.Subnet, filters=filters) + def _create_subnetpool_prefix(self, context, cidr, subnetpool_id): + prefix_args = {'cidr': cidr, 'subnetpool_id': subnetpool_id} + subnetpool_prefix = models_v2.SubnetPoolPrefix(**prefix_args) + context.session.add(subnetpool_prefix) + + def create_subnetpool(self, context, subnetpool): + """Create a subnetpool""" + sp = subnetpool['subnetpool'] + sp_reader = subnet_alloc.SubnetPoolReader(sp) + tenant_id = self._get_tenant_id_for_create(context, sp) + with context.session.begin(subtransactions=True): + pool_args = {'tenant_id': tenant_id, + 'id': sp_reader.id, + 'name': sp_reader.name, + 'ip_version': sp_reader.ip_version, + 'default_prefixlen': + sp_reader.default_prefixlen, + 'min_prefixlen': sp_reader.min_prefixlen, + 'max_prefixlen': sp_reader.max_prefixlen, + 'shared': sp_reader.shared, + 'allow_overlap': sp_reader.allow_overlap} + subnetpool = models_v2.SubnetPool(**pool_args) + context.session.add(subnetpool) + for prefix in sp_reader.prefixes: + self._create_subnetpool_prefix(context, + prefix, + subnetpool.id) + + return self._make_subnetpool_dict(subnetpool) + + def _update_subnetpool_prefixes(self, context, prefix_list, id): + with context.session.begin(subtransactions=True): + context.session.query(models_v2.SubnetPoolPrefix).filter_by( + subnetpool_id=id).delete() + for prefix in prefix_list: + model_prefix = models_v2.SubnetPoolPrefix(cidr=prefix, + subnetpool_id=id) + context.session.add(model_prefix) + + def _updated_subnetpool_dict(self, model, new_pool): + updated = {} + new_prefixes = new_pool.get('prefixes', attributes.ATTR_NOT_SPECIFIED) + orig_prefixes = [str(x.cidr) for x in model['prefixes']] + if new_prefixes is not attributes.ATTR_NOT_SPECIFIED: + orig_set = netaddr.IPSet(orig_prefixes) + new_set = netaddr.IPSet(new_prefixes) + if not orig_set.issubset(new_set): + msg = _("Existing prefixes must be " + "a subset of the new prefixes") + raise n_exc.IllegalSubnetPoolPrefixUpdate(msg=msg) + new_set.compact() + updated['prefixes'] = [str(x.cidr) for x in new_set.iter_cidrs()] + else: + updated['prefixes'] = orig_prefixes + + for key in ['id', 'name', 'ip_version', 'min_prefixlen', + 'max_prefixlen', 'default_prefixlen', 'allow_overlap', + 'shared']: + self._write_key(key, updated, model, new_pool) + + return updated + + def _write_key(self, key, update, orig, new_dict): + new_val = new_dict.get(key, attributes.ATTR_NOT_SPECIFIED) + if new_val is not attributes.ATTR_NOT_SPECIFIED: + update[key] = new_dict[key] + else: + update[key] = orig[key] + + def update_subnetpool(self, context, id, subnetpool): + """Update a subnetpool""" + new_sp = subnetpool['subnetpool'] + + with context.session.begin(subtransactions=True): + orig_sp = self._get_subnetpool(context, id) + updated = self._updated_subnetpool_dict(orig_sp, new_sp) + updated['tenant_id'] = orig_sp.tenant_id + reader = subnet_alloc.SubnetPoolReader(updated) + orig_sp.update(self._filter_non_model_columns( + reader.subnetpool, + models_v2.SubnetPool)) + self._update_subnetpool_prefixes(context, + reader.prefixes, + id) + for key in ['min_prefixlen', 'max_prefixlen', 'default_prefixlen']: + updated['key'] = str(updated[key]) + + return updated + + def get_subnetpool(self, context, id, fields=None): + """Retrieve a subnetpool.""" + subnetpool = self._get_subnetpool(context, id) + return self._make_subnetpool_dict(subnetpool, fields) + + def get_subnetpools(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + """Retrieve list of subnetpools.""" + marker_obj = self._get_marker_obj(context, 'subnetpool', limit, marker) + collection = self._get_collection(context, models_v2.SubnetPool, + self._make_subnetpool_dict, + filters=filters, fields=fields, + sorts=sorts, + limit=limit, + marker_obj=marker_obj, + page_reverse=page_reverse) + return collection + + def delete_subnetpool(self, context, id): + """Delete a subnetpool.""" + with context.session.begin(subtransactions=True): + subnetpool = self._get_subnetpool(context, id) + context.session.delete(subnetpool) + def _check_mac_addr_update(self, context, port, new_mac, device_owner): if (device_owner and device_owner.startswith('network:')): raise n_exc.UnsupportedPortDeviceOwner( diff --git a/neutron/db/migration/alembic_migrations/versions/51c54792158e_subnetpools.py b/neutron/db/migration/alembic_migrations/versions/51c54792158e_subnetpools.py new file mode 100644 index 00000000000..1c96574ab38 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/51c54792158e_subnetpools.py @@ -0,0 +1,62 @@ +# 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 for subnetpools + +Revision ID: 51c54792158e +Revises: 341ee8a4ccb5 +Create Date: 2015-01-27 13:07:50.713838 + +""" + +# revision identifiers, used by Alembic. +revision = '51c54792158e' +down_revision = '1955efc66455' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('subnetpools', + sa.Column('tenant_id', + sa.String(length=255), + nullable=True, + index=True), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('ip_version', sa.Integer(), nullable=False), + sa.Column('default_prefixlen', + sa.Integer(), + nullable=False), + sa.Column('min_prefixlen', sa.Integer(), nullable=False), + sa.Column('max_prefixlen', sa.Integer(), nullable=False), + sa.Column('shared', sa.Boolean(), nullable=False), + sa.Column('allow_overlap', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id')) + op.create_table('subnetpoolprefixes', + sa.Column('cidr', sa.String(length=64), nullable=False), + sa.Column('subnetpool_id', + sa.String(length=36), + nullable=False), + sa.ForeignKeyConstraint(['subnetpool_id'], + ['subnetpools.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('cidr', 'subnetpool_id')) + + +def downgrade(): + op.drop_table('subnetpoolprefixes') + op.drop_table('subnetpools') diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index 750594a58cd..a5f702c7f64 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -1955efc66455 +51c54792158e diff --git a/neutron/db/models_v2.py b/neutron/db/models_v2.py index c0578003970..48e8296960d 100644 --- a/neutron/db/models_v2.py +++ b/neutron/db/models_v2.py @@ -209,6 +209,36 @@ class Subnet(model_base.BASEV2, HasId, HasTenant): name='ipv6_address_modes'), nullable=True) +class SubnetPoolPrefix(model_base.BASEV2): + """Represents a neutron subnet pool prefix + """ + + __tablename__ = 'subnetpoolprefixes' + + cidr = sa.Column(sa.String(64), nullable=False, primary_key=True) + subnetpool_id = sa.Column(sa.String(36), + sa.ForeignKey('subnetpools.id'), + nullable=False, + primary_key=True) + + +class SubnetPool(model_base.BASEV2, HasId, HasTenant): + """Represents a neutron subnet pool. + """ + + name = sa.Column(sa.String(255)) + ip_version = sa.Column(sa.Integer, nullable=False) + default_prefixlen = sa.Column(sa.Integer, nullable=False) + min_prefixlen = sa.Column(sa.Integer, nullable=False) + max_prefixlen = sa.Column(sa.Integer, nullable=False) + shared = sa.Column(sa.Boolean, nullable=False) + allow_overlap = sa.Column(sa.Boolean, nullable=False) + prefixes = orm.relationship(SubnetPoolPrefix, + backref='subnetpools', + cascade='all, delete, delete-orphan', + lazy='joined') + + class Network(model_base.BASEV2, HasId, HasTenant): """Represents a v2 neutron network.""" diff --git a/neutron/ipam/subnet_alloc.py b/neutron/ipam/subnet_alloc.py new file mode 100644 index 00000000000..72c9af091ce --- /dev/null +++ b/neutron/ipam/subnet_alloc.py @@ -0,0 +1,188 @@ +# Copyright (c) 2015 Hewlett-Packard Co. +# All Rights Reserved. +# +# 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. + +import netaddr +from neutron.api.v2 import attributes +from neutron.common import constants +from neutron.common import exceptions as n_exc +from neutron.openstack.common import uuidutils + + +class SubnetPoolReader(object): + '''Class to assist with reading a subnetpool, loading defaults, and + inferring IP version from prefix list. Provides a common way of + reading a stored model or a create request with defaultable attributes. + ''' + MIN_PREFIX_TYPE = 'min' + MAX_PREFIX_TYPE = 'max' + DEFAULT_PREFIX_TYPE = 'default' + + _sp_helper = None + + def __init__(self, subnetpool): + self._read_prefix_list(subnetpool) + self._sp_helper = SubnetPoolHelper() + self._read_id(subnetpool) + self._read_prefix_bounds(subnetpool) + self._read_attrs(subnetpool, + ['tenant_id', 'name', 'allow_overlap', 'shared']) + self.subnetpool = {'id': self.id, + 'name': self.name, + 'tenant_id': self.tenant_id, + 'prefixes': self.prefixes, + 'min_prefix': self.min_prefix, + 'min_prefixlen': self.min_prefixlen, + 'max_prefix': self.max_prefix, + 'max_prefixlen': self.max_prefixlen, + 'default_prefix': self.default_prefix, + 'default_prefixlen': self.default_prefixlen, + 'allow_overlap': self.allow_overlap, + 'shared': self.shared} + + def _read_attrs(self, subnetpool, keys): + for key in keys: + setattr(self, key, subnetpool[key]) + + def _ip_version_from_cidr(self, cidr): + return netaddr.IPNetwork(cidr).version + + def _prefixlen_from_cidr(self, cidr): + return netaddr.IPNetwork(cidr).prefixlen + + def _read_id(self, subnetpool): + id = subnetpool.get('id', attributes.ATTR_NOT_SPECIFIED) + if id is attributes.ATTR_NOT_SPECIFIED: + id = uuidutils.generate_uuid() + self.id = id + + def _read_prefix_bounds(self, subnetpool): + ip_version = self.ip_version + default_min = self._sp_helper.default_min_prefixlen(ip_version) + default_max = self._sp_helper.default_max_prefixlen(ip_version) + + self._read_prefix_bound(self.MIN_PREFIX_TYPE, + subnetpool, + default_min) + self._read_prefix_bound(self.MAX_PREFIX_TYPE, + subnetpool, + default_max) + self._read_prefix_bound(self.DEFAULT_PREFIX_TYPE, + subnetpool, + self.min_prefixlen) + + self._sp_helper.validate_min_prefixlen(self.min_prefixlen, + self.max_prefixlen) + self._sp_helper.validate_max_prefixlen(self.max_prefixlen, + ip_version) + self._sp_helper.validate_default_prefixlen(self.min_prefixlen, + self.max_prefixlen, + self.default_prefixlen) + + def _read_prefix_bound(self, type, subnetpool, default_bound=None): + prefixlen_attr = type + '_prefixlen' + prefix_attr = type + '_prefix' + prefixlen = subnetpool.get(prefixlen_attr, + attributes.ATTR_NOT_SPECIFIED) + wildcard = self._sp_helper.wildcard(self.ip_version) + + if prefixlen is attributes.ATTR_NOT_SPECIFIED and default_bound: + prefixlen = default_bound + + if prefixlen is not attributes.ATTR_NOT_SPECIFIED: + prefix_cidr = '/'.join((wildcard, + str(prefixlen))) + setattr(self, prefix_attr, prefix_cidr) + setattr(self, prefixlen_attr, prefixlen) + + def _read_prefix_list(self, subnetpool): + prefix_list = subnetpool['prefixes'] + if not prefix_list: + raise n_exc.EmptySubnetPoolPrefixList() + + ip_version = None + for prefix in prefix_list: + if not ip_version: + ip_version = netaddr.IPNetwork(prefix).version + elif netaddr.IPNetwork(prefix).version != ip_version: + raise n_exc.PrefixVersionMismatch() + + self.ip_version = ip_version + self.prefixes = self._compact_subnetpool_prefix_list(prefix_list) + + def _compact_subnetpool_prefix_list(self, prefix_list): + """Compact any overlapping prefixes in prefix_list and return the + result + """ + ip_set = netaddr.IPSet() + for prefix in prefix_list: + ip_set.add(netaddr.IPNetwork(prefix)) + ip_set.compact() + return [str(x.cidr) for x in ip_set.iter_cidrs()] + + +class SubnetPoolHelper(object): + + PREFIX_VERSION_INFO = {4: {'max_prefixlen': constants.IPv4_BITS, + 'wildcard': '0.0.0.0', + 'default_min_prefixlen': 8}, + 6: {'max_prefixlen': constants.IPv6_BITS, + 'wildcard': '::', + 'default_min_prefixlen': 64}} + + def validate_min_prefixlen(self, min_prefixlen, max_prefixlen): + if min_prefixlen < 0: + raise n_exc.UnsupportedMinSubnetPoolPrefix(prefix=min_prefixlen, + version=4) + if min_prefixlen > max_prefixlen: + raise n_exc.IllegalSubnetPoolPrefixBounds( + prefix_type='min_prefixlen', + prefixlen=min_prefixlen, + base_prefix_type='max_prefixlen', + base_prefixlen=max_prefixlen) + + def validate_max_prefixlen(self, prefixlen, ip_version): + max = self.PREFIX_VERSION_INFO[ip_version]['max_prefixlen'] + if prefixlen > max: + raise n_exc.IllegalSubnetPoolPrefixBounds( + prefix_type='max_prefixlen', + prefixlen=prefixlen, + base_prefix_type='ip_version_max', + base_prefixlen=max) + + def validate_default_prefixlen(self, + min_prefixlen, + max_prefixlen, + default_prefixlen): + if default_prefixlen < min_prefixlen: + raise n_exc.IllegalSubnetPoolPrefixBounds( + prefix_type='default_prefixlen', + prefixlen=default_prefixlen, + base_prefix_type='min_prefixlen', + base_prefixlen=min_prefixlen) + if default_prefixlen > max_prefixlen: + raise n_exc.IllegalSubnetPoolPrefixBounds( + prefix_type='default_prefixlen', + prefixlen=default_prefixlen, + base_prefix_type='max_prefixlen', + base_prefixlen=max_prefixlen) + + def wildcard(self, ip_version): + return self.PREFIX_VERSION_INFO[ip_version]['wildcard'] + + def default_max_prefixlen(self, ip_version): + 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'] diff --git a/neutron/neutron_plugin_base_v2.py b/neutron/neutron_plugin_base_v2.py index 1310d4e028f..374dd19e7ef 100644 --- a/neutron/neutron_plugin_base_v2.py +++ b/neutron/neutron_plugin_base_v2.py @@ -127,6 +127,45 @@ class NeutronPluginBaseV2(object): """ pass + def create_subnetpool(self, context, subnetpool): + """Create a subnet pool. + + :param context: neutron api request context + :param subnetpool: Dictionary representing the subnetpool to create. + """ + raise NotImplementedError() + + def update_subnetpool(self, context, id, subnetpool): + """Update a subnet pool. + + :param context: neutron api request context + :param subnetpool: Dictionary representing the subnetpool attributes + to update. + """ + raise NotImplementedError() + + def get_subnetpool(self, context, id, fields=None): + """Show a subnet pool. + + :param context: neutron api request context + :param id: The UUID of the subnetpool to show. + """ + raise NotImplementedError() + + def get_subnetpools(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + """Retrieve list of subnet pools.""" + raise NotImplementedError() + + def delete_subnetpool(self, context, id): + """Delete a subnet pool. + + :param context: neutron api request context + :param id: The UUID of the subnet pool to delete. + """ + raise NotImplementedError() + @abc.abstractmethod def create_network(self, context, network): """Create a network. diff --git a/neutron/tests/unit/test_db_plugin.py b/neutron/tests/unit/test_db_plugin.py index ff7246212a7..a189c28ff42 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -18,6 +18,7 @@ import copy import itertools import mock +import netaddr from oslo_config import cfg from oslo_utils import importutils from testtools import matchers @@ -357,6 +358,23 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, kwargs.update({'override': overrides}) return self._create_bulk(fmt, number, 'subnet', base_data, **kwargs) + def _create_subnetpool(self, fmt, prefixes, + expected_res_status=None, admin=False, **kwargs): + subnetpool = {'subnetpool': {'prefixes': prefixes}} + for k, v in kwargs.items(): + subnetpool['subnetpool'][k] = str(v) + + api = self._api_for_resource('subnetpools') + subnetpools_req = self.new_create_request('subnetpools', + subnetpool, fmt) + if not admin: + neutron_context = context.Context('', kwargs['tenant_id']) + subnetpools_req.environ['neutron.context'] = neutron_context + subnetpool_res = subnetpools_req.get_response(api) + if expected_res_status: + self.assertEqual(subnetpool_res.status_int, expected_res_status) + return subnetpool_res + def _create_port(self, fmt, net_id, expected_res_status=None, arg_list=None, **kwargs): data = {'port': {'network_id': net_id, @@ -447,6 +465,18 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, raise webob.exc.HTTPClientError(code=res.status_int) return self.deserialize(fmt, res) + def _make_subnetpool(self, fmt, prefixes, admin=False, **kwargs): + res = self._create_subnetpool(fmt, + prefixes, + None, + admin, + **kwargs) + # Things can go wrong - raise HTTP exc with res code only + # so it can be caught by unit tests + if res.status_int >= webob.exc.HTTPClientError.code: + raise webob.exc.HTTPClientError(code=res.status_int) + return self.deserialize(fmt, res) + def _make_port(self, fmt, net_id, expected_res_status=None, **kwargs): res = self._create_port(fmt, net_id, expected_res_status, **kwargs) # Things can go wrong - raise HTTP exc with res code only @@ -456,7 +486,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, return self.deserialize(fmt, res) def _api_for_resource(self, resource): - if resource in ['networks', 'subnets', 'ports']: + if resource in ['networks', 'subnets', 'ports', 'subnetpools']: return self.api else: return self.ext_api @@ -538,8 +568,8 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, neutron_context=neutron_context, query_params=query_params) resource = resource.replace('-', '_') - self.assertEqual(sorted([i['id'] for i in res['%ss' % resource]]), - sorted([i[resource]['id'] for i in items])) + self.assertItemsEqual([i['id'] for i in res['%ss' % resource]], + [i[resource]['id'] for i in items]) @contextlib.contextmanager def network(self, name='net1', @@ -578,6 +608,14 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, ipv6_address_mode=ipv6_address_mode) yield subnet + @contextlib.contextmanager + def subnetpool(self, prefixes, admin=False, **kwargs): + subnetpool = self._make_subnetpool(self.fmt, + prefixes, + admin, + **kwargs) + yield subnetpool + @contextlib.contextmanager def port(self, subnet=None, fmt=None, **kwargs): with optional_ctx(subnet, self.subnet) as subnet_to_use: @@ -676,6 +714,27 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase, expected_res.reverse() self.assertEqual(expected_res, [n['id'] for n in item_res]) + def _compare_resource(self, observed_res, expected_res, res_name): + ''' + Compare the observed and expected resources (ie compare subnets) + ''' + for k in expected_res: + self.assertIn(k, observed_res[res_name]) + if isinstance(expected_res[k], list): + self.assertEqual(sorted(observed_res[res_name][k]), + sorted(expected_res[k])) + else: + self.assertEqual(observed_res[res_name][k], expected_res[k]) + + def _validate_resource(self, resource, keys, res_name): + for k in keys: + self.assertIn(k, resource[res_name]) + if isinstance(keys[k], list): + self.assertEqual(sorted(resource[res_name][k]), + sorted(keys[k])) + else: + self.assertEqual(resource[res_name][k], keys[k]) + class TestBasicGet(NeutronDbPluginV2TestCase): @@ -2535,22 +2594,10 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): keys.setdefault('enable_dhcp', True) with self.subnet(network=network, **keys) as subnet: # verify the response has each key with the correct value - for k in keys: - self.assertIn(k, subnet['subnet']) - if isinstance(keys[k], list): - self.assertEqual(sorted(subnet['subnet'][k]), - sorted(keys[k])) - else: - self.assertEqual(subnet['subnet'][k], keys[k]) + self._validate_resource(subnet, keys, 'subnet') # verify the configured validations are correct if expected: - for k in expected: - self.assertIn(k, subnet['subnet']) - if isinstance(expected[k], list): - self.assertEqual(sorted(subnet['subnet'][k]), - sorted(expected[k])) - else: - self.assertEqual(subnet['subnet'][k], expected[k]) + self._compare_resource(subnet, expected, 'subnet') self._delete('subnets', subnet['subnet']['id']) return subnet @@ -4246,6 +4293,378 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): expected_code=webob.exc.HTTPConflict.code) +class TestSubnetPoolsV2(NeutronDbPluginV2TestCase): + + _POOL_NAME = 'test-pool' + + def _test_create_subnetpool(self, prefixes, expected=None, + admin=False, **kwargs): + keys = kwargs.copy() + keys.setdefault('tenant_id', self._tenant_id) + with self.subnetpool(prefixes, admin, **keys) as subnetpool: + self._validate_resource(subnetpool, keys, 'subnetpool') + if expected: + self._compare_resource(subnetpool, expected, 'subnetpool') + return subnetpool + + def _validate_default_prefix(self, prefix, subnetpool): + self.assertEqual(subnetpool['subnetpool']['default_prefixlen'], prefix) + + def _validate_min_prefix(self, prefix, subnetpool): + self.assertEqual(subnetpool['subnetpool']['min_prefixlen'], prefix) + + def _validate_max_prefix(self, prefix, subnetpool): + self.assertEqual(subnetpool['subnetpool']['max_prefixlen'], prefix) + + def test_create_subnetpool_empty_prefix_list(self): + self.assertRaises(webob.exc.HTTPClientError, + self._test_create_subnetpool, + [], + name=self._POOL_NAME, + tenant_id=self._tenant_id, + min_prefixlen='21') + + def test_create_subnetpool_ipv4_24_with_defaults(self): + subnet = netaddr.IPNetwork('10.10.10.0/24') + subnetpool = self._test_create_subnetpool([subnet.cidr], + name=self._POOL_NAME, + tenant_id=self._tenant_id, + min_prefixlen='21') + self._validate_default_prefix('21', subnetpool) + self._validate_min_prefix('21', subnetpool) + + def test_create_subnetpool_ipv4_21_with_defaults(self): + subnet = netaddr.IPNetwork('10.10.10.0/21') + subnetpool = self._test_create_subnetpool([subnet.cidr], + name=self._POOL_NAME, + tenant_id=self._tenant_id, + min_prefixlen='21') + self._validate_default_prefix('21', subnetpool) + self._validate_min_prefix('21', subnetpool) + + def test_create_subnetpool_ipv4_default_prefix_too_small(self): + subnet = netaddr.IPNetwork('10.10.10.0/21') + self.assertRaises(webob.exc.HTTPClientError, + self._test_create_subnetpool, + [subnet.cidr], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21', + default_prefixlen='20') + + def test_create_subnetpool_ipv4_default_prefix_too_large(self): + subnet = netaddr.IPNetwork('10.10.10.0/21') + self.assertRaises(webob.exc.HTTPClientError, + self._test_create_subnetpool, + [subnet.cidr], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + max_prefixlen=24, + default_prefixlen='32') + + def test_create_subnetpool_ipv4_default_prefix_bounds(self): + subnet = netaddr.IPNetwork('10.10.10.0/21') + subnetpool = self._test_create_subnetpool([subnet.cidr], + tenant_id=self._tenant_id, + name=self._POOL_NAME) + self._validate_min_prefix('8', subnetpool) + self._validate_default_prefix('8', subnetpool) + self._validate_max_prefix('32', subnetpool) + + def test_create_subnetpool_ipv6_default_prefix_bounds(self): + subnet = netaddr.IPNetwork('fe80::/48') + subnetpool = self._test_create_subnetpool([subnet.cidr], + tenant_id=self._tenant_id, + name=self._POOL_NAME) + self._validate_min_prefix('64', subnetpool) + self._validate_default_prefix('64', subnetpool) + self._validate_max_prefix('128', subnetpool) + + def test_create_subnetpool_ipv4_supported_default_prefix(self): + subnet = netaddr.IPNetwork('10.10.10.0/21') + subnetpool = self._test_create_subnetpool([subnet.cidr], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21', + default_prefixlen='26') + self._validate_default_prefix('26', subnetpool) + + def test_create_subnetpool_ipv4_supported_min_prefix(self): + subnet = netaddr.IPNetwork('10.10.10.0/24') + subnetpool = self._test_create_subnetpool([subnet.cidr], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='26') + self._validate_min_prefix('26', subnetpool) + self._validate_default_prefix('26', subnetpool) + + def test_create_subnetpool_ipv4_default_prefix_smaller_than_min(self): + subnet = netaddr.IPNetwork('10.10.10.0/21') + self.assertRaises(webob.exc.HTTPClientError, + self._test_create_subnetpool, + [subnet.cidr], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + default_prefixlen='22', + min_prefixlen='23') + + def test_create_subnetpool_mixed_ip_version(self): + subnet_v4 = netaddr.IPNetwork('10.10.10.0/21') + subnet_v6 = netaddr.IPNetwork('fe80::/48') + self.assertRaises(webob.exc.HTTPClientError, + self._test_create_subnetpool, + [subnet_v4.cidr, subnet_v6.cidr], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21') + + def test_create_subnetpool_ipv6_with_defaults(self): + subnet = netaddr.IPNetwork('fe80::/48') + subnetpool = self._test_create_subnetpool([subnet.cidr], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='48') + self._validate_default_prefix('48', subnetpool) + self._validate_min_prefix('48', subnetpool) + + def test_get_subnetpool(self): + subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='24') + req = self.new_show_request('subnetpools', + subnetpool['subnetpool']['id']) + res = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertEqual(subnetpool['subnetpool']['id'], + res['subnetpool']['id']) + + def test_get_subnetpool_different_tenants_not_shared(self): + subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + shared=False, + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='24') + req = self.new_show_request('subnetpools', + subnetpool['subnetpool']['id']) + neutron_context = context.Context('', 'not-the-owner') + req.environ['neutron.context'] = neutron_context + res = req.get_response(self.api) + self.assertEqual(res.status_int, 404) + + def test_get_subnetpool_different_tenants_shared(self): + subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + None, + True, + name=self._POOL_NAME, + min_prefixlen='24', + shared=True) + req = self.new_show_request('subnetpools', + subnetpool['subnetpool']['id']) + neutron_context = context.Context('', self._tenant_id) + req.environ['neutron.context'] = neutron_context + res = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertEqual(subnetpool['subnetpool']['id'], + res['subnetpool']['id']) + + def test_list_subnetpools_different_tenants_shared(self): + self._test_create_subnetpool(['10.10.10.0/24'], + None, + True, + name=self._POOL_NAME, + min_prefixlen='24', + shared=True) + admin_res = self._list('subnetpools') + mortal_res = self._list('subnetpools', + neutron_context=context.Context('', 'not-the-owner')) + self.assertEqual(len(admin_res['subnetpools']), 1) + self.assertEqual(len(mortal_res['subnetpools']), 1) + + def test_list_subnetpools_different_tenants_not_shared(self): + self._test_create_subnetpool(['10.10.10.0/24'], + None, + True, + name=self._POOL_NAME, + min_prefixlen='24', + shared=False) + admin_res = self._list('subnetpools') + mortal_res = self._list('subnetpools', + neutron_context=context.Context('', 'not-the-owner')) + self.assertEqual(len(admin_res['subnetpools']), 1) + self.assertEqual(len(mortal_res['subnetpools']), 0) + + def test_delete_subnetpool(self): + subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='24') + req = self.new_delete_request('subnetpools', + subnetpool['subnetpool']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 204) + + def test_delete_nonexistent_subnetpool(self): + req = self.new_delete_request('subnetpools', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') + res = req.get_response(self._api_for_resource('subnetpools')) + self.assertEqual(res.status_int, 404) + + def test_update_subnetpool_prefix_list_append(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.8.0/21'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='24') + + data = {'subnetpool': {'prefixes': ['10.10.8.0/21', '3.3.3.0/24', + '2.2.2.0/24']}} + req = self.new_update_request('subnetpools', data, + initial_subnetpool['subnetpool']['id']) + api = self._api_for_resource('subnetpools') + res = self.deserialize(self.fmt, req.get_response(api)) + self.assertItemsEqual(res['subnetpool']['prefixes'], + ['10.10.8.0/21', '3.3.3.0/24', '2.2.2.0/24']) + + def test_update_subnetpool_prefix_list_compaction(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='24') + + data = {'subnetpool': {'prefixes': ['10.10.10.0/24', + '10.10.11.0/24']}} + req = self.new_update_request('subnetpools', data, + initial_subnetpool['subnetpool']['id']) + api = self._api_for_resource('subnetpools') + res = self.deserialize(self.fmt, req.get_response(api)) + self.assertItemsEqual(res['subnetpool']['prefixes'], + ['10.10.10.0/23']) + + def test_illegal_subnetpool_prefix_list_update(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='24') + + data = {'subnetpool': {'prefixes': ['10.10.11.0/24']}} + req = self.new_update_request('subnetpools', data, + initial_subnetpool['subnetpool']['id']) + api = self._api_for_resource('subnetpools') + res = req.get_response(api) + self.assertEqual(res.status_int, 400) + + def test_update_subnetpool_default_prefix(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.8.0/21'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='24') + + data = {'subnetpool': {'default_prefixlen': '26'}} + req = self.new_update_request('subnetpools', data, + initial_subnetpool['subnetpool']['id']) + api = self._api_for_resource('subnetpools') + res = self.deserialize(self.fmt, req.get_response(api)) + self.assertEqual(res['subnetpool']['default_prefixlen'], 26) + + def test_update_subnetpool_min_prefix(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='24') + + data = {'subnetpool': {'min_prefixlen': '21'}} + 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']['min_prefixlen'], 21) + + def test_update_subnetpool_min_prefix_larger_than_max(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21', + max_prefixlen='24') + + data = {'subnetpool': {'min_prefixlen': '28'}} + req = self.new_update_request('subnetpools', data, + initial_subnetpool['subnetpool']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + def test_update_subnetpool_max_prefix(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21', + max_prefixlen='24') + + data = {'subnetpool': {'max_prefixlen': '26'}} + 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']['max_prefixlen'], 26) + + def test_update_subnetpool_max_prefix_less_than_min(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='24') + + data = {'subnetpool': {'max_prefixlen': '21'}} + req = self.new_update_request('subnetpools', data, + initial_subnetpool['subnetpool']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + def test_update_subnetpool_max_prefix_less_than_default(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21', + default_prefixlen='24') + + data = {'subnetpool': {'max_prefixlen': '22'}} + req = self.new_update_request('subnetpools', data, + initial_subnetpool['subnetpool']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + def test_update_subnetpool_default_prefix_less_than_min(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21') + + data = {'subnetpool': {'default_prefixlen': '20'}} + req = self.new_update_request('subnetpools', data, + initial_subnetpool['subnetpool']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + def test_update_subnetpool_default_prefix_larger_than_max(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21', + max_prefixlen='24') + + data = {'subnetpool': {'default_prefixlen': '28'}} + req = self.new_update_request('subnetpools', data, + initial_subnetpool['subnetpool']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + def test_update_subnetpool_prefix_list_mixed_ip_version(self): + initial_subnetpool = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='24') + + data = {'subnetpool': {'prefixes': ['fe80::/48']}} + req = self.new_update_request('subnetpools', data, + initial_subnetpool['subnetpool']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + class DbModelTestCase(base.BaseTestCase): """DB model tests.""" def test_repr(self):