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