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