Home | History | Annotate | Download | only in network
      1 # Copyright (c) 2013 The Chromium 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 copy
      7 import logging
      8 import operator
      9 import re
     10 import time
     11 
     12 from autotest_lib.client.common_lib import error
     13 from autotest_lib.client.common_lib import utils
     14 from autotest_lib.client.common_lib.cros.network import iw_event_logger
     15 
     16 # These must mirror the values in 'iw list' output.
     17 CHAN_FLAG_DISABLED = 'disabled'
     18 CHAN_FLAG_NO_IR = 'no IR'
     19 CHAN_FLAG_PASSIVE_SCAN = 'passive scan'
     20 CHAN_FLAG_RADAR_DETECT = 'radar detection'
     21 DEV_MODE_AP = 'AP'
     22 DEV_MODE_IBSS = 'IBSS'
     23 DEV_MODE_MONITOR = 'monitor'
     24 DEV_MODE_MESH_POINT = 'mesh point'
     25 DEV_MODE_STATION = 'managed'
     26 SUPPORTED_DEV_MODES = (DEV_MODE_AP, DEV_MODE_IBSS, DEV_MODE_MONITOR,
     27                        DEV_MODE_MESH_POINT, DEV_MODE_STATION)
     28 
     29 HT20 = 'HT20'
     30 HT40_ABOVE = 'HT40+'
     31 HT40_BELOW = 'HT40-'
     32 
     33 SECURITY_OPEN = 'open'
     34 SECURITY_WEP = 'wep'
     35 SECURITY_WPA = 'wpa'
     36 SECURITY_WPA2 = 'wpa2'
     37 # Mixed mode security is WPA2/WPA
     38 SECURITY_MIXED = 'mixed'
     39 
     40 # Table of lookups between the output of item 'secondary channel offset:' from
     41 # iw <device> scan to constants.
     42 
     43 HT_TABLE = {'no secondary': HT20,
     44             'above': HT40_ABOVE,
     45             'below': HT40_BELOW}
     46 
     47 IwBand = collections.namedtuple(
     48     'Band', ['num', 'frequencies', 'frequency_flags', 'mcs_indices'])
     49 IwBss = collections.namedtuple('IwBss', ['bss', 'frequency', 'ssid', 'security',
     50                                          'ht', 'signal'])
     51 IwNetDev = collections.namedtuple('IwNetDev', ['phy', 'if_name', 'if_type'])
     52 IwTimedScan = collections.namedtuple('IwTimedScan', ['time', 'bss_list'])
     53 
     54 # The fields for IwPhy are as follows:
     55 #   name: string name of the phy, such as "phy0"
     56 #   bands: list of IwBand objects.
     57 #   modes: List of strings containing interface modes supported, such as "AP".
     58 #   commands: List of strings containing nl80211 commands supported, such as
     59 #          "authenticate".
     60 #   features: List of strings containing nl80211 features supported, such as
     61 #          "T-DLS".
     62 #   max_scan_ssids: Maximum number of SSIDs which can be scanned at once.
     63 IwPhy = collections.namedtuple(
     64     'Phy', ['name', 'bands', 'modes', 'commands', 'features',
     65             'max_scan_ssids', 'avail_tx_antennas', 'avail_rx_antennas',
     66             'supports_setting_antenna_mask', 'support_vht'])
     67 
     68 DEFAULT_COMMAND_IW = 'iw'
     69 
     70 # Redirect stderr to stdout on Cros since adb commands cannot distinguish them
     71 # on Brillo.
     72 IW_TIME_COMMAND_FORMAT = '(time -p %s) 2>&1'
     73 IW_TIME_COMMAND_OUTPUT_START = 'real'
     74 
     75 IW_LINK_KEY_BEACON_INTERVAL = 'beacon int'
     76 IW_LINK_KEY_DTIM_PERIOD = 'dtim period'
     77 IW_LINK_KEY_FREQUENCY = 'freq'
     78 IW_LINK_KEY_SIGNAL = 'signal'
     79 IW_LINK_KEY_RX_BITRATE = 'rx bitrate'
     80 IW_LINK_KEY_TX_BITRATE = 'tx bitrate'
     81 IW_LOCAL_EVENT_LOG_FILE = './debug/iw_event_%d.log'
     82 
     83 
     84 def _get_all_link_keys(link_information):
     85     """Parses link or station dump output for link key value pairs.
     86 
     87     Link or station dump information is in the format below:
     88 
     89     Connected to 74:e5:43:10:4f:c0 (on wlan0)
     90           SSID: PMKSACaching_4m9p5_ch1
     91           freq: 5220
     92           RX: 5370 bytes (37 packets)
     93           TX: 3604 bytes (15 packets)
     94           signal: -59 dBm
     95           tx bitrate: 13.0 MBit/s MCS 1
     96 
     97           bss flags:      short-slot-time
     98           dtim period:    5
     99           beacon int:     100
    100 
    101     @param link_information: string containing the raw link or station dump
    102         information as reported by iw. Note that this parsing assumes a single
    103         entry, in the case of multiple entries (e.g. listing stations from an
    104         AP, or listing mesh peers), the entries must be split on a per
    105         peer/client basis before this parsing operation.
    106     @return a dictionary containing all the link key/value pairs.
    107 
    108     """
    109     link_key_value_pairs = {}
    110     keyval_regex = re.compile(r'^\s+(.*):\s+(.*)$')
    111     for link_key in link_information.splitlines()[1:]:
    112         match = re.search(keyval_regex, link_key)
    113         if match:
    114             # Station dumps can contain blank lines.
    115             link_key_value_pairs[match.group(1)] = match.group(2)
    116     return link_key_value_pairs
    117 
    118 
    119 def _extract_bssid(link_information, interface_name, station_dump=False):
    120     """Get the BSSID that |interface_name| is associated with.
    121 
    122     See doc for _get_all_link_keys() for expected format of the station or link
    123     information entry.
    124 
    125     @param link_information: string containing the raw link or station dump
    126         information as reported by iw. Note that this parsing assumes a single
    127         entry, in the case of multiple entries (e.g. listing stations from an AP
    128         or listing mesh peers), the entries must be split on a per peer/client
    129         basis before this parsing operation.
    130     @param interface_name: string name of interface (e.g. 'wlan0').
    131     @param station_dump: boolean indicator of whether the link information is
    132         from a 'station dump' query. If False, it is assumed the string is from
    133         a 'link' query.
    134     @return string bssid of the current association, or None if no matching
    135         association information is found.
    136 
    137     """
    138     # We're looking for a line like this when parsing the output of a 'link'
    139     # query:
    140     #   Connected to 04:f0:21:03:7d:bb (on wlan0)
    141     # We're looking for a line like this when parsing the output of a
    142     # 'station dump' query:
    143     #   Station 04:f0:21:03:7d:bb (on mesh-5000mhz)
    144     identifier = 'Station' if station_dump else 'Connected to'
    145     search_re = r'%s ([0-9a-fA-F:]{17}) \(on %s\)' % (identifier,
    146                                                       interface_name)
    147     match = re.match(search_re, link_information)
    148     if match is None:
    149         return None
    150     return match.group(1)
    151 
    152 
    153 class IwRunner(object):
    154     """Defines an interface to the 'iw' command."""
    155 
    156 
    157     def __init__(self, remote_host=None, command_iw=DEFAULT_COMMAND_IW):
    158         self._run = utils.run
    159         self._host = remote_host
    160         if remote_host:
    161             self._run = remote_host.run
    162         self._command_iw = command_iw
    163         self._log_id = 0
    164 
    165 
    166     def _parse_scan_results(self, output):
    167         """Parse the output of the 'scan' and 'scan dump' commands.
    168 
    169         Here is an example of what a single network would look like for
    170         the input parameter.  Some fields have been removed in this example:
    171           BSS 00:11:22:33:44:55(on wlan0)
    172           freq: 2447
    173           beacon interval: 100 TUs
    174           signal: -46.00 dBm
    175           Information elements from Probe Response frame:
    176           SSID: my_open_network
    177           Extended supported rates: 24.0 36.0 48.0 54.0
    178           HT capabilities:
    179           Capabilities: 0x0c
    180           HT20
    181           HT operation:
    182           * primary channel: 8
    183           * secondary channel offset: no secondary
    184           * STA channel width: 20 MHz
    185           RSN: * Version: 1
    186           * Group cipher: CCMP
    187           * Pairwise ciphers: CCMP
    188           * Authentication suites: PSK
    189           * Capabilities: 1-PTKSA-RC 1-GTKSA-RC (0x0000)
    190 
    191         @param output: string command output.
    192 
    193         @returns a list of IwBss namedtuples; None if the scan fails
    194 
    195         """
    196         bss = None
    197         frequency = None
    198         ssid = None
    199         ht = None
    200         signal = None
    201         security = None
    202         supported_securities = []
    203         bss_list = []
    204         for line in output.splitlines():
    205             line = line.strip()
    206             bss_match = re.match('BSS ([0-9a-f:]+)', line)
    207             if bss_match:
    208                 if bss != None:
    209                     security = self.determine_security(supported_securities)
    210                     iwbss = IwBss(bss, frequency, ssid, security, ht, signal)
    211                     bss_list.append(iwbss)
    212                     bss = frequency = ssid = security = ht = None
    213                     supported_securities = []
    214                 bss = bss_match.group(1)
    215             if line.startswith('freq:'):
    216                 frequency = int(line.split()[1])
    217             if line.startswith('signal:'):
    218                 signal = float(line.split()[1])
    219             if line.startswith('SSID: '):
    220                 _, ssid = line.split(': ', 1)
    221             if line.startswith('* secondary channel offset'):
    222                 ht = HT_TABLE[line.split(':')[1].strip()]
    223             if line.startswith('WPA'):
    224                supported_securities.append(SECURITY_WPA)
    225             if line.startswith('RSN'):
    226                supported_securities.append(SECURITY_WPA2)
    227         security = self.determine_security(supported_securities)
    228         bss_list.append(IwBss(bss, frequency, ssid, security, ht, signal))
    229         return bss_list
    230 
    231 
    232     def _parse_scan_time(self, output):
    233         """
    234         Parse the scan time in seconds from the output of the 'time -p "scan"'
    235         command.
    236 
    237         'time -p' Command output format is below:
    238         real     0.01
    239         user     0.01
    240         sys      0.00
    241 
    242         @param output: string command output.
    243 
    244         @returns float time in seconds.
    245 
    246         """
    247         output_lines = output.splitlines()
    248         for line_num, line in enumerate(output_lines):
    249             line = line.strip()
    250             if (line.startswith(IW_TIME_COMMAND_OUTPUT_START) and
    251                 output_lines[line_num + 1].startswith('user') and
    252                 output_lines[line_num + 2].startswith('sys')):
    253                 return float(line.split()[1])
    254         raise error.TestFail('Could not parse scan time.')
    255 
    256 
    257     def add_interface(self, phy, interface, interface_type):
    258         """
    259         Add an interface to a WiFi PHY.
    260 
    261         @param phy: string name of PHY to add an interface to.
    262         @param interface: string name of interface to add.
    263         @param interface_type: string type of interface to add (e.g. 'monitor').
    264 
    265         """
    266         self._run('%s phy %s interface add %s type %s' %
    267                   (self._command_iw, phy, interface, interface_type))
    268 
    269 
    270     def disconnect_station(self, interface):
    271         """
    272         Disconnect a STA from a network.
    273 
    274         @param interface: string name of interface to disconnect.
    275 
    276         """
    277         self._run('%s dev %s disconnect' % (self._command_iw, interface))
    278 
    279 
    280     def get_current_bssid(self, interface_name):
    281         """Get the BSSID that |interface_name| is associated with.
    282 
    283         @param interface_name: string name of interface (e.g. 'wlan0').
    284         @return string bssid of our current association, or None.
    285 
    286         """
    287         result = self._run('%s dev %s link' %
    288                            (self._command_iw, interface_name),
    289                            ignore_status=True)
    290         if result.exit_status:
    291             # See comment in get_link_value.
    292             return None
    293 
    294         return _extract_bssid(result.stdout, interface_name)
    295 
    296 
    297     def get_interface(self, interface_name):
    298         """Get full information about an interface given an interface name.
    299 
    300         @param interface_name: string name of interface (e.g. 'wlan0').
    301         @return IwNetDev tuple.
    302 
    303         """
    304         matching_interfaces = [iw_if for iw_if in self.list_interfaces()
    305                                      if iw_if.if_name == interface_name]
    306         if len(matching_interfaces) != 1:
    307             raise error.TestFail('Could not find interface named %s' %
    308                                  interface_name)
    309 
    310         return matching_interfaces[0]
    311 
    312 
    313     def get_link_value(self, interface, iw_link_key):
    314         """Get the value of a link property for |interface|.
    315 
    316         Checks the link using iw, and parses the result to return a link key.
    317 
    318         @param iw_link_key: string one of IW_LINK_KEY_* defined above.
    319         @param interface: string desired value of iw link property.
    320         @return string containing the corresponding link property value, None
    321             if there was a parsing error or the iw command failed.
    322 
    323         """
    324         result = self._run('%s dev %s link' % (self._command_iw, interface),
    325                            ignore_status=True)
    326         if result.exit_status:
    327             # When roaming, there is a period of time for mac80211 based drivers
    328             # when the driver is 'associated' with an SSID but not a particular
    329             # BSS.  This causes iw to return an error code (-2) when attempting
    330             # to retrieve information specific to the BSS.  This does not happen
    331             # in mwifiex drivers.
    332             return None
    333         actual_value = _get_all_link_keys(result.stdout).get(iw_link_key)
    334         if actual_value is not None:
    335             logging.info('Found iw link key %s with value %s.',
    336                          iw_link_key, actual_value)
    337         return actual_value
    338 
    339 
    340     def get_station_dump(self, interface):
    341         """Gets information about connected peers.
    342 
    343         Returns information about the currently connected peers. When the host
    344         is in station mode, it returns a single entry, with information about
    345         the link to the AP it is currently connected to. If the host is in mesh
    346         or AP mode, it can return multiple entries, one for each connected
    347         station, or mesh peer.
    348 
    349         @param interface: string name of interface to get peer information
    350             from.
    351         @return a list of dictionaries with link information about each
    352             connected peer (ordered by peer mac address).
    353 
    354         """
    355         result = self._run('%s dev %s station dump' %
    356                            (self._command_iw, interface))
    357         parts = re.split(r'^Station ', result.stdout, flags=re.MULTILINE)[1:]
    358         peer_list_raw = ['Station ' + x for x in parts]
    359         parsed_peer_info = []
    360         for peer in peer_list_raw:
    361             peer_link_keys = _get_all_link_keys(peer)
    362             rssi_str = peer_link_keys[IW_LINK_KEY_SIGNAL]
    363             tx_bitrate = peer_link_keys.get(IW_LINK_KEY_TX_BITRATE)
    364             # Station may not have rx_bitrate in station dump
    365             rx_bitrate = peer_link_keys.get(IW_LINK_KEY_RX_BITRATE)
    366             rssi_int = int(rssi_str.split()[0])
    367             mac = _extract_bssid(link_information=peer,
    368                                  interface_name=interface,
    369                                  station_dump=True)
    370             parsed_peer_info.append({'rssi_int': rssi_int,
    371                                      'rssi_str': rssi_str,
    372                                      'tx_bitrate': tx_bitrate,
    373                                      'rx_bitrate': rx_bitrate,
    374                                      'mac': mac})
    375         return sorted(parsed_peer_info, key=operator.itemgetter('mac'))
    376 
    377 
    378     def get_operating_mode(self, interface):
    379         """Gets the operating mode for |interface|.
    380 
    381         @param interface: string name of interface to get peer information
    382             about.
    383 
    384         @return string one of DEV_MODE_* defined above, or None if no mode is
    385             found, or if an unsupported mode is found.
    386 
    387         """
    388         ret = self._run('%s dev %s info' % (self._command_iw, interface))
    389         mode_regex = r'^\s*type (.*)$'
    390         match = re.search(mode_regex, ret.stdout, re.MULTILINE)
    391         if match:
    392             operating_mode = match.group(1)
    393             if operating_mode in SUPPORTED_DEV_MODES:
    394                 return operating_mode
    395             logging.warning(
    396                 'Unsupported operating mode %s found for interface: %s. '
    397                 'Supported modes: %s', operating_mode, interface,
    398                 SUPPORTED_DEV_MODES)
    399         return None
    400 
    401 
    402     def get_radio_config(self, interface):
    403         """Gets the channel information of a specfic interface using iw.
    404 
    405         @param interface: string name of interface to get radio information
    406             from.
    407 
    408         @return dictionary containing the channel information.
    409 
    410         """
    411         channel_config = {}
    412         ret = self._run('%s dev %s info' % (self._command_iw, interface))
    413         channel_config_regex = (r'^\s*channel ([0-9]+) \(([0-9]+) MHz\), '
    414                                  'width: ([2,4,8]0) MHz, center1: ([0-9]+) MHz')
    415         match = re.search(channel_config_regex, ret.stdout, re.MULTILINE)
    416 
    417         if match:
    418             channel_config['number'] = int(match.group(1))
    419             channel_config['freq'] = int(match.group(2))
    420             channel_config['width'] = int(match.group(3))
    421             channel_config['center1_freq'] = int(match.group(4))
    422 
    423         return channel_config
    424 
    425 
    426     def ibss_join(self, interface, ssid, frequency):
    427         """
    428         Join a WiFi interface to an IBSS.
    429 
    430         @param interface: string name of interface to join to the IBSS.
    431         @param ssid: string SSID of IBSS to join.
    432         @param frequency: int frequency of IBSS in Mhz.
    433 
    434         """
    435         self._run('%s dev %s ibss join %s %d' %
    436                   (self._command_iw, interface, ssid, frequency))
    437 
    438 
    439     def ibss_leave(self, interface):
    440         """
    441         Leave an IBSS.
    442 
    443         @param interface: string name of interface to remove from the IBSS.
    444 
    445         """
    446         self._run('%s dev %s ibss leave' % (self._command_iw, interface))
    447 
    448 
    449     def list_interfaces(self, desired_if_type=None):
    450         """List WiFi related interfaces on this system.
    451 
    452         @param desired_if_type: string type of interface to filter
    453                 our returned list of interfaces for (e.g. 'managed').
    454 
    455         @return list of IwNetDev tuples.
    456 
    457         """
    458 
    459         # Parse output in the following format:
    460         #
    461         #   $ adb shell iw dev
    462         #   phy#0
    463         #     Unnamed/non-netdev interface
    464         #       wdev 0x2
    465         #       addr aa:bb:cc:dd:ee:ff
    466         #       type P2P-device
    467         #     Interface wlan0
    468         #       ifindex 4
    469         #       wdev 0x1
    470         #       addr aa:bb:cc:dd:ee:ff
    471         #       ssid Whatever
    472         #       type managed
    473 
    474         output = self._run('%s dev' % self._command_iw).stdout
    475         interfaces = []
    476         phy = None
    477         if_name = None
    478         if_type = None
    479         for line in output.splitlines():
    480             m = re.match('phy#([0-9]+)', line)
    481             if m:
    482                 phy = 'phy%d' % int(m.group(1))
    483                 if_name = None
    484                 if_type = None
    485                 continue
    486             if not phy:
    487                 continue
    488             m = re.match('[\s]*Interface (.*)', line)
    489             if m:
    490                 if_name = m.group(1)
    491                 continue
    492             if not if_name:
    493                 continue
    494             # Common values for type are 'managed', 'monitor', and 'IBSS'.
    495             m = re.match('[\s]*type ([a-zA-Z]+)', line)
    496             if m:
    497                 if_type = m.group(1)
    498                 interfaces.append(IwNetDev(phy=phy, if_name=if_name,
    499                                            if_type=if_type))
    500                 # One phy may have many interfaces, so don't reset it.
    501                 if_name = None
    502 
    503         if desired_if_type:
    504             interfaces = [interface for interface in interfaces
    505                           if interface.if_type == desired_if_type]
    506         return interfaces
    507 
    508 
    509     def list_phys(self):
    510         """
    511         List WiFi PHYs on the given host.
    512 
    513         @return list of IwPhy tuples.
    514 
    515         """
    516         output = self._run('%s list' % self._command_iw).stdout
    517 
    518         pending_phy_name = None
    519         current_band = None
    520         current_section = None
    521         all_phys = []
    522 
    523         def add_pending_phy():
    524             """Add the pending phy into |all_phys|."""
    525             bands = tuple(IwBand(band.num,
    526                                  tuple(band.frequencies),
    527                                  dict(band.frequency_flags),
    528                                  tuple(band.mcs_indices))
    529                           for band in pending_phy_bands)
    530             new_phy = IwPhy(pending_phy_name,
    531                             bands,
    532                             tuple(pending_phy_modes),
    533                             tuple(pending_phy_commands),
    534                             tuple(pending_phy_features),
    535                             pending_phy_max_scan_ssids,
    536                             pending_phy_tx_antennas,
    537                             pending_phy_rx_antennas,
    538                             pending_phy_tx_antennas and pending_phy_rx_antennas,
    539                             pending_phy_support_vht)
    540             all_phys.append(new_phy)
    541 
    542         for line in output.splitlines():
    543             match_phy = re.search('Wiphy (.*)', line)
    544             if match_phy:
    545                 if pending_phy_name:
    546                     add_pending_phy()
    547                 pending_phy_name = match_phy.group(1)
    548                 pending_phy_bands = []
    549                 pending_phy_modes = []
    550                 pending_phy_commands = []
    551                 pending_phy_features = []
    552                 pending_phy_max_scan_ssids = None
    553                 pending_phy_tx_antennas = 0
    554                 pending_phy_rx_antennas = 0
    555                 pending_phy_support_vht = False
    556                 continue
    557 
    558             match_section = re.match('\s*(\w.*):\s*$', line)
    559             if match_section:
    560                 current_section = match_section.group(1)
    561                 match_band = re.match('Band (\d+)', current_section)
    562                 if match_band:
    563                     current_band = IwBand(num=int(match_band.group(1)),
    564                                           frequencies=[],
    565                                           frequency_flags={},
    566                                           mcs_indices=[])
    567                     pending_phy_bands.append(current_band)
    568                 continue
    569 
    570             # Check for max_scan_ssids. This isn't a section, but it
    571             # also isn't within a section.
    572             match_max_scan_ssids = re.match('\s*max # scan SSIDs: (\d+)',
    573                                             line)
    574             if match_max_scan_ssids and pending_phy_name:
    575                 pending_phy_max_scan_ssids = int(
    576                     match_max_scan_ssids.group(1))
    577                 continue
    578 
    579             if (current_section == 'Supported interface modes' and
    580                 pending_phy_name):
    581                 mode_match = re.search('\* (\w+)', line)
    582                 if mode_match:
    583                     pending_phy_modes.append(mode_match.group(1))
    584                     continue
    585 
    586             if current_section == 'Supported commands' and pending_phy_name:
    587                 command_match = re.search('\* (\w+)', line)
    588                 if command_match:
    589                     pending_phy_commands.append(command_match.group(1))
    590                     continue
    591 
    592             if (current_section is not None and
    593                 current_section.startswith('VHT Capabilities') and
    594                 pending_phy_name):
    595                 pending_phy_support_vht = True
    596                 continue
    597 
    598             match_avail_antennas = re.match('\s*Available Antennas: TX (\S+)'
    599                                             ' RX (\S+)', line)
    600             if match_avail_antennas and pending_phy_name:
    601                 pending_phy_tx_antennas = int(
    602                         match_avail_antennas.group(1), 16)
    603                 pending_phy_rx_antennas = int(
    604                         match_avail_antennas.group(2), 16)
    605                 continue
    606 
    607             match_device_support = re.match('\s*Device supports (.*)\.', line)
    608             if match_device_support and pending_phy_name:
    609                 pending_phy_features.append(match_device_support.group(1))
    610                 continue
    611 
    612             if not all([current_band, pending_phy_name,
    613                         line.startswith('\t')]):
    614                 continue
    615 
    616             # E.g.
    617             # * 2412 MHz [1] (20.0 dBm)
    618             # * 2467 MHz [12] (20.0 dBm) (passive scan)
    619             # * 2472 MHz [13] (disabled)
    620             # * 5260 MHz [52] (19.0 dBm) (no IR, radar detection)
    621             match_chan_info = re.search(
    622                 r'(?P<frequency>\d+) MHz'
    623                 r' (?P<chan_num>\[\d+\])'
    624                 r'(?: \((?P<tx_power_limit>[0-9.]+ dBm)\))?'
    625                 r'(?: \((?P<flags>[a-zA-Z, ]+)\))?', line)
    626             if match_chan_info:
    627                 frequency = int(match_chan_info.group('frequency'))
    628                 current_band.frequencies.append(frequency)
    629                 flags_string = match_chan_info.group('flags')
    630                 if flags_string:
    631                     current_band.frequency_flags[frequency] = frozenset(
    632                         flags_string.split(','))
    633                 else:
    634                     # Populate the dict with an empty set, to make
    635                     # things uniform for client code.
    636                     current_band.frequency_flags[frequency] = frozenset()
    637                 continue
    638 
    639             # re_mcs needs to match something like:
    640             # HT TX/RX MCS rate indexes supported: 0-15, 32
    641             if re.search('HT TX/RX MCS rate indexes supported: ', line):
    642                 rate_string = line.split(':')[1].strip()
    643                 for piece in rate_string.split(','):
    644                     if piece.find('-') > 0:
    645                         # Must be a range like '  0-15'
    646                         begin, end = piece.split('-')
    647                         for index in range(int(begin), int(end) + 1):
    648                             current_band.mcs_indices.append(index)
    649                     else:
    650                         # Must be a single rate like '32   '
    651                         current_band.mcs_indices.append(int(piece))
    652         if pending_phy_name:
    653             add_pending_phy()
    654         return all_phys
    655 
    656 
    657     def remove_interface(self, interface, ignore_status=False):
    658         """
    659         Remove a WiFi interface from a PHY.
    660 
    661         @param interface: string name of interface (e.g. mon0)
    662         @param ignore_status: boolean True iff we should ignore failures
    663                 to remove the interface.
    664 
    665         """
    666         self._run('%s dev %s del' % (self._command_iw, interface),
    667                   ignore_status=ignore_status)
    668 
    669 
    670     def determine_security(self, supported_securities):
    671         """Determines security from the given list of supported securities.
    672 
    673         @param supported_securities: list of supported securities from scan
    674 
    675         """
    676         if not supported_securities:
    677             security = SECURITY_OPEN
    678         elif len(supported_securities) == 1:
    679             security = supported_securities[0]
    680         else:
    681             security = SECURITY_MIXED
    682         return security
    683 
    684 
    685     def scan(self, interface, frequencies=(), ssids=()):
    686         """Performs a scan.
    687 
    688         @param interface: the interface to run the iw command against
    689         @param frequencies: list of int frequencies in Mhz to scan.
    690         @param ssids: list of string SSIDs to send probe requests for.
    691 
    692         @returns a list of IwBss namedtuples; None if the scan fails
    693 
    694         """
    695         scan_result = self.timed_scan(interface, frequencies, ssids)
    696         if scan_result is None:
    697             return None
    698         return scan_result.bss_list
    699 
    700 
    701     def timed_scan(self, interface, frequencies=(), ssids=()):
    702         """Performs a timed scan.
    703 
    704         @param interface: the interface to run the iw command against
    705         @param frequencies: list of int frequencies in Mhz to scan.
    706         @param ssids: list of string SSIDs to send probe requests for.
    707 
    708         @returns a IwTimedScan namedtuple; None if the scan fails
    709 
    710         """
    711         freq_param = ''
    712         if frequencies:
    713             freq_param = ' freq %s' % ' '.join(map(str, frequencies))
    714         ssid_param = ''
    715         if ssids:
    716            ssid_param = ' ssid "%s"' % '" "'.join(ssids)
    717 
    718         iw_command = '%s dev %s scan%s%s' % (self._command_iw,
    719                 interface, freq_param, ssid_param)
    720         command = IW_TIME_COMMAND_FORMAT % iw_command
    721         scan = self._run(command, ignore_status=True)
    722         if scan.exit_status != 0:
    723             # The device was busy
    724             logging.debug('scan exit_status: %d', scan.exit_status)
    725             return None
    726         if not scan.stdout:
    727             raise error.TestFail('Missing scan parse time')
    728 
    729         if scan.stdout.startswith(IW_TIME_COMMAND_OUTPUT_START):
    730             logging.debug('Empty scan result')
    731             bss_list = []
    732         else:
    733             bss_list = self._parse_scan_results(scan.stdout)
    734         scan_time = self._parse_scan_time(scan.stdout)
    735         return IwTimedScan(scan_time, bss_list)
    736 
    737 
    738     def scan_dump(self, interface):
    739         """Dump the contents of the scan cache.
    740 
    741         Note that this does not trigger a scan.  Instead, it returns
    742         the kernel's idea of what BSS's are currently visible.
    743 
    744         @param interface: the interface to run the iw command against
    745 
    746         @returns a list of IwBss namedtuples; None if the scan fails
    747 
    748         """
    749         result = self._run('%s dev %s scan dump' % (self._command_iw,
    750                                                     interface))
    751         return self._parse_scan_results(result.stdout)
    752 
    753 
    754     def set_tx_power(self, interface, power):
    755         """
    756         Set the transmission power for an interface.
    757 
    758         @param interface: string name of interface to set Tx power on.
    759         @param power: string power parameter. (e.g. 'auto').
    760 
    761         """
    762         self._run('%s dev %s set txpower %s' %
    763                   (self._command_iw, interface, power))
    764 
    765 
    766     def set_freq(self, interface, freq):
    767         """
    768         Set the frequency for an interface.
    769 
    770         @param interface: string name of interface to set frequency on.
    771         @param freq: int frequency
    772 
    773         """
    774         self._run('%s dev %s set freq %d' %
    775                   (self._command_iw, interface, freq))
    776 
    777 
    778     def set_regulatory_domain(self, domain_string):
    779         """
    780         Set the regulatory domain of the current machine.  Note that
    781         the regulatory change happens asynchronously to the exit of
    782         this function.
    783 
    784         @param domain_string: string regulatory domain name (e.g. 'US').
    785 
    786         """
    787         self._run('%s reg set %s' % (self._command_iw, domain_string))
    788 
    789 
    790     def get_regulatory_domain(self):
    791         """
    792         Get the regulatory domain of the current machine.
    793 
    794         @returns a string containing the 2-letter regulatory domain name
    795             (e.g. 'US').
    796 
    797         """
    798         output = self._run('%s reg get' % self._command_iw).stdout
    799         m = re.match('^country (..):', output)
    800         if not m:
    801             return None
    802         return m.group(1)
    803 
    804 
    805     def wait_for_scan_result(self, interface, bsses=(), ssids=(),
    806                              timeout_seconds=30, wait_for_all=False):
    807         """Returns a list of IWBSS objects for given list of bsses or ssids.
    808 
    809         This method will scan for a given timeout and return all of the networks
    810         that have a matching ssid or bss.  If wait_for_all is true and all
    811         networks are not found within the given timeout an empty list will
    812         be returned.
    813 
    814         @param interface: which interface to run iw against
    815         @param bsses: a list of BSS strings
    816         @param ssids: a list of ssid strings
    817         @param timeout_seconds: the amount of time to wait in seconds
    818         @param wait_for_all: True to wait for all listed bsses or ssids; False
    819                              to return if any of the networks were found
    820 
    821         @returns a list of IwBss collections that contain the given bss or ssid;
    822             if the scan is empty or returns an error code None is returned.
    823 
    824         """
    825         start_time = time.time()
    826         scan_failure_attempts = 0
    827         logging.info('Performing a scan with a max timeout of %d seconds.',
    828                      timeout_seconds)
    829         remaining_bsses = copy.copy(bsses)
    830         remaining_ssids = copy.copy(ssids)
    831         while time.time() - start_time < timeout_seconds:
    832             scan_results = self.scan(interface)
    833             if scan_results is None or len(scan_results) == 0:
    834                 scan_failure_attempts += 1
    835                 # Allow in-progress scan to complete
    836                 time.sleep(5)
    837                 # If the in-progress scan takes more than 30 seconds to
    838                 # complete it will most likely never complete; abort.
    839                 # See crbug.com/309148.
    840                 if scan_failure_attempts > 5:
    841                     logging.error('Scan failed to run, see debug log for '
    842                                   'error code.')
    843                     return None
    844                 continue
    845             scan_failure_attempts = 0
    846             matching_iwbsses = set()
    847             for iwbss in scan_results:
    848               if iwbss.bss in bsses and len(remaining_bsses) > 0:
    849                     remaining_bsses.remove(iwbss.bss)
    850                     matching_iwbsses.add(iwbss)
    851               if iwbss.ssid in ssids and len(remaining_ssids) > 0:
    852                     remaining_ssids.remove(iwbss.ssid)
    853                     matching_iwbsses.add(iwbss)
    854             if wait_for_all:
    855                 if len(remaining_bsses) == 0 and len(remaining_ssids) == 0:
    856                     return list(matching_iwbsses)
    857             else:
    858                 if len(matching_iwbsses) > 0:
    859                     return list(matching_iwbsses)
    860 
    861 
    862         if scan_failure_attempts > 0:
    863             return None
    864         # The SSID wasn't found, but the device is fine.
    865         return list()
    866 
    867 
    868     def wait_for_link(self, interface, timeout_seconds=10):
    869         """Waits until a link completes on |interface|.
    870 
    871         @param interface: which interface to run iw against.
    872         @param timeout_seconds: the amount of time to wait in seconds.
    873 
    874         @returns True if link was established before the timeout.
    875 
    876         """
    877         start_time = time.time()
    878         while time.time() - start_time < timeout_seconds:
    879             link_results = self._run('%s dev %s link' %
    880                                      (self._command_iw, interface))
    881             if 'Not connected' not in link_results.stdout:
    882                 return True
    883             time.sleep(1)
    884         return False
    885 
    886 
    887     def set_antenna_bitmap(self, phy, tx_bitmap, rx_bitmap):
    888         """Set antenna chain mask on given phy (radio).
    889 
    890         This function will set the antennas allowed to use for TX and
    891         RX on the |phy| based on the |tx_bitmap| and |rx_bitmap|.
    892         This command is only allowed when the interfaces on the phy are down.
    893 
    894         @param phy: phy name
    895         @param tx_bitmap: bitmap of allowed antennas to use for TX
    896         @param rx_bitmap: bitmap of allowed antennas to use for RX
    897 
    898         """
    899         command = '%s phy %s set antenna %d %d' % (self._command_iw, phy,
    900                                                    tx_bitmap, rx_bitmap)
    901         self._run(command)
    902 
    903 
    904     def get_event_logger(self):
    905         """Create and return a IwEventLogger object.
    906 
    907         @returns a IwEventLogger object.
    908 
    909         """
    910         local_file = IW_LOCAL_EVENT_LOG_FILE % (self._log_id)
    911         self._log_id += 1
    912         return iw_event_logger.IwEventLogger(self._host, self._command_iw,
    913                                              local_file)
    914 
    915 
    916     def vht_supported(self):
    917         """Returns True if VHT is supported; False otherwise."""
    918         result = self._run('%s list' % self._command_iw).stdout
    919         if 'VHT Capabilities' in result:
    920             return True
    921         return False
    922 
    923 
    924     def frequency_supported(self, frequency):
    925         """Returns True if the given frequency is supported; False otherwise.
    926 
    927         @param frequency: int Wifi frequency to check if it is supported by
    928                           DUT.
    929         """
    930         phys = self.list_phys()
    931         for phy in phys:
    932             for band in phy.bands:
    933                 if frequency in band.frequencies:
    934                     return True
    935         return False
    936 
    937 
    938     def get_fragmentation_threshold(self, phy):
    939         """Returns the fragmentation threshold for |phy|.
    940 
    941         @param phy: phy name
    942         """
    943         ret = self._run('%s phy %s info' % (self._command_iw, phy))
    944         frag_regex = r'^\s+Fragmentation threshold:\s+([0-9]+)$'
    945         match = re.search(frag_regex, ret.stdout, re.MULTILINE)
    946 
    947         if match:
    948             return int(match.group(1))
    949 
    950         return None
    951