Parse the output of ip route more robustly

The code parsing the output of ip route assumes that the output is the
destination network, and then key/value pairs. This is not necessarily
the case, as the output may contain flags.

This change modifies the parsing of ip route's output to look
specifically for the keys of interest, and read their values
exclusively.

Change-Id: I34b6c0afb05550c970b1cacda8ec472294215403
Closes-Bug: 1640008
This commit is contained in:
Omer Anson 2016-11-08 18:08:50 +02:00 committed by Brian Haley
parent f51c872a12
commit 7647e3e56f
2 changed files with 47 additions and 18 deletions

View File

@ -961,6 +961,41 @@ def get_device_mac(device_name, namespace=None):
return IPDevice(device_name, namespace=namespace).link.address
_IP_ROUTE_PARSE_KEYS = {
'via': 'nexthop',
'dev': 'device',
'scope': 'scope'
}
def _parse_ip_route_line(line):
"""Parse a line output from ip route.
Example for output from 'ip route':
default via 192.168.3.120 dev wlp3s0 proto static metric 1024
10.0.0.0/8 dev tun0 proto static scope link metric 1024
10.0.1.0/8 dev tun1 proto static scope link metric 1024 linkdown
The first column is the destination, followed by key/value pairs and flags.
@param line A line output from ip route
@return: a dictionary representing a route.
"""
line = line.split()
result = {
'destination': line[0],
'nexthop': None,
'device': None,
'scope': None
}
idx = 1
while idx < len(line):
field = _IP_ROUTE_PARSE_KEYS.get(line[idx])
if not field:
idx = idx + 1
else:
result[field] = line[idx + 1]
idx = idx + 2
return result
def get_routing_table(ip_version, namespace=None):
"""Return a list of dictionaries, each representing a route.
@ -978,24 +1013,8 @@ def get_routing_table(ip_version, namespace=None):
['ip', '-%s' % ip_version, 'route'],
check_exit_code=True)
routes = []
# Example for route_lines:
# default via 192.168.3.120 dev wlp3s0 proto static metric 1024
# 10.0.0.0/8 dev tun0 proto static scope link metric 1024
# The first column is the destination, followed by key/value pairs.
# The generator splits the routing table by newline, then strips and splits
# each individual line.
route_lines = (line.split() for line in table.split('\n') if line.strip())
for route in route_lines:
network = route[0]
# Create a dict of key/value pairs (For example - 'dev': 'tun0')
# excluding the first column.
data = dict(route[i:i + 2] for i in range(1, len(route), 2))
routes.append({'destination': network,
'nexthop': data.get('via'),
'device': data.get('dev'),
'scope': data.get('scope')})
return routes
return [_parse_ip_route_line(line)
for line in table.split('\n') if line.strip()]
def ensure_device_is_ready(device_name, namespace=None):

View File

@ -1320,6 +1320,7 @@ class TestGetRoutingTable(base.BaseTestCase):
ip_route_output = ("""
default via 192.168.3.120 dev wlp3s0 proto static metric 1024
10.0.0.0/8 dev tun0 proto static scope link metric 1024
10.0.1.0/8 dev tun1 proto static scope link metric 1024 linkdown
""")
expected = [{'destination': 'default',
'nexthop': '192.168.3.120',
@ -1328,18 +1329,27 @@ default via 192.168.3.120 dev wlp3s0 proto static metric 1024
{'destination': '10.0.0.0/8',
'nexthop': None,
'device': 'tun0',
'scope': 'link'},
{'destination': '10.0.1.0/8',
'nexthop': None,
'device': 'tun1',
'scope': 'link'}]
self._test_get_routing_table(4, ip_route_output, expected)
def test_get_routing_table_6(self):
ip_route_output = ("""
2001:db8:0:f101::/64 dev tap-1 proto kernel metric 256 pref medium
2001:db8:0:f102::/64 dev tap-2 proto kernel metric 256 pref medium linkdown
default via 2001:db8:0:f101::4 dev tap-1 metric 1024 pref medium
""")
expected = [{'destination': '2001:db8:0:f101::/64',
'nexthop': None,
'device': 'tap-1',
'scope': None},
{'destination': '2001:db8:0:f102::/64',
'nexthop': None,
'device': 'tap-2',
'scope': None},
{'destination': 'default',
'nexthop': '2001:db8:0:f101::4',
'device': 'tap-1',