blob: c83abfc6cd8f6d3f321bccadf27fdd39fe1af35d [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2019 - 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 os
import logging
import psutil
import socket
import tarfile
import tempfile
import time
import usbinfo
from acts import utils
from acts.controllers.fuchsia_lib.ssh import FuchsiaSSHError
from acts.libs.proc import job
from acts.utils import get_fuchsia_mdns_ipv6_address
MDNS_LOOKUP_RETRY_MAX = 3
FASTBOOT_TIMEOUT = 30
AFTER_FLASH_BOOT_TIME = 30
WAIT_FOR_EXISTING_FLASH_TO_FINISH_SEC = 360
PROCESS_CHECK_WAIT_TIME_SEC = 30
FUCHSIA_SDK_URL = "gs://fuchsia-sdk/development"
FUCHSIA_RELEASE_TESTING_URL = "gs://fuchsia-release-testing/images"
def flash(fuchsia_device, use_ssh=False,
fuchsia_reconnect_after_reboot_time=5):
"""A function to flash, not pave, a fuchsia_device
Args:
fuchsia_device: An ACTS fuchsia_device
Returns:
True if successful.
"""
if not fuchsia_device.authorized_file:
raise ValueError('A ssh authorized_file must be present in the '
'ACTS config to flash fuchsia_devices.')
# This is the product type from the fx set command.
# Do 'fx list-products' to see options in Fuchsia source tree.
if not fuchsia_device.product_type:
raise ValueError('A product type must be specified to flash '
'fuchsia_devices.')
# This is the board type from the fx set command.
# Do 'fx list-boards' to see options in Fuchsia source tree.
if not fuchsia_device.board_type:
raise ValueError('A board type must be specified to flash '
'fuchsia_devices.')
if not fuchsia_device.build_number:
fuchsia_device.build_number = 'LATEST'
if not fuchsia_device.mdns_name:
raise ValueError(
'Either fuchsia_device mdns_name must be specified or '
'ip must be the mDNS name to be able to flash.')
file_to_download = None
image_archive_path = None
image_path = None
if not fuchsia_device.specific_image:
product_build = fuchsia_device.product_type
if fuchsia_device.build_type:
product_build = f'{product_build}_{fuchsia_device.build_type}'
if 'LATEST' in fuchsia_device.build_number:
sdk_version = 'sdk'
if 'LATEST_F' in fuchsia_device.build_number:
f_branch = fuchsia_device.build_number.split('LATEST_F', 1)[1]
sdk_version = f'f{f_branch}_sdk'
file_to_download = (
f'{FUCHSIA_RELEASE_TESTING_URL}/'
f'{sdk_version}-{product_build}.{fuchsia_device.board_type}-release.tgz'
)
else:
# Must be a fully qualified build number (e.g. 5.20210721.4.1215)
file_to_download = (
f'{FUCHSIA_SDK_URL}/{fuchsia_device.build_number}/images/'
f'{product_build}.{fuchsia_device.board_type}-release.tgz')
elif 'gs://' in fuchsia_device.specific_image:
file_to_download = fuchsia_device.specific_image
elif os.path.isdir(fuchsia_device.specific_image):
image_path = fuchsia_device.specific_image
elif tarfile.is_tarfile(fuchsia_device.specific_image):
image_archive_path = fuchsia_device.specific_image
else:
raise ValueError(
f'Invalid specific_image "{fuchsia_device.specific_image}"')
if image_path:
reboot_to_bootloader(fuchsia_device, use_ssh,
fuchsia_reconnect_after_reboot_time)
logging.info(
f'Flashing {fuchsia_device.mdns_name} with {image_path} using authorized keys "{fuchsia_device.authorized_file}".'
)
run_flash_script(fuchsia_device, image_path)
else:
suffix = fuchsia_device.board_type
with tempfile.TemporaryDirectory(suffix=suffix) as image_path:
if file_to_download:
logging.info(f'Downloading {file_to_download} to {image_path}')
job.run(f'gsutil cp {file_to_download} {image_path}')
image_archive_path = os.path.join(
image_path, os.path.basename(file_to_download))
if image_archive_path:
# Use tar command instead of tarfile.extractall, as it takes too long.
job.run(f'tar xfvz {image_archive_path} -C {image_path}',
timeout=120)
reboot_to_bootloader(fuchsia_device, use_ssh,
fuchsia_reconnect_after_reboot_time)
logging.info(
f'Flashing {fuchsia_device.mdns_name} with {image_archive_path} using authorized keys "{fuchsia_device.authorized_file}".'
)
run_flash_script(fuchsia_device, image_path)
return True
def reboot_to_bootloader(fuchsia_device,
use_ssh=False,
fuchsia_reconnect_after_reboot_time=5):
if use_ssh:
logging.info('Sending reboot command via SSH to '
'get into bootloader.')
# Sending this command will put the device in fastboot
# but it does not guarantee the device will be in fastboot
# after this command. There is no check so if there is an
# expectation of the device being in fastboot, then some
# other check needs to be done.
try:
fuchsia_device.ssh.run(
'dm rb', timeout_sec=fuchsia_reconnect_after_reboot_time)
except FuchsiaSSHError as e:
if 'closed by remote host' not in e.result.stderr:
raise e
else:
pass
## Todo: Add elif for SL4F if implemented in SL4F
time_counter = 0
while time_counter < FASTBOOT_TIMEOUT:
logging.info('Checking to see if fuchsia_device(%s) SN: %s is in '
'fastboot. (Attempt #%s Timeout: %s)' %
(fuchsia_device.mdns_name, fuchsia_device.serial_number,
str(time_counter + 1), FASTBOOT_TIMEOUT))
for usb_device in usbinfo.usbinfo():
if (usb_device['iSerialNumber'] == fuchsia_device.serial_number
and usb_device['iProduct'] == 'USB_download_gadget'):
logging.info(
'fuchsia_device(%s) SN: %s is in fastboot.' %
(fuchsia_device.mdns_name, fuchsia_device.serial_number))
time_counter = FASTBOOT_TIMEOUT
time_counter = time_counter + 1
if time_counter == FASTBOOT_TIMEOUT:
for fail_usb_device in usbinfo.usbinfo():
logging.debug(fail_usb_device)
raise TimeoutError(
'fuchsia_device(%s) SN: %s '
'never went into fastboot' %
(fuchsia_device.mdns_name, fuchsia_device.serial_number))
time.sleep(1)
end_time = time.time() + WAIT_FOR_EXISTING_FLASH_TO_FINISH_SEC
# Attempt to wait for existing flashing process to finish
while time.time() < end_time:
flash_process_found = False
for proc in psutil.process_iter():
if "bash" in proc.name() and "flash.sh" in proc.cmdline():
logging.info(
"Waiting for existing flash.sh process to complete.")
time.sleep(PROCESS_CHECK_WAIT_TIME_SEC)
flash_process_found = True
if not flash_process_found:
break
def run_flash_script(fuchsia_device, flash_dir):
try:
flash_output = job.run(
f'bash {flash_dir}/flash.sh --ssh-key={fuchsia_device.authorized_file} -s {fuchsia_device.serial_number}',
timeout=120)
logging.debug(flash_output.stderr)
except job.TimeoutError as err:
raise TimeoutError(err)
logging.info('Waiting %s seconds for device'
' to come back up after flashing.' % AFTER_FLASH_BOOT_TIME)
time.sleep(AFTER_FLASH_BOOT_TIME)
logging.info('Updating device to new IP addresses.')
mdns_ip = None
for retry_counter in range(MDNS_LOOKUP_RETRY_MAX):
mdns_ip = get_fuchsia_mdns_ipv6_address(fuchsia_device.mdns_name)
if mdns_ip:
break
else:
time.sleep(1)
if mdns_ip and utils.is_valid_ipv6_address(mdns_ip):
logging.info('IP for fuchsia_device(%s) changed from %s to %s' %
(fuchsia_device.mdns_name, fuchsia_device.ip, mdns_ip))
fuchsia_device.ip = mdns_ip
fuchsia_device.address = "http://[{}]:{}".format(
fuchsia_device.ip, fuchsia_device.sl4f_port)
else:
raise ValueError('Invalid IP: %s after flashing.' %
fuchsia_device.mdns_name)
def wait_for_port(host: str, port: int, timeout_sec: int = 5) -> None:
"""Wait for the host to start accepting connections on the port.
Some services take some time to start. Call this after launching the service
to avoid race conditions.
Args:
host: IP of the running service.
port: Port of the running service.
timeout_sec: Seconds to wait until raising TimeoutError
Raises:
TimeoutError: when timeout_sec has expired without a successful
connection to the service
"""
timeout = time.perf_counter() + timeout_sec
while True:
try:
with socket.create_connection((host, port), timeout=timeout_sec):
return
except ConnectionRefusedError as e:
if time.perf_counter() > timeout:
raise TimeoutError(
f'Waited over {timeout_sec}s for the service to start '
f'accepting connections at {host}:{port}') from e