1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2022 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li# 6*9c5db199SXin Li# Expects to be run in an environment with sudo and no interactive password 7*9c5db199SXin Li# prompt, such as within the Chromium OS development chroot. 8*9c5db199SXin Li"""This is the base host class for attached devices""" 9*9c5db199SXin Li 10*9c5db199SXin Liimport logging 11*9c5db199SXin Liimport time 12*9c5db199SXin Li 13*9c5db199SXin Liimport common 14*9c5db199SXin Li 15*9c5db199SXin Lifrom autotest_lib.client.bin import utils 16*9c5db199SXin Lifrom autotest_lib.client.common_lib import error 17*9c5db199SXin Lifrom autotest_lib.server.hosts import ssh_host 18*9c5db199SXin Li 19*9c5db199SXin Li 20*9c5db199SXin Liclass AttachedDeviceHost(ssh_host.SSHHost): 21*9c5db199SXin Li """Host class for all attached devices(e.g. Android)""" 22*9c5db199SXin Li 23*9c5db199SXin Li # Since we currently use labstation as phone host, the repair logic 24*9c5db199SXin Li # of labstation checks /var/lib/servod/ path to make reboot decision. 25*9c5db199SXin Li #TODO(b:226151633): use a separated path after adjust repair logic. 26*9c5db199SXin Li TEMP_FILE_DIR = '/var/lib/servod/' 27*9c5db199SXin Li LOCK_FILE_POSTFIX = "_in_use" 28*9c5db199SXin Li REBOOT_TIMEOUT_SECONDS = 240 29*9c5db199SXin Li 30*9c5db199SXin Li def _initialize(self, 31*9c5db199SXin Li hostname, 32*9c5db199SXin Li serial_number, 33*9c5db199SXin Li phone_station_ssh_port=None, 34*9c5db199SXin Li *args, 35*9c5db199SXin Li **dargs): 36*9c5db199SXin Li """Construct a AttachedDeviceHost object. 37*9c5db199SXin Li 38*9c5db199SXin Li Args: 39*9c5db199SXin Li hostname: Hostname of the attached device host. 40*9c5db199SXin Li serial_number: Usb serial number of the associated 41*9c5db199SXin Li device(e.g. Android). 42*9c5db199SXin Li phone_station_ssh_port: port for ssh to phone station, it 43*9c5db199SXin Li use default 22 if the value is None. 44*9c5db199SXin Li """ 45*9c5db199SXin Li self.serial_number = serial_number 46*9c5db199SXin Li if phone_station_ssh_port: 47*9c5db199SXin Li dargs['port'] = int(phone_station_ssh_port) 48*9c5db199SXin Li super(AttachedDeviceHost, self)._initialize(hostname=hostname, 49*9c5db199SXin Li *args, 50*9c5db199SXin Li **dargs) 51*9c5db199SXin Li 52*9c5db199SXin Li # When run local test against a remote DUT in lab, user may use 53*9c5db199SXin Li # port forwarding to bypass corp ssh relay. So the hostname may 54*9c5db199SXin Li # be localhost while the command intended to run on a remote DUT, 55*9c5db199SXin Li # we can differentiate this by checking if a non-default port 56*9c5db199SXin Li # is specified. 57*9c5db199SXin Li self._is_localhost = (self.hostname in {'localhost', "127.0.0.1"} 58*9c5db199SXin Li and phone_station_ssh_port is None) 59*9c5db199SXin Li # Commands on the the host must be run by the superuser. 60*9c5db199SXin Li # Our account on a remote host is root, but if our target is 61*9c5db199SXin Li # localhost then we might be running unprivileged. If so, 62*9c5db199SXin Li # `sudo` will have to be added to the commands. 63*9c5db199SXin Li self._sudo_required = False 64*9c5db199SXin Li if self._is_localhost: 65*9c5db199SXin Li self._sudo_required = utils.system_output('id -u') != '0' 66*9c5db199SXin Li 67*9c5db199SXin Li # We need to lock the attached device host to prevent other task 68*9c5db199SXin Li # perform any interruptive actions(e.g. reboot) since they can 69*9c5db199SXin Li # be shared by multiple devices 70*9c5db199SXin Li self._is_locked = False 71*9c5db199SXin Li self._lock_file = (self.TEMP_FILE_DIR + self.serial_number + 72*9c5db199SXin Li self.LOCK_FILE_POSTFIX) 73*9c5db199SXin Li if not self.wait_up(self.REBOOT_TIMEOUT_SECONDS): 74*9c5db199SXin Li raise error.AutoservError( 75*9c5db199SXin Li 'Attached device host %s is not reachable via ssh.' % 76*9c5db199SXin Li self.hostname) 77*9c5db199SXin Li if not self._is_localhost: 78*9c5db199SXin Li self._lock() 79*9c5db199SXin Li self.wait_ready() 80*9c5db199SXin Li 81*9c5db199SXin Li def _lock(self): 82*9c5db199SXin Li logging.debug('Locking host %s by touching %s file', self.hostname, 83*9c5db199SXin Li self._lock_file) 84*9c5db199SXin Li self.run('mkdir -p %s' % self.TEMP_FILE_DIR) 85*9c5db199SXin Li self.run('touch %s' % self._lock_file) 86*9c5db199SXin Li self._is_locked = True 87*9c5db199SXin Li 88*9c5db199SXin Li def _unlock(self): 89*9c5db199SXin Li logging.debug('Unlocking host by removing %s file', self._lock_file) 90*9c5db199SXin Li self.run('rm %s' % self._lock_file, ignore_status=True) 91*9c5db199SXin Li self._is_locked = False 92*9c5db199SXin Li 93*9c5db199SXin Li def make_ssh_command(self, 94*9c5db199SXin Li user='root', 95*9c5db199SXin Li port=22, 96*9c5db199SXin Li opts='', 97*9c5db199SXin Li hosts_file=None, 98*9c5db199SXin Li connect_timeout=None, 99*9c5db199SXin Li alive_interval=None, 100*9c5db199SXin Li alive_count_max=None, 101*9c5db199SXin Li connection_attempts=None): 102*9c5db199SXin Li """Override default make_ssh_command to use tuned options. 103*9c5db199SXin Li 104*9c5db199SXin Li Tuning changes: 105*9c5db199SXin Li - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH 106*9c5db199SXin Li connection failure. Consistency with remote_access.py. 107*9c5db199SXin Li 108*9c5db199SXin Li - ServerAliveInterval=180; which causes SSH to ping connection every 109*9c5db199SXin Li 180 seconds. In conjunction with ServerAliveCountMax ensures 110*9c5db199SXin Li that if the connection dies, Autotest will bail out quickly. 111*9c5db199SXin Li 112*9c5db199SXin Li - ServerAliveCountMax=3; consistency with remote_access.py. 113*9c5db199SXin Li 114*9c5db199SXin Li - ConnectAttempts=4; reduce flakiness in connection errors; 115*9c5db199SXin Li consistency with remote_access.py. 116*9c5db199SXin Li 117*9c5db199SXin Li - UserKnownHostsFile=/dev/null; we don't care about the keys. 118*9c5db199SXin Li 119*9c5db199SXin Li - SSH protocol forced to 2; needed for ServerAliveInterval. 120*9c5db199SXin Li 121*9c5db199SXin Li Args: 122*9c5db199SXin Li user: User name to use for the ssh connection. 123*9c5db199SXin Li port: Port on the target host to use for ssh connection. 124*9c5db199SXin Li opts: Additional options to the ssh command. 125*9c5db199SXin Li hosts_file: Ignored. 126*9c5db199SXin Li connect_timeout: Ignored. 127*9c5db199SXin Li alive_interval: Ignored. 128*9c5db199SXin Li alive_count_max: Ignored. 129*9c5db199SXin Li connection_attempts: Ignored. 130*9c5db199SXin Li 131*9c5db199SXin Li Returns: 132*9c5db199SXin Li An ssh command with the requested settings. 133*9c5db199SXin Li """ 134*9c5db199SXin Li options = ' '.join([opts, '-o Protocol=2']) 135*9c5db199SXin Li return super(AttachedDeviceHost, 136*9c5db199SXin Li self).make_ssh_command(user=user, 137*9c5db199SXin Li port=port, 138*9c5db199SXin Li opts=options, 139*9c5db199SXin Li hosts_file='/dev/null', 140*9c5db199SXin Li connect_timeout=30, 141*9c5db199SXin Li alive_interval=180, 142*9c5db199SXin Li alive_count_max=3, 143*9c5db199SXin Li connection_attempts=4) 144*9c5db199SXin Li 145*9c5db199SXin Li def _make_scp_cmd(self, sources, dest): 146*9c5db199SXin Li """Format scp command. 147*9c5db199SXin Li 148*9c5db199SXin Li Given a list of source paths and a destination path, produces the 149*9c5db199SXin Li appropriate scp command for encoding it. Remote paths must be 150*9c5db199SXin Li pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost 151*9c5db199SXin Li to allow additional ssh options. 152*9c5db199SXin Li 153*9c5db199SXin Li Args: 154*9c5db199SXin Li sources: A list of source paths to copy from. 155*9c5db199SXin Li dest: Destination path to copy to. 156*9c5db199SXin Li 157*9c5db199SXin Li Returns: 158*9c5db199SXin Li An scp command that copies |sources| on local machine to 159*9c5db199SXin Li |dest| on the remote host. 160*9c5db199SXin Li """ 161*9c5db199SXin Li command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no ' 162*9c5db199SXin Li '-o UserKnownHostsFile=/dev/null %s %s "%s"') 163*9c5db199SXin Li port = self.port 164*9c5db199SXin Li if port is None: 165*9c5db199SXin Li logging.info('AttachedDeviceHost: defaulting to port 22.' 166*9c5db199SXin Li ' See b/204502754.') 167*9c5db199SXin Li port = 22 168*9c5db199SXin Li args = ( 169*9c5db199SXin Li self._main_ssh.ssh_option, 170*9c5db199SXin Li ("-P %s" % port), 171*9c5db199SXin Li sources, 172*9c5db199SXin Li dest, 173*9c5db199SXin Li ) 174*9c5db199SXin Li return command % args 175*9c5db199SXin Li 176*9c5db199SXin Li def run(self, 177*9c5db199SXin Li command, 178*9c5db199SXin Li timeout=3600, 179*9c5db199SXin Li ignore_status=False, 180*9c5db199SXin Li stdout_tee=utils.TEE_TO_LOGS, 181*9c5db199SXin Li stderr_tee=utils.TEE_TO_LOGS, 182*9c5db199SXin Li connect_timeout=30, 183*9c5db199SXin Li ssh_failure_retry_ok=False, 184*9c5db199SXin Li options='', 185*9c5db199SXin Li stdin=None, 186*9c5db199SXin Li verbose=True, 187*9c5db199SXin Li args=()): 188*9c5db199SXin Li """Run a command on the attached device host. 189*9c5db199SXin Li 190*9c5db199SXin Li Extends method `run` in SSHHost. If the host is a remote device, 191*9c5db199SXin Li it will call `run` in SSHost without changing anything. 192*9c5db199SXin Li If the host is 'localhost', it will call utils.system_output. 193*9c5db199SXin Li 194*9c5db199SXin Li Args: 195*9c5db199SXin Li command: The command line string. 196*9c5db199SXin Li timeout: Time limit in seconds before attempting to 197*9c5db199SXin Li kill the running process. The run() function 198*9c5db199SXin Li will take a few seconds longer than 'timeout' 199*9c5db199SXin Li to complete if it has to kill the process. 200*9c5db199SXin Li ignore_status: Do not raise an exception, no matter 201*9c5db199SXin Li what the exit code of the command is. 202*9c5db199SXin Li stdout_tee: Where to tee the stdout. 203*9c5db199SXin Li stderr_tee: Where to tee the stderr. 204*9c5db199SXin Li connect_timeout: SSH connection timeout (in seconds) 205*9c5db199SXin Li Ignored if host is 'localhost'. 206*9c5db199SXin Li options: String with additional ssh command options 207*9c5db199SXin Li Ignored if host is 'localhost'. 208*9c5db199SXin Li ssh_failure_retry_ok: when True and ssh connection failure is 209*9c5db199SXin Li suspected, OK to retry command (but not 210*9c5db199SXin Li compulsory, and likely not needed here) 211*9c5db199SXin Li stdin: Stdin to pass (a string) to the executed command. 212*9c5db199SXin Li verbose: Log the commands. 213*9c5db199SXin Li args: Sequence of strings to pass as arguments to command by 214*9c5db199SXin Li quoting them in " and escaping their contents if 215*9c5db199SXin Li necessary. 216*9c5db199SXin Li 217*9c5db199SXin Li Returns: 218*9c5db199SXin Li A utils.CmdResult object. 219*9c5db199SXin Li 220*9c5db199SXin Li Raises: 221*9c5db199SXin Li AutoservRunError: If the command failed. 222*9c5db199SXin Li AutoservSSHTimeout: SSH connection has timed out. Only applies 223*9c5db199SXin Li when the host is not 'localhost'. 224*9c5db199SXin Li """ 225*9c5db199SXin Li run_args = { 226*9c5db199SXin Li 'command': command, 227*9c5db199SXin Li 'timeout': timeout, 228*9c5db199SXin Li 'ignore_status': ignore_status, 229*9c5db199SXin Li 'stdout_tee': stdout_tee, 230*9c5db199SXin Li 'stderr_tee': stderr_tee, 231*9c5db199SXin Li # connect_timeout n/a for localhost 232*9c5db199SXin Li # options n/a for localhost 233*9c5db199SXin Li # ssh_failure_retry_ok n/a for localhost 234*9c5db199SXin Li 'stdin': stdin, 235*9c5db199SXin Li 'verbose': verbose, 236*9c5db199SXin Li 'args': args, 237*9c5db199SXin Li } 238*9c5db199SXin Li if self._is_localhost: 239*9c5db199SXin Li if self._sudo_required: 240*9c5db199SXin Li run_args['command'] = 'sudo -n sh -c "%s"' % utils.sh_escape( 241*9c5db199SXin Li command) 242*9c5db199SXin Li try: 243*9c5db199SXin Li return utils.run(**run_args) 244*9c5db199SXin Li except error.CmdError as e: 245*9c5db199SXin Li logging.error(e) 246*9c5db199SXin Li raise error.AutoservRunError('command execution error', 247*9c5db199SXin Li e.result_obj) 248*9c5db199SXin Li else: 249*9c5db199SXin Li run_args['connect_timeout'] = connect_timeout 250*9c5db199SXin Li run_args['options'] = options 251*9c5db199SXin Li run_args['ssh_failure_retry_ok'] = ssh_failure_retry_ok 252*9c5db199SXin Li return super(AttachedDeviceHost, self).run(**run_args) 253*9c5db199SXin Li 254*9c5db199SXin Li def wait_ready(self, required_uptime=300): 255*9c5db199SXin Li """Wait ready for the host if it has been rebooted recently. 256*9c5db199SXin Li 257*9c5db199SXin Li It may take a few minutes until the system and usb components 258*9c5db199SXin Li re-enumerated and become ready after a attached device reboot, 259*9c5db199SXin Li so we need to make sure the host has been up for a given a mount 260*9c5db199SXin Li of time before trying to start any actions. 261*9c5db199SXin Li 262*9c5db199SXin Li Args: 263*9c5db199SXin Li required_uptime: Minimum uptime in seconds that we can 264*9c5db199SXin Li consider an attached device host be ready. 265*9c5db199SXin Li """ 266*9c5db199SXin Li uptime = float(self.check_uptime()) 267*9c5db199SXin Li # To prevent unexpected output from check_uptime() that causes long 268*9c5db199SXin Li # sleep, make sure the maximum wait time <= required_uptime. 269*9c5db199SXin Li diff = min(required_uptime - uptime, required_uptime) 270*9c5db199SXin Li if diff > 0: 271*9c5db199SXin Li logging.info( 272*9c5db199SXin Li 'The attached device host was just rebooted, wait %s' 273*9c5db199SXin Li ' seconds for all system services ready and usb' 274*9c5db199SXin Li ' components re-enumerated.', diff) 275*9c5db199SXin Li #TODO(b:226401363): Use a poll to ensure all dependencies are ready. 276*9c5db199SXin Li time.sleep(diff) 277*9c5db199SXin Li 278*9c5db199SXin Li def close(self): 279*9c5db199SXin Li try: 280*9c5db199SXin Li if self._is_locked: 281*9c5db199SXin Li self._unlock() 282*9c5db199SXin Li except error.AutoservSSHTimeout: 283*9c5db199SXin Li logging.error('Unlock attached device host failed due to ssh' 284*9c5db199SXin Li ' timeout. It may caused by the host went down' 285*9c5db199SXin Li ' during the task.') 286*9c5db199SXin Li finally: 287*9c5db199SXin Li super(AttachedDeviceHost, self).close() 288