diff --git a/neutron/common/ipv6_utils.py b/neutron/common/ipv6_utils.py index 96e1ef23440..ec9168ea364 100644 --- a/neutron/common/ipv6_utils.py +++ b/neutron/common/ipv6_utils.py @@ -20,6 +20,7 @@ import os import netaddr +from neutron.common import constants from neutron.openstack.common.gettextutils import _LI from neutron.openstack.common import log @@ -61,3 +62,9 @@ def is_enabled(): if not _IS_IPV6_ENABLED: LOG.info(_LI("IPv6 is not enabled on this system.")) return _IS_IPV6_ENABLED + + +def is_slaac_subnet(subnet): + """Check if subnet uses SLAAC addressing.""" + return (subnet['ipv6_address_mode'] == constants.IPV6_SLAAC + or subnet['ipv6_address_mode'] == constants.DHCPV6_STATELESS) diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index efb6a61615c..70827c58afd 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -176,12 +176,6 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, ip_address=ip_address, subnet_id=subnet_id).delete() - @staticmethod - def _check_if_subnet_uses_eui64(subnet): - """Check if ipv6 address will be calculated via EUI64.""" - return (subnet['ipv6_address_mode'] == constants.IPV6_SLAAC - or subnet['ipv6_address_mode'] == constants.DHCPV6_STATELESS) - @staticmethod def _store_ip_allocation(context, ip_address, network_id, subnet_id, port_id): @@ -394,7 +388,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, return True return False - def _test_fixed_ips_for_port(self, context, network_id, fixed_ips): + def _test_fixed_ips_for_port(self, context, network_id, fixed_ips, + device_owner): """Test fixed IPs for port. Check that configured subnets are valid prior to allocating any @@ -449,7 +444,9 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, msg = _('IP address %s is not a valid IP for the defined ' 'subnet') % fixed['ip_address'] raise n_exc.InvalidInput(error_message=msg) - if self._check_if_subnet_uses_eui64(subnet): + if (ipv6_utils.is_slaac_subnet(subnet) and device_owner not in + (constants.DEVICE_OWNER_ROUTER_INTF, + constants.DEVICE_OWNER_DVR_INTERFACE)): msg = (_("IPv6 address %(address)s can not be directly " "assigned to a port on subnet %(id)s with " "%(mode)s address mode") % @@ -481,7 +478,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, else: subnet = self._get_subnet(context, fixed['subnet_id']) if (subnet['ip_version'] == 6 and - self._check_if_subnet_uses_eui64(subnet)): + ipv6_utils.is_slaac_subnet(subnet)): prefix = subnet['cidr'] ip_address = ipv6_utils.get_ipv6_addr_by_EUI64( prefix, mac_address) @@ -496,7 +493,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, return ips def _update_ips_for_port(self, context, network_id, port_id, original_ips, - new_ips, mac_address): + new_ips, mac_address, device_owner): """Add or remove IPs from the port.""" ips = [] # These ips are still on the port and haven't been removed @@ -517,7 +514,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, prev_ips.append(original_ip) # Check if the IP's to add are OK - to_add = self._test_fixed_ips_for_port(context, network_id, new_ips) + to_add = self._test_fixed_ips_for_port(context, network_id, new_ips, + device_owner) for ip in original_ips: LOG.debug(_("Port update. Hold %s"), ip) NeutronDbPluginV2._delete_ip_allocation(context, @@ -544,7 +542,8 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, if fixed_configured: configured_ips = self._test_fixed_ips_for_port(context, p["network_id"], - p['fixed_ips']) + p['fixed_ips'], + p['device_owner']) ips = self._allocate_fixed_ips(context, configured_ips, p['mac_address']) @@ -560,7 +559,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, else: v6.append(subnet) for subnet in v6: - if self._check_if_subnet_uses_eui64(subnet): + if ipv6_utils.is_slaac_subnet(subnet): #(dzyu) If true, calculate an IPv6 address # by mac address and prefix, then remove this # subnet from the array of subnets that will be passed @@ -776,7 +775,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, # id together should be equal to 128. Currently neutron supports # EUI64 interface id only, thus limiting the prefix # length to be 64 only. - if self._check_if_subnet_uses_eui64(subnet): + if ipv6_utils.is_slaac_subnet(subnet): if netaddr.IPNetwork(subnet['cidr']).prefixlen != 64: msg = _('Invalid CIDR %s for IPv6 address mode. ' 'OpenStack uses the EUI-64 address format, ' @@ -1084,7 +1083,10 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, s = subnet['subnet'] if s['gateway_ip'] is attributes.ATTR_NOT_SPECIFIED: - s['gateway_ip'] = str(netaddr.IPAddress(net.first + 1)) + if s['ip_version'] == 6 and ipv6_utils.is_slaac_subnet(s): + s['gateway_ip'] = None + else: + s['gateway_ip'] = str(netaddr.IPAddress(net.first + 1)) if s['allocation_pools'] == attributes.ATTR_NOT_SPECIFIED: s['allocation_pools'] = self._allocate_pools_for_subnet(context, s) @@ -1396,7 +1398,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, added_ips, prev_ips = self._update_ips_for_port( context, port["network_id"], id, original["fixed_ips"], p['fixed_ips'], - original['mac_address']) + original['mac_address'], port['device_owner']) # Update ips if necessary for ip in added_ips: diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index 91949d13dbf..3cf84be325e 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -21,6 +21,7 @@ from neutron.api.rpc.agentnotifiers import l3_rpc_agent_api from neutron.api.v2 import attributes from neutron.common import constants as l3_constants from neutron.common import exceptions as n_exc +from neutron.common import ipv6_utils from neutron.common import rpc as n_rpc from neutron.common import utils from neutron.db import model_base @@ -459,15 +460,20 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase): def _add_interface_by_subnet(self, context, router, subnet_id, owner): subnet = self._core_plugin._get_subnet(context, subnet_id) - if not subnet['gateway_ip']: + if (not subnet['gateway_ip'] + and not ipv6_utils.is_slaac_subnet(subnet)): msg = _('Subnet for router interface must have a gateway IP') raise n_exc.BadRequest(resource='router', msg=msg) self._check_for_dup_router_subnet(context, router, subnet['network_id'], subnet_id, subnet['cidr']) - fixed_ip = {'ip_address': subnet['gateway_ip'], - 'subnet_id': subnet['id']} + if subnet['gateway_ip']: + fixed_ip = {'ip_address': subnet['gateway_ip'], + 'subnet_id': subnet['id']} + else: + fixed_ip = {'subnet_id': subnet['id']} + return self._core_plugin.create_port(context, { 'port': {'tenant_id': subnet['tenant_id'], diff --git a/neutron/tests/unit/plumgrid/test_plumgrid_plugin.py b/neutron/tests/unit/plumgrid/test_plumgrid_plugin.py index ff275205b10..b40883976cf 100644 --- a/neutron/tests/unit/plumgrid/test_plumgrid_plugin.py +++ b/neutron/tests/unit/plumgrid/test_plumgrid_plugin.py @@ -81,6 +81,7 @@ class TestPlumgridPluginSubnetsV2(test_plugin.TestSubnetsV2, _unsupported = ( 'test_create_subnet_default_gw_conflict_allocation_pool_returns_409', 'test_create_subnet_defaults', 'test_create_subnet_gw_values', + 'test_create_subnet_ipv6_gw_values', 'test_update_subnet_gateway_in_allocation_pool_returns_409', 'test_update_subnet_allocation_pools', 'test_update_subnet_allocation_pools_invalid_pool_for_cidr') diff --git a/neutron/tests/unit/test_db_plugin.py b/neutron/tests/unit/test_db_plugin.py index 74292fac074..24eab084400 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -1419,6 +1419,22 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s self.assertEqual(res.status_int, webob.exc.HTTPClientError.code) + def test_requested_fixed_ip_address_v6_slaac_router_iface(self): + with self.subnet(gateway_ip='fe80::1', + cidr='fe80::/64', + ip_version=6, + ipv6_address_mode=constants.IPV6_SLAAC) as subnet: + kwargs = {"fixed_ips": [{'subnet_id': subnet['subnet']['id'], + 'ip_address': 'fe80::1'}]} + net_id = subnet['subnet']['network_id'] + device_owner = constants.DEVICE_OWNER_ROUTER_INTF + res = self._create_port(self.fmt, net_id=net_id, + device_owner=device_owner, **kwargs) + port = self.deserialize(self.fmt, res) + self.assertEqual(len(port['port']['fixed_ips']), 1) + self.assertEqual(port['port']['fixed_ips'][0]['ip_address'], + 'fe80::1') + def test_requested_subnet_id_v6_slaac(self): with self.subnet(gateway_ip='fe80::1', cidr='2607:f0d0:1002:51::/64', @@ -2830,6 +2846,38 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): self._test_create_subnet(expected=expected, gateway_ip=gateway) + def test_create_subnet_ipv6_gw_values(self): + cidr = '2001::/64' + # Gateway is last IP in IPv6 DHCPv6 stateful subnet + gateway = '2001::ffff:ffff:ffff:fffe' + allocation_pools = [{'start': '2001::1', + 'end': '2001::ffff:ffff:ffff:fffd'}] + expected = {'gateway_ip': gateway, + 'cidr': cidr, + 'allocation_pools': allocation_pools} + self._test_create_subnet(expected=expected, gateway_ip=gateway, + cidr=cidr, ip_version=6, + ipv6_ra_mode=constants.DHCPV6_STATEFUL, + ipv6_address_mode=constants.DHCPV6_STATEFUL) + # Gateway is first IP in IPv6 DHCPv6 stateful subnet + gateway = '2001::1' + allocation_pools = [{'start': '2001::2', + 'end': '2001::ffff:ffff:ffff:fffe'}] + expected = {'gateway_ip': gateway, + 'cidr': cidr, + 'allocation_pools': allocation_pools} + self._test_create_subnet(expected=expected, gateway_ip=gateway, + cidr=cidr, ip_version=6, + ipv6_ra_mode=constants.DHCPV6_STATEFUL, + ipv6_address_mode=constants.DHCPV6_STATEFUL) + # Gateway not specified for IPv6 SLAAC subnet + expected = {'gateway_ip': None, + 'cidr': cidr} + self._test_create_subnet(expected=expected, + cidr=cidr, ip_version=6, + ipv6_ra_mode=constants.IPV6_SLAAC, + ipv6_address_mode=constants.IPV6_SLAAC) + def test_create_subnet_gw_outside_cidr_returns_400(self): with self.network() as network: self._create_subnet(self.fmt,