From: Carl Baldwin Date: Thu, 24 Apr 2014 23:06:10 +0000 (+0000) Subject: Set onlink routes for all subnets on an external network X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=fc7cffedbe60;p=openstack-build%2Fneutron-build.git Set onlink routes for all subnets on an external network The addition of the on-link routes gives us some freedom to allocate a router's IP address from any one of multiple subnets on one external network. Different routers can get their IPs from different subnets and they still have direct on-link connectivity to each other. For example, one router with its primary IP from 10.0.0.0/24 and another from 192.168.0.0/24 can communicate directly. It is important that each router has on-link routes to *all* of the subnets. Any router can host floating ips from any of the subnets regardless of which subnet the primary IP address comes from. This is an alternative to the "Multiple floating IP pools" section in the administration guide. It is a simpler alternative that avoids having to create multiple external networks. It is also more flexible because routers will no longer be restricted to getting floating IPs from the pool to which they happen to be connected. DocImpact Document the procedure for adding subnets to the external network. Potentially remove the existing procedure for "Multiple floating IP pools" from the docs. Change-Id: I2c283f5be0cbb6b5d350cafc1b636c300b796a7b Closes-Bug: #1312467 --- diff --git a/neutron/agent/l3_agent.py b/neutron/agent/l3_agent.py index aa185e430..d81deea83 100644 --- a/neutron/agent/l3_agent.py +++ b/neutron/agent/l3_agent.py @@ -458,7 +458,7 @@ class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback, manager.Manager): interface_name = None if ex_gw_port_id: interface_name = self.get_external_device_name(ex_gw_port_id) - if ex_gw_port and not ri.ex_gw_port: + if ex_gw_port and ex_gw_port != ri.ex_gw_port: self._set_subnet_info(ex_gw_port) self.external_gateway_added(ri, ex_gw_port, interface_name, internal_cidrs) @@ -646,6 +646,7 @@ class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback, manager.Manager): self.driver.init_l3(interface_name, [ex_gw_port['ip_cidr']], namespace=ri.ns_name, gateway=ex_gw_port['subnet'].get('gateway_ip'), + extra_subnets=ex_gw_port.get('extra_subnets', []), preserve_ips=preserve_ips) ip_address = ex_gw_port['ip_cidr'].split('/')[0] self._send_gratuitous_arp_packet(ri, interface_name, ip_address) diff --git a/neutron/agent/linux/interface.py b/neutron/agent/linux/interface.py index 84c0f3bb9..a31250ee7 100644 --- a/neutron/agent/linux/interface.py +++ b/neutron/agent/linux/interface.py @@ -72,7 +72,7 @@ class LinuxInterfaceDriver(object): self.root_helper = config.get_root_helper(conf) def init_l3(self, device_name, ip_cidrs, namespace=None, - preserve_ips=[], gateway=None): + preserve_ips=[], gateway=None, extra_subnets=[]): """Set the L3 settings for the interface using data from the port. ip_cidrs: list of 'X.X.X.X/YY' strings @@ -108,6 +108,13 @@ class LinuxInterfaceDriver(object): if gateway: device.route.add_gateway(gateway) + new_onlink_routes = set(s['cidr'] for s in extra_subnets) + existing_onlink_routes = set(device.route.list_onlink_routes()) + for route in new_onlink_routes - existing_onlink_routes: + device.route.add_onlink_route(route) + for route in existing_onlink_routes - new_onlink_routes: + device.route.delete_onlink_route(route) + def check_bridge_exists(self, bridge): if not ip_lib.device_exists(bridge): raise exceptions.BridgeDoesNotExist(bridge=bridge) diff --git a/neutron/agent/linux/ip_lib.py b/neutron/agent/linux/ip_lib.py index 40acb4c18..bc32139f2 100644 --- a/neutron/agent/linux/ip_lib.py +++ b/neutron/agent/linux/ip_lib.py @@ -376,6 +376,22 @@ class IpRouteCommand(IpDeviceCommandBase): 'dev', self.name) + def list_onlink_routes(self): + def iterate_routes(): + output = self._run('list', 'dev', self.name, 'scope', 'link') + for line in output.split('\n'): + line = line.strip() + if line and not line.count('src'): + yield line + + return [x for x in iterate_routes()] + + def add_onlink_route(self, cidr): + self._as_root('replace', cidr, 'dev', self.name, 'scope', 'link') + + def delete_onlink_route(self, cidr): + self._as_root('del', cidr, 'dev', self.name, 'scope', 'link') + def get_gateway(self, scope=None, filters=None): if filters is None: filters = [] diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index beebaee87..9eea54fce 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -924,36 +924,41 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): """ if not ports: return - subnet_id_ports_dict = {} - for port in ports: - fixed_ips = port.get('fixed_ips', []) - if len(fixed_ips) > 1: - LOG.info(_("Ignoring multiple IPs on router port %s"), - port['id']) - continue - elif not fixed_ips: - # Skip ports without IPs, which can occur if a subnet - # attached to a router is deleted - LOG.info(_("Skipping port %s as no IP is configure on it"), - port['id']) - continue - fixed_ip = fixed_ips[0] - my_ports = subnet_id_ports_dict.get(fixed_ip['subnet_id'], []) - my_ports.append(port) - subnet_id_ports_dict[fixed_ip['subnet_id']] = my_ports - if not subnet_id_ports_dict: - return - filters = {'id': subnet_id_ports_dict.keys()} - fields = ['id', 'cidr', 'gateway_ip'] - subnet_dicts = self._core_plugin.get_subnets(context, filters, fields) - for subnet_dict in subnet_dicts: - ports = subnet_id_ports_dict.get(subnet_dict['id'], []) + + def each_port_with_ip(): for port in ports: - # TODO(gongysh) stash the subnet into fixed_ips - # to make the payload smaller. - port['subnet'] = {'id': subnet_dict['id'], - 'cidr': subnet_dict['cidr'], - 'gateway_ip': subnet_dict['gateway_ip']} + fixed_ips = port.get('fixed_ips', []) + if len(fixed_ips) > 1: + LOG.info(_("Ignoring multiple IPs on router port %s"), + port['id']) + continue + elif not fixed_ips: + # Skip ports without IPs, which can occur if a subnet + # attached to a router is deleted + LOG.info(_("Skipping port %s as no IP is configure on it"), + port['id']) + continue + yield (port, fixed_ips[0]) + + network_ids = set(p['network_id'] for p, _ in each_port_with_ip()) + filters = {'network_id': [id for id in network_ids]} + fields = ['id', 'cidr', 'gateway_ip', 'network_id'] + + subnets_by_network = dict((id, []) for id in network_ids) + for subnet in self._core_plugin.get_subnets(context, filters, fields): + subnets_by_network[subnet['network_id']].append(subnet) + + for port, fixed_ip in each_port_with_ip(): + port['extra_subnets'] = [] + for subnet in subnets_by_network[port['network_id']]: + subnet_info = {'id': subnet['id'], + 'cidr': subnet['cidr'], + 'gateway_ip': subnet['gateway_ip']} + + if subnet['id'] == fixed_ip['subnet_id']: + port['subnet'] = subnet_info + else: + port['extra_subnets'].append(subnet_info) def _process_sync_data(self, routers, interfaces, floating_ips): routers_dict = {} diff --git a/neutron/tests/unit/test_l3_agent.py b/neutron/tests/unit/test_l3_agent.py index 1b3f231ed..222290d62 100644 --- a/neutron/tests/unit/test_l3_agent.py +++ b/neutron/tests/unit/test_l3_agent.py @@ -160,6 +160,7 @@ class TestBasicRouterOperations(base.BaseTestCase): ex_gw_port = {'fixed_ips': [{'ip_address': '20.0.0.30', 'subnet_id': _uuid()}], 'subnet': {'gateway_ip': '20.0.0.1'}, + 'extra_subnets': [{'cidr': '172.16.0.0/24'}], 'id': _uuid(), 'network_id': _uuid(), 'mac_address': 'ca:fe:de:ad:be:ef', @@ -179,7 +180,8 @@ class TestBasicRouterOperations(base.BaseTestCase): '20.0.0.30') kwargs = {'preserve_ips': ['192.168.1.34/32'], 'namespace': 'qrouter-' + router_id, - 'gateway': '20.0.0.1'} + 'gateway': '20.0.0.1', + 'extra_subnets': [{'cidr': '172.16.0.0/24'}]} self.mock_driver.init_l3.assert_called_with(interface_name, ['20.0.0.30/24'], **kwargs) diff --git a/neutron/tests/unit/test_linux_interface.py b/neutron/tests/unit/test_linux_interface.py index 010409731..749f786b4 100644 --- a/neutron/tests/unit/test_linux_interface.py +++ b/neutron/tests/unit/test_linux_interface.py @@ -81,15 +81,32 @@ class TestABCDriver(TestBase): addresses = [dict(ip_version=4, scope='global', dynamic=False, cidr='172.16.77.240/24')] self.ip_dev().addr.list = mock.Mock(return_value=addresses) + self.ip_dev().route.list_onlink_routes.return_value = [] bc = BaseChild(self.conf) ns = '12345678-1234-5678-90ab-ba0987654321' - bc.init_l3('tap0', ['192.168.1.2/24'], namespace=ns) + bc.init_l3('tap0', ['192.168.1.2/24'], namespace=ns, + extra_subnets=[{'cidr': '172.20.0.0/24'}]) self.ip_dev.assert_has_calls( [mock.call('tap0', 'sudo', namespace=ns), mock.call().addr.list(scope='global', filters=['permanent']), mock.call().addr.add(4, '192.168.1.2/24', '192.168.1.255'), - mock.call().addr.delete(4, '172.16.77.240/24')]) + mock.call().addr.delete(4, '172.16.77.240/24'), + mock.call().route.list_onlink_routes(), + mock.call().route.add_onlink_route('172.20.0.0/24')]) + + def test_l3_init_delete_onlink_routes(self): + addresses = [dict(ip_version=4, scope='global', + dynamic=False, cidr='172.16.77.240/24')] + self.ip_dev().addr.list = mock.Mock(return_value=addresses) + self.ip_dev().route.list_onlink_routes.return_value = ['172.20.0.0/24'] + + bc = BaseChild(self.conf) + ns = '12345678-1234-5678-90ab-ba0987654321' + bc.init_l3('tap0', ['192.168.1.2/24'], namespace=ns) + self.ip_dev.assert_has_calls( + [mock.call().route.list_onlink_routes(), + mock.call().route.delete_onlink_route('172.20.0.0/24')]) def test_l3_init_with_preserve(self): addresses = [dict(ip_version=4, scope='global',