From: Sean M. Collins Date: Thu, 30 Jan 2014 19:12:17 +0000 (-0500) Subject: Create new IPv6 attributes for Subnets X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=2a4e50caeaa271937a23ab7c052c7e9c47c1933f;p=openstack-build%2Fneutron-build.git Create new IPv6 attributes for Subnets * Introduces two new optional attributes for Subnets: * ipv6_ra_mode * ipv6_address_mode Both attributes accept the following values: * dhcpv6-stateful * dhcpv6-stateless * slaac In addition to these values, additional behaviors are specified for when only one of the attributes is set. For example, a Neutron network that uses a physical router as a gateway, that transmits ICMPv6 Router Advertisement packets to configure hosts on the network will create Neutron Subnets that have ipv6_ra_mode *not* set, and ipv6_address_mode set to 'slaac' so that Neutron will calculate EUI64 addresses for each port assigned to the subnet, and not spawn a Dnsmasq process. These attributes maintain backwards compatability with the enable_dhcp Subnet attribute, by requiring a subnet with these attributes to also have enable_dhcp set to True. DocImpact Implements bp ipv6-two-attributes Change-Id: I5b2313fff5dca1c16ff939fdc4397d7f95ba3ba5 --- diff --git a/neutron/api/v2/attributes.py b/neutron/api/v2/attributes.py index 1b90bb73a..42d23d75d 100644 --- a/neutron/api/v2/attributes.py +++ b/neutron/api/v2/attributes.py @@ -720,6 +720,15 @@ RESOURCE_ATTRIBUTE_MAP = { 'default': True, 'convert_to': convert_to_boolean, 'is_visible': True}, + 'ipv6_ra_mode': {'allow_post': True, 'allow_put': True, + 'default': ATTR_NOT_SPECIFIED, + 'validate': {'type:values': constants.IPV6_MODES}, + 'is_visible': True}, + 'ipv6_address_mode': {'allow_post': True, 'allow_put': True, + 'default': ATTR_NOT_SPECIFIED, + 'validate': {'type:values': + constants.IPV6_MODES}, + 'is_visible': True}, SHARED: {'allow_post': False, 'allow_put': False, 'default': False, diff --git a/neutron/common/constants.py b/neutron/common/constants.py index cfa7cce87..13f1ca939 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -110,3 +110,8 @@ PROTO_NUM_UDP = 17 # Neighbor Solicitation (135), # Neighbor Advertisement (136) ICMPV6_ALLOWED_TYPES = [130, 131, 132, 134, 135, 136] + +DHCPV6_STATEFUL = 'dhcpv6-stateful' +DHCPV6_STATELESS = 'dhcpv6-stateless' +IPV6_SLAAC = 'slaac' +IPV6_MODES = [DHCPV6_STATEFUL, DHCPV6_STATELESS, IPV6_SLAAC] diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index b7ad1ecff..4db152eb2 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -819,6 +819,60 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, tenant_ids.pop() != original.tenant_id): raise n_exc.InvalidSharedSetting(network=original.name) + def _validate_ipv6_attributes(self, subnet, cur_subnet): + ra_mode_set = attributes.is_attr_set(subnet.get('ipv6_ra_mode')) + address_mode_set = attributes.is_attr_set( + subnet.get('ipv6_address_mode')) + if cur_subnet: + ra_mode = (subnet['ipv6_ra_mode'] if ra_mode_set + else cur_subnet['ipv6_ra_mode']) + addr_mode = (subnet['ipv6_address_mode'] if address_mode_set + else cur_subnet['ipv6_address_mode']) + if ra_mode_set or address_mode_set: + # Check that updated subnet ipv6 attributes do not conflict + self._validate_ipv6_combination(ra_mode, addr_mode) + self._validate_ipv6_update_dhcp(subnet, cur_subnet) + else: + self._validate_ipv6_dhcp(ra_mode_set, address_mode_set, + subnet['enable_dhcp']) + if ra_mode_set and address_mode_set: + self._validate_ipv6_combination(subnet['ipv6_ra_mode'], + subnet['ipv6_address_mode']) + + def _validate_ipv6_combination(self, ra_mode, address_mode): + if ra_mode != address_mode: + msg = _("ipv6_ra_mode set to '%(ra_mode)s' with ipv6_address_mode " + "set to '%(addr_mode)s' is not valid. " + "If both attributes are set, they must be the same value" + ) % {'ra_mode': ra_mode, 'addr_mode': address_mode} + raise n_exc.InvalidInput(error_message=msg) + + def _validate_ipv6_dhcp(self, ra_mode_set, address_mode_set, enable_dhcp): + if (ra_mode_set or address_mode_set) and not enable_dhcp: + msg = _("ipv6_ra_mode or ipv6_address_mode cannot be set when " + "enable_dhcp is set to False.") + raise n_exc.InvalidInput(error_message=msg) + + def _validate_ipv6_update_dhcp(self, subnet, cur_subnet): + if ('enable_dhcp' in subnet and not subnet['enable_dhcp']): + msg = _("Cannot disable enable_dhcp with " + "ipv6 attributes set") + + ra_mode_set = attributes.is_attr_set(subnet.get('ipv6_ra_mode')) + address_mode_set = attributes.is_attr_set( + subnet.get('ipv6_address_mode')) + + if ra_mode_set or address_mode_set: + raise n_exc.InvalidInput(error_message=msg) + + old_ra_mode_set = attributes.is_attr_set( + cur_subnet.get('ipv6_ra_mode')) + old_address_mode_set = attributes.is_attr_set( + cur_subnet.get('ipv6_address_mode')) + + if old_ra_mode_set or old_address_mode_set: + raise n_exc.InvalidInput(error_message=msg) + def _make_network_dict(self, network, fields=None, process_extensions=True): res = {'id': network['id'], @@ -847,6 +901,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, for pool in subnet['allocation_pools']], 'gateway_ip': subnet['gateway_ip'], 'enable_dhcp': subnet['enable_dhcp'], + 'ipv6_ra_mode': subnet['ipv6_ra_mode'], + 'ipv6_address_mode': subnet['ipv6_address_mode'], 'dns_nameservers': [dns['address'] for dns in subnet['dns_nameservers']], 'host_routes': [{'destination': route['destination'], @@ -1050,6 +1106,9 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, for rt in s['host_routes']: self._validate_host_route(rt, ip_ver) + if ip_ver == 6: + self._validate_ipv6_attributes(s, cur_subnet) + def _validate_gw_out_of_pools(self, gateway_ip, pools): for allocation_pool in pools: pool_range = netaddr.IPRange( @@ -1093,6 +1152,11 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, 'enable_dhcp': s['enable_dhcp'], 'gateway_ip': s['gateway_ip'], 'shared': network.shared} + if s['ip_version'] == 6 and s['enable_dhcp']: + if attributes.is_attr_set(s['ipv6_ra_mode']): + args['ipv6_ra_mode'] = s['ipv6_ra_mode'] + if attributes.is_attr_set(s['ipv6_address_mode']): + args['ipv6_address_mode'] = s['ipv6_address_mode'] subnet = models_v2.Subnet(**args) context.session.add(subnet) diff --git a/neutron/db/migration/alembic_migrations/versions/2447ad0e9585_add_ipv6_mode_props.py b/neutron/db/migration/alembic_migrations/versions/2447ad0e9585_add_ipv6_mode_props.py new file mode 100644 index 000000000..fa8d99d1f --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/2447ad0e9585_add_ipv6_mode_props.py @@ -0,0 +1,78 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. +# +# @author Sean M. Collins (Comcast) + +"""Add IPv6 Subnet properties + +Revision ID: 2447ad0e9585 +Revises: 33dd0a9fa487 +Create Date: 2013-10-23 16:36:44.188904 + +""" + +# revision identifiers, used by Alembic. +revision = '2447ad0e9585' +down_revision = '33dd0a9fa487' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + '*' +] + +from alembic import op +import sqlalchemy as sa + + +from neutron.db import migration + + +def upgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + # Workaround for Alemic bug #89 + # https://bitbucket.org/zzzeek/alembic/issue/89 + context = op.get_context() + if context.bind.dialect.name == 'postgresql': + op.execute("CREATE TYPE ipv6_modes AS ENUM ('%s', '%s', '%s')" + % ('slaac', 'dhcpv6-stateful', 'dhcpv6-stateless')) + op.add_column('subnets', + sa.Column('ipv6_ra_mode', + sa.Enum('slaac', + 'dhcpv6-stateful', + 'dhcpv6-stateless', + name='ipv6_modes'), + nullable=True) + ) + op.add_column('subnets', + sa.Column('ipv6_address_mode', + sa.Enum('slaac', + 'dhcpv6-stateful', + 'dhcpv6-stateless', + name='ipv6_modes'), + nullable=True) + ) + + +def downgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.drop_column('subnets', 'ipv6_ra_mode') + op.drop_column('subnets', 'ipv6_address_mode') + context = op.get_context() + if context.bind.dialect.name == 'postgresql': + op.execute('DROP TYPE ipv6_modes') diff --git a/neutron/db/models_v2.py b/neutron/db/models_v2.py index 387adcfdb..2c77ec67e 100644 --- a/neutron/db/models_v2.py +++ b/neutron/db/models_v2.py @@ -18,6 +18,7 @@ import sqlalchemy as sa from sqlalchemy import orm +from neutron.common import constants from neutron.db import model_base from neutron.openstack.common import uuidutils @@ -183,6 +184,14 @@ class Subnet(model_base.BASEV2, HasId, HasTenant): backref='subnet', cascade='all, delete, delete-orphan') shared = sa.Column(sa.Boolean) + ipv6_ra_mode = sa.Column(sa.Enum(constants.IPV6_SLAAC, + constants.DHCPV6_STATEFUL, + constants.DHCPV6_STATELESS, + name='ipv6_modes'), nullable=True) + ipv6_address_mode = sa.Column(sa.Enum(constants.IPV6_SLAAC, + constants.DHCPV6_STATEFUL, + constants.DHCPV6_STATELESS, + name='ipv6_modes'), nullable=True) class Network(model_base.BASEV2, HasId, HasTenant): diff --git a/neutron/tests/unit/oneconvergence/test_nvsd_plugin.py b/neutron/tests/unit/oneconvergence/test_nvsd_plugin.py index 02b0fef1d..4a20ce742 100644 --- a/neutron/tests/unit/oneconvergence/test_nvsd_plugin.py +++ b/neutron/tests/unit/oneconvergence/test_nvsd_plugin.py @@ -69,6 +69,24 @@ class TestOneConvergencePluginSubnetsV2(test_plugin.TestSubnetsV2, def test_update_subnet_inconsistent_ipv6_hostroute_np_v4(self): self.skipTest("NVSD Plugin does not support IPV6.") + def test_create_subnet_ipv6_attributes(self): + self.skipTest("NVSD Plugin does not support IPV6.") + + def test_create_subnet_ipv6_single_attribute_set(self): + self.skipTest("NVSD Plugin does not support IPV6.") + + def test_update_subnet_ipv6_attributes(self): + self.skipTest("NVSD Plugin does not support IPV6.") + + def test_update_subnet_ipv6_inconsistent_enable_dhcp(self): + self.skipTest("NVSD Plugin does not support IPV6.") + + def test_update_subnet_ipv6_inconsistent_ra_attribute(self): + self.skipTest("NVSD Plugin does not support IPV6.") + + def test_update_subnet_ipv6_inconsistent_address_attribute(self): + self.skipTest("NVSD Plugin does not support IPV6.") + class TestOneConvergencePluginPortsV2(test_plugin.TestPortsV2, test_bindings.PortBindingsTestCase, diff --git a/neutron/tests/unit/test_db_plugin.py b/neutron/tests/unit/test_db_plugin.py index c7e780ac0..3cbd357ed 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -312,7 +312,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase): for arg in ('ip_version', 'tenant_id', 'enable_dhcp', 'allocation_pools', 'dns_nameservers', 'host_routes', - 'shared'): + 'shared', 'ipv6_ra_mode', 'ipv6_address_mode'): # Arg must be present and not null (but can be false) if arg in kwargs and kwargs[arg] is not None: data['subnet'][arg] = kwargs[arg] @@ -405,7 +405,8 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase): def _make_subnet(self, fmt, network, gateway, cidr, allocation_pools=None, ip_version=4, enable_dhcp=True, - dns_nameservers=None, host_routes=None, shared=None): + dns_nameservers=None, host_routes=None, shared=None, + ipv6_ra_mode=None, ipv6_address_mode=None): res = self._create_subnet(fmt, net_id=network['network']['id'], cidr=cidr, @@ -416,7 +417,9 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase): enable_dhcp=enable_dhcp, dns_nameservers=dns_nameservers, host_routes=host_routes, - shared=shared) + shared=shared, + ipv6_ra_mode=ipv6_ra_mode, + ipv6_address_mode=ipv6_address_mode) # Things can go wrong - raise HTTP exc with res code only # so it can be caught by unit tests if res.status_int >= webob.exc.HTTPClientError.code: @@ -542,7 +545,9 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase): dns_nameservers=None, host_routes=None, shared=None, - do_delete=True): + do_delete=True, + ipv6_ra_mode=None, + ipv6_address_mode=None): with optional_ctx(network, self.network) as network_to_use: subnet = self._make_subnet(fmt or self.fmt, network_to_use, @@ -553,7 +558,9 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase): enable_dhcp, dns_nameservers, host_routes, - shared=shared) + shared=shared, + ipv6_ra_mode=ipv6_ra_mode, + ipv6_address_mode=ipv6_address_mode) try: yield subnet finally: @@ -2947,6 +2954,79 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): res = subnet_req.get_response(self.api) self.assertEqual(res.status_int, webob.exc.HTTPClientError.code) + def test_create_subnet_ipv6_attributes(self): + gateway_ip = 'fe80::1' + cidr = 'fe80::/80' + + for mode in constants.IPV6_MODES: + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + ipv6_ra_mode=mode, + ipv6_address_mode=mode) + + def test_create_subnet_ipv6_attributes_no_dhcp_enabled(self): + gateway_ip = 'fe80::1' + cidr = 'fe80::/80' + with testlib_api.ExpectedException( + webob.exc.HTTPClientError) as ctx_manager: + for mode in constants.IPV6_MODES: + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + enable_dhcp=False, + ipv6_ra_mode=mode, + ipv6_address_mode=mode) + self.assertEqual(ctx_manager.exception.code, + webob.exc.HTTPClientError.code) + + def test_create_subnet_invalid_ipv6_ra_mode(self): + gateway_ip = 'fe80::1' + cidr = 'fe80::/80' + with testlib_api.ExpectedException( + webob.exc.HTTPClientError) as ctx_manager: + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + ipv6_ra_mode='foo', + ipv6_address_mode='slaac') + self.assertEqual(ctx_manager.exception.code, + webob.exc.HTTPClientError.code) + + def test_create_subnet_invalid_ipv6_address_mode(self): + gateway_ip = 'fe80::1' + cidr = 'fe80::/80' + with testlib_api.ExpectedException( + webob.exc.HTTPClientError) as ctx_manager: + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + ipv6_ra_mode='slaac', + ipv6_address_mode='baz') + self.assertEqual(ctx_manager.exception.code, + webob.exc.HTTPClientError.code) + + def test_create_subnet_invalid_ipv6_combination(self): + gateway_ip = 'fe80::1' + cidr = 'fe80::/80' + with testlib_api.ExpectedException( + webob.exc.HTTPClientError) as ctx_manager: + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + ipv6_ra_mode='stateful', + ipv6_address_mode='stateless') + self.assertEqual(ctx_manager.exception.code, + webob.exc.HTTPClientError.code) + + def test_create_subnet_ipv6_single_attribute_set(self): + gateway_ip = 'fe80::1' + cidr = 'fe80::/80' + for mode in constants.IPV6_MODES: + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + ipv6_ra_mode=None, + ipv6_address_mode=mode) + self._test_create_subnet(gateway_ip=gateway_ip, + cidr=cidr, ip_version=6, + ipv6_ra_mode=mode, + ipv6_address_mode=None) + def test_update_subnet_no_gateway(self): with self.subnet() as subnet: data = {'subnet': {'gateway_ip': '11.0.0.1'}} @@ -3108,6 +3188,53 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): self.assertEqual(res.status_int, webob.exc.HTTPConflict.code) + def test_update_subnet_ipv6_attributes(self): + with self.subnet(ip_version=6, cidr='fe80::/80', + ipv6_ra_mode=constants.IPV6_SLAAC, + ipv6_address_mode=constants.IPV6_SLAAC) as subnet: + data = {'subnet': {'ipv6_ra_mode': constants.DHCPV6_STATEFUL, + 'ipv6_address_mode': constants.DHCPV6_STATEFUL}} + req = self.new_update_request('subnets', data, + subnet['subnet']['id']) + res = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertEqual(res['subnet']['ipv6_ra_mode'], + data['subnet']['ipv6_ra_mode']) + self.assertEqual(res['subnet']['ipv6_address_mode'], + data['subnet']['ipv6_address_mode']) + + def test_update_subnet_ipv6_inconsistent_ra_attribute(self): + with self.subnet(ip_version=6, cidr='fe80::/80', + ipv6_ra_mode=constants.IPV6_SLAAC, + ipv6_address_mode=constants.IPV6_SLAAC) as subnet: + data = {'subnet': {'ipv6_ra_mode': constants.DHCPV6_STATEFUL}} + req = self.new_update_request('subnets', data, + subnet['subnet']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, + webob.exc.HTTPClientError.code) + + def test_update_subnet_ipv6_inconsistent_address_attribute(self): + with self.subnet(ip_version=6, cidr='fe80::/80', + ipv6_ra_mode=constants.IPV6_SLAAC, + ipv6_address_mode=constants.IPV6_SLAAC) as subnet: + data = {'subnet': {'ipv6_address_mode': constants.DHCPV6_STATEFUL}} + req = self.new_update_request('subnets', data, + subnet['subnet']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, + webob.exc.HTTPClientError.code) + + def test_update_subnet_ipv6_inconsistent_enable_dhcp(self): + with self.subnet(ip_version=6, cidr='fe80::/80', + ipv6_ra_mode=constants.IPV6_SLAAC, + ipv6_address_mode=constants.IPV6_SLAAC) as subnet: + data = {'subnet': {'enable_dhcp': False}} + req = self.new_update_request('subnets', data, + subnet['subnet']['id']) + res = req.get_response(self.api) + self.assertEqual(res.status_int, + webob.exc.HTTPClientError.code) + def test_show_subnet(self): with self.network() as network: with self.subnet(network=network) as subnet: