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