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.