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