Browse Source
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: #1792901changes/97/648197/11
5 changed files with 372 additions and 1 deletions
@ -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) |
@ -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