From 07077bebb69da29994257d061d3a8d7ea9598c3d Mon Sep 17 00:00:00 2001 From: Abishek Subramanian Date: Mon, 30 Mar 2015 13:24:09 -0400 Subject: [PATCH] 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 --- etc/l3_agent.ini | 14 ++ neutron/agent/l3/agent.py | 14 ++ neutron/agent/l3/config.py | 17 ++ neutron/agent/l3/router_info.py | 29 +++- neutron/agent/linux/interface.py | 14 +- neutron/db/l3_db.py | 7 +- .../tests/functional/agent/test_l3_agent.py | 40 +++-- neutron/tests/unit/test_l3_agent.py | 149 ++++++++++++++---- neutron/tests/unit/test_l3_plugin.py | 13 +- 9 files changed, 239 insertions(+), 58 deletions(-) diff --git a/etc/l3_agent.ini b/etc/l3_agent.ini index af6740d7930..2d8661ee05a 100644 --- a/etc/l3_agent.ini +++ b/etc/l3_agent.ini @@ -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 diff --git a/neutron/agent/l3/agent.py b/neutron/agent/l3/agent.py index 6797ef2704a..fc06fd440a2 100644 --- a/neutron/agent/l3/agent.py +++ b/neutron/agent/l3/agent.py @@ -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: diff --git a/neutron/agent/l3/config.py b/neutron/agent/l3/config.py index 49e5dd51614..a302af34886 100644 --- a/neutron/agent/l3/config.py +++ b/neutron/agent/l3/config.py @@ -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, diff --git a/neutron/agent/l3/router_info.py b/neutron/agent/l3/router_info.py index 813ff6a5210..eddc6f6f6f2 100644 --- a/neutron/agent/l3/router_info.py +++ b/neutron/agent/l3/router_info.py @@ -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( diff --git a/neutron/agent/linux/interface.py b/neutron/agent/linux/interface.py index e531c820365..99654cfc8f4 100644 --- a/neutron/agent/linux/interface.py +++ b/neutron/agent/linux/interface.py @@ -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): diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index 6464bc6e971..0873264f25f 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -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(), diff --git a/neutron/tests/functional/agent/test_l3_agent.py b/neutron/tests/functional/agent/test_l3_agent.py index 9671a17debd..2f19db3b1cf 100644 --- a/neutron/tests/functional/agent/test_l3_agent.py +++ b/neutron/tests/functional/agent/test_l3_agent.py @@ -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): diff --git a/neutron/tests/unit/test_l3_agent.py b/neutron/tests/unit/test_l3_agent.py index 6e233845ae0..96e3e622c97 100644 --- a/neutron/tests/unit/test_l3_agent.py +++ b/neutron/tests/unit/test_l3_agent.py @@ -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) diff --git a/neutron/tests/unit/test_l3_plugin.py b/neutron/tests/unit/test_l3_plugin.py index c9194493266..9e067ac0000 100644 --- a/neutron/tests/unit/test_l3_plugin.py +++ b/neutron/tests/unit/test_l3_plugin.py @@ -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: