blob: c32224b40f946d6d6268766436bd92815cb7e971 [file] [log] [blame]
# Lint as: python2, python3
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# Expects to be run in an environment with sudo and no interactive password
# prompt, such as within the Chromium OS development chroot.
"""This is the base host class for attached devices"""
import logging
import common
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.server.hosts import ssh_host
class AttachedDeviceHost(ssh_host.SSHHost):
"""Host class for all attached devices(e.g. Android)"""
# Since we currently use labstation as phone host, the repair logic
# of labstation checks /var/lib/servod/ path to make reboot decision.
#TODO(b:226151633): use a separated path after adjust repair logic.
TEMP_FILE_DIR = '/var/lib/servod/'
LOCK_FILE_POSTFIX = "_in_use"
REBOOT_TIMEOUT_SECONDS = 240
USB_POLL_INTERVAL_SECONDS = 2
def _initialize(self,
hostname,
serial_number,
phone_station_ssh_port=None,
*args,
**dargs):
"""Construct a AttachedDeviceHost object.
Args:
hostname: Hostname of the attached device host.
serial_number: Usb serial number of the associated
device(e.g. Android).
phone_station_ssh_port: port for ssh to phone station, it
use default 22 if the value is None.
"""
self.serial_number = serial_number
if phone_station_ssh_port:
dargs['port'] = int(phone_station_ssh_port)
super(AttachedDeviceHost, self)._initialize(hostname=hostname,
*args,
**dargs)
# When run local test against a remote DUT in lab, user may use
# port forwarding to bypass corp ssh relay. So the hostname may
# be localhost while the command intended to run on a remote DUT,
# we can differentiate this by checking if a non-default port
# is specified.
self._is_localhost = (self.hostname in {'localhost', "127.0.0.1"}
and phone_station_ssh_port is None)
# Commands on the the host must be run by the superuser.
# Our account on a remote host is root, but if our target is
# localhost then we might be running unprivileged. If so,
# `sudo` will have to be added to the commands.
self._sudo_required = False
if self._is_localhost:
self._sudo_required = utils.system_output('id -u') != '0'
# We need to lock the attached device host to prevent other task
# perform any interruptive actions(e.g. reboot) since they can
# be shared by multiple devices
self._is_locked = False
self._lock_file = (self.TEMP_FILE_DIR + self.serial_number +
self.LOCK_FILE_POSTFIX)
if not self.wait_up(self.REBOOT_TIMEOUT_SECONDS):
raise error.AutoservError(
'Attached device host %s is not reachable via ssh.' %
self.hostname)
if not self._is_localhost:
self._lock()
self.wait_ready()
def _lock(self):
logging.debug('Locking host %s by touching %s file', self.hostname,
self._lock_file)
self.run('mkdir -p %s' % self.TEMP_FILE_DIR)
self.run('touch %s' % self._lock_file)
self._is_locked = True
def _unlock(self):
logging.debug('Unlocking host by removing %s file', self._lock_file)
self.run('rm %s' % self._lock_file, ignore_status=True)
self._is_locked = False
def make_ssh_command(self,
user='root',
port=22,
opts='',
hosts_file=None,
connect_timeout=None,
alive_interval=None,
alive_count_max=None,
connection_attempts=None):
"""Override default make_ssh_command to use tuned options.
Tuning changes:
- ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
connection failure. Consistency with remote_access.py.
- ServerAliveInterval=180; which causes SSH to ping connection every
180 seconds. In conjunction with ServerAliveCountMax ensures
that if the connection dies, Autotest will bail out quickly.
- ServerAliveCountMax=3; consistency with remote_access.py.
- ConnectAttempts=4; reduce flakiness in connection errors;
consistency with remote_access.py.
- UserKnownHostsFile=/dev/null; we don't care about the keys.
- SSH protocol forced to 2; needed for ServerAliveInterval.
Args:
user: User name to use for the ssh connection.
port: Port on the target host to use for ssh connection.
opts: Additional options to the ssh command.
hosts_file: Ignored.
connect_timeout: Ignored.
alive_interval: Ignored.
alive_count_max: Ignored.
connection_attempts: Ignored.
Returns:
An ssh command with the requested settings.
"""
options = ' '.join([opts, '-o Protocol=2'])
return super(AttachedDeviceHost,
self).make_ssh_command(user=user,
port=port,
opts=options,
hosts_file='/dev/null',
connect_timeout=30,
alive_interval=180,
alive_count_max=3,
connection_attempts=4)
def _make_scp_cmd(self, sources, dest):
"""Format scp command.
Given a list of source paths and a destination path, produces the
appropriate scp command for encoding it. Remote paths must be
pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost
to allow additional ssh options.
Args:
sources: A list of source paths to copy from.
dest: Destination path to copy to.
Returns:
An scp command that copies |sources| on local machine to
|dest| on the remote host.
"""
command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no '
'-o UserKnownHostsFile=/dev/null %s %s "%s"')
port = self.port
if port is None:
logging.info('AttachedDeviceHost: defaulting to port 22.'
' See b/204502754.')
port = 22
args = (
self._main_ssh.ssh_option,
("-P %s" % port),
sources,
dest,
)
return command % args
def run(self,
command,
timeout=3600,
ignore_status=False,
stdout_tee=utils.TEE_TO_LOGS,
stderr_tee=utils.TEE_TO_LOGS,
connect_timeout=30,
ssh_failure_retry_ok=False,
options='',
stdin=None,
verbose=True,
args=()):
"""Run a command on the attached device host.
Extends method `run` in SSHHost. If the host is a remote device,
it will call `run` in SSHost without changing anything.
If the host is 'localhost', it will call utils.system_output.
Args:
command: The command line string.
timeout: Time limit in seconds before attempting to
kill the running process. The run() function
will take a few seconds longer than 'timeout'
to complete if it has to kill the process.
ignore_status: Do not raise an exception, no matter
what the exit code of the command is.
stdout_tee: Where to tee the stdout.
stderr_tee: Where to tee the stderr.
connect_timeout: SSH connection timeout (in seconds)
Ignored if host is 'localhost'.
options: String with additional ssh command options
Ignored if host is 'localhost'.
ssh_failure_retry_ok: when True and ssh connection failure is
suspected, OK to retry command (but not
compulsory, and likely not needed here)
stdin: Stdin to pass (a string) to the executed command.
verbose: Log the commands.
args: Sequence of strings to pass as arguments to command by
quoting them in " and escaping their contents if
necessary.
Returns:
A utils.CmdResult object.
Raises:
AutoservRunError: If the command failed.
AutoservSSHTimeout: SSH connection has timed out. Only applies
when the host is not 'localhost'.
"""
run_args = {
'command': command,
'timeout': timeout,
'ignore_status': ignore_status,
'stdout_tee': stdout_tee,
'stderr_tee': stderr_tee,
# connect_timeout n/a for localhost
# options n/a for localhost
# ssh_failure_retry_ok n/a for localhost
'stdin': stdin,
'verbose': verbose,
'args': args,
}
if self._is_localhost:
if self._sudo_required:
run_args['command'] = 'sudo -n sh -c "%s"' % utils.sh_escape(
command)
try:
return utils.run(**run_args)
except error.CmdError as e:
logging.error(e)
raise error.AutoservRunError('command execution error',
e.result_obj)
else:
run_args['connect_timeout'] = connect_timeout
run_args['options'] = options
run_args['ssh_failure_retry_ok'] = ssh_failure_retry_ok
return super(AttachedDeviceHost, self).run(**run_args)
def wait_ready(self, required_uptime=300):
"""Wait ready for the host if it has been rebooted recently.
It may take a few minutes until the system and usb components
re-enumerated and become ready after attached device reboot,
so we need to make sure the host has been up for a given amount
of time before trying to start any actions.
Args:
required_uptime: Minimum uptime in seconds that we can
consider an attached device host be ready.
Raises:
AutoservRunError: If the host has been rebooted recently
and it is not ready after timeout.
"""
uptime = float(self.check_uptime())
# Limit the maximum wait time.
wait_time = min(required_uptime - uptime, required_uptime)
if wait_time <= 0:
return
logging.info('The attached device host was recently rebooted.'
' Checking USB components.')
utils.poll_for_condition(
lambda: self._is_ready(wait_time),
timeout=wait_time,
sleep_interval=self.USB_POLL_INTERVAL_SECONDS,
exception=error.AutoservRunError(
'Attached device host %s is not ready.' %
self.hostname, None))
def _is_ready(self, timeout):
"""Return True if USB component with a serial number of the attached
device is available.
Args:
timeout: Time limit in seconds before killing the running process.
"""
result = self.run('lsusb -v | grep %s' % self.serial_number,
timeout=timeout,
ignore_status=True,
stdout_tee=None)
return result.exit_status == 0 and result.stdout.strip()
def close(self):
try:
if self._is_locked:
self._unlock()
except error.AutoservSSHTimeout:
logging.error('Unlock attached device host failed due to ssh'
' timeout. It may caused by the host went down'
' during the task.')
finally:
super(AttachedDeviceHost, self).close()