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