blob: a42a7229c708e99715c08444144c5685df1a154e [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2018 - The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import backoff
import json
import logging
import os
import random
import re
import requests
import socket
import subprocess
import time
from acts import context
from acts import logger as acts_logger
from acts import signals
from acts import utils
from acts.controllers import pdu
from acts.libs.proc import job
from acts.utils import get_fuchsia_mdns_ipv6_address
from acts.controllers.fuchsia_lib.audio_lib import FuchsiaAudioLib
from acts.controllers.fuchsia_lib.backlight_lib import FuchsiaBacklightLib
from acts.controllers.fuchsia_lib.basemgr_lib import FuchsiaBasemgrLib
from acts.controllers.fuchsia_lib.bt.avdtp_lib import FuchsiaAvdtpLib
from acts.controllers.fuchsia_lib.bt.ble_lib import FuchsiaBleLib
from acts.controllers.fuchsia_lib.bt.bts_lib import FuchsiaBtsLib
from acts.controllers.fuchsia_lib.bt.gattc_lib import FuchsiaGattcLib
from acts.controllers.fuchsia_lib.bt.gatts_lib import FuchsiaGattsLib
from acts.controllers.fuchsia_lib.bt.hfp_lib import FuchsiaHfpLib
from acts.controllers.fuchsia_lib.bt.rfcomm_lib import FuchsiaRfcommLib
from acts.controllers.fuchsia_lib.bt.sdp_lib import FuchsiaProfileServerLib
from acts.controllers.fuchsia_lib.ffx import FFX
from acts.controllers.fuchsia_lib.gpio_lib import FuchsiaGpioLib
from acts.controllers.fuchsia_lib.hardware_power_statecontrol_lib import FuchsiaHardwarePowerStatecontrolLib
from acts.controllers.fuchsia_lib.hwinfo_lib import FuchsiaHwinfoLib
from acts.controllers.fuchsia_lib.i2c_lib import FuchsiaI2cLib
from acts.controllers.fuchsia_lib.input_report_lib import FuchsiaInputReportLib
from acts.controllers.fuchsia_lib.kernel_lib import FuchsiaKernelLib
from acts.controllers.fuchsia_lib.lib_controllers.netstack_controller import NetstackController
from acts.controllers.fuchsia_lib.lib_controllers.wlan_controller import WlanController
from acts.controllers.fuchsia_lib.lib_controllers.wlan_policy_controller import WlanPolicyController
from acts.controllers.fuchsia_lib.light_lib import FuchsiaLightLib
from acts.controllers.fuchsia_lib.location.regulatory_region_lib import FuchsiaRegulatoryRegionLib
from acts.controllers.fuchsia_lib.logging_lib import FuchsiaLoggingLib
from acts.controllers.fuchsia_lib.netstack.netstack_lib import FuchsiaNetstackLib
from acts.controllers.fuchsia_lib.ram_lib import FuchsiaRamLib
from acts.controllers.fuchsia_lib.session_manager_lib import FuchsiaSessionManagerLib
from acts.controllers.fuchsia_lib.sysinfo_lib import FuchsiaSysInfoLib
from acts.controllers.fuchsia_lib.syslog_lib import FuchsiaSyslogError
from acts.controllers.fuchsia_lib.syslog_lib import create_syslog_process
from acts.controllers.fuchsia_lib.utils_lib import SshResults
from acts.controllers.fuchsia_lib.utils_lib import create_ssh_connection
from acts.controllers.fuchsia_lib.utils_lib import flash
from acts.controllers.fuchsia_lib.wlan_ap_policy_lib import FuchsiaWlanApPolicyLib
from acts.controllers.fuchsia_lib.wlan_deprecated_configuration_lib import FuchsiaWlanDeprecatedConfigurationLib
from acts.controllers.fuchsia_lib.wlan_lib import FuchsiaWlanLib
from acts.controllers.fuchsia_lib.wlan_policy_lib import FuchsiaWlanPolicyLib
MOBLY_CONTROLLER_CONFIG_NAME = "FuchsiaDevice"
ACTS_CONTROLLER_REFERENCE_NAME = "fuchsia_devices"
CONTROL_PATH_REPLACE_VALUE = " ControlPath /tmp/fuchsia--%r@%h:%p"
FUCHSIA_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
FUCHSIA_DEVICE_INVALID_CONFIG = ("Fuchsia device config must be either a str "
"or dict. abort! Invalid element %i in %r")
FUCHSIA_DEVICE_NO_IP_MSG = "No IP address specified, abort!"
FUCHSIA_COULD_NOT_GET_DESIRED_STATE = "Could not %s %s."
FUCHSIA_INVALID_CONTROL_STATE = "Invalid control state (%s). abort!"
FUCHSIA_SSH_CONFIG_NOT_DEFINED = ("Cannot send ssh commands since the "
"ssh_config was not specified in the Fuchsia"
"device config.")
FUCHSIA_SSH_USERNAME = "fuchsia"
FUCHSIA_TIME_IN_NANOSECONDS = 1000000000
SL4F_APK_NAME = "com.googlecode.android_scripting"
DAEMON_INIT_TIMEOUT_SEC = 1
DAEMON_ACTIVATED_STATES = ["running", "start"]
DAEMON_DEACTIVATED_STATES = ["stop", "stopped"]
FUCHSIA_DEFAULT_LOG_CMD = 'iquery --absolute_paths --cat --format= --recursive'
FUCHSIA_DEFAULT_LOG_ITEMS = [
'/hub/c/scenic.cmx/[0-9]*/out/objects',
'/hub/c/root_presenter.cmx/[0-9]*/out/objects',
'/hub/c/wlanstack2.cmx/[0-9]*/out/public',
'/hub/c/basemgr.cmx/[0-9]*/out/objects'
]
FUCHSIA_RECONNECT_AFTER_REBOOT_TIME = 5
CHANNEL_OPEN_TIMEOUT = 5
FUCHSIA_GET_VERSION_CMD = 'cat /config/build-info/version'
FUCHSIA_REBOOT_TYPE_SOFT = 'soft'
FUCHSIA_REBOOT_TYPE_SOFT_AND_FLASH = 'flash'
FUCHSIA_REBOOT_TYPE_HARD = 'hard'
FUCHSIA_DEFAULT_CONNECT_TIMEOUT = 60
FUCHSIA_DEFAULT_COMMAND_TIMEOUT = 60
FUCHSIA_DEFAULT_CLEAN_UP_COMMAND_TIMEOUT = 15
FUCHSIA_COUNTRY_CODE_TIMEOUT = 15
FUCHSIA_DEFAULT_COUNTRY_CODE_US = 'US'
MDNS_LOOKUP_RETRY_MAX = 3
VALID_ASSOCIATION_MECHANISMS = {None, 'policy', 'drivers'}
class FuchsiaDeviceError(signals.ControllerError):
pass
def create(configs):
if not configs:
raise FuchsiaDeviceError(FUCHSIA_DEVICE_EMPTY_CONFIG_MSG)
elif not isinstance(configs, list):
raise FuchsiaDeviceError(FUCHSIA_DEVICE_NOT_LIST_CONFIG_MSG)
for index, config in enumerate(configs):
if isinstance(config, str):
configs[index] = {"ip": config}
elif not isinstance(config, dict):
raise FuchsiaDeviceError(FUCHSIA_DEVICE_INVALID_CONFIG %
(index, configs))
return get_instances(configs)
def destroy(fds):
for fd in fds:
fd.clean_up()
del fd
def get_info(fds):
"""Get information on a list of FuchsiaDevice objects.
Args:
fds: A list of FuchsiaDevice objects.
Returns:
A list of dict, each representing info for FuchsiaDevice objects.
"""
device_info = []
for fd in fds:
info = {"ip": fd.ip}
device_info.append(info)
return device_info
def get_instances(fds_conf_data):
"""Create FuchsiaDevice instances from a list of Fuchsia ips.
Args:
fds_conf_data: A list of dicts that contain Fuchsia device info.
Returns:
A list of FuchsiaDevice objects.
"""
return [FuchsiaDevice(fd_conf_data) for fd_conf_data in fds_conf_data]
class FuchsiaDevice:
"""Class representing a Fuchsia device.
Each object of this class represents one Fuchsia device in ACTS.
Attributes:
ip: The full address or Fuchsia abstract name to contact the Fuchsia
device at
log: A logger object.
ssh_port: The SSH TCP port number of the Fuchsia device.
sl4f_port: The SL4F HTTP port number of the Fuchsia device.
ssh_config: The ssh_config for connecting to the Fuchsia device.
"""
def __init__(self, fd_conf_data):
"""
Args:
fd_conf_data: A dict of a fuchsia device configuration data
Required keys:
ip: IP address of fuchsia device
optional key:
sl4_port: Port for the sl4f web server on the fuchsia device
(Default: 80)
ssh_config: Location of the ssh_config file to connect to
the fuchsia device
(Default: None)
ssh_port: Port for the ssh server on the fuchsia device
(Default: 22)
"""
self.conf_data = fd_conf_data
if "ip" not in fd_conf_data:
raise FuchsiaDeviceError(FUCHSIA_DEVICE_NO_IP_MSG)
self.ip = fd_conf_data["ip"]
self.orig_ip = fd_conf_data["ip"]
self.sl4f_port = fd_conf_data.get("sl4f_port", 80)
self.ssh_port = fd_conf_data.get("ssh_port", 22)
self.ssh_config = fd_conf_data.get("ssh_config", None)
self.ssh_priv_key = fd_conf_data.get("ssh_priv_key", None)
self.authorized_file = fd_conf_data.get("authorized_file_loc", None)
self.serial_number = fd_conf_data.get("serial_number", None)
self.device_type = fd_conf_data.get("device_type", None)
self.product_type = fd_conf_data.get("product_type", None)
self.board_type = fd_conf_data.get("board_type", None)
self.build_number = fd_conf_data.get("build_number", None)
self.build_type = fd_conf_data.get("build_type", None)
self.server_path = fd_conf_data.get("server_path", None)
self.specific_image = fd_conf_data.get("specific_image", None)
self.ffx_binary_path = fd_conf_data.get("ffx_binary_path", None)
self.mdns_name = fd_conf_data.get("mdns_name", None)
# Instead of the input ssh_config, a new config is generated with proper
# ControlPath to the test output directory.
output_path = context.get_current_context().get_base_output_path()
generated_ssh_config = os.path.join(output_path,
"ssh_config_{}".format(self.ip))
self._set_control_path_config(self.ssh_config, generated_ssh_config)
self.ssh_config = generated_ssh_config
self.ssh_username = fd_conf_data.get("ssh_username",
FUCHSIA_SSH_USERNAME)
self.hard_reboot_on_fail = fd_conf_data.get("hard_reboot_on_fail",
False)
self.take_bug_report_on_fail = fd_conf_data.get(
"take_bug_report_on_fail", False)
self.device_pdu_config = fd_conf_data.get("PduDevice", None)
self.config_country_code = fd_conf_data.get(
'country_code', FUCHSIA_DEFAULT_COUNTRY_CODE_US).upper()
self._persistent_ssh_conn = None
# WLAN interface info is populated inside configure_wlan
self.wlan_client_interfaces = {}
self.wlan_ap_interfaces = {}
self.wlan_client_test_interface_name = fd_conf_data.get(
'wlan_client_test_interface', None)
self.wlan_ap_test_interface_name = fd_conf_data.get(
'wlan_ap_test_interface', None)
# Whether to use 'policy' or 'drivers' for WLAN connect/disconnect calls
# If set to None, wlan is not configured.
self.association_mechanism = None
# Defaults to policy layer, unless otherwise specified in the config
self.default_association_mechanism = fd_conf_data.get(
'association_mechanism', 'policy')
# Whether to clear and preserve existing saved networks and client
# connections state, to be restored at device teardown.
self.default_preserve_saved_networks = fd_conf_data.get(
'preserve_saved_networks', True)
if utils.is_valid_ipv4_address(self.ip):
self.address = "http://{}:{}".format(self.ip, self.sl4f_port)
elif utils.is_valid_ipv6_address(self.ip):
self.address = "http://[{}]:{}".format(self.ip, self.sl4f_port)
else:
mdns_ip = None
for retry_counter in range(MDNS_LOOKUP_RETRY_MAX):
mdns_ip = get_fuchsia_mdns_ipv6_address(self.ip)
if mdns_ip:
break
else:
time.sleep(1)
if mdns_ip and utils.is_valid_ipv6_address(mdns_ip):
# self.ip was actually an mdns name. Use it for self.mdns_name
# unless one was explicitly provided.
self.mdns_name = self.mdns_name or self.ip
self.ip = mdns_ip
self.address = "http://[{}]:{}".format(self.ip, self.sl4f_port)
else:
raise ValueError('Invalid IP: %s' % self.ip)
self.log = acts_logger.create_tagged_trace_logger(
"FuchsiaDevice | %s" % self.orig_ip)
self.init_address = self.address + "/init"
self.cleanup_address = self.address + "/cleanup"
self.print_address = self.address + "/print_clients"
self.ping_rtt_match = re.compile(r'RTT Min/Max/Avg '
r'= \[ (.*?) / (.*?) / (.*?) \] ms')
# TODO(): Come up with better client numbering system
self.client_id = "FuchsiaClient" + str(random.randint(0, 1000000))
self.test_counter = 0
self.serial = re.sub('[.:%]', '_', self.ip)
log_path_base = getattr(logging, 'log_path', '/tmp/logs')
self.log_path = os.path.join(log_path_base,
'FuchsiaDevice%s' % self.serial)
self.fuchsia_log_file_path = os.path.join(
self.log_path, "fuchsialog_%s_debug.txt" % self.serial)
self.log_process = None
self.init_libraries()
self.setup_commands = fd_conf_data.get('setup_commands', [])
self.teardown_commands = fd_conf_data.get('teardown_commands', [])
try:
self.start_services()
self.run_commands_from_config(self.setup_commands)
except Exception as e:
# Prevent a threading error, since controller isn't fully up yet.
self.clean_up()
raise e
def _set_control_path_config(self, old_config, new_config):
"""Given an input ssh_config, write to a new config with proper
ControlPath values in place, if it doesn't exist already.
Args:
old_config: string, path to the input config
new_config: string, path to store the new config
"""
if os.path.isfile(new_config):
return
ssh_config_copy = ""
with open(old_config, 'r') as file:
ssh_config_copy = re.sub('(\sControlPath\s.*)',
CONTROL_PATH_REPLACE_VALUE,
file.read(),
flags=re.M)
with open(new_config, 'w') as file:
file.write(ssh_config_copy)
def init_libraries(self):
# Grab commands from FuchsiaAudioLib
self.audio_lib = FuchsiaAudioLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaAvdtpLib
self.avdtp_lib = FuchsiaAvdtpLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaHfpLib
self.hfp_lib = FuchsiaHfpLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaRfcommLib
self.rfcomm_lib = FuchsiaRfcommLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaLightLib
self.light_lib = FuchsiaLightLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaBacklightLib
self.backlight_lib = FuchsiaBacklightLib(self.address,
self.test_counter,
self.client_id)
# Grab commands from FuchsiaBasemgrLib
self.basemgr_lib = FuchsiaBasemgrLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaBleLib
self.ble_lib = FuchsiaBleLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaBtsLib
self.bts_lib = FuchsiaBtsLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaGattcLib
self.gattc_lib = FuchsiaGattcLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaGattsLib
self.gatts_lib = FuchsiaGattsLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaGpioLib
self.gpio_lib = FuchsiaGpioLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaHardwarePowerStatecontrolLib
self.hardware_power_statecontrol_lib = (
FuchsiaHardwarePowerStatecontrolLib(self.address,
self.test_counter,
self.client_id))
# Grab commands from FuchsiaHwinfoLib
self.hwinfo_lib = FuchsiaHwinfoLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaI2cLib
self.i2c_lib = FuchsiaI2cLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaInputReportLib
self.input_report_lib = FuchsiaInputReportLib(self.address,
self.test_counter,
self.client_id)
# Grab commands from FuchsiaKernelLib
self.kernel_lib = FuchsiaKernelLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaLoggingLib
self.logging_lib = FuchsiaLoggingLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaNetstackLib
self.netstack_lib = FuchsiaNetstackLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaLightLib
self.ram_lib = FuchsiaRamLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaProfileServerLib
self.sdp_lib = FuchsiaProfileServerLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaRegulatoryRegionLib
self.regulatory_region_lib = FuchsiaRegulatoryRegionLib(
self.address, self.test_counter, self.client_id)
# Grab commands from FuchsiaSysInfoLib
self.sysinfo_lib = FuchsiaSysInfoLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaSessionManagerLib
self.session_manager_lib = FuchsiaSessionManagerLib(self)
# Grabs command from FuchsiaWlanDeprecatedConfigurationLib
self.wlan_deprecated_configuration_lib = (
FuchsiaWlanDeprecatedConfigurationLib(self.address,
self.test_counter,
self.client_id))
# Grab commands from FuchsiaWlanLib
self.wlan_lib = FuchsiaWlanLib(self.address, self.test_counter,
self.client_id)
# Grab commands from FuchsiaWlanApPolicyLib
self.wlan_ap_policy_lib = FuchsiaWlanApPolicyLib(
self.address, self.test_counter, self.client_id)
# Grab commands from FuchsiaWlanPolicyLib
self.wlan_policy_lib = FuchsiaWlanPolicyLib(self.address,
self.test_counter,
self.client_id)
# Contains Netstack functions
self.netstack_controller = NetstackController(self)
# Contains WLAN core functions
self.wlan_controller = WlanController(self)
# Contains WLAN policy functions like save_network, remove_network, etc
self.wlan_policy_controller = WlanPolicyController(self)
@backoff.on_exception(
backoff.constant,
(ConnectionRefusedError, requests.exceptions.ConnectionError),
interval=1.5,
max_tries=4)
def init_sl4f_connection(self):
"""Initializes HTTP connection with SL4F server."""
self.log.debug("Initializing SL4F server connection")
init_data = json.dumps({
"jsonrpc": "2.0",
"id": self.build_id(self.test_counter),
"method": "sl4f.sl4f_init",
"params": {
"client_id": self.client_id
}
})
requests.get(url=self.init_address, data=init_data)
self.test_counter += 1
def init_ffx_connection(self):
"""Initializes ffx's connection to the device.
If ffx has already been initialized, it will be reinitialized. This will
break any running tests calling ffx for this device.
"""
self.log.debug("Initializing ffx connection")
if not self.ffx_binary_path:
raise ValueError(
'Must provide "ffx_binary_path: <path to FFX binary>" in the device config'
)
if not self.mdns_name:
raise ValueError(
'Must provide "mdns_name: <device mDNS name>" in the device config'
)
if hasattr(self, 'ffx'):
self.ffx.clean_up()
self.ffx = FFX(self.ffx_binary_path, self.mdns_name, self.ssh_priv_key)
def run_commands_from_config(self, cmd_dicts):
"""Runs commands on the Fuchsia device from the config file. Useful for
device and/or Fuchsia specific configuration.
Args:
cmd_dicts: list of dictionaries containing the following
'cmd': string, command to run on device
'timeout': int, seconds to wait for command to run (optional)
'skip_status_code_check': bool, disregard errors if true
"""
for cmd_dict in cmd_dicts:
try:
cmd = cmd_dict['cmd']
except KeyError:
raise FuchsiaDeviceError(
'To run a command via config, you must provide key "cmd" '
'containing the command string.')
timeout = cmd_dict.get('timeout', FUCHSIA_DEFAULT_COMMAND_TIMEOUT)
# Catch both boolean and string values from JSON
skip_status_code_check = 'true' == str(
cmd_dict.get('skip_status_code_check', False)).lower()
self.log.info(
'Running command "%s".%s' %
(cmd, ' Ignoring result.' if skip_status_code_check else ''))
result = self.send_command_ssh(
cmd,
timeout=timeout,
skip_status_code_check=skip_status_code_check)
if isinstance(result, Exception):
raise result
elif not skip_status_code_check and result.stderr:
raise FuchsiaDeviceError(
'Error when running command "%s": %s' %
(cmd, result.stderr))
def build_id(self, test_id):
"""Concatenates client_id and test_id to form a command_id
Args:
test_id: string, unique identifier of test command
"""
return self.client_id + "." + str(test_id)
def configure_wlan(self,
association_mechanism=None,
preserve_saved_networks=None):
"""
Readies device for WLAN functionality. If applicable, connects to the
policy layer and clears/saves preexisting saved networks.
Args:
association_mechanism: string, 'policy' or 'drivers'. If None, uses
the default value from init (can be set by ACTS config)
preserve_saved_networks: bool, whether to clear existing saved
networks, and preserve them for restoration later. If None, uses
the default value from init (can be set by ACTS config)
Raises:
FuchsiaDeviceError, if configuration fails
"""
# Set the country code US by default, or country code provided
# in ACTS config
self.configure_regulatory_domain(self.config_country_code)
# If args aren't provided, use the defaults, which can be set in the
# config.
if association_mechanism is None:
association_mechanism = self.default_association_mechanism
if preserve_saved_networks is None:
preserve_saved_networks = self.default_preserve_saved_networks
if association_mechanism not in VALID_ASSOCIATION_MECHANISMS:
raise FuchsiaDeviceError(
'Invalid FuchsiaDevice association_mechanism: %s' %
association_mechanism)
# Allows for wlan to be set up differently in different tests
if self.association_mechanism:
self.deconfigure_wlan()
self.association_mechanism = association_mechanism
self.log.info('Configuring WLAN w/ association mechanism: %s' %
association_mechanism)
if association_mechanism == 'drivers':
self.log.warn(
'You may encounter unusual device behavior when using the '
'drivers directly for WLAN. This should be reserved for '
'debugging specific issues. Normal test runs should use the '
'policy layer.')
if preserve_saved_networks:
self.log.warn(
'Unable to preserve saved networks when using drivers '
'association mechanism (requires policy layer control).')
else:
# This requires SL4F calls, so it can only happen with actual
# devices, not with unit tests.
self.wlan_policy_controller._configure_wlan(
preserve_saved_networks)
# Retrieve WLAN client and AP interfaces
self.wlan_controller.update_wlan_interfaces()
def deconfigure_wlan(self):
"""
Stops WLAN functionality (if it has been started). Used to allow
different tests to use WLAN differently (e.g. some tests require using
wlan policy, while the abstract wlan_device can be setup to use policy
or drivers)
Raises:
FuchsiaDeviveError, if deconfigure fails.
"""
if not self.association_mechanism:
self.log.debug(
'WLAN not configured before deconfigure was called.')
return
# If using policy, stop client connections. Otherwise, just clear
# variables.
if self.association_mechanism != 'drivers':
self.wlan_policy_controller._deconfigure_wlan()
self.association_mechanism = None
def reboot(self,
use_ssh=False,
unreachable_timeout=FUCHSIA_DEFAULT_CONNECT_TIMEOUT,
ping_timeout=FUCHSIA_DEFAULT_CONNECT_TIMEOUT,
ssh_timeout=FUCHSIA_DEFAULT_CONNECT_TIMEOUT,
reboot_type=FUCHSIA_REBOOT_TYPE_SOFT,
testbed_pdus=None):
"""Reboot a FuchsiaDevice.
Soft reboots the device, verifies it becomes unreachable, then verifies
it comes back online. Re-initializes services so the tests can continue.
Args:
use_ssh: bool, if True, use fuchsia shell command via ssh to reboot
instead of SL4F.
unreachable_timeout: int, time to wait for device to become
unreachable.
ping_timeout: int, time to wait for device to respond to pings.
ssh_timeout: int, time to wait for device to be reachable via ssh.
reboot_type: boolFUCHSIA_REBOOT_TYPE_SOFT or
FUCHSIA_REBOOT_TYPE_HARD
testbed_pdus: list, all testbed PDUs
Raises:
ConnectionError, if device fails to become unreachable, fails to
come back up, or if SL4F does not setup correctly.
"""
skip_unreachable_check = False
# Call Reboot
if reboot_type == FUCHSIA_REBOOT_TYPE_SOFT:
if use_ssh:
self.log.info('Sending reboot command via SSH.')
with utils.SuppressLogOutput():
self.clean_up_services()
self.send_command_ssh(
'dm reboot',
timeout=FUCHSIA_RECONNECT_AFTER_REBOOT_TIME,
skip_status_code_check=True)
else:
self.log.info('Calling SL4F reboot command.')
with utils.SuppressLogOutput():
self.hardware_power_statecontrol_lib.suspendReboot(
timeout=3)
self.clean_up_services()
elif reboot_type == FUCHSIA_REBOOT_TYPE_SOFT_AND_FLASH:
flash(self, use_ssh, FUCHSIA_RECONNECT_AFTER_REBOOT_TIME)
skip_unreachable_check = True
elif reboot_type == FUCHSIA_REBOOT_TYPE_HARD:
self.log.info('Power cycling FuchsiaDevice (%s)' % self.ip)
if not testbed_pdus:
raise AttributeError('Testbed PDUs must be supplied '
'to hard reboot a fuchsia_device.')
device_pdu, device_pdu_port = pdu.get_pdu_port_for_device(
self.device_pdu_config, testbed_pdus)
with utils.SuppressLogOutput():
self.clean_up_services()
self.log.info('Killing power to FuchsiaDevice (%s)...' % self.ip)
device_pdu.off(str(device_pdu_port))
else:
raise ValueError('Invalid reboot type: %s' % reboot_type)
if not skip_unreachable_check:
# Wait for unreachable
self.log.info('Verifying device is unreachable.')
timeout = time.time() + unreachable_timeout
while (time.time() < timeout):
if utils.can_ping(job, self.ip):
self.log.debug('Device is still pingable. Retrying.')
else:
if reboot_type == FUCHSIA_REBOOT_TYPE_HARD:
self.log.info(
'Restoring power to FuchsiaDevice (%s)...' %
self.ip)
device_pdu.on(str(device_pdu_port))
break
else:
self.log.info(
'Device failed to go offline. Restarting services...')
self.start_services()
raise ConnectionError('Device never went down.')
self.log.info('Device is unreachable as expected.')
if reboot_type == FUCHSIA_REBOOT_TYPE_HARD:
self.log.info('Restoring power to FuchsiaDevice (%s)...' % self.ip)
device_pdu.on(str(device_pdu_port))
self.log.info('Waiting for device to respond to pings.')
end_time = time.time() + ping_timeout
while time.time() < end_time:
if utils.can_ping(job, self.ip):
break
else:
self.log.debug('Device is not pingable. Retrying in 1 second.')
time.sleep(1)
else:
raise ConnectionError('Device never came back online.')
self.log.info('Device responded to pings.')
self.log.info('Waiting for device to allow ssh connection.')
end_time = time.time() + ssh_timeout
while time.time() < end_time:
try:
self.send_command_ssh('\n')
except Exception:
self.log.debug(
'Could not SSH to device. Retrying in 1 second.')
time.sleep(1)
else:
break
else:
raise ConnectionError('Failed to connect to device via SSH.')
self.log.info('Device now available via ssh.')
# Creating new log process, start it, start new persistent ssh session,
# start SL4F, and connect via SL4F
self.log.info(f'Restarting services on FuchsiaDevice {self.ip}')
self.start_services()
# Verify SL4F is up.
self.log.info('Verifying SL4F commands can run.')
try:
self.hwinfo_lib.getDeviceInfo()
except Exception as err:
raise ConnectionError(
'Failed to connect and run command via SL4F. Err: %s' % err)
# Reconfigure country code, as it does not persist after reboots
self.configure_regulatory_domain(self.config_country_code)
try:
self.run_commands_from_config(self.setup_commands)
except FuchsiaDeviceError:
# Prevent a threading error, since controller isn't fully up yet.
self.clean_up()
raise FuchsiaDeviceError(
'Failed to run setup commands after reboot.')
# If wlan was configured before reboot, it must be configured again
# after rebooting, as it was before reboot. No preserving should occur.
if self.association_mechanism:
pre_reboot_association_mechanism = self.association_mechanism
# Prevent configure_wlan from thinking it needs to deconfigure first
self.association_mechanism = None
self.configure_wlan(
association_mechanism=pre_reboot_association_mechanism,
preserve_saved_networks=False)
self.log.info(
'Device has rebooted, SL4F is reconnected and functional.')
def send_command_ssh(self,
test_cmd,
connect_timeout=FUCHSIA_DEFAULT_CONNECT_TIMEOUT,
timeout=FUCHSIA_DEFAULT_COMMAND_TIMEOUT,
skip_status_code_check=False):
"""Sends an SSH command to a Fuchsia device
Args:
test_cmd: string, command to send to Fuchsia device over SSH.
connect_timeout: Timeout to wait for connecting via SSH.
timeout: Timeout to wait for a command to complete.
skip_status_code_check: Whether to check for the status code.
Returns:
A SshResults object containing the results of the ssh command.
"""
command_result = False
ssh_conn = None
if not self.ssh_config:
self.log.warning(FUCHSIA_SSH_CONFIG_NOT_DEFINED)
else:
try:
ssh_conn = create_ssh_connection(
self.ip,
self.ssh_username,
self.ssh_config,
ssh_port=self.ssh_port,
connect_timeout=connect_timeout)
cmd_result_stdin, cmd_result_stdout, cmd_result_stderr = (
ssh_conn.exec_command(test_cmd, timeout=timeout))
if not skip_status_code_check:
command_result = SshResults(cmd_result_stdin,
cmd_result_stdout,
cmd_result_stderr,
cmd_result_stdout.channel)
except Exception as e:
self.log.warning("Problem running ssh command: %s"
"\n Exception: %s" % (test_cmd, e))
return e
finally:
if ssh_conn is not None:
ssh_conn.close()
return command_result
def version(self, timeout=FUCHSIA_DEFAULT_COMMAND_TIMEOUT):
"""Returns the version of Fuchsia running on the device.
Args:
timeout: (int) Seconds to wait for command to run.
Returns:
A string containing the Fuchsia version number.
For example, "5.20210713.2.1".
Raises:
DeviceOffline: If SSH to the device fails.
"""
return self.send_command_ssh(FUCHSIA_GET_VERSION_CMD,
timeout=timeout).stdout
def ping(self,
dest_ip,
count=3,
interval=1000,
timeout=1000,
size=25,
additional_ping_params=None):
"""Pings from a Fuchsia device to an IPv4 address or hostname
Args:
dest_ip: (str) The ip or hostname to ping.
count: (int) How many icmp packets to send.
interval: (int) How long to wait between pings (ms)
timeout: (int) How long to wait before having the icmp packet
timeout (ms).
size: (int) Size of the icmp packet.
additional_ping_params: (str) command option flags to
append to the command string
Returns:
A dictionary for the results of the ping. The dictionary contains
the following items:
status: Whether the ping was successful.
rtt_min: The minimum round trip time of the ping.
rtt_max: The minimum round trip time of the ping.
rtt_avg: The avg round trip time of the ping.
stdout: The standard out of the ping command.
stderr: The standard error of the ping command.
"""
rtt_min = None
rtt_max = None
rtt_avg = None
self.log.debug("Pinging %s..." % dest_ip)
if not additional_ping_params:
additional_ping_params = ''
ping_result = self.send_command_ssh(
'ping -c %s -i %s -t %s -s %s %s %s' %
(count, interval, timeout, size, additional_ping_params, dest_ip))
if isinstance(ping_result, job.Error):
ping_result = ping_result.result
if ping_result.stderr:
status = False
else:
status = True
rtt_line = ping_result.stdout.split('\n')[:-1]
rtt_line = rtt_line[-1]
rtt_stats = re.search(self.ping_rtt_match, rtt_line)
rtt_min = rtt_stats.group(1)
rtt_max = rtt_stats.group(2)
rtt_avg = rtt_stats.group(3)
return {
'status': status,
'rtt_min': rtt_min,
'rtt_max': rtt_max,
'rtt_avg': rtt_avg,
'stdout': ping_result.stdout,
'stderr': ping_result.stderr
}
def can_ping(self,
dest_ip,
count=1,
interval=1000,
timeout=1000,
size=25,
additional_ping_params=None):
"""Returns whether fuchsia device can ping a given dest address"""
ping_result = self.ping(dest_ip,
count=count,
interval=interval,
timeout=timeout,
size=size,
additional_ping_params=additional_ping_params)
return ping_result['status']
def print_clients(self):
"""Gets connected clients from SL4F server"""
self.log.debug("Request to print clients")
print_id = self.build_id(self.test_counter)
print_args = {}
print_method = "sl4f.sl4f_print_clients"
data = json.dumps({
"jsonrpc": "2.0",
"id": print_id,
"method": print_method,
"params": print_args
})
r = requests.get(url=self.print_address, data=data).json()
self.test_counter += 1
return r
def clean_up(self):
"""Cleans up the FuchsiaDevice object, releases any resources it
claimed, and restores saved networks is applicable. For reboots, use
clean_up_services only.
Note: Any exceptions thrown in this method must be caught and handled,
ensuring that clean_up_services is run. Otherwise, the syslog listening
thread will never join and will leave tests hanging.
"""
# If and only if wlan is configured, and using the policy layer
if self.association_mechanism == 'policy':
try:
self.wlan_policy_controller._clean_up()
except Exception as err:
self.log.warning('Unable to clean up WLAN Policy layer: %s' %
err)
try:
self.run_commands_from_config(self.teardown_commands)
except Exception as err:
self.log.warning('Failed to run teardown_commands: %s' % err)
# This MUST be run, otherwise syslog threads will never join.
self.clean_up_services()
def clean_up_services(self):
""" Cleans up FuchsiaDevice services (e.g. SL4F). Subset of clean_up,
to be used for reboots, when testing is to continue (as opposed to
teardown after testing is finished.)
"""
cleanup_id = self.build_id(self.test_counter)
cleanup_args = {}
cleanup_method = "sl4f.sl4f_cleanup"
data = json.dumps({
"jsonrpc": "2.0",
"id": cleanup_id,
"method": cleanup_method,
"params": cleanup_args
})
try:
response = requests.get(
url=self.cleanup_address,
data=data,
timeout=FUCHSIA_DEFAULT_CLEAN_UP_COMMAND_TIMEOUT).json()
self.log.debug(response)
except Exception as err:
self.log.exception("Cleanup request failed with %s:" % err)
finally:
self.test_counter += 1
self.stop_services()
def check_process_state(self, process_name):
"""Checks the state of a process on the Fuchsia device
Returns:
True if the process_name is running
False if process_name is not running
"""
ps_cmd = self.send_command_ssh("ps")
return process_name in ps_cmd.stdout
def check_process_with_expectation(self, process_name, expectation=None):
"""Checks the state of a process on the Fuchsia device and returns
true or false depending the stated expectation
Args:
process_name: The name of the process to check for.
expectation: The state expectation of state of process
Returns:
True if the state of the process matches the expectation
False if the state of the process does not match the expectation
"""
process_state = self.check_process_state(process_name)
if expectation in DAEMON_ACTIVATED_STATES:
return process_state
elif expectation in DAEMON_DEACTIVATED_STATES:
return not process_state
else:
raise ValueError("Invalid expectation value (%s). abort!" %
expectation)
def control_daemon(self, process_name, action):
"""Starts or stops a process on a Fuchsia device
Args:
process_name: the name of the process to start or stop
action: specify whether to start or stop a process
"""
if not (process_name[-4:] == '.cmx' or process_name[-4:] == '.cml'):
process_name = '%s.cmx' % process_name
unable_to_connect_msg = None
process_state = False
try:
if not self._persistent_ssh_conn:
self._persistent_ssh_conn = (create_ssh_connection(
self.ip,
self.ssh_username,
self.ssh_config,
ssh_port=self.ssh_port))
self._persistent_ssh_conn.exec_command(
"killall %s" % process_name, timeout=CHANNEL_OPEN_TIMEOUT)
# This command will effectively stop the process but should
# be used as a cleanup before starting a process. It is a bit
# confusing to have the msg saying "attempting to stop
# the process" after the command already tried but since both start
# and stop need to run this command, this is the best place
# for the command.
if action in DAEMON_ACTIVATED_STATES:
self.log.debug("Attempting to start Fuchsia "
"devices services.")
self._persistent_ssh_conn.exec_command(
"run fuchsia-pkg://fuchsia.com/%s#meta/%s &" %
(process_name[:-4], process_name))
process_initial_msg = (
"%s has not started yet. Waiting %i second and "
"checking again." %
(process_name, DAEMON_INIT_TIMEOUT_SEC))
process_timeout_msg = ("Timed out waiting for %s to start." %
process_name)
unable_to_connect_msg = ("Unable to start %s no Fuchsia "
"device via SSH. %s may not "
"be started." %
(process_name, process_name))
elif action in DAEMON_DEACTIVATED_STATES:
process_initial_msg = ("%s is running. Waiting %i second and "
"checking again." %
(process_name, DAEMON_INIT_TIMEOUT_SEC))
process_timeout_msg = ("Timed out waiting trying to kill %s." %
process_name)
unable_to_connect_msg = ("Unable to stop %s on Fuchsia "
"device via SSH. %s may "
"still be running." %
(process_name, process_name))
else:
raise FuchsiaDeviceError(FUCHSIA_INVALID_CONTROL_STATE %
action)
timeout_counter = 0
while not process_state:
self.log.info(process_initial_msg)
time.sleep(DAEMON_INIT_TIMEOUT_SEC)
timeout_counter += 1
process_state = (self.check_process_with_expectation(
process_name, expectation=action))
if timeout_counter == (DAEMON_INIT_TIMEOUT_SEC * 3):
self.log.info(process_timeout_msg)
break
if not process_state:
raise FuchsiaDeviceError(FUCHSIA_COULD_NOT_GET_DESIRED_STATE %
(action, process_name))
except Exception as e:
self.log.info(unable_to_connect_msg)
raise e
finally:
if action == 'stop' and (process_name == 'sl4f'
or process_name == 'sl4f.cmx'):
self._persistent_ssh_conn.close()
self._persistent_ssh_conn = None
def check_connect_response(self, connect_response):
if connect_response.get("error") is None:
# Checks the response from SL4F and if there is no error, check
# the result.
connection_result = connect_response.get("result")
if not connection_result:
# Ideally the error would be present but just outputting a log
# message until available.
self.log.debug("Connect call failed, aborting!")
return False
else:
# Returns True if connection was successful.
return True
else:
# the response indicates an error - log and raise failure
self.log.debug("Aborting! - Connect call failed with error: %s" %
connect_response.get("error"))
return False
def check_disconnect_response(self, disconnect_response):
if disconnect_response.get("error") is None:
# Returns True if disconnect was successful.
return True
else:
# the response indicates an error - log and raise failure
self.log.debug("Disconnect call failed with error: %s" %
disconnect_response.get("error"))
return False
# TODO(fxb/64657): Determine more stable solution to country code config on
# device bring up.
def configure_regulatory_domain(self, desired_country_code):
"""Allows the user to set the device country code via ACTS config
Usage:
In FuchsiaDevice config, add "country_code": "<CC>"
"""
if self.ssh_config:
# Country code can be None, from ACTS config.
if desired_country_code:
desired_country_code = desired_country_code.upper()
response = self.regulatory_region_lib.setRegion(
desired_country_code)
if response.get('error'):
raise FuchsiaDeviceError(
'Failed to set regulatory domain. Err: %s' %
response['error'])
end_time = time.time() + FUCHSIA_COUNTRY_CODE_TIMEOUT
while time.time() < end_time:
ascii_cc = self.wlan_lib.wlanGetCountry(0).get('result')
# Convert ascii_cc to string, then compare
if ascii_cc and (''.join(chr(c) for c in ascii_cc).upper()
== desired_country_code):
self.log.debug('Country code successfully set to %s.' %
desired_country_code)
return
self.log.debug('Country code not yet updated. Retrying.')
time.sleep(1)
raise FuchsiaDeviceError('Country code never updated to %s' %
desired_country_code)
@backoff.on_exception(backoff.constant,
(FuchsiaSyslogError, socket.timeout),
interval=1.5,
max_tries=4)
def start_services(self):
"""Starts long running services on the Fuchsia device.
Starts a syslog streaming process, SL4F server, initializes a connection
to the SL4F server, then starts an isolated ffx daemon.
"""
self.log.debug("Attempting to start Fuchsia device services on %s." %
self.ip)
if self.ssh_config:
self.log_process = create_syslog_process(self.serial,
self.log_path,
self.ip,
self.ssh_username,
self.ssh_config,
ssh_port=self.ssh_port)
try:
self.log_process.start()
except FuchsiaSyslogError as e:
# Before backing off and retrying, stop the syslog if it
# failed to setup correctly, to prevent threading error when
# retrying
self.log_process.stop()
raise
self.control_daemon("sl4f.cmx", "start")
self.init_sl4f_connection()
out_name = "fuchsia_device_%s_%s.txt" % (self.serial, 'fw_version')
full_out_path = os.path.join(self.log_path, out_name)
fw_file = open(full_out_path, 'w')
fw_file.write('%s\n' % self.version())
fw_file.close()
self.init_ffx_connection()
def stop_services(self):
"""Stops long running services on the fuchsia device.
Terminates the syslog streaming process, the SL4F server on the device,
and the ffx daemon.
"""
self.log.debug("Attempting to stop Fuchsia device services on %s." %
self.ip)
if hasattr(self, 'ffx'):
self.ffx.clean_up()
if self.ssh_config:
try:
self.control_daemon("sl4f.cmx", "stop")
except Exception as err:
self.log.exception("Failed to stop sl4f.cmx with: %s" % err)
if self.log_process:
self.log_process.stop()
def load_config(self, config):
pass
def take_bug_report(self,
test_name=None,
begin_time=None,
additional_log_objects=None):
"""Takes a bug report on the device and stores it in a file.
Args:
test_name: Name of the test case that triggered this bug report.
begin_time: Epoch time when the test started. If not specified, the
current time will be used.
additional_log_objects: A list of additional objects in Fuchsia to
query in the bug report. Must be in the following format:
/hub/c/scenic.cmx/[0-9]*/out/objects
"""
if not additional_log_objects:
additional_log_objects = []
log_items = []
matching_log_items = FUCHSIA_DEFAULT_LOG_ITEMS
for additional_log_object in additional_log_objects:
if additional_log_object not in matching_log_items:
matching_log_items.append(additional_log_object)
sn_path = context.get_current_context().get_full_output_path()
os.makedirs(sn_path, exist_ok=True)
epoch = begin_time if begin_time else utils.get_current_epoch_time()
time_stamp = acts_logger.normalize_log_line_timestamp(
acts_logger.epoch_to_log_line_timestamp(epoch))
out_name = f"{self.mdns_name}_{time_stamp}"
snapshot_out_name = f"{out_name}.zip"
out_name = "%s.txt" % out_name
full_out_path = os.path.join(sn_path, out_name)
full_sn_out_path = os.path.join(sn_path, snapshot_out_name)
if test_name:
self.log.info(
f"Taking snapshot of {self.mdns_name} for {test_name}")
else:
self.log.info(f"Taking snapshot of {self.mdns_name}")
if self.ssh_config is not None:
try:
subprocess.run([
f"ssh -F {self.ssh_config} {self.ip} snapshot > {full_sn_out_path}"
],
shell=True)
self.log.info("Snapshot saved at: {}".format(full_sn_out_path))
except Exception as err:
self.log.error("Failed to take snapshot with: {}".format(err))
system_objects = self.send_command_ssh('iquery --find /hub').stdout
system_objects = system_objects.split()
for matching_log_item in matching_log_items:
for system_object in system_objects:
if re.match(matching_log_item, system_object):
log_items.append(system_object)
log_command = '%s %s' % (FUCHSIA_DEFAULT_LOG_CMD, ' '.join(log_items))
bug_report_data = self.send_command_ssh(log_command).stdout
bug_report_file = open(full_out_path, 'w')
bug_report_file.write(bug_report_data)
bug_report_file.close()
def take_bt_snoop_log(self, custom_name=None):
"""Takes a the bt-snoop log from the device and stores it in a file
in a pcap format.
"""
bt_snoop_path = context.get_current_context().get_full_output_path()
time_stamp = acts_logger.normalize_log_line_timestamp(
acts_logger.epoch_to_log_line_timestamp(time.time()))
out_name = "FuchsiaDevice%s_%s" % (
self.serial, time_stamp.replace(" ", "_").replace(":", "-"))
out_name = "%s.pcap" % out_name
if custom_name:
out_name = "%s_%s.pcap" % (self.serial, custom_name)
else:
out_name = "%s.pcap" % out_name
full_out_path = os.path.join(bt_snoop_path, out_name)
bt_snoop_data = self.send_command_ssh(
'bt-snoop-cli -d -f pcap').raw_stdout
bt_snoop_file = open(full_out_path, 'wb')
bt_snoop_file.write(bt_snoop_data)
bt_snoop_file.close()
class FuchsiaDeviceLoggerAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
msg = "[FuchsiaDevice|%s] %s" % (self.extra["ip"], msg)
return msg, kwargs