diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index bb313a619e1..022319691b5 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -593,11 +593,52 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon, :param subnet: The subnet dict from the request """ - subnetpool_id = subnet.get('subnetpool_id', - attributes.ATTR_NOT_SPECIFIED) - if subnetpool_id != attributes.ATTR_NOT_SPECIFIED: + use_default_subnetpool = subnet.get('use_default_subnetpool') + if use_default_subnetpool == attributes.ATTR_NOT_SPECIFIED: + use_default_subnetpool = False + subnetpool_id = subnet.get('subnetpool_id') + if subnetpool_id == attributes.ATTR_NOT_SPECIFIED: + subnetpool_id = None + + if use_default_subnetpool and subnetpool_id: + msg = _('subnetpool_id and use_default_subnetpool cannot both be ' + 'specified') + raise n_exc.BadRequest(resource='subnets', msg=msg) + + if subnetpool_id: return subnetpool_id + if not use_default_subnetpool: + return + + cidr = subnet.get('cidr') + if attributes.is_attr_set(cidr): + ip_version = netaddr.IPNetwork(cidr).version + else: + ip_version = subnet.get('ip_version') + if not attributes.is_attr_set(ip_version): + msg = _('ip_version must be specified in the absence of ' + 'cidr and subnetpool_id') + raise n_exc.BadRequest(resource='subnets', msg=msg) + + if ip_version == 6 and cfg.CONF.ipv6_pd_enabled: + return constants.IPV6_PD_POOL_ID + + subnetpool = self.get_default_subnetpool(context, ip_version) + if subnetpool: + return subnetpool['id'] + + # Until the default_subnet_pool config options are removed in the N + # release, check for them after get_default_subnetpool returns None. + # TODO(john-davidge): Remove after Mitaka release. + if ip_version == 4 and cfg.CONF.default_ipv4_subnet_pool: + return cfg.CONF.default_ipv4_subnet_pool + if ip_version == 6 and cfg.CONF.default_ipv6_subnet_pool: + return cfg.CONF.default_ipv6_subnet_pool + + msg = _('No default subnetpool found for IPv%s') % ip_version + raise n_exc.BadRequest(resource='subnets', msg=msg) + def create_subnet(self, context, subnet): s = subnet['subnet'] diff --git a/neutron/extensions/default_subnetpools.py b/neutron/extensions/default_subnetpools.py new file mode 100644 index 00000000000..fa50834f296 --- /dev/null +++ b/neutron/extensions/default_subnetpools.py @@ -0,0 +1,56 @@ +# +# 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.api import extensions +from neutron.api.v2 import attributes +from neutron.common import constants + + +EXTENDED_ATTRIBUTES_2_0 = { + attributes.SUBNETS: { + 'use_default_subnetpool': {'allow_post': True, + 'allow_put': False, + 'default': False, + 'convert_to': attributes.convert_to_boolean, + 'is_visible': False, }, + }, +} + + +class Default_subnetpools(extensions.ExtensionDescriptor): + """Extension class supporting default subnetpools.""" + + @classmethod + def get_name(cls): + return "Default Subnetpools" + + @classmethod + def get_alias(cls): + return "default-subnetpools" + + @classmethod + def get_description(cls): + return "Provides ability to mark and use a subnetpool as the default" + + @classmethod + def get_updated(cls): + return "2016-02-18T18:00:00-00:00" + + def get_required_extensions(self): + return [constants.SUBNET_ALLOCATION_EXT_ALIAS] + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 200d4d4f5bd..76b2d94056e 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -127,7 +127,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, "net-mtu", "vlan-transparent", "address-scope", "availability_zone", - "network_availability_zone"] + "network_availability_zone", + "default-subnetpools"] @property def supported_extension_aliases(self): diff --git a/neutron/tests/unit/api/rpc/handlers/test_l3_rpc.py b/neutron/tests/unit/api/rpc/handlers/test_l3_rpc.py index 8c7147892bf..67eba74d415 100644 --- a/neutron/tests/unit/api/rpc/handlers/test_l3_rpc.py +++ b/neutron/tests/unit/api/rpc/handlers/test_l3_rpc.py @@ -42,12 +42,11 @@ class TestL3RpcCallback(testlib_api.SqlTestCase): return self.plugin.create_network(self.ctx, network) def _prepare_ipv6_pd_subnet(self): - # TODO(Carl) Use the default subnet pool extension when available subnet = {'subnet': {'network_id': self.network['id'], 'tenant_id': 'tenant_id', 'cidr': None, 'ip_version': 6, - 'subnetpool_id': constants.IPV6_PD_POOL_ID, + 'use_default_subnetpool': True, 'name': 'ipv6_pd', 'enable_dhcp': True, 'host_routes': None, diff --git a/neutron/tests/unit/db/test_db_base_plugin_v2.py b/neutron/tests/unit/db/test_db_base_plugin_v2.py index 1397f678092..18e757020b3 100644 --- a/neutron/tests/unit/db/test_db_base_plugin_v2.py +++ b/neutron/tests/unit/db/test_db_base_plugin_v2.py @@ -1779,7 +1779,6 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s if ipv6_pd: cidr = None gateway = None - # TODO(Carl) Use the default subnet pool extension when available subnetpool_id = constants.IPV6_PD_POOL_ID cfg.CONF.set_override('ipv6_pd_enabled', True) return (self._make_subnet(self.fmt, network, gateway=gateway, @@ -2929,109 +2928,6 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): res = subnet_req.get_response(self.api) self.assertEqual(webob.exc.HTTPClientError.code, res.status_int) - def test_create_subnet_only_ip_version_v4(self): - # TODO(carl_baldwin): add test to allow create_subnet - # to work with 'default' flag for subnet pool on - with self.network() as network: - tenant_id = network['network']['tenant_id'] - subnetpool_prefix = '10.0.0.0/8' - with self.subnetpool(prefixes=[subnetpool_prefix], - admin=True, - name="My subnet pool", - tenant_id=tenant_id, - min_prefixlen='25', - is_default=True) as subnetpool: - subnetpool_id = subnetpool['subnetpool']['id'] - data = {'subnet': {'network_id': network['network']['id'], - 'ip_version': '4', - 'prefixlen': '27', - 'tenant_id': tenant_id, - 'subnetpool_id': subnetpool_id}} - subnet_req = self.new_create_request('subnets', data) - res = subnet_req.get_response(self.api) - subnet = self.deserialize(self.fmt, res)['subnet'] - ip_net = netaddr.IPNetwork(subnet['cidr']) - self.assertIn(ip_net, netaddr.IPNetwork(subnetpool_prefix)) - self.assertEqual(27, ip_net.prefixlen) - self.assertEqual(subnetpool_id, subnet['subnetpool_id']) - - def test_create_subnet_only_ip_version_v4_old(self): - # TODO(john-davidge): Remove after Mitaka release. - with self.network() as network: - tenant_id = network['network']['tenant_id'] - subnetpool_prefix = '10.0.0.0/8' - with self.subnetpool(prefixes=[subnetpool_prefix], - admin=False, - name="My subnet pool", - tenant_id=tenant_id, - min_prefixlen='25') as subnetpool: - subnetpool_id = subnetpool['subnetpool']['id'] - cfg.CONF.set_override('default_ipv4_subnet_pool', - subnetpool_id) - data = {'subnet': {'network_id': network['network']['id'], - 'ip_version': '4', - 'prefixlen': '27', - 'tenant_id': tenant_id, - 'subnetpool_id': subnetpool_id}} - subnet_req = self.new_create_request('subnets', data) - res = subnet_req.get_response(self.api) - subnet = self.deserialize(self.fmt, res)['subnet'] - ip_net = netaddr.IPNetwork(subnet['cidr']) - self.assertIn(ip_net, netaddr.IPNetwork(subnetpool_prefix)) - self.assertEqual(27, ip_net.prefixlen) - self.assertEqual(subnetpool_id, subnet['subnetpool_id']) - - def test_create_subnet_only_ip_version_v6(self): - # this test mirrors its v4 counterpart - with self.network() as network: - tenant_id = network['network']['tenant_id'] - subnetpool_prefix = '2000::/56' - with self.subnetpool(prefixes=[subnetpool_prefix], - admin=True, - name="My ipv6 subnet pool", - tenant_id=tenant_id, - min_prefixlen='64', - is_default=True) as subnetpool: - subnetpool_id = subnetpool['subnetpool']['id'] - cfg.CONF.set_override('ipv6_pd_enabled', False) - data = {'subnet': {'network_id': network['network']['id'], - 'ip_version': '6', - 'tenant_id': tenant_id, - 'subnetpool_id': subnetpool_id}} - subnet_req = self.new_create_request('subnets', data) - res = subnet_req.get_response(self.api) - subnet = self.deserialize(self.fmt, res)['subnet'] - self.assertEqual(subnetpool_id, subnet['subnetpool_id']) - ip_net = netaddr.IPNetwork(subnet['cidr']) - self.assertIn(ip_net, netaddr.IPNetwork(subnetpool_prefix)) - self.assertEqual(64, ip_net.prefixlen) - - def test_create_subnet_only_ip_version_v6_old(self): - # TODO(john-davidge): Remove after Mitaka release. - with self.network() as network: - tenant_id = network['network']['tenant_id'] - subnetpool_prefix = '2000::/56' - with self.subnetpool(prefixes=[subnetpool_prefix], - admin=False, - name="My ipv6 subnet pool", - tenant_id=tenant_id, - min_prefixlen='64') as subnetpool: - subnetpool_id = subnetpool['subnetpool']['id'] - cfg.CONF.set_override('default_ipv6_subnet_pool', - subnetpool_id) - cfg.CONF.set_override('ipv6_pd_enabled', False) - data = {'subnet': {'network_id': network['network']['id'], - 'ip_version': '6', - 'tenant_id': tenant_id, - 'subnetpool_id': subnetpool_id}} - subnet_req = self.new_create_request('subnets', data) - res = subnet_req.get_response(self.api) - subnet = self.deserialize(self.fmt, res)['subnet'] - self.assertEqual(subnetpool_id, subnet['subnetpool_id']) - ip_net = netaddr.IPNetwork(subnet['cidr']) - self.assertIn(ip_net, netaddr.IPNetwork(subnetpool_prefix)) - self.assertEqual(64, ip_net.prefixlen) - def test_create_subnet_bad_V4_cidr_prefix_len(self): with self.network() as network: data = {'subnet': {'network_id': network['network']['id'], @@ -3066,41 +2962,6 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): res = subnet_req.get_response(self.api) self.assertEqual(webob.exc.HTTPClientError.code, res.status_int) - def _test_create_subnet_V6_pd_modes(self, ra_addr_mode, expect_fail=False): - cfg.CONF.set_override('ipv6_pd_enabled', True) - # TODO(carl_baldwin): replace explicit subnetpool_id with request to - # default subnetpool - with self.network() as network: - data = {'subnet': {'network_id': network['network']['id'], - 'ip_version': '6', - 'tenant_id': network['network']['tenant_id'], - 'subnetpool_id': constants.IPV6_PD_POOL_ID}} - if ra_addr_mode: - data['subnet']['ipv6_ra_mode'] = ra_addr_mode - data['subnet']['ipv6_address_mode'] = ra_addr_mode - subnet_req = self.new_create_request('subnets', data) - res = subnet_req.get_response(self.api) - if expect_fail: - self.assertEqual(webob.exc.HTTPClientError.code, - res.status_int) - else: - subnet = self.deserialize(self.fmt, res)['subnet'] - self.assertEqual(constants.IPV6_PD_POOL_ID, - subnet['subnetpool_id']) - - def test_create_subnet_V6_pd_slaac(self): - self._test_create_subnet_V6_pd_modes('slaac') - - def test_create_subnet_V6_pd_stateless(self): - self._test_create_subnet_V6_pd_modes('dhcpv6-stateless') - - def test_create_subnet_V6_pd_statefull(self): - self._test_create_subnet_V6_pd_modes('dhcpv6-statefull', - expect_fail=True) - - def test_create_subnet_V6_pd_no_mode(self): - self._test_create_subnet_V6_pd_modes(None, expect_fail=True) - def test_create_2_subnets_overlapping_cidr_allowed_returns_200(self): cidr_1 = '10.0.0.0/23' cidr_2 = '10.0.0.0/24' diff --git a/neutron/tests/unit/db/test_ipam_pluggable_backend.py b/neutron/tests/unit/db/test_ipam_pluggable_backend.py index 98560c62be5..90c387a3ae3 100644 --- a/neutron/tests/unit/db/test_ipam_pluggable_backend.py +++ b/neutron/tests/unit/db/test_ipam_pluggable_backend.py @@ -292,7 +292,6 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase): cfg.CONF.set_override('ipv6_pd_enabled', True) cidr = constants.PROVISIONAL_IPV6_PD_PREFIX allocation_pools = [netaddr.IPRange('::2', '::ffff:ffff:ffff:ffff')] - # TODO(Carl) Use the default subnet pool extension when available with self.subnet(cidr=None, ip_version=6, subnetpool_id=constants.IPV6_PD_POOL_ID, ipv6_ra_mode=constants.IPV6_SLAAC, diff --git a/neutron/tests/unit/extensions/test_default_subnetpools.py b/neutron/tests/unit/extensions/test_default_subnetpools.py new file mode 100644 index 00000000000..5c49d7a171b --- /dev/null +++ b/neutron/tests/unit/extensions/test_default_subnetpools.py @@ -0,0 +1,190 @@ +# 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 netaddr +from oslo_config import cfg +import webob.exc + +from neutron.common import constants +from neutron.db import db_base_plugin_v2 +from neutron.extensions import default_subnetpools +from neutron.tests.unit.db import test_db_base_plugin_v2 + + +class DefaultSubnetpoolsExtensionManager(object): + + def get_resources(self): + return [] + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + def get_extended_resources(self, version): + return default_subnetpools.get_extended_resources(version) + + +class DefaultSubnetpoolsExtensionTestPlugin( + db_base_plugin_v2.NeutronDbPluginV2): + """Test plugin to mixin the default subnet pools extension. + """ + + supported_extension_aliases = ["default-subnetpools", "subnet_allocation"] + + +class DefaultSubnetpoolsExtensionTestCase( + test_db_base_plugin_v2.NeutronDbPluginV2TestCase): + """Test API extension default_subnetpools attributes. + """ + + def setUp(self): + plugin = ('neutron.tests.unit.extensions.test_default_subnetpools.' + + 'DefaultSubnetpoolsExtensionTestPlugin') + ext_mgr = DefaultSubnetpoolsExtensionManager() + super(DefaultSubnetpoolsExtensionTestCase, + self).setUp(plugin=plugin, ext_mgr=ext_mgr) + + def test_create_subnet_only_ip_version_v4(self): + with self.network() as network: + tenant_id = network['network']['tenant_id'] + subnetpool_prefix = '10.0.0.0/8' + with self.subnetpool(prefixes=[subnetpool_prefix], + admin=True, + name="My subnet pool", + tenant_id=tenant_id, + min_prefixlen='25', + is_default=True) as subnetpool: + subnetpool_id = subnetpool['subnetpool']['id'] + data = {'subnet': {'network_id': network['network']['id'], + 'ip_version': '4', + 'prefixlen': '27', + 'tenant_id': tenant_id, + 'use_default_subnetpool': True}} + subnet_req = self.new_create_request('subnets', data) + res = subnet_req.get_response(self.api) + subnet = self.deserialize(self.fmt, res)['subnet'] + ip_net = netaddr.IPNetwork(subnet['cidr']) + self.assertIn(ip_net, netaddr.IPNetwork(subnetpool_prefix)) + self.assertEqual(27, ip_net.prefixlen) + self.assertEqual(subnetpool_id, subnet['subnetpool_id']) + + def test_create_subnet_only_ip_version_v4_old(self): + # TODO(john-davidge): Remove after Mitaka release. + with self.network() as network: + tenant_id = network['network']['tenant_id'] + subnetpool_prefix = '10.0.0.0/8' + with self.subnetpool(prefixes=[subnetpool_prefix], + admin=False, + name="My subnet pool", + tenant_id=tenant_id, + min_prefixlen='25') as subnetpool: + subnetpool_id = subnetpool['subnetpool']['id'] + cfg.CONF.set_override('default_ipv4_subnet_pool', + subnetpool_id) + data = {'subnet': {'network_id': network['network']['id'], + 'ip_version': '4', + 'prefixlen': '27', + 'tenant_id': tenant_id, + 'use_default_subnetpool': True}} + subnet_req = self.new_create_request('subnets', data) + res = subnet_req.get_response(self.api) + subnet = self.deserialize(self.fmt, res)['subnet'] + ip_net = netaddr.IPNetwork(subnet['cidr']) + self.assertIn(ip_net, netaddr.IPNetwork(subnetpool_prefix)) + self.assertEqual(27, ip_net.prefixlen) + self.assertEqual(subnetpool_id, subnet['subnetpool_id']) + + def test_create_subnet_only_ip_version_v6(self): + # this test mirrors its v4 counterpart + with self.network() as network: + tenant_id = network['network']['tenant_id'] + subnetpool_prefix = '2000::/56' + with self.subnetpool(prefixes=[subnetpool_prefix], + admin=True, + name="My ipv6 subnet pool", + tenant_id=tenant_id, + min_prefixlen='64', + is_default=True) as subnetpool: + subnetpool_id = subnetpool['subnetpool']['id'] + cfg.CONF.set_override('ipv6_pd_enabled', False) + data = {'subnet': {'network_id': network['network']['id'], + 'ip_version': '6', + 'tenant_id': tenant_id, + 'use_default_subnetpool': True}} + subnet_req = self.new_create_request('subnets', data) + res = subnet_req.get_response(self.api) + subnet = self.deserialize(self.fmt, res)['subnet'] + self.assertEqual(subnetpool_id, subnet['subnetpool_id']) + ip_net = netaddr.IPNetwork(subnet['cidr']) + self.assertIn(ip_net, netaddr.IPNetwork(subnetpool_prefix)) + self.assertEqual(64, ip_net.prefixlen) + + def test_create_subnet_only_ip_version_v6_old(self): + # TODO(john-davidge): Remove after Mitaka release. + with self.network() as network: + tenant_id = network['network']['tenant_id'] + subnetpool_prefix = '2000::/56' + with self.subnetpool(prefixes=[subnetpool_prefix], + admin=False, + name="My ipv6 subnet pool", + tenant_id=tenant_id, + min_prefixlen='64') as subnetpool: + subnetpool_id = subnetpool['subnetpool']['id'] + cfg.CONF.set_override('default_ipv6_subnet_pool', + subnetpool_id) + cfg.CONF.set_override('ipv6_pd_enabled', False) + data = {'subnet': {'network_id': network['network']['id'], + 'ip_version': '6', + 'tenant_id': tenant_id, + 'use_default_subnetpool': True}} + subnet_req = self.new_create_request('subnets', data) + res = subnet_req.get_response(self.api) + subnet = self.deserialize(self.fmt, res)['subnet'] + self.assertEqual(subnetpool_id, subnet['subnetpool_id']) + ip_net = netaddr.IPNetwork(subnet['cidr']) + self.assertIn(ip_net, netaddr.IPNetwork(subnetpool_prefix)) + self.assertEqual(64, ip_net.prefixlen) + + def _test_create_subnet_V6_pd_modes(self, ra_addr_mode, expect_fail=False): + cfg.CONF.set_override('ipv6_pd_enabled', True) + with self.network() as network: + data = {'subnet': {'network_id': network['network']['id'], + 'ip_version': '6', + 'tenant_id': network['network']['tenant_id'], + 'use_default_subnetpool': True}} + if ra_addr_mode: + data['subnet']['ipv6_ra_mode'] = ra_addr_mode + data['subnet']['ipv6_address_mode'] = ra_addr_mode + subnet_req = self.new_create_request('subnets', data) + res = subnet_req.get_response(self.api) + if expect_fail: + self.assertEqual(webob.exc.HTTPClientError.code, + res.status_int) + else: + subnet = self.deserialize(self.fmt, res)['subnet'] + self.assertEqual(constants.IPV6_PD_POOL_ID, + subnet['subnetpool_id']) + + def test_create_subnet_V6_pd_slaac(self): + self._test_create_subnet_V6_pd_modes('slaac') + + def test_create_subnet_V6_pd_stateless(self): + self._test_create_subnet_V6_pd_modes('dhcpv6-stateless') + + def test_create_subnet_V6_pd_statefull(self): + self._test_create_subnet_V6_pd_modes('dhcpv6-statefull', + expect_fail=True) + + def test_create_subnet_V6_pd_no_mode(self): + self._test_create_subnet_V6_pd_modes(None, expect_fail=True) diff --git a/releasenotes/notes/default-subnetpool-semantics-1cdc5cdde2be88c2.yaml b/releasenotes/notes/default-subnetpool-semantics-1cdc5cdde2be88c2.yaml new file mode 100644 index 00000000000..b226c6083b3 --- /dev/null +++ b/releasenotes/notes/default-subnetpool-semantics-1cdc5cdde2be88c2.yaml @@ -0,0 +1,27 @@ +--- +features: + - The subnet API now includes a new + use_default_subnetpool attribute. This attribute can + be specified on creating a subnet in lieu of a + subnetpool_id. The two are mutually exclusive. If + it is specified as True, the default subnet pool for + the requested ip_version will be looked up and used. + If no default exists, an error will be returned. +deprecations: + - The default_subnet_pools option is now deprecated and + will be removed in the Newton release. The same + functionality is now provided by setting is_default + attribute on subnetpools to True using the API or + client. +fixes: + - Before Mitaka, when a default subnetpool was defined + in the configuration, a request to create a subnet + would fall back to using it if no specific subnet + pool was specified. This behavior broke the + semantics of subnet create calls in this scenario and + is now considered an API bug. This bug has been + fixed so that there is no automatic fallback with the + presence of a default subnet pool. Workflows which + depended on this new behavior will have to be + modified to set the new use_default_subnetpool + attribute when creating a subnet.