]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Create new IPv6 attributes for Subnets
authorSean M. Collins <sean_collins2@cable.comcast.com>
Thu, 30 Jan 2014 19:12:17 +0000 (14:12 -0500)
committerSean M. Collins <sean_collins2@cable.comcast.com>
Mon, 17 Mar 2014 18:35:46 +0000 (14:35 -0400)
* 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

neutron/api/v2/attributes.py
neutron/common/constants.py
neutron/db/db_base_plugin_v2.py
neutron/db/migration/alembic_migrations/versions/2447ad0e9585_add_ipv6_mode_props.py [new file with mode: 0644]
neutron/db/models_v2.py
neutron/tests/unit/oneconvergence/test_nvsd_plugin.py
neutron/tests/unit/test_db_plugin.py

index 1b90bb73ad71cc107b8a26aacb7ae9505a198ad4..42d23d75d420b7f3ccc07446045d7c5283c8efee 100644 (file)
@@ -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,
index cfa7cce87f8973ece79a1af428988fb2ed0513e9..13f1ca939ce70b754b470f16246baac15c231541 100644 (file)
@@ -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]
index b7ad1ecff602fd250e614cb868c69d1fa257de31..4db152eb2808b18c856a8b2e7e0400325863a6cc 100644 (file)
@@ -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 (file)
index 0000000..fa8d99d
--- /dev/null
@@ -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')
index 387adcfdbfde72c5160a8004cd8ba97a502163a3..2c77ec67e1a8e705b328adecfdd5adaa62c7650f 100644 (file)
@@ -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):
index 02b0fef1d72fe14e7ac7431616419f183e861602..4a20ce742bddf17acf47c0e96783a73c7a10187b 100644 (file)
@@ -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,
index c7e780ac069df913a53db0a7aa463f2661274d1b..3cbd357ed8695a1580df21aa1e1c79e60be3f259 100644 (file)
@@ -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: