From: Ryan Tidwell Date: Thu, 19 Feb 2015 23:29:08 +0000 (-0800) Subject: Subnet allocation from a subnet pool X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=fb8ea72240700573e97a70597418453374fbd02f;p=openstack-build%2Fneutron-build.git Subnet allocation from a subnet pool Contains API changes, model changes, and logic required to enable a subnet to be allocated from a subnet pool. Users can request a subnet allocation by supplying subnetpool_id and optionally prefixlen or cidr. If cidr is specified, an attempt is made to allocate the given CIDR from the pool. If prefixlen is specified, an attempt is made to allocate any CIDR with the given prefix length from the pool. If neither is specified, a CIDR is chosen from the pool using the default prefix length for the pool. ApiImpact Partially-Implements: blueprint subnet-allocation Change-Id: I59a221f4f434718fb77bd132dbbe1ff50fce4b0c --- diff --git a/neutron/api/v2/attributes.py b/neutron/api/v2/attributes.py index 08085c5bd..1ccbf779d 100644 --- a/neutron/api/v2/attributes.py +++ b/neutron/api/v2/attributes.py @@ -778,8 +778,24 @@ RESOURCE_ATTRIBUTE_MAP = { 'required_by_policy': True, 'validate': {'type:uuid': None}, 'is_visible': True}, - 'cidr': {'allow_post': True, 'allow_put': False, - 'validate': {'type:subnet': None}, + 'subnetpool_id': {'allow_post': True, + 'allow_put': False, + 'default': ATTR_NOT_SPECIFIED, + 'required_by_policy': False, + 'validate': {'type:uuid': None}, + 'is_visible': True}, + 'prefixlen': {'allow_post': True, + 'allow_put': False, + 'validate': {'type:non_negative': None}, + 'convert_to': convert_to_int, + 'default': ATTR_NOT_SPECIFIED, + 'required_by_policy': False, + 'is_visible': False}, + 'cidr': {'allow_post': True, + 'allow_put': False, + 'default': ATTR_NOT_SPECIFIED, + 'validate': {'type:subnet_or_none': None}, + 'required_by_policy': False, 'is_visible': True}, 'gateway_ip': {'allow_post': True, 'allow_put': True, 'default': ATTR_NOT_SPECIFIED, diff --git a/neutron/common/constants.py b/neutron/common/constants.py index 4839dde3e..662f44758 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -115,6 +115,7 @@ DHCP_AGENT_SCHEDULER_EXT_ALIAS = 'dhcp_agent_scheduler' LBAAS_AGENT_SCHEDULER_EXT_ALIAS = 'lbaas_agent_scheduler' L3_DISTRIBUTED_EXT_ALIAS = 'dvr' L3_HA_MODE_EXT_ALIAS = 'l3-ha' +SUBNET_ALLOCATION_EXT_ALIAS = 'subnet_allocation' # Protocol names and numbers for Security Groups/Firewalls PROTO_NAME_TCP = 'tcp' diff --git a/neutron/common/exceptions.py b/neutron/common/exceptions.py index 6bd99a88d..521f9922d 100644 --- a/neutron/common/exceptions.py +++ b/neutron/common/exceptions.py @@ -431,3 +431,21 @@ class IllegalSubnetPoolPrefixBounds(BadRequest): class IllegalSubnetPoolPrefixUpdate(BadRequest): message = _("Illegal update to prefixes: %(msg)s") + + +class SubnetAllocationError(NeutronException): + message = _("Failed to allocate subnet: %(reason)s") + + +class MinPrefixSubnetAllocationError(BadRequest): + message = _("Unable to allocate subnet with prefix length %(prefixlen)s, " + "minimum allowed prefix is %(min_prefixlen)s") + + +class MaxPrefixSubnetAllocationError(BadRequest): + message = _("Unable to allocate subnet with prefix length %(prefixlen)s, " + "maximum allowed prefix is %(max_prefixlen)s") + + +class SubnetPoolDeleteError(BadRequest): + message = _("Unable to delete subnet pool: %(reason)s") diff --git a/neutron/common/utils.py b/neutron/common/utils.py index 868995809..2502c4d71 100644 --- a/neutron/common/utils.py +++ b/neutron/common/utils.py @@ -410,3 +410,11 @@ def is_cidr_host(cidr): if net.version == 4: return net.prefixlen == q_const.IPv4_BITS return net.prefixlen == q_const.IPv6_BITS + + +def ip_version_from_int(ip_version_int): + if ip_version_int == 4: + return q_const.IPv4 + if ip_version_int == 6: + return q_const.IPv6 + raise ValueError(_('Illegal IP version number')) diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index 841242532..ea3461b2a 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -15,6 +15,7 @@ import netaddr from oslo_config import cfg +from oslo_db import api as oslo_db_api from oslo_db import exception as db_exc from oslo_log import log as logging from oslo_utils import excutils @@ -29,11 +30,13 @@ from neutron.common import exceptions as n_exc from neutron.common import ipv6_utils from neutron.common import utils from neutron import context as ctx +from neutron.db import api as db_api from neutron.db import common_db_mixin from neutron.db import models_v2 from neutron.db import sqlalchemyutils from neutron.extensions import l3 from neutron.i18n import _LE, _LI +from neutron import ipam from neutron.ipam import subnet_alloc from neutron import manager from neutron import neutron_plugin_base_v2 @@ -131,6 +134,10 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, subnet_qry = context.session.query(models_v2.Subnet) return subnet_qry.filter_by(network_id=network_id).all() + def _get_subnets_by_subnetpool(self, context, subnetpool_id): + subnet_qry = context.session.query(models_v2.Subnet) + return subnet_qry.filter_by(subnetpool_id=subnetpool_id).all() + def _get_all_subnets(self, context): # NOTE(salvatore-orlando): This query might end up putting # a lot of stress on the db. Consider adding a cache layer @@ -845,6 +852,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, 'network_id': subnet['network_id'], 'ip_version': subnet['ip_version'], 'cidr': subnet['cidr'], + 'subnetpool_id': subnet.get('subnetpool_id'), 'allocation_pools': [{'start': pool['first_ip'], 'end': pool['last_ip']} for pool in subnet['allocation_pools']], @@ -1022,7 +1030,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, ip_ver = s['ip_version'] - if 'cidr' in s: + if attributes.is_attr_set(s.get('cidr')): self._validate_ip_version(ip_ver, s['cidr'], 'cidr') if attributes.is_attr_set(s.get('gateway_ip')): @@ -1109,81 +1117,185 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, external_gateway_info}} l3plugin.update_router(context, id, info) - def create_subnet(self, context, subnet): - - net = netaddr.IPNetwork(subnet['subnet']['cidr']) - # turn the CIDR into a proper subnet - subnet['subnet']['cidr'] = '%s/%s' % (net.network, net.prefixlen) + def _save_subnet(self, context, + network, + subnet_args, + dns_nameservers, + host_routes, + allocation_pools): - s = subnet['subnet'] + if not attributes.is_attr_set(allocation_pools): + allocation_pools = self._allocate_pools_for_subnet(context, + subnet_args) + else: + self._validate_allocation_pools(allocation_pools, + subnet_args['cidr']) + if subnet_args['gateway_ip']: + self._validate_gw_out_of_pools(subnet_args['gateway_ip'], + allocation_pools) + + self._validate_subnet_cidr(context, network, subnet_args['cidr']) + + subnet = models_v2.Subnet(**subnet_args) + context.session.add(subnet) + if attributes.is_attr_set(dns_nameservers): + for addr in dns_nameservers: + ns = models_v2.DNSNameServer(address=addr, + subnet_id=subnet.id) + context.session.add(ns) + + if attributes.is_attr_set(host_routes): + for rt in host_routes: + route = models_v2.SubnetRoute( + subnet_id=subnet.id, + destination=rt['destination'], + nexthop=rt['nexthop']) + context.session.add(route) + + for pool in allocation_pools: + ip_pool = models_v2.IPAllocationPool(subnet=subnet, + first_ip=pool['start'], + last_ip=pool['end']) + context.session.add(ip_pool) + ip_range = models_v2.IPAvailabilityRange( + ipallocationpool=ip_pool, + first_ip=pool['start'], + last_ip=pool['end']) + context.session.add(ip_range) - if s['gateway_ip'] is attributes.ATTR_NOT_SPECIFIED: - s['gateway_ip'] = str(netaddr.IPAddress(net.first + 1)) + return subnet - if s['allocation_pools'] == attributes.ATTR_NOT_SPECIFIED: - s['allocation_pools'] = self._allocate_pools_for_subnet(context, s) + def _make_subnet_args(self, context, shared, detail, + subnet, subnetpool_id=None): + args = {'tenant_id': detail.tenant_id, + 'id': detail.subnet_id, + 'name': subnet['name'], + 'network_id': subnet['network_id'], + 'ip_version': subnet['ip_version'], + 'cidr': str(detail.subnet.cidr), + 'subnetpool_id': subnetpool_id, + 'enable_dhcp': subnet['enable_dhcp'], + 'gateway_ip': self._gateway_ip_str(subnet, detail.subnet), + 'shared': shared} + if subnet['ip_version'] == 6 and subnet['enable_dhcp']: + if attributes.is_attr_set(subnet['ipv6_ra_mode']): + args['ipv6_ra_mode'] = subnet['ipv6_ra_mode'] + if attributes.is_attr_set(subnet['ipv6_address_mode']): + args['ipv6_address_mode'] = subnet['ipv6_address_mode'] + return args + + def _make_subnet_request(self, tenant_id, subnet, subnetpool): + cidr = subnet.get('cidr') + subnet_id = subnet.get('id', uuidutils.generate_uuid()) + is_any_subnetpool_request = not attributes.is_attr_set(cidr) + + if is_any_subnetpool_request: + prefixlen = subnet['prefixlen'] + if not attributes.is_attr_set(prefixlen): + prefixlen = int(subnetpool['default_prefixlen']) + + return ipam.AnySubnetRequest( + tenant_id, + subnet_id, + utils.ip_version_from_int(subnetpool['ip_version']), + prefixlen) else: - self._validate_allocation_pools(s['allocation_pools'], s['cidr']) - if s['gateway_ip'] is not None: - self._validate_gw_out_of_pools(s['gateway_ip'], - s['allocation_pools']) + return ipam.SpecificSubnetRequest(tenant_id, + subnet_id, + cidr) + + def _gateway_ip_str(self, subnet, cidr_net): + if subnet.get('gateway_ip') is attributes.ATTR_NOT_SPECIFIED: + return str(cidr_net.network + 1) + return subnet.get('gateway_ip') + + @oslo_db_api.wrap_db_retry(max_retries=db_api.MAX_RETRIES, + retry_on_request=True, + retry_on_deadlock=True) + def _create_subnet_from_pool(self, context, subnet): + s = subnet['subnet'] + tenant_id = self._get_tenant_id_for_create(context, s) + has_allocpool = attributes.is_attr_set(s['allocation_pools']) + is_any_subnetpool_request = not attributes.is_attr_set(s['cidr']) + if is_any_subnetpool_request and has_allocpool: + reason = _("allocation_pools allowed only " + "for specific subnet requests.") + raise n_exc.BadRequest(resource='subnets', msg=reason) - self._validate_subnet(context, s) + with context.session.begin(subtransactions=True): + subnetpool = self._get_subnetpool(context, s['subnetpool_id']) + network = self._get_network(context, s["network_id"]) + allocator = subnet_alloc.SubnetAllocator(subnetpool) + req = self._make_subnet_request(tenant_id, s, subnetpool) + + ipam_subnet = allocator.allocate_subnet(context.session, req) + detail = ipam_subnet.get_details() + subnet = self._save_subnet(context, + network, + self._make_subnet_args( + context, + network.shared, + detail, + s, + subnetpool_id=subnetpool['id']), + s['dns_nameservers'], + s['host_routes'], + s['allocation_pools']) + if network.external: + self._update_router_gw_ports(context, + subnet['id'], + subnet['network_id']) + return self._make_subnet_dict(subnet) + def _create_subnet_from_implicit_pool(self, context, subnet): + s = subnet['subnet'] + self._validate_subnet(context, s) tenant_id = self._get_tenant_id_for_create(context, s) - subnet_id = s.get('id') or uuidutils.generate_uuid() + id = s.get('id', uuidutils.generate_uuid()) + detail = ipam.SpecificSubnetRequest(tenant_id, + id, + s['cidr']) with context.session.begin(subtransactions=True): network = self._get_network(context, s["network_id"]) self._validate_subnet_cidr(context, network, s['cidr']) - # The 'shared' attribute for subnets is for internal plugin - # use only. It is not exposed through the API - args = {'tenant_id': tenant_id, - 'id': subnet_id, - 'name': s['name'], - 'network_id': s['network_id'], - 'ip_version': s['ip_version'], - 'cidr': s['cidr'], - 'enable_dhcp': s['enable_dhcp'], - 'gateway_ip': s['gateway_ip'], - 'shared': network.shared} - if s['ip_version'] == 6 and s['enable_dhcp']: - if attributes.is_attr_set(s['ipv6_ra_mode']): - args['ipv6_ra_mode'] = s['ipv6_ra_mode'] - if attributes.is_attr_set(s['ipv6_address_mode']): - args['ipv6_address_mode'] = s['ipv6_address_mode'] - subnet = models_v2.Subnet(**args) - - context.session.add(subnet) - if s['dns_nameservers'] is not attributes.ATTR_NOT_SPECIFIED: - for addr in s['dns_nameservers']: - ns = models_v2.DNSNameServer(address=addr, - subnet_id=subnet.id) - context.session.add(ns) - - if s['host_routes'] is not attributes.ATTR_NOT_SPECIFIED: - for rt in s['host_routes']: - route = models_v2.SubnetRoute( - subnet_id=subnet.id, - destination=rt['destination'], - nexthop=rt['nexthop']) - context.session.add(route) - - for pool in s['allocation_pools']: - ip_pool = models_v2.IPAllocationPool(subnet=subnet, - first_ip=pool['start'], - last_ip=pool['end']) - context.session.add(ip_pool) - ip_range = models_v2.IPAvailabilityRange( - ipallocationpool=ip_pool, - first_ip=pool['start'], - last_ip=pool['end']) - context.session.add(ip_range) - + subnet = self._save_subnet(context, + network, + self._make_subnet_args(context, + network.shared, + detail, + s), + s['dns_nameservers'], + s['host_routes'], + s['allocation_pools']) if network.external: - self._update_router_gw_ports(context, subnet_id, s['network_id']) - + self._update_router_gw_ports(context, + subnet['id'], + subnet['network_id']) return self._make_subnet_dict(subnet) + def create_subnet(self, context, subnet): + + s = subnet['subnet'] + cidr = s.get('cidr', attributes.ATTR_NOT_SPECIFIED) + prefixlen = s.get('prefixlen', attributes.ATTR_NOT_SPECIFIED) + has_cidr = attributes.is_attr_set(cidr) + has_prefixlen = attributes.is_attr_set(prefixlen) + + if has_cidr and has_prefixlen: + msg = _('cidr and prefixlen must not be supplied together') + raise n_exc.BadRequest(resource='subnets', msg=msg) + + if has_cidr: + # turn the CIDR into a proper subnet + net = netaddr.IPNetwork(s['cidr']) + subnet['subnet']['cidr'] = '%s/%s' % (net.network, net.prefixlen) + + subnetpool_id = s.get('subnetpool_id', attributes.ATTR_NOT_SPECIFIED) + if not attributes.is_attr_set(subnetpool_id): + # Create subnet from the implicit(AKA null) pool + return self._create_subnet_from_implicit_pool(context, subnet) + return self._create_subnet_from_pool(context, subnet) + def _update_subnet_dns_nameservers(self, context, id, s): old_dns_list = self._get_dns_by_subnet(context, id) new_dns_addr_set = set(s["dns_nameservers"]) @@ -1387,6 +1499,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, 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) @@ -1490,6 +1603,10 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, """Delete a subnetpool.""" with context.session.begin(subtransactions=True): subnetpool = self._get_subnetpool(context, id) + subnets = self._get_subnets_by_subnetpool(context, id) + if subnets: + reason = _("Subnet pool has existing allocations") + raise n_exc.SubnetPoolDeleteError(reason=reason) context.session.delete(subnetpool) def _check_mac_addr_update(self, context, port, new_mac, device_owner): diff --git a/neutron/db/migration/alembic_migrations/versions/268fb5e99aa2_subnetpool_allocation.py b/neutron/db/migration/alembic_migrations/versions/268fb5e99aa2_subnetpool_allocation.py new file mode 100644 index 000000000..681429d1f --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/268fb5e99aa2_subnetpool_allocation.py @@ -0,0 +1,33 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# 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. +# + +"""Initial operations in support of subnet allocation from a pool + +""" + +revision = '268fb5e99aa2' +down_revision = '034883111f' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('subnets', + sa.Column('subnetpool_id', + sa.String(length=36), + nullable=True, + index=True)) diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index 176168463..bcef6fb15 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -034883111f +268fb5e99aa2 diff --git a/neutron/db/models_v2.py b/neutron/db/models_v2.py index ee0ddb2ea..5bf2541f3 100644 --- a/neutron/db/models_v2.py +++ b/neutron/db/models_v2.py @@ -184,6 +184,7 @@ class Subnet(model_base.BASEV2, HasId, HasTenant): name = sa.Column(sa.String(attr.NAME_MAX_LEN)) network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id')) + subnetpool_id = sa.Column(sa.String(36), index=True) ip_version = sa.Column(sa.Integer, nullable=False) cidr = sa.Column(sa.String(64), nullable=False) gateway_ip = sa.Column(sa.String(64)) diff --git a/neutron/extensions/subnetallocation.py b/neutron/extensions/subnetallocation.py new file mode 100644 index 000000000..fd8035c10 --- /dev/null +++ b/neutron/extensions/subnetallocation.py @@ -0,0 +1,53 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# 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. + +from neutron.api import extensions +from neutron.common import constants + + +class Subnetallocation(extensions.ExtensionDescriptor): + """Extension class supporting subnet allocation.""" + + @classmethod + def get_name(cls): + return "Subnet Allocation" + + @classmethod + def get_alias(cls): + return constants.SUBNET_ALLOCATION_EXT_ALIAS + + @classmethod + def get_description(cls): + return "Enables allocation of subnets from a subnet pool" + + @classmethod + def get_namespace(cls): + return ("http://docs.openstack.org/ext/" + "%s/api/v1.0" % constants.SUBNET_ALLOCATION_EXT_ALIAS) + + @classmethod + def get_updated(cls): + return "2015-03-30T10:00:00-00:00" + + def get_required_extensions(self): + return ["router"] + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + return [] + + def get_extended_resources(self, version): + return {} diff --git a/neutron/ipam/subnet_alloc.py b/neutron/ipam/subnet_alloc.py index 876727dd4..b366e241d 100644 --- a/neutron/ipam/subnet_alloc.py +++ b/neutron/ipam/subnet_alloc.py @@ -13,13 +13,133 @@ # License for the specific language governing permissions and limitations # under the License. +import operator + import netaddr from neutron.api.v2 import attributes from neutron.common import constants from neutron.common import exceptions as n_exc +from neutron.db import models_v2 +import neutron.ipam as ipam +from neutron.ipam import driver from neutron.openstack.common import uuidutils +class SubnetAllocator(driver.Pool): + """Class for handling allocation of subnet prefixes from a subnet pool. + + This class leverages the pluggable IPAM interface where possible to + make merging into IPAM framework easier in future cycles. + """ + + def __init__(self, subnetpool): + self._subnetpool = subnetpool + + def _get_allocated_cidrs(self, session): + query = session.query( + models_v2.Subnet).with_lockmode('update') + subnets = query.filter_by(subnetpool_id=self._subnetpool['id']) + return (x.cidr for x in subnets) + + def _get_available_prefix_list(self, session): + 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) + available_set = prefix_set.difference(allocation_set) + available_set.compact() + return sorted(available_set.iter_cidrs(), + key=operator.attrgetter('prefixlen'), + reverse=True) + + def _allocate_any_subnet(self, session, request): + with session.begin(subtransactions=True): + prefix_pool = self._get_available_prefix_list(session) + for prefix in prefix_pool: + if request.prefixlen >= prefix.prefixlen: + subnet = prefix.subnet(request.prefixlen).next() + gateway_ip = request.gateway_ip + if not gateway_ip: + gateway_ip = subnet.network + 1 + + return IpamSubnet(request.tenant_id, + request.subnet_id, + subnet.cidr, + gateway_ip=gateway_ip, + allocation_pools=None) + msg = _("Insufficient prefix space to allocate subnet size /%s") + raise n_exc.SubnetAllocationError(reason=msg % + str(request.prefixlen)) + + def _allocate_specific_subnet(self, session, request): + with session.begin(subtransactions=True): + subnet = request.subnet + available = self._get_available_prefix_list(session) + matched = netaddr.all_matching_cidrs(subnet, available) + if len(matched) is 1 and matched[0].prefixlen <= subnet.prefixlen: + return IpamSubnet(request.tenant_id, + request.subnet_id, + subnet.cidr, + gateway_ip=request.gateway_ip, + allocation_pools=request.allocation_pools) + msg = _("Cannot allocate requested subnet from the available " + "set of prefixes") + raise n_exc.SubnetAllocationError(reason=msg) + + def allocate_subnet(self, session, request): + max_prefixlen = int(self._subnetpool['max_prefixlen']) + min_prefixlen = int(self._subnetpool['min_prefixlen']) + if request.prefixlen > max_prefixlen: + raise n_exc.MaxPrefixSubnetAllocationError( + prefixlen=request.prefixlen, + max_prefixlen=max_prefixlen) + if request.prefixlen < min_prefixlen: + raise n_exc.MinPrefixSubnetAllocationError( + prefixlen=request.prefixlen, + min_prefixlen=min_prefixlen) + + if isinstance(request, ipam.AnySubnetRequest): + return self._allocate_any_subnet(session, request) + elif isinstance(request, ipam.SpecificSubnetRequest): + return self._allocate_specific_subnet(session, request) + else: + msg = _("Unsupported request type") + raise n_exc.SubnetAllocationError(reason=msg) + + def get_subnet(self, subnet, subnet_id): + raise NotImplementedError() + + def update_subnet(self, request): + raise NotImplementedError() + + def remove_subnet(self, subnet, subnet_id): + raise NotImplementedError() + + +class IpamSubnet(driver.Subnet): + + def __init__(self, + tenant_id, + subnet_id, + cidr, + gateway_ip=None, + allocation_pools=None): + self._req = ipam.SpecificSubnetRequest(tenant_id, + subnet_id, + cidr, + gateway_ip=gateway_ip, + allocation_pools=None) + + def allocate(self, address_request): + raise NotImplementedError() + + def deallocate(self, address): + raise NotImplementedError() + + def get_details(self): + return self._req + + class SubnetPoolReader(object): '''Class to assist with reading a subnetpool, loading defaults, and inferring IP version from prefix list. Provides a common way of diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 0f42741f8..01089ca8f 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -112,7 +112,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, "quotas", "security-group", "agent", "dhcp_agent_scheduler", "multi-provider", "allowed-address-pairs", - "extra_dhcp_opt"] + "extra_dhcp_opt", "subnet_allocation"] @property def supported_extension_aliases(self): diff --git a/neutron/tests/unit/ipam/test_subnet_alloc.py b/neutron/tests/unit/ipam/test_subnet_alloc.py new file mode 100644 index 000000000..256d3b947 --- /dev/null +++ b/neutron/tests/unit/ipam/test_subnet_alloc.py @@ -0,0 +1,144 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# 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 oslo_config import cfg + +from neutron.api.v2 import attributes +from neutron.common import constants +from neutron.common import exceptions as n_exc +from neutron import context +import neutron.ipam as ipam +from neutron.ipam import subnet_alloc +from neutron import manager +from neutron.openstack.common import uuidutils +from neutron.tests.unit import test_db_plugin +from neutron.tests.unit import testlib_api + + +class TestSubnetAllocation(testlib_api.SqlTestCase): + + def setUp(self): + super(TestSubnetAllocation, self).setUp() + self._tenant_id = 'test-tenant' + self.setup_coreplugin(test_db_plugin.DB_PLUGIN_KLASS) + self.plugin = manager.NeutronManager.get_plugin() + self.ctx = context.get_admin_context() + cfg.CONF.set_override('allow_overlapping_ips', True) + + def _create_subnet_pool(self, plugin, ctx, name, prefix_list, + min_prefixlen, ip_version, + max_prefixlen=attributes.ATTR_NOT_SPECIFIED, + default_prefixlen=attributes.ATTR_NOT_SPECIFIED, + shared=False): + subnetpool = {'subnetpool': {'name': name, + 'tenant_id': self._tenant_id, + 'prefixes': prefix_list, + 'min_prefixlen': min_prefixlen, + 'max_prefixlen': max_prefixlen, + 'default_prefixlen': default_prefixlen, + 'shared': shared}} + return plugin.create_subnetpool(ctx, subnetpool) + + def _get_subnetpool(self, ctx, plugin, id): + return plugin.get_subnetpool(ctx, id) + + def test_allocate_any_subnet(self): + prefix_list = ['10.1.0.0/16', '192.168.1.0/24'] + sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp', + prefix_list, 21, 4) + sp = self.plugin._get_subnetpool(self.ctx, sp['id']) + with self.ctx.session.begin(subtransactions=True): + sa = subnet_alloc.SubnetAllocator(sp) + req = ipam.AnySubnetRequest(self._tenant_id, + uuidutils.generate_uuid(), + constants.IPv4, 21) + res = sa.allocate_subnet(self.ctx.session, req) + detail = res.get_details() + prefix_set = netaddr.IPSet(iterable=prefix_list) + allocated_set = netaddr.IPSet(iterable=[detail.subnet.cidr]) + self.assertTrue(allocated_set.issubset(prefix_set)) + self.assertEqual(detail.prefixlen, 21) + + def test_allocate_specific_subnet(self): + sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp', + ['10.1.0.0/16', '192.168.1.0/24'], + 21, 4) + with self.ctx.session.begin(subtransactions=True): + sp = self.plugin._get_subnetpool(self.ctx, sp['id']) + sa = subnet_alloc.SubnetAllocator(sp) + req = ipam.SpecificSubnetRequest(self._tenant_id, + uuidutils.generate_uuid(), + '10.1.2.0/24') + res = sa.allocate_subnet(self.ctx.session, req) + detail = res.get_details() + sp = self._get_subnetpool(self.ctx, self.plugin, sp['id']) + self.assertEqual(str(detail.subnet.cidr), '10.1.2.0/24') + self.assertEqual(detail.prefixlen, 24) + + def test_insufficient_prefix_space_for_any_allocation(self): + sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp', + ['10.1.1.0/24', '192.168.1.0/24'], + 21, 4) + sp = self.plugin._get_subnetpool(self.ctx, sp['id']) + sa = subnet_alloc.SubnetAllocator(sp) + req = ipam.AnySubnetRequest(self._tenant_id, + uuidutils.generate_uuid(), + constants.IPv4, + 21) + self.assertRaises(n_exc.SubnetAllocationError, + sa.allocate_subnet, self.ctx.session, req) + + def test_insufficient_prefix_space_for_specific_allocation(self): + sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp', + ['10.1.0.0/24'], + 21, 4) + sp = self.plugin._get_subnetpool(self.ctx, sp['id']) + sa = subnet_alloc.SubnetAllocator(sp) + req = ipam.SpecificSubnetRequest(self._tenant_id, + uuidutils.generate_uuid(), + '10.1.0.0/21') + self.assertRaises(n_exc.SubnetAllocationError, + sa.allocate_subnet, self.ctx.session, req) + + def test_allocate_any_subnet_gateway(self): + sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp', + ['10.1.0.0/16', '192.168.1.0/24'], + 21, 4) + sp = self.plugin._get_subnetpool(self.ctx, sp['id']) + with self.ctx.session.begin(subtransactions=True): + sa = subnet_alloc.SubnetAllocator(sp) + req = ipam.AnySubnetRequest(self._tenant_id, + uuidutils.generate_uuid(), + constants.IPv4, 21) + res = sa.allocate_subnet(self.ctx.session, req) + detail = res.get_details() + self.assertEqual(detail.gateway_ip, detail.subnet.network + 1) + + def test_allocate_specific_subnet_specific_gateway(self): + sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp', + ['10.1.0.0/16', '192.168.1.0/24'], + 21, 4) + sp = self.plugin._get_subnetpool(self.ctx, sp['id']) + with self.ctx.session.begin(subtransactions=True): + sa = subnet_alloc.SubnetAllocator(sp) + req = ipam.SpecificSubnetRequest(self._tenant_id, + uuidutils.generate_uuid(), + '10.1.2.0/24', + gateway_ip='10.1.2.254') + res = sa.allocate_subnet(self.ctx.session, req) + detail = res.get_details() + self.assertEqual(detail.gateway_ip, + netaddr.IPAddress('10.1.2.254')) diff --git a/neutron/tests/unit/test_common_utils.py b/neutron/tests/unit/test_common_utils.py index dd16a2d97..7a370f13b 100644 --- a/neutron/tests/unit/test_common_utils.py +++ b/neutron/tests/unit/test_common_utils.py @@ -617,3 +617,18 @@ class TestCidrIsHost(base.BaseTestCase): self.assertRaises(ValueError, utils.is_cidr_host, ip_address) + + +class TestIpVersionFromInt(base.BaseTestCase): + def test_ip_version_from_int_ipv4(self): + self.assertEqual(utils.ip_version_from_int(4), + constants.IPv4) + + def test_ip_version_from_int_ipv6(self): + self.assertEqual(utils.ip_version_from_int(6), + constants.IPv6) + + def test_ip_version_from_int_illegal_int(self): + self.assertRaises(ValueError, + utils.ip_version_from_int, + 8) diff --git a/neutron/tests/unit/test_db_plugin.py b/neutron/tests/unit/test_db_plugin.py index 1bdcc622a..077f4e90f 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -86,6 +86,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase): super(NeutronDbPluginV2TestCase, self).setUp() cfg.CONF.set_override('notify_nova_on_port_status_changes', False) + cfg.CONF.set_override('allow_overlapping_ips', True) # Make sure at each test according extensions for the plugin is loaded extensions.PluginAwareExtensionManager._instance = None # Save the attributes map in case the plugin will alter it @@ -4814,6 +4815,258 @@ class TestSubnetPoolsV2(NeutronDbPluginV2TestCase): res = req.get_response(self.api) self.assertEqual(res.status_int, 400) + def test_allocate_any_subnet_with_prefixlen(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') + + # Request a subnet allocation (no CIDR) + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'prefixlen': 24, + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = self.deserialize(self.fmt, req.get_response(self.api)) + + subnet = netaddr.IPNetwork(res['subnet']['cidr']) + self.assertEqual(subnet.prefixlen, 24) + # Assert the allocated subnet CIDR is a subnet of our pool prefix + supernet = netaddr.smallest_matching_cidr( + subnet, + sp['subnetpool']['prefixes']) + self.assertEqual(supernet, netaddr.IPNetwork('10.10.0.0/16')) + + def test_allocate_any_subnet_with_default_prefixlen(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') + + # Request any subnet allocation using default prefix + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = self.deserialize(self.fmt, req.get_response(self.api)) + + subnet = netaddr.IPNetwork(res['subnet']['cidr']) + self.assertEqual(subnet.prefixlen, + int(sp['subnetpool']['default_prefixlen'])) + + def test_allocate_specific_subnet_with_mismatch_prefixlen(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') + + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '10.10.1.0/24', + 'prefixlen': 26, + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + def test_allocate_specific_subnet_with_matching_prefixlen(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') + + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '10.10.1.0/24', + 'prefixlen': 24, + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + def test_allocate_specific_subnet(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') + + # Request a specific subnet allocation + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '10.10.1.0/24', + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = self.deserialize(self.fmt, req.get_response(self.api)) + + # Assert the allocated subnet CIDR is what we expect + subnet = netaddr.IPNetwork(res['subnet']['cidr']) + self.assertEqual(subnet, netaddr.IPNetwork('10.10.1.0/24')) + + def test_allocate_specific_subnet_non_existent_prefix(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') + + # Request a specific subnet allocation + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '192.168.1.0/24', + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 500) + + def test_allocate_specific_subnet_already_allocated(self): + with self.network() as network: + sp = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21') + + # Request a specific subnet allocation + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '10.10.10.0/24', + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + # Allocate the subnet + res = req.get_response(self.api) + self.assertEqual(res.status_int, 201) + # Attempt to allocate it again + res = req.get_response(self.api) + # Assert error + self.assertEqual(res.status_int, 500) + + def test_allocate_specific_subnet_prefix_too_small(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') + + # Request a specific subnet allocation + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '10.10.0.0/20', + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + def test_allocate_specific_subnet_prefix_specific_gw(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') + + # Request a specific subnet allocation + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '10.10.1.0/24', + 'gateway_ip': '10.10.1.254', + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertEqual(res['subnet']['gateway_ip'], '10.10.1.254') + + def test_allocate_specific_subnet_prefix_allocation_pools(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') + + # Request a specific subnet allocation + pools = [{'start': '10.10.1.2', + 'end': '10.10.1.253'}] + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '10.10.1.0/24', + 'gateway_ip': '10.10.1.1', + 'ip_version': 4, + 'allocation_pools': pools, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertEqual(res['subnet']['allocation_pools'][0]['start'], + pools[0]['start']) + self.assertEqual(res['subnet']['allocation_pools'][0]['end'], + pools[0]['end']) + + def test_allocate_any_subnet_prefix_allocation_pools(self): + with self.network() as network: + sp = self._test_create_subnetpool(['10.10.10.0/24'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21') + + # Request an any subnet allocation + pools = [{'start': '10.10.10.1', + 'end': '10.10.10.254'}] + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'prefixlen': '24', + 'ip_version': 4, + 'allocation_pools': pools, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + def test_allocate_specific_subnet_prefix_too_large(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', + max_prefixlen='21') + + # Request a specific subnet allocation + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '10.10.0.0/24', + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + + def test_delete_subnetpool_existing_allocations(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') + + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '10.10.0.0/24', + 'ip_version': 4, + 'tenant_id': network['network']['tenant_id']}} + req = self.new_create_request('subnets', data) + req.get_response(self.api) + req = self.new_delete_request('subnetpools', + sp['subnetpool']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + class DbModelTestCase(base.BaseTestCase): """DB model tests."""