diff --git a/neutron/conf/policies/subnetpool.py b/neutron/conf/policies/subnetpool.py index debac2d8978..42ab11c65f8 100644 --- a/neutron/conf/policies/subnetpool.py +++ b/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, + }, + ] + ), ] diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index cec85737708..515a9f2e7da 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/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} diff --git a/neutron/extensions/subnetpool_prefix_ops.py b/neutron/extensions/subnetpool_prefix_ops.py new file mode 100644 index 00000000000..69b68663604 --- /dev/null +++ b/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) diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index f9c6c7decfa..ce8b0743f13 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/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 diff --git a/neutron/tests/unit/extensions/test_subnetpool_prefix_ops.py b/neutron/tests/unit/extensions/test_subnetpool_prefix_ops.py new file mode 100644 index 00000000000..6619fdbda7d --- /dev/null +++ b/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