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