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