1# Lint as: python2, python3
2# Copyright 2019 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9
10import collections
11import logging
12import operator
13import re
14from six.moves import map
15from six.moves import range
16
17from autotest_lib.client.common_lib import error
18from autotest_lib.client.common_lib import utils
19from autotest_lib.client.common_lib.cros.network import iw_event_logger
20
21# These must mirror the values in 'iw list' output.
22CHAN_FLAG_DISABLED = 'disabled'
23CHAN_FLAG_NO_IR = 'no IR'
24CHAN_FLAG_PASSIVE_SCAN = 'passive scan'
25CHAN_FLAG_RADAR_DETECT = 'radar detection'
26DEV_MODE_AP = 'AP'
27DEV_MODE_IBSS = 'IBSS'
28DEV_MODE_MONITOR = 'monitor'
29DEV_MODE_MESH_POINT = 'mesh point'
30DEV_MODE_STATION = 'managed'
31SUPPORTED_DEV_MODES = (DEV_MODE_AP, DEV_MODE_IBSS, DEV_MODE_MONITOR,
32                       DEV_MODE_MESH_POINT, DEV_MODE_STATION)
33
34class _PrintableWidth:
35    """Printable width constant objects used by packet_capturer."""
36    def __init__(self, name):
37        self._name = name
38
39    def __repr__(self):
40        return '\'%s\'' % self._name
41
42    def __str__(self):
43        return self._name
44
45WIDTH_HT20 = _PrintableWidth('HT20')
46WIDTH_HT40_PLUS = _PrintableWidth('HT40+')
47WIDTH_HT40_MINUS = _PrintableWidth('HT40-')
48WIDTH_VHT80 = _PrintableWidth('VHT80')
49WIDTH_VHT160 = _PrintableWidth('VHT160')
50WIDTH_VHT80_80 = _PrintableWidth('VHT80+80')
51
52VHT160_CENTER_CHANNELS = ('50','114')
53
54SECURITY_OPEN = 'open'
55SECURITY_WEP = 'wep'
56SECURITY_WPA = 'wpa'
57SECURITY_WPA2 = 'wpa2'
58# Mixed mode security is WPA2/WPA
59SECURITY_MIXED = 'mixed'
60
61# Table of lookups between the output of item 'secondary channel offset:' from
62# iw <device> scan to constants.
63
64HT_TABLE = {'no secondary': WIDTH_HT20,
65            'above': WIDTH_HT40_PLUS,
66            'below': WIDTH_HT40_MINUS}
67
68IwBand = collections.namedtuple(
69    'Band', ['num', 'frequencies', 'frequency_flags', 'mcs_indices'])
70IwBss = collections.namedtuple('IwBss', ['bss', 'frequency', 'ssid', 'security',
71                                         'width', 'signal'])
72IwNetDev = collections.namedtuple('IwNetDev', ['phy', 'if_name', 'if_type'])
73IwTimedScan = collections.namedtuple('IwTimedScan', ['time', 'bss_list'])
74
75# The fields for IwPhy are as follows:
76#   name: string name of the phy, such as "phy0"
77#   bands: list of IwBand objects.
78#   modes: List of strings containing interface modes supported, such as "AP".
79#   commands: List of strings containing nl80211 commands supported, such as
80#          "authenticate".
81#   features: List of strings containing nl80211 features supported, such as
82#          "T-DLS".
83#   max_scan_ssids: Maximum number of SSIDs which can be scanned at once.
84IwPhy = collections.namedtuple(
85    'Phy', ['name', 'bands', 'modes', 'commands', 'features',
86            'max_scan_ssids', 'avail_tx_antennas', 'avail_rx_antennas',
87            'supports_setting_antenna_mask', 'support_vht'])
88
89DEFAULT_COMMAND_IW = 'iw'
90
91# Redirect stderr to stdout on Cros since adb commands cannot distinguish them
92# on Brillo.
93IW_TIME_COMMAND_FORMAT = '(time -p %s) 2>&1'
94IW_TIME_COMMAND_OUTPUT_START = 'real'
95
96IW_LINK_KEY_BEACON_INTERVAL = 'beacon int'
97IW_LINK_KEY_DTIM_PERIOD = 'dtim period'
98IW_LINK_KEY_FREQUENCY = 'freq'
99IW_LINK_KEY_SIGNAL = 'signal'
100IW_LINK_KEY_RX_BITRATE = 'rx bitrate'
101IW_LINK_KEY_RX_DROPS = 'rx drop misc'
102IW_LINK_KEY_RX_PACKETS = 'rx packets'
103IW_LINK_KEY_TX_BITRATE = 'tx bitrate'
104IW_LINK_KEY_TX_FAILURES = 'tx failed'
105IW_LINK_KEY_TX_PACKETS = 'tx packets'
106IW_LINK_KEY_TX_RETRIES = 'tx retries'
107IW_LOCAL_EVENT_LOG_FILE = './debug/iw_event_%d.log'
108
109# Strings from iw/util.c describing supported HE features
110HE_MAC_PLUS_HTC_HE = '+HTC HE Supported'
111HE_MAC_TWT_REQUESTER = 'TWT Requester'
112HE_MAC_TWT_RESPONDER = 'TWT Responder'
113HE_MAC_DYNAMIC_BA_FRAGMENTATION = 'Dynamic BA Fragementation Level'
114HE_MAC_MAX_MSDUS = 'Maximum number of MSDUS Fragments'
115HE_MAC_MIN_PAYLOAD_128 = 'Minimum Payload size of 128 bytes'
116HE_MAC_TRIGGER_FRAME_PADDING = 'Trigger Frame MAC Padding Duration'
117HE_MAC_MULTI_TID_AGGREGATION = 'Multi-TID Aggregation Support'
118HE_MAC_ALL_ACK = 'All Ack'
119HE_MAC_TRS = 'TRS'
120HE_MAC_BSR = 'BSR'
121HE_MAC_TWT_BROADCAST = 'Broadcast TWT'
122HE_MAC_32_BIT_BA_BITMAP = '32-bit BA Bitmap'
123HE_MAC_MU_CASCADING = 'MU Cascading'
124HE_MAC_ACK_AGGREGATION = 'Ack-Enabled Aggregation'
125HE_MAC_OM_CONTROL = 'OM Control'
126HE_MAC_OFDMA_RA = 'OFDMA RA'
127HE_MAC_MAX_AMPDU_LENGTH_EXPONENT = 'Maximum A-MPDU Length Exponent'
128HE_MAC_AMSDU_FRAGMENTATION = 'A-MSDU Fragmentation'
129HE_MAC_FLEXIBLE_TWT = 'Flexible TWT Scheduling'
130HE_MAC_RX_CONTROL_FRAME_TO_MULTIBSS = 'RX Control Frame to MultiBSS'
131HE_MAC_BSRP_BQRP_AMPDU_AGGREGATION = 'BSRP BQRP A-MPDU Aggregation'
132HE_MAC_QTP = 'QTP'
133HE_MAC_BQR = 'BQR'
134HE_MAC_SRP_RESPONDER_ROLE = 'SRP Responder Role'
135HE_MAC_NDP_FEEDBACK_REPORT = 'NDP Feedback Report'
136HE_MAC_OPS = 'OPS'
137HE_MAC_AMSDU_IN_AMPDU = 'A-MSDU in A-MPDU'
138HE_MAC_MULTI_TID_AGGREGATION_TX = 'Multi-TID Aggregation TX'
139HE_MAC_SUBCHANNEL_SELECTIVE = 'HE Subchannel Selective Transmission'
140HE_MAC_UL_2X966_TONE_RU = 'UL 2x996-Tone RU'
141HE_MAC_OM_CONTROL_DISABLE_RX = 'OM Control UL MU Data Disable RX'
142
143HE_PHY_24HE40 = 'HE40/2.4GHz'
144HE_PHY_5HE40_80 = 'HE40/HE80/5GHz'
145HE_PHY_5HE160 = 'HE160/5GHz'
146HE_PHY_5HE160_80_80 = 'HE160/HE80+80/5GHz'
147HE_PHY_242_TONE_RU_24 = '242 tone RUs/2.4GHz'
148HE_PHY_242_TONE_RU_5 = '242 tone RUs/5GHz'
149HE_PHY_PUNCTURED_PREAMBLE_RX = 'Punctured Preamble RX'
150HE_PHY_DEVICE_CLASS = 'Device Class'
151HE_PHY_LDPC_CODING_IN_PAYLOAD = 'LDPC Coding in Payload'
152HE_PHY_HE_SU_PPDU_1X_HE_LTF_08_GI = 'HE SU PPDU with 1x HE-LTF and 0.8us GI'
153HE_PHY_HE_MIDAMBLE_RX_MAX_NSTS = 'Midamble Rx Max NSTS'
154HE_PHY_NDP_4X_HE_LTF_32_GI = 'NDP with 4x HE-LTF and 3.2us GI'
155HE_PHY_STBC_TX_LEQ_80 = 'STBC Tx <= 80MHz'
156HE_PHY_STBC_RX_LEQ_80 = 'STBC Rx <= 80MHz'
157HE_PHY_DOPPLER_TX = 'Doppler Tx'
158HE_PHY_DOPPLER_RX = 'Doppler Rx'
159HE_PHY_FULL_BAND_UL_MU_MIMO = 'Full Bandwidth UL MU-MIMO'
160HE_PHY_PART_BAND_UL_MU_MIMO = 'Partial Bandwidth UL MU-MIMO'
161HE_PHY_DCM_MAX_CONSTELLATION = 'DCM Max Constellation'
162HE_PHY_DCM_MAX_NSS_TX = 'DCM Max NSS Tx'
163HE_PHY_DCM_MAX_CONSTELLATION_RX = 'DCM Max Constellation Rx'
164HE_PHY_DCM_MAX_NSS_RX = 'DCM Max NSS Rx'
165HE_PHY_RX_MU_PPDU_FROM_NON_AP = 'Rx HE MU PPDU from Non-AP STA'
166HE_PHY_SU_BEAMFORMER = 'SU Beamformer'
167HE_PHY_SU_BEAMFORMEE = 'SU Beamformee'
168HE_PHY_MU_BEAMFORMER = 'MU Beamformer'
169HE_PHY_BEAMFORMEE_STS_LEQ_80 = 'Beamformee STS <= 80Mhz'
170HE_PHY_BEAMFORMEE_STS_GT_80 = 'Beamformee STS > 80Mhz'
171HE_PHY_SOUNDING_DIMENSIONS_LEQ_80 = 'Sounding Dimensions <= 80Mhz'
172HE_PHY_SOUNDING_DIMENSIONS_GT_80 = 'Sounding Dimensions > 80Mhz'
173HE_PHY_NG_EQ_16_SU_FB = 'Ng = 16 SU Feedback'
174HE_PHY_NG_EQ_16_MU_FB = 'Ng = 16 MU Feedback'
175HE_PHY_CODEBOOK_SIZE_SU_FB = 'Codebook Size SU Feedback'
176HE_PHY_CODEBOOK_SIZE_MU_FB = 'Codebook Size MU Feedback'
177HE_PHY_TRIGGERED_SU_BEAMFORMING_FB = 'Triggered SU Beamforming Feedback'
178HE_PHY_TRIGGERED_MU_BEAMFORMING_FB = 'Triggered MU Beamforming Feedback'
179HE_PHY_TRIGGERED_CQI_FB = 'Triggered CQI Feedback'
180HE_PHY_PART_BAND_EXT_RANGE = 'Partial Bandwidth Extended Range'
181HE_PHY_PART_BAND_DL_MU_MIMO = 'Partial Bandwidth DL MU-MIMO'
182HE_PHY_PPE_THRESHOLD = 'PPE Threshold Present'
183HE_PHY_SRP_SR = 'SRP-based SR'
184HE_PHY_POWER_BOOST_FACTOR_AR = 'Power Boost Factor ar'
185HE_PHY_SU_PPDU_4X_HE_LTF_08_GI = 'HE SU PPDU & HE PPDU 4x HE-LTF 0.8us GI'
186HE_PHY_MAX_NC = 'Max NC'
187HE_PHY_STBC_TX_GT_80 = 'STBC Tx > 80MHz'
188HE_PHY_STBC_RX_GT_80 = 'STBC Rx > 80MHz'
189HE_PHY_ER_SU_PPDU_4X_HE_LTF_08_GI = 'HE ER SU PPDU 4x HE-LTF 0.8us GI'
190HE_PHY_20_IN_44_PPDU_24 = '20MHz in 40MHz HE PPDU 2.4GHz'
191HE_PHY_20_IN_160_80_80 = '20MHz in 160/80+80MHz HE PPDU'
192HE_PHY_80_IN_160_80_80 = '80MHz in 160/80+80MHz HE PPDU'
193HE_PHY_ER_SU_PPDU_1X_HE_LTF_08_GI = 'HE ER SU PPDU 1x HE-LTF 0.8us GI'
194HE_PHY_MIDAMBLE_RX_2X_AND_1X_HE_LTF = 'Midamble Rx 2x & 1x HE-LTF'
195HE_PHY_DCM_MAX_BW = 'DCM Max BW'
196HE_PHY_LONGER_THAN_16HE_OFDM_SYM = 'Longer Than 16HE SIG-B OFDM Symbols'
197HE_PHY_NON_TRIGGERED_CQI_FB = 'Non-Triggered CQI Feedback'
198HE_PHY_TX_1024_QAM = 'TX 1024-QAM'
199HE_PHY_RX_1024_QAM = 'RX 1024-QAM'
200HE_PHY_RX_FULL_BW_SU_USING_MU_COMPRESSION_SIGB = \
201        'RX Full BW SU Using HE MU PPDU with Compression SIGB'
202HE_PHY_RX_FULL_BW_SU_USING_MU_NON_COMPRESSION_SIGB = \
203        'RX Full BW SU Using HE MU PPDU with Non-Compression SIGB'
204
205
206def _get_all_link_keys(link_information):
207    """Parses link or station dump output for link key value pairs.
208
209    Link or station dump information is in the format below:
210
211    Connected to 74:e5:43:10:4f:c0 (on wlan0)
212          SSID: PMKSACaching_4m9p5_ch1
213          freq: 5220
214          RX: 5370 bytes (37 packets)
215          TX: 3604 bytes (15 packets)
216          signal: -59 dBm
217          tx bitrate: 13.0 MBit/s MCS 1
218
219          bss flags:      short-slot-time
220          dtim period:    5
221          beacon int:     100
222
223    @param link_information: string containing the raw link or station dump
224        information as reported by iw. Note that this parsing assumes a single
225        entry, in the case of multiple entries (e.g. listing stations from an
226        AP, or listing mesh peers), the entries must be split on a per
227        peer/client basis before this parsing operation.
228    @return a dictionary containing all the link key/value pairs.
229
230    """
231    link_key_value_pairs = {}
232    keyval_regex = re.compile(r'^\s+(.*):\s+(.*)$')
233    for link_key in link_information.splitlines()[1:]:
234        match = re.search(keyval_regex, link_key)
235        if match:
236            # Station dumps can contain blank lines.
237            link_key_value_pairs[match.group(1)] = match.group(2)
238    return link_key_value_pairs
239
240
241def _extract_bssid(link_information, interface_name, station_dump=False):
242    """Get the BSSID that |interface_name| is associated with.
243
244    See doc for _get_all_link_keys() for expected format of the station or link
245    information entry.
246
247    @param link_information: string containing the raw link or station dump
248        information as reported by iw. Note that this parsing assumes a single
249        entry, in the case of multiple entries (e.g. listing stations from an AP
250        or listing mesh peers), the entries must be split on a per peer/client
251        basis before this parsing operation.
252    @param interface_name: string name of interface (e.g. 'wlan0').
253    @param station_dump: boolean indicator of whether the link information is
254        from a 'station dump' query. If False, it is assumed the string is from
255        a 'link' query.
256    @return string bssid of the current association, or None if no matching
257        association information is found.
258
259    """
260    # We're looking for a line like this when parsing the output of a 'link'
261    # query:
262    #   Connected to 04:f0:21:03:7d:bb (on wlan0)
263    # We're looking for a line like this when parsing the output of a
264    # 'station dump' query:
265    #   Station 04:f0:21:03:7d:bb (on mesh-5000mhz)
266    identifier = 'Station' if station_dump else 'Connected to'
267    search_re = r'%s ([0-9a-fA-F:]{17}) \(on %s\)' % (identifier,
268                                                      interface_name)
269    match = re.match(search_re, link_information)
270    if match is None:
271        return None
272    return match.group(1)
273
274
275class IwRunner(object):
276    """Defines an interface to the 'iw' command."""
277
278
279    def __init__(self, remote_host=None, command_iw=DEFAULT_COMMAND_IW):
280        self._run = utils.run
281        self._host = remote_host
282        if remote_host:
283            self._run = remote_host.run
284        self._command_iw = command_iw
285        self._log_id = 0
286
287
288    def _parse_scan_results(self, output):
289        """Parse the output of the 'scan' and 'scan dump' commands.
290
291        Here is an example of what a single network would look like for
292        the input parameter.  Some fields have been removed in this example:
293          BSS 00:11:22:33:44:55(on wlan0)
294          freq: 2447
295          beacon interval: 100 TUs
296          signal: -46.00 dBm
297          Information elements from Probe Response frame:
298          SSID: my_open_network
299          Extended supported rates: 24.0 36.0 48.0 54.0
300          HT capabilities:
301          Capabilities: 0x0c
302          HT20
303          HT operation:
304          * primary channel: 8
305          * secondary channel offset: no secondary
306          * STA channel width: 20 MHz
307          RSN: * Version: 1
308          * Group cipher: CCMP
309          * Pairwise ciphers: CCMP
310          * Authentication suites: PSK
311          * Capabilities: 1-PTKSA-RC 1-GTKSA-RC (0x0000)
312
313        @param output: string command output.
314
315        @returns a list of IwBss namedtuples; None if the scan fails
316
317        """
318        bss = None
319        frequency = None
320        ssid = None
321        ht = None
322        vht = None
323        signal = None
324        security = None
325        supported_securities = []
326        bss_list = []
327        # TODO(crbug.com/1032892): The parsing logic here wasn't really designed
328        # for the presence of multiple information elements like HT, VHT, and
329        # (eventually) HE. We should eventually update it to check that we are
330        # in the right section (e.g., verify the '* channel width' match is a
331        # match in the VHT section and not a different section). Also, we should
332        # probably add in VHT20, and VHT40 whenever we finish this bug.
333        for line in output.splitlines():
334            line = line.strip()
335            bss_match = re.match('BSS ([0-9a-f:]+)', line)
336            if bss_match:
337                if bss != None:
338                    security = self.determine_security(supported_securities)
339                    iwbss = IwBss(bss, frequency, ssid, security,
340                                  vht if vht else ht, signal)
341                    bss_list.append(iwbss)
342                    bss = frequency = ssid = security = ht = vht = None
343                    supported_securities = []
344                bss = bss_match.group(1)
345            if line.startswith('freq:'):
346                frequency = int(line.split()[1])
347            if line.startswith('signal:'):
348                signal = float(line.split()[1])
349            if line.startswith('SSID: '):
350                _, ssid = line.split(': ', 1)
351            if line.startswith('* secondary channel offset'):
352                ht = HT_TABLE[line.split(':')[1].strip()]
353            # Checking for the VHT channel width based on IEEE 802.11-2016
354            # Table 9-252.
355            if line.startswith('* channel width:'):
356                chan_width_subfield = line.split(':')[1].strip()[0]
357                if chan_width_subfield == '1':
358                    vht = WIDTH_VHT80
359                # 2 and 3 are deprecated but are included here for older APs.
360                if chan_width_subfield == '2':
361                    vht = WIDTH_VHT160
362                if chan_width_subfield == '3':
363                    vht = WIDTH_VHT80_80
364            if line.startswith('* center freq segment 2:'):
365                center_chan_two = line.split(':')[1].strip()
366                if vht == WIDTH_VHT80:
367                    if center_chan_two in VHT160_CENTER_CHANNELS:
368                        vht = WIDTH_VHT160
369                    elif center_chan_two != '0':
370                        vht = WIDTH_VHT80_80
371            if line.startswith('WPA'):
372                supported_securities.append(SECURITY_WPA)
373            if line.startswith('RSN'):
374                supported_securities.append(SECURITY_WPA2)
375        security = self.determine_security(supported_securities)
376        bss_list.append(IwBss(bss, frequency, ssid, security,
377                              vht if vht else ht, signal))
378        return bss_list
379
380
381    def _parse_scan_time(self, output):
382        """
383        Parse the scan time in seconds from the output of the 'time -p "scan"'
384        command.
385
386        'time -p' Command output format is below:
387        real     0.01
388        user     0.01
389        sys      0.00
390
391        @param output: string command output.
392
393        @returns float time in seconds.
394
395        """
396        output_lines = output.splitlines()
397        for line_num, line in enumerate(output_lines):
398            line = line.strip()
399            if (line.startswith(IW_TIME_COMMAND_OUTPUT_START) and
400                output_lines[line_num + 1].startswith('user') and
401                output_lines[line_num + 2].startswith('sys')):
402                return float(line.split()[1])
403        raise error.TestFail('Could not parse scan time.')
404
405
406    def add_interface(self, phy, interface, interface_type):
407        """
408        Add an interface to a WiFi PHY.
409
410        @param phy: string name of PHY to add an interface to.
411        @param interface: string name of interface to add.
412        @param interface_type: string type of interface to add (e.g. 'monitor').
413
414        """
415        self._run('%s phy %s interface add %s type %s' %
416                  (self._command_iw, phy, interface, interface_type))
417
418
419    def disconnect_station(self, interface):
420        """
421        Disconnect a STA from a network.
422
423        @param interface: string name of interface to disconnect.
424
425        """
426        self._run('%s dev %s disconnect' % (self._command_iw, interface))
427
428
429    def get_current_bssid(self, interface_name):
430        """Get the BSSID that |interface_name| is associated with.
431
432        @param interface_name: string name of interface (e.g. 'wlan0').
433        @return string bssid of our current association, or None.
434
435        """
436        result = self._run('%s dev %s link' %
437                           (self._command_iw, interface_name),
438                           ignore_status=True)
439        if result.exit_status:
440            # See comment in get_link_value.
441            return None
442
443        return _extract_bssid(result.stdout, interface_name)
444
445
446    def get_interface(self, interface_name):
447        """Get full information about an interface given an interface name.
448
449        @param interface_name: string name of interface (e.g. 'wlan0').
450        @return IwNetDev tuple.
451
452        """
453        matching_interfaces = [iw_if for iw_if in self.list_interfaces()
454                                     if iw_if.if_name == interface_name]
455        if len(matching_interfaces) != 1:
456            raise error.TestFail('Could not find interface named %s' %
457                                 interface_name)
458
459        return matching_interfaces[0]
460
461
462    def get_link_value(self, interface, iw_link_key):
463        """Get the value of a link property for |interface|.
464
465        Checks the link using iw, and parses the result to return a link key.
466
467        @param iw_link_key: string one of IW_LINK_KEY_* defined above.
468        @param interface: string desired value of iw link property.
469        @return string containing the corresponding link property value, None
470            if there was a parsing error or the iw command failed.
471
472        """
473        result = self._run('%s dev %s link' % (self._command_iw, interface),
474                           ignore_status=True)
475        if result.exit_status:
476            # When roaming, there is a period of time for mac80211 based drivers
477            # when the driver is 'associated' with an SSID but not a particular
478            # BSS.  This causes iw to return an error code (-2) when attempting
479            # to retrieve information specific to the BSS.  This does not happen
480            # in mwifiex drivers.
481            return None
482        actual_value = _get_all_link_keys(result.stdout).get(iw_link_key)
483        if actual_value is not None:
484            logging.info('Found iw link key %s with value %s.',
485                         iw_link_key, actual_value)
486        return actual_value
487
488
489    def get_station_dump(self, interface):
490        """Gets information about connected peers.
491
492        Returns information about the currently connected peers. When the host
493        is in station mode, it returns a single entry, with information about
494        the link to the AP it is currently connected to. If the host is in mesh
495        or AP mode, it can return multiple entries, one for each connected
496        station, or mesh peer.
497
498        @param interface: string name of interface to get peer information
499            from.
500        @return a list of dictionaries with link information about each
501            connected peer (ordered by peer mac address).
502
503        """
504        result = self._run('%s dev %s station dump' %
505                           (self._command_iw, interface))
506        parts = re.split(r'^Station ', result.stdout, flags=re.MULTILINE)[1:]
507        peer_list_raw = ['Station ' + x for x in parts]
508        parsed_peer_info = []
509
510        for peer in peer_list_raw:
511            peer_link_keys = _get_all_link_keys(peer)
512            rssi_str = peer_link_keys.get(IW_LINK_KEY_SIGNAL, '0')
513            rssi_int = int(rssi_str.split()[0])
514
515            tx_bitrate = peer_link_keys.get(IW_LINK_KEY_TX_BITRATE, '0')
516            tx_failures = int(peer_link_keys.get(IW_LINK_KEY_TX_FAILURES, 0))
517            tx_packets = int(peer_link_keys.get(IW_LINK_KEY_TX_PACKETS, 0))
518            tx_retries = int(peer_link_keys.get(IW_LINK_KEY_TX_RETRIES, 0))
519
520            rx_bitrate = peer_link_keys.get(IW_LINK_KEY_RX_BITRATE, '0')
521            rx_drops = int(peer_link_keys.get(IW_LINK_KEY_RX_DROPS, 0))
522            rx_packets = int(peer_link_keys.get(IW_LINK_KEY_RX_PACKETS, 0))
523
524            mac = _extract_bssid(link_information=peer,
525                                 interface_name=interface,
526                                 station_dump=True)
527
528            # If any of these are missing, they will be None
529            peer_info = {'rssi_int': rssi_int,
530                         'rssi_str': rssi_str,
531                         'tx_bitrate': tx_bitrate,
532                         'tx_failures': tx_failures,
533                         'tx_packets': tx_packets,
534                         'tx_retries': tx_retries,
535                         'rx_bitrate': rx_bitrate,
536                         'rx_drops': rx_drops,
537                         'rx_packets': rx_packets,
538                         'mac': mac}
539
540            # don't evaluate if tx_packets 0
541            if tx_packets:
542                peer_info['tx_retry_rate'] = tx_retries / float(tx_packets)
543                peer_info['tx_failure_rate'] =  tx_failures / float(tx_packets)
544
545            # don't evaluate if rx_packets is 0
546            if rx_packets:
547                peer_info['rx_drop_rate'] = rx_drops / float(rx_packets)
548
549            parsed_peer_info.append(peer_info)
550        return sorted(parsed_peer_info, key=operator.itemgetter('mac'))
551
552
553    def get_operating_mode(self, interface):
554        """Gets the operating mode for |interface|.
555
556        @param interface: string name of interface to get peer information
557            about.
558
559        @return string one of DEV_MODE_* defined above, or None if no mode is
560            found, or if an unsupported mode is found.
561
562        """
563        ret = self._run('%s dev %s info' % (self._command_iw, interface))
564        mode_regex = r'^\s*type (.*)$'
565        match = re.search(mode_regex, ret.stdout, re.MULTILINE)
566        if match:
567            operating_mode = match.group(1)
568            if operating_mode in SUPPORTED_DEV_MODES:
569                return operating_mode
570            logging.warning(
571                'Unsupported operating mode %s found for interface: %s. '
572                'Supported modes: %s', operating_mode, interface,
573                SUPPORTED_DEV_MODES)
574        return None
575
576
577    def get_radio_config(self, interface):
578        """Gets the channel information of a specfic interface using iw.
579
580        @param interface: string name of interface to get radio information
581            from.
582
583        @return dictionary containing the channel information.
584
585        """
586        channel_config = {}
587        ret = self._run('%s dev %s info' % (self._command_iw, interface))
588        channel_config_regex = (r'^\s*channel ([0-9]+) \(([0-9]+) MHz\), '
589                                 'width: ([2,4,8]0) MHz, center1: ([0-9]+) MHz')
590        match = re.search(channel_config_regex, ret.stdout, re.MULTILINE)
591
592        if match:
593            channel_config['number'] = int(match.group(1))
594            channel_config['freq'] = int(match.group(2))
595            channel_config['width'] = int(match.group(3))
596            channel_config['center1_freq'] = int(match.group(4))
597
598        return channel_config
599
600
601    def ibss_join(self, interface, ssid, frequency):
602        """
603        Join a WiFi interface to an IBSS.
604
605        @param interface: string name of interface to join to the IBSS.
606        @param ssid: string SSID of IBSS to join.
607        @param frequency: int frequency of IBSS in Mhz.
608
609        """
610        self._run('%s dev %s ibss join %s %d' %
611                  (self._command_iw, interface, ssid, frequency))
612
613
614    def ibss_leave(self, interface):
615        """
616        Leave an IBSS.
617
618        @param interface: string name of interface to remove from the IBSS.
619
620        """
621        self._run('%s dev %s ibss leave' % (self._command_iw, interface))
622
623
624    def list_interfaces(self, desired_if_type=None):
625        """List WiFi related interfaces on this system.
626
627        @param desired_if_type: string type of interface to filter
628                our returned list of interfaces for (e.g. 'managed').
629
630        @return list of IwNetDev tuples.
631
632        """
633
634        # Parse output in the following format:
635        #
636        #   $ adb shell iw dev
637        #   phy#0
638        #     Unnamed/non-netdev interface
639        #       wdev 0x2
640        #       addr aa:bb:cc:dd:ee:ff
641        #       type P2P-device
642        #     Interface wlan0
643        #       ifindex 4
644        #       wdev 0x1
645        #       addr aa:bb:cc:dd:ee:ff
646        #       ssid Whatever
647        #       type managed
648
649        output = self._run('%s dev' % self._command_iw).stdout
650        interfaces = []
651        phy = None
652        if_name = None
653        if_type = None
654        for line in output.splitlines():
655            m = re.match('phy#([0-9]+)', line)
656            if m:
657                phy = 'phy%d' % int(m.group(1))
658                if_name = None
659                if_type = None
660                continue
661            if not phy:
662                continue
663            m = re.match('[\s]*Interface (.*)', line)
664            if m:
665                if_name = m.group(1)
666                continue
667            if not if_name:
668                continue
669            # Common values for type are 'managed', 'monitor', and 'IBSS'.
670            m = re.match('[\s]*type ([a-zA-Z]+)', line)
671            if m:
672                if_type = m.group(1)
673                interfaces.append(IwNetDev(phy=phy, if_name=if_name,
674                                           if_type=if_type))
675                # One phy may have many interfaces, so don't reset it.
676                if_name = None
677
678        if desired_if_type:
679            interfaces = [interface for interface in interfaces
680                          if interface.if_type == desired_if_type]
681        return interfaces
682
683
684    def list_phys(self):
685        """
686        List WiFi PHYs on the given host.
687
688        @return list of IwPhy tuples.
689
690        """
691        output = self._run('%s list' % self._command_iw).stdout
692
693        pending_phy_name = None
694        current_band = None
695        current_section = None
696        all_phys = []
697
698        def add_pending_phy():
699            """Add the pending phy into |all_phys|."""
700            bands = tuple(IwBand(band.num,
701                                 tuple(band.frequencies),
702                                 dict(band.frequency_flags),
703                                 tuple(band.mcs_indices))
704                          for band in pending_phy_bands)
705            new_phy = IwPhy(pending_phy_name,
706                            bands,
707                            tuple(pending_phy_modes),
708                            tuple(pending_phy_commands),
709                            tuple(pending_phy_features),
710                            pending_phy_max_scan_ssids,
711                            pending_phy_tx_antennas,
712                            pending_phy_rx_antennas,
713                            pending_phy_tx_antennas and pending_phy_rx_antennas,
714                            pending_phy_support_vht)
715            all_phys.append(new_phy)
716
717        for line in output.splitlines():
718            match_phy = re.search('Wiphy (.*)', line)
719            if match_phy:
720                if pending_phy_name:
721                    add_pending_phy()
722                pending_phy_name = match_phy.group(1)
723                pending_phy_bands = []
724                pending_phy_modes = []
725                pending_phy_commands = []
726                pending_phy_features = []
727                pending_phy_max_scan_ssids = None
728                pending_phy_tx_antennas = 0
729                pending_phy_rx_antennas = 0
730                pending_phy_support_vht = False
731                continue
732
733            match_section = re.match('\s*(\w.*):\s*$', line)
734            if match_section:
735                current_section = match_section.group(1)
736                match_band = re.match('Band (\d+)', current_section)
737                if match_band:
738                    current_band = IwBand(num=int(match_band.group(1)),
739                                          frequencies=[],
740                                          frequency_flags={},
741                                          mcs_indices=[])
742                    pending_phy_bands.append(current_band)
743                continue
744
745            # Check for max_scan_ssids. This isn't a section, but it
746            # also isn't within a section.
747            match_max_scan_ssids = re.match('\s*max # scan SSIDs: (\d+)',
748                                            line)
749            if match_max_scan_ssids and pending_phy_name:
750                pending_phy_max_scan_ssids = int(
751                    match_max_scan_ssids.group(1))
752                continue
753
754            if (current_section == 'Supported interface modes' and
755                pending_phy_name):
756                mode_match = re.search('\* (\w+)', line)
757                if mode_match:
758                    pending_phy_modes.append(mode_match.group(1))
759                    continue
760
761            if current_section == 'Supported commands' and pending_phy_name:
762                command_match = re.search('\* (\w+)', line)
763                if command_match:
764                    pending_phy_commands.append(command_match.group(1))
765                    continue
766
767            if (current_section is not None and
768                current_section.startswith('VHT Capabilities') and
769                pending_phy_name):
770                pending_phy_support_vht = True
771                continue
772
773            match_avail_antennas = re.match('\s*Available Antennas: TX (\S+)'
774                                            ' RX (\S+)', line)
775            if match_avail_antennas and pending_phy_name:
776                pending_phy_tx_antennas = int(
777                        match_avail_antennas.group(1), 16)
778                pending_phy_rx_antennas = int(
779                        match_avail_antennas.group(2), 16)
780                continue
781
782            match_device_support = re.match('\s*Device supports (.*)\.', line)
783            if match_device_support and pending_phy_name:
784                pending_phy_features.append(match_device_support.group(1))
785                continue
786
787            if not all([current_band, pending_phy_name,
788                        line.startswith('\t')]):
789                continue
790
791            # E.g.
792            # * 2412 MHz [1] (20.0 dBm)
793            # * 2467 MHz [12] (20.0 dBm) (passive scan)
794            # * 2472 MHz [13] (disabled)
795            # * 5260 MHz [52] (19.0 dBm) (no IR, radar detection)
796            match_chan_info = re.search(
797                r'(?P<frequency>\d+) MHz'
798                r' (?P<chan_num>\[\d+\])'
799                r'(?: \((?P<tx_power_limit>[0-9.]+ dBm)\))?'
800                r'(?: \((?P<flags>[a-zA-Z, ]+)\))?', line)
801            if match_chan_info:
802                frequency = int(match_chan_info.group('frequency'))
803                current_band.frequencies.append(frequency)
804                flags_string = match_chan_info.group('flags')
805                if flags_string:
806                    current_band.frequency_flags[frequency] = frozenset(
807                        flags_string.split(','))
808                else:
809                    # Populate the dict with an empty set, to make
810                    # things uniform for client code.
811                    current_band.frequency_flags[frequency] = frozenset()
812                continue
813
814            # re_mcs needs to match something like:
815            # HT TX/RX MCS rate indexes supported: 0-15, 32
816            if re.search('HT TX/RX MCS rate indexes supported: ', line):
817                rate_string = line.split(':')[1].strip()
818                for piece in rate_string.split(','):
819                    if piece.find('-') > 0:
820                        # Must be a range like '  0-15'
821                        begin, end = piece.split('-')
822                        for index in range(int(begin), int(end) + 1):
823                            current_band.mcs_indices.append(index)
824                    else:
825                        # Must be a single rate like '32   '
826                        current_band.mcs_indices.append(int(piece))
827        if pending_phy_name:
828            add_pending_phy()
829        return all_phys
830
831
832    def remove_interface(self, interface, ignore_status=False):
833        """
834        Remove a WiFi interface from a PHY.
835
836        @param interface: string name of interface (e.g. mon0)
837        @param ignore_status: boolean True iff we should ignore failures
838                to remove the interface.
839
840        """
841        self._run('%s dev %s del' % (self._command_iw, interface),
842                  ignore_status=ignore_status)
843
844
845    def determine_security(self, supported_securities):
846        """Determines security from the given list of supported securities.
847
848        @param supported_securities: list of supported securities from scan
849
850        """
851        if not supported_securities:
852            security = SECURITY_OPEN
853        elif len(supported_securities) == 1:
854            security = supported_securities[0]
855        else:
856            security = SECURITY_MIXED
857        return security
858
859
860    def scan(self, interface, frequencies=(), ssids=()):
861        """Performs a scan.
862
863        @param interface: the interface to run the iw command against
864        @param frequencies: list of int frequencies in Mhz to scan.
865        @param ssids: list of string SSIDs to send probe requests for.
866
867        @returns a list of IwBss namedtuples; None if the scan fails
868
869        """
870        scan_result = self.timed_scan(interface, frequencies, ssids)
871        if scan_result is None:
872            return None
873        return scan_result.bss_list
874
875
876    def timed_scan(self, interface, frequencies=(), ssids=()):
877        """Performs a timed scan.
878
879        @param interface: the interface to run the iw command against
880        @param frequencies: list of int frequencies in Mhz to scan.
881        @param ssids: list of string SSIDs to send probe requests for.
882
883        @returns a IwTimedScan namedtuple; None if the scan fails
884
885        """
886        freq_param = ''
887        if frequencies:
888            freq_param = ' freq %s' % ' '.join(map(str, frequencies))
889        ssid_param = ''
890        if ssids:
891            ssid_param = ' ssid "%s"' % '" "'.join(ssids)
892
893        iw_command = '%s dev %s scan%s%s' % (self._command_iw,
894                interface, freq_param, ssid_param)
895        command = IW_TIME_COMMAND_FORMAT % iw_command
896        scan = self._run(command, ignore_status=True)
897        if scan.exit_status != 0:
898            # The device was busy
899            logging.debug('scan exit_status: %d', scan.exit_status)
900            return None
901        if not scan.stdout:
902            raise error.TestFail('Missing scan parse time')
903
904        if scan.stdout.startswith(IW_TIME_COMMAND_OUTPUT_START):
905            logging.debug('Empty scan result')
906            bss_list = []
907        else:
908            bss_list = self._parse_scan_results(scan.stdout)
909        scan_time = self._parse_scan_time(scan.stdout)
910        return IwTimedScan(scan_time, bss_list)
911
912
913    def scan_dump(self, interface):
914        """Dump the contents of the scan cache.
915
916        Note that this does not trigger a scan.  Instead, it returns
917        the kernel's idea of what BSS's are currently visible.
918
919        @param interface: the interface to run the iw command against
920
921        @returns a list of IwBss namedtuples; None if the scan fails
922
923        """
924        result = self._run('%s dev %s scan dump' % (self._command_iw,
925                                                    interface))
926        return self._parse_scan_results(result.stdout)
927
928
929    def set_tx_power(self, interface, power):
930        """
931        Set the transmission power for an interface.
932
933        @param interface: string name of interface to set Tx power on.
934        @param power: string power parameter. (e.g. 'auto').
935
936        """
937        self._run('%s dev %s set txpower %s' %
938                  (self._command_iw, interface, power))
939
940
941    def set_freq(self, interface, freq):
942        """
943        Set the frequency for an interface.
944
945        @param interface: string name of interface to set frequency on.
946        @param freq: int frequency
947
948        """
949        self._run('%s dev %s set freq %d' %
950                  (self._command_iw, interface, freq))
951
952
953    def set_regulatory_domain(self, domain_string):
954        """
955        Set the regulatory domain of the current machine.  Note that
956        the regulatory change happens asynchronously to the exit of
957        this function.
958
959        @param domain_string: string regulatory domain name (e.g. 'US').
960
961        """
962        self._run('%s reg set %s' % (self._command_iw, domain_string))
963
964
965    def get_regulatory_domain(self, wiphy=None):
966        """
967        Get the regulatory domain of the current machine.
968
969        @param wiphy: string; if provided, check for the phy-specific domain,
970                      rather than the global one.
971
972        @returns a string containing the 2-letter regulatory domain name
973            (e.g. 'US').
974
975        """
976        cmd = self._command_iw
977        if wiphy:
978            cmd += ' phy ' + wiphy
979        cmd += ' reg get'
980        output = self._run(cmd).stdout
981        m = re.search('^country (..):', output, re.MULTILINE)
982        if not m:
983            return None
984        return m.group(1)
985
986
987    def is_regulatory_self_managed(self):
988        """
989        Determine if any WiFi device on the system manages its own regulatory
990        info (NL80211_ATTR_WIPHY_SELF_MANAGED_REG).
991
992        @returns True if self-managed, False otherwise.
993        """
994        output = self._run('%s reg get' % self._command_iw).stdout
995        m = re.search('^phy#.*\(self-managed\)', output, re.MULTILINE)
996        return not m is None
997
998
999    def wait_for_scan_result(self, interface, bsses=(), ssids=(),
1000                             timeout_seconds=30, wait_for_all=False):
1001        """Returns a list of IWBSS objects for given list of bsses or ssids.
1002
1003        This method will scan for a given timeout and return all of the networks
1004        that have a matching ssid or bss.  If wait_for_all is true and all
1005        networks are not found within the given timeout an empty list will
1006        be returned.
1007
1008        @param interface: which interface to run iw against
1009        @param bsses: a list of BSS strings
1010        @param ssids: a list of ssid strings
1011        @param timeout_seconds: the amount of time to wait in seconds
1012        @param wait_for_all: True to wait for all listed bsses or ssids; False
1013                             to return if any of the networks were found
1014
1015        @returns a list of IwBss collections that contain the given bss or ssid;
1016            if the scan is empty or returns an error code None is returned.
1017
1018        """
1019
1020        logging.info('Performing a scan with a max timeout of %d seconds.',
1021                     timeout_seconds)
1022
1023        # If the in-progress scan takes more than 30 seconds to
1024        # complete it will most likely never complete; abort.
1025        # See crbug.com/309148
1026        scan_results = list()
1027        try:
1028            scan_results = utils.poll_for_condition(
1029                    condition=lambda: self.scan(interface),
1030                    timeout=timeout_seconds,
1031                    sleep_interval=5, # to allow in-progress scans to complete
1032                    desc='Timed out getting IWBSSes that match desired')
1033        except utils.TimeoutError as e:
1034            pass
1035
1036        if not scan_results: # empty list or None
1037            return None
1038
1039        # get all IWBSSes from the scan that match any of the desired
1040        # ssids or bsses passed in
1041        matching_iwbsses = [iwbss for iwbss in scan_results
1042                if iwbss.ssid in ssids or iwbss.bss in bsses]
1043        if wait_for_all:
1044            found_bsses = [iwbss.bss for iwbss in matching_iwbsses]
1045            found_ssids = [iwbss.ssid for iwbss in matching_iwbsses]
1046            # if an expected bss or ssid was not found, and it was required
1047            # by the caller that all expected be found, return empty list
1048            if any(bss not in found_bsses for bss in bsses) or any(
1049                    ssid not in found_ssids for ssid in ssids):
1050                return list()
1051        return list(matching_iwbsses)
1052
1053
1054    def set_antenna_bitmap(self, phy, tx_bitmap, rx_bitmap):
1055        """Set antenna chain mask on given phy (radio).
1056
1057        This function will set the antennas allowed to use for TX and
1058        RX on the |phy| based on the |tx_bitmap| and |rx_bitmap|.
1059        This command is only allowed when the interfaces on the phy are down.
1060
1061        @param phy: phy name
1062        @param tx_bitmap: bitmap of allowed antennas to use for TX
1063        @param rx_bitmap: bitmap of allowed antennas to use for RX
1064
1065        """
1066        command = '%s phy %s set antenna %d %d' % (self._command_iw, phy,
1067                                                   tx_bitmap, rx_bitmap)
1068        self._run(command)
1069
1070
1071    def get_event_logger(self):
1072        """Create and return a IwEventLogger object.
1073
1074        @returns a IwEventLogger object.
1075
1076        """
1077        local_file = IW_LOCAL_EVENT_LOG_FILE % (self._log_id)
1078        self._log_id += 1
1079        return iw_event_logger.IwEventLogger(self._host, self._command_iw,
1080                                             local_file)
1081
1082
1083    def vht_supported(self):
1084        """Returns True if VHT is supported; False otherwise."""
1085        result = self._run('%s list' % self._command_iw).stdout
1086        if 'VHT Capabilities' in result:
1087            return True
1088        return False
1089
1090
1091    def he_supported(self):
1092        """Returns True if HE (802.11ax) is supported; False otherwise."""
1093        result = self._run('%s list' % self._command_iw).stdout
1094        if 'HE MAC Capabilities' in result:
1095            return True
1096        return False
1097
1098
1099    def frequency_supported(self, frequency):
1100        """Returns True if the given frequency is supported; False otherwise.
1101
1102        @param frequency: int Wifi frequency to check if it is supported by
1103                          DUT.
1104        """
1105        phys = self.list_phys()
1106        for phy in phys:
1107            for band in phy.bands:
1108                if frequency in band.frequencies:
1109                    return True
1110        return False
1111
1112
1113    def get_fragmentation_threshold(self, phy):
1114        """Returns the fragmentation threshold for |phy|.
1115
1116        @param phy: phy name
1117        """
1118        ret = self._run('%s phy %s info' % (self._command_iw, phy))
1119        frag_regex = r'^\s+Fragmentation threshold:\s+([0-9]+)$'
1120        match = re.search(frag_regex, ret.stdout, re.MULTILINE)
1121
1122        if match:
1123            return int(match.group(1))
1124
1125        return None
1126
1127
1128    def get_info(self, phy=None):
1129        """
1130        Returns the output of 'iw phy info' for |phy|, or 'iw list' if no phy
1131        specified.
1132
1133        @param phy: optional string giving the name of the phy
1134        @return string stdout of the command run
1135        """
1136        if phy and phy not in [iw_phy.name for iw_phy in self.list_phys()]:
1137            logging.info('iw could not find phy %s', phy)
1138            return None
1139
1140        if phy:
1141            out = self._run('%s phy %s info' % (self._command_iw, phy)).stdout
1142        else:
1143            out = self._run('%s list' % self._command_iw).stdout
1144        if 'Wiphy' in out:
1145            return out
1146        return None
1147