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