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