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