• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2018 - 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 backoff
18import json
19import logging
20import os
21import random
22import re
23import requests
24import socket
25import subprocess
26import time
27
28from acts import context
29from acts import logger as acts_logger
30from acts import signals
31from acts import utils
32from acts.controllers import pdu
33from acts.libs.proc import job
34from acts.utils import get_fuchsia_mdns_ipv6_address
35
36from acts.controllers.fuchsia_lib.audio_lib import FuchsiaAudioLib
37from acts.controllers.fuchsia_lib.backlight_lib import FuchsiaBacklightLib
38from acts.controllers.fuchsia_lib.basemgr_lib import FuchsiaBasemgrLib
39from acts.controllers.fuchsia_lib.bt.avdtp_lib import FuchsiaAvdtpLib
40from acts.controllers.fuchsia_lib.bt.ble_lib import FuchsiaBleLib
41from acts.controllers.fuchsia_lib.bt.bts_lib import FuchsiaBtsLib
42from acts.controllers.fuchsia_lib.bt.gattc_lib import FuchsiaGattcLib
43from acts.controllers.fuchsia_lib.bt.gatts_lib import FuchsiaGattsLib
44from acts.controllers.fuchsia_lib.bt.hfp_lib import FuchsiaHfpLib
45from acts.controllers.fuchsia_lib.bt.rfcomm_lib import FuchsiaRfcommLib
46from acts.controllers.fuchsia_lib.bt.sdp_lib import FuchsiaProfileServerLib
47from acts.controllers.fuchsia_lib.ffx import FFX
48from acts.controllers.fuchsia_lib.gpio_lib import FuchsiaGpioLib
49from acts.controllers.fuchsia_lib.hardware_power_statecontrol_lib import FuchsiaHardwarePowerStatecontrolLib
50from acts.controllers.fuchsia_lib.hwinfo_lib import FuchsiaHwinfoLib
51from acts.controllers.fuchsia_lib.i2c_lib import FuchsiaI2cLib
52from acts.controllers.fuchsia_lib.input_report_lib import FuchsiaInputReportLib
53from acts.controllers.fuchsia_lib.kernel_lib import FuchsiaKernelLib
54from acts.controllers.fuchsia_lib.lib_controllers.netstack_controller import NetstackController
55from acts.controllers.fuchsia_lib.lib_controllers.wlan_controller import WlanController
56from acts.controllers.fuchsia_lib.lib_controllers.wlan_policy_controller import WlanPolicyController
57from acts.controllers.fuchsia_lib.light_lib import FuchsiaLightLib
58from acts.controllers.fuchsia_lib.location.regulatory_region_lib import FuchsiaRegulatoryRegionLib
59from acts.controllers.fuchsia_lib.logging_lib import FuchsiaLoggingLib
60from acts.controllers.fuchsia_lib.netstack.netstack_lib import FuchsiaNetstackLib
61from acts.controllers.fuchsia_lib.ram_lib import FuchsiaRamLib
62from acts.controllers.fuchsia_lib.session_manager_lib import FuchsiaSessionManagerLib
63from acts.controllers.fuchsia_lib.sysinfo_lib import FuchsiaSysInfoLib
64from acts.controllers.fuchsia_lib.syslog_lib import FuchsiaSyslogError
65from acts.controllers.fuchsia_lib.syslog_lib import create_syslog_process
66from acts.controllers.fuchsia_lib.utils_lib import SshResults
67from acts.controllers.fuchsia_lib.utils_lib import create_ssh_connection
68from acts.controllers.fuchsia_lib.utils_lib import flash
69from acts.controllers.fuchsia_lib.wlan_ap_policy_lib import FuchsiaWlanApPolicyLib
70from acts.controllers.fuchsia_lib.wlan_deprecated_configuration_lib import FuchsiaWlanDeprecatedConfigurationLib
71from acts.controllers.fuchsia_lib.wlan_lib import FuchsiaWlanLib
72from acts.controllers.fuchsia_lib.wlan_policy_lib import FuchsiaWlanPolicyLib
73
74MOBLY_CONTROLLER_CONFIG_NAME = "FuchsiaDevice"
75ACTS_CONTROLLER_REFERENCE_NAME = "fuchsia_devices"
76
77CONTROL_PATH_REPLACE_VALUE = " ControlPath /tmp/fuchsia--%r@%h:%p"
78
79FUCHSIA_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
80FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
81FUCHSIA_DEVICE_INVALID_CONFIG = ("Fuchsia device config must be either a str "
82                                 "or dict. abort! Invalid element %i in %r")
83FUCHSIA_DEVICE_NO_IP_MSG = "No IP address specified, abort!"
84FUCHSIA_COULD_NOT_GET_DESIRED_STATE = "Could not %s %s."
85FUCHSIA_INVALID_CONTROL_STATE = "Invalid control state (%s). abort!"
86FUCHSIA_SSH_CONFIG_NOT_DEFINED = ("Cannot send ssh commands since the "
87                                  "ssh_config was not specified in the Fuchsia"
88                                  "device config.")
89
90FUCHSIA_SSH_USERNAME = "fuchsia"
91FUCHSIA_TIME_IN_NANOSECONDS = 1000000000
92
93SL4F_APK_NAME = "com.googlecode.android_scripting"
94DAEMON_INIT_TIMEOUT_SEC = 1
95
96DAEMON_ACTIVATED_STATES = ["running", "start"]
97DAEMON_DEACTIVATED_STATES = ["stop", "stopped"]
98
99FUCHSIA_DEFAULT_LOG_CMD = 'iquery --absolute_paths --cat --format= --recursive'
100FUCHSIA_DEFAULT_LOG_ITEMS = [
101    '/hub/c/scenic.cmx/[0-9]*/out/objects',
102    '/hub/c/root_presenter.cmx/[0-9]*/out/objects',
103    '/hub/c/wlanstack2.cmx/[0-9]*/out/public',
104    '/hub/c/basemgr.cmx/[0-9]*/out/objects'
105]
106
107FUCHSIA_RECONNECT_AFTER_REBOOT_TIME = 5
108
109CHANNEL_OPEN_TIMEOUT = 5
110
111FUCHSIA_GET_VERSION_CMD = 'cat /config/build-info/version'
112
113FUCHSIA_REBOOT_TYPE_SOFT = 'soft'
114FUCHSIA_REBOOT_TYPE_SOFT_AND_FLASH = 'flash'
115FUCHSIA_REBOOT_TYPE_HARD = 'hard'
116
117FUCHSIA_DEFAULT_CONNECT_TIMEOUT = 60
118FUCHSIA_DEFAULT_COMMAND_TIMEOUT = 60
119
120FUCHSIA_DEFAULT_CLEAN_UP_COMMAND_TIMEOUT = 15
121
122FUCHSIA_COUNTRY_CODE_TIMEOUT = 15
123FUCHSIA_DEFAULT_COUNTRY_CODE_US = 'US'
124
125MDNS_LOOKUP_RETRY_MAX = 3
126
127VALID_ASSOCIATION_MECHANISMS = {None, 'policy', 'drivers'}
128
129
130class FuchsiaDeviceError(signals.ControllerError):
131    pass
132
133
134def create(configs):
135    if not configs:
136        raise FuchsiaDeviceError(FUCHSIA_DEVICE_EMPTY_CONFIG_MSG)
137    elif not isinstance(configs, list):
138        raise FuchsiaDeviceError(FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG)
139    for index, config in enumerate(configs):
140        if isinstance(config, str):
141            configs[index] = {"ip": config}
142        elif not isinstance(config, dict):
143            raise FuchsiaDeviceError(FUCHSIA_DEVICE_INVALID_CONFIG %
144                                     (index, configs))
145    return get_instances(configs)
146
147
148def destroy(fds):
149    for fd in fds:
150        fd.clean_up()
151        del fd
152
153
154def get_info(fds):
155    """Get information on a list of FuchsiaDevice objects.
156
157    Args:
158        fds: A list of FuchsiaDevice objects.
159
160    Returns:
161        A list of dict, each representing info for FuchsiaDevice objects.
162    """
163    device_info = []
164    for fd in fds:
165        info = {"ip": fd.ip}
166        device_info.append(info)
167    return device_info
168
169
170def get_instances(fds_conf_data):
171    """Create FuchsiaDevice instances from a list of Fuchsia ips.
172
173    Args:
174        fds_conf_data: A list of dicts that contain Fuchsia device info.
175
176    Returns:
177        A list of FuchsiaDevice objects.
178    """
179
180    return [FuchsiaDevice(fd_conf_data) for fd_conf_data in fds_conf_data]
181
182
183class FuchsiaDevice:
184    """Class representing a Fuchsia device.
185
186    Each object of this class represents one Fuchsia device in ACTS.
187
188    Attributes:
189        ip: The full address or Fuchsia abstract name to contact the Fuchsia
190            device at
191        log: A logger object.
192        ssh_port: The SSH TCP port number of the Fuchsia device.
193        sl4f_port: The SL4F HTTP port number of the Fuchsia device.
194        ssh_config: The ssh_config for connecting to the Fuchsia device.
195    """
196
197    def __init__(self, fd_conf_data):
198        """
199        Args:
200            fd_conf_data: A dict of a fuchsia device configuration data
201                Required keys:
202                    ip: IP address of fuchsia device
203                optional key:
204                    sl4_port: Port for the sl4f web server on the fuchsia device
205                              (Default: 80)
206                    ssh_config: Location of the ssh_config file to connect to
207                        the fuchsia device
208                        (Default: None)
209                    ssh_port: Port for the ssh server on the fuchsia device
210                              (Default: 22)
211        """
212        self.conf_data = fd_conf_data
213        if "ip" not in fd_conf_data:
214            raise FuchsiaDeviceError(FUCHSIA_DEVICE_NO_IP_MSG)
215        self.ip = fd_conf_data["ip"]
216        self.orig_ip = fd_conf_data["ip"]
217        self.sl4f_port = fd_conf_data.get("sl4f_port", 80)
218        self.ssh_port = fd_conf_data.get("ssh_port", 22)
219        self.ssh_config = fd_conf_data.get("ssh_config", None)
220        self.ssh_priv_key = fd_conf_data.get("ssh_priv_key", None)
221        self.authorized_file = fd_conf_data.get("authorized_file_loc", None)
222        self.serial_number = fd_conf_data.get("serial_number", None)
223        self.device_type = fd_conf_data.get("device_type", None)
224        self.product_type = fd_conf_data.get("product_type", None)
225        self.board_type = fd_conf_data.get("board_type", None)
226        self.build_number = fd_conf_data.get("build_number", None)
227        self.build_type = fd_conf_data.get("build_type", None)
228        self.server_path = fd_conf_data.get("server_path", None)
229        self.specific_image = fd_conf_data.get("specific_image", None)
230        self.ffx_binary_path = fd_conf_data.get("ffx_binary_path", None)
231        self.mdns_name = fd_conf_data.get("mdns_name", None)
232
233        # Instead of the input ssh_config, a new config is generated with proper
234        # ControlPath to the test output directory.
235        output_path = context.get_current_context().get_base_output_path()
236        generated_ssh_config = os.path.join(output_path,
237                                            "ssh_config_{}".format(self.ip))
238        self._set_control_path_config(self.ssh_config, generated_ssh_config)
239        self.ssh_config = generated_ssh_config
240
241        self.ssh_username = fd_conf_data.get("ssh_username",
242                                             FUCHSIA_SSH_USERNAME)
243        self.hard_reboot_on_fail = fd_conf_data.get("hard_reboot_on_fail",
244                                                    False)
245        self.take_bug_report_on_fail = fd_conf_data.get(
246            "take_bug_report_on_fail", False)
247        self.device_pdu_config = fd_conf_data.get("PduDevice", None)
248        self.config_country_code = fd_conf_data.get(
249            'country_code', FUCHSIA_DEFAULT_COUNTRY_CODE_US).upper()
250        self._persistent_ssh_conn = None
251
252        # WLAN interface info is populated inside configure_wlan
253        self.wlan_client_interfaces = {}
254        self.wlan_ap_interfaces = {}
255        self.wlan_client_test_interface_name = fd_conf_data.get(
256            'wlan_client_test_interface', None)
257        self.wlan_ap_test_interface_name = fd_conf_data.get(
258            'wlan_ap_test_interface', None)
259
260        # Whether to use 'policy' or 'drivers' for WLAN connect/disconnect calls
261        # If set to None, wlan is not configured.
262        self.association_mechanism = None
263        # Defaults to policy layer, unless otherwise specified in the config
264        self.default_association_mechanism = fd_conf_data.get(
265            'association_mechanism', 'policy')
266
267        # Whether to clear and preserve existing saved networks and client
268        # connections state, to be restored at device teardown.
269        self.default_preserve_saved_networks = fd_conf_data.get(
270            'preserve_saved_networks', True)
271
272        if utils.is_valid_ipv4_address(self.ip):
273            self.address = "http://{}:{}".format(self.ip, self.sl4f_port)
274        elif utils.is_valid_ipv6_address(self.ip):
275            self.address = "http://[{}]:{}".format(self.ip, self.sl4f_port)
276        else:
277            mdns_ip = None
278            for retry_counter in range(MDNS_LOOKUP_RETRY_MAX):
279                mdns_ip = get_fuchsia_mdns_ipv6_address(self.ip)
280                if mdns_ip:
281                    break
282                else:
283                    time.sleep(1)
284            if mdns_ip and utils.is_valid_ipv6_address(mdns_ip):
285                # self.ip was actually an mdns name. Use it for self.mdns_name
286                # unless one was explicitly provided.
287                self.mdns_name = self.mdns_name or self.ip
288                self.ip = mdns_ip
289                self.address = "http://[{}]:{}".format(self.ip, self.sl4f_port)
290            else:
291                raise ValueError('Invalid IP: %s' % self.ip)
292
293        self.log = acts_logger.create_tagged_trace_logger(
294            "FuchsiaDevice | %s" % self.orig_ip)
295
296        self.init_address = self.address + "/init"
297        self.cleanup_address = self.address + "/cleanup"
298        self.print_address = self.address + "/print_clients"
299        self.ping_rtt_match = re.compile(r'RTT Min/Max/Avg '
300                                         r'= \[ (.*?) / (.*?) / (.*?) \] ms')
301
302        # TODO(): Come up with better client numbering system
303        self.client_id = "FuchsiaClient" + str(random.randint(0, 1000000))
304        self.test_counter = 0
305        self.serial = re.sub('[.:%]', '_', self.ip)
306        log_path_base = getattr(logging, 'log_path', '/tmp/logs')
307        self.log_path = os.path.join(log_path_base,
308                                     'FuchsiaDevice%s' % self.serial)
309        self.fuchsia_log_file_path = os.path.join(
310            self.log_path, "fuchsialog_%s_debug.txt" % self.serial)
311        self.log_process = None
312
313        self.init_libraries()
314
315        self.setup_commands = fd_conf_data.get('setup_commands', [])
316        self.teardown_commands = fd_conf_data.get('teardown_commands', [])
317
318        try:
319            self.start_services()
320            self.run_commands_from_config(self.setup_commands)
321        except Exception as e:
322            # Prevent a threading error, since controller isn't fully up yet.
323            self.clean_up()
324            raise e
325
326    def _set_control_path_config(self, old_config, new_config):
327        """Given an input ssh_config, write to a new config with proper
328        ControlPath values in place, if it doesn't exist already.
329
330        Args:
331            old_config: string, path to the input config
332            new_config: string, path to store the new config
333        """
334        if os.path.isfile(new_config):
335            return
336
337        ssh_config_copy = ""
338
339        with open(old_config, 'r') as file:
340            ssh_config_copy = re.sub('(\sControlPath\s.*)',
341                                     CONTROL_PATH_REPLACE_VALUE,
342                                     file.read(),
343                                     flags=re.M)
344        with open(new_config, 'w') as file:
345            file.write(ssh_config_copy)
346
347    def init_libraries(self):
348        # Grab commands from FuchsiaAudioLib
349        self.audio_lib = FuchsiaAudioLib(self.address, self.test_counter,
350                                         self.client_id)
351
352        # Grab commands from FuchsiaAvdtpLib
353        self.avdtp_lib = FuchsiaAvdtpLib(self.address, self.test_counter,
354                                         self.client_id)
355
356        # Grab commands from FuchsiaHfpLib
357        self.hfp_lib = FuchsiaHfpLib(self.address, self.test_counter,
358                                     self.client_id)
359
360        # Grab commands from FuchsiaRfcommLib
361        self.rfcomm_lib = FuchsiaRfcommLib(self.address, self.test_counter,
362                                           self.client_id)
363
364        # Grab commands from FuchsiaLightLib
365        self.light_lib = FuchsiaLightLib(self.address, self.test_counter,
366                                         self.client_id)
367
368        # Grab commands from FuchsiaBacklightLib
369        self.backlight_lib = FuchsiaBacklightLib(self.address,
370                                                 self.test_counter,
371                                                 self.client_id)
372
373        # Grab commands from FuchsiaBasemgrLib
374        self.basemgr_lib = FuchsiaBasemgrLib(self.address, self.test_counter,
375                                             self.client_id)
376        # Grab commands from FuchsiaBleLib
377        self.ble_lib = FuchsiaBleLib(self.address, self.test_counter,
378                                     self.client_id)
379        # Grab commands from FuchsiaBtsLib
380        self.bts_lib = FuchsiaBtsLib(self.address, self.test_counter,
381                                     self.client_id)
382        # Grab commands from FuchsiaGattcLib
383        self.gattc_lib = FuchsiaGattcLib(self.address, self.test_counter,
384                                         self.client_id)
385        # Grab commands from FuchsiaGattsLib
386        self.gatts_lib = FuchsiaGattsLib(self.address, self.test_counter,
387                                         self.client_id)
388
389        # Grab commands from FuchsiaGpioLib
390        self.gpio_lib = FuchsiaGpioLib(self.address, self.test_counter,
391                                       self.client_id)
392
393        # Grab commands from FuchsiaHardwarePowerStatecontrolLib
394        self.hardware_power_statecontrol_lib = (
395            FuchsiaHardwarePowerStatecontrolLib(self.address,
396                                                self.test_counter,
397                                                self.client_id))
398
399        # Grab commands from FuchsiaHwinfoLib
400        self.hwinfo_lib = FuchsiaHwinfoLib(self.address, self.test_counter,
401                                           self.client_id)
402
403        # Grab commands from FuchsiaI2cLib
404        self.i2c_lib = FuchsiaI2cLib(self.address, self.test_counter,
405                                     self.client_id)
406
407        # Grab commands from FuchsiaInputReportLib
408        self.input_report_lib = FuchsiaInputReportLib(self.address,
409                                                      self.test_counter,
410                                                      self.client_id)
411
412        # Grab commands from FuchsiaKernelLib
413        self.kernel_lib = FuchsiaKernelLib(self.address, self.test_counter,
414                                           self.client_id)
415
416        # Grab commands from FuchsiaLoggingLib
417        self.logging_lib = FuchsiaLoggingLib(self.address, self.test_counter,
418                                             self.client_id)
419
420        # Grab commands from FuchsiaNetstackLib
421        self.netstack_lib = FuchsiaNetstackLib(self.address, self.test_counter,
422                                               self.client_id)
423
424        # Grab commands from FuchsiaLightLib
425        self.ram_lib = FuchsiaRamLib(self.address, self.test_counter,
426                                     self.client_id)
427
428        # Grab commands from FuchsiaProfileServerLib
429        self.sdp_lib = FuchsiaProfileServerLib(self.address, self.test_counter,
430                                               self.client_id)
431
432        # Grab commands from FuchsiaRegulatoryRegionLib
433        self.regulatory_region_lib = FuchsiaRegulatoryRegionLib(
434            self.address, self.test_counter, self.client_id)
435
436        # Grab commands from FuchsiaSysInfoLib
437        self.sysinfo_lib = FuchsiaSysInfoLib(self.address, self.test_counter,
438                                             self.client_id)
439
440        # Grab commands from FuchsiaSessionManagerLib
441        self.session_manager_lib = FuchsiaSessionManagerLib(self)
442
443        # Grabs command from FuchsiaWlanDeprecatedConfigurationLib
444        self.wlan_deprecated_configuration_lib = (
445            FuchsiaWlanDeprecatedConfigurationLib(self.address,
446                                                  self.test_counter,
447                                                  self.client_id))
448
449        # Grab commands from FuchsiaWlanLib
450        self.wlan_lib = FuchsiaWlanLib(self.address, self.test_counter,
451                                       self.client_id)
452
453        # Grab commands from FuchsiaWlanApPolicyLib
454        self.wlan_ap_policy_lib = FuchsiaWlanApPolicyLib(
455            self.address, self.test_counter, self.client_id)
456
457        # Grab commands from FuchsiaWlanPolicyLib
458        self.wlan_policy_lib = FuchsiaWlanPolicyLib(self.address,
459                                                    self.test_counter,
460                                                    self.client_id)
461
462        # Contains Netstack functions
463        self.netstack_controller = NetstackController(self)
464
465        # Contains WLAN core functions
466        self.wlan_controller = WlanController(self)
467
468        # Contains WLAN policy functions like save_network, remove_network, etc
469        self.wlan_policy_controller = WlanPolicyController(self)
470
471    @backoff.on_exception(
472        backoff.constant,
473        (ConnectionRefusedError, requests.exceptions.ConnectionError),
474        interval=1.5,
475        max_tries=4)
476    def init_sl4f_connection(self):
477        """Initializes HTTP connection with SL4F server."""
478        self.log.debug("Initializing SL4F server connection")
479        init_data = json.dumps({
480            "jsonrpc": "2.0",
481            "id": self.build_id(self.test_counter),
482            "method": "sl4f.sl4f_init",
483            "params": {
484                "client_id": self.client_id
485            }
486        })
487
488        requests.get(url=self.init_address, data=init_data)
489        self.test_counter += 1
490
491    def init_ffx_connection(self):
492        """Initializes ffx's connection to the device.
493
494        If ffx has already been initialized, it will be reinitialized. This will
495        break any running tests calling ffx for this device.
496        """
497        self.log.debug("Initializing ffx connection")
498
499        if not self.ffx_binary_path:
500            raise ValueError(
501                'Must provide "ffx_binary_path: <path to FFX binary>" in the device config'
502            )
503        if not self.mdns_name:
504            raise ValueError(
505                'Must provide "mdns_name: <device mDNS name>" in the device config'
506            )
507
508        if hasattr(self, 'ffx'):
509            self.ffx.clean_up()
510
511        self.ffx = FFX(self.ffx_binary_path, self.mdns_name, self.ssh_priv_key)
512
513    def run_commands_from_config(self, cmd_dicts):
514        """Runs commands on the Fuchsia device from the config file. Useful for
515        device and/or Fuchsia specific configuration.
516
517        Args:
518            cmd_dicts: list of dictionaries containing the following
519                'cmd': string, command to run on device
520                'timeout': int, seconds to wait for command to run (optional)
521                'skip_status_code_check': bool, disregard errors if true
522        """
523        for cmd_dict in cmd_dicts:
524            try:
525                cmd = cmd_dict['cmd']
526            except KeyError:
527                raise FuchsiaDeviceError(
528                    'To run a command via config, you must provide key "cmd" '
529                    'containing the command string.')
530
531            timeout = cmd_dict.get('timeout', FUCHSIA_DEFAULT_COMMAND_TIMEOUT)
532            # Catch both boolean and string values from JSON
533            skip_status_code_check = 'true' == str(
534                cmd_dict.get('skip_status_code_check', False)).lower()
535
536            self.log.info(
537                'Running command "%s".%s' %
538                (cmd, ' Ignoring result.' if skip_status_code_check else ''))
539            result = self.send_command_ssh(
540                cmd,
541                timeout=timeout,
542                skip_status_code_check=skip_status_code_check)
543
544            if isinstance(result, Exception):
545                raise result
546
547            elif not skip_status_code_check and result.stderr:
548                raise FuchsiaDeviceError(
549                    'Error when running command "%s": %s' %
550                    (cmd, result.stderr))
551
552    def build_id(self, test_id):
553        """Concatenates client_id and test_id to form a command_id
554
555        Args:
556            test_id: string, unique identifier of test command
557        """
558        return self.client_id + "." + str(test_id)
559
560    def configure_wlan(self,
561                       association_mechanism=None,
562                       preserve_saved_networks=None):
563        """
564        Readies device for WLAN functionality. If applicable, connects to the
565        policy layer and clears/saves preexisting saved networks.
566
567        Args:
568            association_mechanism: string, 'policy' or 'drivers'. If None, uses
569                the default value from init (can be set by ACTS config)
570            preserve_saved_networks: bool, whether to clear existing saved
571                networks, and preserve them for restoration later. If None, uses
572                the default value from init (can be set by ACTS config)
573
574        Raises:
575            FuchsiaDeviceError, if configuration fails
576        """
577
578        # Set the country code US by default, or country code provided
579        # in ACTS config
580        self.configure_regulatory_domain(self.config_country_code)
581
582        # If args aren't provided, use the defaults, which can be set in the
583        # config.
584        if association_mechanism is None:
585            association_mechanism = self.default_association_mechanism
586        if preserve_saved_networks is None:
587            preserve_saved_networks = self.default_preserve_saved_networks
588
589        if association_mechanism not in VALID_ASSOCIATION_MECHANISMS:
590            raise FuchsiaDeviceError(
591                'Invalid FuchsiaDevice association_mechanism: %s' %
592                association_mechanism)
593
594        # Allows for wlan to be set up differently in different tests
595        if self.association_mechanism:
596            self.deconfigure_wlan()
597
598        self.association_mechanism = association_mechanism
599
600        self.log.info('Configuring WLAN w/ association mechanism: %s' %
601                      association_mechanism)
602        if association_mechanism == 'drivers':
603            self.log.warn(
604                'You may encounter unusual device behavior when using the '
605                'drivers directly for WLAN. This should be reserved for '
606                'debugging specific issues. Normal test runs should use the '
607                'policy layer.')
608            if preserve_saved_networks:
609                self.log.warn(
610                    'Unable to preserve saved networks when using drivers '
611                    'association mechanism (requires policy layer control).')
612        else:
613            # This requires SL4F calls, so it can only happen with actual
614            # devices, not with unit tests.
615            self.wlan_policy_controller._configure_wlan(
616                preserve_saved_networks)
617
618        # Retrieve WLAN client and AP interfaces
619        self.wlan_controller.update_wlan_interfaces()
620
621    def deconfigure_wlan(self):
622        """
623        Stops WLAN functionality (if it has been started). Used to allow
624        different tests to use WLAN differently (e.g. some tests require using
625        wlan policy, while the abstract wlan_device can be setup to use policy
626        or drivers)
627
628        Raises:
629            FuchsiaDeviveError, if deconfigure fails.
630        """
631        if not self.association_mechanism:
632            self.log.debug(
633                'WLAN not configured before deconfigure was called.')
634            return
635        # If using policy, stop client connections. Otherwise, just clear
636        # variables.
637        if self.association_mechanism != 'drivers':
638            self.wlan_policy_controller._deconfigure_wlan()
639        self.association_mechanism = None
640
641    def reboot(self,
642               use_ssh=False,
643               unreachable_timeout=FUCHSIA_DEFAULT_CONNECT_TIMEOUT,
644               ping_timeout=FUCHSIA_DEFAULT_CONNECT_TIMEOUT,
645               ssh_timeout=FUCHSIA_DEFAULT_CONNECT_TIMEOUT,
646               reboot_type=FUCHSIA_REBOOT_TYPE_SOFT,
647               testbed_pdus=None):
648        """Reboot a FuchsiaDevice.
649
650        Soft reboots the device, verifies it becomes unreachable, then verifies
651        it comes back online. Re-initializes services so the tests can continue.
652
653        Args:
654            use_ssh: bool, if True, use fuchsia shell command via ssh to reboot
655                instead of SL4F.
656            unreachable_timeout: int, time to wait for device to become
657                unreachable.
658            ping_timeout: int, time to wait for device to respond to pings.
659            ssh_timeout: int, time to wait for device to be reachable via ssh.
660            reboot_type: boolFUCHSIA_REBOOT_TYPE_SOFT or
661                FUCHSIA_REBOOT_TYPE_HARD
662            testbed_pdus: list, all testbed PDUs
663
664        Raises:
665            ConnectionError, if device fails to become unreachable, fails to
666                come back up, or if SL4F does not setup correctly.
667        """
668        skip_unreachable_check = False
669        # Call Reboot
670        if reboot_type == FUCHSIA_REBOOT_TYPE_SOFT:
671            if use_ssh:
672                self.log.info('Sending reboot command via SSH.')
673                with utils.SuppressLogOutput():
674                    self.clean_up_services()
675                    self.send_command_ssh(
676                        'dm reboot',
677                        timeout=FUCHSIA_RECONNECT_AFTER_REBOOT_TIME,
678                        skip_status_code_check=True)
679            else:
680                self.log.info('Calling SL4F reboot command.')
681                with utils.SuppressLogOutput():
682                    self.hardware_power_statecontrol_lib.suspendReboot(
683                        timeout=3)
684                    self.clean_up_services()
685        elif reboot_type == FUCHSIA_REBOOT_TYPE_SOFT_AND_FLASH:
686            flash(self, use_ssh, FUCHSIA_RECONNECT_AFTER_REBOOT_TIME)
687            skip_unreachable_check = True
688        elif reboot_type == FUCHSIA_REBOOT_TYPE_HARD:
689            self.log.info('Power cycling FuchsiaDevice (%s)' % self.ip)
690            if not testbed_pdus:
691                raise AttributeError('Testbed PDUs must be supplied '
692                                     'to hard reboot a fuchsia_device.')
693            device_pdu, device_pdu_port = pdu.get_pdu_port_for_device(
694                self.device_pdu_config, testbed_pdus)
695            with utils.SuppressLogOutput():
696                self.clean_up_services()
697            self.log.info('Killing power to FuchsiaDevice (%s)...' % self.ip)
698            device_pdu.off(str(device_pdu_port))
699        else:
700            raise ValueError('Invalid reboot type: %s' % reboot_type)
701        if not skip_unreachable_check:
702            # Wait for unreachable
703            self.log.info('Verifying device is unreachable.')
704            timeout = time.time() + unreachable_timeout
705            while (time.time() < timeout):
706                if utils.can_ping(job, self.ip):
707                    self.log.debug('Device is still pingable. Retrying.')
708                else:
709                    if reboot_type == FUCHSIA_REBOOT_TYPE_HARD:
710                        self.log.info(
711                            'Restoring power to FuchsiaDevice (%s)...' %
712                            self.ip)
713                        device_pdu.on(str(device_pdu_port))
714                    break
715            else:
716                self.log.info(
717                    'Device failed to go offline. Restarting services...')
718                self.start_services()
719                raise ConnectionError('Device never went down.')
720            self.log.info('Device is unreachable as expected.')
721        if reboot_type == FUCHSIA_REBOOT_TYPE_HARD:
722            self.log.info('Restoring power to FuchsiaDevice (%s)...' % self.ip)
723            device_pdu.on(str(device_pdu_port))
724
725        self.log.info('Waiting for device to respond to pings.')
726        end_time = time.time() + ping_timeout
727        while time.time() < end_time:
728            if utils.can_ping(job, self.ip):
729                break
730            else:
731                self.log.debug('Device is not pingable. Retrying in 1 second.')
732                time.sleep(1)
733        else:
734            raise ConnectionError('Device never came back online.')
735        self.log.info('Device responded to pings.')
736
737        self.log.info('Waiting for device to allow ssh connection.')
738        end_time = time.time() + ssh_timeout
739        while time.time() < end_time:
740            try:
741                self.send_command_ssh('\n')
742            except Exception:
743                self.log.debug(
744                    'Could not SSH to device. Retrying in 1 second.')
745                time.sleep(1)
746            else:
747                break
748        else:
749            raise ConnectionError('Failed to connect to device via SSH.')
750        self.log.info('Device now available via ssh.')
751
752        # Creating new log process, start it, start new persistent ssh session,
753        # start SL4F, and connect via SL4F
754        self.log.info(f'Restarting services on FuchsiaDevice {self.ip}')
755        self.start_services()
756
757        # Verify SL4F is up.
758        self.log.info('Verifying SL4F commands can run.')
759        try:
760            self.hwinfo_lib.getDeviceInfo()
761        except Exception as err:
762            raise ConnectionError(
763                'Failed to connect and run command via SL4F. Err: %s' % err)
764
765        # Reconfigure country code, as it does not persist after reboots
766        self.configure_regulatory_domain(self.config_country_code)
767        try:
768            self.run_commands_from_config(self.setup_commands)
769        except FuchsiaDeviceError:
770            # Prevent a threading error, since controller isn't fully up yet.
771            self.clean_up()
772            raise FuchsiaDeviceError(
773                'Failed to run setup commands after reboot.')
774
775        # If wlan was configured before reboot, it must be configured again
776        # after rebooting, as it was before reboot. No preserving should occur.
777        if self.association_mechanism:
778            pre_reboot_association_mechanism = self.association_mechanism
779            # Prevent configure_wlan from thinking it needs to deconfigure first
780            self.association_mechanism = None
781            self.configure_wlan(
782                association_mechanism=pre_reboot_association_mechanism,
783                preserve_saved_networks=False)
784
785        self.log.info(
786            'Device has rebooted, SL4F is reconnected and functional.')
787
788    def send_command_ssh(self,
789                         test_cmd,
790                         connect_timeout=FUCHSIA_DEFAULT_CONNECT_TIMEOUT,
791                         timeout=FUCHSIA_DEFAULT_COMMAND_TIMEOUT,
792                         skip_status_code_check=False):
793        """Sends an SSH command to a Fuchsia device
794
795        Args:
796            test_cmd: string, command to send to Fuchsia device over SSH.
797            connect_timeout: Timeout to wait for connecting via SSH.
798            timeout: Timeout to wait for a command to complete.
799            skip_status_code_check: Whether to check for the status code.
800
801        Returns:
802            A SshResults object containing the results of the ssh command.
803        """
804        command_result = False
805        ssh_conn = None
806        if not self.ssh_config:
807            self.log.warning(FUCHSIA_SSH_CONFIG_NOT_DEFINED)
808        else:
809            try:
810                ssh_conn = create_ssh_connection(
811                    self.ip,
812                    self.ssh_username,
813                    self.ssh_config,
814                    ssh_port=self.ssh_port,
815                    connect_timeout=connect_timeout)
816                cmd_result_stdin, cmd_result_stdout, cmd_result_stderr = (
817                    ssh_conn.exec_command(test_cmd, timeout=timeout))
818                if not skip_status_code_check:
819                    command_result = SshResults(cmd_result_stdin,
820                                                cmd_result_stdout,
821                                                cmd_result_stderr,
822                                                cmd_result_stdout.channel)
823            except Exception as e:
824                self.log.warning("Problem running ssh command: %s"
825                                 "\n Exception: %s" % (test_cmd, e))
826                return e
827            finally:
828                if ssh_conn is not None:
829                    ssh_conn.close()
830        return command_result
831
832    def version(self, timeout=FUCHSIA_DEFAULT_COMMAND_TIMEOUT):
833        """Returns the version of Fuchsia running on the device.
834
835        Args:
836            timeout: (int) Seconds to wait for command to run.
837
838        Returns:
839            A string containing the Fuchsia version number.
840            For example, "5.20210713.2.1".
841
842        Raises:
843            DeviceOffline: If SSH to the device fails.
844        """
845        return self.send_command_ssh(FUCHSIA_GET_VERSION_CMD,
846                                     timeout=timeout).stdout
847
848    def ping(self,
849             dest_ip,
850             count=3,
851             interval=1000,
852             timeout=1000,
853             size=25,
854             additional_ping_params=None):
855        """Pings from a Fuchsia device to an IPv4 address or hostname
856
857        Args:
858            dest_ip: (str) The ip or hostname to ping.
859            count: (int) How many icmp packets to send.
860            interval: (int) How long to wait between pings (ms)
861            timeout: (int) How long to wait before having the icmp packet
862                timeout (ms).
863            size: (int) Size of the icmp packet.
864            additional_ping_params: (str) command option flags to
865                append to the command string
866
867        Returns:
868            A dictionary for the results of the ping.  The dictionary contains
869            the following items:
870                status: Whether the ping was successful.
871                rtt_min: The minimum round trip time of the ping.
872                rtt_max: The minimum round trip time of the ping.
873                rtt_avg: The avg round trip time of the ping.
874                stdout: The standard out of the ping command.
875                stderr: The standard error of the ping command.
876        """
877        rtt_min = None
878        rtt_max = None
879        rtt_avg = None
880        self.log.debug("Pinging %s..." % dest_ip)
881        if not additional_ping_params:
882            additional_ping_params = ''
883        ping_result = self.send_command_ssh(
884            'ping -c %s -i %s -t %s -s %s %s %s' %
885            (count, interval, timeout, size, additional_ping_params, dest_ip))
886        if isinstance(ping_result, job.Error):
887            ping_result = ping_result.result
888
889        if ping_result.stderr:
890            status = False
891        else:
892            status = True
893            rtt_line = ping_result.stdout.split('\n')[:-1]
894            rtt_line = rtt_line[-1]
895            rtt_stats = re.search(self.ping_rtt_match, rtt_line)
896            rtt_min = rtt_stats.group(1)
897            rtt_max = rtt_stats.group(2)
898            rtt_avg = rtt_stats.group(3)
899        return {
900            'status': status,
901            'rtt_min': rtt_min,
902            'rtt_max': rtt_max,
903            'rtt_avg': rtt_avg,
904            'stdout': ping_result.stdout,
905            'stderr': ping_result.stderr
906        }
907
908    def can_ping(self,
909                 dest_ip,
910                 count=1,
911                 interval=1000,
912                 timeout=1000,
913                 size=25,
914                 additional_ping_params=None):
915        """Returns whether fuchsia device can ping a given dest address"""
916        ping_result = self.ping(dest_ip,
917                                count=count,
918                                interval=interval,
919                                timeout=timeout,
920                                size=size,
921                                additional_ping_params=additional_ping_params)
922        return ping_result['status']
923
924    def print_clients(self):
925        """Gets connected clients from SL4F server"""
926        self.log.debug("Request to print clients")
927        print_id = self.build_id(self.test_counter)
928        print_args = {}
929        print_method = "sl4f.sl4f_print_clients"
930        data = json.dumps({
931            "jsonrpc": "2.0",
932            "id": print_id,
933            "method": print_method,
934            "params": print_args
935        })
936
937        r = requests.get(url=self.print_address, data=data).json()
938        self.test_counter += 1
939
940        return r
941
942    def clean_up(self):
943        """Cleans up the FuchsiaDevice object, releases any resources it
944        claimed, and restores saved networks is applicable. For reboots, use
945        clean_up_services only.
946
947        Note: Any exceptions thrown in this method must be caught and handled,
948        ensuring that clean_up_services is run. Otherwise, the syslog listening
949        thread will never join and will leave tests hanging.
950        """
951        # If and only if wlan is configured, and using the policy layer
952        if self.association_mechanism == 'policy':
953            try:
954                self.wlan_policy_controller._clean_up()
955            except Exception as err:
956                self.log.warning('Unable to clean up WLAN Policy layer: %s' %
957                                 err)
958        try:
959            self.run_commands_from_config(self.teardown_commands)
960        except Exception as err:
961            self.log.warning('Failed to run teardown_commands: %s' % err)
962
963        # This MUST be run, otherwise syslog threads will never join.
964        self.clean_up_services()
965
966    def clean_up_services(self):
967        """ Cleans up FuchsiaDevice services (e.g. SL4F). Subset of clean_up,
968        to be used for reboots, when testing is to continue (as opposed to
969        teardown after testing is finished.)
970        """
971        cleanup_id = self.build_id(self.test_counter)
972        cleanup_args = {}
973        cleanup_method = "sl4f.sl4f_cleanup"
974        data = json.dumps({
975            "jsonrpc": "2.0",
976            "id": cleanup_id,
977            "method": cleanup_method,
978            "params": cleanup_args
979        })
980
981        try:
982            response = requests.get(
983                url=self.cleanup_address,
984                data=data,
985                timeout=FUCHSIA_DEFAULT_CLEAN_UP_COMMAND_TIMEOUT).json()
986            self.log.debug(response)
987        except Exception as err:
988            self.log.exception("Cleanup request failed with %s:" % err)
989        finally:
990            self.test_counter += 1
991            self.stop_services()
992
993    def check_process_state(self, process_name):
994        """Checks the state of a process on the Fuchsia device
995
996        Returns:
997            True if the process_name is running
998            False if process_name is not running
999        """
1000        ps_cmd = self.send_command_ssh("ps")
1001        return process_name in ps_cmd.stdout
1002
1003    def check_process_with_expectation(self, process_name, expectation=None):
1004        """Checks the state of a process on the Fuchsia device and returns
1005        true or false depending the stated expectation
1006
1007        Args:
1008            process_name: The name of the process to check for.
1009            expectation: The state expectation of state of process
1010        Returns:
1011            True if the state of the process matches the expectation
1012            False if the state of the process does not match the expectation
1013        """
1014        process_state = self.check_process_state(process_name)
1015        if expectation in DAEMON_ACTIVATED_STATES:
1016            return process_state
1017        elif expectation in DAEMON_DEACTIVATED_STATES:
1018            return not process_state
1019        else:
1020            raise ValueError("Invalid expectation value (%s). abort!" %
1021                             expectation)
1022
1023    def control_daemon(self, process_name, action):
1024        """Starts or stops a process on a Fuchsia device
1025
1026        Args:
1027            process_name: the name of the process to start or stop
1028            action: specify whether to start or stop a process
1029        """
1030        if not (process_name[-4:] == '.cmx' or process_name[-4:] == '.cml'):
1031            process_name = '%s.cmx' % process_name
1032        unable_to_connect_msg = None
1033        process_state = False
1034        try:
1035            if not self._persistent_ssh_conn:
1036                self._persistent_ssh_conn = (create_ssh_connection(
1037                    self.ip,
1038                    self.ssh_username,
1039                    self.ssh_config,
1040                    ssh_port=self.ssh_port))
1041            self._persistent_ssh_conn.exec_command(
1042                "killall %s" % process_name, timeout=CHANNEL_OPEN_TIMEOUT)
1043            # This command will effectively stop the process but should
1044            # be used as a cleanup before starting a process.  It is a bit
1045            # confusing to have the msg saying "attempting to stop
1046            # the process" after the command already tried but since both start
1047            # and stop need to run this command, this is the best place
1048            # for the command.
1049            if action in DAEMON_ACTIVATED_STATES:
1050                self.log.debug("Attempting to start Fuchsia "
1051                               "devices services.")
1052                self._persistent_ssh_conn.exec_command(
1053                    "run fuchsia-pkg://fuchsia.com/%s#meta/%s &" %
1054                    (process_name[:-4], process_name))
1055                process_initial_msg = (
1056                    "%s has not started yet. Waiting %i second and "
1057                    "checking again." %
1058                    (process_name, DAEMON_INIT_TIMEOUT_SEC))
1059                process_timeout_msg = ("Timed out waiting for %s to start." %
1060                                       process_name)
1061                unable_to_connect_msg = ("Unable to start %s no Fuchsia "
1062                                         "device via SSH. %s may not "
1063                                         "be started." %
1064                                         (process_name, process_name))
1065            elif action in DAEMON_DEACTIVATED_STATES:
1066                process_initial_msg = ("%s is running. Waiting %i second and "
1067                                       "checking again." %
1068                                       (process_name, DAEMON_INIT_TIMEOUT_SEC))
1069                process_timeout_msg = ("Timed out waiting trying to kill %s." %
1070                                       process_name)
1071                unable_to_connect_msg = ("Unable to stop %s on Fuchsia "
1072                                         "device via SSH. %s may "
1073                                         "still be running." %
1074                                         (process_name, process_name))
1075            else:
1076                raise FuchsiaDeviceError(FUCHSIA_INVALID_CONTROL_STATE %
1077                                         action)
1078            timeout_counter = 0
1079            while not process_state:
1080                self.log.info(process_initial_msg)
1081                time.sleep(DAEMON_INIT_TIMEOUT_SEC)
1082                timeout_counter += 1
1083                process_state = (self.check_process_with_expectation(
1084                    process_name, expectation=action))
1085                if timeout_counter == (DAEMON_INIT_TIMEOUT_SEC * 3):
1086                    self.log.info(process_timeout_msg)
1087                    break
1088            if not process_state:
1089                raise FuchsiaDeviceError(FUCHSIA_COULD_NOT_GET_DESIRED_STATE %
1090                                         (action, process_name))
1091        except Exception as e:
1092            self.log.info(unable_to_connect_msg)
1093            raise e
1094        finally:
1095            if action == 'stop' and (process_name == 'sl4f'
1096                                     or process_name == 'sl4f.cmx'):
1097                self._persistent_ssh_conn.close()
1098                self._persistent_ssh_conn = None
1099
1100    def check_connect_response(self, connect_response):
1101        if connect_response.get("error") is None:
1102            # Checks the response from SL4F and if there is no error, check
1103            # the result.
1104            connection_result = connect_response.get("result")
1105            if not connection_result:
1106                # Ideally the error would be present but just outputting a log
1107                # message until available.
1108                self.log.debug("Connect call failed, aborting!")
1109                return False
1110            else:
1111                # Returns True if connection was successful.
1112                return True
1113        else:
1114            # the response indicates an error - log and raise failure
1115            self.log.debug("Aborting! - Connect call failed with error: %s" %
1116                           connect_response.get("error"))
1117            return False
1118
1119    def check_disconnect_response(self, disconnect_response):
1120        if disconnect_response.get("error") is None:
1121            # Returns True if disconnect was successful.
1122            return True
1123        else:
1124            # the response indicates an error - log and raise failure
1125            self.log.debug("Disconnect call failed with error: %s" %
1126                           disconnect_response.get("error"))
1127            return False
1128
1129    # TODO(fxb/64657): Determine more stable solution to country code config on
1130    # device bring up.
1131    def configure_regulatory_domain(self, desired_country_code):
1132        """Allows the user to set the device country code via ACTS config
1133
1134        Usage:
1135            In FuchsiaDevice config, add "country_code": "<CC>"
1136        """
1137        if self.ssh_config:
1138            # Country code can be None, from ACTS config.
1139            if desired_country_code:
1140                desired_country_code = desired_country_code.upper()
1141                response = self.regulatory_region_lib.setRegion(
1142                    desired_country_code)
1143                if response.get('error'):
1144                    raise FuchsiaDeviceError(
1145                        'Failed to set regulatory domain. Err: %s' %
1146                        response['error'])
1147                end_time = time.time() + FUCHSIA_COUNTRY_CODE_TIMEOUT
1148                while time.time() < end_time:
1149                    ascii_cc = self.wlan_lib.wlanGetCountry(0).get('result')
1150                    # Convert ascii_cc to string, then compare
1151                    if ascii_cc and (''.join(chr(c) for c in ascii_cc).upper()
1152                                     == desired_country_code):
1153                        self.log.debug('Country code successfully set to %s.' %
1154                                       desired_country_code)
1155                        return
1156                    self.log.debug('Country code not yet updated. Retrying.')
1157                    time.sleep(1)
1158                raise FuchsiaDeviceError('Country code never updated to %s' %
1159                                         desired_country_code)
1160
1161    @backoff.on_exception(backoff.constant,
1162                          (FuchsiaSyslogError, socket.timeout),
1163                          interval=1.5,
1164                          max_tries=4)
1165    def start_services(self):
1166        """Starts long running services on the Fuchsia device.
1167
1168        Starts a syslog streaming process, SL4F server, initializes a connection
1169        to the SL4F server, then starts an isolated ffx daemon.
1170
1171        """
1172        self.log.debug("Attempting to start Fuchsia device services on %s." %
1173                       self.ip)
1174        if self.ssh_config:
1175            self.log_process = create_syslog_process(self.serial,
1176                                                     self.log_path,
1177                                                     self.ip,
1178                                                     self.ssh_username,
1179                                                     self.ssh_config,
1180                                                     ssh_port=self.ssh_port)
1181
1182            try:
1183                self.log_process.start()
1184            except FuchsiaSyslogError as e:
1185                # Before backing off and retrying, stop the syslog if it
1186                # failed to setup correctly, to prevent threading error when
1187                # retrying
1188                self.log_process.stop()
1189                raise
1190
1191            self.control_daemon("sl4f.cmx", "start")
1192            self.init_sl4f_connection()
1193
1194            out_name = "fuchsia_device_%s_%s.txt" % (self.serial, 'fw_version')
1195            full_out_path = os.path.join(self.log_path, out_name)
1196            fw_file = open(full_out_path, 'w')
1197            fw_file.write('%s\n' % self.version())
1198            fw_file.close()
1199
1200        self.init_ffx_connection()
1201
1202    def stop_services(self):
1203        """Stops long running services on the fuchsia device.
1204
1205        Terminates the syslog streaming process, the SL4F server on the device,
1206        and the ffx daemon.
1207        """
1208        self.log.debug("Attempting to stop Fuchsia device services on %s." %
1209                       self.ip)
1210        if hasattr(self, 'ffx'):
1211            self.ffx.clean_up()
1212        if self.ssh_config:
1213            try:
1214                self.control_daemon("sl4f.cmx", "stop")
1215            except Exception as err:
1216                self.log.exception("Failed to stop sl4f.cmx with: %s" % err)
1217            if self.log_process:
1218                self.log_process.stop()
1219
1220    def load_config(self, config):
1221        pass
1222
1223    def take_bug_report(self,
1224                        test_name=None,
1225                        begin_time=None,
1226                        additional_log_objects=None):
1227        """Takes a bug report on the device and stores it in a file.
1228
1229        Args:
1230            test_name: Name of the test case that triggered this bug report.
1231            begin_time: Epoch time when the test started. If not specified, the
1232                current time will be used.
1233            additional_log_objects: A list of additional objects in Fuchsia to
1234                query in the bug report.  Must be in the following format:
1235                /hub/c/scenic.cmx/[0-9]*/out/objects
1236        """
1237        if not additional_log_objects:
1238            additional_log_objects = []
1239        log_items = []
1240        matching_log_items = FUCHSIA_DEFAULT_LOG_ITEMS
1241        for additional_log_object in additional_log_objects:
1242            if additional_log_object not in matching_log_items:
1243                matching_log_items.append(additional_log_object)
1244        sn_path = context.get_current_context().get_full_output_path()
1245        os.makedirs(sn_path, exist_ok=True)
1246
1247        epoch = begin_time if begin_time else utils.get_current_epoch_time()
1248        time_stamp = acts_logger.normalize_log_line_timestamp(
1249            acts_logger.epoch_to_log_line_timestamp(epoch))
1250        out_name = f"{self.mdns_name}_{time_stamp}"
1251        snapshot_out_name = f"{out_name}.zip"
1252        out_name = "%s.txt" % out_name
1253        full_out_path = os.path.join(sn_path, out_name)
1254        full_sn_out_path = os.path.join(sn_path, snapshot_out_name)
1255
1256        if test_name:
1257            self.log.info(
1258                f"Taking snapshot of {self.mdns_name} for {test_name}")
1259        else:
1260            self.log.info(f"Taking snapshot of {self.mdns_name}")
1261
1262        if self.ssh_config is not None:
1263            try:
1264                subprocess.run([
1265                    f"ssh -F {self.ssh_config} {self.ip} snapshot > {full_sn_out_path}"
1266                ],
1267                               shell=True)
1268                self.log.info("Snapshot saved at: {}".format(full_sn_out_path))
1269            except Exception as err:
1270                self.log.error("Failed to take snapshot with: {}".format(err))
1271
1272        system_objects = self.send_command_ssh('iquery --find /hub').stdout
1273        system_objects = system_objects.split()
1274
1275        for matching_log_item in matching_log_items:
1276            for system_object in system_objects:
1277                if re.match(matching_log_item, system_object):
1278                    log_items.append(system_object)
1279
1280        log_command = '%s %s' % (FUCHSIA_DEFAULT_LOG_CMD, ' '.join(log_items))
1281        bug_report_data = self.send_command_ssh(log_command).stdout
1282
1283        bug_report_file = open(full_out_path, 'w')
1284        bug_report_file.write(bug_report_data)
1285        bug_report_file.close()
1286
1287    def take_bt_snoop_log(self, custom_name=None):
1288        """Takes a the bt-snoop log from the device and stores it in a file
1289        in a pcap format.
1290        """
1291        bt_snoop_path = context.get_current_context().get_full_output_path()
1292        time_stamp = acts_logger.normalize_log_line_timestamp(
1293            acts_logger.epoch_to_log_line_timestamp(time.time()))
1294        out_name = "FuchsiaDevice%s_%s" % (
1295            self.serial, time_stamp.replace(" ", "_").replace(":", "-"))
1296        out_name = "%s.pcap" % out_name
1297        if custom_name:
1298            out_name = "%s_%s.pcap" % (self.serial, custom_name)
1299        else:
1300            out_name = "%s.pcap" % out_name
1301        full_out_path = os.path.join(bt_snoop_path, out_name)
1302        bt_snoop_data = self.send_command_ssh(
1303            'bt-snoop-cli -d -f pcap').raw_stdout
1304        bt_snoop_file = open(full_out_path, 'wb')
1305        bt_snoop_file.write(bt_snoop_data)
1306        bt_snoop_file.close()
1307
1308
1309class FuchsiaDeviceLoggerAdapter(logging.LoggerAdapter):
1310    def process(self, msg, kwargs):
1311        msg = "[FuchsiaDevice|%s] %s" % (self.extra["ip"], msg)
1312        return msg, kwargs
1313