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