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