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