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