L3 router support ECMP

This patch changes the policy for updating routes to support ECMP,
and will now add ECMP routes to the Neutron router namespace when
there are multiple routes pointing to the same destination address.

Change-Id: I842c1408ee0235bc54441e9ed69c8b87ea30651b
Related-Bug: #1880532
This commit is contained in:
XiaoYu Zhu 2020-07-28 17:34:27 +08:00
parent 93ff5afdbf
commit 9b2983743b
5 changed files with 227 additions and 10 deletions

View File

@ -187,20 +187,71 @@ class RouterInfo(BaseRouterInfo):
def update_routing_table(self, operation, route):
self._update_routing_table(operation, route, self.ns_name)
def update_routing_table_ecmp(self, route_list):
multipath = [dict(via=route['nexthop'])
for route in route_list]
try:
ip_lib.add_ip_route(self.ns_name, route_list[0]['destination'],
via=multipath)
except (RuntimeError, OSError, pyroute2_exc.NetlinkError):
pass
def check_and_remove_ecmp_route(self, old_routes, remove_route):
route_list = []
for route in old_routes:
if route['destination'] == remove_route['destination']:
route_list.append(route)
# An ECMP route is composed of multiple routes with the same
# destination address, and two scenarios should be considered
# when removing a nexthop address from an ECMP route.
# a. The original ECMP route has only two nexthops, deleting
# one of them will make it a normal route.
# b. The original ECMP route has more than two nexthops,
# delete one of the nexthops, it is still an ECMP route.
if len(route_list) == 2:
for r in route_list:
if r['nexthop'] != remove_route['nexthop']:
self.update_routing_table('replace', r)
return True
if len(route_list) > 2:
route_list.remove(remove_route)
self.update_routing_table_ecmp(route_list)
return True
return False
def check_and_add_ecmp_route(self, old_routes, new_route):
route_list = []
for route in old_routes:
if route['destination'] == new_route['destination']:
route_list.append(route)
if route_list:
route_list.append(new_route)
self.update_routing_table_ecmp(route_list)
return True
return False
def routes_updated(self, old_routes, new_routes):
adds, removes = helpers.diff_list_of_dict(old_routes,
new_routes)
for route in adds:
LOG.debug("Added route entry is '%s'", route)
# remove replaced route from deleted route
for del_route in removes:
if route['destination'] == del_route['destination']:
removes.remove(del_route)
# replace success even if there is no existing route
self.update_routing_table('replace', route)
for route in removes:
LOG.debug("Removed route entry is '%s'", route)
self.update_routing_table('delete', route)
# Judge if modifying an ECMP route or not, if not,
# just delete it, if it is, replace it
# update old_routes after modify
if not self.check_and_remove_ecmp_route(old_routes, route):
LOG.debug("Removed route entry is '%s'", route)
self.update_routing_table('delete', route)
old_routes.remove(route)
for route in adds:
if not self.check_and_add_ecmp_route(old_routes, route):
LOG.debug("Added route entry is '%s'", route)
# replace success even if there is no existing route
self.update_routing_table('replace', route)
old_routes.append(route)
def get_floating_ips(self):
"""Filter Floating IPs to be hosted on this agent."""

View File

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.api.definitions import ecmp_routes as apidef
from neutron_lib.api import extensions
class Ecmp_routes(extensions.APIExtensionDescriptor):
"""Extension class supporting configuration of ECMP routes."""
api_definition = apidef

View File

@ -2185,3 +2185,74 @@ class TestDvrRouter(DvrRouterTestFramework, framework.L3AgentTestFramework):
in fip_agent_gw_port['extra_subnets'])
routes_cidr = set(route['cidr'] for route in routes)
self.assertEqual(extra_subnet_cidr, routes_cidr)
def test_dvr_router_update_ecmp_routes(self):
self.agent.conf.agent_mode = 'dvr'
router_info = self.generate_dvr_router_info()
print(router_info)
router1 = self.manage_router(self.agent, router_info)
router1.router['routes'] = [{'destination': '20.0.10.10/32',
'nexthop': '35.4.0.11'},
{'destination': '20.0.10.10/32',
'nexthop': '35.4.0.22'},
{'destination': '20.0.10.10/32',
'nexthop': '35.4.0.33'}]
self.agent._process_updated_router(router1.router)
updated_route = ip_lib.list_ip_routes(
router1.ns_name,
ip_version=lib_constants.IP_VERSION_4,)
expected_route = [{'cidr': '20.0.10.10/32',
'table': 'main',
'via': [{'via': '35.4.0.11'},
{'via': '35.4.0.22'},
{'via': '35.4.0.33'}]
}]
actual_routes = [{key: route[key] for key in expected_route[0].keys()}
for route in updated_route]
for entry in actual_routes:
if entry['via']:
if isinstance(entry['via'], (list, tuple)):
via_list = [{'via': hop['via']}
for hop in entry['via']]
entry['via'] = sorted(via_list, key=lambda i: i['via'])
self.assertIn(expected_route[0], actual_routes)
# delete one route
router1.router['routes'] = [{'destination': '20.0.10.10/32',
'nexthop': '35.4.0.11'},
{'destination': '20.0.10.10/32',
'nexthop': '35.4.0.22'}]
self.agent._process_updated_router(router1.router)
updated_route = ip_lib.list_ip_routes(
router1.ns_name,
ip_version=lib_constants.IP_VERSION_4, )
expected_route = [{'cidr': '20.0.10.10/32',
'table': 'main',
'via': [{'via': '35.4.0.11'},
{'via': '35.4.0.22'}]
}]
actual_routes = [{key: route[key] for key in expected_route[0].keys()}
for route in updated_route]
for entry in actual_routes:
if entry['via']:
if isinstance(entry['via'], (list, tuple)):
via_list = [{'via': hop['via']}
for hop in entry['via']]
entry['via'] = sorted(via_list, key=lambda i: i['via'])
self.assertIn(expected_route[0], actual_routes)
# delete one route again
router1.router['routes'] = [{'destination': '20.0.10.10/32',
'nexthop': '35.4.0.11'}]
self.agent._process_updated_router(router1.router)
updated_route = ip_lib.list_ip_routes(
router1.ns_name,
ip_version=lib_constants.IP_VERSION_4, )
expected_route = [{'cidr': '20.0.10.10/32',
'table': 'main',
'via': '35.4.0.11'
}]
actual_routes = [{key: route[key] for key in expected_route[0].keys()}
for route in updated_route]
self.assertIn(expected_route[0], actual_routes)

View File

@ -52,6 +52,74 @@ class TestRouterInfo(base.BaseTestCase):
self.mock_delete_ip_route)
mock_method.assert_has_calls(mock_calls, any_order=True)
def _check_ip_wrapper_method_called(self, calls):
self.mock_ip.netns.execute.assert_has_calls(
[mock.call(call, check_exit_code=False) for call in calls],
any_order=True)
def test_update_routing_table_ecmp(self):
ri = router_info.RouterInfo(mock.Mock(), _uuid(), {}, **self.ri_kwargs)
ri.router = {}
fake_route_list = [{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.112'},
{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.113'}]
expected_dst = '135.207.111.111/32'
expected_next_hops = [{'via': '135.207.111.112'},
{'via': '135.207.111.113'}]
ri.update_routing_table_ecmp(fake_route_list)
self.mock_add_ip_route.assert_called_once_with(
ri.ns_name,
expected_dst,
via=expected_next_hops)
def test_check_and_remove_ecmp_route(self):
ri = router_info.RouterInfo(mock.Mock(), _uuid(), {}, **self.ri_kwargs)
ri.router = {}
fake_old_routes1 = [{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.112'},
{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.113'}]
fake_route1 = {'destination': '135.207.111.111/32',
'nexthop': '135.207.111.113'}
fake_old_routes2 = [{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.112'},
{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.113'},
{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.114'}]
fake_route2 = [{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.113'},
{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.114'}]
fake_remove_route = {'destination': '135.207.111.111/32',
'nexthop': '135.207.111.112'}
ri.update_routing_table = mock.Mock()
ri.check_and_remove_ecmp_route(fake_old_routes1, fake_remove_route)
ri.update_routing_table.assert_called_once_with('replace',
fake_route1)
ri.update_routing_table_ecmp = mock.Mock()
ri.check_and_remove_ecmp_route(fake_old_routes2, fake_remove_route)
ri.update_routing_table_ecmp.assert_called_once_with(fake_route2)
def test_check_and_add_ecmp_route(self):
ri = router_info.RouterInfo(mock.Mock(), _uuid(), {}, **self.ri_kwargs)
ri.router = {}
fake_old_routes = [{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.112'}]
fake_new_route = {'destination': '135.207.111.111/32',
'nexthop': '135.207.111.113'}
ri.update_routing_table_ecmp = mock.Mock()
ri.check_and_add_ecmp_route(fake_old_routes, fake_new_route)
expected_routes = [{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.112'},
{'destination': '135.207.111.111/32',
'nexthop': '135.207.111.113'}]
ri.update_routing_table_ecmp.assert_called_once_with(expected_routes)
def test_routing_table_update(self):
ri = router_info.RouterInfo(mock.Mock(), _uuid(), {}, **self.ri_kwargs)
ri.router = {}

View File

@ -0,0 +1,7 @@
---
features:
- |
Neutron supports ECMP routes now, with this change, neutron will
consolidate multiple routes with the same destination address into
a single ECMP route. For more information see bug
`1880532 <https://bugs.launchpad.net/neutron/+bug/1880532>`_.