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 17from typing import Optional 18import json 19import logging 20import os 21import re 22import subprocess 23import time 24 25from acts import context 26from acts import logger as acts_logger 27from acts import signals 28from acts import utils 29from acts.controllers import pdu 30from acts.libs.proc import job 31from acts.utils import get_fuchsia_mdns_ipv6_address, get_interface_ip_addresses 32 33from acts.controllers.fuchsia_lib.ffx import FFX 34from acts.controllers.fuchsia_lib.sl4f import SL4F 35from acts.controllers.fuchsia_lib.lib_controllers.netstack_controller import NetstackController 36from acts.controllers.fuchsia_lib.lib_controllers.wlan_controller import WlanController 37from acts.controllers.fuchsia_lib.lib_controllers.wlan_policy_controller import WlanPolicyController 38from acts.controllers.fuchsia_lib.package_server import PackageServer 39from acts.controllers.fuchsia_lib.ssh import DEFAULT_SSH_PORT, DEFAULT_SSH_USER, SSHConfig, SSHProvider, FuchsiaSSHError 40from acts.controllers.fuchsia_lib.utils_lib import flash 41 42MOBLY_CONTROLLER_CONFIG_NAME = "FuchsiaDevice" 43ACTS_CONTROLLER_REFERENCE_NAME = "fuchsia_devices" 44 45CONTROL_PATH_REPLACE_VALUE = " ControlPath /tmp/fuchsia--%r@%h:%p" 46 47FUCHSIA_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!" 48FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!" 49FUCHSIA_DEVICE_INVALID_CONFIG = ("Fuchsia device config must be either a str " 50 "or dict. abort! Invalid element %i in %r") 51FUCHSIA_DEVICE_NO_IP_MSG = "No IP address specified, abort!" 52FUCHSIA_COULD_NOT_GET_DESIRED_STATE = "Could not %s %s." 53FUCHSIA_INVALID_CONTROL_STATE = "Invalid control state (%s). abort!" 54 55FUCHSIA_TIME_IN_NANOSECONDS = 1000000000 56 57SL4F_APK_NAME = "com.googlecode.android_scripting" 58DAEMON_INIT_TIMEOUT_SEC = 1 59 60DAEMON_ACTIVATED_STATES = ["running", "start"] 61DAEMON_DEACTIVATED_STATES = ["stop", "stopped"] 62 63FUCHSIA_RECONNECT_AFTER_REBOOT_TIME = 5 64 65CHANNEL_OPEN_TIMEOUT = 5 66 67FUCHSIA_REBOOT_TYPE_SOFT = 'soft' 68FUCHSIA_REBOOT_TYPE_SOFT_AND_FLASH = 'flash' 69FUCHSIA_REBOOT_TYPE_HARD = 'hard' 70 71FUCHSIA_DEFAULT_CONNECT_TIMEOUT = 90 72FUCHSIA_DEFAULT_COMMAND_TIMEOUT = 60 73 74FUCHSIA_DEFAULT_CLEAN_UP_COMMAND_TIMEOUT = 15 75 76FUCHSIA_COUNTRY_CODE_TIMEOUT = 15 77FUCHSIA_DEFAULT_COUNTRY_CODE_US = 'US' 78 79MDNS_LOOKUP_RETRY_MAX = 3 80 81VALID_ASSOCIATION_MECHANISMS = {None, 'policy', 'drivers'} 82IP_ADDRESS_TIMEOUT = 15 83 84 85class FuchsiaDeviceError(signals.ControllerError): 86 pass 87 88 89class FuchsiaConfigError(signals.ControllerError): 90 """Incorrect FuchsiaDevice configuration.""" 91 92 93def create(configs): 94 if not configs: 95 raise FuchsiaDeviceError(FUCHSIA_DEVICE_EMPTY_CONFIG_MSG) 96 elif not isinstance(configs, list): 97 raise FuchsiaDeviceError(FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG) 98 for index, config in enumerate(configs): 99 if isinstance(config, str): 100 configs[index] = {"ip": config} 101 elif not isinstance(config, dict): 102 raise FuchsiaDeviceError(FUCHSIA_DEVICE_INVALID_CONFIG % 103 (index, configs)) 104 return get_instances(configs) 105 106 107def destroy(fds): 108 for fd in fds: 109 fd.clean_up() 110 del fd 111 112 113def get_info(fds): 114 """Get information on a list of FuchsiaDevice objects. 115 116 Args: 117 fds: A list of FuchsiaDevice objects. 118 119 Returns: 120 A list of dict, each representing info for FuchsiaDevice objects. 121 """ 122 device_info = [] 123 for fd in fds: 124 info = {"ip": fd.ip} 125 device_info.append(info) 126 return device_info 127 128 129def get_instances(fds_conf_data): 130 """Create FuchsiaDevice instances from a list of Fuchsia ips. 131 132 Args: 133 fds_conf_data: A list of dicts that contain Fuchsia device info. 134 135 Returns: 136 A list of FuchsiaDevice objects. 137 """ 138 139 return [FuchsiaDevice(fd_conf_data) for fd_conf_data in fds_conf_data] 140 141 142class FuchsiaDevice: 143 """Class representing a Fuchsia device. 144 145 Each object of this class represents one Fuchsia device in ACTS. 146 147 Attributes: 148 ip: The full address or Fuchsia abstract name to contact the Fuchsia 149 device at 150 log: A logger object. 151 ssh_port: The SSH TCP port number of the Fuchsia device. 152 sl4f_port: The SL4F HTTP port number of the Fuchsia device. 153 ssh_config: The ssh_config for connecting to the Fuchsia device. 154 """ 155 156 def __init__(self, fd_conf_data): 157 """ 158 Args: 159 fd_conf_data: A dict of a fuchsia device configuration data 160 Required keys: 161 ip: IP address of fuchsia device 162 optional key: 163 sl4_port: Port for the sl4f web server on the fuchsia device 164 (Default: 80) 165 ssh_config: Location of the ssh_config file to connect to 166 the fuchsia device 167 (Default: None) 168 ssh_port: Port for the ssh server on the fuchsia device 169 (Default: 22) 170 """ 171 self.conf_data = fd_conf_data 172 if "ip" not in fd_conf_data: 173 raise FuchsiaDeviceError(FUCHSIA_DEVICE_NO_IP_MSG) 174 self.ip: str = fd_conf_data["ip"] 175 self.orig_ip: str = fd_conf_data["ip"] 176 self.sl4f_port: int = fd_conf_data.get("sl4f_port", 80) 177 self.ssh_port: int = fd_conf_data.get("ssh_port", DEFAULT_SSH_PORT) 178 self.ssh_config: Optional[str] = fd_conf_data.get("ssh_config", None) 179 self.ssh_priv_key: Optional[str] = fd_conf_data.get( 180 "ssh_priv_key", None) 181 self.authorized_file: Optional[str] = fd_conf_data.get( 182 "authorized_file_loc", None) 183 self.serial_number: Optional[str] = fd_conf_data.get( 184 "serial_number", None) 185 self.device_type: Optional[str] = fd_conf_data.get("device_type", None) 186 self.product_type: Optional[str] = fd_conf_data.get( 187 "product_type", None) 188 self.board_type: Optional[str] = fd_conf_data.get("board_type", None) 189 self.build_number: Optional[str] = fd_conf_data.get( 190 "build_number", None) 191 self.build_type: Optional[str] = fd_conf_data.get("build_type", None) 192 self.server_path: Optional[str] = fd_conf_data.get("server_path", None) 193 self.specific_image: Optional[str] = fd_conf_data.get( 194 "specific_image", None) 195 self.ffx_binary_path: Optional[str] = fd_conf_data.get( 196 "ffx_binary_path", None) 197 # Path to a tar.gz archive with pm and amber-files, as necessary for 198 # starting a package server. 199 self.packages_archive_path: Optional[str] = fd_conf_data.get( 200 "packages_archive_path", None) 201 self.mdns_name: Optional[str] = fd_conf_data.get("mdns_name", None) 202 203 # Instead of the input ssh_config, a new config is generated with proper 204 # ControlPath to the test output directory. 205 output_path = context.get_current_context().get_base_output_path() 206 generated_ssh_config = os.path.join(output_path, 207 "ssh_config_{}".format(self.ip)) 208 self._set_control_path_config(self.ssh_config, generated_ssh_config) 209 self.ssh_config = generated_ssh_config 210 211 self.ssh_username = fd_conf_data.get("ssh_username", DEFAULT_SSH_USER) 212 self.hard_reboot_on_fail = fd_conf_data.get("hard_reboot_on_fail", 213 False) 214 self.take_bug_report_on_fail = fd_conf_data.get( 215 "take_bug_report_on_fail", False) 216 self.device_pdu_config = fd_conf_data.get("PduDevice", None) 217 self.config_country_code = fd_conf_data.get( 218 'country_code', FUCHSIA_DEFAULT_COUNTRY_CODE_US).upper() 219 220 # WLAN interface info is populated inside configure_wlan 221 self.wlan_client_interfaces = {} 222 self.wlan_ap_interfaces = {} 223 self.wlan_client_test_interface_name = fd_conf_data.get( 224 'wlan_client_test_interface', None) 225 self.wlan_ap_test_interface_name = fd_conf_data.get( 226 'wlan_ap_test_interface', None) 227 228 # Whether to use 'policy' or 'drivers' for WLAN connect/disconnect calls 229 # If set to None, wlan is not configured. 230 self.association_mechanism = None 231 # Defaults to policy layer, unless otherwise specified in the config 232 self.default_association_mechanism = fd_conf_data.get( 233 'association_mechanism', 'policy') 234 235 # Whether to clear and preserve existing saved networks and client 236 # connections state, to be restored at device teardown. 237 self.default_preserve_saved_networks = fd_conf_data.get( 238 'preserve_saved_networks', True) 239 240 if not utils.is_valid_ipv4_address( 241 self.ip) and not utils.is_valid_ipv6_address(self.ip): 242 mdns_ip = None 243 for retry_counter in range(MDNS_LOOKUP_RETRY_MAX): 244 mdns_ip = get_fuchsia_mdns_ipv6_address(self.ip) 245 if mdns_ip: 246 break 247 else: 248 time.sleep(1) 249 if mdns_ip and utils.is_valid_ipv6_address(mdns_ip): 250 # self.ip was actually an mdns name. Use it for self.mdns_name 251 # unless one was explicitly provided. 252 self.mdns_name = self.mdns_name or self.ip 253 self.ip = mdns_ip 254 else: 255 raise ValueError('Invalid IP: %s' % self.ip) 256 257 self.log = acts_logger.create_tagged_trace_logger( 258 "FuchsiaDevice | %s" % self.orig_ip) 259 260 self.ping_rtt_match = re.compile(r'RTT Min/Max/Avg ' 261 r'= \[ (.*?) / (.*?) / (.*?) \] ms') 262 self.serial = re.sub('[.:%]', '_', self.ip) 263 log_path_base = getattr(logging, 'log_path', '/tmp/logs') 264 self.log_path = os.path.join(log_path_base, 265 'FuchsiaDevice%s' % self.serial) 266 self.fuchsia_log_file_path = os.path.join( 267 self.log_path, "fuchsialog_%s_debug.txt" % self.serial) 268 self.log_process = None 269 self.package_server = None 270 271 self.init_controllers() 272 273 @property 274 def sl4f(self): 275 """Get the sl4f module configured for this device. 276 277 The sl4f module uses lazy-initialization; it will initialize an sl4f 278 server on the host device when it is required. 279 """ 280 if not hasattr(self, '_sl4f'): 281 self._sl4f = SL4F(self.ssh, self.sl4f_port) 282 self.log.info('Started SL4F server') 283 return self._sl4f 284 285 @sl4f.deleter 286 def sl4f(self): 287 if not hasattr(self, '_sl4f'): 288 return 289 del self._sl4f 290 291 @property 292 def ssh(self): 293 """Get the SSH provider module configured for this device.""" 294 if not hasattr(self, '_ssh'): 295 if not self.ssh_port: 296 raise FuchsiaConfigError( 297 'Must provide "ssh_port: <int>" in the device config') 298 if not self.ssh_priv_key: 299 raise FuchsiaConfigError( 300 'Must provide "ssh_priv_key: <file path>" in the device config' 301 ) 302 self._ssh = SSHProvider( 303 SSHConfig(self.ip, self.ssh_priv_key, port=self.ssh_port)) 304 return self._ssh 305 306 @ssh.deleter 307 def ssh(self): 308 if not hasattr(self, '_ssh'): 309 return 310 del self._ssh 311 312 @property 313 def ffx(self): 314 """Get the ffx module configured for this device. 315 316 The ffx module uses lazy-initialization; it will initialize an ffx 317 connection to the device when it is required. 318 319 If ffx needs to be reinitialized, delete the "ffx" property and attempt 320 access again. Note re-initialization will interrupt any running ffx 321 calls. 322 """ 323 if not hasattr(self, '_ffx'): 324 if not self.ffx_binary_path: 325 raise FuchsiaConfigError( 326 'Must provide "ffx_binary_path: <path to FFX binary>" in the device config' 327 ) 328 if not self.mdns_name: 329 raise FuchsiaConfigError( 330 'Must provide "mdns_name: <device mDNS name>" in the device config' 331 ) 332 self._ffx = FFX(self.ffx_binary_path, self.mdns_name, self.ip, 333 self.ssh_priv_key) 334 return self._ffx 335 336 @ffx.deleter 337 def ffx(self): 338 if not hasattr(self, '_ffx'): 339 return 340 self._ffx.clean_up() 341 del self._ffx 342 343 def _set_control_path_config(self, old_config, new_config): 344 """Given an input ssh_config, write to a new config with proper 345 ControlPath values in place, if it doesn't exist already. 346 347 Args: 348 old_config: string, path to the input config 349 new_config: string, path to store the new config 350 """ 351 if os.path.isfile(new_config): 352 return 353 354 ssh_config_copy = "" 355 356 with open(old_config, 'r') as file: 357 ssh_config_copy = re.sub('(\sControlPath\s.*)', 358 CONTROL_PATH_REPLACE_VALUE, 359 file.read(), 360 flags=re.M) 361 with open(new_config, 'w') as file: 362 file.write(ssh_config_copy) 363 364 def init_controllers(self): 365 # Contains Netstack functions 366 self.netstack_controller = NetstackController(self) 367 368 # Contains WLAN core functions 369 self.wlan_controller = WlanController(self) 370 371 # Contains WLAN policy functions like save_network, remove_network, etc 372 self.wlan_policy_controller = WlanPolicyController(self.sl4f, self.ffx) 373 374 def start_package_server(self): 375 if not self.packages_archive_path: 376 self.log.warn( 377 "packages_archive_path is not specified. " 378 "Assuming a package server is already running and configured on " 379 "the DUT. If this is not the case, either run your own package " 380 "server, or configure these fields appropriately. " 381 "This is usually required for the Fuchsia iPerf3 client or " 382 "other testing utilities not on device cache.") 383 return 384 if self.package_server: 385 self.log.warn( 386 "Skipping to start the package server since is already running" 387 ) 388 return 389 390 self.package_server = PackageServer(self.packages_archive_path) 391 self.package_server.start() 392 self.package_server.configure_device(self.ssh) 393 394 def run_commands_from_config(self, cmd_dicts): 395 """Runs commands on the Fuchsia device from the config file. Useful for 396 device and/or Fuchsia specific configuration. 397 398 Args: 399 cmd_dicts: list of dictionaries containing the following 400 'cmd': string, command to run on device 401 'timeout': int, seconds to wait for command to run (optional) 402 'skip_status_code_check': bool, disregard errors if true 403 404 Raises: 405 FuchsiaDeviceError: if any of the commands return a non-zero status 406 code and skip_status_code_check is false or undefined. 407 """ 408 for cmd_dict in cmd_dicts: 409 try: 410 cmd = cmd_dict['cmd'] 411 except KeyError: 412 raise FuchsiaDeviceError( 413 'To run a command via config, you must provide key "cmd" ' 414 'containing the command string.') 415 416 timeout = cmd_dict.get('timeout', FUCHSIA_DEFAULT_COMMAND_TIMEOUT) 417 # Catch both boolean and string values from JSON 418 skip_status_code_check = 'true' == str( 419 cmd_dict.get('skip_status_code_check', False)).lower() 420 421 if skip_status_code_check: 422 self.log.info(f'Running command "{cmd}" and ignoring result.') 423 else: 424 self.log.info(f'Running command "{cmd}".') 425 426 try: 427 result = self.ssh.run(cmd, timeout_sec=timeout) 428 self.log.debug(result) 429 except FuchsiaSSHError as e: 430 if not skip_status_code_check: 431 raise FuchsiaDeviceError( 432 'Failed device specific commands for initial configuration' 433 ) from e 434 435 def configure_wlan(self, 436 association_mechanism=None, 437 preserve_saved_networks=None): 438 """ 439 Readies device for WLAN functionality. If applicable, connects to the 440 policy layer and clears/saves preexisting saved networks. 441 442 Args: 443 association_mechanism: string, 'policy' or 'drivers'. If None, uses 444 the default value from init (can be set by ACTS config) 445 preserve_saved_networks: bool, whether to clear existing saved 446 networks, and preserve them for restoration later. If None, uses 447 the default value from init (can be set by ACTS config) 448 449 Raises: 450 FuchsiaDeviceError, if configuration fails 451 """ 452 453 # Set the country code US by default, or country code provided 454 # in ACTS config 455 self.configure_regulatory_domain(self.config_country_code) 456 457 # If args aren't provided, use the defaults, which can be set in the 458 # config. 459 if association_mechanism is None: 460 association_mechanism = self.default_association_mechanism 461 if preserve_saved_networks is None: 462 preserve_saved_networks = self.default_preserve_saved_networks 463 464 if association_mechanism not in VALID_ASSOCIATION_MECHANISMS: 465 raise FuchsiaDeviceError( 466 'Invalid FuchsiaDevice association_mechanism: %s' % 467 association_mechanism) 468 469 # Allows for wlan to be set up differently in different tests 470 if self.association_mechanism: 471 self.log.info('Deconfiguring WLAN') 472 self.deconfigure_wlan() 473 474 self.association_mechanism = association_mechanism 475 476 self.log.info('Configuring WLAN w/ association mechanism: %s' % 477 association_mechanism) 478 if association_mechanism == 'drivers': 479 self.log.warn( 480 'You may encounter unusual device behavior when using the ' 481 'drivers directly for WLAN. This should be reserved for ' 482 'debugging specific issues. Normal test runs should use the ' 483 'policy layer.') 484 if preserve_saved_networks: 485 self.log.warn( 486 'Unable to preserve saved networks when using drivers ' 487 'association mechanism (requires policy layer control).') 488 else: 489 # This requires SL4F calls, so it can only happen with actual 490 # devices, not with unit tests. 491 self.wlan_policy_controller.configure_wlan(preserve_saved_networks) 492 493 # Retrieve WLAN client and AP interfaces 494 self.wlan_controller.update_wlan_interfaces() 495 496 def deconfigure_wlan(self): 497 """ 498 Stops WLAN functionality (if it has been started). Used to allow 499 different tests to use WLAN differently (e.g. some tests require using 500 wlan policy, while the abstract wlan_device can be setup to use policy 501 or drivers) 502 503 Raises: 504 FuchsiaDeviveError, if deconfigure fails. 505 """ 506 if not self.association_mechanism: 507 self.log.debug( 508 'WLAN not configured before deconfigure was called.') 509 return 510 # If using policy, stop client connections. Otherwise, just clear 511 # variables. 512 if self.association_mechanism != 'drivers': 513 self.wlan_policy_controller._deconfigure_wlan() 514 self.association_mechanism = None 515 516 def reboot(self, 517 use_ssh: bool = False, 518 unreachable_timeout: int = FUCHSIA_DEFAULT_CONNECT_TIMEOUT, 519 ping_timeout: int = FUCHSIA_DEFAULT_CONNECT_TIMEOUT, 520 ssh_timeout: int = FUCHSIA_DEFAULT_CONNECT_TIMEOUT, 521 reboot_type: int = FUCHSIA_REBOOT_TYPE_SOFT, 522 testbed_pdus: list[pdu.PduDevice] = None) -> None: 523 """Reboot a FuchsiaDevice. 524 525 Soft reboots the device, verifies it becomes unreachable, then verifies 526 it comes back online. Re-initializes services so the tests can continue. 527 528 Args: 529 use_ssh: if True, use fuchsia shell command via ssh to reboot 530 instead of SL4F. 531 unreachable_timeout: time to wait for device to become unreachable. 532 ping_timeout:time to wait for device to respond to pings. 533 ssh_timeout: time to wait for device to be reachable via ssh. 534 reboot_type: 'soft', 'hard' or 'flash'. 535 testbed_pdus: all testbed PDUs. 536 537 Raises: 538 ConnectionError, if device fails to become unreachable or fails to 539 come back up. 540 """ 541 if reboot_type == FUCHSIA_REBOOT_TYPE_SOFT: 542 if use_ssh: 543 self.log.info('Soft rebooting via SSH') 544 try: 545 self.ssh.run( 546 'dm reboot', 547 timeout_sec=FUCHSIA_RECONNECT_AFTER_REBOOT_TIME) 548 except FuchsiaSSHError as e: 549 if 'closed by remote host' not in e.result.stderr: 550 raise e 551 else: 552 self.log.info('Soft rebooting via SL4F') 553 self.sl4f.hardware_power_statecontrol_lib.suspendReboot( 554 timeout=3) 555 self._check_unreachable(timeout_sec=unreachable_timeout) 556 557 elif reboot_type == FUCHSIA_REBOOT_TYPE_HARD: 558 self.log.info('Hard rebooting via PDU') 559 if not testbed_pdus: 560 raise AttributeError('Testbed PDUs must be supplied ' 561 'to hard reboot a fuchsia_device.') 562 device_pdu, device_pdu_port = pdu.get_pdu_port_for_device( 563 self.device_pdu_config, testbed_pdus) 564 self.log.info('Killing power to FuchsiaDevice') 565 device_pdu.off(str(device_pdu_port)) 566 self._check_unreachable(timeout_sec=unreachable_timeout) 567 self.log.info('Restoring power to FuchsiaDevice') 568 device_pdu.on(str(device_pdu_port)) 569 570 elif reboot_type == FUCHSIA_REBOOT_TYPE_SOFT_AND_FLASH: 571 flash(self, use_ssh, FUCHSIA_RECONNECT_AFTER_REBOOT_TIME) 572 573 else: 574 raise ValueError('Invalid reboot type: %s' % reboot_type) 575 576 self._check_reachable(timeout_sec=ping_timeout) 577 578 # Cleanup services 579 self.stop_services() 580 581 self.log.info('Waiting for device to allow ssh connection.') 582 end_time = time.time() + ssh_timeout 583 while time.time() < end_time: 584 try: 585 self.ssh.run('echo') 586 except Exception as e: 587 self.log.debug(f'Retrying SSH to device. Details: {e}') 588 else: 589 break 590 else: 591 raise ConnectionError('Failed to connect to device via SSH.') 592 self.log.info('Device now available via ssh.') 593 594 # TODO (b/246852449): Move configure_wlan to other controllers. 595 # If wlan was configured before reboot, it must be configured again 596 # after rebooting, as it was before reboot. No preserving should occur. 597 if self.association_mechanism: 598 pre_reboot_association_mechanism = self.association_mechanism 599 # Prevent configure_wlan from thinking it needs to deconfigure first 600 self.association_mechanism = None 601 self.configure_wlan( 602 association_mechanism=pre_reboot_association_mechanism, 603 preserve_saved_networks=False) 604 605 self.log.info('Device has rebooted') 606 607 def version(self): 608 """Returns the version of Fuchsia running on the device. 609 610 Returns: 611 A string containing the Fuchsia version number or nothing if there 612 is no version information attached during the build. 613 For example, "5.20210713.2.1" or "". 614 615 Raises: 616 FFXTimeout: when the command times out. 617 FFXError: when the command returns non-zero and skip_status_code_check is False. 618 """ 619 target_info_json = self.ffx.run("target show --json").stdout 620 target_info = json.loads(target_info_json) 621 build_info = [ 622 entry for entry in target_info if entry["label"] == "build" 623 ] 624 if len(build_info) != 1: 625 self.log.warning( 626 f'Expected one entry with label "build", found {build_info}') 627 return "" 628 version_info = [ 629 child for child in build_info[0]["child"] 630 if child["label"] == "version" 631 ] 632 if len(version_info) != 1: 633 self.log.warning( 634 f'Expected one entry child with label "version", found {build_info}' 635 ) 636 return "" 637 return version_info[0]["value"] 638 639 def ping(self, 640 dest_ip, 641 count=3, 642 interval=1000, 643 timeout=1000, 644 size=25, 645 additional_ping_params=None): 646 """Pings from a Fuchsia device to an IPv4 address or hostname 647 648 Args: 649 dest_ip: (str) The ip or hostname to ping. 650 count: (int) How many icmp packets to send. 651 interval: (int) How long to wait between pings (ms) 652 timeout: (int) How long to wait before having the icmp packet 653 timeout (ms). 654 size: (int) Size of the icmp packet. 655 additional_ping_params: (str) command option flags to 656 append to the command string 657 658 Returns: 659 A dictionary for the results of the ping. The dictionary contains 660 the following items: 661 status: Whether the ping was successful. 662 rtt_min: The minimum round trip time of the ping. 663 rtt_max: The minimum round trip time of the ping. 664 rtt_avg: The avg round trip time of the ping. 665 stdout: The standard out of the ping command. 666 stderr: The standard error of the ping command. 667 """ 668 rtt_min = None 669 rtt_max = None 670 rtt_avg = None 671 self.log.debug("Pinging %s..." % dest_ip) 672 if not additional_ping_params: 673 additional_ping_params = '' 674 675 try: 676 ping_result = self.ssh.run( 677 f'ping -c {count} -i {interval} -t {timeout} -s {size} ' 678 f'{additional_ping_params} {dest_ip}') 679 except FuchsiaSSHError as e: 680 ping_result = e.result 681 682 if ping_result.stderr: 683 status = False 684 else: 685 status = True 686 rtt_line = ping_result.stdout.split('\n')[:-1] 687 rtt_line = rtt_line[-1] 688 rtt_stats = re.search(self.ping_rtt_match, rtt_line) 689 rtt_min = rtt_stats.group(1) 690 rtt_max = rtt_stats.group(2) 691 rtt_avg = rtt_stats.group(3) 692 return { 693 'status': status, 694 'rtt_min': rtt_min, 695 'rtt_max': rtt_max, 696 'rtt_avg': rtt_avg, 697 'stdout': ping_result.stdout, 698 'stderr': ping_result.stderr 699 } 700 701 def can_ping(self, 702 dest_ip, 703 count=1, 704 interval=1000, 705 timeout=1000, 706 size=25, 707 additional_ping_params=None): 708 """Returns whether fuchsia device can ping a given dest address""" 709 ping_result = self.ping(dest_ip, 710 count=count, 711 interval=interval, 712 timeout=timeout, 713 size=size, 714 additional_ping_params=additional_ping_params) 715 return ping_result['status'] 716 717 def clean_up(self): 718 """Cleans up the FuchsiaDevice object, releases any resources it 719 claimed, and restores saved networks if applicable. For reboots, use 720 clean_up_services only. 721 722 Note: Any exceptions thrown in this method must be caught and handled, 723 ensuring that clean_up_services is run. Otherwise, the syslog listening 724 thread will never join and will leave tests hanging. 725 """ 726 # If and only if wlan is configured, and using the policy layer 727 if self.association_mechanism == 'policy': 728 try: 729 self.wlan_policy_controller.clean_up() 730 except Exception as err: 731 self.log.warning('Unable to clean up WLAN Policy layer: %s' % 732 err) 733 734 self.stop_services() 735 736 if self.package_server: 737 self.package_server.clean_up() 738 739 def get_interface_ip_addresses(self, interface): 740 return get_interface_ip_addresses(self, interface) 741 742 def wait_for_ipv4_addr(self, interface: str) -> None: 743 """Checks if device has an ipv4 private address. Sleeps 1 second between 744 retries. 745 746 Args: 747 interface: name of interface from which to get ipv4 address. 748 749 Raises: 750 ConnectionError, if device does not have an ipv4 address after all 751 timeout. 752 """ 753 self.log.info( 754 f'Checking for valid ipv4 addr. Retry {IP_ADDRESS_TIMEOUT} seconds.' 755 ) 756 timeout = time.time() + IP_ADDRESS_TIMEOUT 757 while time.time() < timeout: 758 ip_addrs = self.get_interface_ip_addresses(interface) 759 760 if len(ip_addrs['ipv4_private']) > 0: 761 self.log.info("Device has an ipv4 address: " 762 f"{ip_addrs['ipv4_private'][0]}") 763 break 764 else: 765 self.log.debug( 766 'Device does not yet have an ipv4 address...retrying in 1 ' 767 'second.') 768 time.sleep(1) 769 else: 770 raise ConnectionError('Device failed to get an ipv4 address.') 771 772 def wait_for_ipv6_addr(self, interface: str) -> None: 773 """Checks if device has an ipv6 private local address. Sleeps 1 second 774 between retries. 775 776 Args: 777 interface: name of interface from which to get ipv6 address. 778 779 Raises: 780 ConnectionError, if device does not have an ipv6 address after all 781 timeout. 782 """ 783 self.log.info( 784 f'Checking for valid ipv6 addr. Retry {IP_ADDRESS_TIMEOUT} seconds.' 785 ) 786 timeout = time.time() + IP_ADDRESS_TIMEOUT 787 while time.time() < timeout: 788 ip_addrs = self.get_interface_ip_addresses(interface) 789 if len(ip_addrs['ipv6_private_local']) > 0: 790 self.log.info("Device has an ipv6 private local address: " 791 f"{ip_addrs['ipv6_private_local'][0]}") 792 break 793 else: 794 self.log.debug( 795 'Device does not yet have an ipv6 address...retrying in 1 ' 796 'second.') 797 time.sleep(1) 798 else: 799 raise ConnectionError('Device failed to get an ipv6 address.') 800 801 def _check_reachable(self, 802 timeout_sec: int = FUCHSIA_DEFAULT_CONNECT_TIMEOUT 803 ) -> None: 804 """Checks the reachability of the Fuchsia device.""" 805 end_time = time.time() + timeout_sec 806 self.log.info('Verifying device is reachable.') 807 while time.time() < end_time: 808 # TODO (b/249343632): Consolidate ping commands and fix timeout in 809 # utils.can_ping. 810 if utils.can_ping(job, self.ip): 811 self.log.info('Device is reachable.') 812 break 813 else: 814 self.log.debug( 815 'Device is not reachable. Retrying in 1 second.') 816 time.sleep(1) 817 else: 818 raise ConnectionError('Device is unreachable.') 819 820 def _check_unreachable(self, 821 timeout_sec: int = FUCHSIA_DEFAULT_CONNECT_TIMEOUT 822 ) -> None: 823 """Checks the Fuchsia device becomes unreachable.""" 824 end_time = time.time() + timeout_sec 825 self.log.info('Verifying device is unreachable.') 826 while (time.time() < end_time): 827 if utils.can_ping(job, self.ip): 828 self.log.debug( 829 'Device is still reachable. Retrying in 1 second.') 830 time.sleep(1) 831 else: 832 self.log.info('Device is not reachable.') 833 break 834 else: 835 raise ConnectionError('Device failed to become unreachable.') 836 837 def check_connect_response(self, connect_response): 838 if connect_response.get("error") is None: 839 # Checks the response from SL4F and if there is no error, check 840 # the result. 841 connection_result = connect_response.get("result") 842 if not connection_result: 843 # Ideally the error would be present but just outputting a log 844 # message until available. 845 self.log.debug("Connect call failed, aborting!") 846 return False 847 else: 848 # Returns True if connection was successful. 849 return True 850 else: 851 # the response indicates an error - log and raise failure 852 self.log.debug("Aborting! - Connect call failed with error: %s" % 853 connect_response.get("error")) 854 return False 855 856 def check_disconnect_response(self, disconnect_response): 857 if disconnect_response.get("error") is None: 858 # Returns True if disconnect was successful. 859 return True 860 else: 861 # the response indicates an error - log and raise failure 862 self.log.debug("Disconnect call failed with error: %s" % 863 disconnect_response.get("error")) 864 return False 865 866 # TODO(fxb/64657): Determine more stable solution to country code config on 867 # device bring up. 868 def configure_regulatory_domain(self, desired_country_code): 869 """Allows the user to set the device country code via ACTS config 870 871 Usage: 872 In FuchsiaDevice config, add "country_code": "<CC>" 873 """ 874 if self.ssh_config: 875 # Country code can be None, from ACTS config. 876 if desired_country_code: 877 desired_country_code = desired_country_code.upper() 878 response = self.sl4f.regulatory_region_lib.setRegion( 879 desired_country_code) 880 if response.get('error'): 881 raise FuchsiaDeviceError( 882 'Failed to set regulatory domain. Err: %s' % 883 response['error']) 884 end_time = time.time() + FUCHSIA_COUNTRY_CODE_TIMEOUT 885 while time.time() < end_time: 886 ascii_cc = self.sl4f.wlan_lib.wlanGetCountry(0).get( 887 'result') 888 # Convert ascii_cc to string, then compare 889 if ascii_cc and (''.join(chr(c) for c in ascii_cc).upper() 890 == desired_country_code): 891 self.log.debug('Country code successfully set to %s.' % 892 desired_country_code) 893 return 894 self.log.debug('Country code not yet updated. Retrying.') 895 time.sleep(1) 896 raise FuchsiaDeviceError('Country code never updated to %s' % 897 desired_country_code) 898 899 def stop_services(self): 900 """Stops the ffx daemon and deletes SL4F property.""" 901 self.log.info('Stopping host device services.') 902 del self.sl4f 903 del self.ffx 904 905 def load_config(self, config): 906 pass 907 908 def take_bug_report(self, test_name=None, begin_time=None): 909 """Takes a bug report on the device and stores it in a file. 910 911 Args: 912 test_name: DEPRECATED. Do not specify this argument; it is only used 913 for logging. Name of the test case that triggered this bug 914 report. 915 begin_time: DEPRECATED. Do not specify this argument; it allows 916 overwriting of bug reports when this function is called several 917 times in one test. Epoch time when the test started. If not 918 specified, the current time will be used. 919 """ 920 if not self.ssh_config: 921 self.log.warn( 922 'Skipping take_bug_report because ssh_config is not specified') 923 return 924 925 if test_name: 926 self.log.info( 927 f"Taking snapshot of {self.mdns_name} for {test_name}") 928 else: 929 self.log.info(f"Taking snapshot of {self.mdns_name}") 930 931 epoch = begin_time if begin_time else utils.get_current_epoch_time() 932 time_stamp = acts_logger.normalize_log_line_timestamp( 933 acts_logger.epoch_to_log_line_timestamp(epoch)) 934 out_dir = context.get_current_context().get_full_output_path() 935 out_path = os.path.join(out_dir, f'{self.mdns_name}_{time_stamp}.zip') 936 937 try: 938 subprocess.run( 939 [f"ssh -F {self.ssh_config} {self.ip} snapshot > {out_path}"], 940 shell=True) 941 self.log.info(f'Snapshot saved to {out_path}') 942 except Exception as err: 943 self.log.error(f'Failed to take snapshot: {err}') 944 945 def take_bt_snoop_log(self, custom_name=None): 946 """Takes a the bt-snoop log from the device and stores it in a file 947 in a pcap format. 948 """ 949 bt_snoop_path = context.get_current_context().get_full_output_path() 950 time_stamp = acts_logger.normalize_log_line_timestamp( 951 acts_logger.epoch_to_log_line_timestamp(time.time())) 952 out_name = "FuchsiaDevice%s_%s" % ( 953 self.serial, time_stamp.replace(" ", "_").replace(":", "-")) 954 out_name = "%s.pcap" % out_name 955 if custom_name: 956 out_name = "%s_%s.pcap" % (self.serial, custom_name) 957 else: 958 out_name = "%s.pcap" % out_name 959 full_out_path = os.path.join(bt_snoop_path, out_name) 960 bt_snoop_data = self.ssh.run('bt-snoop-cli -d -f pcap').raw_stdout 961 bt_snoop_file = open(full_out_path, 'wb') 962 bt_snoop_file.write(bt_snoop_data) 963 bt_snoop_file.close() 964