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