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