# 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
#
import eventlet
+import netaddr
from oslo_config import cfg
from oslo_log import log as logging
import oslo_messaging
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:
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,
# 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(
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)
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) +
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):
'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(),
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'])
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)
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:
# 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)
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):
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):
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'
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()
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',
'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',
'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
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)
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)
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)
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)
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: