Implement subnetpool prefix operations
This changes provides the implementation of the subnetpool prefix operations extension. This exposes explicit API's for adding to and removing from the prefix list of a subnetpool. Prefixes added to a subnetpool are subject to the prefix uniqueness constraints imposed by address scopes. Prefixes to be removed from a subnetpool must not be allocated to an existing subnet, and the subnet using the prefix must be deleted before the prefix can be removed from the subnetpool. Change-Id: I76783a4edaf46e184b4dea1d572b89e594bad0ac Related-Bug: #1792901
This commit is contained in:
parent
538e663f0d
commit
7eb74d2c4a
@ -18,6 +18,8 @@ from neutron.conf.policies import base
|
||||
COLLECTION_PATH = '/subnetpools'
|
||||
RESOURCE_PATH = '/subnetpools/{id}'
|
||||
ONBOARD_PATH = '/subnetpools/{id}/onboard_network_subnets'
|
||||
ADD_PREFIXES_PATH = '/subnetpools/{id}/add_prefixes'
|
||||
REMOVE_PREFIXES_PATH = '/subnetpools/{id}/remove_prefixes'
|
||||
|
||||
|
||||
rules = [
|
||||
@ -119,6 +121,28 @@ rules = [
|
||||
},
|
||||
]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
'add_prefixes',
|
||||
base.RULE_ADMIN_OR_OWNER,
|
||||
'Add prefixes to a subnetpool',
|
||||
[
|
||||
{
|
||||
'method': 'Put',
|
||||
'path': ADD_PREFIXES_PATH,
|
||||
},
|
||||
]
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
'remove_prefixes',
|
||||
base.RULE_ADMIN_OR_OWNER,
|
||||
'Remove unallocated prefixes from a subnetpool',
|
||||
[
|
||||
{
|
||||
'method': 'Put',
|
||||
'path': REMOVE_PREFIXES_PATH,
|
||||
},
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -52,6 +52,7 @@ from neutron.db import ipam_pluggable_backend
|
||||
from neutron.db import models_v2
|
||||
from neutron.db import rbac_db_mixin as rbac_mixin
|
||||
from neutron.db import standardattrdescription_db as stattr_db
|
||||
from neutron.extensions import subnetpool_prefix_ops
|
||||
from neutron import ipam
|
||||
from neutron.ipam import exceptions as ipam_exc
|
||||
from neutron.ipam import subnet_alloc
|
||||
@ -1568,3 +1569,58 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||
device_id=device_id)
|
||||
if tenant_id != router['tenant_id']:
|
||||
raise exc.DeviceIDNotOwnedByTenant(device_id=device_id)
|
||||
|
||||
@db_api.retry_if_session_inactive()
|
||||
def add_prefixes(self, context, subnetpool_id, body):
|
||||
prefixes = subnetpool_prefix_ops.get_operation_request_body(body)
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
subnetpool = subnetpool_obj.SubnetPool.get_object(
|
||||
context, id=subnetpool_id)
|
||||
|
||||
if not subnetpool:
|
||||
raise exc.SubnetPoolNotFound(subnetpool_id=id)
|
||||
if len(prefixes) == 0:
|
||||
# No prefixes were included in the request, simply return
|
||||
return {'prefixes': subnetpool.prefixes}
|
||||
|
||||
new_sp_prefixes = subnetpool.prefixes + prefixes
|
||||
sp_update_req = {'subnetpool': {'prefixes': new_sp_prefixes}}
|
||||
sp = self.update_subnetpool(context, subnetpool_id, sp_update_req)
|
||||
return {'prefixes': sp['prefixes']}
|
||||
|
||||
@db_api.retry_if_session_inactive()
|
||||
def remove_prefixes(self, context, subnetpool_id, body):
|
||||
prefixes = subnetpool_prefix_ops.get_operation_request_body(body)
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
subnetpool = subnetpool_obj.SubnetPool.get_object(
|
||||
context, id=subnetpool_id)
|
||||
if not subnetpool:
|
||||
raise exc.SubnetPoolNotFound(subnetpool_id=id)
|
||||
if len(prefixes) == 0:
|
||||
# No prefixes were included in the request, simply return
|
||||
return {'prefixes': subnetpool.prefixes}
|
||||
|
||||
all_prefix_set = netaddr.IPSet(subnetpool.prefixes)
|
||||
removal_prefix_set = netaddr.IPSet([x for x in prefixes])
|
||||
if all_prefix_set.isdisjoint(removal_prefix_set):
|
||||
# The prefixes requested for removal are not in the prefix
|
||||
# list making this a no-op, so simply return.
|
||||
return {'prefixes': subnetpool.prefixes}
|
||||
|
||||
subnets = subnet_obj.Subnet.get_objects(
|
||||
context, subnetpool_id=subnetpool_id)
|
||||
allocated_prefix_set = netaddr.IPSet([x.cidr for x in subnets])
|
||||
|
||||
if not allocated_prefix_set.isdisjoint(removal_prefix_set):
|
||||
# One or more of the prefixes requested for removal have
|
||||
# been allocated by a real subnet, raise an exception to
|
||||
# indicate this.
|
||||
msg = _("One or more the prefixes to be removed is in use "
|
||||
"by a subnet.")
|
||||
raise exc.IllegalSubnetPoolPrefixUpdate(msg=msg)
|
||||
|
||||
new_prefixes = all_prefix_set.difference(removal_prefix_set)
|
||||
new_prefixes.compact()
|
||||
subnetpool.prefixes = [str(x) for x in new_prefixes.iter_cidrs()]
|
||||
subnetpool.update()
|
||||
return {'prefixes': subnetpool.prefixes}
|
||||
|
54
neutron/extensions/subnetpool_prefix_ops.py
Normal file
54
neutron/extensions/subnetpool_prefix_ops.py
Normal file
@ -0,0 +1,54 @@
|
||||
# (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.
|
||||
|
||||
from neutron_lib.api.definitions import subnetpool as subnetpool_def
|
||||
from neutron_lib.api.definitions import subnetpool_prefix_ops \
|
||||
as subnetpool_prefix_ops_def
|
||||
from neutron_lib.api import extensions
|
||||
import webob.exc
|
||||
|
||||
from neutron._i18n import _
|
||||
from neutron.api.v2 import resource_helper
|
||||
|
||||
|
||||
def get_operation_request_body(body):
|
||||
if not isinstance(body, dict):
|
||||
msg = _('Request body contains invalid data')
|
||||
raise webob.exc.HTTPBadRequest(msg)
|
||||
prefixes = body.get('prefixes')
|
||||
if not prefixes or not isinstance(prefixes, list):
|
||||
msg = _('Request body contains invalid data')
|
||||
raise webob.exc.HTTPBadRequest(msg)
|
||||
|
||||
return prefixes
|
||||
|
||||
|
||||
class Subnetpool_prefix_ops(extensions.APIExtensionDescriptor):
|
||||
"""API extension for subnet onboard."""
|
||||
|
||||
api_definition = subnetpool_prefix_ops_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=subnetpool_prefix_ops_def.ACTION_MAP,
|
||||
register_quota=True)
|
@ -47,6 +47,8 @@ from neutron_lib.api.definitions import rbac_security_groups as rbac_sg_apidef
|
||||
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 subnetpool_prefix_ops \
|
||||
as subnetpool_prefix_ops_def
|
||||
from neutron_lib.api.definitions import vlantransparent as vlan_apidef
|
||||
from neutron_lib.api import extensions
|
||||
from neutron_lib.api import validators
|
||||
@ -201,7 +203,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
port_mac_address_regenerate.ALIAS,
|
||||
pbe_ext.ALIAS,
|
||||
agent_resources_synced.ALIAS,
|
||||
subnet_onboard_def.ALIAS]
|
||||
subnet_onboard_def.ALIAS,
|
||||
subnetpool_prefix_ops_def.ALIAS]
|
||||
|
||||
# List of agent types for which all binding_failed ports should try to be
|
||||
# rebound when agent revive
|
||||
|
234
neutron/tests/unit/extensions/test_subnetpool_prefix_ops.py
Normal file
234
neutron/tests/unit/extensions/test_subnetpool_prefix_ops.py
Normal file
@ -0,0 +1,234 @@
|
||||
# (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
|
||||
import webob.exc
|
||||
|
||||
from neutron.objects import subnetpool as subnetpool_obj
|
||||
from neutron.tests.unit.plugins.ml2 import test_plugin
|
||||
|
||||
_uuid = uuidutils.generate_uuid
|
||||
|
||||
|
||||
class SubnetpoolPrefixOpsTestBase(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 _make_request_payload(self, prefixes):
|
||||
return {'prefixes': prefixes}
|
||||
|
||||
def test_add_prefix_no_address_scope(self):
|
||||
with self.subnetpool(self.ip_version,
|
||||
prefixes=self.subnetpool_prefixes) as subnetpool:
|
||||
self.driver.add_prefixes(
|
||||
self.context,
|
||||
subnetpool['id'],
|
||||
self._make_request_payload([self.cidr_to_add]))
|
||||
self._validate_prefix_list(subnetpool['id'],
|
||||
[self.cidr_to_add])
|
||||
|
||||
def test_add_prefix_invalid_request_body_structure(self):
|
||||
with self.subnetpool(self.ip_version,
|
||||
prefixes=self.subnetpool_prefixes) as subnetpool:
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.driver.add_prefixes,
|
||||
self.context,
|
||||
subnetpool['id'],
|
||||
[self.cidr_to_add])
|
||||
|
||||
def test_add_prefix_invalid_request_data(self):
|
||||
with self.subnetpool(self.ip_version,
|
||||
prefixes=self.subnetpool_prefixes) as subnetpool:
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.driver.add_prefixes,
|
||||
self.context,
|
||||
subnetpool['id'],
|
||||
['not a CIDR'])
|
||||
|
||||
def test_add_prefix_no_address_scope_overlapping_cidr(self):
|
||||
with self.subnetpool(self.ip_version,
|
||||
prefixes=self.subnetpool_prefixes) as subnetpool:
|
||||
prefixes_to_add = [self.cidr_to_add, self.overlapping_cidr]
|
||||
self.driver.add_prefixes(
|
||||
self.context,
|
||||
subnetpool['id'],
|
||||
self._make_request_payload([self.cidr_to_add]))
|
||||
self._validate_prefix_list(subnetpool['id'], prefixes_to_add)
|
||||
|
||||
def test_add_prefix_with_address_scope_overlapping_cidr(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 sp_to_augment,\
|
||||
self.subnetpool(self.ip_version,
|
||||
prefixes=[self.subnetpool_prefixes[1]],
|
||||
address_scope_id=addr_scope['id']):
|
||||
prefixes_to_add = [self.cidr_to_add]
|
||||
self.driver.add_prefixes(
|
||||
self.context,
|
||||
sp_to_augment['id'],
|
||||
self._make_request_payload([self.cidr_to_add]))
|
||||
self._validate_prefix_list(sp_to_augment['id'],
|
||||
prefixes_to_add)
|
||||
|
||||
def test_add_prefix_with_address_scope(self):
|
||||
with self.address_scope(self.ip_version) as addr_scope:
|
||||
with self.subnetpool(self.ip_version,
|
||||
prefixes=[self.subnetpool_prefixes[1]],
|
||||
address_scope_id=addr_scope['id']) as sp_to_augment,\
|
||||
self.subnetpool(self.ip_version,
|
||||
prefixes=[self.subnetpool_prefixes[0]],
|
||||
address_scope_id=addr_scope['id']):
|
||||
prefixes_to_add = [self.overlapping_cidr]
|
||||
self.assertRaises(exc.AddressScopePrefixConflict,
|
||||
self.driver.add_prefixes,
|
||||
self.context,
|
||||
sp_to_augment['id'],
|
||||
self._make_request_payload(prefixes_to_add))
|
||||
|
||||
def test_remove_prefix(self):
|
||||
with self.subnetpool(self.ip_version,
|
||||
prefixes=self.subnetpool_prefixes) as subnetpool:
|
||||
prefixes_to_remove = [self.subnetpool_prefixes[0]]
|
||||
self.driver.remove_prefixes(
|
||||
self.context,
|
||||
subnetpool['id'],
|
||||
self._make_request_payload(prefixes_to_remove))
|
||||
self._validate_prefix_list(subnetpool['id'],
|
||||
[self.subnetpool_prefixes[1]],
|
||||
excluded_prefixes=prefixes_to_remove)
|
||||
|
||||
def test_remove_prefix_invalid_request_body_structure(self):
|
||||
with self.subnetpool(self.ip_version,
|
||||
prefixes=self.subnetpool_prefixes) as subnetpool:
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.driver.remove_prefixes,
|
||||
self.context,
|
||||
subnetpool['id'],
|
||||
[self.subnetpool_prefixes[0]])
|
||||
|
||||
def test_remove_prefix_invalid_request_data(self):
|
||||
with self.subnetpool(self.ip_version,
|
||||
prefixes=self.subnetpool_prefixes) as subnetpool:
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.driver.remove_prefixes,
|
||||
self.context,
|
||||
subnetpool['id'],
|
||||
['not a CIDR'])
|
||||
|
||||
def test_remove_prefix_with_allocated_subnet(self):
|
||||
with self.subnetpool(self.ip_version,
|
||||
default_prefixlen=self.default_prefixlen,
|
||||
min_prefixlen=self.default_prefixlen,
|
||||
prefixes=self.subnetpool_prefixes) as subnetpool:
|
||||
with self.subnet(
|
||||
cidr=None,
|
||||
subnetpool_id=subnetpool['id'],
|
||||
ip_version=self.ip_version) as subnet:
|
||||
subnet = subnet['subnet']
|
||||
prefixes_to_remove = [subnet['cidr']]
|
||||
self.assertRaises(
|
||||
exc.IllegalSubnetPoolPrefixUpdate,
|
||||
self.driver.remove_prefixes,
|
||||
self.context,
|
||||
subnetpool['id'],
|
||||
self._make_request_payload(prefixes_to_remove))
|
||||
|
||||
def test_remove_overlapping_prefix_with_allocated_subnet(self):
|
||||
with self.subnetpool(
|
||||
self.ip_version,
|
||||
default_prefixlen=self.default_prefixlen,
|
||||
min_prefixlen=self.default_prefixlen,
|
||||
prefixes=[self.subnetpool_prefixes[0]]) as subnetpool:
|
||||
with self.subnet(
|
||||
cidr=None,
|
||||
subnetpool_id=subnetpool['id'],
|
||||
ip_version=self.ip_version) as subnet:
|
||||
subnet = subnet['subnet']
|
||||
prefixes_to_remove = [self.overlapping_cidr]
|
||||
self.assertRaises(
|
||||
exc.IllegalSubnetPoolPrefixUpdate,
|
||||
self.driver.remove_prefixes,
|
||||
self.context,
|
||||
subnetpool['id'],
|
||||
self._make_request_payload(prefixes_to_remove))
|
||||
|
||||
def _validate_prefix_list(self, subnetpool_id, expected_prefixes,
|
||||
excluded_prefixes=None):
|
||||
if not excluded_prefixes:
|
||||
excluded_prefixes = []
|
||||
|
||||
subnetpool = subnetpool_obj.SubnetPool.get_object(
|
||||
self.context,
|
||||
id=subnetpool_id)
|
||||
current_prefix_set = netaddr.IPSet([x for x in subnetpool.prefixes])
|
||||
expected_prefix_set = netaddr.IPSet(expected_prefixes)
|
||||
excluded_prefix_set = netaddr.IPSet(excluded_prefixes)
|
||||
self.assertTrue(expected_prefix_set.issubset(current_prefix_set))
|
||||
self.assertTrue(excluded_prefix_set.isdisjoint(current_prefix_set))
|
||||
|
||||
|
||||
class SubnetpoolPrefixOpsTestsIpv4(SubnetpoolPrefixOpsTestBase,
|
||||
test_plugin.Ml2PluginV2TestCase):
|
||||
|
||||
subnetpool_prefixes = ["192.168.1.0/24", "192.168.2.0/24"]
|
||||
cidr_to_add = "10.0.0.0/24"
|
||||
overlapping_cidr = "192.168.1.128/25"
|
||||
default_prefixlen = 24
|
||||
ip_version = 4
|
||||
|
||||
|
||||
class SubnetpoolPrefixOpsTestsIpv6(SubnetpoolPrefixOpsTestBase,
|
||||
test_plugin.Ml2PluginV2TestCase):
|
||||
|
||||
subnetpool_prefixes = ["2001:db8:1234::/48",
|
||||
"2001:db8:1235::/48"]
|
||||
cidr_to_add = "2001:db8:4321::/48"
|
||||
overlapping_cidr = "2001:db8:1234:1111::/64"
|
||||
default_prefixlen = 48
|
||||
ip_version = 6
|
Loading…
Reference in New Issue
Block a user