Merge "Enable adoption of subnets into a subnet pool"
This commit is contained in:
commit
018595d4b0
@ -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,
|
||||
},
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -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(
|
||||
|
39
neutron/extensions/subnet_onboard.py
Normal file
39
neutron/extensions/subnet_onboard.py
Normal 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)
|
@ -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
|
||||
|
256
neutron/tests/unit/extensions/test_subnet_onboard.py
Normal file
256
neutron/tests/unit/extensions/test_subnet_onboard.py
Normal 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
|
13
releasenotes/notes/subnet-onboard-e4d09fa403a1053e.yaml
Normal file
13
releasenotes/notes/subnet-onboard-e4d09fa403a1053e.yaml
Normal 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.
|
Loading…
Reference in New Issue
Block a user