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