1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import math
7import os
8import re
9import time
10
11from contextlib import contextmanager
12from collections import namedtuple
13
14from autotest_lib.client.bin import site_utils as client_site_utils
15from autotest_lib.client.bin import utils
16from autotest_lib.client.common_lib import base_utils
17from autotest_lib.client.common_lib import error
18from autotest_lib.client.common_lib.cros.network import interface
19from autotest_lib.client.common_lib.cros.network import iw_runner
20from autotest_lib.client.common_lib.cros.network import ping_runner
21from autotest_lib.client.cros import constants
22from autotest_lib.server import autotest
23from autotest_lib.server import frontend
24from autotest_lib.server import site_linux_system
25from autotest_lib.server import site_utils
26from autotest_lib.server.cros.network import wpa_cli_proxy
27from autotest_lib.server.hosts import adb_host
28
29# Wake-on-WiFi feature strings
30WAKE_ON_WIFI_NONE = 'none'
31WAKE_ON_WIFI_PACKET = 'packet'
32WAKE_ON_WIFI_DARKCONNECT = 'darkconnect'
33WAKE_ON_WIFI_PACKET_DARKCONNECT = 'packet_and_darkconnect'
34WAKE_ON_WIFI_NOT_SUPPORTED = 'not_supported'
35
36# Wake-on-WiFi test timing constants
37SUSPEND_WAIT_TIME_SECONDS = 10
38RECEIVE_PACKET_WAIT_TIME_SECONDS = 10
39DARK_RESUME_WAIT_TIME_SECONDS = 25
40WAKE_TO_SCAN_PERIOD_SECONDS = 30
41NET_DETECT_SCAN_WAIT_TIME_SECONDS = 15
42WAIT_UP_TIMEOUT_SECONDS = 10
43DISCONNECT_WAIT_TIME_SECONDS = 10
44INTERFACE_DOWN_WAIT_TIME_SECONDS = 10
45
46ConnectTime = namedtuple('ConnectTime', 'state, time')
47
48XMLRPC_BRINGUP_TIMEOUT_SECONDS = 60
49SHILL_XMLRPC_LOG_PATH = '/var/log/shill_xmlrpc_server.log'
50SHILL_BRILLO_XMLRPC_LOG_PATH = '/data/shill_xmlrpc_server.log'
51ANDROID_XMLRPC_LOG_PATH = '/var/log/android_xmlrpc_server.log'
52ANDROID_XMLRPC_SERVER_AUTOTEST_PATH = (
53        '../../../client/cros/networking/android_xmlrpc_server.py')
54
55def install_android_xmlrpc_server(host):
56    """Install Android XMLRPC server script on |host|.
57
58    @param host: host object representing a remote device.
59
60    """
61    current_dir = os.path.dirname(os.path.realpath(__file__))
62    xmlrpc_server_script = os.path.join(
63            current_dir, ANDROID_XMLRPC_SERVER_AUTOTEST_PATH)
64    host.send_file(
65            xmlrpc_server_script, constants.ANDROID_XMLRPC_SERVER_TARGET_DIR)
66
67
68def get_xmlrpc_proxy(host):
69    """Get a shill XMLRPC proxy for |host|.
70
71    The returned object has no particular type.  Instead, when you call
72    a method on the object, it marshalls the objects passed as arguments
73    and uses them to make RPCs on the remote server.  Thus, you should
74    read shill_xmlrpc_server.py to find out what methods are supported.
75
76    @param host: host object representing a remote device.
77    @return proxy object for remote XMLRPC server.
78
79    """
80    # Make sure the client library is on the device so that the proxy
81    # code is there when we try to call it.
82    if host.is_client_install_supported:
83        client_at = autotest.Autotest(host)
84        client_at.install()
85    if host.get_os_type() == adb_host.OS_TYPE_BRILLO:
86        xmlrpc_server_command = constants.SHILL_BRILLO_XMLRPC_SERVER_COMMAND
87        log_path = SHILL_BRILLO_XMLRPC_LOG_PATH
88        command_name = constants.SHILL_BRILLO_XMLRPC_SERVER_CLEANUP_PATTERN
89    elif host.get_os_type() == adb_host.OS_TYPE_ANDROID:
90        xmlrpc_server_command = constants.ANDROID_XMLRPC_SERVER_COMMAND
91        log_path = ANDROID_XMLRPC_LOG_PATH
92        command_name = constants.ANDROID_XMLRPC_SERVER_CLEANUP_PATTERN
93        # If there is more than one device attached to the host, use serial to
94        # identify the DUT.
95        if host.adb_serial:
96            xmlrpc_server_command = (
97                    '%s -s %s' % (xmlrpc_server_command, host.adb_serial))
98        # For android, start the XML RPC server on the accompanying host.
99        host = host.teststation
100        install_android_xmlrpc_server(host)
101    else:
102        xmlrpc_server_command = constants.SHILL_XMLRPC_SERVER_COMMAND
103        log_path = SHILL_XMLRPC_LOG_PATH
104        command_name = constants.SHILL_XMLRPC_SERVER_CLEANUP_PATTERN
105    # Start up the XMLRPC proxy on the client
106    proxy = host.rpc_server_tracker.xmlrpc_connect(
107            xmlrpc_server_command,
108            constants.SHILL_XMLRPC_SERVER_PORT,
109            command_name=command_name,
110            ready_test_name=constants.SHILL_XMLRPC_SERVER_READY_METHOD,
111            timeout_seconds=XMLRPC_BRINGUP_TIMEOUT_SECONDS,
112            logfile=log_path
113      )
114    return proxy
115
116
117def _is_conductive(hostname):
118    if utils.host_could_be_in_afe(hostname):
119        conductive = site_utils.get_label_from_afe(hostname.split('.')[0],
120                                                  'conductive:',
121                                                   frontend.AFE())
122        if conductive and conductive.lower() == 'true':
123            return True
124    return False
125
126
127class WiFiClient(site_linux_system.LinuxSystem):
128    """WiFiClient is a thin layer of logic over a remote DUT in wifitests."""
129
130    DEFAULT_PING_COUNT = 10
131    COMMAND_PING = 'ping'
132
133    MAX_SERVICE_GONE_TIMEOUT_SECONDS = 60
134
135    # List of interface names we won't consider for use as "the" WiFi interface
136    # on Android hosts.
137    WIFI_IF_BLACKLIST = ['p2p0']
138
139    UNKNOWN_BOARD_TYPE = 'unknown'
140
141    # DBus device properties. Wireless interfaces should support these.
142    ROAM_THRESHOLD = 'RoamThreshold'
143    WAKE_ON_WIFI_FEATURES = 'WakeOnWiFiFeaturesEnabled'
144    NET_DETECT_SCAN_PERIOD = 'NetDetectScanPeriodSeconds'
145    WAKE_TO_SCAN_PERIOD = 'WakeToScanPeriodSeconds'
146    FORCE_WAKE_TO_SCAN_TIMER = 'ForceWakeToScanTimer'
147
148    CONNECTED_STATES = ['ready', 'portal', 'online']
149
150
151    @property
152    def machine_id(self):
153        """@return string unique to a particular board/cpu configuration."""
154        if self._machine_id:
155            return self._machine_id
156
157        uname_result = self.host.run('uname -m', ignore_status=True)
158        kernel_arch = ''
159        if not uname_result.exit_status and uname_result.stdout.find(' ') < 0:
160            kernel_arch = uname_result.stdout.strip()
161        cpu_info = self.host.run('cat /proc/cpuinfo').stdout.splitlines()
162        cpu_count = len(filter(lambda x: x.lower().startswith('bogomips'),
163                               cpu_info))
164        cpu_count_str = ''
165        if cpu_count:
166            cpu_count_str = 'x%d' % cpu_count
167        ghz_value = ''
168        ghz_pattern = re.compile('([0-9.]+GHz)')
169        for line in cpu_info:
170            match = ghz_pattern.search(line)
171            if match is not None:
172                ghz_value = '_' + match.group(1)
173                break
174
175        return '%s_%s%s%s' % (self.board, kernel_arch, ghz_value, cpu_count_str)
176
177
178    @property
179    def powersave_on(self):
180        """@return bool True iff WiFi powersave mode is enabled."""
181        result = self.host.run("iw dev %s get power_save" % self.wifi_if)
182        output = result.stdout.rstrip()       # NB: chop \n
183        # Output should be either "Power save: on" or "Power save: off".
184        find_re = re.compile('([^:]+):\s+(\w+)')
185        find_results = find_re.match(output)
186        if not find_results:
187            raise error.TestFail('Failed to find power_save parameter '
188                                 'in iw results.')
189
190        return find_results.group(2) == 'on'
191
192
193    @property
194    def shill(self):
195        """@return shill RPCProxy object."""
196        return self._shill_proxy
197
198
199    @property
200    def client(self):
201        """Deprecated accessor for the client host.
202
203        The term client is used very loosely in old autotests and this
204        accessor should not be used in new code.  Use host() instead.
205
206        @return host object representing a remote DUT.
207
208        """
209        return self.host
210
211
212    @property
213    def command_ip(self):
214        """@return string path to ip command."""
215        return self._command_ip
216
217
218    @property
219    def command_iptables(self):
220        """@return string path to iptables command."""
221        return self._command_iptables
222
223
224    @property
225    def command_ping6(self):
226        """@return string path to ping6 command."""
227        return self._command_ping6
228
229
230    @property
231    def command_wpa_cli(self):
232        """@return string path to wpa_cli command."""
233        return self._command_wpa_cli
234
235
236    @property
237    def conductive(self):
238        """@return True if the rig is conductive; False otherwise."""
239        if self._conductive is None:
240            self._conductive = _is_conductive(self._client_hostname)
241        return self._conductive
242
243
244    @conductive.setter
245    def conductive(self, value):
246        """Set the conductive member to True or False.
247
248        @param value: boolean value to set the conductive member to.
249        """
250        self._conductive = value
251
252
253    @property
254    def wifi_if(self):
255        """@return string wifi device on machine (e.g. mlan0)."""
256        return self._wifi_if
257
258
259    @property
260    def wifi_mac(self):
261        """@return string MAC address of self.wifi_if."""
262        return self._interface.mac_address
263
264
265    @property
266    def wifi_ip(self):
267        """@return string IPv4 address of self.wifi_if."""
268        return self._interface.ipv4_address
269
270
271    @property
272    def wifi_ip_subnet(self):
273        """@return string IPv4 subnet prefix of self.wifi_if."""
274        return self._interface.ipv4_subnet
275
276
277    @property
278    def wifi_signal_level(self):
279        """Returns the signal level of this DUT's WiFi interface.
280
281        @return int signal level of connected WiFi interface or None (e.g. -67).
282
283        """
284        return self._interface.signal_level
285
286
287    @staticmethod
288    def assert_bsses_include_ssids(found_bsses, expected_ssids):
289        """Verifies that |found_bsses| includes |expected_ssids|.
290
291        @param found_bsses list of IwBss objects.
292        @param expected_ssids list of string SSIDs.
293        @raise error.TestFail if any element of |expected_ssids| is not found.
294
295        """
296        for ssid in expected_ssids:
297            if not ssid:
298                continue
299
300            for bss in found_bsses:
301                if bss.ssid == ssid:
302                    break
303            else:
304                raise error.TestFail('SSID %s is not in scan results: %r' %
305                                     (ssid, found_bsses))
306
307
308    def wifi_noise_level(self, frequency_mhz):
309        """Returns the noise level of this DUT's WiFi interface.
310
311        @param frequency_mhz: frequency at which the noise level should be
312               measured and reported.
313        @return int signal level of connected WiFi interface in dBm (e.g. -67)
314                or None if the value is unavailable.
315
316        """
317        return self._interface.noise_level(frequency_mhz)
318
319
320    def __init__(self, client_host, result_dir, use_wpa_cli):
321        """
322        Construct a WiFiClient.
323
324        @param client_host host object representing a remote host.
325        @param result_dir string directory to store test logs/packet caps.
326        @param use_wpa_cli bool True if we want to use |wpa_cli| commands for
327               Android testing.
328
329        """
330        super(WiFiClient, self).__init__(client_host, 'client',
331                                         inherit_interfaces=True)
332        self._command_ip = 'ip'
333        self._command_iptables = 'iptables'
334        self._command_ping6 = 'ping6'
335        self._command_wpa_cli = 'wpa_cli'
336        self._machine_id = None
337        self._result_dir = result_dir
338        self._conductive = None
339        self._client_hostname = client_host.hostname
340
341        if self.host.get_os_type() == adb_host.OS_TYPE_ANDROID and use_wpa_cli:
342            # Look up the WiFi device (and its MAC) on the client.
343            devs = self.iw_runner.list_interfaces(desired_if_type='managed')
344            devs = [dev for dev in devs
345                    if dev.if_name not in self.WIFI_IF_BLACKLIST]
346            if not devs:
347                raise error.TestFail('No wlan devices found on %s.' %
348                                     self.host.hostname)
349
350            if len(devs) > 1:
351                logging.warning('Warning, found multiple WiFi devices on '
352                                '%s: %r', self.host.hostname, devs)
353            self._wifi_if = devs[0].if_name
354            self._shill_proxy = wpa_cli_proxy.WpaCliProxy(
355                    self.host, self._wifi_if)
356            self._wpa_cli_proxy = self._shill_proxy
357        else:
358            self._shill_proxy = get_xmlrpc_proxy(self.host)
359            interfaces = self._shill_proxy.list_controlled_wifi_interfaces()
360            if not interfaces:
361                logging.debug('No interfaces managed by shill. Rebooting host')
362                self.host.reboot()
363                raise error.TestError('No interfaces managed by shill on %s' %
364                                      self.host.hostname)
365            self._wifi_if = interfaces[0]
366            self._wpa_cli_proxy = wpa_cli_proxy.WpaCliProxy(
367                    self.host, self._wifi_if)
368            self._raise_logging_level()
369        self._interface = interface.Interface(self._wifi_if, host=self.host)
370        logging.debug('WiFi interface is: %r',
371                      self._interface.device_description)
372        self._firewall_rules = []
373        # Turn off powersave mode by default.
374        self.powersave_switch(False)
375        # All tests that use this object assume the interface starts enabled.
376        self.set_device_enabled(self._wifi_if, True)
377        # Invoke the |capabilities| property defined in the parent |Linuxsystem|
378        # to workaround the lazy loading of the capabilities cache and supported
379        # frequency list. This is needed for tests that may need access to these
380        # when the DUT is unreachable (for ex: suspended).
381        self.capabilities
382
383
384    def _assert_method_supported(self, method_name):
385        """Raise a TestNAError if the XMLRPC proxy has no method |method_name|.
386
387        @param method_name: string name of method that should exist on the
388                XMLRPC proxy.
389
390        """
391        if not self._supports_method(method_name):
392            raise error.TestNAError('%s() is not supported' % method_name)
393
394
395    def _raise_logging_level(self):
396        """Raises logging levels for WiFi on DUT."""
397        self.host.run('wpa_debug excessive', ignore_status=True)
398        self.host.run('ff_debug --level -5', ignore_status=True)
399        self.host.run('ff_debug +wifi', ignore_status=True)
400
401
402    def is_vht_supported(self):
403        """Returns True if VHT supported; False otherwise"""
404        return self.CAPABILITY_VHT in self.capabilities
405
406
407    def is_5ghz_supported(self):
408        """Returns True if 5Ghz bands are supported; False otherwise."""
409        return self.CAPABILITY_5GHZ in self.capabilities
410
411
412    def is_ibss_supported(self):
413        """Returns True if IBSS mode is supported; False otherwise."""
414        return self.CAPABILITY_IBSS in self.capabilities
415
416
417    def is_frequency_supported(self, frequency):
418        """Returns True if the given frequency is supported; False otherwise.
419
420        @param frequency: int Wifi frequency to check if it is supported by
421                          DUT.
422        """
423        return frequency in self.phys_for_frequency
424
425
426    def _supports_method(self, method_name):
427        """Checks if |method_name| is supported on the remote XMLRPC proxy.
428
429        autotest will, for their own reasons, install python files in the
430        autotest client package that correspond the version of the build
431        rather than the version running on the autotest drone.  This
432        creates situations where we call methods on the client XMLRPC proxy
433        that don't exist in that version of the code.  This detects those
434        situations so that we can degrade more or less gracefully.
435
436        @param method_name: string name of method that should exist on the
437                XMLRPC proxy.
438        @return True if method is available, False otherwise.
439
440        """
441        # Make no assertions about ADBHost support.  We don't use an XMLRPC
442        # proxy with those hosts anyway.
443        supported = (isinstance(self.host, adb_host.ADBHost) or
444                     method_name in self._shill_proxy.system.listMethods())
445        if not supported:
446            logging.warning('%s() is not supported on older images',
447                            method_name)
448        return supported
449
450
451    def close(self):
452        """Tear down state associated with the client."""
453        self.stop_capture()
454        self.powersave_switch(False)
455        self.shill.clean_profiles()
456        super(WiFiClient, self).close()
457
458
459    def firewall_open(self, proto, src):
460        """Opens up firewall to run netperf tests.
461
462        By default, we have a firewall rule for NFQUEUE (see crbug.com/220736).
463        In order to run netperf test, we need to add a new firewall rule BEFORE
464        this NFQUEUE rule in the INPUT chain.
465
466        @param proto a string, test traffic protocol, e.g. udp, tcp.
467        @param src a string, subnet/mask.
468
469        @return a string firewall rule added.
470
471        """
472        rule = 'INPUT -s %s/32 -p %s -m %s -j ACCEPT' % (src, proto, proto)
473        self.host.run('%s -I %s' % (self._command_iptables, rule))
474        self._firewall_rules.append(rule)
475        return rule
476
477
478    def firewall_cleanup(self):
479        """Cleans up firewall rules."""
480        for rule in self._firewall_rules:
481            self.host.run('%s -D %s' % (self._command_iptables, rule))
482        self._firewall_rules = []
483
484
485    def sync_host_times(self):
486        """Set time on our DUT to match local time."""
487        epoch_seconds = time.time()
488        self.shill.sync_time_to(epoch_seconds)
489
490
491    def check_iw_link_value(self, iw_link_key, desired_value):
492        """Assert that the current wireless link property is |desired_value|.
493
494        @param iw_link_key string one of IW_LINK_KEY_* defined in iw_runner.
495        @param desired_value string desired value of iw link property.
496
497        """
498        actual_value = self.get_iw_link_value(iw_link_key)
499        desired_value = str(desired_value)
500        if actual_value != desired_value:
501            raise error.TestFail('Wanted iw link property %s value %s, but '
502                                 'got %s instead.' % (iw_link_key,
503                                                      desired_value,
504                                                      actual_value))
505
506
507    def get_iw_link_value(self, iw_link_key):
508        """Get the current value of a link property for this WiFi interface.
509
510        @param iw_link_key string one of IW_LINK_KEY_* defined in iw_runner.
511
512        """
513        return self.iw_runner.get_link_value(self.wifi_if, iw_link_key)
514
515
516    def powersave_switch(self, turn_on):
517        """Toggle powersave mode for the DUT.
518
519        @param turn_on bool True iff powersave mode should be turned on.
520
521        """
522        mode = 'off'
523        if turn_on:
524            mode = 'on'
525        self.host.run('iw dev %s set power_save %s' % (self.wifi_if, mode))
526
527
528    def timed_scan(self, frequencies, ssids, scan_timeout_seconds=10,
529                   retry_timeout_seconds=10):
530        """Request timed scan to discover given SSIDs.
531
532        This method will retry for a default of |retry_timeout_seconds| until it
533        is able to successfully kick off a scan.  Sometimes, the device on the
534        DUT claims to be busy and rejects our requests. It will raise error
535        if the scan did not complete within |scan_timeout_seconds| or it was
536        not able to discover the given SSIDs.
537
538        @param frequencies list of int WiFi frequencies to scan for.
539        @param ssids list of string ssids to probe request for.
540        @param scan_timeout_seconds: float number of seconds the scan
541                operation not to exceed.
542        @param retry_timeout_seconds: float number of seconds to retry scanning
543                if the interface is busy.  This does not retry if certain
544                SSIDs are missing from the results.
545        @return time in seconds took to complete scan request.
546
547        """
548        start_time = time.time()
549        while time.time() - start_time < retry_timeout_seconds:
550            scan_result = self.iw_runner.timed_scan(
551                    self.wifi_if, frequencies=frequencies, ssids=ssids)
552
553            if scan_result is not None:
554                break
555
556            time.sleep(0.5)
557        else:
558            raise error.TestFail('Unable to trigger scan on client.')
559
560        # Verify scan operation completed within given timeout
561        if scan_result.time > scan_timeout_seconds:
562            raise error.TestFail('Scan time %.2fs exceeds the scan timeout' %
563                                 (scan_result.time))
564
565        # Verify all ssids are discovered
566        self.assert_bsses_include_ssids(scan_result.bss_list, ssids)
567
568        logging.info('Wifi scan completed in %.2f seconds', scan_result.time)
569        return scan_result.time
570
571
572    def scan(self, frequencies, ssids, timeout_seconds=10, require_match=True):
573        """Request a scan and (optionally) check that requested SSIDs appear in
574        the results.
575
576        This method will retry for a default of |timeout_seconds| until it is
577        able to successfully kick off a scan.  Sometimes, the device on the DUT
578        claims to be busy and rejects our requests.
579
580        If |ssids| is non-empty, we will speficially probe for those SSIDs.
581
582        If |require_match| is True, we will verify that every element
583        of |ssids| was found in scan results.
584
585        @param frequencies list of int WiFi frequencies to scan for.
586        @param ssids list of string ssids to probe request for.
587        @param timeout_seconds: float number of seconds to retry scanning
588                if the interface is busy.  This does not retry if certain
589                SSIDs are missing from the results.
590        @param require_match: bool True if we must find |ssids|.
591
592        """
593        start_time = time.time()
594        while time.time() - start_time < timeout_seconds:
595            bss_list = self.iw_runner.scan(
596                    self.wifi_if, frequencies=frequencies, ssids=ssids)
597            if bss_list is not None:
598                break
599
600            time.sleep(0.5)
601        else:
602            raise error.TestFail('Unable to trigger scan on client.')
603
604        if require_match:
605            self.assert_bsses_include_ssids(bss_list, ssids)
606
607
608    def wait_for_bsses(self, ssid, num_bss_expected, timeout_seconds=15):
609      """Wait for all BSSes associated with given SSID to be discovered in the
610      scan.
611
612      @param ssid string name of network being queried
613      @param num_bss_expected int number of BSSes expected
614      @param timeout_seconds int seconds to wait for BSSes to be discovered
615
616      """
617      start_time = time.time()
618      while time.time() - start_time < timeout_seconds:
619          bss_list = self.iw_runner.scan(
620                  self.wifi_if, frequencies=[], ssids=[ssid])
621
622          # Determine number of BSSes found in the scan result.
623          num_bss_found = 0
624          if bss_list is not None:
625              for bss in bss_list:
626                  if bss.ssid == ssid:
627                      num_bss_found += 1
628
629          # Verify all BSSes are found.
630          if num_bss_found == num_bss_expected:
631              break
632
633          time.sleep(0.5)
634      else:
635          raise error.TestFail('Failed to discover all BSSes.')
636
637
638    def wait_for_service_states(self, ssid, states, timeout_seconds):
639        """Waits for a WiFi service to achieve one of |states|.
640
641        @param ssid string name of network being queried
642        @param states tuple list of states for which the caller is waiting
643        @param timeout_seconds int seconds to wait for a state in |states|
644
645        """
646        logging.info('Waiting for %s to reach one of %r...', ssid, states)
647        success, state, time  = self._shill_proxy.wait_for_service_states(
648                ssid, states, timeout_seconds)
649        logging.info('...ended up in state \'%s\' (%s) after %f seconds.',
650                     state, 'success' if success else 'failure', time)
651        return success, state, time
652
653
654    def do_suspend(self, seconds):
655        """Puts the DUT in suspend power state for |seconds| seconds.
656
657        @param seconds: The number of seconds to suspend the device.
658
659        """
660        logging.info('Suspending DUT for %d seconds...', seconds)
661        self._shill_proxy.do_suspend(seconds)
662        logging.info('...done suspending')
663
664
665    def do_suspend_bg(self, seconds):
666        """Suspend DUT using the power manager - non-blocking.
667
668        @param seconds: The number of seconds to suspend the device.
669
670        """
671        logging.info('Suspending DUT (in background) for %d seconds...',
672                     seconds)
673        self._shill_proxy.do_suspend_bg(seconds)
674
675
676    def clear_supplicant_blacklist(self):
677        """Clear's the AP blacklist on the DUT.
678
679        @return stdout and stderror returns passed from wpa_cli command.
680
681        """
682        result = self._wpa_cli_proxy.run_wpa_cli_cmd('blacklist clear',
683                                                     check_result=False);
684        logging.info('wpa_cli blacklist clear: out:%r err:%r', result.stdout,
685                     result.stderr)
686        return result.stdout, result.stderr
687
688
689    def get_active_wifi_SSIDs(self):
690        """Get a list of visible SSID's around the DUT
691
692        @return list of string SSIDs
693
694        """
695        self._assert_method_supported('get_active_wifi_SSIDs')
696        return self._shill_proxy.get_active_wifi_SSIDs()
697
698
699    def roam_threshold(self, value):
700        """Get a context manager to temporarily change wpa_supplicant's
701        roaming threshold for the specified interface.
702
703        The correct way to use this method is:
704
705        with client.roam_threshold(40):
706            ...
707
708        @param value: the desired roam threshold for the test.
709
710        @return a context manager for the threshold.
711
712        """
713        return TemporaryDBusProperty(self._shill_proxy,
714                                     self.wifi_if,
715                                     self.ROAM_THRESHOLD,
716                                     value)
717
718
719    def set_device_enabled(self, wifi_interface, value,
720                           fail_on_unsupported=False):
721        """Enable or disable the WiFi device.
722
723        @param wifi_interface: string name of interface being modified.
724        @param enabled: boolean; true if this device should be enabled,
725                false if this device should be disabled.
726        @return True if it worked; False, otherwise.
727
728        """
729        if fail_on_unsupported:
730            self._assert_method_supported('set_device_enabled')
731        elif not self._supports_method('set_device_enabled'):
732            return False
733        return self._shill_proxy.set_device_enabled(wifi_interface, value)
734
735
736    def add_arp_entry(self, ip_address, mac_address):
737        """Add an ARP entry to the table associated with the WiFi interface.
738
739        @param ip_address: string IP address associated with the new ARP entry.
740        @param mac_address: string MAC address associated with the new ARP
741                entry.
742
743        """
744        self.host.run('ip neigh add %s lladdr %s dev %s nud perm' %
745                      (ip_address, mac_address, self.wifi_if))
746
747
748    def discover_tdls_link(self, mac_address):
749        """Send a TDLS Discover to |peer_mac_address|.
750
751        @param mac_address: string MAC address associated with the TDLS peer.
752
753        @return bool True if operation initiated successfully, False otherwise.
754
755        """
756        return self._shill_proxy.discover_tdls_link(self.wifi_if, mac_address)
757
758
759    def establish_tdls_link(self, mac_address):
760        """Establish a TDLS link with |mac_address|.
761
762        @param mac_address: string MAC address associated with the TDLS peer.
763
764        @return bool True if operation initiated successfully, False otherwise.
765
766        """
767        return self._shill_proxy.establish_tdls_link(self.wifi_if, mac_address)
768
769
770    def query_tdls_link(self, mac_address):
771        """Query a TDLS link with |mac_address|.
772
773        @param mac_address: string MAC address associated with the TDLS peer.
774
775        @return string indicating current TDLS connectivity.
776
777        """
778        return self._shill_proxy.query_tdls_link(self.wifi_if, mac_address)
779
780
781    def add_wake_packet_source(self, source_ip):
782        """Add |source_ip| as a source that can wake us up with packets.
783
784        @param source_ip: IP address from which to wake upon receipt of packets
785
786        @return True if successful, False otherwise.
787
788        """
789        return self._shill_proxy.add_wake_packet_source(
790            self.wifi_if, source_ip)
791
792
793    def remove_wake_packet_source(self, source_ip):
794        """Remove |source_ip| as a source that can wake us up with packets.
795
796        @param source_ip: IP address to stop waking on packets from
797
798        @return True if successful, False otherwise.
799
800        """
801        return self._shill_proxy.remove_wake_packet_source(
802            self.wifi_if, source_ip)
803
804
805    def remove_all_wake_packet_sources(self):
806        """Remove all IPs as sources that can wake us up with packets.
807
808        @return True if successful, False otherwise.
809
810        """
811        return self._shill_proxy.remove_all_wake_packet_sources(self.wifi_if)
812
813
814    def wake_on_wifi_features(self, features):
815        """Shill supports programming the NIC to wake on special kinds of
816        incoming packets, or on changes to the available APs (disconnect,
817        coming in range of a known SSID). This method allows you to configure
818        what wake-on-WiFi mechanisms are active. It returns a context manager,
819        because this is a system-wide setting and we don't want it to persist
820        across different tests.
821
822        If you enable wake-on-packet, then the IPs registered by
823        add_wake_packet_source will be able to wake the system from suspend.
824
825        The correct way to use this method is:
826
827        with client.wake_on_wifi_features(WAKE_ON_WIFI_DARKCONNECT):
828            ...
829
830        @param features: string from the WAKE_ON_WIFI constants above.
831
832        @return a context manager for the features.
833
834        """
835        return TemporaryDBusProperty(self._shill_proxy,
836                                     self.wifi_if,
837                                     self.WAKE_ON_WIFI_FEATURES,
838                                     features)
839
840
841    def net_detect_scan_period_seconds(self, period):
842        """Sets the period between net detect scans performed by the NIC to look
843        for whitelisted SSIDs to |period|. This setting only takes effect if the
844        NIC is programmed to wake on SSID. Use as with roam_threshold.
845
846        @param period: integer number of seconds between NIC net detect scans
847
848        @return a context manager for the net detect scan period
849
850        """
851        return TemporaryDBusProperty(self._shill_proxy,
852                                     self.wifi_if,
853                                     self.NET_DETECT_SCAN_PERIOD,
854                                     period)
855
856
857    def wake_to_scan_period_seconds(self, period):
858        """Sets the period between RTC timer wakeups where the system is woken
859        from suspend to perform scans. This setting only takes effect if the
860        NIC is programmed to wake on SSID. Use as with roam_threshold.
861
862        @param period: integer number of seconds between wake to scan RTC timer
863                wakes.
864
865        @return a context manager for the net detect scan period
866
867        """
868        return TemporaryDBusProperty(self._shill_proxy,
869                                     self.wifi_if,
870                                     self.WAKE_TO_SCAN_PERIOD,
871                                     period)
872
873
874    def force_wake_to_scan_timer(self, is_forced):
875        """Sets the boolean value determining whether or not to force the use of
876        the wake to scan RTC timer, which wakes the system from suspend
877        periodically to scan for networks.
878
879        @param is_forced: boolean whether or not to force the use of the wake to
880                scan timer
881
882        @return a context manager for the net detect scan period
883
884        """
885        return TemporaryDBusProperty(self._shill_proxy,
886                                     self.wifi_if,
887                                     self.FORCE_WAKE_TO_SCAN_TIMER,
888                                     is_forced)
889
890
891    def request_roam(self, bssid):
892        """Request that we roam to the specified BSSID.
893
894        Note that this operation assumes that:
895
896        1) We're connected to an SSID for which |bssid| is a member.
897        2) There is a BSS with an appropriate ID in our scan results.
898
899        This method does not check for success of either the command or
900        the roaming operation.
901
902        @param bssid: string MAC address of bss to roam to.
903
904        """
905        self._wpa_cli_proxy.run_wpa_cli_cmd('roam %s' % bssid,
906                                            check_result=False);
907        return True
908
909
910    def request_roam_dbus(self, bssid, interface):
911        """Request that we roam to the specified BSSID through dbus.
912
913        Note that this operation assumes that:
914
915        1) We're connected to an SSID for which |bssid| is a member.
916        2) There is a BSS with an appropriate ID in our scan results.
917
918        @param bssid: string MAC address of bss to roam to.
919
920        """
921        self._assert_method_supported('request_roam_dbus')
922        self._shill_proxy.request_roam_dbus(bssid, interface)
923
924
925    def wait_for_roam(self, bssid, timeout_seconds=10.0):
926        """Wait for a roam to the given |bssid|.
927
928        @param bssid: string bssid to expect a roam to
929                (e.g.  '00:11:22:33:44:55').
930        @param timeout_seconds: float number of seconds to wait for a roam.
931        @return True iff we detect an association to the given |bssid| within
932                |timeout_seconds|.
933
934        """
935        start_time = time.time()
936        success = False
937        duration = 0.0
938        while time.time() - start_time < timeout_seconds:
939            duration = time.time() - start_time
940            current_bssid = self.iw_runner.get_current_bssid(self.wifi_if)
941            logging.debug('Current BSSID is %s.', current_bssid)
942            if current_bssid == bssid:
943                success = True
944                break
945            time.sleep(0.5)
946
947        logging.debug('%s to %s in %f seconds.',
948                      'Roamed ' if success else 'Failed to roam ',
949                      bssid,
950                      duration)
951        return success
952
953
954    def wait_for_ssid_vanish(self, ssid):
955        """Wait for shill to notice that there are no BSS's for an SSID present.
956
957        Raise a test failing exception if this does not come to pass.
958
959        @param ssid: string SSID of the network to require be missing.
960
961        """
962        start_time = time.time()
963        while time.time() - start_time < self.MAX_SERVICE_GONE_TIMEOUT_SECONDS:
964            visible_ssids = self.get_active_wifi_SSIDs()
965            logging.info('Got service list: %r', visible_ssids)
966            if ssid not in visible_ssids:
967                return
968
969            self.scan(frequencies=[], ssids=[], timeout_seconds=30)
970        else:
971            raise error.TestFail('shill should mark the BSS as not present')
972
973
974    def reassociate(self, timeout_seconds=10):
975        """Reassociate to the connected network.
976
977        @param timeout_seconds: float number of seconds to wait for operation
978                to complete.
979
980        """
981        logging.info('Attempt to reassociate')
982        with self.iw_runner.get_event_logger() as logger:
983            logger.start()
984            # Issue reattach command to wpa_supplicant
985            self._wpa_cli_proxy.run_wpa_cli_cmd('reattach');
986
987            # Wait for the timeout seconds for association to complete
988            time.sleep(timeout_seconds)
989
990            # Stop iw event logger
991            logger.stop()
992
993            # Get association time based on the iw event log
994            reassociate_time = logger.get_reassociation_time()
995            if reassociate_time is None or reassociate_time > timeout_seconds:
996                raise error.TestFail(
997                        'Failed to reassociate within given timeout')
998            logging.info('Reassociate time: %.2f seconds', reassociate_time)
999
1000
1001    def wait_for_connection(self, ssid, timeout_seconds=30, freq=None,
1002                            ping_ip=None, desired_subnet=None):
1003        """Verifies a connection to network ssid, optionally verifying
1004        frequency, ping connectivity and subnet.
1005
1006        @param ssid string ssid of the network to check.
1007        @param timeout_seconds int number of seconds to wait for
1008                connection on the given frequency.
1009        @param freq int frequency of network to check.
1010        @param ping_ip string ip address to ping for verification.
1011        @param desired_subnet string expected subnet in which client
1012                ip address should reside.
1013
1014        @returns a named tuple of (state, time)
1015        """
1016        POLLING_INTERVAL_SECONDS = 1.0
1017        start_time = time.time()
1018        duration = lambda: time.time() - start_time
1019        success = False
1020        while duration() < timeout_seconds:
1021            success, state, conn_time  = self.wait_for_service_states(
1022                    ssid, self.CONNECTED_STATES,
1023                    int(math.ceil(timeout_seconds - duration())))
1024            if not success:
1025                time.sleep(POLLING_INTERVAL_SECONDS)
1026                continue
1027
1028            if freq:
1029                actual_freq = self.get_iw_link_value(
1030                        iw_runner.IW_LINK_KEY_FREQUENCY)
1031                if str(freq) != actual_freq:
1032                    logging.debug('Waiting for desired frequency %s (got %s).',
1033                                  freq, actual_freq)
1034                    time.sleep(POLLING_INTERVAL_SECONDS)
1035                    continue
1036
1037            if desired_subnet:
1038                actual_subnet = self.wifi_ip_subnet
1039                if actual_subnet != desired_subnet:
1040                    logging.debug('Waiting for desired subnet %s (got %s).',
1041                                  desired_subnet, actual_subnet)
1042                    time.sleep(POLLING_INTERVAL_SECONDS)
1043                    continue
1044
1045            if ping_ip:
1046                ping_config = ping_runner.PingConfig(ping_ip)
1047                self.ping(ping_config)
1048
1049            return ConnectTime(state, conn_time)
1050
1051        freq_error_str = (' on frequency %d Mhz' % freq) if freq else ''
1052        raise error.TestFail(
1053                'Failed to connect to "%s"%s in %f seconds (state=%s)' %
1054                (ssid, freq_error_str, duration(), state))
1055
1056
1057    @contextmanager
1058    def assert_disconnect_count(self, count):
1059        """Context asserting |count| disconnects for the context lifetime.
1060
1061        Creates an iw logger during the lifetime of the context and asserts
1062        that the client disconnects exactly |count| times.
1063
1064        @param count int the expected number of disconnections.
1065
1066        """
1067        with self.iw_runner.get_event_logger() as logger:
1068            logger.start()
1069            yield
1070            logger.stop()
1071            if logger.get_disconnect_count() != count:
1072                raise error.TestFail(
1073                    'Client disconnected %d times; expected %d' %
1074                    (logger.get_disconnect_count(), count))
1075
1076
1077    def assert_no_disconnects(self):
1078        """Context asserting no disconnects for the context lifetime."""
1079        return self.assert_disconnect_count(0)
1080
1081
1082    @contextmanager
1083    def assert_disconnect_event(self):
1084        """Context asserting at least one disconnect for the context lifetime.
1085
1086        Creates an iw logger during the lifetime of the context and asserts
1087        that the client disconnects at least one time.
1088
1089        """
1090        with self.iw_runner.get_event_logger() as logger:
1091            logger.start()
1092            yield
1093            logger.stop()
1094            if logger.get_disconnect_count() == 0:
1095                raise error.TestFail('Client did not disconnect')
1096
1097
1098    def get_num_card_resets(self):
1099        """Get card reset count."""
1100        reset_msg = 'mwifiex_sdio_card_reset'
1101        result = self.host.run('grep -c %s /var/log/messages' % reset_msg,
1102                               ignore_status=True)
1103        if result.exit_status == 1:
1104            return 0
1105        count = int(result.stdout.strip())
1106        return count
1107
1108
1109    def get_disconnect_reasons(self):
1110        """Get disconnect reason codes."""
1111        disconnect_reason_msg = "updated DisconnectReason to ";
1112        disconnect_reason_cleared = "clearing DisconnectReason for ";
1113        result = self.host.run('grep -E "(%s|%s)" /var/log/net.log' %
1114                               (disconnect_reason_msg,
1115                               disconnect_reason_cleared),
1116                               ignore_status=True)
1117        if result.exit_status == 1:
1118            return None
1119
1120        lines = result.stdout.strip().split('\n')
1121        disconnect_reasons = []
1122        disconnect_reason_regex = re.compile(' to (\D?\d+)')
1123
1124        found = False
1125        for line in reversed(lines):
1126          match = disconnect_reason_regex.search(line)
1127          if match is not None:
1128            disconnect_reasons.append(match.group(1))
1129            found = True
1130          else:
1131            if (found):
1132                break
1133        return list(reversed(disconnect_reasons))
1134
1135
1136    def release_wifi_if(self):
1137        """Release the control over the wifi interface back to normal operation.
1138
1139        This will give the ownership of the wifi interface back to shill and
1140        wpa_supplicant.
1141
1142        """
1143        self.set_device_enabled(self._wifi_if, True)
1144
1145
1146    def claim_wifi_if(self):
1147        """Claim the control over the wifi interface from this wifi client.
1148
1149        This claim the ownership of the wifi interface from shill and
1150        wpa_supplicant. The wifi interface should be UP when this call returns.
1151
1152        """
1153        # Disabling a wifi device in shill will remove that device from
1154        # wpa_supplicant as well.
1155        self.set_device_enabled(self._wifi_if, False)
1156
1157        # Wait for shill to bring down the wifi interface.
1158        is_interface_down = lambda: not self._interface.is_up
1159        client_site_utils.poll_for_condition(
1160                is_interface_down,
1161                timeout=INTERFACE_DOWN_WAIT_TIME_SECONDS,
1162                sleep_interval=0.5,
1163                desc='Timeout waiting for interface to go down.')
1164        # Bring up the wifi interface to allow the test to use the interface.
1165        self.host.run('%s link set %s up' % (self.cmd_ip, self.wifi_if))
1166
1167
1168    def set_sched_scan(self, enable, fail_on_unsupported=False):
1169        """enable/disable scheduled scan.
1170
1171        @param enable bool flag indicating to enable/disable scheduled scan.
1172
1173        """
1174        if fail_on_unsupported:
1175            self._assert_method_supported('set_sched_scan')
1176        elif not self._supports_method('set_sched_scan'):
1177            return False
1178        return self._shill_proxy.set_sched_scan(enable)
1179
1180
1181    def check_connected_on_last_resume(self):
1182        """Checks whether the DUT was connected on its last resume.
1183
1184        Checks that the DUT was connected after waking from suspend by parsing
1185        the last instance shill log message that reports shill's connection
1186        status on resume. Fails the test if this log message reports that
1187        the DUT woke up disconnected.
1188
1189        """
1190        # As of build R43 6913.0.0, the shill log message from the function
1191        # OnAfterResume is called as soon as shill resumes from suspend, and
1192        # will report whether or not shill is connected. The log message will
1193        # take one of the following two forms:
1194        #
1195        #       [...] [INFO:wifi.cc(1941)] OnAfterResume: connected
1196        #       [...] [INFO:wifi.cc(1941)] OnAfterResume: not connected
1197        #
1198        # where 1941 is an arbitrary PID number. By checking if the last
1199        # instance of this message contains the substring "not connected", we
1200        # can determine whether or not shill was connected on its last resume.
1201        connection_status_log_regex_str = 'INFO:wifi\.cc.*OnAfterResume'
1202        not_connected_substr = 'not connected'
1203        connected_substr = 'connected'
1204
1205        cmd = ('grep -E %s /var/log/net.log | tail -1' %
1206               connection_status_log_regex_str)
1207        connection_status_log = self.host.run(cmd).stdout
1208        if not connection_status_log:
1209            raise error.TestFail('Could not find resume connection status log '
1210                                 'message.')
1211
1212        logging.debug('Connection status message:\n%s', connection_status_log)
1213        if not_connected_substr in connection_status_log:
1214            raise error.TestFail('Client was not connected upon waking from '
1215                                 'suspend.')
1216
1217        if not connected_substr in connection_status_log:
1218            raise error.TestFail('Last resume log message did not contain '
1219                                 'connection status.')
1220
1221        logging.info('Client was connected upon waking from suspend.')
1222
1223
1224    def check_wake_on_wifi_throttled(self):
1225        """
1226        Checks whether wake on WiFi was throttled on the DUT on the last dark
1227        resume. Check for this by parsing shill logs for a throttling message.
1228
1229        """
1230        # We are looking for an dark resume error log message indicating that
1231        # wake on WiFi was throttled. This is an example of the error message:
1232        #     [...] [ERROR:wake_on_wifi.cc(1304)] OnDarkResume: Too many dark \
1233        #       resumes; disabling wake on WiFi temporarily
1234        dark_resume_log_regex_str = 'ERROR:wake_on_wifi\.cc.*OnDarkResume:.*'
1235        throttled_msg_substr = ('Too many dark resumes; disabling wake on '
1236                                   'WiFi temporarily')
1237
1238        cmd = ('grep -E %s /var/log/net.log | tail -1' %
1239               dark_resume_log_regex_str)
1240        last_dark_resume_error_log = self.host.run(cmd).stdout
1241        if not last_dark_resume_error_log:
1242            raise error.TestFail('Could not find a dark resume log message.')
1243
1244        logging.debug('Last dark resume log message:\n%s',
1245                last_dark_resume_error_log)
1246        if not throttled_msg_substr in last_dark_resume_error_log:
1247            raise error.TestFail('Wake on WiFi was not throttled on the last '
1248                                 'dark resume.')
1249
1250        logging.info('Wake on WiFi was throttled on the last dark resume.')
1251
1252
1253    def shill_debug_log(self, message):
1254        """Logs a message to the shill log (i.e. /var/log/net.log). This
1255           message will be logged at the DEBUG level.
1256
1257        @param message: the message to be logged.
1258
1259        """
1260        logging.info(message)
1261
1262        # Skip this command if running on Android, since Android does not
1263        # have a /usr/bin/logger (or an equivalent that takes the same
1264        # parameters as the Linux logger does.)
1265        if not isinstance(self.host, adb_host.ADBHost):
1266            logger_command = ('/usr/bin/logger'
1267                              ' --tag shill'
1268                              ' --priority daemon.debug'
1269                              ' "%s"' % base_utils.sh_escape(message))
1270            self.host.run(logger_command)
1271
1272
1273    def is_wake_on_wifi_supported(self):
1274        """Returns true iff wake-on-WiFi is supported by the DUT."""
1275
1276        if (self.shill.get_dbus_property_on_device(
1277                    self.wifi_if, self.WAKE_ON_WIFI_FEATURES) ==
1278             WAKE_ON_WIFI_NOT_SUPPORTED):
1279            return False
1280        return True
1281
1282
1283class TemporaryDBusProperty:
1284    """Utility class to temporarily change a dbus property for the WiFi device.
1285
1286    Since dbus properties are global and persistent settings, we want
1287    to make sure that we change them back to what they were before the test
1288    started.
1289
1290    """
1291
1292    def __init__(self, shill_proxy, interface, prop_name, value):
1293        """Construct a TemporaryDBusProperty context manager.
1294
1295        @param shill_proxy: the shill proxy to use to communicate via dbus
1296        @param interface: the name of the interface we are setting the property for
1297        @param prop_name: the name of the property we want to set
1298        @param value: the desired value of the property
1299
1300        """
1301        self._shill = shill_proxy
1302        self._interface = interface
1303        self._prop_name = prop_name
1304        self._value = value
1305        self._saved_value = None
1306
1307
1308    def __enter__(self):
1309        logging.info('- Setting property %s on device %s',
1310                self._prop_name, self._interface)
1311
1312        self._saved_value = self._shill.get_dbus_property_on_device(
1313                self._interface, self._prop_name)
1314        if self._saved_value is None:
1315            raise error.TestFail('Device or property not found.')
1316        if not self._shill.set_dbus_property_on_device(self._interface,
1317                                                       self._prop_name,
1318                                                       self._value):
1319            raise error.TestFail('Could not set property')
1320
1321        logging.info('- Changed value from %s to %s',
1322                     self._saved_value,
1323                     self._value)
1324
1325
1326    def __exit__(self, exception, value, traceback):
1327        logging.info('- Resetting property %s', self._prop_name)
1328
1329        if not self._shill.set_dbus_property_on_device(self._interface,
1330                                                       self._prop_name,
1331                                                       self._saved_value):
1332            raise error.TestFail('Could not reset property')
1333