From d5896025b78cfc1e4783c0c5231b9b39266ebf58 Mon Sep 17 00:00:00 2001 From: Ryan Tidwell Date: Fri, 31 Aug 2018 09:47:43 +0200 Subject: [PATCH] Enable adoption of subnets into a subnet pool This patch enables the adoption of existing subnets into a subnetpool. Adoption of a subnet is done by passing the ID of the hosting network and the address family (ip_version) which causes all subnets in the specified address family on the given network to be adopted by the subnet pool. This continues to work within the constraints on subnet pool membership of subnets on the same network. This also ensures prefix uniqueness across an address scope before comitting the adoption of subnets. Change-Id: I5d3c07beb7f109142d2e3633e69f86ca39edc450 Partially-Implements: blueprint subnet-onboard Co-Authored-By: Ryan Tidwell Co-Authored-By: Reedip Co-Authored-By: Trevor McCasland Co-Authored-By: Bernard Caffarelli --- neutron/conf/policies/subnetpool.py | 14 +- neutron/db/db_base_plugin_v2.py | 64 +++++ neutron/extensions/subnet_onboard.py | 39 +++ neutron/plugins/ml2/plugin.py | 4 +- .../unit/extensions/test_subnet_onboard.py | 256 ++++++++++++++++++ .../subnet-onboard-e4d09fa403a1053e.yaml | 13 + 6 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 neutron/extensions/subnet_onboard.py create mode 100644 neutron/tests/unit/extensions/test_subnet_onboard.py create mode 100644 releasenotes/notes/subnet-onboard-e4d09fa403a1053e.yaml diff --git a/neutron/conf/policies/subnetpool.py b/neutron/conf/policies/subnetpool.py index f3c3da5c525..debac2d8978 100644 --- a/neutron/conf/policies/subnetpool.py +++ b/neutron/conf/policies/subnetpool.py @@ -17,6 +17,7 @@ from neutron.conf.policies import base COLLECTION_PATH = '/subnetpools' RESOURCE_PATH = '/subnetpools/{id}' +ONBOARD_PATH = '/subnetpools/{id}/onboard_network_subnets' rules = [ @@ -106,7 +107,18 @@ rules = [ 'path': RESOURCE_PATH, }, ] - ) + ), + policy.DocumentedRuleDefault( + 'onboard_network_subnets', + base.RULE_ADMIN_OR_OWNER, + 'Onboard existing subnet into a subnetpool', + [ + { + 'method': 'Put', + 'path': ONBOARD_PATH, + }, + ] + ), ] diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index 1d2b4b80be6..befc1664ffc 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -1254,6 +1254,70 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon, raise exc.SubnetPoolDeleteError(reason=reason) subnetpool.delete() + @db_api.retry_if_session_inactive() + def onboard_network_subnets(self, context, subnetpool_id, network_info): + network_id = network_info.get('network_id') + if not validators.is_attr_set(network_id): + msg = _("network_id must be specified.") + raise exc.InvalidInput(error_message=msg) + if not network_obj.Network.objects_exist(context, id=network_id): + raise exc.NetworkNotFound(net_id=network_id) + + subnetpool = subnetpool_obj.SubnetPool.get_object(context, + id=subnetpool_id) + if not subnetpool: + raise exc.SubnetPoolNotFound(subnetpool_id=id) + + subnets_to_onboard = subnet_obj.Subnet.get_objects( + context, + network_id=network_id, + ip_version=subnetpool.ip_version) + + self._onboard_network_subnets(context, subnets_to_onboard, subnetpool) + + if subnetpool.address_scope_id: + # Notify all affected routers of any address scope changes + registry.notify(resources.SUBNETPOOL_ADDRESS_SCOPE, + events.AFTER_UPDATE, + self.onboard_network_subnets, + payload=events.DBEventPayload( + context, resource_id=subnetpool_id)) + + onboard_info = [] + for subnet in subnets_to_onboard: + onboard_info.append({'id': subnet.id, 'cidr': subnet.cidr}) + + return onboard_info + + def _onboard_network_subnets(self, context, subnets_to_onboard, + subnetpool): + allocated_prefix_set = netaddr.IPSet( + [x.cidr for x in subnet_obj.Subnet.get_objects( + context, + subnetpool_id=subnetpool.id)]) + prefixes_to_add = [] + + for subnet in subnets_to_onboard: + to_onboard_ipset = netaddr.IPSet([subnet.cidr]) + if to_onboard_ipset & allocated_prefix_set: + args = {'subnet_id': subnet.id, + 'cidr': subnet.cidr, + 'subnetpool_id': subnetpool.id} + msg = _('Onboarding subnet %(subnet_id)s: %(cidr)s conflicts ' + 'with allocated prefixes in subnet pool ' + '%(subnetpool_id)s') % args + raise exc.IllegalSubnetPoolUpdate(reason=msg) + prefixes_to_add.append(subnet.cidr) + + with db_api.CONTEXT_WRITER.using(context): + new_sp_prefixes = subnetpool.prefixes + prefixes_to_add + sp_update_req = {'subnetpool': {'prefixes': new_sp_prefixes}} + + self.update_subnetpool(context, subnetpool.id, sp_update_req) + for subnet in subnets_to_onboard: + subnet.subnetpool_id = subnetpool.id + subnet.update() + def _check_mac_addr_update(self, context, port, new_mac, device_owner): if (device_owner and device_owner.startswith( diff --git a/neutron/extensions/subnet_onboard.py b/neutron/extensions/subnet_onboard.py new file mode 100644 index 00000000000..9f774ce91cf --- /dev/null +++ b/neutron/extensions/subnet_onboard.py @@ -0,0 +1,39 @@ +# (c) Copyright 2017 Hewlett Packard Enterprise Development LP +# +# 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_lib.api.definitions import subnet_onboard as subnet_onboard_def +from neutron_lib.api.definitions import subnetpool as subnetpool_def +from neutron_lib.api import extensions + +from neutron.api.v2 import resource_helper + + +class Subnet_onboard(extensions.APIExtensionDescriptor): + """API extension for subnet onboard.""" + + api_definition = subnet_onboard_def + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + plural_mappings = resource_helper.build_plural_mappings( + {}, subnetpool_def.RESOURCE_ATTRIBUTE_MAP) + return resource_helper.build_resource_info( + plural_mappings, + subnetpool_def.RESOURCE_ATTRIBUTE_MAP, + None, + action_map=subnet_onboard_def.ACTION_MAP, + register_quota=True) diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 51caa219603..cc006f7ddd6 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -42,6 +42,7 @@ from neutron_lib.api.definitions import portbindings_extended as pbe_ext from neutron_lib.api.definitions import provider_net from neutron_lib.api.definitions import security_groups_port_filtering from neutron_lib.api.definitions import subnet as subnet_def +from neutron_lib.api.definitions import subnet_onboard as subnet_onboard_def from neutron_lib.api.definitions import vlantransparent as vlan_apidef from neutron_lib.api import extensions from neutron_lib.api import validators @@ -193,7 +194,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, filter_apidef.ALIAS, port_mac_address_regenerate.ALIAS, pbe_ext.ALIAS, - agent_resources_synced.ALIAS] + agent_resources_synced.ALIAS, + subnet_onboard_def.ALIAS] # List of agent types for which all binding_failed ports should try to be # rebound when agent revive diff --git a/neutron/tests/unit/extensions/test_subnet_onboard.py b/neutron/tests/unit/extensions/test_subnet_onboard.py new file mode 100644 index 00000000000..f3752c556f0 --- /dev/null +++ b/neutron/tests/unit/extensions/test_subnet_onboard.py @@ -0,0 +1,256 @@ +# (c) Copyright 2019 SUSE LLC +# +# 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 contextlib + +import netaddr +from neutron_lib.db import api as db_api +from neutron_lib import exceptions as exc +from oslo_utils import uuidutils + +from neutron.objects import subnet as subnet_obj +from neutron.objects import subnetpool as subnetpool_obj +from neutron.tests.unit.plugins.ml2 import test_plugin + +_uuid = uuidutils.generate_uuid + + +class SubnetOnboardTestsBase(object): + + @contextlib.contextmanager + def address_scope(self, ip_version, prefixes=None, shared=False, + admin=True, name='test-scope', is_default_pool=False, + tenant_id=None, **kwargs): + if not tenant_id: + tenant_id = _uuid() + + scope_data = {'tenant_id': tenant_id, 'ip_version': ip_version, + 'shared': shared, 'name': name + '-scope'} + with db_api.CONTEXT_WRITER.using(self.context): + yield self.driver.create_address_scope( + self.context, + {'address_scope': scope_data}) + + @contextlib.contextmanager + def subnetpool(self, ip_version, prefixes=None, shared=False, admin=True, + name='test-pool', is_default_pool=False, tenant_id=None, + address_scope_id=None, **kwargs): + if not tenant_id: + tenant_id = _uuid() + pool_data = {'tenant_id': tenant_id, 'shared': shared, 'name': name, + 'address_scope_id': address_scope_id, + 'prefixes': prefixes, 'is_default': is_default_pool} + for key in kwargs: + pool_data[key] = kwargs[key] + + with db_api.CONTEXT_WRITER.using(self.context): + yield self.driver.create_subnetpool(self.context, + {'subnetpool': pool_data}) + + def test_onboard_subnet_no_address_scope(self): + with self.subnetpool(self.ip_version, + prefixes=self.subnetpool_prefixes) as subnetpool: + self._test_onboard_cidr(subnetpool['id'], self.cidr_to_onboard) + + def test_onboard_subnet_address_scope(self): + with self.address_scope(self.ip_version) as addr_scope: + with self.subnetpool(self.ip_version, + prefixes=self.subnetpool_prefixes, + address_scope_id=addr_scope['id']) as subnetpool: + self._test_onboard_cidr(subnetpool['id'], self.cidr_to_onboard) + + def test_onboard_subnet_overlapping_cidr_no_address_scope(self): + with self.subnetpool(self.ip_version, + prefixes=self.subnetpool_prefixes) as subnetpool: + with self.subnet(cidr=self.overlapping_cidr, + subnetpool_id=subnetpool['id'], + ip_version=self.ip_version): + self.assertRaises(exc.IllegalSubnetPoolUpdate, + self._test_onboard_cidr, + subnetpool['id'], + self.overlapping_cidr) + + def test_onboard_subnet_address_scope_multiple_pools(self): + with self.address_scope(self.ip_version) as addr_scope: + with self.subnetpool(self.ip_version, + prefixes=[self.subnetpool_prefixes[0]], + address_scope_id=addr_scope['id']) as onboard_pool,\ + self.subnetpool(self.ip_version, + prefixes=[self.subnetpool_prefixes[1]], + address_scope_id=addr_scope['id']): + self._test_onboard_cidr(onboard_pool['id'], + self.cidr_to_onboard) + + def test_onboard_subnet_address_scope_overlap_multiple_pools(self): + with self.address_scope(self.ip_version) as addr_scope: + with self.subnetpool(self.ip_version, + prefixes=[self.subnetpool_prefixes[0]], + address_scope_id=addr_scope['id']) as onboard_pool,\ + self.subnetpool(self.ip_version, + prefixes=[self.subnetpool_prefixes[1]], + address_scope_id=addr_scope['id']) as other_pool: + self.assertRaises(exc.AddressScopePrefixConflict, + self._test_onboard_cidr, + onboard_pool['id'], + other_pool['prefixes'][0]) + + def test_onboard_subnet_move_between_pools_same_address_scope(self): + with self.address_scope(self.ip_version) as addr_scope: + with self.subnetpool(self.ip_version, + prefixes=[self.cidr_to_onboard], + address_scope_id=addr_scope['id']) as source: + with self.subnetpool( + self.ip_version, + address_scope_id=addr_scope['id'], + prefixes=self.subnetpool_prefixes) as target: + with self.subnet( + cidr=self.cidr_to_onboard, + ip_version=self.ip_version) as subnet_to_onboard: + subnet_to_onboard = subnet_to_onboard['subnet'] + + # Onboard subnet into an initial subnet pool + self._test_onboard_network_subnets( + subnet_to_onboard['network_id'], source['id']) + source_pool_subnets = subnet_obj.Subnet.get_objects( + self.context, + subnetpool_id=source['id']) + self.assertEqual(1, len(source_pool_subnets)) + + # Attempt to move the subnet to the target pool + self.assertRaises(exc.AddressScopePrefixConflict, + self._test_onboard_network_subnets, + subnet_to_onboard['network_id'], target['id']) + + def test_onboard_subnet_move_between_pools(self): + with self.subnetpool(self.ip_version, + prefixes=self.subnetpool_prefixes) as source: + with self.subnetpool(self.ip_version, + prefixes=self.subnetpool_prefixes) as target: + with self.subnet( + cidr=self.cidr_to_onboard, + ip_version=self.ip_version) as subnet_to_onboard: + subnet_to_onboard = subnet_to_onboard['subnet'] + + # Onboard subnet into an initial subnet pool + self._test_onboard_network_subnets( + subnet_to_onboard['network_id'], source['id']) + source_pool_subnets = subnet_obj.Subnet.get_objects( + self.context, + subnetpool_id=source['id']) + self.assertEqual(1, len(source_pool_subnets)) + + # Attempt to onboard subnet into a different pool + self._test_onboard_network_subnets( + subnet_to_onboard['network_id'], target['id']) + source_pool_subnets = subnet_obj.Subnet.get_objects( + self.context, + subnetpool_id=source['id']) + target_pool_subnets = subnet_obj.Subnet.get_objects( + self.context, + subnetpool_id=target['id']) + source_subnetpool = subnetpool_obj.SubnetPool.get_object( + self.context, + id=source['id']) + + # Assert that the subnet prefix has not been removed + # from the the source prefix list. The prefix should + # simply be released back to the pool, not removed. + self.assertIn( + netaddr.IPNetwork(self.cidr_to_onboard), + netaddr.IPSet(source_subnetpool['prefixes'])) + # Assert the subnet is associated with the proper pool + self.assertEqual(0, len(source_pool_subnets)) + self.assertEqual(1, len(target_pool_subnets)) + + def test_onboard_subnet_invalid_request(self): + with self.subnetpool(self.ip_version, + prefixes=self.subnetpool_prefixes) as subnetpool: + self.assertRaises(exc.InvalidInput, + self._test_onboard_subnet_no_network_id, + subnetpool['id'], self.cidr_to_onboard) + + def test_onboard_subnet_network_not_found(self): + with self.subnetpool(self.ip_version, + prefixes=self.subnetpool_prefixes) as subnetpool: + self.assertRaises(exc.NetworkNotFound, + self._test_onboard_subnet_non_existing_network, + subnetpool['id'], self.cidr_to_onboard) + + def _test_onboard_subnet_no_network_id(self, subnetpool_id, + cidr_to_onboard): + with self.subnet(cidr=cidr_to_onboard, + ip_version=self.ip_version) as subnet_to_onboard: + subnet_to_onboard = subnet_to_onboard['subnet'] + self.driver.onboard_network_subnets( + self.context, subnetpool_id, {}) + + def _test_onboard_subnet_non_existing_network(self, subnetpool_id, + cidr_to_onboard): + with self.subnet(cidr=cidr_to_onboard, + ip_version=self.ip_version) as subnet_to_onboard: + subnet_to_onboard = subnet_to_onboard['subnet'] + self.driver.onboard_network_subnets( + self.context, subnetpool_id, + {'network_id': _uuid()}) + + def _test_onboard_network_subnets(self, network_id, subnetpool_id): + response = self.driver.onboard_network_subnets( + self.context, + subnetpool_id, + {'network_id': network_id}) + subnetpool = subnetpool_obj.SubnetPool.get_object(self.context, + id=subnetpool_id) + subnetpool_prefixes = netaddr.IPSet(subnetpool.prefixes) + + for onboarded_subnet in subnet_obj.Subnet.get_objects( + self.context, + ip_version=self.ip_version, + network_id=network_id): + onboarded_prefix = netaddr.IPNetwork(onboarded_subnet.cidr) + self.assertIn({'id': onboarded_subnet.id, + 'cidr': onboarded_subnet.cidr}, response) + self.assertEqual(subnetpool_id, + onboarded_subnet.subnetpool_id) + self.assertIn(onboarded_prefix, subnetpool_prefixes) + + def _test_onboard_cidr(self, subnetpool_id, cidr_to_onboard): + with self.subnet(cidr=cidr_to_onboard, + ip_version=self.ip_version) as subnet_to_onboard: + subnet_to_onboard = subnet_to_onboard['subnet'] + self._test_onboard_network_subnets( + subnet_to_onboard['network_id'], + subnetpool_id) + + +class SubnetOnboardTestsIpv4(SubnetOnboardTestsBase, + test_plugin.Ml2PluginV2TestCase): + + subnetpool_prefixes = ["192.168.1.0/24", "192.168.2.0/24"] + cidr_to_onboard = "10.0.0.0/24" + overlapping_cidr = "192.168.1.128/25" + default_prefixlen = 24 + ip_version = 4 + + +class SubnetOnboardTestsIpv6(SubnetOnboardTestsBase, + test_plugin.Ml2PluginV2TestCase): + + subnetpool_prefixes = ["2001:db8:1234::/48", + "2001:db8:1235::/48"] + cidr_to_onboard = "2001:db8:4321::/48" + overlapping_cidr = "2001:db8:1234:1111::/64" + default_prefixlen = 64 + ip_version = 6 diff --git a/releasenotes/notes/subnet-onboard-e4d09fa403a1053e.yaml b/releasenotes/notes/subnet-onboard-e4d09fa403a1053e.yaml new file mode 100644 index 00000000000..616681e0949 --- /dev/null +++ b/releasenotes/notes/subnet-onboard-e4d09fa403a1053e.yaml @@ -0,0 +1,13 @@ +--- +prelude: > + Existing subnets that were created outside of a subnet pool can know + be moved, or "onboarded" into an existing subnet pool. This provides + a way for subnets to be brought under the management of a subnet pool + and begin participating in an address scope. By enabling onboarding, + existing subnets can be used with features that build on subnet pools + and address scopes. Subnet onboarding is subject to all the same + restrictions as and guarantees currently enforced by subnet pools + and address scopes. +features: + - Existing subnets can now be moved into a subnet pool, and by extension + can be moved into address scopes they were not initially participating in.