blob: 72ce7b4cbfd6bbd5db529aa3be0246263a926d53 [file] [log] [blame]
Derek Beckettf73baca2020-08-19 15:08:47 -07001# Lint as: python2, python3
Garry Wangebc015b2019-06-06 17:45:06 -07002# Copyright (c) 2019 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5#
6# Expects to be run in an environment with sudo and no interactive password
7# prompt, such as within the Chromium OS development chroot.
8
9
10"""This is a base host class for servohost and labstation."""
11
12
Derek Beckettf73baca2020-08-19 15:08:47 -070013import six.moves.http_client
Garry Wangebc015b2019-06-06 17:45:06 -070014import logging
15import socket
Derek Beckettf73baca2020-08-19 15:08:47 -070016import six.moves.xmlrpc_client
Garry Wang78ce64d2020-10-13 18:23:45 -070017import time
Garry Wang9ff569f2020-10-20 19:11:30 -070018import os
Garry Wangebc015b2019-06-06 17:45:06 -070019
20from autotest_lib.client.bin import utils
Derek Beckett15775132020-10-01 12:49:45 -070021from autotest_lib.client.common_lib import autotest_enum
Garry Wangebc015b2019-06-06 17:45:06 -070022from autotest_lib.client.common_lib import error
23from autotest_lib.client.common_lib import hosts
24from autotest_lib.client.common_lib import lsbrelease_utils
25from autotest_lib.client.common_lib.cros import dev_server
Garry Wang358aad42020-08-02 20:56:04 -070026from autotest_lib.client.common_lib.cros import kernel_utils
Garry Wangebc015b2019-06-06 17:45:06 -070027from autotest_lib.client.cros import constants as client_constants
Garry Wang358aad42020-08-02 20:56:04 -070028from autotest_lib.server import autotest
Garry Wangebc015b2019-06-06 17:45:06 -070029from autotest_lib.server import site_utils as server_utils
Jae Hoon Kim5f6ca6e2020-09-10 16:11:23 -070030from autotest_lib.server.cros import provisioner
Garry Wangebc015b2019-06-06 17:45:06 -070031from autotest_lib.server.hosts import ssh_host
32from autotest_lib.site_utils.rpm_control_system import rpm_client
33
Garry Wangebc015b2019-06-06 17:45:06 -070034
35class BaseServoHost(ssh_host.SSHHost):
36 """Base host class for a host that manage servo(s).
37 E.g. beaglebone, labstation.
38 """
Garry Wang3d84a162020-01-24 13:29:43 +000039 REBOOT_CMD = 'sleep 5; reboot & sleep 10; reboot -f'
Garry Wangebc015b2019-06-06 17:45:06 -070040
Garry Wang79e9af62019-06-12 15:19:19 -070041 TEMP_FILE_DIR = '/var/lib/servod/'
42
43 LOCK_FILE_POSTFIX = '_in_use'
44 REBOOT_FILE_POSTFIX = '_reboot'
Garry Wangebc015b2019-06-06 17:45:06 -070045
Garry Wang5715ee52019-12-23 11:00:47 -080046 # Time to wait a rebooting servohost, in seconds.
Garry Wangfb253432019-09-11 17:08:38 -070047 REBOOT_TIMEOUT = 240
Garry Wangebc015b2019-06-06 17:45:06 -070048
Garry Wang5715ee52019-12-23 11:00:47 -080049 # Timeout value to power cycle a servohost, in seconds.
50 BOOT_TIMEOUT = 240
51
Garry Wang358aad42020-08-02 20:56:04 -070052 # Constants that reflect current host update state.
Derek Beckett15775132020-10-01 12:49:45 -070053 UPDATE_STATE = autotest_enum.AutotestEnum('IDLE', 'RUNNING',
54 'PENDING_REBOOT')
Garry Wangebc015b2019-06-06 17:45:06 -070055
Andrew Luo4be621d2020-03-21 07:01:13 -070056 def _initialize(self,
57 hostname,
58 is_in_lab=None,
59 servo_host_ssh_port=None,
60 *args,
61 **dargs):
Garry Wangebc015b2019-06-06 17:45:06 -070062 """Construct a BaseServoHost object.
63
64 @param is_in_lab: True if the servo host is in Cros Lab. Default is set
65 to None, for which utils.host_is_in_lab_zone will be
66 called to check if the servo host is in Cros lab.
67
68 """
Andrew Luo4be621d2020-03-21 07:01:13 -070069 if servo_host_ssh_port is not None:
70 dargs['port'] = int(servo_host_ssh_port)
71
Garry Wangebc015b2019-06-06 17:45:06 -070072 super(BaseServoHost, self)._initialize(hostname=hostname,
73 *args, **dargs)
Andrew Luo4be621d2020-03-21 07:01:13 -070074 self._is_localhost = (self.hostname == 'localhost'
75 and servo_host_ssh_port is not None)
Garry Wangebc015b2019-06-06 17:45:06 -070076 if self._is_localhost:
77 self._is_in_lab = False
78 elif is_in_lab is None:
79 self._is_in_lab = utils.host_is_in_lab_zone(self.hostname)
80 else:
81 self._is_in_lab = is_in_lab
82
83 # Commands on the servo host must be run by the superuser.
84 # Our account on a remote host is root, but if our target is
85 # localhost then we might be running unprivileged. If so,
86 # `sudo` will have to be added to the commands.
87 if self._is_localhost:
88 self._sudo_required = utils.system_output('id -u') != '0'
89 else:
90 self._sudo_required = False
91
92 self._is_labstation = None
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -080093 self._dut_host_info = None
Otabek Kasimov2b50cdb2020-07-06 19:16:06 -070094 self._dut_hostname = None
Garry Wangebc015b2019-06-06 17:45:06 -070095
96
97 def get_board(self):
98 """Determine the board for this servo host. E.g. fizz-labstation
99
Garry Wang5e118c02019-09-25 14:24:57 -0700100 @returns a string representing this labstation's board or None if
101 target host is not using a ChromeOS image(e.g. test in chroot).
Garry Wangebc015b2019-06-06 17:45:06 -0700102 """
Garry Wang5e118c02019-09-25 14:24:57 -0700103 output = self.run('cat /etc/lsb-release', ignore_status=True).stdout
104 return lsbrelease_utils.get_current_board(lsb_release_content=output)
Garry Wangebc015b2019-06-06 17:45:06 -0700105
106
Garry Wangd7367482020-02-27 13:52:40 -0800107 def set_dut_host_info(self, dut_host_info):
108 """
109 @param dut_host_info: A HostInfo object.
110 """
111 logging.info('setting dut_host_info field to (%s)', dut_host_info)
112 self._dut_host_info = dut_host_info
113
114
115 def get_dut_host_info(self):
116 """
117 @return A HostInfo object.
118 """
119 return self._dut_host_info
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -0800120
121
Otabek Kasimov2b50cdb2020-07-06 19:16:06 -0700122 def set_dut_hostname(self, dut_hostname):
123 """
124 @param dut_hostname: hostname of the DUT that connected to this servo.
125 """
126 logging.info('setting dut_hostname as (%s)', dut_hostname)
127 self._dut_hostname = dut_hostname
128
129
130 def get_dut_hostname(self):
131 """
132 @returns hostname of the DUT that connected to this servo.
133 """
134 return self._dut_hostname
135
136
Garry Wangebc015b2019-06-06 17:45:06 -0700137 def is_labstation(self):
138 """Determine if the host is a labstation
139
140 @returns True if ths host is a labstation otherwise False.
141 """
142 if self._is_labstation is None:
143 board = self.get_board()
Garry Wang88dc8632019-07-24 16:53:50 -0700144 self._is_labstation = board is not None and 'labstation' in board
Garry Wangebc015b2019-06-06 17:45:06 -0700145
146 return self._is_labstation
147
148
Garry Wang14831832020-03-04 17:21:49 -0800149 def _get_lsb_release_content(self):
150 """Return the content of lsb-release file of host."""
151 return self.run(
152 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
153
154
155 def get_release_version(self):
Garry Wangebc015b2019-06-06 17:45:06 -0700156 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
157
158 @returns The version string in lsb-release, under attribute
Garry Wang14831832020-03-04 17:21:49 -0800159 CHROMEOS_RELEASE_VERSION(e.g. 12900.0.0). None on fail.
Garry Wangebc015b2019-06-06 17:45:06 -0700160 """
Garry Wangebc015b2019-06-06 17:45:06 -0700161 return lsbrelease_utils.get_chromeos_release_version(
Garry Wang14831832020-03-04 17:21:49 -0800162 lsb_release_content=self._get_lsb_release_content()
163 )
164
165
166 def get_full_release_path(self):
167 """Get full release path from servohost as string.
168
169 @returns full release path as a string
170 (e.g. fizz-labstation-release/R82.12900.0.0). None on fail.
171 """
172 return lsbrelease_utils.get_chromeos_release_builder_path(
173 lsb_release_content=self._get_lsb_release_content()
174 )
Garry Wangebc015b2019-06-06 17:45:06 -0700175
176
177 def _check_update_status(self):
Garry Wang358aad42020-08-02 20:56:04 -0700178 """ Check servohost's current update state.
179
180 @returns: one of below state of from self.UPDATE_STATE
181 IDLE -- if the target host is not currently updating and not
182 pending on a reboot.
183 RUNNING -- if there is another updating process that running on
184 target host(note: we don't expect to hit this scenario).
185 PENDING_REBOOT -- if the target host had an update and pending
186 on reboot.
187 """
188 result = self.run('pgrep -f quick-provision | grep -v $$',
189 ignore_status=True)
190 # We don't expect any output unless there are another quick
191 # provision process is running.
192 if result.exit_status == 0:
193 return self.UPDATE_STATE.RUNNING
194
195 # Determine if we have an update that pending on reboot by check if
196 # the current inactive kernel has priority for the next boot.
197 try:
198 inactive_kernel = kernel_utils.get_kernel_state(self)[1]
199 next_kernel = kernel_utils.get_next_kernel(self)
200 if inactive_kernel == next_kernel:
201 return self.UPDATE_STATE.PENDING_REBOOT
202 except Exception as e:
203 logging.error('Unexpected error while checking kernel info; %s', e)
204 return self.UPDATE_STATE.IDLE
Garry Wangebc015b2019-06-06 17:45:06 -0700205
206
207 def is_in_lab(self):
208 """Check whether the servo host is a lab device.
209
210 @returns: True if the servo host is in Cros Lab, otherwise False.
211
212 """
213 return self._is_in_lab
214
215
216 def is_localhost(self):
217 """Checks whether the servo host points to localhost.
218
219 @returns: True if it points to localhost, otherwise False.
220
221 """
222 return self._is_localhost
223
224
225 def is_cros_host(self):
226 """Check if a servo host is running chromeos.
227
228 @return: True if the servo host is running chromeos.
229 False if it isn't, or we don't have enough information.
230 """
231 try:
232 result = self.run('grep -q CHROMEOS /etc/lsb-release',
233 ignore_status=True, timeout=10)
234 except (error.AutoservRunError, error.AutoservSSHTimeout):
235 return False
236 return result.exit_status == 0
237
238
Garry Wang358aad42020-08-02 20:56:04 -0700239 def prepare_for_update(self):
240 """Prepares the DUT for an update.
241 Subclasses may override this to perform any special actions
242 required before updating.
243 """
244 pass
245
246
Garry Wangebc015b2019-06-06 17:45:06 -0700247 def reboot(self, *args, **dargs):
248 """Reboot using special servo host reboot command."""
249 super(BaseServoHost, self).reboot(reboot_cmd=self.REBOOT_CMD,
250 *args, **dargs)
251
252
Garry Wang358aad42020-08-02 20:56:04 -0700253 def update_image(self, stable_version=None):
Garry Wangebc015b2019-06-06 17:45:06 -0700254 """Update the image on the servo host, if needed.
255
256 This method recognizes the following cases:
257 * If the Host is not running Chrome OS, do nothing.
258 * If a previously triggered update is now complete, reboot
259 to the new version.
Garry Wang358aad42020-08-02 20:56:04 -0700260 * If the host is processing an update do nothing.
261 * If the host has an update that pending on reboot, do nothing.
Garry Wangebc015b2019-06-06 17:45:06 -0700262 * If the host is running a version of Chrome OS different
Garry Wang358aad42020-08-02 20:56:04 -0700263 from the default for servo Hosts, start an update.
Garry Wangebc015b2019-06-06 17:45:06 -0700264
Garry Wang14831832020-03-04 17:21:49 -0800265 @stable_version the target build number.(e.g. R82-12900.0.0)
266
Garry Wangebc015b2019-06-06 17:45:06 -0700267 @raises dev_server.DevServerException: If all the devservers are down.
268 @raises site_utils.ParseBuildNameException: If the devserver returns
269 an invalid build name.
Garry Wangebc015b2019-06-06 17:45:06 -0700270 """
271 # servod could be running in a Ubuntu workstation.
272 if not self.is_cros_host():
273 logging.info('Not attempting an update, either %s is not running '
274 'chromeos or we cannot find enough information about '
275 'the host.', self.hostname)
276 return
277
278 if lsbrelease_utils.is_moblab():
279 logging.info('Not attempting an update, %s is running moblab.',
280 self.hostname)
281 return
282
Garry Wang14831832020-03-04 17:21:49 -0800283 if not stable_version:
284 logging.debug("BaseServoHost::update_image attempting to get"
285 " servo cros stable version")
286 try:
287 stable_version = (self.get_dut_host_info().
288 servo_cros_stable_version)
289 except AttributeError:
290 logging.error("BaseServoHost::update_image failed to get"
291 " servo cros stable version.")
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -0800292
Garry Wang14831832020-03-04 17:21:49 -0800293 target_build = "%s-release/%s" % (self.get_board(), stable_version)
Garry Wangebc015b2019-06-06 17:45:06 -0700294 target_build_number = server_utils.ParseBuildName(
295 target_build)[3]
Garry Wang14831832020-03-04 17:21:49 -0800296 current_build_number = self.get_release_version()
Garry Wangebc015b2019-06-06 17:45:06 -0700297
298 if current_build_number == target_build_number:
299 logging.info('servo host %s does not require an update.',
300 self.hostname)
301 return
302
303 status = self._check_update_status()
Garry Wang358aad42020-08-02 20:56:04 -0700304 if status == self.UPDATE_STATE.RUNNING:
305 logging.info('servo host %s already processing an update',
306 self.hostname)
307 return
308 if status == self.UPDATE_STATE.PENDING_REBOOT:
Garry Wangebc015b2019-06-06 17:45:06 -0700309 # Labstation reboot is handled separately here as it require
Garry Wang358aad42020-08-02 20:56:04 -0700310 # synchronized reboot among all managed DUTs. For servo_v3, we'll
311 # reboot when initialize Servohost, if there is a update pending.
312 logging.info('An update has been completed and pending reboot.')
313 return
Garry Wangebc015b2019-06-06 17:45:06 -0700314
Garry Wang358aad42020-08-02 20:56:04 -0700315 ds = dev_server.ImageServer.resolve(self.hostname,
316 hostname=self.hostname)
317 url = ds.get_update_url(target_build)
Jae Hoon Kim5f6ca6e2020-09-10 16:11:23 -0700318 cros_provisioner = provisioner.ChromiumOSProvisioner(update_url=url,
Jae Hoon Kim3f004992020-09-10 17:48:33 -0700319 host=self,
320 is_servohost=True)
Garry Wang358aad42020-08-02 20:56:04 -0700321 logging.info('Using devserver url: %s to trigger update on '
322 'servo host %s, from %s to %s', url, self.hostname,
323 current_build_number, target_build_number)
Jae Hoon Kim5f6ca6e2020-09-10 16:11:23 -0700324 cros_provisioner.run_provision()
Garry Wangebc015b2019-06-06 17:45:06 -0700325
326
327 def has_power(self):
328 """Return whether or not the servo host is powered by PoE or RPM."""
329 # TODO(fdeng): See crbug.com/302791
330 # For now, assume all servo hosts in the lab have power.
331 return self.is_in_lab()
332
333
Garry Wang358aad42020-08-02 20:56:04 -0700334 def _post_update_reboot(self):
335 """ Reboot servohost after an quick provision.
336
337 We need to do some specifal cleanup before and after reboot
338 when there is an update pending.
339 """
340 # Regarding the 'crossystem' command below: In some cases,
341 # the update flow puts the TPM into a state such that it
342 # fails verification. We don't know why. However, this
343 # call papers over the problem by clearing the TPM during
344 # the reboot.
345 #
346 # We ignore failures from 'crossystem'. Although failure
347 # here is unexpected, and could signal a bug, the point of
348 # the exercise is to paper over problems; allowing this to
349 # fail would defeat the purpose.
Garry Wang9ff569f2020-10-20 19:11:30 -0700350
351 # Preserve critical files before reboot since post-provision
352 # clobbering will wipe the stateful partition.
353 # TODO(xianuowang@) Remove this logic once we have updated to
354 # a image with https://crrev.com/c/2485908.
355 path_to_preserve = [
356 '/var/lib/servod',
357 '/var/lib/device_health_profile',
358 ]
359 safe_location = '/mnt/stateful_partition/unencrypted/preserve/'
360 for item in path_to_preserve:
361 dest = os.path.join(safe_location, item.split('/')[-1])
362 self.run('rm -rf %s' % dest, ignore_status=True)
363 self.run('mv %s %s' % (item, safe_location), ignore_status=True)
364
Garry Wang358aad42020-08-02 20:56:04 -0700365 self.run('crossystem clear_tpm_owner_request=1', ignore_status=True)
366 self._servo_host_reboot()
367 logging.debug('Cleaning up autotest directories if exist.')
368 try:
369 installed_autodir = autotest.Autotest.get_installed_autodir(self)
370 self.run('rm -rf ' + installed_autodir)
371 except autotest.AutodirNotFoundError:
372 logging.debug('No autotest installed directory found.')
373
Garry Wang9ff569f2020-10-20 19:11:30 -0700374 # Recover preserved files to original location.
375 # TODO(xianuowang@) Remove this logic once we have updated to
376 # a image with https://crrev.com/c/2485908.
377 for item in path_to_preserve:
378 src = os.path.join(safe_location, item.split('/')[-1])
379 dest = '/'.join(item.split('/')[:-1])
380 self.run('mv %s %s' % (src, dest), ignore_status=True)
Garry Wang358aad42020-08-02 20:56:04 -0700381
Garry Wangebc015b2019-06-06 17:45:06 -0700382 def power_cycle(self):
383 """Cycle power to this host via PoE(servo v3) or RPM(labstation)
384 if it is a lab device.
385
386 @raises AutoservRepairError if it fails to power cycle the
387 servo host.
388
389 """
390 if self.has_power():
391 try:
392 rpm_client.set_power(self, 'CYCLE')
Derek Beckettf73baca2020-08-19 15:08:47 -0700393 except (socket.error, six.moves.xmlrpc_client.Error,
394 six.moves.http_client.BadStatusLine,
Garry Wangebc015b2019-06-06 17:45:06 -0700395 rpm_client.RemotePowerException) as e:
396 raise hosts.AutoservRepairError(
397 'Power cycling %s failed: %s' % (self.hostname, e),
398 'power_cycle_via_rpm_failed'
399 )
400 else:
401 logging.info('Skipping power cycling, not a lab device.')
402
403
404 def _servo_host_reboot(self):
405 """Reboot this servo host because a reboot is requested."""
Otabek Kasimovb676b072020-12-02 10:50:08 -0800406 try:
407 # TODO(otabek) remove if found the fix for b/174514811
408 # The default factory firmware remember the latest chromeboxes
409 # status after power off. If box was in sleep mode before the
410 # break, the box will stay at sleep mode after power on.
411 # Disable power manager has make chromebox to boot always when
412 # we deliver the power to the device.
413 logging.info('Stoping powerd service on device')
414 self.run('stop powerd', ignore_status=True, timeout=30)
415 except Exception as e:
416 logging.debug('(Not critical) Fail to stop powerd; %s', e)
417
Garry Wangebc015b2019-06-06 17:45:06 -0700418 logging.info('Rebooting servo host %s from build %s', self.hostname,
Garry Wang14831832020-03-04 17:21:49 -0800419 self.get_release_version())
Garry Wangebc015b2019-06-06 17:45:06 -0700420 # Tell the reboot() call not to wait for completion.
421 # Otherwise, the call will log reboot failure if servo does
422 # not come back. The logged reboot failure will lead to
423 # test job failure. If the test does not require servo, we
424 # don't want servo failure to fail the test with error:
425 # `Host did not return from reboot` in status.log.
426 self.reboot(fastsync=True, wait=False)
427
428 # We told the reboot() call not to wait, but we need to wait
429 # for the reboot before we continue. Alas. The code from
430 # here below is basically a copy of Host.wait_for_restart(),
431 # with the logging bits ripped out, so that they can't cause
432 # the failure logging problem described above.
433 #
Derek Beckett4d102242020-12-01 14:24:37 -0800434 # The stain that this has left on my soul can never be
Garry Wangebc015b2019-06-06 17:45:06 -0700435 # erased.
436 old_boot_id = self.get_boot_id()
437 if not self.wait_down(timeout=self.WAIT_DOWN_REBOOT_TIMEOUT,
438 warning_timer=self.WAIT_DOWN_REBOOT_WARNING,
439 old_boot_id=old_boot_id):
440 raise error.AutoservHostError(
441 'servo host %s failed to shut down.' %
442 self.hostname)
Garry Wang79e9af62019-06-12 15:19:19 -0700443 if self.wait_up(timeout=self.REBOOT_TIMEOUT):
Garry Wangebc015b2019-06-06 17:45:06 -0700444 logging.info('servo host %s back from reboot, with build %s',
Garry Wang14831832020-03-04 17:21:49 -0800445 self.hostname, self.get_release_version())
Garry Wangebc015b2019-06-06 17:45:06 -0700446 else:
447 raise error.AutoservHostError(
448 'servo host %s failed to come back from reboot.' %
449 self.hostname)
450
451
452 def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
453 connect_timeout=None, alive_interval=None, alive_count_max=None,
454 connection_attempts=None):
455 """Override default make_ssh_command to use tuned options.
456
457 Tuning changes:
458 - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
459 connection failure. Consistency with remote_access.py.
460
461 - ServerAliveInterval=180; which causes SSH to ping connection every
462 180 seconds. In conjunction with ServerAliveCountMax ensures
463 that if the connection dies, Autotest will bail out quickly.
464
465 - ServerAliveCountMax=3; consistency with remote_access.py.
466
467 - ConnectAttempts=4; reduce flakiness in connection errors;
468 consistency with remote_access.py.
469
470 - UserKnownHostsFile=/dev/null; we don't care about the keys.
471
472 - SSH protocol forced to 2; needed for ServerAliveInterval.
473
474 @param user User name to use for the ssh connection.
475 @param port Port on the target host to use for ssh connection.
476 @param opts Additional options to the ssh command.
477 @param hosts_file Ignored.
478 @param connect_timeout Ignored.
479 @param alive_interval Ignored.
480 @param alive_count_max Ignored.
481 @param connection_attempts Ignored.
482
483 @returns: An ssh command with the requested settings.
484
485 """
486 options = ' '.join([opts, '-o Protocol=2'])
487 return super(BaseServoHost, self).make_ssh_command(
488 user=user, port=port, opts=options, hosts_file='/dev/null',
489 connect_timeout=30, alive_interval=180, alive_count_max=3,
490 connection_attempts=4)
491
492
493 def _make_scp_cmd(self, sources, dest):
494 """Format scp command.
495
496 Given a list of source paths and a destination path, produces the
497 appropriate scp command for encoding it. Remote paths must be
498 pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost
499 to allow additional ssh options.
500
501 @param sources: A list of source paths to copy from.
502 @param dest: Destination path to copy to.
503
504 @returns: An scp command that copies |sources| on local machine to
505 |dest| on the remote servo host.
506
507 """
508 command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no '
509 '-o UserKnownHostsFile=/dev/null -P %d %s "%s"')
Derek Beckett4d102242020-12-01 14:24:37 -0800510 return command % (self._main_ssh.ssh_option,
Garry Wangebc015b2019-06-06 17:45:06 -0700511 self.port, sources, dest)
512
513
514 def run(self, command, timeout=3600, ignore_status=False,
515 stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
516 connect_timeout=30, ssh_failure_retry_ok=False,
517 options='', stdin=None, verbose=True, args=()):
518 """Run a command on the servo host.
519
520 Extends method `run` in SSHHost. If the servo host is a remote device,
521 it will call `run` in SSHost without changing anything.
522 If the servo host is 'localhost', it will call utils.system_output.
523
524 @param command: The command line string.
525 @param timeout: Time limit in seconds before attempting to
526 kill the running process. The run() function
527 will take a few seconds longer than 'timeout'
528 to complete if it has to kill the process.
529 @param ignore_status: Do not raise an exception, no matter
530 what the exit code of the command is.
531 @param stdout_tee/stderr_tee: Where to tee the stdout/stderr.
532 @param connect_timeout: SSH connection timeout (in seconds)
533 Ignored if host is 'localhost'.
534 @param options: String with additional ssh command options
535 Ignored if host is 'localhost'.
536 @param ssh_failure_retry_ok: when True and ssh connection failure is
537 suspected, OK to retry command (but not
538 compulsory, and likely not needed here)
539 @param stdin: Stdin to pass (a string) to the executed command.
540 @param verbose: Log the commands.
541 @param args: Sequence of strings to pass as arguments to command by
542 quoting them in " and escaping their contents if necessary.
543
544 @returns: A utils.CmdResult object.
545
546 @raises AutoservRunError if the command failed.
547 @raises AutoservSSHTimeout SSH connection has timed out. Only applies
548 when servo host is not 'localhost'.
549
550 """
Gregory Nisbet32e74022020-07-14 18:42:30 -0700551 run_args = {
552 'command' : command,
553 'timeout' : timeout,
554 'ignore_status' : ignore_status,
555 'stdout_tee' : stdout_tee,
556 'stderr_tee' : stderr_tee,
557 # connect_timeout n/a for localhost
558 # options n/a for localhost
Andrew McRaeed8b52f2020-07-20 11:29:26 +1000559 # ssh_failure_retry_ok n/a for localhost
Gregory Nisbet32e74022020-07-14 18:42:30 -0700560 'stdin' : stdin,
561 'verbose' : verbose,
562 'args' : args,
563 }
Garry Wangebc015b2019-06-06 17:45:06 -0700564 if self.is_localhost():
565 if self._sudo_required:
566 run_args['command'] = 'sudo -n sh -c "%s"' % utils.sh_escape(
567 command)
568 try:
569 return utils.run(**run_args)
570 except error.CmdError as e:
571 logging.error(e)
572 raise error.AutoservRunError('command execution error',
573 e.result_obj)
574 else:
575 run_args['connect_timeout'] = connect_timeout
576 run_args['options'] = options
Andrew McRaeed8b52f2020-07-20 11:29:26 +1000577 run_args['ssh_failure_retry_ok'] = ssh_failure_retry_ok
Garry Wangebc015b2019-06-06 17:45:06 -0700578 return super(BaseServoHost, self).run(**run_args)
Garry Wang2b5eef92020-08-21 16:23:35 -0700579
580 def _mount_drive(self, src_path, dst_path):
581 """Mount an external drive on servohost.
582
583 @param: src_path the drive path to mount(e.g. /dev/sda3).
584 @param: dst_path the destination directory on servohost to mount
585 the drive.
586
587 @returns: True if mount success otherwise False.
588 """
589 # Make sure the dst dir exists.
590 self.run('mkdir -p %s' % dst_path)
591
592 result = self.run('mount -o ro %s %s' % (src_path, dst_path),
593 ignore_status=True)
594 return result.exit_status == 0
595
596 def _unmount_drive(self, mount_path):
597 """Unmount a drive from servohost.
598
599 @param: mount_path the path on servohost to unmount.
600
601 @returns: True if unmount success otherwise False.
602 """
603 result = self.run('umount %s' % mount_path, ignore_status=True)
604 return result.exit_status == 0
Garry Wang78ce64d2020-10-13 18:23:45 -0700605
606 def wait_ready(self, required_uptime=300):
607 """Wait ready for a servohost if it has been rebooted recently.
608
609 It may take a few minutes until all servos and their componments
610 re-enumerated and become ready after a servohost(especially labstation
611 as it supports multiple servos) reboot, so we need to make sure the
612 servohost has been up for a given a mount of time before trying to
613 start any actions.
614
615 @param required_uptime: Minimum uptime in seconds that we can
616 consdier a servohost be ready.
617 """
618 uptime = float(self.check_uptime())
619 # To prevent unexpected output from check_uptime() that causes long
620 # sleep, make sure the maximum wait time <= required_uptime.
621 diff = min(required_uptime - uptime, required_uptime)
622 if diff > 0:
623 logging.info(
624 'The servohost was just rebooted, wait %s'
625 ' seconds for it to become ready', diff)
626 time.sleep(diff)