blob: 277477bd7381e6d1368db827901ee1d1693fc21c [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
Garry Wangebc015b2019-06-06 17:45:06 -070013import logging
Garry Wang78ce64d2020-10-13 18:23:45 -070014import time
Garry Wang9ff569f2020-10-20 19:11:30 -070015import os
Garry Wangebc015b2019-06-06 17:45:06 -070016
17from autotest_lib.client.bin import utils
Derek Beckett15775132020-10-01 12:49:45 -070018from autotest_lib.client.common_lib import autotest_enum
Garry Wangebc015b2019-06-06 17:45:06 -070019from autotest_lib.client.common_lib import error
Garry Wangebc015b2019-06-06 17:45:06 -070020from autotest_lib.client.common_lib import lsbrelease_utils
21from autotest_lib.client.common_lib.cros import dev_server
Garry Wang358aad42020-08-02 20:56:04 -070022from autotest_lib.client.common_lib.cros import kernel_utils
Garry Wangebc015b2019-06-06 17:45:06 -070023from autotest_lib.client.cros import constants as client_constants
Garry Wang358aad42020-08-02 20:56:04 -070024from autotest_lib.server import autotest
Garry Wangebc015b2019-06-06 17:45:06 -070025from autotest_lib.server import site_utils as server_utils
Garry Wangebc015b2019-06-06 17:45:06 -070026from autotest_lib.server.hosts import ssh_host
Garry Wangebc015b2019-06-06 17:45:06 -070027
Garry Wangebc015b2019-06-06 17:45:06 -070028
29class BaseServoHost(ssh_host.SSHHost):
30 """Base host class for a host that manage servo(s).
31 E.g. beaglebone, labstation.
32 """
Garry Wang3d84a162020-01-24 13:29:43 +000033 REBOOT_CMD = 'sleep 5; reboot & sleep 10; reboot -f'
Garry Wangebc015b2019-06-06 17:45:06 -070034
Garry Wang79e9af62019-06-12 15:19:19 -070035 TEMP_FILE_DIR = '/var/lib/servod/'
36
37 LOCK_FILE_POSTFIX = '_in_use'
38 REBOOT_FILE_POSTFIX = '_reboot'
Garry Wangebc015b2019-06-06 17:45:06 -070039
Garry Wang5715ee52019-12-23 11:00:47 -080040 # Time to wait a rebooting servohost, in seconds.
Garry Wangfb253432019-09-11 17:08:38 -070041 REBOOT_TIMEOUT = 240
Garry Wangebc015b2019-06-06 17:45:06 -070042
Garry Wang5715ee52019-12-23 11:00:47 -080043 # Timeout value to power cycle a servohost, in seconds.
44 BOOT_TIMEOUT = 240
45
Garry Wang358aad42020-08-02 20:56:04 -070046 # Constants that reflect current host update state.
Derek Beckett15775132020-10-01 12:49:45 -070047 UPDATE_STATE = autotest_enum.AutotestEnum('IDLE', 'RUNNING',
48 'PENDING_REBOOT')
Garry Wangebc015b2019-06-06 17:45:06 -070049
Andrew Luo4be621d2020-03-21 07:01:13 -070050 def _initialize(self,
51 hostname,
52 is_in_lab=None,
53 servo_host_ssh_port=None,
54 *args,
55 **dargs):
Garry Wangebc015b2019-06-06 17:45:06 -070056 """Construct a BaseServoHost object.
57
58 @param is_in_lab: True if the servo host is in Cros Lab. Default is set
59 to None, for which utils.host_is_in_lab_zone will be
60 called to check if the servo host is in Cros lab.
61
62 """
Andrew Luo4be621d2020-03-21 07:01:13 -070063 if servo_host_ssh_port is not None:
64 dargs['port'] = int(servo_host_ssh_port)
65
Garry Wangebc015b2019-06-06 17:45:06 -070066 super(BaseServoHost, self)._initialize(hostname=hostname,
67 *args, **dargs)
Anhb4c21d82021-07-01 22:07:15 +000068
69 self._is_containerized_servod = self.hostname.endswith('docker_servod')
70
Andrew Luo4be621d2020-03-21 07:01:13 -070071 self._is_localhost = (self.hostname == 'localhost'
Jeremy Bettis2eefe932021-02-08 16:10:34 -070072 and servo_host_ssh_port is None)
Anhb4c21d82021-07-01 22:07:15 +000073 if self._is_localhost or self._is_containerized_servod:
Garry Wangebc015b2019-06-06 17:45:06 -070074 self._is_in_lab = False
75 elif is_in_lab is None:
76 self._is_in_lab = utils.host_is_in_lab_zone(self.hostname)
77 else:
78 self._is_in_lab = is_in_lab
79
80 # Commands on the servo host must be run by the superuser.
81 # Our account on a remote host is root, but if our target is
82 # localhost then we might be running unprivileged. If so,
83 # `sudo` will have to be added to the commands.
84 if self._is_localhost:
85 self._sudo_required = utils.system_output('id -u') != '0'
86 else:
87 self._sudo_required = False
88
89 self._is_labstation = None
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -080090 self._dut_host_info = None
Otabek Kasimov2b50cdb2020-07-06 19:16:06 -070091 self._dut_hostname = None
Garry Wangebc015b2019-06-06 17:45:06 -070092
93
94 def get_board(self):
95 """Determine the board for this servo host. E.g. fizz-labstation
96
Garry Wang5e118c02019-09-25 14:24:57 -070097 @returns a string representing this labstation's board or None if
98 target host is not using a ChromeOS image(e.g. test in chroot).
Garry Wangebc015b2019-06-06 17:45:06 -070099 """
Garry Wang5e118c02019-09-25 14:24:57 -0700100 output = self.run('cat /etc/lsb-release', ignore_status=True).stdout
101 return lsbrelease_utils.get_current_board(lsb_release_content=output)
Garry Wangebc015b2019-06-06 17:45:06 -0700102
103
Garry Wangd7367482020-02-27 13:52:40 -0800104 def set_dut_host_info(self, dut_host_info):
105 """
106 @param dut_host_info: A HostInfo object.
107 """
108 logging.info('setting dut_host_info field to (%s)', dut_host_info)
109 self._dut_host_info = dut_host_info
110
111
112 def get_dut_host_info(self):
113 """
114 @return A HostInfo object.
115 """
116 return self._dut_host_info
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -0800117
118
Otabek Kasimov2b50cdb2020-07-06 19:16:06 -0700119 def set_dut_hostname(self, dut_hostname):
120 """
121 @param dut_hostname: hostname of the DUT that connected to this servo.
122 """
123 logging.info('setting dut_hostname as (%s)', dut_hostname)
124 self._dut_hostname = dut_hostname
125
126
127 def get_dut_hostname(self):
128 """
129 @returns hostname of the DUT that connected to this servo.
130 """
131 return self._dut_hostname
132
133
Garry Wangebc015b2019-06-06 17:45:06 -0700134 def is_labstation(self):
135 """Determine if the host is a labstation
136
137 @returns True if ths host is a labstation otherwise False.
138 """
139 if self._is_labstation is None:
140 board = self.get_board()
Garry Wang88dc8632019-07-24 16:53:50 -0700141 self._is_labstation = board is not None and 'labstation' in board
Garry Wangebc015b2019-06-06 17:45:06 -0700142
143 return self._is_labstation
144
145
Garry Wang14831832020-03-04 17:21:49 -0800146 def _get_lsb_release_content(self):
147 """Return the content of lsb-release file of host."""
148 return self.run(
149 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
150
151
152 def get_release_version(self):
Garry Wangebc015b2019-06-06 17:45:06 -0700153 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
154
155 @returns The version string in lsb-release, under attribute
Garry Wang14831832020-03-04 17:21:49 -0800156 CHROMEOS_RELEASE_VERSION(e.g. 12900.0.0). None on fail.
Garry Wangebc015b2019-06-06 17:45:06 -0700157 """
Garry Wangebc015b2019-06-06 17:45:06 -0700158 return lsbrelease_utils.get_chromeos_release_version(
Garry Wang14831832020-03-04 17:21:49 -0800159 lsb_release_content=self._get_lsb_release_content()
160 )
161
162
163 def get_full_release_path(self):
164 """Get full release path from servohost as string.
165
166 @returns full release path as a string
167 (e.g. fizz-labstation-release/R82.12900.0.0). None on fail.
168 """
169 return lsbrelease_utils.get_chromeos_release_builder_path(
170 lsb_release_content=self._get_lsb_release_content()
171 )
Garry Wangebc015b2019-06-06 17:45:06 -0700172
173
174 def _check_update_status(self):
Garry Wang358aad42020-08-02 20:56:04 -0700175 """ Check servohost's current update state.
176
177 @returns: one of below state of from self.UPDATE_STATE
178 IDLE -- if the target host is not currently updating and not
179 pending on a reboot.
180 RUNNING -- if there is another updating process that running on
181 target host(note: we don't expect to hit this scenario).
182 PENDING_REBOOT -- if the target host had an update and pending
183 on reboot.
184 """
185 result = self.run('pgrep -f quick-provision | grep -v $$',
186 ignore_status=True)
187 # We don't expect any output unless there are another quick
188 # provision process is running.
189 if result.exit_status == 0:
190 return self.UPDATE_STATE.RUNNING
191
192 # Determine if we have an update that pending on reboot by check if
193 # the current inactive kernel has priority for the next boot.
194 try:
195 inactive_kernel = kernel_utils.get_kernel_state(self)[1]
196 next_kernel = kernel_utils.get_next_kernel(self)
197 if inactive_kernel == next_kernel:
198 return self.UPDATE_STATE.PENDING_REBOOT
199 except Exception as e:
200 logging.error('Unexpected error while checking kernel info; %s', e)
201 return self.UPDATE_STATE.IDLE
Garry Wangebc015b2019-06-06 17:45:06 -0700202
203
204 def is_in_lab(self):
205 """Check whether the servo host is a lab device.
206
207 @returns: True if the servo host is in Cros Lab, otherwise False.
208
209 """
210 return self._is_in_lab
211
212
213 def is_localhost(self):
214 """Checks whether the servo host points to localhost.
215
216 @returns: True if it points to localhost, otherwise False.
217
218 """
219 return self._is_localhost
220
221
Anhb4c21d82021-07-01 22:07:15 +0000222 def is_containerized_servod(self):
223 """Checks whether the servo host is a containerized servod.
224
225 @returns: True if using containerized servod, otherwise False.
226
227 """
228 return self._is_containerized_servod
229
Garry Wangebc015b2019-06-06 17:45:06 -0700230 def is_cros_host(self):
231 """Check if a servo host is running chromeos.
232
233 @return: True if the servo host is running chromeos.
234 False if it isn't, or we don't have enough information.
235 """
236 try:
237 result = self.run('grep -q CHROMEOS /etc/lsb-release',
238 ignore_status=True, timeout=10)
239 except (error.AutoservRunError, error.AutoservSSHTimeout):
240 return False
241 return result.exit_status == 0
242
243
Garry Wang358aad42020-08-02 20:56:04 -0700244 def prepare_for_update(self):
245 """Prepares the DUT for an update.
246 Subclasses may override this to perform any special actions
247 required before updating.
248 """
249 pass
250
251
Garry Wangebc015b2019-06-06 17:45:06 -0700252 def reboot(self, *args, **dargs):
253 """Reboot using special servo host reboot command."""
254 super(BaseServoHost, self).reboot(reboot_cmd=self.REBOOT_CMD,
255 *args, **dargs)
256
257
Garry Wang358aad42020-08-02 20:56:04 -0700258 def update_image(self, stable_version=None):
Garry Wangebc015b2019-06-06 17:45:06 -0700259 """Update the image on the servo host, if needed.
260
261 This method recognizes the following cases:
262 * If the Host is not running Chrome OS, do nothing.
263 * If a previously triggered update is now complete, reboot
264 to the new version.
Garry Wang358aad42020-08-02 20:56:04 -0700265 * If the host is processing an update do nothing.
266 * If the host has an update that pending on reboot, do nothing.
Garry Wangebc015b2019-06-06 17:45:06 -0700267 * If the host is running a version of Chrome OS different
Garry Wang358aad42020-08-02 20:56:04 -0700268 from the default for servo Hosts, start an update.
Garry Wangebc015b2019-06-06 17:45:06 -0700269
Garry Wang14831832020-03-04 17:21:49 -0800270 @stable_version the target build number.(e.g. R82-12900.0.0)
271
Garry Wangebc015b2019-06-06 17:45:06 -0700272 @raises dev_server.DevServerException: If all the devservers are down.
273 @raises site_utils.ParseBuildNameException: If the devserver returns
274 an invalid build name.
Garry Wangebc015b2019-06-06 17:45:06 -0700275 """
276 # servod could be running in a Ubuntu workstation.
277 if not self.is_cros_host():
278 logging.info('Not attempting an update, either %s is not running '
279 'chromeos or we cannot find enough information about '
280 'the host.', self.hostname)
281 return
282
283 if lsbrelease_utils.is_moblab():
284 logging.info('Not attempting an update, %s is running moblab.',
285 self.hostname)
286 return
287
Garry Wang14831832020-03-04 17:21:49 -0800288 if not stable_version:
289 logging.debug("BaseServoHost::update_image attempting to get"
290 " servo cros stable version")
291 try:
292 stable_version = (self.get_dut_host_info().
293 servo_cros_stable_version)
294 except AttributeError:
295 logging.error("BaseServoHost::update_image failed to get"
296 " servo cros stable version.")
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -0800297
Garry Wang14831832020-03-04 17:21:49 -0800298 target_build = "%s-release/%s" % (self.get_board(), stable_version)
Garry Wangebc015b2019-06-06 17:45:06 -0700299 target_build_number = server_utils.ParseBuildName(
300 target_build)[3]
Garry Wang14831832020-03-04 17:21:49 -0800301 current_build_number = self.get_release_version()
Garry Wangebc015b2019-06-06 17:45:06 -0700302
303 if current_build_number == target_build_number:
304 logging.info('servo host %s does not require an update.',
305 self.hostname)
306 return
307
308 status = self._check_update_status()
Garry Wang358aad42020-08-02 20:56:04 -0700309 if status == self.UPDATE_STATE.RUNNING:
310 logging.info('servo host %s already processing an update',
311 self.hostname)
312 return
313 if status == self.UPDATE_STATE.PENDING_REBOOT:
Garry Wangebc015b2019-06-06 17:45:06 -0700314 # Labstation reboot is handled separately here as it require
Garry Wang358aad42020-08-02 20:56:04 -0700315 # synchronized reboot among all managed DUTs. For servo_v3, we'll
316 # reboot when initialize Servohost, if there is a update pending.
317 logging.info('An update has been completed and pending reboot.')
318 return
Garry Wangebc015b2019-06-06 17:45:06 -0700319
Garry Wang358aad42020-08-02 20:56:04 -0700320 ds = dev_server.ImageServer.resolve(self.hostname,
321 hostname=self.hostname)
322 url = ds.get_update_url(target_build)
Derek Beckett08ca8572021-08-25 14:46:35 -0700323 # TODO dbeckett@, strip this out in favor of services.
324 # cros_provisioner = provisioner.ChromiumOSProvisioner(update_url=url,
325 # host=self,
326 # is_servohost=True)
327 # logging.info('Using devserver url: %s to trigger update on '
328 # 'servo host %s, from %s to %s', url, self.hostname,
329 # current_build_number, target_build_number)
330 # cros_provisioner.run_provision()
Garry Wangebc015b2019-06-06 17:45:06 -0700331
332
333 def has_power(self):
334 """Return whether or not the servo host is powered by PoE or RPM."""
335 # TODO(fdeng): See crbug.com/302791
336 # For now, assume all servo hosts in the lab have power.
337 return self.is_in_lab()
338
339
Garry Wang358aad42020-08-02 20:56:04 -0700340 def _post_update_reboot(self):
341 """ Reboot servohost after an quick provision.
342
343 We need to do some specifal cleanup before and after reboot
344 when there is an update pending.
345 """
346 # Regarding the 'crossystem' command below: In some cases,
347 # the update flow puts the TPM into a state such that it
348 # fails verification. We don't know why. However, this
349 # call papers over the problem by clearing the TPM during
350 # the reboot.
351 #
352 # We ignore failures from 'crossystem'. Although failure
353 # here is unexpected, and could signal a bug, the point of
354 # the exercise is to paper over problems; allowing this to
355 # fail would defeat the purpose.
Garry Wang9ff569f2020-10-20 19:11:30 -0700356
357 # Preserve critical files before reboot since post-provision
358 # clobbering will wipe the stateful partition.
359 # TODO(xianuowang@) Remove this logic once we have updated to
360 # a image with https://crrev.com/c/2485908.
361 path_to_preserve = [
362 '/var/lib/servod',
363 '/var/lib/device_health_profile',
364 ]
365 safe_location = '/mnt/stateful_partition/unencrypted/preserve/'
366 for item in path_to_preserve:
367 dest = os.path.join(safe_location, item.split('/')[-1])
368 self.run('rm -rf %s' % dest, ignore_status=True)
369 self.run('mv %s %s' % (item, safe_location), ignore_status=True)
370
Garry Wang358aad42020-08-02 20:56:04 -0700371 self.run('crossystem clear_tpm_owner_request=1', ignore_status=True)
372 self._servo_host_reboot()
373 logging.debug('Cleaning up autotest directories if exist.')
374 try:
375 installed_autodir = autotest.Autotest.get_installed_autodir(self)
376 self.run('rm -rf ' + installed_autodir)
377 except autotest.AutodirNotFoundError:
378 logging.debug('No autotest installed directory found.')
379
Garry Wang9ff569f2020-10-20 19:11:30 -0700380 # Recover preserved files to original location.
381 # TODO(xianuowang@) Remove this logic once we have updated to
382 # a image with https://crrev.com/c/2485908.
383 for item in path_to_preserve:
384 src = os.path.join(safe_location, item.split('/')[-1])
385 dest = '/'.join(item.split('/')[:-1])
386 self.run('mv %s %s' % (src, dest), ignore_status=True)
Garry Wang358aad42020-08-02 20:56:04 -0700387
Garry Wangebc015b2019-06-06 17:45:06 -0700388 def power_cycle(self):
389 """Cycle power to this host via PoE(servo v3) or RPM(labstation)
390 if it is a lab device.
391
392 @raises AutoservRepairError if it fails to power cycle the
393 servo host.
394
395 """
396 if self.has_power():
Derek Beckett5ea7bf32021-08-03 13:09:20 -0700397 # TODO b/195443964: Re-wire as needed once TLW is available.
398 logging.warning("Need TLW rpm_controller wiring.")
Garry Wangebc015b2019-06-06 17:45:06 -0700399
400 def _servo_host_reboot(self):
401 """Reboot this servo host because a reboot is requested."""
Otabek Kasimovb676b072020-12-02 10:50:08 -0800402 try:
403 # TODO(otabek) remove if found the fix for b/174514811
404 # The default factory firmware remember the latest chromeboxes
405 # status after power off. If box was in sleep mode before the
406 # break, the box will stay at sleep mode after power on.
407 # Disable power manager has make chromebox to boot always when
408 # we deliver the power to the device.
409 logging.info('Stoping powerd service on device')
410 self.run('stop powerd', ignore_status=True, timeout=30)
411 except Exception as e:
412 logging.debug('(Not critical) Fail to stop powerd; %s', e)
413
Garry Wangebc015b2019-06-06 17:45:06 -0700414 logging.info('Rebooting servo host %s from build %s', self.hostname,
Garry Wang14831832020-03-04 17:21:49 -0800415 self.get_release_version())
Garry Wangebc015b2019-06-06 17:45:06 -0700416 # Tell the reboot() call not to wait for completion.
417 # Otherwise, the call will log reboot failure if servo does
418 # not come back. The logged reboot failure will lead to
419 # test job failure. If the test does not require servo, we
420 # don't want servo failure to fail the test with error:
421 # `Host did not return from reboot` in status.log.
422 self.reboot(fastsync=True, wait=False)
423
424 # We told the reboot() call not to wait, but we need to wait
425 # for the reboot before we continue. Alas. The code from
426 # here below is basically a copy of Host.wait_for_restart(),
427 # with the logging bits ripped out, so that they can't cause
428 # the failure logging problem described above.
429 #
Derek Beckett4d102242020-12-01 14:24:37 -0800430 # The stain that this has left on my soul can never be
Garry Wangebc015b2019-06-06 17:45:06 -0700431 # erased.
432 old_boot_id = self.get_boot_id()
433 if not self.wait_down(timeout=self.WAIT_DOWN_REBOOT_TIMEOUT,
434 warning_timer=self.WAIT_DOWN_REBOOT_WARNING,
435 old_boot_id=old_boot_id):
436 raise error.AutoservHostError(
437 'servo host %s failed to shut down.' %
438 self.hostname)
Garry Wang79e9af62019-06-12 15:19:19 -0700439 if self.wait_up(timeout=self.REBOOT_TIMEOUT):
Garry Wangebc015b2019-06-06 17:45:06 -0700440 logging.info('servo host %s back from reboot, with build %s',
Garry Wang14831832020-03-04 17:21:49 -0800441 self.hostname, self.get_release_version())
Garry Wangebc015b2019-06-06 17:45:06 -0700442 else:
443 raise error.AutoservHostError(
444 'servo host %s failed to come back from reboot.' %
445 self.hostname)
446
447
448 def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
449 connect_timeout=None, alive_interval=None, alive_count_max=None,
450 connection_attempts=None):
451 """Override default make_ssh_command to use tuned options.
452
453 Tuning changes:
454 - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
455 connection failure. Consistency with remote_access.py.
456
457 - ServerAliveInterval=180; which causes SSH to ping connection every
458 180 seconds. In conjunction with ServerAliveCountMax ensures
459 that if the connection dies, Autotest will bail out quickly.
460
461 - ServerAliveCountMax=3; consistency with remote_access.py.
462
463 - ConnectAttempts=4; reduce flakiness in connection errors;
464 consistency with remote_access.py.
465
466 - UserKnownHostsFile=/dev/null; we don't care about the keys.
467
468 - SSH protocol forced to 2; needed for ServerAliveInterval.
469
470 @param user User name to use for the ssh connection.
471 @param port Port on the target host to use for ssh connection.
472 @param opts Additional options to the ssh command.
473 @param hosts_file Ignored.
474 @param connect_timeout Ignored.
475 @param alive_interval Ignored.
476 @param alive_count_max Ignored.
477 @param connection_attempts Ignored.
478
479 @returns: An ssh command with the requested settings.
480
481 """
482 options = ' '.join([opts, '-o Protocol=2'])
483 return super(BaseServoHost, self).make_ssh_command(
484 user=user, port=port, opts=options, hosts_file='/dev/null',
485 connect_timeout=30, alive_interval=180, alive_count_max=3,
486 connection_attempts=4)
487
488
489 def _make_scp_cmd(self, sources, dest):
490 """Format scp command.
491
492 Given a list of source paths and a destination path, produces the
493 appropriate scp command for encoding it. Remote paths must be
494 pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost
495 to allow additional ssh options.
496
497 @param sources: A list of source paths to copy from.
498 @param dest: Destination path to copy to.
499
500 @returns: An scp command that copies |sources| on local machine to
501 |dest| on the remote servo host.
502
503 """
504 command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no '
505 '-o UserKnownHostsFile=/dev/null -P %d %s "%s"')
Derek Beckett4d102242020-12-01 14:24:37 -0800506 return command % (self._main_ssh.ssh_option,
Garry Wangebc015b2019-06-06 17:45:06 -0700507 self.port, sources, dest)
508
509
510 def run(self, command, timeout=3600, ignore_status=False,
511 stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
512 connect_timeout=30, ssh_failure_retry_ok=False,
513 options='', stdin=None, verbose=True, args=()):
514 """Run a command on the servo host.
515
516 Extends method `run` in SSHHost. If the servo host is a remote device,
517 it will call `run` in SSHost without changing anything.
518 If the servo host is 'localhost', it will call utils.system_output.
519
520 @param command: The command line string.
521 @param timeout: Time limit in seconds before attempting to
522 kill the running process. The run() function
523 will take a few seconds longer than 'timeout'
524 to complete if it has to kill the process.
525 @param ignore_status: Do not raise an exception, no matter
526 what the exit code of the command is.
527 @param stdout_tee/stderr_tee: Where to tee the stdout/stderr.
528 @param connect_timeout: SSH connection timeout (in seconds)
529 Ignored if host is 'localhost'.
530 @param options: String with additional ssh command options
531 Ignored if host is 'localhost'.
532 @param ssh_failure_retry_ok: when True and ssh connection failure is
533 suspected, OK to retry command (but not
534 compulsory, and likely not needed here)
535 @param stdin: Stdin to pass (a string) to the executed command.
536 @param verbose: Log the commands.
537 @param args: Sequence of strings to pass as arguments to command by
538 quoting them in " and escaping their contents if necessary.
539
540 @returns: A utils.CmdResult object.
541
542 @raises AutoservRunError if the command failed.
543 @raises AutoservSSHTimeout SSH connection has timed out. Only applies
544 when servo host is not 'localhost'.
545
546 """
Gregory Nisbet32e74022020-07-14 18:42:30 -0700547 run_args = {
548 'command' : command,
549 'timeout' : timeout,
550 'ignore_status' : ignore_status,
551 'stdout_tee' : stdout_tee,
552 'stderr_tee' : stderr_tee,
553 # connect_timeout n/a for localhost
554 # options n/a for localhost
Andrew McRaeed8b52f2020-07-20 11:29:26 +1000555 # ssh_failure_retry_ok n/a for localhost
Gregory Nisbet32e74022020-07-14 18:42:30 -0700556 'stdin' : stdin,
557 'verbose' : verbose,
558 'args' : args,
559 }
Garry Wangebc015b2019-06-06 17:45:06 -0700560 if self.is_localhost():
561 if self._sudo_required:
562 run_args['command'] = 'sudo -n sh -c "%s"' % utils.sh_escape(
563 command)
564 try:
565 return utils.run(**run_args)
566 except error.CmdError as e:
567 logging.error(e)
568 raise error.AutoservRunError('command execution error',
569 e.result_obj)
570 else:
571 run_args['connect_timeout'] = connect_timeout
572 run_args['options'] = options
Andrew McRaeed8b52f2020-07-20 11:29:26 +1000573 run_args['ssh_failure_retry_ok'] = ssh_failure_retry_ok
Garry Wangebc015b2019-06-06 17:45:06 -0700574 return super(BaseServoHost, self).run(**run_args)
Garry Wang2b5eef92020-08-21 16:23:35 -0700575
576 def _mount_drive(self, src_path, dst_path):
577 """Mount an external drive on servohost.
578
579 @param: src_path the drive path to mount(e.g. /dev/sda3).
580 @param: dst_path the destination directory on servohost to mount
581 the drive.
582
583 @returns: True if mount success otherwise False.
584 """
585 # Make sure the dst dir exists.
586 self.run('mkdir -p %s' % dst_path)
587
588 result = self.run('mount -o ro %s %s' % (src_path, dst_path),
589 ignore_status=True)
590 return result.exit_status == 0
591
592 def _unmount_drive(self, mount_path):
593 """Unmount a drive from servohost.
594
595 @param: mount_path the path on servohost to unmount.
596
597 @returns: True if unmount success otherwise False.
598 """
599 result = self.run('umount %s' % mount_path, ignore_status=True)
600 return result.exit_status == 0
Garry Wang78ce64d2020-10-13 18:23:45 -0700601
602 def wait_ready(self, required_uptime=300):
603 """Wait ready for a servohost if it has been rebooted recently.
604
605 It may take a few minutes until all servos and their componments
606 re-enumerated and become ready after a servohost(especially labstation
607 as it supports multiple servos) reboot, so we need to make sure the
608 servohost has been up for a given a mount of time before trying to
609 start any actions.
610
611 @param required_uptime: Minimum uptime in seconds that we can
612 consdier a servohost be ready.
613 """
614 uptime = float(self.check_uptime())
615 # To prevent unexpected output from check_uptime() that causes long
616 # sleep, make sure the maximum wait time <= required_uptime.
617 diff = min(required_uptime - uptime, required_uptime)
618 if diff > 0:
619 logging.info(
620 'The servohost was just rebooted, wait %s'
621 ' seconds for it to become ready', diff)
622 time.sleep(diff)