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