# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import collections import logging import os import re from autotest_lib.client.bin import local_host from autotest_lib.client.bin import utils from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib.cros.network import netblock # A tuple consisting of a readable part number (one of NAME_* below) # and a kernel module that provides the driver for this part (e.g. ath9k). DeviceDescription = collections.namedtuple('DeviceDescription', ['name', 'kernel_module']) # A tuple describing a default route, consisting of an interface name, # gateway IP address, and the metric value visible in the routing table. DefaultRoute = collections.namedtuple('DefaultRoute', ['interface_name', 'gateway', 'metric']) NAME_MARVELL_88W8797_SDIO = 'Marvell 88W8797 SDIO' NAME_MARVELL_88W8887_SDIO = 'Marvell 88W8887 SDIO' NAME_MARVELL_88W8897_SDIO = 'Marvell 88W8897 SDIO' NAME_MARVELL_88W8897_PCIE = 'Marvell 88W8897 PCIE' NAME_MARVELL_88W8997_PCIE = 'Marvell 88W8997 PCIE' NAME_ATHEROS_AR9280 = 'Atheros AR9280' NAME_ATHEROS_AR9382 = 'Atheros AR9382' NAME_ATHEROS_AR9462 = 'Atheros AR9462' NAME_QUALCOMM_ATHEROS_QCA6174 = 'Qualcomm Atheros QCA6174' NAME_QUALCOMM_WCN3990 = 'Qualcomm WCN3990' NAME_INTEL_7260 = 'Intel 7260' NAME_INTEL_7265 = 'Intel 7265' NAME_INTEL_9000 = 'Intel 9000' NAME_INTEL_9260 = 'Intel 9260' NAME_INTEL_22260 = 'Intel 22260' NAME_BROADCOM_BCM4354_SDIO = 'Broadcom BCM4354 SDIO' NAME_BROADCOM_BCM4356_PCIE = 'Broadcom BCM4356 PCIE' NAME_BROADCOM_BCM4371_PCIE = 'Broadcom BCM4371 PCIE' NAME_UNKNOWN = 'Unknown WiFi Device' DEVICE_INFO_ROOT = '/sys/class/net' DeviceInfo = collections.namedtuple('DeviceInfo', ['vendor', 'device', 'compatible']) # Provide default values for parameters. DeviceInfo.__new__.__defaults__ = (None, None, None) DEVICE_NAME_LOOKUP = { DeviceInfo('0x02df', '0x9129'): NAME_MARVELL_88W8797_SDIO, DeviceInfo('0x02df', '0x912d'): NAME_MARVELL_88W8897_SDIO, DeviceInfo('0x02df', '0x9135'): NAME_MARVELL_88W8887_SDIO, DeviceInfo('0x11ab', '0x2b38'): NAME_MARVELL_88W8897_PCIE, DeviceInfo('0x1b4b', '0x2b42'): NAME_MARVELL_88W8997_PCIE, DeviceInfo('0x168c', '0x002a'): NAME_ATHEROS_AR9280, DeviceInfo('0x168c', '0x0030'): NAME_ATHEROS_AR9382, DeviceInfo('0x168c', '0x0034'): NAME_ATHEROS_AR9462, DeviceInfo('0x168c', '0x003e'): NAME_QUALCOMM_ATHEROS_QCA6174, DeviceInfo('0x105b', '0xe09d'): NAME_QUALCOMM_ATHEROS_QCA6174, DeviceInfo('0x8086', '0x08b1'): NAME_INTEL_7260, DeviceInfo('0x8086', '0x08b2'): NAME_INTEL_7260, DeviceInfo('0x8086', '0x095a'): NAME_INTEL_7265, DeviceInfo('0x8086', '0x095b'): NAME_INTEL_7265, DeviceInfo('0x8086', '0x9df0'): NAME_INTEL_9000, DeviceInfo('0x8086', '0x31dc'): NAME_INTEL_9000, DeviceInfo('0x8086', '0x2526'): NAME_INTEL_9260, DeviceInfo('0x8086', '0x2723'): NAME_INTEL_22260, DeviceInfo('0x02d0', '0x4354'): NAME_BROADCOM_BCM4354_SDIO, DeviceInfo('0x14e4', '0x43ec'): NAME_BROADCOM_BCM4356_PCIE, DeviceInfo('0x14e4', '0x440d'): NAME_BROADCOM_BCM4371_PCIE, DeviceInfo(compatible='qcom,wcn3990-wifi'): NAME_QUALCOMM_WCN3990, } class Interface: """Interace is a class that contains the queriable address properties of an network device. """ ADDRESS_TYPE_MAC = 'link/ether' ADDRESS_TYPE_IPV4 = 'inet' ADDRESS_TYPE_IPV6 = 'inet6' ADDRESS_TYPES = [ ADDRESS_TYPE_MAC, ADDRESS_TYPE_IPV4, ADDRESS_TYPE_IPV6 ] @staticmethod def get_connected_ethernet_interface(ignore_failures=False): """Get an interface object representing a connected ethernet device. Raises an exception if no such interface exists. @param ignore_failures bool function will return None instead of raising an exception on failures. @return an Interface object except under the conditions described above. """ # Assume that ethernet devices are called ethX until proven otherwise. for device_name in ['eth%d' % i for i in range(5)]: ethernet_if = Interface(device_name) if ethernet_if.exists and ethernet_if.ipv4_address: return ethernet_if else: if ignore_failures: return None raise error.TestFail('Failed to find ethernet interface.') def __init__(self, name, host=None): self._name = name if host is None: self.host = local_host.LocalHost() else: self.host = host self._run = self.host.run @property def name(self): """@return name of the interface (e.g. 'wlan0').""" return self._name @property def addresses(self): """@return the addresses (MAC, IP) associated with interface.""" # "ip addr show %s 2> /dev/null" returns something that looks like: # # 2: eth0: mtu 1500 qdisc pfifo_fast # link/ether ac:16:2d:07:51:0f brd ff:ff:ff:ff:ff:ff # inet 172.22.73.124/22 brd 172.22.75.255 scope global eth0 # inet6 2620:0:1000:1b02:ae16:2dff:fe07:510f/64 scope global dynamic # valid_lft 2591982sec preferred_lft 604782sec # inet6 fe80::ae16:2dff:fe07:510f/64 scope link # valid_lft forever preferred_lft forever # # We extract the second column from any entry for which the first # column is an address type we are interested in. For example, # for "inet 172.22.73.124/22 ...", we will capture "172.22.73.124/22". result = self._run('ip addr show %s 2> /dev/null' % self._name, ignore_status=True) address_info = result.stdout if result.exit_status != 0: # The "ip" command will return non-zero if the interface does # not exist. return {} addresses = {} for address_line in address_info.splitlines(): address_parts = address_line.lstrip().split() if len(address_parts) < 2: continue address_type, address_value = address_parts[:2] if address_type in self.ADDRESS_TYPES: if address_type not in addresses: addresses[address_type] = [] addresses[address_type].append(address_value) return addresses @property def device_path(self): """@return the sysfs path of the interface device""" # This assumes that our path separator is the same as the remote host. device_path = os.path.join(DEVICE_INFO_ROOT, self._name, 'device') if not self.host.path_exists(device_path): logging.error('No device information found at %s', device_path) return None return device_path @property def wiphy_name(self): """ @return name of the wiphy (e.g., 'phy0'), if available. Otherwise None. """ readlink_result = self._run('readlink "%s"' % os.path.join(DEVICE_INFO_ROOT, self._name, 'phy80211'), ignore_status=True) if readlink_result.exit_status != 0: return None return os.path.basename(readlink_result.stdout.strip()) @property def module_name(self): """@return Name of kernel module in use by this interface.""" module_readlink_result = self._run('readlink "%s"' % os.path.join(self.device_path, 'driver', 'module'), ignore_status=True) if module_readlink_result.exit_status != 0: return None return os.path.basename(module_readlink_result.stdout.strip()) @property def parent_device_name(self): """ @return Name of device at which wiphy device is present. For example, for a wifi NIC present on a PCI bus, this would be the same as PCI_SLOT_PATH. """ path_readlink_result = self._run('readlink "%s"' % self.device_path) if path_readlink_result.exit_status != 0: return None return os.path.basename(path_readlink_result.stdout.strip()) @property def device_description(self): """@return DeviceDescription object for a WiFi interface, or None.""" read_file = (lambda path: self._run('cat "%s"' % path).stdout.rstrip() if self.host.path_exists(path) else None) if not self.is_wifi_device(): logging.error('Device description not supported on non-wifi ' 'interface: %s.', self._name) return None device_path = self.device_path if not device_path: logging.error('No device path found') return None # Try to identify using either vendor/product ID, or using device tree # "OF_COMPATIBLE_x". vendor_id = read_file(os.path.join(device_path, 'vendor')) product_id = read_file(os.path.join(device_path, 'device')) uevent = read_file(os.path.join(device_path, 'uevent')) # Vendor/product ID. infos = [DeviceInfo(vendor_id, product_id)] # Compatible value(s). for line in uevent.splitlines(): key, _, value = line.partition('=') if re.match('^OF_COMPATIBLE_[0-9]+$', key): infos += [DeviceInfo(compatible=value)] for info in infos: if info in DEVICE_NAME_LOOKUP: device_name = DEVICE_NAME_LOOKUP[info] logging.debug('Device is %s', device_name) break else: logging.error('Device is unknown. Info: %r', infos) device_name = NAME_UNKNOWN module_name = self.module_name if module_name is not None: kernel_release = self._run('uname -r').stdout.strip() module_path = self._run('find ' '/lib/modules/%s/kernel/drivers/net ' '-name %s.ko -printf %%P' % (kernel_release, module_name)).stdout else: module_path = 'Unknown (kernel might have modules disabled)' return DeviceDescription(device_name, module_path) @property def exists(self): """@return True if this interface exists, False otherwise.""" # No valid interface has no addresses at all. return bool(self.addresses) def get_ip_flags(self): """@return List of flags from 'ip addr show'.""" # "ip addr show %s 2> /dev/null" returns something that looks like: # # 2: eth0: mtu 1500 qdisc pfifo_fast # link/ether ac:16:2d:07:51:0f brd ff:ff:ff:ff:ff:ff # inet 172.22.73.124/22 brd 172.22.75.255 scope global eth0 # inet6 2620:0:1000:1b02:ae16:2dff:fe07:510f/64 scope global dynamic # valid_lft 2591982sec preferred_lft 604782sec # inet6 fe80::ae16:2dff:fe07:510f/64 scope link # valid_lft forever preferred_lft forever # # We only cares about the flags in the first line. result = self._run('ip addr show %s 2> /dev/null' % self._name, ignore_status=True) address_info = result.stdout if result.exit_status != 0: # The "ip" command will return non-zero if the interface does # not exist. return [] status_line = address_info.splitlines()[0] flags_str = status_line[status_line.find('<')+1:status_line.find('>')] return flags_str.split(',') @property def is_up(self): """@return True if this interface is UP, False otherwise.""" return 'UP' in self.get_ip_flags() @property def is_lower_up(self): """ Check if the interface is in LOWER_UP state. This usually means (e.g., for ethernet) a link is detected. @return True if this interface is LOWER_UP, False otherwise.""" return 'LOWER_UP' in self.get_ip_flags() def is_link_operational(self): """@return True if RFC 2683 IfOperStatus is UP (i.e., is able to pass packets). """ command = 'ip link show %s' % self._name result = self._run(command, ignore_status=True) if result.exit_status: return False return result.stdout.find('state UP') >= 0 @property def mac_address(self): """@return the (first) MAC address, e.g., "00:11:22:33:44:55".""" return self.addresses.get(self.ADDRESS_TYPE_MAC, [None])[0] @property def ipv4_address_and_prefix(self): """@return the IPv4 address/prefix, e.g., "192.186.0.1/24".""" return self.addresses.get(self.ADDRESS_TYPE_IPV4, [None])[0] @property def ipv4_address(self): """@return the (first) IPv4 address, e.g., "192.168.0.1".""" netblock_addr = self.netblock return netblock_addr.addr if netblock_addr else None @property def ipv4_prefix(self): """@return the IPv4 address prefix e.g., 24.""" addr = self.netblock return addr.prefix_len if addr else None @property def ipv4_subnet(self): """@return string subnet of IPv4 address (e.g. '192.168.0.0')""" addr = self.netblock return addr.subnet if addr else None @property def ipv4_subnet_mask(self): """@return the IPv4 subnet mask e.g., "255.255.255.0".""" addr = self.netblock return addr.netmask if addr else None def is_wifi_device(self): """@return True if iw thinks this is a wifi device.""" if self._run('iw dev %s info' % self._name, ignore_status=True).exit_status: logging.debug('%s does not seem to be a wireless device.', self._name) return False return True @property def netblock(self): """Return Netblock object for this interface's IPv4 address. @return Netblock object (or None if no IPv4 address found). """ netblock_str = self.ipv4_address_and_prefix return netblock.from_addr(netblock_str) if netblock_str else None @property def signal_level(self): """Get the signal level for an interface. This is currently only defined for WiFi interfaces. localhost test # iw dev mlan0 link Connected to 04:f0:21:03:7d:b2 (on mlan0) SSID: Perf_slvf0_ch36 freq: 5180 RX: 699407596 bytes (8165441 packets) TX: 58632580 bytes (9923989 packets) signal: -54 dBm tx bitrate: 130.0 MBit/s MCS 15 bss flags: dtim period: 2 beacon int: 100 @return signal level in dBm (a negative, integral number). """ if not self.is_wifi_device(): return None result_lines = self._run('iw dev %s link' % self._name).stdout.splitlines() signal_pattern = re.compile('signal:\s+([-0-9]+)\s+dbm') for line in result_lines: cleaned = line.strip().lower() match = re.search(signal_pattern, cleaned) if match is not None: return int(match.group(1)) logging.error('Failed to find signal level for %s.', self._name) return None @property def mtu(self): """@return the interface configured maximum transmission unit (MTU).""" # "ip addr show %s 2> /dev/null" returns something that looks like: # # 2: eth0: mtu 1500 qdisc pfifo_fast # link/ether ac:16:2d:07:51:0f brd ff:ff:ff:ff:ff:ff # inet 172.22.73.124/22 brd 172.22.75.255 scope global eth0 # inet6 2620:0:1000:1b02:ae16:2dff:fe07:510f/64 scope global dynamic # valid_lft 2591982sec preferred_lft 604782sec # inet6 fe80::ae16:2dff:fe07:510f/64 scope link # valid_lft forever preferred_lft forever # # We extract the 'mtu' value (in this example "1500") try: result = self._run('ip addr show %s 2> /dev/null' % self._name) address_info = result.stdout except error.CmdError, e: # The "ip" command will return non-zero if the interface does # not exist. return None match = re.search('mtu\s+(\d+)', address_info) if not match: raise error.TestFail('MTU information is not available.') return int(match.group(1)) def noise_level(self, frequency_mhz): """Get the noise level for an interface at a given frequency. This is currently only defined for WiFi interfaces. This only works on some devices because 'iw survey dump' (the method used to get the noise) only works on some devices. On other devices, this method returns None. @param frequency_mhz: frequency at which the noise level should be measured and reported. @return noise level in dBm (a negative, integral number) or None. """ if not self.is_wifi_device(): return None # This code has to find the frequency and then find the noise # associated with that frequency because 'iw survey dump' output looks # like this: # # localhost test # iw dev mlan0 survey dump # ... # Survey data from mlan0 # frequency: 5805 MHz # noise: -91 dBm # channel active time: 124 ms # channel busy time: 1 ms # channel receive time: 1 ms # channel transmit time: 0 ms # Survey data from mlan0 # frequency: 5825 MHz # ... result_lines = self._run('iw dev %s survey dump' % self._name).stdout.splitlines() my_frequency_pattern = re.compile('frequency:\s*%d mhz' % frequency_mhz) any_frequency_pattern = re.compile('frequency:\s*\d{4} mhz') inside_desired_frequency_block = False noise_pattern = re.compile('noise:\s*([-0-9]+)\s+dbm') for line in result_lines: cleaned = line.strip().lower() if my_frequency_pattern.match(cleaned): inside_desired_frequency_block = True elif inside_desired_frequency_block: match = noise_pattern.match(cleaned) if match is not None: return int(match.group(1)) if any_frequency_pattern.match(cleaned): inside_desired_frequency_block = False logging.error('Failed to find noise level for %s at %d MHz.', self._name, frequency_mhz) return None def get_interfaces(): """ Retrieve the list of network interfaces found on the system. @return List of interfaces. """ return [Interface(nic.strip()) for nic in os.listdir(DEVICE_INFO_ROOT)] def get_prioritized_default_route(host=None, interface_name_regex=None): """ Query a local or remote host for its prioritized default interface and route. @param interface_name_regex string regex to filter routes by interface. @return DefaultRoute tuple, or None if no default routes are found. """ # Build a list of default routes, filtered by interface if requested. # Example command output: 'default via 172.23.188.254 dev eth0 metric 2' run = host.run if host is not None else utils.run output = run('ip route show').stdout output_regex_str = 'default\s+via\s+(\S+)\s+dev\s+(\S+)\s+metric\s+(\d+)' output_regex = re.compile(output_regex_str) defaults = [] for item in output.splitlines(): if 'default' not in item: continue match = output_regex.match(item.strip()) if match is None: raise error.TestFail('Unexpected route output: %s' % item) gateway = match.group(1) interface_name = match.group(2) metric = int(match.group(3)) if interface_name_regex is not None: if re.match(interface_name_regex, interface_name) is None: continue defaults.append(DefaultRoute(interface_name=interface_name, gateway=gateway, metric=metric)) if not defaults: return None # Sort and return the route with the lowest metric value. defaults.sort(key=lambda x: x.metric) return defaults[0]