1#!/usr/bin/env python3 2# 3# Copyright 2020 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import requests 18import time 19 20from acts import logger 21from acts import signals 22 23import typing 24if typing.TYPE_CHECKING: 25 from acts.controllers.fuchsia_device import FuchsiaDevice 26 27SAVED_NETWORKS = "saved_networks" 28CLIENT_STATE = "client_connections_state" 29CONNECTIONS_ENABLED = "ConnectionsEnabled" 30CONNECTIONS_DISABLED = "ConnectionsDisabled" 31 32STATE_CONNECTED = 'Connected' 33STATE_CONNECTING = 'Connecting' 34STATE_DISCONNECTED = 'Disconnected' 35STATE_CONNECTION_STOPPED = 'ConnectionStopped' 36 37 38class WlanPolicyControllerError(signals.ControllerError): 39 pass 40 41 42class WlanPolicyController: 43 """Contains methods related to the wlan policy layer, to be used in the 44 FuchsiaDevice object. 45 """ 46 47 def __init__(self, fuchsia_device): 48 self.device: FuchsiaDevice = fuchsia_device 49 self.log = logger.create_tagged_trace_logger( 50 'WlanPolicyController for FuchsiaDevice | %s' % self.device.ip) 51 self.client_controller = False 52 self.preserved_networks_and_client_state = None 53 self.policy_configured = False 54 self._paused_session = False 55 56 def _configure_wlan(self, preserve_saved_networks, timeout=15): 57 """Sets up wlan policy layer. 58 59 Args: 60 preserve_saved_networks: bool, whether to clear existing saved 61 networks and client state, to be restored at test close. 62 """ 63 end_time = time.time() + timeout 64 65 # Kill basemgr (Component v1 version of session manager) 66 while time.time() < end_time: 67 response = self.device.basemgr_lib.killBasemgr() 68 if not response.get('error'): 69 self.log.debug('Basemgr kill call successfully issued.') 70 break 71 self.log.debug(response['error']) 72 time.sleep(1) 73 else: 74 raise WlanPolicyControllerError( 75 'Failed to issue successful basemgr kill call.') 76 77 # Stop the session manager, which also holds the Policy controller. 78 response = self.device.session_manager_lib.pauseSession() 79 if response.get('error'): 80 self.log.error('Failed to stop the session.') 81 raise WlanPolicyControllerError(response['error']) 82 else: 83 if response.get('result') == 'Success': 84 self._paused_session = True 85 self.log.debug(f"Paused session: {response.get('result')}") 86 87 # Acquire control of policy layer 88 controller_errors = [] 89 while time.time() < end_time: 90 # Create a client controller 91 response = self.device.wlan_policy_lib.wlanCreateClientController() 92 if response.get('error'): 93 controller_errors.append(response['error']) 94 self.log.debug(response['error']) 95 time.sleep(1) 96 continue 97 # Attempt to use the client controller (failure indicates a closed 98 # channel, meaning the client controller was rejected. 99 response = self.device.wlan_policy_lib.wlanGetSavedNetworks() 100 if response.get('error'): 101 controller_errors.append(response['error']) 102 self.log.debug(response['error']) 103 time.sleep(1) 104 continue 105 break 106 else: 107 self.log.warning( 108 "Failed to create and use a WLAN policy client controller. Errors: [" 109 + "; ".join(controller_errors) + "]") 110 raise WlanPolicyControllerError( 111 'Failed to create and use a WLAN policy client controller.') 112 113 self.log.info('ACTS tests now have control of the WLAN policy layer.') 114 115 if preserve_saved_networks and not self.preserved_networks_and_client_state: 116 self.preserved_networks_and_client_state = self.remove_and_preserve_networks_and_client_state( 117 ) 118 if not self.start_client_connections(): 119 raise WlanPolicyControllerError( 120 'Failed to start client connections during configuration.') 121 122 self.policy_configured = True 123 124 def _deconfigure_wlan(self): 125 if not self.stop_client_connections(): 126 raise WlanPolicyControllerError( 127 'Failed to stop client connections during deconfiguration.') 128 self.policy_configured = False 129 130 def _clean_up(self): 131 if self.preserved_networks_and_client_state: 132 # It is possible for policy to have been configured before, but 133 # deconfigured before test end. In this case, in must be setup 134 # before restoring networks 135 if not self.policy_configured: 136 self._configure_wlan() 137 self.restore_preserved_networks_and_client_state() 138 if self._paused_session: 139 response = self.device.session_manager_lib.resumeSession() 140 if response.get('error'): 141 self.log.warning('Failed to resume the session.') 142 self.log.warning(response['error']) 143 else: 144 self.log.debug(f"Resumed session: {response.get('result')}") 145 146 def start_client_connections(self): 147 """Allow device to connect to networks via policy layer (including 148 autoconnecting to saved networks). 149 150 Returns: 151 True, if successful. False otherwise.""" 152 start_response = self.device.wlan_policy_lib.wlanStartClientConnections( 153 ) 154 if start_response.get('error'): 155 self.log.error('Failed to start client connections. Err: %s' % 156 start_response['error']) 157 return False 158 return True 159 160 def stop_client_connections(self): 161 """Prevent device from connecting and autoconnecting to networks via the 162 policy layer. 163 164 Returns: 165 True, if successful. False otherwise.""" 166 stop_response = self.device.wlan_policy_lib.wlanStopClientConnections() 167 if stop_response.get('error'): 168 self.log.error('Failed to stop client connections. Err: %s' % 169 stop_response['error']) 170 return False 171 return True 172 173 def save_and_connect(self, ssid, security, password=None, timeout=30): 174 """ Saves and connects to the network. This is the policy version of 175 connect and check_connect_response because the policy layer 176 requires a saved network and the policy connect does not return 177 success or failure 178 179 Args: 180 ssid: string, the network name 181 security: string, security type of network (see wlan_policy_lib) 182 password: string, the credential of the network if applicable 183 timeout: int, time in seconds to wait for connection 184 185 Returns: 186 True, if successful. False otherwise. 187 """ 188 # Save network and check response 189 if not self.save_network(ssid, security, password=password): 190 return False 191 # Make connect call and check response 192 self.device.wlan_policy_lib.wlanSetNewListener() 193 if not self.send_connect_command(ssid, security): 194 return False 195 return self.wait_for_connect(ssid, security, timeout=timeout) 196 197 def save_and_wait_for_autoconnect(self, 198 ssid, 199 security, 200 password=None, 201 timeout=30): 202 """Saves a network and waits, expecting an autoconnection to the newly 203 saved network. This differes from save_and_connect, as it doesn't 204 expressly trigger a connection first. There are cases in which an 205 autoconnect won't occur after a save (like if the device is connected 206 already), so this should be used with caution to test very specific 207 situations. 208 209 Args: 210 ssid: string, the network name 211 security: string, security type of network (see wlan_policy_lib) 212 password: string, the credential of the network if applicable 213 timeout: int, time in seconds to wait for connection 214 215 Returns: 216 True, if successful. False otherwise. 217 """ 218 if not self.save_network(ssid, security, password=password): 219 return False 220 return self.wait_for_connect(ssid, security, timeout=timeout) 221 222 def remove_and_wait_for_disconnect(self, 223 ssid, 224 security_type, 225 password=None, 226 state=None, 227 status=None, 228 timeout=30): 229 """Removes a single network and waits for a disconnect. It is not 230 guaranteed the device will stay disconnected, as it may autoconnect 231 to a different saved network. 232 233 Args: 234 ssid: string, the network name 235 security: string, security type of network (see wlan_policy_lib) 236 password: string, the credential of the network if applicable 237 state: string, The connection state we are expecting, ie "Disconnected" or 238 "Failed" 239 status: string, The disconnect status we expect, it "ConnectionStopped" or 240 "ConnectionFailed" 241 timeout: int, time in seconds to wait for connection 242 243 Returns: 244 True, if successful. False otherwise. 245 """ 246 self.device.wlan_policy_lib.wlanSetNewListener() 247 if not self.remove_network(ssid, security_type, password=password): 248 return False 249 return self.wait_for_disconnect(ssid, 250 security_type, 251 state=state, 252 status=status, 253 timeout=timeout) 254 255 def remove_all_networks_and_wait_for_no_connections(self, timeout=30): 256 """Removes all networks and waits until device is not connected to any 257 networks. This should be used as the policy version of disconnect. 258 259 Returns: 260 True, if successful. False otherwise. 261 """ 262 self.device.wlan_policy_lib.wlanSetNewListener() 263 if not self.remove_all_networks(): 264 self.log.error('Failed to remove all networks. Cannot continue to ' 265 'wait_for_no_connections.') 266 return False 267 return self.wait_for_no_connections(timeout=timeout) 268 269 def save_network(self, ssid, security_type, password=None): 270 """Save a network via the policy layer. 271 272 Args: 273 ssid: string, the network name 274 security: string, security type of network (see wlan_policy_lib) 275 password: string, the credential of the network if applicable 276 277 Returns: 278 True, if successful. False otherwise. 279 """ 280 save_response = self.device.wlan_policy_lib.wlanSaveNetwork( 281 ssid, security_type, target_pwd=password) 282 if save_response.get('error'): 283 self.log.error('Failed to save network %s with error: %s' % 284 (ssid, save_response['error'])) 285 return False 286 return True 287 288 def remove_network(self, ssid, security_type, password=None): 289 """Remove a saved network via the policy layer. 290 291 Args: 292 ssid: string, the network name 293 security: string, security type of network (see wlan_policy_lib) 294 password: string, the credential of the network if applicable 295 296 Returns: 297 True, if successful. False otherwise. 298 """ 299 remove_response = self.device.wlan_policy_lib.wlanRemoveNetwork( 300 ssid, security_type, target_pwd=password) 301 if remove_response.get('error'): 302 self.log.error('Failed to remove network %s with error: %s' % 303 (ssid, remove_response['error'])) 304 return False 305 return True 306 307 def remove_all_networks(self): 308 """Removes all saved networks from device. 309 310 Returns: 311 True, if successful. False otherwise. 312 """ 313 remove_all_response = self.device.wlan_policy_lib.wlanRemoveAllNetworks( 314 ) 315 if remove_all_response.get('error'): 316 self.log.error('Error occurred removing all networks: %s' % 317 remove_all_response['error']) 318 return False 319 return True 320 321 def get_saved_networks(self): 322 """Retrieves saved networks from device. 323 324 Returns: 325 list of saved networks 326 327 Raises: 328 WlanPolicyControllerError, if retrieval fails. 329 """ 330 saved_networks_response = self.device.wlan_policy_lib.wlanGetSavedNetworks( 331 ) 332 if saved_networks_response.get('error'): 333 raise WlanPolicyControllerError( 334 'Failed to retrieve saved networks: %s' % 335 saved_networks_response['error']) 336 return saved_networks_response['result'] 337 338 def send_connect_command(self, ssid, security_type): 339 """Sends a connect command to a network that is already saved. This does 340 not wait to guarantee the connection is successful (for that, use 341 save_and_connect). 342 343 Args: 344 ssid: string, the network name 345 security: string, security type of network (see wlan_policy_lib) 346 password: string, the credential of the network if applicable 347 348 Returns: 349 True, if command send successfully. False otherwise. 350 """ 351 connect_response = self.device.wlan_policy_lib.wlanConnect( 352 ssid, security_type) 353 if connect_response.get('error'): 354 self.log.error( 355 'Error occurred when sending policy connect command: %s' % 356 connect_response['error']) 357 return False 358 return True 359 360 def wait_for_connect(self, ssid, security_type, timeout=30): 361 """ Wait until the device has connected to the specified network. 362 Args: 363 ssid: string, the network name 364 security: string, security type of network (see wlan_policy_lib) 365 timeout: int, seconds to wait for a update showing connection 366 Returns: 367 True if we see a connect to the network, False otherwise. 368 """ 369 security_type = str(security_type) 370 # Wait until we've connected. 371 end_time = time.time() + timeout 372 while time.time() < end_time: 373 time_left = max(1, int(end_time - time.time())) 374 375 try: 376 update = self.device.wlan_policy_lib.wlanGetUpdate( 377 timeout=time_left) 378 except requests.exceptions.Timeout: 379 self.log.error('Timed out waiting for response from device ' 380 'while waiting for network with SSID "%s" to ' 381 'connect. Device took too long to connect or ' 382 'the request timed out for another reason.' % 383 ssid) 384 self.device.wlan_policy_lib.wlanSetNewListener() 385 return False 386 if update.get('error'): 387 # This can occur for many reasons, so it is not necessarily a 388 # failure. 389 self.log.debug('Error occurred getting status update: %s' % 390 update['error']) 391 continue 392 393 for network in update['result']['networks']: 394 if network['id']['ssid'] == ssid or network['id'][ 395 'type_'].lower() == security_type.lower(): 396 if 'state' not in network: 397 raise WlanPolicyControllerError( 398 'WLAN status missing state field.') 399 elif network['state'].lower() == STATE_CONNECTED.lower(): 400 return True 401 # Wait a bit before requesting another status update 402 time.sleep(1) 403 # Stopped getting updates because out timeout 404 self.log.error('Timed out waiting for network with SSID "%s" to ' 405 "connect" % ssid) 406 return False 407 408 def wait_for_disconnect(self, 409 ssid, 410 security_type, 411 state=None, 412 status=None, 413 timeout=30): 414 """ Wait for a disconnect of the specified network on the given device. This 415 will check that the correct connection state and disconnect status are 416 given in update. If we do not see a disconnect after some time, 417 return false. 418 419 Args: 420 ssid: string, the network name 421 security: string, security type of network (see wlan_policy_lib) 422 state: string, The connection state we are expecting, ie "Disconnected" or 423 "Failed" 424 status: string, The disconnect status we expect, it "ConnectionStopped" or 425 "ConnectionFailed" 426 timeout: int, seconds to wait before giving up 427 428 Returns: True if we saw a disconnect as specified, or False otherwise. 429 """ 430 if not state: 431 state = STATE_DISCONNECTED 432 if not status: 433 status = STATE_CONNECTION_STOPPED 434 435 end_time = time.time() + timeout 436 while time.time() < end_time: 437 time_left = max(1, int(end_time - time.time())) 438 try: 439 update = self.device.wlan_policy_lib.wlanGetUpdate( 440 timeout=time_left) 441 except requests.exceptions.Timeout: 442 self.log.error( 443 'Timed out waiting for response from device ' 444 'while waiting for network with SSID "%s" to ' 445 'disconnect. Device took too long to disconnect ' 446 'or the request timed out for another reason.' % ssid) 447 self.device.wlan_policy_lib.wlanSetNewListener() 448 return False 449 450 if update.get('error'): 451 # This can occur for many reasons, so it is not necessarily a 452 # failure. 453 self.log.debug('Error occurred getting status update: %s' % 454 update['error']) 455 continue 456 # Update should include network, either connected to or recently disconnected. 457 if len(update['result']['networks']) == 0: 458 raise WlanPolicyControllerError( 459 'WLAN state update is missing network.') 460 461 for network in update['result']['networks']: 462 if network['id']['ssid'] == ssid or network['id'][ 463 'type_'].lower() == security_type.lower(): 464 if 'state' not in network or 'status' not in network: 465 raise WlanPolicyControllerError( 466 'Client state summary\'s network is missing fields' 467 ) 468 # If still connected, we will wait for another update and check again 469 elif network['state'].lower() == STATE_CONNECTED.lower(): 470 continue 471 elif network['state'].lower() == STATE_CONNECTING.lower(): 472 self.log.error( 473 'Update is "Connecting", but device should already be ' 474 'connected; expected disconnect') 475 return False 476 # Check that the network state and disconnect status are expected, ie 477 # that it isn't ConnectionFailed when we expect ConnectionStopped 478 elif network['state'].lower() != state.lower( 479 ) or network['status'].lower() != status.lower(): 480 self.log.error( 481 'Connection failed: a network failure occurred that is unrelated' 482 'to remove network or incorrect status update. \nExpected state: ' 483 '%s, Status: %s,\nActual update: %s' % 484 (state, status, network)) 485 return False 486 else: 487 return True 488 # Wait a bit before requesting another status update 489 time.sleep(1) 490 # Stopped getting updates because out timeout 491 self.log.error('Timed out waiting for network with SSID "%s" to ' 492 'connect' % ssid) 493 return False 494 495 def wait_for_no_connections(self, timeout=30): 496 """ Waits to see that there are no existing connections the device. This 497 is the simplest way to watch for disconnections when only a single 498 network is saved/present. 499 500 Args: 501 timeout: int, time in seconds to wait to see no connections 502 503 Returns: 504 True, if successful. False, if still connected after timeout. 505 """ 506 # If there are already no existing connections when this function is called, 507 # then an update won't be generated by the device, and we'll time out. 508 # Force an update by getting a new listener. 509 self.device.wlan_policy_lib.wlanSetNewListener() 510 end_time = time.time() + timeout 511 while time.time() < end_time: 512 time_left = max(1, int(end_time - time.time())) 513 try: 514 update = self.device.wlan_policy_lib.wlanGetUpdate( 515 timeout=time_left) 516 except requests.exceptions.Timeout: 517 self.log.info( 518 "Timed out getting status update while waiting for all" 519 " connections to end.") 520 self.device.wlan_policy_lib.wlanSetNewListener() 521 return False 522 523 if update["error"] != None: 524 self.log.info("Failed to get status update") 525 return False 526 # If any network is connected or being connected to, wait for them 527 # to disconnect. 528 if any(network['state'].lower() in 529 {STATE_CONNECTED.lower(), 530 STATE_CONNECTING.lower()} 531 for network in update['result']['networks']): 532 continue 533 else: 534 return True 535 return False 536 537 def remove_and_preserve_networks_and_client_state(self): 538 """ Preserves networks already saved on devices before removing them to 539 setup up for a clean test environment. Records the state of client 540 connections before tests. 541 542 Raises: 543 WlanPolicyControllerError, if the network removal is unsuccessful 544 """ 545 # Save preexisting saved networks 546 preserved_networks_and_state = {} 547 saved_networks_response = self.device.wlan_policy_lib.wlanGetSavedNetworks( 548 ) 549 if saved_networks_response.get('error'): 550 raise WlanPolicyControllerError( 551 'Failed to get preexisting saved networks: %s' % 552 saved_networks_response['error']) 553 if saved_networks_response.get('result') != None: 554 preserved_networks_and_state[ 555 SAVED_NETWORKS] = saved_networks_response['result'] 556 557 # Remove preexisting saved networks 558 if not self.remove_all_networks(): 559 raise WlanPolicyControllerError( 560 'Failed to clear networks and disconnect at FuchsiaDevice creation.' 561 ) 562 563 self.device.wlan_policy_lib.wlanSetNewListener() 564 update_response = self.device.wlan_policy_lib.wlanGetUpdate() 565 update_result = update_response.get('result', {}) 566 if update_result.get('state'): 567 preserved_networks_and_state[CLIENT_STATE] = update_result['state'] 568 else: 569 self.log.warn('Failed to get update; test will not start or ' 570 'stop client connections at the end of the test.') 571 572 self.log.info('Saved networks cleared and preserved.') 573 return preserved_networks_and_state 574 575 def restore_preserved_networks_and_client_state(self): 576 """ Restore saved networks and client state onto device if they have 577 been preserved. 578 """ 579 if not self.remove_all_networks(): 580 self.log.warn('Failed to remove saved networks before restore.') 581 restore_success = True 582 for network in self.preserved_networks_and_client_state[ 583 SAVED_NETWORKS]: 584 if not self.save_network(network["ssid"], network["security_type"], 585 network["credential_value"]): 586 self.log.warn('Failed to restore network (%s).' % 587 network['ssid']) 588 restore_success = False 589 starting_state = self.preserved_networks_and_client_state[CLIENT_STATE] 590 if starting_state == CONNECTIONS_ENABLED: 591 state_restored = self.start_client_connections() 592 else: 593 state_restored = self.stop_client_connections() 594 if not state_restored: 595 self.log.warn('Failed to restore client connections state.') 596 restore_success = False 597 if restore_success: 598 self.log.info('Preserved networks and client state restored.') 599 self.preserved_networks_and_client_state = None 600 return restore_success 601