]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Support IPv6 Router
authorAbishek Subramanian <absubram@cisco.com>
Mon, 30 Mar 2015 17:24:09 +0000 (13:24 -0400)
committerAbishek Subramanian <absubram@cisco.com>
Mon, 30 Mar 2015 17:32:46 +0000 (13:32 -0400)
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

etc/l3_agent.ini
neutron/agent/l3/agent.py
neutron/agent/l3/config.py
neutron/agent/l3/router_info.py
neutron/agent/linux/interface.py
neutron/db/l3_db.py
neutron/tests/functional/agent/test_l3_agent.py
neutron/tests/unit/test_l3_agent.py
neutron/tests/unit/test_l3_plugin.py

index af6740d7930655a4ea7e80538863cfe191f43d9f..2d8661ee05a19e3da6d1979967d4044ba8a389e3 100644 (file)
 # 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
index 6797ef2704a3b6f71ee0ae2ed838fe575d6faf11..fc06fd440a2dd96d5d15eef6649e5dd68959bdf5 100644 (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:
index 49e5dd51614b5858a10fede54e53e5a62673ed78..a302af34886fa889d3baa1bd667e50eac9dc2603 100644 (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,
index 813ff6a52101de3ca89aa9c1a2264c27ccb9421e..eddc6f6f6f2d254c70556ca5251e81d336940a40 100644 (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(
index e531c8203652c114336b19d6510a4bd215c0b8be..99654cfc8f47d2b77f112f2268a3db5afbf1fd96 100644 (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):
index 6464bc6e9711e1c545fb4e6c0df2cf39bd18f6be..0873264f25f44825bb5601d6f620299dadab1bd1 100644 (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(),
index 9671a17debd9aa1084c70f86288c43ea9db1a5be..2f19db3b1cfdc0a1f3e9558546078fce1e031422 100644 (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):
index 6e233845ae015f195ef6ca3b4f8be258f07f545d..96e3e622c976235c524b6dfaee844f9d72b5f33f 100644 (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)
index c91944932663e4c2d2dd8996d04620b7e229bf45..9e067ac00002cf7883e35d94e5e66cf85cf7171c 100644 (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: