blob: fe60562d3a6dc80436e1070505629182e091d398 [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)
Anhb4c21d82021-07-01 22:07:15 +000074
75 self._is_containerized_servod = self.hostname.endswith('docker_servod')
76
Andrew Luo4be621d2020-03-21 07:01:13 -070077 self._is_localhost = (self.hostname == 'localhost'
Jeremy Bettis2eefe932021-02-08 16:10:34 -070078 and servo_host_ssh_port is None)
Anhb4c21d82021-07-01 22:07:15 +000079 if self._is_localhost or self._is_containerized_servod:
Garry Wangebc015b2019-06-06 17:45:06 -070080 self._is_in_lab = False
81 elif is_in_lab is None:
82 self._is_in_lab = utils.host_is_in_lab_zone(self.hostname)
83 else:
84 self._is_in_lab = is_in_lab
85
86 # Commands on the servo host must be run by the superuser.
87 # Our account on a remote host is root, but if our target is
88 # localhost then we might be running unprivileged. If so,
89 # `sudo` will have to be added to the commands.
90 if self._is_localhost:
91 self._sudo_required = utils.system_output('id -u') != '0'
92 else:
93 self._sudo_required = False
94
95 self._is_labstation = None
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -080096 self._dut_host_info = None
Otabek Kasimov2b50cdb2020-07-06 19:16:06 -070097 self._dut_hostname = None
Garry Wangebc015b2019-06-06 17:45:06 -070098
99
100 def get_board(self):
101 """Determine the board for this servo host. E.g. fizz-labstation
102
Garry Wang5e118c02019-09-25 14:24:57 -0700103 @returns a string representing this labstation's board or None if
104 target host is not using a ChromeOS image(e.g. test in chroot).
Garry Wangebc015b2019-06-06 17:45:06 -0700105 """
Garry Wang5e118c02019-09-25 14:24:57 -0700106 output = self.run('cat /etc/lsb-release', ignore_status=True).stdout
107 return lsbrelease_utils.get_current_board(lsb_release_content=output)
Garry Wangebc015b2019-06-06 17:45:06 -0700108
109
Garry Wangd7367482020-02-27 13:52:40 -0800110 def set_dut_host_info(self, dut_host_info):
111 """
112 @param dut_host_info: A HostInfo object.
113 """
114 logging.info('setting dut_host_info field to (%s)', dut_host_info)
115 self._dut_host_info = dut_host_info
116
117
118 def get_dut_host_info(self):
119 """
120 @return A HostInfo object.
121 """
122 return self._dut_host_info
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -0800123
124
Otabek Kasimov2b50cdb2020-07-06 19:16:06 -0700125 def set_dut_hostname(self, dut_hostname):
126 """
127 @param dut_hostname: hostname of the DUT that connected to this servo.
128 """
129 logging.info('setting dut_hostname as (%s)', dut_hostname)
130 self._dut_hostname = dut_hostname
131
132
133 def get_dut_hostname(self):
134 """
135 @returns hostname of the DUT that connected to this servo.
136 """
137 return self._dut_hostname
138
139
Garry Wangebc015b2019-06-06 17:45:06 -0700140 def is_labstation(self):
141 """Determine if the host is a labstation
142
143 @returns True if ths host is a labstation otherwise False.
144 """
145 if self._is_labstation is None:
146 board = self.get_board()
Garry Wang88dc8632019-07-24 16:53:50 -0700147 self._is_labstation = board is not None and 'labstation' in board
Garry Wangebc015b2019-06-06 17:45:06 -0700148
149 return self._is_labstation
150
151
Garry Wang14831832020-03-04 17:21:49 -0800152 def _get_lsb_release_content(self):
153 """Return the content of lsb-release file of host."""
154 return self.run(
155 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
156
157
158 def get_release_version(self):
Garry Wangebc015b2019-06-06 17:45:06 -0700159 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
160
161 @returns The version string in lsb-release, under attribute
Garry Wang14831832020-03-04 17:21:49 -0800162 CHROMEOS_RELEASE_VERSION(e.g. 12900.0.0). None on fail.
Garry Wangebc015b2019-06-06 17:45:06 -0700163 """
Garry Wangebc015b2019-06-06 17:45:06 -0700164 return lsbrelease_utils.get_chromeos_release_version(
Garry Wang14831832020-03-04 17:21:49 -0800165 lsb_release_content=self._get_lsb_release_content()
166 )
167
168
169 def get_full_release_path(self):
170 """Get full release path from servohost as string.
171
172 @returns full release path as a string
173 (e.g. fizz-labstation-release/R82.12900.0.0). None on fail.
174 """
175 return lsbrelease_utils.get_chromeos_release_builder_path(
176 lsb_release_content=self._get_lsb_release_content()
177 )
Garry Wangebc015b2019-06-06 17:45:06 -0700178
179
180 def _check_update_status(self):
Garry Wang358aad42020-08-02 20:56:04 -0700181 """ Check servohost's current update state.
182
183 @returns: one of below state of from self.UPDATE_STATE
184 IDLE -- if the target host is not currently updating and not
185 pending on a reboot.
186 RUNNING -- if there is another updating process that running on
187 target host(note: we don't expect to hit this scenario).
188 PENDING_REBOOT -- if the target host had an update and pending
189 on reboot.
190 """
191 result = self.run('pgrep -f quick-provision | grep -v $$',
192 ignore_status=True)
193 # We don't expect any output unless there are another quick
194 # provision process is running.
195 if result.exit_status == 0:
196 return self.UPDATE_STATE.RUNNING
197
198 # Determine if we have an update that pending on reboot by check if
199 # the current inactive kernel has priority for the next boot.
200 try:
201 inactive_kernel = kernel_utils.get_kernel_state(self)[1]
202 next_kernel = kernel_utils.get_next_kernel(self)
203 if inactive_kernel == next_kernel:
204 return self.UPDATE_STATE.PENDING_REBOOT
205 except Exception as e:
206 logging.error('Unexpected error while checking kernel info; %s', e)
207 return self.UPDATE_STATE.IDLE
Garry Wangebc015b2019-06-06 17:45:06 -0700208
209
210 def is_in_lab(self):
211 """Check whether the servo host is a lab device.
212
213 @returns: True if the servo host is in Cros Lab, otherwise False.
214
215 """
216 return self._is_in_lab
217
218
219 def is_localhost(self):
220 """Checks whether the servo host points to localhost.
221
222 @returns: True if it points to localhost, otherwise False.
223
224 """
225 return self._is_localhost
226
227
Anhb4c21d82021-07-01 22:07:15 +0000228 def is_containerized_servod(self):
229 """Checks whether the servo host is a containerized servod.
230
231 @returns: True if using containerized servod, otherwise False.
232
233 """
234 return self._is_containerized_servod
235
Garry Wangebc015b2019-06-06 17:45:06 -0700236 def is_cros_host(self):
237 """Check if a servo host is running chromeos.
238
239 @return: True if the servo host is running chromeos.
240 False if it isn't, or we don't have enough information.
241 """
242 try:
243 result = self.run('grep -q CHROMEOS /etc/lsb-release',
244 ignore_status=True, timeout=10)
245 except (error.AutoservRunError, error.AutoservSSHTimeout):
246 return False
247 return result.exit_status == 0
248
249
Garry Wang358aad42020-08-02 20:56:04 -0700250 def prepare_for_update(self):
251 """Prepares the DUT for an update.
252 Subclasses may override this to perform any special actions
253 required before updating.
254 """
255 pass
256
257
Garry Wangebc015b2019-06-06 17:45:06 -0700258 def reboot(self, *args, **dargs):
259 """Reboot using special servo host reboot command."""
260 super(BaseServoHost, self).reboot(reboot_cmd=self.REBOOT_CMD,
261 *args, **dargs)
262
263
Garry Wang358aad42020-08-02 20:56:04 -0700264 def update_image(self, stable_version=None):
Garry Wangebc015b2019-06-06 17:45:06 -0700265 """Update the image on the servo host, if needed.
266
267 This method recognizes the following cases:
268 * If the Host is not running Chrome OS, do nothing.
269 * If a previously triggered update is now complete, reboot
270 to the new version.
Garry Wang358aad42020-08-02 20:56:04 -0700271 * If the host is processing an update do nothing.
272 * If the host has an update that pending on reboot, do nothing.
Garry Wangebc015b2019-06-06 17:45:06 -0700273 * If the host is running a version of Chrome OS different
Garry Wang358aad42020-08-02 20:56:04 -0700274 from the default for servo Hosts, start an update.
Garry Wangebc015b2019-06-06 17:45:06 -0700275
Garry Wang14831832020-03-04 17:21:49 -0800276 @stable_version the target build number.(e.g. R82-12900.0.0)
277
Garry Wangebc015b2019-06-06 17:45:06 -0700278 @raises dev_server.DevServerException: If all the devservers are down.
279 @raises site_utils.ParseBuildNameException: If the devserver returns
280 an invalid build name.
Garry Wangebc015b2019-06-06 17:45:06 -0700281 """
282 # servod could be running in a Ubuntu workstation.
283 if not self.is_cros_host():
284 logging.info('Not attempting an update, either %s is not running '
285 'chromeos or we cannot find enough information about '
286 'the host.', self.hostname)
287 return
288
289 if lsbrelease_utils.is_moblab():
290 logging.info('Not attempting an update, %s is running moblab.',
291 self.hostname)
292 return
293
Garry Wang14831832020-03-04 17:21:49 -0800294 if not stable_version:
295 logging.debug("BaseServoHost::update_image attempting to get"
296 " servo cros stable version")
297 try:
298 stable_version = (self.get_dut_host_info().
299 servo_cros_stable_version)
300 except AttributeError:
301 logging.error("BaseServoHost::update_image failed to get"
302 " servo cros stable version.")
Gregory Nisbet8e2fbb22019-12-05 11:36:37 -0800303
Garry Wang14831832020-03-04 17:21:49 -0800304 target_build = "%s-release/%s" % (self.get_board(), stable_version)
Garry Wangebc015b2019-06-06 17:45:06 -0700305 target_build_number = server_utils.ParseBuildName(
306 target_build)[3]
Garry Wang14831832020-03-04 17:21:49 -0800307 current_build_number = self.get_release_version()
Garry Wangebc015b2019-06-06 17:45:06 -0700308
309 if current_build_number == target_build_number:
310 logging.info('servo host %s does not require an update.',
311 self.hostname)
312 return
313
314 status = self._check_update_status()
Garry Wang358aad42020-08-02 20:56:04 -0700315 if status == self.UPDATE_STATE.RUNNING:
316 logging.info('servo host %s already processing an update',
317 self.hostname)
318 return
319 if status == self.UPDATE_STATE.PENDING_REBOOT:
Garry Wangebc015b2019-06-06 17:45:06 -0700320 # Labstation reboot is handled separately here as it require
Garry Wang358aad42020-08-02 20:56:04 -0700321 # synchronized reboot among all managed DUTs. For servo_v3, we'll
322 # reboot when initialize Servohost, if there is a update pending.
323 logging.info('An update has been completed and pending reboot.')
324 return
Garry Wangebc015b2019-06-06 17:45:06 -0700325
Garry Wang358aad42020-08-02 20:56:04 -0700326 ds = dev_server.ImageServer.resolve(self.hostname,
327 hostname=self.hostname)
328 url = ds.get_update_url(target_build)
Jae Hoon Kim5f6ca6e2020-09-10 16:11:23 -0700329 cros_provisioner = provisioner.ChromiumOSProvisioner(update_url=url,
Jae Hoon Kim3f004992020-09-10 17:48:33 -0700330 host=self,
331 is_servohost=True)
Garry Wang358aad42020-08-02 20:56:04 -0700332 logging.info('Using devserver url: %s to trigger update on '
333 'servo host %s, from %s to %s', url, self.hostname,
334 current_build_number, target_build_number)
Jae Hoon Kim5f6ca6e2020-09-10 16:11:23 -0700335 cros_provisioner.run_provision()
Garry Wangebc015b2019-06-06 17:45:06 -0700336
337
338 def has_power(self):
339 """Return whether or not the servo host is powered by PoE or RPM."""
340 # TODO(fdeng): See crbug.com/302791
341 # For now, assume all servo hosts in the lab have power.
342 return self.is_in_lab()
343
344
Garry Wang358aad42020-08-02 20:56:04 -0700345 def _post_update_reboot(self):
346 """ Reboot servohost after an quick provision.
347
348 We need to do some specifal cleanup before and after reboot
349 when there is an update pending.
350 """
351 # Regarding the 'crossystem' command below: In some cases,
352 # the update flow puts the TPM into a state such that it
353 # fails verification. We don't know why. However, this
354 # call papers over the problem by clearing the TPM during
355 # the reboot.
356 #
357 # We ignore failures from 'crossystem'. Although failure
358 # here is unexpected, and could signal a bug, the point of
359 # the exercise is to paper over problems; allowing this to
360 # fail would defeat the purpose.
Garry Wang9ff569f2020-10-20 19:11:30 -0700361
362 # Preserve critical files before reboot since post-provision
363 # clobbering will wipe the stateful partition.
364 # TODO(xianuowang@) Remove this logic once we have updated to
365 # a image with https://crrev.com/c/2485908.
366 path_to_preserve = [
367 '/var/lib/servod',
368 '/var/lib/device_health_profile',
369 ]
370 safe_location = '/mnt/stateful_partition/unencrypted/preserve/'
371 for item in path_to_preserve:
372 dest = os.path.join(safe_location, item.split('/')[-1])
373 self.run('rm -rf %s' % dest, ignore_status=True)
374 self.run('mv %s %s' % (item, safe_location), ignore_status=True)
375
Garry Wang358aad42020-08-02 20:56:04 -0700376 self.run('crossystem clear_tpm_owner_request=1', ignore_status=True)
377 self._servo_host_reboot()
378 logging.debug('Cleaning up autotest directories if exist.')
379 try:
380 installed_autodir = autotest.Autotest.get_installed_autodir(self)
381 self.run('rm -rf ' + installed_autodir)
382 except autotest.AutodirNotFoundError:
383 logging.debug('No autotest installed directory found.')
384
Garry Wang9ff569f2020-10-20 19:11:30 -0700385 # Recover preserved files to original location.
386 # TODO(xianuowang@) Remove this logic once we have updated to
387 # a image with https://crrev.com/c/2485908.
388 for item in path_to_preserve:
389 src = os.path.join(safe_location, item.split('/')[-1])
390 dest = '/'.join(item.split('/')[:-1])
391 self.run('mv %s %s' % (src, dest), ignore_status=True)
Garry Wang358aad42020-08-02 20:56:04 -0700392
Garry Wangebc015b2019-06-06 17:45:06 -0700393 def power_cycle(self):
394 """Cycle power to this host via PoE(servo v3) or RPM(labstation)
395 if it is a lab device.
396
397 @raises AutoservRepairError if it fails to power cycle the
398 servo host.
399
400 """
401 if self.has_power():
402 try:
403 rpm_client.set_power(self, 'CYCLE')
Derek Beckettf73baca2020-08-19 15:08:47 -0700404 except (socket.error, six.moves.xmlrpc_client.Error,
405 six.moves.http_client.BadStatusLine,
Garry Wangebc015b2019-06-06 17:45:06 -0700406 rpm_client.RemotePowerException) as e:
407 raise hosts.AutoservRepairError(
408 'Power cycling %s failed: %s' % (self.hostname, e),
409 'power_cycle_via_rpm_failed'
410 )
411 else:
412 logging.info('Skipping power cycling, not a lab device.')
413
414
415 def _servo_host_reboot(self):
416 """Reboot this servo host because a reboot is requested."""
Otabek Kasimovb676b072020-12-02 10:50:08 -0800417 try:
418 # TODO(otabek) remove if found the fix for b/174514811
419 # The default factory firmware remember the latest chromeboxes
420 # status after power off. If box was in sleep mode before the
421 # break, the box will stay at sleep mode after power on.
422 # Disable power manager has make chromebox to boot always when
423 # we deliver the power to the device.
424 logging.info('Stoping powerd service on device')
425 self.run('stop powerd', ignore_status=True, timeout=30)
426 except Exception as e:
427 logging.debug('(Not critical) Fail to stop powerd; %s', e)
428
Garry Wangebc015b2019-06-06 17:45:06 -0700429 logging.info('Rebooting servo host %s from build %s', self.hostname,
Garry Wang14831832020-03-04 17:21:49 -0800430 self.get_release_version())
Garry Wangebc015b2019-06-06 17:45:06 -0700431 # Tell the reboot() call not to wait for completion.
432 # Otherwise, the call will log reboot failure if servo does
433 # not come back. The logged reboot failure will lead to
434 # test job failure. If the test does not require servo, we
435 # don't want servo failure to fail the test with error:
436 # `Host did not return from reboot` in status.log.
437 self.reboot(fastsync=True, wait=False)
438
439 # We told the reboot() call not to wait, but we need to wait
440 # for the reboot before we continue. Alas. The code from
441 # here below is basically a copy of Host.wait_for_restart(),
442 # with the logging bits ripped out, so that they can't cause
443 # the failure logging problem described above.
444 #
Derek Beckett4d102242020-12-01 14:24:37 -0800445 # The stain that this has left on my soul can never be
Garry Wangebc015b2019-06-06 17:45:06 -0700446 # erased.
447 old_boot_id = self.get_boot_id()
448 if not self.wait_down(timeout=self.WAIT_DOWN_REBOOT_TIMEOUT,
449 warning_timer=self.WAIT_DOWN_REBOOT_WARNING,
450 old_boot_id=old_boot_id):
451 raise error.AutoservHostError(
452 'servo host %s failed to shut down.' %
453 self.hostname)
Garry Wang79e9af62019-06-12 15:19:19 -0700454 if self.wait_up(timeout=self.REBOOT_TIMEOUT):
Garry Wangebc015b2019-06-06 17:45:06 -0700455 logging.info('servo host %s back from reboot, with build %s',
Garry Wang14831832020-03-04 17:21:49 -0800456 self.hostname, self.get_release_version())
Garry Wangebc015b2019-06-06 17:45:06 -0700457 else:
458 raise error.AutoservHostError(
459 'servo host %s failed to come back from reboot.' %
460 self.hostname)
461
462
463 def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
464 connect_timeout=None, alive_interval=None, alive_count_max=None,
465 connection_attempts=None):
466 """Override default make_ssh_command to use tuned options.
467
468 Tuning changes:
469 - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
470 connection failure. Consistency with remote_access.py.
471
472 - ServerAliveInterval=180; which causes SSH to ping connection every
473 180 seconds. In conjunction with ServerAliveCountMax ensures
474 that if the connection dies, Autotest will bail out quickly.
475
476 - ServerAliveCountMax=3; consistency with remote_access.py.
477
478 - ConnectAttempts=4; reduce flakiness in connection errors;
479 consistency with remote_access.py.
480
481 - UserKnownHostsFile=/dev/null; we don't care about the keys.
482
483 - SSH protocol forced to 2; needed for ServerAliveInterval.
484
485 @param user User name to use for the ssh connection.
486 @param port Port on the target host to use for ssh connection.
487 @param opts Additional options to the ssh command.
488 @param hosts_file Ignored.
489 @param connect_timeout Ignored.
490 @param alive_interval Ignored.
491 @param alive_count_max Ignored.
492 @param connection_attempts Ignored.
493
494 @returns: An ssh command with the requested settings.
495
496 """
497 options = ' '.join([opts, '-o Protocol=2'])
498 return super(BaseServoHost, self).make_ssh_command(
499 user=user, port=port, opts=options, hosts_file='/dev/null',
500 connect_timeout=30, alive_interval=180, alive_count_max=3,
501 connection_attempts=4)
502
503
504 def _make_scp_cmd(self, sources, dest):
505 """Format scp command.
506
507 Given a list of source paths and a destination path, produces the
508 appropriate scp command for encoding it. Remote paths must be
509 pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost
510 to allow additional ssh options.
511
512 @param sources: A list of source paths to copy from.
513 @param dest: Destination path to copy to.
514
515 @returns: An scp command that copies |sources| on local machine to
516 |dest| on the remote servo host.
517
518 """
519 command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no '
520 '-o UserKnownHostsFile=/dev/null -P %d %s "%s"')
Derek Beckett4d102242020-12-01 14:24:37 -0800521 return command % (self._main_ssh.ssh_option,
Garry Wangebc015b2019-06-06 17:45:06 -0700522 self.port, sources, dest)
523
524
525 def run(self, command, timeout=3600, ignore_status=False,
526 stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
527 connect_timeout=30, ssh_failure_retry_ok=False,
528 options='', stdin=None, verbose=True, args=()):
529 """Run a command on the servo host.
530
531 Extends method `run` in SSHHost. If the servo host is a remote device,
532 it will call `run` in SSHost without changing anything.
533 If the servo host is 'localhost', it will call utils.system_output.
534
535 @param command: The command line string.
536 @param timeout: Time limit in seconds before attempting to
537 kill the running process. The run() function
538 will take a few seconds longer than 'timeout'
539 to complete if it has to kill the process.
540 @param ignore_status: Do not raise an exception, no matter
541 what the exit code of the command is.
542 @param stdout_tee/stderr_tee: Where to tee the stdout/stderr.
543 @param connect_timeout: SSH connection timeout (in seconds)
544 Ignored if host is 'localhost'.
545 @param options: String with additional ssh command options
546 Ignored if host is 'localhost'.
547 @param ssh_failure_retry_ok: when True and ssh connection failure is
548 suspected, OK to retry command (but not
549 compulsory, and likely not needed here)
550 @param stdin: Stdin to pass (a string) to the executed command.
551 @param verbose: Log the commands.
552 @param args: Sequence of strings to pass as arguments to command by
553 quoting them in " and escaping their contents if necessary.
554
555 @returns: A utils.CmdResult object.
556
557 @raises AutoservRunError if the command failed.
558 @raises AutoservSSHTimeout SSH connection has timed out. Only applies
559 when servo host is not 'localhost'.
560
561 """
Gregory Nisbet32e74022020-07-14 18:42:30 -0700562 run_args = {
563 'command' : command,
564 'timeout' : timeout,
565 'ignore_status' : ignore_status,
566 'stdout_tee' : stdout_tee,
567 'stderr_tee' : stderr_tee,
568 # connect_timeout n/a for localhost
569 # options n/a for localhost
Andrew McRaeed8b52f2020-07-20 11:29:26 +1000570 # ssh_failure_retry_ok n/a for localhost
Gregory Nisbet32e74022020-07-14 18:42:30 -0700571 'stdin' : stdin,
572 'verbose' : verbose,
573 'args' : args,
574 }
Garry Wangebc015b2019-06-06 17:45:06 -0700575 if self.is_localhost():
576 if self._sudo_required:
577 run_args['command'] = 'sudo -n sh -c "%s"' % utils.sh_escape(
578 command)
579 try:
580 return utils.run(**run_args)
581 except error.CmdError as e:
582 logging.error(e)
583 raise error.AutoservRunError('command execution error',
584 e.result_obj)
585 else:
586 run_args['connect_timeout'] = connect_timeout
587 run_args['options'] = options
Andrew McRaeed8b52f2020-07-20 11:29:26 +1000588 run_args['ssh_failure_retry_ok'] = ssh_failure_retry_ok
Garry Wangebc015b2019-06-06 17:45:06 -0700589 return super(BaseServoHost, self).run(**run_args)
Garry Wang2b5eef92020-08-21 16:23:35 -0700590
591 def _mount_drive(self, src_path, dst_path):
592 """Mount an external drive on servohost.
593
594 @param: src_path the drive path to mount(e.g. /dev/sda3).
595 @param: dst_path the destination directory on servohost to mount
596 the drive.
597
598 @returns: True if mount success otherwise False.
599 """
600 # Make sure the dst dir exists.
601 self.run('mkdir -p %s' % dst_path)
602
603 result = self.run('mount -o ro %s %s' % (src_path, dst_path),
604 ignore_status=True)
605 return result.exit_status == 0
606
607 def _unmount_drive(self, mount_path):
608 """Unmount a drive from servohost.
609
610 @param: mount_path the path on servohost to unmount.
611
612 @returns: True if unmount success otherwise False.
613 """
614 result = self.run('umount %s' % mount_path, ignore_status=True)
615 return result.exit_status == 0
Garry Wang78ce64d2020-10-13 18:23:45 -0700616
617 def wait_ready(self, required_uptime=300):
618 """Wait ready for a servohost if it has been rebooted recently.
619
620 It may take a few minutes until all servos and their componments
621 re-enumerated and become ready after a servohost(especially labstation
622 as it supports multiple servos) reboot, so we need to make sure the
623 servohost has been up for a given a mount of time before trying to
624 start any actions.
625
626 @param required_uptime: Minimum uptime in seconds that we can
627 consdier a servohost be ready.
628 """
629 uptime = float(self.check_uptime())
630 # To prevent unexpected output from check_uptime() that causes long
631 # sleep, make sure the maximum wait time <= required_uptime.
632 diff = min(required_uptime - uptime, required_uptime)
633 if diff > 0:
634 logging.info(
635 'The servohost was just rebooted, wait %s'
636 ' seconds for it to become ready', diff)
637 time.sleep(diff)