Browse Source

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
changes/97/648197/11
Ryan Tidwell 3 years ago
parent
commit
7eb74d2c4a
  1. 24
      neutron/conf/policies/subnetpool.py
  2. 56
      neutron/db/db_base_plugin_v2.py
  3. 54
      neutron/extensions/subnetpool_prefix_ops.py
  4. 5
      neutron/plugins/ml2/plugin.py
  5. 234
      neutron/tests/unit/extensions/test_subnetpool_prefix_ops.py

24
neutron/conf/policies/subnetpool.py

@ -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,
},
]
),
]

56
neutron/db/db_base_plugin_v2.py

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

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

5
neutron/plugins/ml2/plugin.py

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

@ -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…
Cancel
Save