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