blob: 306d9d296b4fc92dcc3a165a5c3a6814ab9ba43e [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 backoff
import itertools
import os
import logging
import paramiko
import shutil
import socket
import tarfile
import time
import usbinfo
from acts import utils
from acts.controllers.fuchsia_lib.base_lib import DeviceOffline
from acts.libs.proc import job
from acts.utils import get_fuchsia_mdns_ipv6_address
logging.getLogger("paramiko").setLevel(logging.WARNING)
# paramiko-ng will throw INFO messages when things get disconnect or cannot
# connect perfectly the first time. In this library those are all handled by
# either retrying and/or throwing an exception for the appropriate case.
# Therefore, in order to reduce confusion in the logs the log level is set to
# WARNING.
MDNS_LOOKUP_RETRY_MAX = 3
FASTBOOT_TIMEOUT = 30
AFTER_FLASH_BOOT_TIME = 30
def get_private_key(ip_address, ssh_config):
"""Tries to load various ssh key types.
Args:
ip_address: IP address of ssh server.
ssh_config: ssh_config location for the ssh server.
Returns:
The ssh private key
"""
exceptions = []
try:
logging.debug('Trying to load SSH key type: ed25519')
return paramiko.ed25519key.Ed25519Key(
filename=get_ssh_key_for_host(ip_address, ssh_config))
except paramiko.SSHException as e:
exceptions.append(e)
logging.debug('Failed loading SSH key type: ed25519')
try:
logging.debug('Trying to load SSH key type: rsa')
return paramiko.RSAKey.from_private_key_file(
filename=get_ssh_key_for_host(ip_address, ssh_config))
except paramiko.SSHException as e:
exceptions.append(e)
logging.debug('Failed loading SSH key type: rsa')
raise Exception('No valid ssh key type found', exceptions)
@backoff.on_exception(
backoff.constant,
(paramiko.ssh_exception.SSHException,
paramiko.ssh_exception.AuthenticationException, socket.timeout,
socket.error, ConnectionRefusedError, ConnectionResetError),
interval=1.5,
max_tries=4)
def create_ssh_connection(ip_address,
ssh_username,
ssh_config,
ssh_port=22,
connect_timeout=10,
auth_timeout=10,
banner_timeout=10):
"""Creates and ssh connection to a Fuchsia device
Args:
ip_address: IP address of ssh server.
ssh_username: Username for ssh server.
ssh_config: ssh_config location for the ssh server.
ssh_port: port for the ssh server.
connect_timeout: Timeout value for connecting to ssh_server.
auth_timeout: Timeout value to wait for authentication.
banner_timeout: Timeout to wait for ssh banner.
Returns:
A paramiko ssh object
"""
if not utils.can_ping(job, ip_address):
raise DeviceOffline("Device %s is not reachable via "
"the network." % ip_address)
ssh_key = get_private_key(ip_address=ip_address, ssh_config=ssh_config)
ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_client.connect(hostname=ip_address,
username=ssh_username,
allow_agent=False,
pkey=ssh_key,
port=ssh_port,
timeout=connect_timeout,
auth_timeout=auth_timeout,
banner_timeout=banner_timeout)
ssh_client.get_transport().set_keepalive(1)
return ssh_client
def ssh_is_connected(ssh_client):
"""Checks to see if the SSH connection is alive.
Args:
ssh_client: A paramiko SSH client instance.
Returns:
True if connected, False or None if not connected.
"""
return ssh_client and ssh_client.get_transport().is_active()
def get_ssh_key_for_host(host, ssh_config_file):
"""Gets the SSH private key path from a supplied ssh_config_file and the
host.
Args:
host (str): The ip address or host name that SSH will connect to.
ssh_config_file (str): Path to the ssh_config_file that will be used
to connect to the host.
Returns:
path: A path to the private key for the SSH connection.
"""
ssh_config = paramiko.SSHConfig()
user_config_file = os.path.expanduser(ssh_config_file)
if os.path.exists(user_config_file):
with open(user_config_file) as f:
ssh_config.parse(f)
user_config = ssh_config.lookup(host)
if 'identityfile' not in user_config:
raise ValueError('Could not find identity file in %s.' % ssh_config)
path = os.path.expanduser(user_config['identityfile'][0])
if not os.path.exists(path):
raise FileNotFoundError('Specified IdentityFile %s for %s in %s not '
'existing anymore.' % (path, host, ssh_config))
return path
class SshResults:
"""Class representing the results from a SSH command to mimic the output
of the job.Result class in ACTS. This is to reduce the changes needed from
swapping the ssh connection in ACTS to paramiko.
Attributes:
stdin: The file descriptor to the input channel of the SSH connection.
stdout: The file descriptor to the stdout of the SSH connection.
stderr: The file descriptor to the stderr of the SSH connection.
exit_status: The file descriptor of the SSH command.
"""
def __init__(self, stdin, stdout, stderr, exit_status):
self._raw_stdout = stdout.read()
self._stdout = self._raw_stdout.decode('utf-8', errors='replace')
self._stderr = stderr.read().decode('utf-8', errors='replace')
self._exit_status = exit_status.recv_exit_status()
@property
def stdout(self):
return self._stdout
@property
def raw_stdout(self):
return self._raw_stdout
@property
def stderr(self):
return self._stderr
@property
def exit_status(self):
return self._exit_status
def flash(fuchsia_device):
"""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.server_path:
fuchsia_device.server_path = 'gs://fuchsia-sdk/development/'
if (utils.is_valid_ipv4_address(fuchsia_device.orig_ip)
or utils.is_valid_ipv6_address(fuchsia_device.orig_ip)):
raise ValueError('The fuchsia_device ip must be the mDNS name to be '
'able to flash.')
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.orig_ip,
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.orig_ip,
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.orig_ip,
fuchsia_device.serial_number))
time.sleep(1)
if not fuchsia_device.specific_image:
file_download_needed = True
if 'LATEST' in fuchsia_device.build_number:
gsutil_process = job.run('gsutil ls %s'
% fuchsia_device.server_path).stdout
build_list = list(
# filter out builds that are not part of branches
filter(None, gsutil_process.replace(
fuchsia_device.server_path, '').replace(
'/', '').split('\n')))
if 'LATEST_F' in fuchsia_device.build_number:
build_number = fuchsia_device.build_number.split(
'LATEST_F', 1)[1]
build_list = [x for x in build_list if x.startswith(
'%s.' % build_number)]
elif fuchsia_device.build_number == 'LATEST':
build_list = [x for x in build_list if '.' in x]
if build_list:
fuchsia_device.build_number = build_list[-2]
else:
raise FileNotFoundError('No build(%s) on the found on %s.' % (
fuchsia_device.build_number, fuchsia_device.server_path))
image_front_string = fuchsia_device.product_type
if fuchsia_device.build_type:
image_front_string = '%s_%s' % (image_front_string,
fuchsia_device.build_type)
full_image_string = '%s.%s-release.tgz' % (image_front_string,
fuchsia_device.board_type)
file_to_download = '%s%s/images/%s' % (fuchsia_device.server_path,
fuchsia_device.build_number,
full_image_string)
elif 'gs://' in fuchsia_device.specific_image:
file_download_needed = True
file_to_download = fuchsia_device.specific_image
elif tarfile.is_tarfile(fuchsia_device.specific_image):
file_download_needed = False
file_to_download = fuchsia_device.specific_image
else:
raise ValueError('A suitable build could not be found.')
tmp_path = '/tmp/%s_%s' % (str(int(time.time()*10000)),
fuchsia_device.board_type)
os.mkdir(tmp_path)
if file_download_needed:
job.run('gsutil cp %s %s' % (file_to_download, tmp_path))
logging.info('Downloading %s to %s' % (file_to_download, tmp_path))
image_tgz = os.path.basename(file_to_download)
else:
job.run('cp %s %s' % (fuchsia_device.specific_image, tmp_path))
logging.info('Copying %s to %s' % (file_to_download, tmp_path))
image_tgz = os.path.basename(fuchsia_device.specific_image)
job.run('tar xfvz %s/%s -C %s' % (tmp_path, image_tgz, tmp_path))
os.chdir(tmp_path)
all_files = []
for root, _dirs, files in itertools.islice(os.walk(tmp_path), 1, None):
for filename in files:
all_files.append(os.path.join(root, filename))
for filename in all_files:
shutil.move(filename, tmp_path)
logging.info('Flashing fuchsia_device(%s) with %s/%s.' % (
fuchsia_device.orig_ip, tmp_path, image_tgz))
try:
flash_output = job.run('bash flash.sh --ssh-key=%s -s %s' % (
fuchsia_device.authorized_file, fuchsia_device.serial_number),
timeout=120)
logging.debug(flash_output.stderr)
except job.TimeoutError as err:
raise TimeoutError(err)
try:
os.rmdir(tmp_path)
except Exception:
job.run('rm -fr %s' % tmp_path)
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.orig_ip)
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.orig_ip,
fuchsia_device.ip,
mdns_ip))
fuchsia_device.ip = mdns_ip
fuchsia_device.address = "http://[{}]:{}".format(
fuchsia_device.ip, fuchsia_device.sl4f_port)
fuchsia_device.init_address = fuchsia_device.address + "/init"
fuchsia_device.cleanup_address = fuchsia_device.address + "/cleanup"
fuchsia_device.print_address = fuchsia_device.address + "/print_clients"
fuchsia_device.init_libraries()
else:
raise ValueError('Invalid IP: %s after flashing.' %
fuchsia_device.orig_ip)
return True