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
This commit is contained in:
Sean M. Collins 2014-01-30 14:12:17 -05:00
parent 903aa84ff9
commit 2a4e50caea
7 changed files with 315 additions and 5 deletions

View 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,

View 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]

View 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)

View File

@ -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')

View 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):

View 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,

View 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: