Support IPv6 Router

Allow router-gateway-set to work even without an assigned
subnet with the net_id so as to enable IPv6 L3 routing
using the assigned LLA for the gateway.

The goal is to allow for IPv6 routing using just
the allocated LLA address for the gateway port to be
used as the external gateway to connect to the upstream
router. For this purpose router-gateway-set no
longer has a requirement of an assigned subnet.

A new config has also been added to the l3_agent.ini
to allow the user to set a valid ipv6_gateway address
to be used as the gateway for the default ::/0 route

If the ipv6_gateway config is not set and a gateway
is still created without a subnet, the gateway interface
will be configured to accept router advertisements (RAs)
from the upstream router so as to build the default route.

Unit test changes and additions reflect these changes.

APIImpact
DocImpact
UpgradeImpact

Implements: blueprint ipv6-router
Change-Id: Iaefa95f788053ded9fc9c7ff6845c3030c6fd6df
This commit is contained in:
Abishek Subramanian 2015-03-30 13:24:09 -04:00
parent 0264074966
commit 07077bebb6
9 changed files with 239 additions and 58 deletions

View File

@ -36,6 +36,20 @@
# must be left empty.
# gateway_external_network_id =
# With IPv6, the network used for the external gateway does not need
# to have an associated subnet, since the automatically assigned
# link-local address (LLA) can be used. However, an IPv6 gateway address
# is needed for use as the next-hop for the default route. If no IPv6
# gateway address is configured here, (and only then) the neutron router
# will be configured to get its default route from router advertisements (RAs)
# from the upstream router; in which case the upstream router must also be
# configured to send these RAs.
# The ipv6_gateway, when configured, should be the LLA of the interface
# on the upstream router. If a next-hop using a global unique address (GUA)
# is desired, it needs to be done via a subnet allocated to the network
# and not through this parameter.
# ipv6_gateway =
# Indicates that this L3 agent should also handle routers that do not have
# an external network gateway configured. This option should be True only
# for a single agent in a Neutron deployment, and may be False for all agents

View File

@ -14,6 +14,7 @@
#
import eventlet
import netaddr
from oslo_config import cfg
from oslo_log import log as logging
import oslo_messaging
@ -239,6 +240,19 @@ class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback,
LOG.error(msg)
raise SystemExit(1)
if self.conf.ipv6_gateway:
# ipv6_gateway configured. Check for valid v6 link-local address.
try:
msg = _LE("%s used in config as ipv6_gateway is not a valid "
"IPv6 link-local address."),
ip_addr = netaddr.IPAddress(self.conf.ipv6_gateway)
if ip_addr.version != 6 or not ip_addr.is_link_local():
LOG.error(msg, self.conf.ipv6_gateway)
raise SystemExit(1)
except netaddr.AddrFormatError:
LOG.error(msg, self.conf.ipv6_gateway)
raise SystemExit(1)
def _fetch_external_net_id(self, force=False):
"""Find UUID of single external network for this agent."""
if self.conf.gateway_external_network_id:

View File

@ -57,6 +57,23 @@ OPTS = [
cfg.StrOpt('gateway_external_network_id', default='',
help=_("UUID of external network for routers implemented "
"by the agents.")),
cfg.StrOpt('ipv6_gateway', default='',
help=_("With IPv6, the network used for the external gateway "
"does not need to have an associated subnet, since the "
"automatically assigned link-local address (LLA) can "
"be used. However, an IPv6 gateway address is needed "
"for use as the next-hop for the default route. "
"If no IPv6 gateway address is configured here, "
"(and only then) the neutron router will be configured "
"to get its default route from router advertisements "
"(RAs) from the upstream router; in which case the "
"upstream router must also be configured to send "
"these RAs. "
"The ipv6_gateway, when configured, should be the LLA "
"of the interface on the upstream router. If a "
"next-hop using a global unique address (GUA) is "
"desired, it needs to be done via a subnet allocated "
"to the network and not through this parameter. ")),
cfg.BoolOpt('enable_metadata_proxy', default=True,
help=_("Allow running metadata proxy.")),
cfg.BoolOpt('router_delete_namespaces', default=False,

View File

@ -375,21 +375,42 @@ class RouterInfo(object):
# Build up the interface and gateway IP addresses that
# will be added to the interface.
ip_cidrs = common_utils.fixed_ip_cidrs(ex_gw_port['fixed_ips'])
gateway_ips = [subnet['gateway_ip']
for subnet in ex_gw_port['subnets']
if subnet['gateway_ip']]
gateway_ips = []
enable_ra_on_gw = False
if 'subnets' in ex_gw_port:
gateway_ips = [subnet['gateway_ip']
for subnet in ex_gw_port['subnets']
if subnet['gateway_ip']]
if self.use_ipv6 and not self.is_v6_gateway_set(gateway_ips):
# No IPv6 gateway is available, but IPv6 is enabled.
if self.agent_conf.ipv6_gateway:
# ipv6_gateway configured, use address for default route.
gateway_ips.append(self.agent_conf.ipv6_gateway)
else:
# ipv6_gateway is also not configured.
# Use RA for default route.
enable_ra_on_gw = True
self.driver.init_l3(interface_name,
ip_cidrs,
namespace=ns_name,
gateway_ips=gateway_ips,
extra_subnets=ex_gw_port.get('extra_subnets', []),
preserve_ips=preserve_ips)
preserve_ips=preserve_ips,
enable_ra_on_gw=enable_ra_on_gw)
for fixed_ip in ex_gw_port['fixed_ips']:
ip_lib.send_gratuitous_arp(ns_name,
interface_name,
fixed_ip['ip_address'],
self.agent_conf.send_arp_for_ha)
def is_v6_gateway_set(self, gateway_ips):
"""Check to see if list of gateway_ips has an IPv6 gateway.
"""
# Note - don't require a try-except here as all
# gateway_ips elements are valid addresses, if they exist.
return any(netaddr.IPAddress(gw_ip).version == 6
for gw_ip in gateway_ips)
def external_gateway_added(self, ex_gw_port, interface_name):
preserve_ips = self._list_floating_ip_cidrs()
self._external_gateway_added(

View File

@ -78,12 +78,14 @@ class LinuxInterfaceDriver(object):
self.conf = conf
def init_l3(self, device_name, ip_cidrs, namespace=None,
preserve_ips=[], gateway_ips=None, extra_subnets=[]):
preserve_ips=[], gateway_ips=None, extra_subnets=[],
enable_ra_on_gw=False):
"""Set the L3 settings for the interface using data from the port.
ip_cidrs: list of 'X.X.X.X/YY' strings
preserve_ips: list of ip cidrs that should not be removed from device
gateway_ips: For gateway ports, list of external gateway ip addresses
enable_ra_on_gw: Boolean to indicate configuring acceptance of IPv6 RA
"""
device = ip_lib.IPDevice(device_name, namespace=namespace)
@ -114,6 +116,9 @@ class LinuxInterfaceDriver(object):
for gateway_ip in gateway_ips or []:
device.route.add_gateway(gateway_ip)
if enable_ra_on_gw:
self._configure_ipv6_ra(namespace, device_name)
new_onlink_routes = set(s['cidr'] for s in extra_subnets)
existing_onlink_routes = set(
device.route.list_onlink_routes(n_const.IP_VERSION_4) +
@ -166,6 +171,13 @@ class LinuxInterfaceDriver(object):
def get_device_name(self, port):
return (self.DEV_NAME_PREFIX + port.id)[:self.DEV_NAME_LEN]
@staticmethod
def _configure_ipv6_ra(namespace, dev_name):
"""Configure acceptance of IPv6 route advertisements on an intf."""
# Learn the default router's IP address via RAs
ip_lib.IPWrapper(namespace=namespace).netns.execute(
['sysctl', '-w', 'net.ipv6.conf.%s.accept_ra=2' % dev_name])
@abc.abstractmethod
def plug(self, network_id, port_id, device_name, mac_address,
bridge=None, namespace=None, prefix=None):

View File

@ -288,11 +288,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
'name': ''}})
if not gw_port['fixed_ips']:
self._core_plugin.delete_port(context.elevated(), gw_port['id'],
l3_port_check=False)
msg = (_('No IPs available for external network %s') %
network_id)
raise n_exc.BadRequest(resource='router', msg=msg)
LOG.debug('No IPs available for external network %s',
network_id)
with context.session.begin(subtransactions=True):
router.gw_port = self._core_plugin._get_port(context.elevated(),

View File

@ -103,18 +103,23 @@ class L3AgentTestFramework(base.BaseOVSLinuxTestCase):
def generate_router_info(self, enable_ha, ip_version=4, extra_routes=True,
enable_fip=True, enable_snat=True,
dual_stack=False):
dual_stack=False, v6_ext_gw_with_sub=True):
if ip_version == 6 and not dual_stack:
enable_snat = False
enable_fip = False
extra_routes = False
if not v6_ext_gw_with_sub:
self.agent.conf.set_override('ipv6_gateway',
'fe80::f816:3eff:fe2e:1')
return test_l3_agent.prepare_router_data(ip_version=ip_version,
enable_snat=enable_snat,
enable_floating_ip=enable_fip,
enable_ha=enable_ha,
extra_routes=extra_routes,
dual_stack=dual_stack)
dual_stack=dual_stack,
v6_ext_gw_with_sub=(
v6_ext_gw_with_sub))
def manage_router(self, agent, router):
self.addCleanup(self._delete_router, agent, router['id'])
@ -365,6 +370,10 @@ class L3AgentTestCase(L3AgentTestFramework):
def test_legacy_router_lifecycle(self):
self._router_lifecycle(enable_ha=False, dual_stack=True)
def test_legacy_router_lifecycle_with_no_gateway_subnet(self):
self._router_lifecycle(enable_ha=False, dual_stack=True,
v6_ext_gw_with_sub=False)
def test_ha_router_lifecycle(self):
self._router_lifecycle(enable_ha=True)
@ -518,9 +527,12 @@ class L3AgentTestCase(L3AgentTestFramework):
self.assertFalse(self._namespace_exists(
namespaces.NS_PREFIX + routers_to_delete[i]['id']))
def _router_lifecycle(self, enable_ha, ip_version=4, dual_stack=False):
def _router_lifecycle(self, enable_ha, ip_version=4,
dual_stack=False, v6_ext_gw_with_sub=True):
router_info = self.generate_router_info(enable_ha, ip_version,
dual_stack=dual_stack)
dual_stack=dual_stack,
v6_ext_gw_with_sub=(
v6_ext_gw_with_sub))
router = self.manage_router(self.agent, router_info)
if enable_ha:
@ -552,7 +564,7 @@ class L3AgentTestCase(L3AgentTestFramework):
# keepalived on Ubuntu14.04 (i.e., check-neutron-dsvm-functional
# platform) is updated to 1.2.10 (or above).
# For more details: https://review.openstack.org/#/c/151284/
self._assert_gateway(router)
self._assert_gateway(router, v6_ext_gw_with_sub)
self.assertTrue(self.floating_ips_configured(router))
self._assert_snat_chains(router)
self._assert_floating_ip_chains(router)
@ -576,18 +588,24 @@ class L3AgentTestCase(L3AgentTestFramework):
external_port, router.get_external_device_name,
router.ns_name))
def _assert_gateway(self, router):
def _assert_gateway(self, router, v6_ext_gw_with_sub=True):
external_port = router.get_ex_gw_port()
external_device_name = router.get_external_device_name(
external_port['id'])
external_device = ip_lib.IPDevice(external_device_name,
namespace=router.ns_name)
for subnet in external_port['subnets']:
expected_gateway = subnet['gateway_ip']
ip_vers = netaddr.IPAddress(expected_gateway).version
existing_gateway = (external_device.route.get_gateway(
ip_version=ip_vers).get('gateway'))
self.assertEqual(expected_gateway, existing_gateway)
self._gateway_check(subnet['gateway_ip'], external_device)
if not v6_ext_gw_with_sub:
self._gateway_check(self.agent.conf.ipv6_gateway,
external_device)
def _gateway_check(self, gateway_ip, external_device):
expected_gateway = gateway_ip
ip_vers = netaddr.IPAddress(expected_gateway).version
existing_gateway = (external_device.route.get_gateway(
ip_version=ip_vers).get('gateway'))
self.assertEqual(expected_gateway, existing_gateway)
def _assert_ha_device(self, router):
def ha_router_dev_name_getter(not_used):

View File

@ -107,7 +107,8 @@ def router_append_interface(router, count=1, ip_version=4, ra_mode=None,
def prepare_router_data(ip_version=4, enable_snat=None, num_internal_ports=1,
enable_floating_ip=False, enable_ha=False,
extra_routes=False, dual_stack=False):
extra_routes=False, dual_stack=False,
v6_ext_gw_with_sub=True):
fixed_ips = []
subnets = []
for loop_version in (4, 6):
@ -116,7 +117,8 @@ def prepare_router_data(ip_version=4, enable_snat=None, num_internal_ports=1,
prefixlen = 24
subnet_cidr = '19.4.4.0/24'
gateway_ip = '19.4.4.1'
elif loop_version == 6 and (ip_version == 6 or dual_stack):
elif (loop_version == 6 and (ip_version == 6 or dual_stack) and
v6_ext_gw_with_sub):
ip_address = 'fd00::4'
prefixlen = 64
subnet_cidr = 'fd00::/64'
@ -486,6 +488,64 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
def test_agent_remove_internal_network_dist(self):
self._test_internal_network_action_dist('remove')
def _add_external_gateway(self, ri, router, ex_gw_port, interface_name,
enable_ra_on_gw=False,
use_fake_fip=False,
no_subnet=False, no_sub_gw=None,
dual_stack=False):
self.device_exists.return_value = False
if no_sub_gw is None:
no_sub_gw = []
if use_fake_fip:
fake_fip = {'floatingips': [{'id': _uuid(),
'floating_ip_address': '192.168.1.34',
'fixed_ip_address': '192.168.0.1',
'port_id': _uuid()}]}
router[l3_constants.FLOATINGIP_KEY] = fake_fip['floatingips']
ri.external_gateway_added(ex_gw_port, interface_name)
if not router.get('distributed'):
self.assertEqual(self.mock_driver.plug.call_count, 1)
self.assertEqual(self.mock_driver.init_l3.call_count, 1)
if no_subnet and not dual_stack:
self.assertEqual(self.send_arp.call_count, 0)
ip_cidrs = []
gateway_ips = []
if no_sub_gw:
gateway_ips.append(no_sub_gw)
kwargs = {'preserve_ips': [],
'gateway_ips': gateway_ips,
'namespace': 'qrouter-' + router['id'],
'extra_subnets': [],
'enable_ra_on_gw': enable_ra_on_gw}
else:
exp_arp_calls = [mock.call(ri.ns_name, interface_name,
'20.0.0.30', mock.ANY)]
if dual_stack and not no_sub_gw:
exp_arp_calls += [mock.call(ri.ns_name, interface_name,
'2001:192:168:100::2',
mock.ANY)]
self.send_arp.assert_has_calls(exp_arp_calls)
ip_cidrs = ['20.0.0.30/24']
gateway_ips = ['20.0.0.1']
if dual_stack:
if no_sub_gw:
gateway_ips.append(no_sub_gw)
else:
ip_cidrs.append('2001:192:168:100::2/64')
gateway_ips.append('2001:192:168:100::1')
kwargs = {'preserve_ips': ['192.168.1.34/32'],
'gateway_ips': gateway_ips,
'namespace': 'qrouter-' + router['id'],
'extra_subnets': [{'cidr': '172.16.0.0/24'}],
'enable_ra_on_gw': enable_ra_on_gw}
self.mock_driver.init_l3.assert_called_with(interface_name,
ip_cidrs,
**kwargs)
else:
ri._create_dvr_gateway.assert_called_once_with(
ex_gw_port, interface_name,
self.snat_ports)
def _test_external_gateway_action(self, action, router, dual_stack=False):
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
ex_net_id = _uuid()
@ -509,6 +569,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
router['id'], router,
**self.ri_kwargs)
ri.use_ipv6 = False
subnet_id = _uuid()
fixed_ips = [{'subnet_id': subnet_id,
'ip_address': '20.0.0.30',
@ -517,6 +578,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
'cidr': '20.0.0.0/24',
'gateway_ip': '20.0.0.1'}]
if dual_stack:
ri.use_ipv6 = True
subnet_id_v6 = _uuid()
fixed_ips.append({'subnet_id': subnet_id_v6,
'ip_address': '2001:192:168:100::2',
@ -530,42 +592,40 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
'id': _uuid(),
'network_id': ex_net_id,
'mac_address': 'ca:fe:de:ad:be:ef'}
ex_gw_port_no_sub = {'fixed_ips': [],
'id': _uuid(),
'network_id': ex_net_id,
'mac_address': 'ca:fe:de:ad:be:ef'}
interface_name = ri.get_external_device_name(ex_gw_port['id'])
if action == 'add':
self.device_exists.return_value = False
fake_fip = {'floatingips': [{'id': _uuid(),
'floating_ip_address': '192.168.1.34',
'fixed_ip_address': '192.168.0.1',
'port_id': _uuid()}]}
router[l3_constants.FLOATINGIP_KEY] = fake_fip['floatingips']
ri.external_gateway_added(ex_gw_port, interface_name)
if not router.get('distributed'):
self.assertEqual(self.mock_driver.plug.call_count, 1)
self.assertEqual(self.mock_driver.init_l3.call_count, 1)
exp_arp_calls = [mock.call(ri.ns_name, interface_name,
'20.0.0.30', mock.ANY)]
if dual_stack:
exp_arp_calls += [mock.call(ri.ns_name, interface_name,
'2001:192:168:100::2',
mock.ANY)]
self.send_arp.assert_has_calls(exp_arp_calls)
ip_cidrs = ['20.0.0.30/24']
gateway_ips = ['20.0.0.1']
if dual_stack:
ip_cidrs.append('2001:192:168:100::2/64')
gateway_ips.append('2001:192:168:100::1')
kwargs = {'preserve_ips': ['192.168.1.34/32'],
'gateway_ips': gateway_ips,
'namespace': 'qrouter-' + router['id'],
'extra_subnets': [{'cidr': '172.16.0.0/24'}]}
self.mock_driver.init_l3.assert_called_with(interface_name,
ip_cidrs,
**kwargs)
self._add_external_gateway(ri, router, ex_gw_port, interface_name,
use_fake_fip=True,
dual_stack=dual_stack)
elif action == 'add_no_sub':
ri.use_ipv6 = True
self._add_external_gateway(ri, router, ex_gw_port_no_sub,
interface_name, enable_ra_on_gw=True,
no_subnet=True)
elif action == 'add_no_sub_v6_gw':
ri.use_ipv6 = True
self.conf.set_override('ipv6_gateway',
'fe80::f816:3eff:fe2e:1')
if dual_stack:
use_fake_fip = True
# Remove v6 entries
del ex_gw_port['fixed_ips'][-1]
del ex_gw_port['subnets'][-1]
else:
ri._create_dvr_gateway.assert_called_once_with(
ex_gw_port, interface_name,
self.snat_ports)
use_fake_fip = False
ex_gw_port = ex_gw_port_no_sub
self._add_external_gateway(ri, router, ex_gw_port,
interface_name, no_subnet=True,
no_sub_gw='fe80::f816:3eff:fe2e:1',
use_fake_fip=use_fake_fip,
dual_stack=dual_stack)
elif action == 'remove':
self.device_exists.return_value = True
@ -616,6 +676,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
def _test_external_gateway_updated(self, dual_stack=False):
router = prepare_router_data(num_internal_ports=2)
ri = l3router.RouterInfo(router['id'], router, **self.ri_kwargs)
ri.use_ipv6 = False
interface_name, ex_gw_port = self._prepare_ext_gw_test(
ri, dual_stack=dual_stack)
@ -630,6 +691,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
exp_arp_calls = [mock.call(ri.ns_name, interface_name,
'20.0.0.30', mock.ANY)]
if dual_stack:
ri.use_ipv6 = True
exp_arp_calls += [mock.call(ri.ns_name, interface_name,
'2001:192:168:100::2', mock.ANY)]
self.send_arp.assert_has_calls(exp_arp_calls)
@ -641,7 +703,8 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
kwargs = {'preserve_ips': ['192.168.1.34/32'],
'gateway_ips': gateway_ips,
'namespace': 'qrouter-' + router['id'],
'extra_subnets': [{'cidr': '172.16.0.0/24'}]}
'extra_subnets': [{'cidr': '172.16.0.0/24'}],
'enable_ra_on_gw': False}
self.mock_driver.init_l3.assert_called_with(interface_name,
ip_cidrs,
**kwargs)
@ -707,6 +770,22 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
router['gw_port_host'] = HOSTNAME
self._test_external_gateway_action('add', router, dual_stack=True)
def test_agent_add_external_gateway_no_subnet(self):
router = prepare_router_data(num_internal_ports=2,
v6_ext_gw_with_sub=False)
self._test_external_gateway_action('add_no_sub', router)
def test_agent_add_external_gateway_no_subnet_with_ipv6_gw(self):
router = prepare_router_data(num_internal_ports=2,
v6_ext_gw_with_sub=False)
self._test_external_gateway_action('add_no_sub_v6_gw', router)
def test_agent_add_external_gateway_dual_stack_no_subnet_w_ipv6_gw(self):
router = prepare_router_data(num_internal_ports=2,
v6_ext_gw_with_sub=False)
self._test_external_gateway_action('add_no_sub_v6_gw',
router, dual_stack=True)
def test_agent_remove_external_gateway(self):
router = prepare_router_data(num_internal_ports=2)
self._test_external_gateway_action('remove', router)

View File

@ -1342,13 +1342,22 @@ class L3NatTestCaseBase(L3NatTestCaseMixin):
s['subnet']['network_id'],
expected_code=exc.HTTPBadRequest.code)
def test_router_add_gateway_no_subnet_returns_400(self):
def test_router_add_gateway_no_subnet(self):
with self.router() as r:
with self.network() as n:
self._set_net_external(n['network']['id'])
self._add_external_gateway_to_router(
r['router']['id'],
n['network']['id'], expected_code=exc.HTTPBadRequest.code)
n['network']['id'])
body = self._show('routers', r['router']['id'])
net_id = body['router']['external_gateway_info']['network_id']
self.assertEqual(net_id, n['network']['id'])
self._remove_external_gateway_from_router(
r['router']['id'],
n['network']['id'])
body = self._show('routers', r['router']['id'])
gw_info = body['router']['external_gateway_info']
self.assertIsNone(gw_info)
def test_router_remove_interface_inuse_returns_409(self):
with self.router() as r: