• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import collections
6import logging
7import os
8import re
9
10from autotest_lib.client.bin import local_host
11from autotest_lib.client.bin import utils
12from autotest_lib.client.common_lib import error
13from autotest_lib.client.common_lib.cros.network import netblock
14
15# A tuple consisting of a readable part number (one of NAME_* below)
16# and a kernel module that provides the driver for this part (e.g. ath9k).
17DeviceDescription = collections.namedtuple('DeviceDescription',
18                                           ['name', 'kernel_module'])
19
20
21# A tuple describing a default route, consisting of an interface name,
22# gateway IP address, and the metric value visible in the routing table.
23DefaultRoute = collections.namedtuple('DefaultRoute', ['interface_name',
24                                                       'gateway',
25                                                       'metric'])
26
27NAME_MARVELL_88W8797_SDIO = 'Marvell 88W8797 SDIO'
28NAME_MARVELL_88W8887_SDIO = 'Marvell 88W8887 SDIO'
29NAME_MARVELL_88W8897_SDIO = 'Marvell 88W8897 SDIO'
30NAME_MARVELL_88W8897_PCIE = 'Marvell 88W8897 PCIE'
31NAME_MARVELL_88W8997_PCIE = 'Marvell 88W8997 PCIE'
32NAME_ATHEROS_AR9280 = 'Atheros AR9280'
33NAME_ATHEROS_AR9382 = 'Atheros AR9382'
34NAME_ATHEROS_AR9462 = 'Atheros AR9462'
35NAME_QUALCOMM_ATHEROS_QCA6174 = 'Qualcomm Atheros QCA6174'
36NAME_QUALCOMM_ATHEROS_QCA6174_SDIO = 'Qualcomm Atheros QCA6174 SDIO'
37NAME_QUALCOMM_WCN3990 = 'Qualcomm WCN3990'
38NAME_INTEL_7260 = 'Intel 7260'
39NAME_INTEL_7265 = 'Intel 7265'
40NAME_INTEL_9000 = 'Intel 9000'
41NAME_INTEL_9260 = 'Intel 9260'
42NAME_INTEL_22260 = 'Intel 22260'
43NAME_INTEL_22560 = 'Intel 22560'
44NAME_BROADCOM_BCM4354_SDIO = 'Broadcom BCM4354 SDIO'
45NAME_BROADCOM_BCM4356_PCIE = 'Broadcom BCM4356 PCIE'
46NAME_BROADCOM_BCM4371_PCIE = 'Broadcom BCM4371 PCIE'
47NAME_REALTEK_8822C_PCIE = 'Realtek 8822C PCIE'
48NAME_UNKNOWN = 'Unknown WiFi Device'
49
50DEVICE_INFO_ROOT = '/sys/class/net'
51
52DeviceInfo = collections.namedtuple('DeviceInfo', ['vendor', 'device',
53                                                   'subsystem',
54                                                   'compatible'])
55# Provide default values for parameters.
56DeviceInfo.__new__.__defaults__ = (None, None, None, None)
57
58DEVICE_NAME_LOOKUP = {
59    DeviceInfo('0x02df', '0x9129'): NAME_MARVELL_88W8797_SDIO,
60    DeviceInfo('0x02df', '0x912d'): NAME_MARVELL_88W8897_SDIO,
61    DeviceInfo('0x02df', '0x9135'): NAME_MARVELL_88W8887_SDIO,
62    DeviceInfo('0x11ab', '0x2b38'): NAME_MARVELL_88W8897_PCIE,
63    DeviceInfo('0x1b4b', '0x2b42'): NAME_MARVELL_88W8997_PCIE,
64    DeviceInfo('0x168c', '0x002a'): NAME_ATHEROS_AR9280,
65    DeviceInfo('0x168c', '0x0030'): NAME_ATHEROS_AR9382,
66    DeviceInfo('0x168c', '0x0034'): NAME_ATHEROS_AR9462,
67    DeviceInfo('0x168c', '0x003e'): NAME_QUALCOMM_ATHEROS_QCA6174,
68    DeviceInfo('0x105b', '0xe09d'): NAME_QUALCOMM_ATHEROS_QCA6174,
69    DeviceInfo('0x0271', '0x050a'): NAME_QUALCOMM_ATHEROS_QCA6174_SDIO,
70    DeviceInfo('0x8086', '0x08b1'): NAME_INTEL_7260,
71    DeviceInfo('0x8086', '0x08b2'): NAME_INTEL_7260,
72    DeviceInfo('0x8086', '0x095a'): NAME_INTEL_7265,
73    DeviceInfo('0x8086', '0x095b'): NAME_INTEL_7265,
74    # Note that Intel 9000 is also Intel 9560 aka Jefferson Peak 2
75    DeviceInfo('0x8086', '0x9df0'): NAME_INTEL_9000,
76    DeviceInfo('0x8086', '0x31dc'): NAME_INTEL_9000,
77    DeviceInfo('0x8086', '0x2526'): NAME_INTEL_9260,
78    DeviceInfo('0x8086', '0x2723'): NAME_INTEL_22260,
79    # For integrated wifi chips, use device_id and subsystem_id together
80    # as an identifier.
81    # 0x02f0 is for Quasar on CML, 0x4070 and 0x0074 is for HrP2
82    DeviceInfo('0x8086', '0x02f0', subsystem='0x4070'): NAME_INTEL_22560,
83    DeviceInfo('0x8086', '0x02f0', subsystem='0x0074'): NAME_INTEL_22560,
84    # With the same Quasar, subsystem_id 0x0034 is JfP2
85    DeviceInfo('0x8086', '0x02f0', subsystem='0x0034'): NAME_INTEL_9000,
86    DeviceInfo('0x02d0', '0x4354'): NAME_BROADCOM_BCM4354_SDIO,
87    DeviceInfo('0x14e4', '0x43ec'): NAME_BROADCOM_BCM4356_PCIE,
88    DeviceInfo('0x14e4', '0x440d'): NAME_BROADCOM_BCM4371_PCIE,
89    DeviceInfo('0x10ec', '0xc822'): NAME_REALTEK_8822C_PCIE,
90
91    DeviceInfo(compatible='qcom,wcn3990-wifi'): NAME_QUALCOMM_WCN3990,
92}
93
94class Interface:
95    """Interace is a class that contains the queriable address properties
96    of an network device.
97    """
98    ADDRESS_TYPE_MAC = 'link/ether'
99    ADDRESS_TYPE_IPV4 = 'inet'
100    ADDRESS_TYPE_IPV6 = 'inet6'
101    ADDRESS_TYPES = [ ADDRESS_TYPE_MAC, ADDRESS_TYPE_IPV4, ADDRESS_TYPE_IPV6 ]
102
103
104    @staticmethod
105    def get_connected_ethernet_interface(ignore_failures=False):
106        """Get an interface object representing a connected ethernet device.
107
108        Raises an exception if no such interface exists.
109
110        @param ignore_failures bool function will return None instead of raising
111                an exception on failures.
112        @return an Interface object except under the conditions described above.
113
114        """
115        # Assume that ethernet devices are called ethX until proven otherwise.
116        for device_name in ['eth%d' % i for i in range(5)]:
117            ethernet_if = Interface(device_name)
118            if ethernet_if.exists and ethernet_if.ipv4_address:
119                return ethernet_if
120
121        else:
122            if ignore_failures:
123                return None
124
125            raise error.TestFail('Failed to find ethernet interface.')
126
127
128    def __init__(self, name, host=None):
129        self._name = name
130        if host is None:
131            self.host = local_host.LocalHost()
132        else:
133            self.host = host
134        self._run = self.host.run
135
136
137    @property
138    def name(self):
139        """@return name of the interface (e.g. 'wlan0')."""
140        return self._name
141
142
143    @property
144    def addresses(self):
145        """@return the addresses (MAC, IP) associated with interface."""
146        # "ip addr show %s 2> /dev/null" returns something that looks like:
147        #
148        # 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast
149        #    link/ether ac:16:2d:07:51:0f brd ff:ff:ff:ff:ff:ff
150        #    inet 172.22.73.124/22 brd 172.22.75.255 scope global eth0
151        #    inet6 2620:0:1000:1b02:ae16:2dff:fe07:510f/64 scope global dynamic
152        #       valid_lft 2591982sec preferred_lft 604782sec
153        #    inet6 fe80::ae16:2dff:fe07:510f/64 scope link
154        #       valid_lft forever preferred_lft forever
155        #
156        # We extract the second column from any entry for which the first
157        # column is an address type we are interested in.  For example,
158        # for "inet 172.22.73.124/22 ...", we will capture "172.22.73.124/22".
159        result = self._run('ip addr show %s 2> /dev/null' % self._name,
160                           ignore_status=True)
161        address_info = result.stdout
162        if result.exit_status != 0:
163            # The "ip" command will return non-zero if the interface does
164            # not exist.
165            return {}
166
167        addresses = {}
168        for address_line in address_info.splitlines():
169            address_parts = address_line.lstrip().split()
170            if len(address_parts) < 2:
171                continue
172            address_type, address_value = address_parts[:2]
173            if address_type in self.ADDRESS_TYPES:
174                if address_type not in addresses:
175                    addresses[address_type] = []
176                addresses[address_type].append(address_value)
177        return addresses
178
179
180    @property
181    def device_path(self):
182        """@return the sysfs path of the interface device"""
183        # This assumes that our path separator is the same as the remote host.
184        device_path = os.path.join(DEVICE_INFO_ROOT, self._name, 'device')
185        if not self.host.path_exists(device_path):
186            logging.error('No device information found at %s', device_path)
187            return None
188
189        return device_path
190
191
192    @property
193    def wiphy_name(self):
194        """
195        @return name of the wiphy (e.g., 'phy0'), if available.
196        Otherwise None.
197        """
198        readlink_result = self._run('readlink "%s"' %
199                os.path.join(DEVICE_INFO_ROOT, self._name, 'phy80211'),
200                ignore_status=True)
201        if readlink_result.exit_status != 0:
202            return None
203
204        return os.path.basename(readlink_result.stdout.strip())
205
206
207    @property
208    def module_name(self):
209        """@return Name of kernel module in use by this interface."""
210        module_readlink_result = self._run('readlink "%s"' %
211                os.path.join(self.device_path, 'driver', 'module'),
212                ignore_status=True)
213        if module_readlink_result.exit_status != 0:
214            return None
215
216        return os.path.basename(module_readlink_result.stdout.strip())
217
218    @property
219    def parent_device_name(self):
220        """
221        @return Name of device at which wiphy device is present. For example,
222        for a wifi NIC present on a PCI bus, this would be the same as
223        PCI_SLOT_PATH. """
224        path_readlink_result = self._run('readlink "%s"' % self.device_path)
225        if path_readlink_result.exit_status != 0:
226            return None
227
228        return os.path.basename(path_readlink_result.stdout.strip())
229
230    def _get_wifi_device_name(self):
231        """Helper for device_description()."""
232        device_path = self.device_path
233        if not device_path:
234            return None
235
236        read_file = (lambda path: self._run('cat "%s"' % path).stdout.rstrip()
237                     if self.host.path_exists(path) else None)
238
239        # Try to identify using either vendor/product ID, or using device tree
240        # "OF_COMPATIBLE_x".
241        vendor_id = read_file(os.path.join(device_path, 'vendor'))
242        product_id = read_file(os.path.join(device_path, 'device'))
243        subsystem_id = read_file(os.path.join(device_path, 'subsystem_device'))
244        uevent = read_file(os.path.join(device_path, 'uevent'))
245
246        # Device tree "compatible".
247        for line in uevent.splitlines():
248            key, _, value = line.partition('=')
249            if re.match('^OF_COMPATIBLE_[0-9]+$', key):
250                info = DeviceInfo(compatible=value)
251                if info in DEVICE_NAME_LOOKUP:
252                    return DEVICE_NAME_LOOKUP[info]
253
254        # {Vendor, Product, Subsystem} ID.
255        if subsystem_id is not None:
256            info = DeviceInfo(vendor_id, product_id, subsystem=subsystem_id)
257            if info in DEVICE_NAME_LOOKUP:
258                return DEVICE_NAME_LOOKUP[info]
259
260
261        # {Vendor, Product} ID.
262        info = DeviceInfo(vendor_id, product_id)
263        if info in DEVICE_NAME_LOOKUP:
264            return DEVICE_NAME_LOOKUP[info]
265
266        return None
267
268    @property
269    def device_description(self):
270        """@return DeviceDescription object for a WiFi interface, or None."""
271        if not self.is_wifi_device():
272            logging.error('Device description not supported on non-wifi '
273                          'interface: %s.', self._name)
274            return None
275
276        device_name = self._get_wifi_device_name()
277        if not device_name:
278            device_name = NAME_UNKNOWN
279            logging.error('Device is unknown.')
280        else:
281            logging.debug('Device is %s',  device_name)
282
283        module_name = self.module_name
284        kernel_release = self._run('uname -r').stdout.strip()
285        net_drivers_path = '/lib/modules/%s/kernel/drivers/net' % kernel_release
286        if module_name is not None and self.host.path_exists(net_drivers_path):
287            module_path = self._run('find %s -name %s.ko -printf %%P' % (
288                net_drivers_path, module_name)).stdout
289        else:
290            module_path = 'Unknown (kernel might have modules disabled)'
291        return DeviceDescription(device_name, module_path)
292
293
294    @property
295    def exists(self):
296        """@return True if this interface exists, False otherwise."""
297        # No valid interface has no addresses at all.
298        return bool(self.addresses)
299
300
301
302    def get_ip_flags(self):
303        """@return List of flags from 'ip addr show'."""
304        # "ip addr show %s 2> /dev/null" returns something that looks like:
305        #
306        # 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast
307        #    link/ether ac:16:2d:07:51:0f brd ff:ff:ff:ff:ff:ff
308        #    inet 172.22.73.124/22 brd 172.22.75.255 scope global eth0
309        #    inet6 2620:0:1000:1b02:ae16:2dff:fe07:510f/64 scope global dynamic
310        #       valid_lft 2591982sec preferred_lft 604782sec
311        #    inet6 fe80::ae16:2dff:fe07:510f/64 scope link
312        #       valid_lft forever preferred_lft forever
313        #
314        # We only cares about the flags in the first line.
315        result = self._run('ip addr show %s 2> /dev/null' % self._name,
316                           ignore_status=True)
317        address_info = result.stdout
318        if result.exit_status != 0:
319            # The "ip" command will return non-zero if the interface does
320            # not exist.
321            return []
322        status_line = address_info.splitlines()[0]
323        flags_str = status_line[status_line.find('<')+1:status_line.find('>')]
324        return flags_str.split(',')
325
326
327    @property
328    def is_up(self):
329        """@return True if this interface is UP, False otherwise."""
330        return 'UP' in self.get_ip_flags()
331
332
333    @property
334    def is_lower_up(self):
335        """
336        Check if the interface is in LOWER_UP state. This usually means (e.g.,
337        for ethernet) a link is detected.
338
339        @return True if this interface is LOWER_UP, False otherwise."""
340        return 'LOWER_UP' in self.get_ip_flags()
341
342
343    def is_link_operational(self):
344        """@return True if RFC 2683 IfOperStatus is UP (i.e., is able to pass
345        packets).
346        """
347        command = 'ip link show %s' % self._name
348        result = self._run(command, ignore_status=True)
349        if result.exit_status:
350            return False
351        return result.stdout.find('state UP') >= 0
352
353
354    @property
355    def mac_address(self):
356        """@return the (first) MAC address, e.g., "00:11:22:33:44:55"."""
357        return self.addresses.get(self.ADDRESS_TYPE_MAC, [None])[0]
358
359
360    @property
361    def ipv4_address_and_prefix(self):
362        """@return the IPv4 address/prefix, e.g., "192.186.0.1/24"."""
363        return self.addresses.get(self.ADDRESS_TYPE_IPV4, [None])[0]
364
365
366    @property
367    def ipv4_address(self):
368        """@return the (first) IPv4 address, e.g., "192.168.0.1"."""
369        netblock_addr = self.netblock
370        return netblock_addr.addr if netblock_addr else None
371
372
373    @property
374    def ipv4_prefix(self):
375        """@return the IPv4 address prefix e.g., 24."""
376        addr = self.netblock
377        return addr.prefix_len if addr else None
378
379
380    @property
381    def ipv4_subnet(self):
382        """@return string subnet of IPv4 address (e.g. '192.168.0.0')"""
383        addr = self.netblock
384        return addr.subnet if addr else None
385
386
387    @property
388    def ipv4_subnet_mask(self):
389        """@return the IPv4 subnet mask e.g., "255.255.255.0"."""
390        addr = self.netblock
391        return addr.netmask if addr else None
392
393
394    def is_wifi_device(self):
395        """@return True if iw thinks this is a wifi device."""
396        if self._run('iw dev %s info' % self._name,
397                     ignore_status=True).exit_status:
398            logging.debug('%s does not seem to be a wireless device.',
399                          self._name)
400            return False
401        return True
402
403
404    @property
405    def netblock(self):
406        """Return Netblock object for this interface's IPv4 address.
407
408        @return Netblock object (or None if no IPv4 address found).
409
410        """
411        netblock_str = self.ipv4_address_and_prefix
412        return netblock.from_addr(netblock_str) if netblock_str else None
413
414
415    @property
416    def signal_level(self):
417        """Get the signal level for an interface.
418
419        This is currently only defined for WiFi interfaces.
420
421        localhost test # iw dev mlan0 link
422        Connected to 04:f0:21:03:7d:b2 (on mlan0)
423                SSID: Perf_slvf0_ch36
424                freq: 5180
425                RX: 699407596 bytes (8165441 packets)
426                TX: 58632580 bytes (9923989 packets)
427                signal: -54 dBm
428                tx bitrate: 130.0 MBit/s MCS 15
429
430                bss flags:
431                dtim period:    2
432                beacon int:     100
433
434        @return signal level in dBm (a negative, integral number).
435
436        """
437        if not self.is_wifi_device():
438            return None
439
440        result_lines = self._run('iw dev %s link' %
441                                 self._name).stdout.splitlines()
442        signal_pattern = re.compile('signal:\s+([-0-9]+)\s+dbm')
443        for line in result_lines:
444            cleaned = line.strip().lower()
445            match = re.search(signal_pattern, cleaned)
446            if match is not None:
447                return int(match.group(1))
448
449        logging.error('Failed to find signal level for %s.', self._name)
450        return None
451
452
453    @property
454    def signal_level_all_chains(self):
455        """Get the signal level for each chain of an interface.
456
457        This is only defined for WiFi interfaces.
458
459        localhost test # iw wlan0 station dump
460        Station 44:48:c1:af:d7:31 (on wlan0)
461            inactive time:  13180 ms
462            rx bytes:   46886
463            rx packets: 459
464            tx bytes:   103159
465            tx packets: 745
466            tx retries: 17
467            tx failed:  0
468            beacon loss:    0
469            beacon rx:  128
470            rx drop misc:   2
471            signal:     -52 [-52, -53] dBm
472            signal avg: 56 dBm
473            beacon signal avg:  -49 dBm
474            tx bitrate: 400.0 MBit/s VHT-MCS 9 40MHz short GI VHT-NSS 2
475            rx bitrate: 400.0 MBit/s VHT-MCS 9 40MHz short GI VHT-NSS 2
476            authorized: yes
477            authenticated:  yes
478            associated: yes
479            preamble:   long
480            WMM/WME:    yes
481            MFP:        no
482            TDLS peer:  no
483            DTIM period:    1
484            beacon interval:100
485            short slot time:yes
486            connected time: 6874 seconds
487
488        @return array of signal level information for each antenna in dBm
489            (an array of negative, integral numbers e.g. [-67, -60]) or None if
490            chain specific data is not provided by the device.
491
492        """
493        if not self.is_wifi_device():
494            return None
495
496        result_lines = self._run('iw %s station dump' %
497                                 self._name).stdout.splitlines()
498        signal_pattern = re.compile('signal:\s+([-0-9]+)\[')
499        for line in result_lines:
500            cleaned = line.strip().replace(' ', '').lower()
501            match = re.search(signal_pattern, cleaned)
502            if match is not None:
503                signal_levels = cleaned[cleaned.find('[') + 1 :
504                                    cleaned.find(']')].split(',')
505                return map(int, signal_levels)
506        return None
507
508
509    @property
510    def mtu(self):
511        """@return the interface configured maximum transmission unit (MTU)."""
512        # "ip addr show %s 2> /dev/null" returns something that looks like:
513        #
514        # 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast
515        #    link/ether ac:16:2d:07:51:0f brd ff:ff:ff:ff:ff:ff
516        #    inet 172.22.73.124/22 brd 172.22.75.255 scope global eth0
517        #    inet6 2620:0:1000:1b02:ae16:2dff:fe07:510f/64 scope global dynamic
518        #       valid_lft 2591982sec preferred_lft 604782sec
519        #    inet6 fe80::ae16:2dff:fe07:510f/64 scope link
520        #       valid_lft forever preferred_lft forever
521        #
522        # We extract the 'mtu' value (in this example "1500")
523        try:
524            result = self._run('ip addr show %s 2> /dev/null' % self._name)
525            address_info = result.stdout
526        except error.CmdError, e:
527            # The "ip" command will return non-zero if the interface does
528            # not exist.
529            return None
530
531        match = re.search('mtu\s+(\d+)', address_info)
532        if not match:
533            raise error.TestFail('MTU information is not available.')
534        return int(match.group(1))
535
536
537    def noise_level(self, frequency_mhz):
538        """Get the noise level for an interface at a given frequency.
539
540        This is currently only defined for WiFi interfaces.
541
542        This only works on some devices because 'iw survey dump' (the method
543        used to get the noise) only works on some devices.  On other devices,
544        this method returns None.
545
546        @param frequency_mhz: frequency at which the noise level should be
547               measured and reported.
548        @return noise level in dBm (a negative, integral number) or None.
549
550        """
551        if not self.is_wifi_device():
552            return None
553
554        # This code has to find the frequency and then find the noise
555        # associated with that frequency because 'iw survey dump' output looks
556        # like this:
557        #
558        # localhost test # iw dev mlan0 survey dump
559        # ...
560        # Survey data from mlan0
561        #     frequency:              5805 MHz
562        #     noise:                  -91 dBm
563        #     channel active time:    124 ms
564        #     channel busy time:      1 ms
565        #     channel receive time:   1 ms
566        #     channel transmit time:  0 ms
567        # Survey data from mlan0
568        #     frequency:              5825 MHz
569        # ...
570
571        result_lines = self._run('iw dev %s survey dump' %
572                                 self._name).stdout.splitlines()
573        my_frequency_pattern = re.compile('frequency:\s*%d mhz' %
574                                          frequency_mhz)
575        any_frequency_pattern = re.compile('frequency:\s*\d{4} mhz')
576        inside_desired_frequency_block = False
577        noise_pattern = re.compile('noise:\s*([-0-9]+)\s+dbm')
578        for line in result_lines:
579            cleaned = line.strip().lower()
580            if my_frequency_pattern.match(cleaned):
581                inside_desired_frequency_block = True
582            elif inside_desired_frequency_block:
583                match = noise_pattern.match(cleaned)
584                if match is not None:
585                    return int(match.group(1))
586                if any_frequency_pattern.match(cleaned):
587                    inside_desired_frequency_block = False
588
589        logging.error('Failed to find noise level for %s at %d MHz.',
590                      self._name, frequency_mhz)
591        return None
592
593
594def get_interfaces():
595    """
596    Retrieve the list of network interfaces found on the system.
597
598    @return List of interfaces.
599
600    """
601    return [Interface(nic.strip()) for nic in os.listdir(DEVICE_INFO_ROOT)]
602
603
604def get_prioritized_default_route(host=None, interface_name_regex=None):
605    """
606    Query a local or remote host for its prioritized default interface
607    and route.
608
609    @param interface_name_regex string regex to filter routes by interface.
610    @return DefaultRoute tuple, or None if no default routes are found.
611
612    """
613    # Build a list of default routes, filtered by interface if requested.
614    # Example command output: 'default via 172.23.188.254 dev eth0  metric 2'
615    run = host.run if host is not None else utils.run
616    output = run('ip route show').stdout
617    output_regex_str = 'default\s+via\s+(\S+)\s+dev\s+(\S+)\s+metric\s+(\d+)'
618    output_regex = re.compile(output_regex_str)
619    defaults = []
620    for item in output.splitlines():
621        if 'default' not in item:
622            continue
623        match = output_regex.match(item.strip())
624        if match is None:
625            raise error.TestFail('Unexpected route output: %s' % item)
626        gateway = match.group(1)
627        interface_name = match.group(2)
628        metric = int(match.group(3))
629        if interface_name_regex is not None:
630            if re.match(interface_name_regex, interface_name) is None:
631                continue
632        defaults.append(DefaultRoute(interface_name=interface_name,
633                                     gateway=gateway, metric=metric))
634    if not defaults:
635        return None
636
637    # Sort and return the route with the lowest metric value.
638    defaults.sort(key=lambda x: x.metric)
639    return defaults[0]
640
641