blob: 4c73461cd7e2a6fc0186df67d91f03fb4abd4968 [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
Jae Hoon Kim5f6ca6e2020-09-10 16:11:23 -070026from autotest_lib.server.cros import provisioner
Garry Wangebc015b2019-06-06 17:45:06 -070027from autotest_lib.server.hosts import ssh_host
Garry Wangebc015b2019-06-06 17:45:06 -070028
Garry Wangebc015b2019-06-06 17:45:06 -070029
30class BaseServoHost(ssh_host.SSHHost):
31 """Base host class for a host that manage servo(s).
32 E.g. beaglebone, labstation.
33 """
Garry Wang3d84a162020-01-24 13:29:43 +000034 REBOOT_CMD = 'sleep 5; reboot & sleep 10; reboot -f'
Garry Wangebc015b2019-06-06 17:45:06 -070035
Garry Wang79e9af62019-06-12 15:19:19 -070036 TEMP_FILE_DIR = '/var/lib/servod/'
37
38 LOCK_FILE_POSTFIX = '_in_use'
39 REBOOT_FILE_POSTFIX = '_reboot'
Garry Wangebc015b2019-06-06 17:45:06 -070040
Garry Wang5715ee52019-12-23 11:00:47 -080041 # Time to wait a rebooting servohost, in seconds.
Garry Wangfb253432019-09-11 17:08:38 -070042 REBOOT_TIMEOUT = 240
Garry Wangebc015b2019-06-06 17:45:06 -070043
Garry Wang5715ee52019-12-23 11:00:47 -080044 # Timeout value to power cycle a servohost, in seconds.
45 BOOT_TIMEOUT = 240
46
Garry Wang358aad42020-08-02 20:56:04 -070047 # Constants that reflect current host update state.
Derek Beckett15775132020-10-01 12:49:45 -070048 UPDATE_STATE = autotest_enum.AutotestEnum('IDLE', 'RUNNING',
49 'PENDING_REBOOT')
Garry Wangebc015b2019-06-06 17:45:06 -070050
Andrew Luo4be621d2020-03-21 07:01:13 -070051 def _initialize(self,
52 hostname,
53 is_in_lab=None,
54 servo_host_ssh_port=None,
55 *args,
56 **dargs):
Garry Wangebc015b2019-06-06 17:45:06 -070057 """Construct a BaseServoHost object.
58
59 @param is_in_lab: True if the servo host is in Cros Lab. Default is set
60 to None, for which utils.host_is_in_lab_zone will be
61 called to check if the servo host is in Cros lab.
62
63 """
Andrew Luo4be621d2020-03-21 07:01:13 -070064 if servo_host_ssh_port is not None:
65 dargs['port'] = int(servo_host_ssh_port)
66
Garry Wangebc015b2019-06-06 17:45:06 -070067 super(BaseServoHost, self)._initialize(hostname=hostname,
68 *args, **dargs)
Anhb4c21d82021-07-01 22:07:15 +000069
70 self._is_containerized_servod = self.hostname.endswith('docker_servod')
71
Andrew Luo4be621d2020-03-21 07:01:13 -070072 self._is_localhost = (self.hostname == 'localhost'
Jeremy Bettis2eefe932021-02-08 16:10:34 -070073 and servo_host_ssh_port is None)
Anhb4c21d82021-07-01 22:07:15 +000074 if self._is_localhost or self._is_containerized_servod:
Garry Wangebc015b2019-06-06 17:45:06 -070075 self._is_in_lab = False
76 elif is_in_lab is None:
77 self._is_in_lab = utils.host_is_in_lab_zone(self.hostname)
78 else:
79 self._is_in_lab = is_in_lab
80
81 # Commands on the servo host must be run by the superuser.
82 # Our account on a remote host is root, but if our target is
83 # localhost then we might be running unprivileged. If so,
84 # `sudo` will have to be added to the commands.
85 if self._is_localhost:
86 self._sudo_required = utils.system_output('id -u') != '0'
87 else:
88 self._sudo_required = False
89
90 self._is_labstation = None
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -080091 self._dut_host_info = None
Otabek Kasimov2b50cdb2020-07-06 19:16:06 -070092 self._dut_hostname = None
Garry Wangebc015b2019-06-06 17:45:06 -070093
94
95 def get_board(self):
96 """Determine the board for this servo host. E.g. fizz-labstation
97
Garry Wang5e118c02019-09-25 14:24:57 -070098 @returns a string representing this labstation's board or None if
99 target host is not using a ChromeOS image(e.g. test in chroot).
Garry Wangebc015b2019-06-06 17:45:06 -0700100 """
Garry Wang5e118c02019-09-25 14:24:57 -0700101 output = self.run('cat /etc/lsb-release', ignore_status=True).stdout
102 return lsbrelease_utils.get_current_board(lsb_release_content=output)
Garry Wangebc015b2019-06-06 17:45:06 -0700103
104
Garry Wangd7367482020-02-27 13:52:40 -0800105 def set_dut_host_info(self, dut_host_info):
106 """
107 @param dut_host_info: A HostInfo object.
108 """
109 logging.info('setting dut_host_info field to (%s)', dut_host_info)
110 self._dut_host_info = dut_host_info
111
112
113 def get_dut_host_info(self):
114 """
115 @return A HostInfo object.
116 """
117 return self._dut_host_info
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -0800118
119
Otabek Kasimov2b50cdb2020-07-06 19:16:06 -0700120 def set_dut_hostname(self, dut_hostname):
121 """
122 @param dut_hostname: hostname of the DUT that connected to this servo.
123 """
124 logging.info('setting dut_hostname as (%s)', dut_hostname)
125 self._dut_hostname = dut_hostname
126
127
128 def get_dut_hostname(self):
129 """
130 @returns hostname of the DUT that connected to this servo.
131 """
132 return self._dut_hostname
133
134
Garry Wangebc015b2019-06-06 17:45:06 -0700135 def is_labstation(self):
136 """Determine if the host is a labstation
137
138 @returns True if ths host is a labstation otherwise False.
139 """
140 if self._is_labstation is None:
141 board = self.get_board()
Garry Wang88dc8632019-07-24 16:53:50 -0700142 self._is_labstation = board is not None and 'labstation' in board
Garry Wangebc015b2019-06-06 17:45:06 -0700143
144 return self._is_labstation
145
146
Garry Wang14831832020-03-04 17:21:49 -0800147 def _get_lsb_release_content(self):
148 """Return the content of lsb-release file of host."""
149 return self.run(
150 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
151
152
153 def get_release_version(self):
Garry Wangebc015b2019-06-06 17:45:06 -0700154 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
155
156 @returns The version string in lsb-release, under attribute
Garry Wang14831832020-03-04 17:21:49 -0800157 CHROMEOS_RELEASE_VERSION(e.g. 12900.0.0). None on fail.
Garry Wangebc015b2019-06-06 17:45:06 -0700158 """
Garry Wangebc015b2019-06-06 17:45:06 -0700159 return lsbrelease_utils.get_chromeos_release_version(
Garry Wang14831832020-03-04 17:21:49 -0800160 lsb_release_content=self._get_lsb_release_content()
161 )
162
163
164 def get_full_release_path(self):
165 """Get full release path from servohost as string.
166
167 @returns full release path as a string
168 (e.g. fizz-labstation-release/R82.12900.0.0). None on fail.
169 """
170 return lsbrelease_utils.get_chromeos_release_builder_path(
171 lsb_release_content=self._get_lsb_release_content()
172 )
Garry Wangebc015b2019-06-06 17:45:06 -0700173
174
175 def _check_update_status(self):
Garry Wang358aad42020-08-02 20:56:04 -0700176 """ Check servohost's current update state.
177
178 @returns: one of below state of from self.UPDATE_STATE
179 IDLE -- if the target host is not currently updating and not
180 pending on a reboot.
181 RUNNING -- if there is another updating process that running on
182 target host(note: we don't expect to hit this scenario).
183 PENDING_REBOOT -- if the target host had an update and pending
184 on reboot.
185 """
186 result = self.run('pgrep -f quick-provision | grep -v $$',
187 ignore_status=True)
188 # We don't expect any output unless there are another quick
189 # provision process is running.
190 if result.exit_status == 0:
191 return self.UPDATE_STATE.RUNNING
192
193 # Determine if we have an update that pending on reboot by check if
194 # the current inactive kernel has priority for the next boot.
195 try:
196 inactive_kernel = kernel_utils.get_kernel_state(self)[1]
197 next_kernel = kernel_utils.get_next_kernel(self)
198 if inactive_kernel == next_kernel:
199 return self.UPDATE_STATE.PENDING_REBOOT
200 except Exception as e:
201 logging.error('Unexpected error while checking kernel info; %s', e)
202 return self.UPDATE_STATE.IDLE
Garry Wangebc015b2019-06-06 17:45:06 -0700203
204
205 def is_in_lab(self):
206 """Check whether the servo host is a lab device.
207
208 @returns: True if the servo host is in Cros Lab, otherwise False.
209
210 """
211 return self._is_in_lab
212
213
214 def is_localhost(self):
215 """Checks whether the servo host points to localhost.
216
217 @returns: True if it points to localhost, otherwise False.
218
219 """
220 return self._is_localhost
221
222
Anhb4c21d82021-07-01 22:07:15 +0000223 def is_containerized_servod(self):
224 """Checks whether the servo host is a containerized servod.
225
226 @returns: True if using containerized servod, otherwise False.
227
228 """
229 return self._is_containerized_servod
230
Garry Wangebc015b2019-06-06 17:45:06 -0700231 def is_cros_host(self):
232 """Check if a servo host is running chromeos.
233
234 @return: True if the servo host is running chromeos.
235 False if it isn't, or we don't have enough information.
236 """
237 try:
238 result = self.run('grep -q CHROMEOS /etc/lsb-release',
239 ignore_status=True, timeout=10)
240 except (error.AutoservRunError, error.AutoservSSHTimeout):
241 return False
242 return result.exit_status == 0
243
244
Garry Wang358aad42020-08-02 20:56:04 -0700245 def prepare_for_update(self):
246 """Prepares the DUT for an update.
247 Subclasses may override this to perform any special actions
248 required before updating.
249 """
250 pass
251
252
Garry Wangebc015b2019-06-06 17:45:06 -0700253 def reboot(self, *args, **dargs):
254 """Reboot using special servo host reboot command."""
255 super(BaseServoHost, self).reboot(reboot_cmd=self.REBOOT_CMD,
256 *args, **dargs)
257
258
Garry Wang358aad42020-08-02 20:56:04 -0700259 def update_image(self, stable_version=None):
Garry Wangebc015b2019-06-06 17:45:06 -0700260 """Update the image on the servo host, if needed.
261
262 This method recognizes the following cases:
263 * If the Host is not running Chrome OS, do nothing.
264 * If a previously triggered update is now complete, reboot
265 to the new version.
Garry Wang358aad42020-08-02 20:56:04 -0700266 * If the host is processing an update do nothing.
267 * If the host has an update that pending on reboot, do nothing.
Garry Wangebc015b2019-06-06 17:45:06 -0700268 * If the host is running a version of Chrome OS different
Garry Wang358aad42020-08-02 20:56:04 -0700269 from the default for servo Hosts, start an update.
Garry Wangebc015b2019-06-06 17:45:06 -0700270
Garry Wang14831832020-03-04 17:21:49 -0800271 @stable_version the target build number.(e.g. R82-12900.0.0)
272
Garry Wangebc015b2019-06-06 17:45:06 -0700273 @raises dev_server.DevServerException: If all the devservers are down.
274 @raises site_utils.ParseBuildNameException: If the devserver returns
275 an invalid build name.
Garry Wangebc015b2019-06-06 17:45:06 -0700276 """
277 # servod could be running in a Ubuntu workstation.
278 if not self.is_cros_host():
279 logging.info('Not attempting an update, either %s is not running '
280 'chromeos or we cannot find enough information about '
281 'the host.', self.hostname)
282 return
283
284 if lsbrelease_utils.is_moblab():
285 logging.info('Not attempting an update, %s is running moblab.',
286 self.hostname)
287 return
288
Garry Wang14831832020-03-04 17:21:49 -0800289 if not stable_version:
290 logging.debug("BaseServoHost::update_image attempting to get"
291 " servo cros stable version")
292 try:
293 stable_version = (self.get_dut_host_info().
294 servo_cros_stable_version)
295 except AttributeError:
296 logging.error("BaseServoHost::update_image failed to get"
297 " servo cros stable version.")
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -0800298
Garry Wang14831832020-03-04 17:21:49 -0800299 target_build = "%s-release/%s" % (self.get_board(), stable_version)
Garry Wangebc015b2019-06-06 17:45:06 -0700300 target_build_number = server_utils.ParseBuildName(
301 target_build)[3]
Garry Wang14831832020-03-04 17:21:49 -0800302 current_build_number = self.get_release_version()
Garry Wangebc015b2019-06-06 17:45:06 -0700303
304 if current_build_number == target_build_number:
305 logging.info('servo host %s does not require an update.',
306 self.hostname)
307 return
308
309 status = self._check_update_status()
Garry Wang358aad42020-08-02 20:56:04 -0700310 if status == self.UPDATE_STATE.RUNNING:
311 logging.info('servo host %s already processing an update',
312 self.hostname)
313 return
314 if status == self.UPDATE_STATE.PENDING_REBOOT:
Garry Wangebc015b2019-06-06 17:45:06 -0700315 # Labstation reboot is handled separately here as it require
Garry Wang358aad42020-08-02 20:56:04 -0700316 # synchronized reboot among all managed DUTs. For servo_v3, we'll
317 # reboot when initialize Servohost, if there is a update pending.
318 logging.info('An update has been completed and pending reboot.')
319 return
Garry Wangebc015b2019-06-06 17:45:06 -0700320
Garry Wang358aad42020-08-02 20:56:04 -0700321 ds = dev_server.ImageServer.resolve(self.hostname,
322 hostname=self.hostname)
323 url = ds.get_update_url(target_build)
Jae Hoon Kim5f6ca6e2020-09-10 16:11:23 -0700324 cros_provisioner = provisioner.ChromiumOSProvisioner(update_url=url,
Jae Hoon Kim3f004992020-09-10 17:48:33 -0700325 host=self,
326 is_servohost=True)
Garry Wang358aad42020-08-02 20:56:04 -0700327 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)
Jae Hoon Kim5f6ca6e2020-09-10 16:11:23 -0700330 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)