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 <rtidwell@suse.com>
Co-Authored-By: Reedip <reedip.banerjee@nectechnologies.in>
Co-Authored-By: Trevor McCasland <TM2086@att.com>
Co-Authored-By: Bernard Caffarelli <bcafarel@redhat.com>
This commit is contained in:
Ryan Tidwell 2018-08-31 09:47:43 +02:00
parent 0cf41c75d5
commit d5896025b7
No known key found for this signature in database
GPG Key ID: A1C63854C1CDF372
6 changed files with 388 additions and 2 deletions

View File

@ -17,6 +17,7 @@ from neutron.conf.policies import base
COLLECTION_PATH = '/subnetpools' COLLECTION_PATH = '/subnetpools'
RESOURCE_PATH = '/subnetpools/{id}' RESOURCE_PATH = '/subnetpools/{id}'
ONBOARD_PATH = '/subnetpools/{id}/onboard_network_subnets'
rules = [ rules = [
@ -106,7 +107,18 @@ rules = [
'path': RESOURCE_PATH, 'path': RESOURCE_PATH,
}, },
] ]
) ),
policy.DocumentedRuleDefault(
'onboard_network_subnets',
base.RULE_ADMIN_OR_OWNER,
'Onboard existing subnet into a subnetpool',
[
{
'method': 'Put',
'path': ONBOARD_PATH,
},
]
),
] ]

View File

@ -1254,6 +1254,70 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
raise exc.SubnetPoolDeleteError(reason=reason) raise exc.SubnetPoolDeleteError(reason=reason)
subnetpool.delete() 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): def _check_mac_addr_update(self, context, port, new_mac, device_owner):
if (device_owner and if (device_owner and
device_owner.startswith( device_owner.startswith(

View File

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

View File

@ -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 provider_net
from neutron_lib.api.definitions import security_groups_port_filtering 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 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.definitions import vlantransparent as vlan_apidef
from neutron_lib.api import extensions from neutron_lib.api import extensions
from neutron_lib.api import validators from neutron_lib.api import validators
@ -193,7 +194,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
filter_apidef.ALIAS, filter_apidef.ALIAS,
port_mac_address_regenerate.ALIAS, port_mac_address_regenerate.ALIAS,
pbe_ext.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 # List of agent types for which all binding_failed ports should try to be
# rebound when agent revive # rebound when agent revive

View File

@ -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

View File

@ -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.