From 342859455690fed57adc9296c457f1bd7a7a93a2 Mon Sep 17 00:00:00 2001 From: Carl Baldwin Date: Thu, 26 Mar 2015 18:10:10 +0000 Subject: [PATCH] Implement default subnet pool configuration settings The default_ipv6_subnet_pool option was added [1] as an integration point between prefix delegation work and subnet allocation work. This patch completes the integration with subnet allocation. This addresses the use case where a deployer wants all ipv6 addresses to come -- by default -- from a globally routable pool of ipv6 addresses. In a deployment with this option set, an API user can still access the old behavior by passing None explicitly as subnetpool_id when creating a subnet. This patch also adds the default_ipv4_subnet_pool for completeness. [1] https://review.openstack.org/#/c/166973 Change-Id: I301189b5cd31d7c5fa4a40fa3e04f8e6ac77592b Partially-Implements: blueprint subnet-allocation --- etc/neutron.conf | 12 +++- neutron/api/v2/attributes.py | 2 +- neutron/common/config.py | 9 ++- neutron/db/db_base_plugin_v2.py | 47 +++++++++++-- .../unit/opencontrail/test_contrail_plugin.py | 2 + neutron/tests/unit/test_db_plugin.py | 68 +++++++++++++++++++ 6 files changed, 130 insertions(+), 10 deletions(-) diff --git a/etc/neutron.conf b/etc/neutron.conf index e25d8e25375..a2c281500e0 100644 --- a/etc/neutron.conf +++ b/etc/neutron.conf @@ -146,10 +146,20 @@ lock_path = $state_path/lock # Maximum number of routes per router # max_routes = 30 +# Default Subnet Pool to be used for IPv4 subnet-allocation. +# Specifies by UUID the pool to be used in case of subnet-create being called +# without a subnet-pool ID. The default of None means that no pool will be +# used unless passed explicitly to subnet create. If no pool is used, then a +# CIDR must be passed to create a subnet and that subnet will not be allocated +# from any pool; it will be considered part of the tenant's private address +# space. +# default_ipv4_subnet_pool = + # Default Subnet Pool to be used for IPv6 subnet-allocation. # Specifies by UUID the pool to be used in case of subnet-create being -# called without CIDR or subnet-pool ID. Set to "prefix_delegation" +# called without a subnet-pool ID. Set to "prefix_delegation" # to enable IPv6 Prefix Delegation in a PD-capable environment. +# See the description for default_ipv4_subnet_pool for more information. # default_ipv6_subnet_pool = # =========== items for MTU selection and advertisement ============= diff --git a/neutron/api/v2/attributes.py b/neutron/api/v2/attributes.py index 1ccbf779d63..a7cefecf593 100644 --- a/neutron/api/v2/attributes.py +++ b/neutron/api/v2/attributes.py @@ -782,7 +782,7 @@ RESOURCE_ATTRIBUTE_MAP = { 'allow_put': False, 'default': ATTR_NOT_SPECIFIED, 'required_by_policy': False, - 'validate': {'type:uuid': None}, + 'validate': {'type:uuid_or_none': None}, 'is_visible': True}, 'prefixlen': {'allow_post': True, 'allow_put': False, diff --git a/neutron/common/config.py b/neutron/common/config.py index 3a17e042d50..2837b4ca695 100644 --- a/neutron/common/config.py +++ b/neutron/common/config.py @@ -71,9 +71,12 @@ core_opts = [ help=_("Maximum number of host routes per subnet")), cfg.IntOpt('max_fixed_ips_per_port', default=5, help=_("Maximum number of fixed ips per port")), - cfg.IntOpt('default_ipv6_subnet_pool', default=None, - help=_("Default subnet-pool to be used for automatic subnet " - "CIDR allocation")), + cfg.StrOpt('default_ipv4_subnet_pool', default=None, + help=_("Default IPv4 subnet-pool to be used for automatic " + "subnet CIDR allocation")), + cfg.StrOpt('default_ipv6_subnet_pool', default=None, + help=_("Default IPv6 subnet-pool to be used for automatic " + "subnet CIDR allocation")), cfg.IntOpt('dhcp_lease_duration', default=86400, deprecated_name='dhcp_lease_time', help=_("DHCP lease duration (in seconds). Use -1 to tell " diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index ea3461b2a13..255544e14df 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -1212,7 +1212,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, @oslo_db_api.wrap_db_retry(max_retries=db_api.MAX_RETRIES, retry_on_request=True, retry_on_deadlock=True) - def _create_subnet_from_pool(self, context, subnet): + def _create_subnet_from_pool(self, context, subnet, subnetpool_id): s = subnet['subnet'] tenant_id = self._get_tenant_id_for_create(context, s) has_allocpool = attributes.is_attr_set(s['allocation_pools']) @@ -1223,7 +1223,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, raise n_exc.BadRequest(resource='subnets', msg=reason) with context.session.begin(subtransactions=True): - subnetpool = self._get_subnetpool(context, s['subnetpool_id']) + subnetpool = self._get_subnetpool(context, subnetpool_id) network = self._get_network(context, s["network_id"]) allocator = subnet_alloc.SubnetAllocator(subnetpool) req = self._make_subnet_request(tenant_id, s, subnetpool) @@ -1273,6 +1273,39 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, subnet['network_id']) return self._make_subnet_dict(subnet) + def _get_subnetpool_id(self, subnet): + """Returns the subnetpool id for this request + + If the pool id was explicitly set in the request then that will be + returned, even if it is None. + + Otherwise, the default pool for the IP version requested will be + returned. This will either be a pool id or None (the default for each + configuration parameter). This implies that the ip version must be + either set implicitly with a specific cidr or explicitly using + ip_version attribute. + + :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: + return subnetpool_id + + 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 == 4: + return cfg.CONF.default_ipv4_subnet_pool + return cfg.CONF.default_ipv6_subnet_pool + def create_subnet(self, context, subnet): s = subnet['subnet'] @@ -1290,11 +1323,15 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, net = netaddr.IPNetwork(s['cidr']) subnet['subnet']['cidr'] = '%s/%s' % (net.network, net.prefixlen) - subnetpool_id = s.get('subnetpool_id', attributes.ATTR_NOT_SPECIFIED) - if not attributes.is_attr_set(subnetpool_id): + subnetpool_id = self._get_subnetpool_id(s) + if not subnetpool_id: + if not has_cidr: + msg = _('A cidr must be specified in the absence of a ' + 'subnet pool') + raise n_exc.BadRequest(resource='subnets', msg=msg) # Create subnet from the implicit(AKA null) pool return self._create_subnet_from_implicit_pool(context, subnet) - return self._create_subnet_from_pool(context, subnet) + return self._create_subnet_from_pool(context, subnet, subnetpool_id) def _update_subnet_dns_nameservers(self, context, id, s): old_dns_list = self._get_dns_by_subnet(context, id) diff --git a/neutron/tests/unit/opencontrail/test_contrail_plugin.py b/neutron/tests/unit/opencontrail/test_contrail_plugin.py index f05407012b8..926b040de66 100644 --- a/neutron/tests/unit/opencontrail/test_contrail_plugin.py +++ b/neutron/tests/unit/opencontrail/test_contrail_plugin.py @@ -215,6 +215,8 @@ class ContrailPluginTestCase(test_plugin.NeutronDbPluginV2TestCase): def setUp(self, plugin=None, ext_mgr=None): if 'v6' in self._testMethodName: self.skipTest("OpenContrail Plugin does not support IPV6.") + if 'test_create_subnet_only_ip_version' in self._testMethodName: + self.skipTest("OpenContrail Plugin does not support subnet pools.") cfg.CONF.keystone_authtoken = KeyStoneInfo() mock.patch('requests.post').start().side_effect = FAKE_SERVER.request super(ContrailPluginTestCase, self).setUp(self._plugin_name) diff --git a/neutron/tests/unit/test_db_plugin.py b/neutron/tests/unit/test_db_plugin.py index 077f4e90fb3..7f5c8a8484c 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -2811,6 +2811,74 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): res = subnet_req.get_response(self.api) self.assertEqual(res.status_int, webob.exc.HTTPClientError.code) + def test_create_subnet_no_ip_version(self): + with self.network() as network: + cfg.CONF.set_override('default_ipv4_subnet_pool', None) + cfg.CONF.set_override('default_ipv6_subnet_pool', None) + data = {'subnet': {'network_id': network['network']['id'], + 'tenant_id': network['network']['tenant_id']}} + subnet_req = self.new_create_request('subnets', data) + res = subnet_req.get_response(self.api) + self.assertEqual(res.status_int, webob.exc.HTTPClientError.code) + + def test_create_subnet_only_ip_version_v6_no_pool(self): + with self.network() as network: + tenant_id = network['network']['tenant_id'] + cfg.CONF.set_override('default_ipv6_subnet_pool', None) + data = {'subnet': {'network_id': network['network']['id'], + 'ip_version': '6', + 'tenant_id': tenant_id}} + subnet_req = self.new_create_request('subnets', data) + res = subnet_req.get_response(self.api) + self.assertEqual(res.status_int, webob.exc.HTTPClientError.code) + + 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=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}} + 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.assertTrue(ip_net in 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): + 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) + data = {'subnet': {'network_id': network['network']['id'], + 'ip_version': '6', + 'tenant_id': tenant_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.assertTrue(ip_net in 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'],