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