blob: 37b942e8f0e3b1c64beed6dda2deb3bbc46a710a [file] [log] [blame]
Fang Deng5d518f42013-08-02 14:04:32 -07001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4#
5# Expects to be run in an environment with sudo and no interactive password
6# prompt, such as within the Chromium OS development chroot.
7
8
9"""This file provides core logic for servo verify/repair process."""
10
11
12import httplib
13import logging
14import socket
Fang Deng5d518f42013-08-02 14:04:32 -070015import xmlrpclib
Raul E Rangel52ca2e82018-07-03 14:10:14 -060016import os
Fang Deng5d518f42013-08-02 14:04:32 -070017
18from autotest_lib.client.bin import utils
19from autotest_lib.client.common_lib import error
beeps5e8c45a2013-12-17 22:05:11 -080020from autotest_lib.client.common_lib import global_config
Richard Barnette9a26ad62016-06-10 12:03:08 -070021from autotest_lib.client.common_lib import hosts
Dan Shi0942b1d2015-03-31 11:07:00 -070022from autotest_lib.client.common_lib import lsbrelease_utils
beeps5e8c45a2013-12-17 22:05:11 -080023from autotest_lib.client.common_lib.cros import dev_server
Fang Deng5d518f42013-08-02 14:04:32 -070024from autotest_lib.client.common_lib.cros import retry
Christopher Wileycef1f902014-06-19 11:11:23 -070025from autotest_lib.client.common_lib.cros.network import ping_runner
Hsinyu Chaoe0b08e62015-08-11 10:50:37 +000026from autotest_lib.client.cros import constants as client_constants
Richard Barnettee519dcd2016-08-15 17:37:17 -070027from autotest_lib.server import afe_utils
Prathmesh Prabhucbd5ebb2018-08-28 17:04:50 -070028from autotest_lib.server import site_utils as server_utils
Richard Barnetted31580e2018-05-14 19:58:00 +000029from autotest_lib.server.cros import autoupdater
Richard Barnetted31580e2018-05-14 19:58:00 +000030from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Richard Barnette9a26ad62016-06-10 12:03:08 -070031from autotest_lib.server.cros.servo import servo
Richard Barnetted31580e2018-05-14 19:58:00 +000032from autotest_lib.server.hosts import servo_repair
Fang Deng5d518f42013-08-02 14:04:32 -070033from autotest_lib.server.hosts import ssh_host
Fang Dengd4fe7392013-09-20 12:18:21 -070034from autotest_lib.site_utils.rpm_control_system import rpm_client
Fang Deng5d518f42013-08-02 14:04:32 -070035
Dan Shi5e2efb72017-02-07 11:40:23 -080036try:
37 from chromite.lib import metrics
38except ImportError:
39 metrics = utils.metrics_mock
40
Fang Deng5d518f42013-08-02 14:04:32 -070041
Simran Basi0739d682015-02-25 16:22:56 -080042# Names of the host attributes in the database that represent the values for
43# the servo_host and servo_port for a servo connected to the DUT.
44SERVO_HOST_ATTR = 'servo_host'
45SERVO_PORT_ATTR = 'servo_port'
Richard Barnettee519dcd2016-08-15 17:37:17 -070046SERVO_BOARD_ATTR = 'servo_board'
Nick Sanders2f3c9852018-10-24 12:10:24 -070047# Model is inferred from host labels.
48SERVO_MODEL_ATTR = 'servo_model'
Kevin Cheng643ce8a2016-09-15 15:42:12 -070049SERVO_SERIAL_ATTR = 'servo_serial'
Prathmesh Prabhucba44292018-08-28 17:44:45 -070050SERVO_ATTR_KEYS = (
51 SERVO_BOARD_ATTR,
52 SERVO_HOST_ATTR,
53 SERVO_PORT_ATTR,
54 SERVO_SERIAL_ATTR,
55)
Simran Basi0739d682015-02-25 16:22:56 -080056
Dan Shi3b2adf62015-09-02 17:46:54 -070057_CONFIG = global_config.global_config
xixuan6cf6d2f2016-01-29 15:29:00 -080058ENABLE_SSH_TUNNEL_FOR_SERVO = _CONFIG.get_config_value(
59 'CROS', 'enable_ssh_tunnel_for_servo', type=bool, default=False)
Simran Basi0739d682015-02-25 16:22:56 -080060
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -070061AUTOTEST_BASE = _CONFIG.get_config_value(
62 'SCHEDULER', 'drone_installation_directory',
63 default='/usr/local/autotest')
64
Fang Deng5d518f42013-08-02 14:04:32 -070065
Fang Deng5d518f42013-08-02 14:04:32 -070066class ServoHost(ssh_host.SSHHost):
67 """Host class for a host that controls a servo, e.g. beaglebone."""
68
Raul E Rangel52ca2e82018-07-03 14:10:14 -060069 DEFAULT_PORT = int(os.getenv('SERVOD_PORT', '9999'))
Richard Barnette9a26ad62016-06-10 12:03:08 -070070
Dan Shie5b3c512014-08-21 12:12:09 -070071 # Timeout for initializing servo signals.
Wai-Hong Tam37b6ed32017-09-19 15:52:39 -070072 INITIALIZE_SERVO_TIMEOUT_SECS = 60
Richard Barnette9a26ad62016-06-10 12:03:08 -070073
xixuan6cf6d2f2016-01-29 15:29:00 -080074 # Ready test function
75 SERVO_READY_METHOD = 'get_version'
Fang Deng5d518f42013-08-02 14:04:32 -070076
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -070077 REBOOT_CMD = 'sleep 1; reboot & sleep 10; reboot -f'
78
Fang Deng5d518f42013-08-02 14:04:32 -070079
Richard Barnette17bfc6c2016-08-04 18:41:43 -070080 def _initialize(self, servo_host='localhost',
Richard Barnettee519dcd2016-08-15 17:37:17 -070081 servo_port=DEFAULT_PORT, servo_board=None,
Nick Sanders2f3c9852018-10-24 12:10:24 -070082 servo_model=None, servo_serial=None, is_in_lab=None,
83 *args, **dargs):
Fang Deng5d518f42013-08-02 14:04:32 -070084 """Initialize a ServoHost instance.
85
86 A ServoHost instance represents a host that controls a servo.
87
88 @param servo_host: Name of the host where the servod process
89 is running.
Raul E Rangel52ca2e82018-07-03 14:10:14 -060090 @param servo_port: Port the servod process is listening on. Defaults
91 to the SERVOD_PORT environment variable if set,
92 otherwise 9999.
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -070093 @param servo_board: Board that the servo is connected to.
Nick Sanders2f3c9852018-10-24 12:10:24 -070094 @param servo_model: Model that the servo is connected to.
Dan Shi4d478522014-02-14 13:46:32 -080095 @param is_in_lab: True if the servo host is in Cros Lab. Default is set
96 to None, for which utils.host_is_in_lab_zone will be
97 called to check if the servo host is in Cros lab.
Fang Deng5d518f42013-08-02 14:04:32 -070098
99 """
100 super(ServoHost, self)._initialize(hostname=servo_host,
101 *args, **dargs)
Richard Barnette42f4db92018-08-23 15:05:15 -0700102 self.servo_port = int(servo_port)
Richard Barnettee519dcd2016-08-15 17:37:17 -0700103 self.servo_board = servo_board
Nick Sanders2f3c9852018-10-24 12:10:24 -0700104 self.servo_model = servo_model
Kevin Cheng643ce8a2016-09-15 15:42:12 -0700105 self.servo_serial = servo_serial
Richard Barnettee519dcd2016-08-15 17:37:17 -0700106 self._servo = None
Richard Barnette9a26ad62016-06-10 12:03:08 -0700107 self._repair_strategy = (
108 servo_repair.create_servo_repair_strategy())
Richard Barnettee519dcd2016-08-15 17:37:17 -0700109 self._is_localhost = (self.hostname == 'localhost')
110 if self._is_localhost:
111 self._is_in_lab = False
112 elif is_in_lab is None:
Dan Shi4d478522014-02-14 13:46:32 -0800113 self._is_in_lab = utils.host_is_in_lab_zone(self.hostname)
114 else:
115 self._is_in_lab = is_in_lab
xixuan6cf6d2f2016-01-29 15:29:00 -0800116
Richard Barnettee519dcd2016-08-15 17:37:17 -0700117 # Commands on the servo host must be run by the superuser.
118 # Our account on a remote host is root, but if our target is
119 # localhost then we might be running unprivileged. If so,
120 # `sudo` will have to be added to the commands.
Fang Deng5d518f42013-08-02 14:04:32 -0700121 if self._is_localhost:
122 self._sudo_required = utils.system_output('id -u') != '0'
123 else:
124 self._sudo_required = False
Richard Barnettee519dcd2016-08-15 17:37:17 -0700125
Richard Barnette9a26ad62016-06-10 12:03:08 -0700126
127 def connect_servo(self):
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700128 """Establish a connection to the servod server on this host.
Richard Barnette9a26ad62016-06-10 12:03:08 -0700129
130 Initializes `self._servo` and then verifies that all network
131 connections are working. This will create an ssh tunnel if
132 it's required.
133
134 As a side effect of testing the connection, all signals on the
135 target servo are reset to default values, and the USB stick is
136 set to the neutral (off) position.
137 """
Kevin Cheng643ce8a2016-09-15 15:42:12 -0700138 servo_obj = servo.Servo(servo_host=self, servo_serial=self.servo_serial)
Richard Barnette9a26ad62016-06-10 12:03:08 -0700139 timeout, _ = retry.timeout(
140 servo_obj.initialize_dut,
141 timeout_sec=self.INITIALIZE_SERVO_TIMEOUT_SECS)
142 if timeout:
143 raise hosts.AutoservVerifyError(
144 'Servo initialize timed out.')
145 self._servo = servo_obj
146
147
148 def disconnect_servo(self):
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700149 """Disconnect our servo if it exists.
Richard Barnette9a26ad62016-06-10 12:03:08 -0700150
151 If we've previously successfully connected to our servo,
152 disconnect any established ssh tunnel, and set `self._servo`
153 back to `None`.
154 """
155 if self._servo:
156 # N.B. This call is safe even without a tunnel:
157 # rpc_server_tracker.disconnect() silently ignores
158 # unknown ports.
159 self.rpc_server_tracker.disconnect(self.servo_port)
160 self._servo = None
Fang Deng5d518f42013-08-02 14:04:32 -0700161
162
163 def is_in_lab(self):
164 """Check whether the servo host is a lab device.
165
166 @returns: True if the servo host is in Cros Lab, otherwise False.
167
168 """
169 return self._is_in_lab
170
171
172 def is_localhost(self):
173 """Checks whether the servo host points to localhost.
174
175 @returns: True if it points to localhost, otherwise False.
176
177 """
178 return self._is_localhost
179
180
181 def get_servod_server_proxy(self):
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700182 """Return a proxy that can be used to communicate with servod server.
Fang Deng5d518f42013-08-02 14:04:32 -0700183
184 @returns: An xmlrpclib.ServerProxy that is connected to the servod
185 server on the host.
Fang Deng5d518f42013-08-02 14:04:32 -0700186 """
Richard Barnette9a26ad62016-06-10 12:03:08 -0700187 if ENABLE_SSH_TUNNEL_FOR_SERVO and not self.is_localhost():
188 return self.rpc_server_tracker.xmlrpc_connect(
189 None, self.servo_port,
190 ready_test_name=self.SERVO_READY_METHOD,
Allen Li2b1a8992018-11-27 14:17:18 -0800191 timeout_seconds=60,
Allen Li556f4532018-12-03 18:11:23 -0800192 request_timeout_seconds=3600)
Richard Barnette9a26ad62016-06-10 12:03:08 -0700193 else:
194 remote = 'http://%s:%s' % (self.hostname, self.servo_port)
195 return xmlrpclib.ServerProxy(remote)
Fang Deng5d518f42013-08-02 14:04:32 -0700196
197
Richard Barnette9a26ad62016-06-10 12:03:08 -0700198 def is_cros_host(self):
beeps5e8c45a2013-12-17 22:05:11 -0800199 """Check if a servo host is running chromeos.
200
201 @return: True if the servo host is running chromeos.
202 False if it isn't, or we don't have enough information.
203 """
204 try:
205 result = self.run('grep -q CHROMEOS /etc/lsb-release',
206 ignore_status=True, timeout=10)
207 except (error.AutoservRunError, error.AutoservSSHTimeout):
208 return False
209 return result.exit_status == 0
210
211
Fang Deng5d518f42013-08-02 14:04:32 -0700212 def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
Dean Liaoe3e75f62017-11-14 10:36:43 +0800213 connect_timeout=None, alive_interval=None,
214 alive_count_max=None, connection_attempts=None):
Fang Deng5d518f42013-08-02 14:04:32 -0700215 """Override default make_ssh_command to use tuned options.
216
217 Tuning changes:
218 - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
219 connection failure. Consistency with remote_access.py.
220
221 - ServerAliveInterval=180; which causes SSH to ping connection every
222 180 seconds. In conjunction with ServerAliveCountMax ensures
223 that if the connection dies, Autotest will bail out quickly.
224
225 - ServerAliveCountMax=3; consistency with remote_access.py.
226
227 - ConnectAttempts=4; reduce flakiness in connection errors;
228 consistency with remote_access.py.
229
230 - UserKnownHostsFile=/dev/null; we don't care about the keys.
231
232 - SSH protocol forced to 2; needed for ServerAliveInterval.
233
234 @param user User name to use for the ssh connection.
235 @param port Port on the target host to use for ssh connection.
236 @param opts Additional options to the ssh command.
237 @param hosts_file Ignored.
238 @param connect_timeout Ignored.
239 @param alive_interval Ignored.
Dean Liaoe3e75f62017-11-14 10:36:43 +0800240 @param alive_count_max Ignored.
241 @param connection_attempts Ignored.
Fang Deng5d518f42013-08-02 14:04:32 -0700242
243 @returns: An ssh command with the requested settings.
244
245 """
Dean Liaoe3e75f62017-11-14 10:36:43 +0800246 options = ' '.join([opts, '-o Protocol=2'])
247 return super(ServoHost, self).make_ssh_command(
248 user=user, port=port, opts=options, hosts_file='/dev/null',
249 connect_timeout=30, alive_interval=180, alive_count_max=3,
250 connection_attempts=4)
Fang Deng5d518f42013-08-02 14:04:32 -0700251
252
253 def _make_scp_cmd(self, sources, dest):
254 """Format scp command.
255
256 Given a list of source paths and a destination path, produces the
257 appropriate scp command for encoding it. Remote paths must be
258 pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost
259 to allow additional ssh options.
260
261 @param sources: A list of source paths to copy from.
262 @param dest: Destination path to copy to.
263
264 @returns: An scp command that copies |sources| on local machine to
265 |dest| on the remote servo host.
266
267 """
268 command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no '
269 '-o UserKnownHostsFile=/dev/null -P %d %s "%s"')
270 return command % (self.master_ssh_option,
271 self.port, ' '.join(sources), dest)
272
273
274 def run(self, command, timeout=3600, ignore_status=False,
275 stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
Luigi Semenzatobfbd1f32017-01-06 10:41:18 -0800276 connect_timeout=30, ssh_failure_retry_ok=False,
277 options='', stdin=None, verbose=True, args=()):
Fang Deng5d518f42013-08-02 14:04:32 -0700278 """Run a command on the servo host.
279
280 Extends method `run` in SSHHost. If the servo host is a remote device,
281 it will call `run` in SSHost without changing anything.
282 If the servo host is 'localhost', it will call utils.system_output.
283
284 @param command: The command line string.
285 @param timeout: Time limit in seconds before attempting to
286 kill the running process. The run() function
287 will take a few seconds longer than 'timeout'
288 to complete if it has to kill the process.
289 @param ignore_status: Do not raise an exception, no matter
290 what the exit code of the command is.
291 @param stdout_tee/stderr_tee: Where to tee the stdout/stderr.
292 @param connect_timeout: SSH connection timeout (in seconds)
293 Ignored if host is 'localhost'.
294 @param options: String with additional ssh command options
295 Ignored if host is 'localhost'.
Luigi Semenzatobfbd1f32017-01-06 10:41:18 -0800296 @param ssh_failure_retry_ok: when True and ssh connection failure is
297 suspected, OK to retry command (but not
298 compulsory, and likely not needed here)
Fang Deng5d518f42013-08-02 14:04:32 -0700299 @param stdin: Stdin to pass (a string) to the executed command.
300 @param verbose: Log the commands.
301 @param args: Sequence of strings to pass as arguments to command by
302 quoting them in " and escaping their contents if necessary.
303
304 @returns: A utils.CmdResult object.
305
306 @raises AutoservRunError if the command failed.
307 @raises AutoservSSHTimeout SSH connection has timed out. Only applies
308 when servo host is not 'localhost'.
309
310 """
311 run_args = {'command': command, 'timeout': timeout,
312 'ignore_status': ignore_status, 'stdout_tee': stdout_tee,
313 'stderr_tee': stderr_tee, 'stdin': stdin,
314 'verbose': verbose, 'args': args}
315 if self.is_localhost():
316 if self._sudo_required:
Michael Tangf9b3ada2016-11-18 16:01:05 -0800317 run_args['command'] = 'sudo -n sh -c "%s"' % utils.sh_escape(
318 command)
Fang Deng5d518f42013-08-02 14:04:32 -0700319 try:
320 return utils.run(**run_args)
321 except error.CmdError as e:
322 logging.error(e)
323 raise error.AutoservRunError('command execution error',
324 e.result_obj)
325 else:
326 run_args['connect_timeout'] = connect_timeout
327 run_args['options'] = options
328 return super(ServoHost, self).run(**run_args)
329
330
Richard Barnette9a26ad62016-06-10 12:03:08 -0700331 def _get_release_version(self):
Dan Shi0942b1d2015-03-31 11:07:00 -0700332 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
333
334 @returns The version string in lsb-release, under attribute
335 CHROMEOS_RELEASE_VERSION.
336 """
337 lsb_release_content = self.run(
338 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
339 return lsbrelease_utils.get_chromeos_release_version(
340 lsb_release_content=lsb_release_content)
341
342
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700343 def get_attached_duts(self, afe):
344 """Gather a list of duts that use this servo host.
345
346 @param afe: afe instance.
347
348 @returns list of duts.
Richard Barnette3a7697f2016-04-20 11:33:27 -0700349 """
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700350 return afe.get_hosts_by_attribute(
351 attribute=SERVO_HOST_ATTR, value=self.hostname)
352
353
354 def get_board(self):
355 """Determine the board for this servo host.
356
357 @returns a string representing this servo host's board.
358 """
359 return lsbrelease_utils.get_current_board(
360 lsb_release_content=self.run('cat /etc/lsb-release').stdout)
361
362
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700363 def reboot(self, *args, **dargs):
364 """Reboot using special servo host reboot command."""
365 super(ServoHost, self).reboot(reboot_cmd=self.REBOOT_CMD,
366 *args, **dargs)
367
368
Prathmesh Prabhu99b100b2018-12-27 13:12:17 -0800369 def _maybe_reboot_post_upgrade(self, updater):
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700370 """Reboot this servo host if an upgrade is waiting.
Richard Barnette3a7697f2016-04-20 11:33:27 -0700371
372 If the host has successfully downloaded and finalized a new
373 build, reboot.
374
375 @param updater: a ChromiumOSUpdater instance for checking
376 whether reboot is needed.
Richard Barnette3a7697f2016-04-20 11:33:27 -0700377 """
Prathmesh Prabhu5d52bc42018-12-27 13:22:46 -0800378 if updater.check_update_status() != autoupdater.UPDATER_NEED_REBOOT:
379 return
380
381 if self._needs_synchronized_reboot():
Prathmesh Prabhu829e6d02018-12-27 13:27:02 -0800382 logging.info('Servohost requies synchronized reboot, which is no'
383 ' longer supported. Manually reboot servohost instead.'
384 ' See crbug/848528')
Prathmesh Prabhu5d52bc42018-12-27 13:22:46 -0800385 return
386
387 self._reboot_post_upgrade()
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700388
Prathmesh Prabhu99b100b2018-12-27 13:12:17 -0800389
Prathmesh Prabhu50be9212018-12-27 13:21:44 -0800390 def _needs_synchronized_reboot(self):
391 """Does this servohost need synchronized reboot across multiple DUTs"""
392 # TODO(pprabhu) Use HostInfo in this check instead of hitting AFE.
Prathmesh Prabhu460905d2018-12-27 13:10:00 -0800393 afe = frontend_wrappers.RetryingAFE(
394 timeout_min=5, delay_sec=10,
395 server=server_utils.get_global_afe_hostname())
396 dut_list = self.get_attached_duts(afe)
Prathmesh Prabhu50be9212018-12-27 13:21:44 -0800397 return len(dut_list) > 1
398
399
400 def _reboot_post_upgrade(self):
401 """Reboot this servo host because an upgrade is waiting."""
Prathmesh Prabhu50be9212018-12-27 13:21:44 -0800402 logging.info('Rebooting servo host %s from build %s', self.hostname,
403 self._get_release_version())
Prathmesh Prabhu460905d2018-12-27 13:10:00 -0800404 # Tell the reboot() call not to wait for completion.
405 # Otherwise, the call will log reboot failure if servo does
406 # not come back. The logged reboot failure will lead to
407 # test job failure. If the test does not require servo, we
408 # don't want servo failure to fail the test with error:
409 # `Host did not return from reboot` in status.log.
410 self.reboot(fastsync=True, wait=False)
411
412 # We told the reboot() call not to wait, but we need to wait
413 # for the reboot before we continue. Alas. The code from
414 # here below is basically a copy of Host.wait_for_restart(),
415 # with the logging bits ripped out, so that they can't cause
416 # the failure logging problem described above.
417 #
418 # The black stain that this has left on my soul can never be
419 # erased.
420 old_boot_id = self.get_boot_id()
421 if not self.wait_down(timeout=self.WAIT_DOWN_REBOOT_TIMEOUT,
422 warning_timer=self.WAIT_DOWN_REBOOT_WARNING,
423 old_boot_id=old_boot_id):
424 raise error.AutoservHostError(
425 'servo host %s failed to shut down.' %
426 self.hostname)
427 if self.wait_up(timeout=120):
428 logging.info('servo host %s back from reboot, with build %s',
429 self.hostname, self._get_release_version())
430 else:
431 raise error.AutoservHostError(
432 'servo host %s failed to come back from reboot.' %
433 self.hostname)
Richard Barnette3a7697f2016-04-20 11:33:27 -0700434
435
Richard Barnette3a7697f2016-04-20 11:33:27 -0700436 def update_image(self, wait_for_update=False):
beeps5e8c45a2013-12-17 22:05:11 -0800437 """Update the image on the servo host, if needed.
438
J. Richard Barnette84895392015-04-30 12:31:01 -0700439 This method recognizes the following cases:
440 * If the Host is not running Chrome OS, do nothing.
441 * If a previously triggered update is now complete, reboot
442 to the new version.
443 * If the host is processing a previously triggered update,
444 do nothing.
445 * If the host is running a version of Chrome OS different
446 from the default for servo Hosts, trigger an update, but
447 don't wait for it to complete.
beeps5e8c45a2013-12-17 22:05:11 -0800448
Richard Barnette3a7697f2016-04-20 11:33:27 -0700449 @param wait_for_update If an update needs to be applied and
450 this is true, then don't return until the update is
451 downloaded and finalized, and the host rebooted.
beeps5e8c45a2013-12-17 22:05:11 -0800452 @raises dev_server.DevServerException: If all the devservers are down.
453 @raises site_utils.ParseBuildNameException: If the devserver returns
454 an invalid build name.
beeps5e8c45a2013-12-17 22:05:11 -0800455 @raises AutoservRunError: If the update_engine_client isn't present on
456 the host, and the host is a cros_host.
J. Richard Barnette84895392015-04-30 12:31:01 -0700457
beeps5e8c45a2013-12-17 22:05:11 -0800458 """
Dan Shib795b5a2015-09-24 13:26:35 -0700459 # servod could be running in a Ubuntu workstation.
Richard Barnette9a26ad62016-06-10 12:03:08 -0700460 if not self.is_cros_host():
beeps5e8c45a2013-12-17 22:05:11 -0800461 logging.info('Not attempting an update, either %s is not running '
462 'chromeos or we cannot find enough information about '
463 'the host.', self.hostname)
464 return
465
Dan Shib795b5a2015-09-24 13:26:35 -0700466 if lsbrelease_utils.is_moblab():
467 logging.info('Not attempting an update, %s is running moblab.',
468 self.hostname)
469 return
470
Richard Barnette383ef9c2016-12-13 11:56:49 -0800471 target_build = afe_utils.get_stable_cros_image_name(self.get_board())
Prathmesh Prabhucbd5ebb2018-08-28 17:04:50 -0700472 target_build_number = server_utils.ParseBuildName(
J. Richard Barnette84895392015-04-30 12:31:01 -0700473 target_build)[3]
xixuanfa2d92a2016-12-09 09:45:27 -0800474 # For servo image staging, we want it as more widely distributed as
475 # possible, so that devservers' load can be evenly distributed. So use
476 # hostname instead of target_build as hash.
477 ds = dev_server.ImageServer.resolve(self.hostname,
478 hostname=self.hostname)
J. Richard Barnette84895392015-04-30 12:31:01 -0700479 url = ds.get_update_url(target_build)
beeps5e8c45a2013-12-17 22:05:11 -0800480
481 updater = autoupdater.ChromiumOSUpdater(update_url=url, host=self)
Prathmesh Prabhu99b100b2018-12-27 13:12:17 -0800482 self._maybe_reboot_post_upgrade(updater)
Prathmesh Prabhuec8414f2018-12-27 13:08:47 -0800483 current_build_number = self._get_release_version()
484 status = updater.check_update_status()
Richard Barnette3a7697f2016-04-20 11:33:27 -0700485 update_pending = True
beeps5e8c45a2013-12-17 22:05:11 -0800486 if status in autoupdater.UPDATER_PROCESSING_UPDATE:
487 logging.info('servo host %s already processing an update, update '
488 'engine client status=%s', self.hostname, status)
Allen Li66aa2542017-06-26 15:26:27 -0700489 elif status == autoupdater.UPDATER_NEED_REBOOT:
490 return
J. Richard Barnette84895392015-04-30 12:31:01 -0700491 elif current_build_number != target_build_number:
beeps5e8c45a2013-12-17 22:05:11 -0800492 logging.info('Using devserver url: %s to trigger update on '
493 'servo host %s, from %s to %s', url, self.hostname,
J. Richard Barnette84895392015-04-30 12:31:01 -0700494 current_build_number, target_build_number)
beeps5e8c45a2013-12-17 22:05:11 -0800495 try:
J. Richard Barnette84895392015-04-30 12:31:01 -0700496 ds.stage_artifacts(target_build,
497 artifacts=['full_payload'])
498 except Exception as e:
499 logging.error('Staging artifacts failed: %s', str(e))
500 logging.error('Abandoning update for this cycle.')
beeps5e8c45a2013-12-17 22:05:11 -0800501 else:
J. Richard Barnette84895392015-04-30 12:31:01 -0700502 try:
503 updater.trigger_update()
504 except autoupdater.RootFSUpdateError as e:
505 trigger_download_status = 'failed with %s' % str(e)
Aviv Keshet11836322016-11-22 11:32:01 -0800506 metrics.Counter('chromeos/autotest/servo/'
507 'rootfs_update_failed').increment()
J. Richard Barnette84895392015-04-30 12:31:01 -0700508 else:
509 trigger_download_status = 'passed'
510 logging.info('Triggered download and update %s for %s, '
511 'update engine currently in status %s',
512 trigger_download_status, self.hostname,
513 updater.check_update_status())
beeps5e8c45a2013-12-17 22:05:11 -0800514 else:
515 logging.info('servo host %s does not require an update.',
516 self.hostname)
Richard Barnette3a7697f2016-04-20 11:33:27 -0700517 update_pending = False
518
519 if update_pending and wait_for_update:
520 logging.info('Waiting for servo update to complete.')
521 self.run('update_engine_client --follow', ignore_status=True)
beeps5e8c45a2013-12-17 22:05:11 -0800522
523
Richard Barnette1edbb162016-11-01 11:47:50 -0700524 def verify(self, silent=False):
525 """Update the servo host and verify it's in a good state.
526
527 @param silent If true, suppress logging in `status.log`.
528 """
Richard Barnetteabbdc252018-07-26 16:57:42 -0700529 message = 'Beginning verify for servo host %s port %s serial %s'
530 message %= (self.hostname, self.servo_port, self.servo_serial)
531 self.record('INFO', None, None, message)
Richard Barnette9a26ad62016-06-10 12:03:08 -0700532 try:
Richard Barnette1edbb162016-11-01 11:47:50 -0700533 self._repair_strategy.verify(self, silent)
Richard Barnette9a26ad62016-06-10 12:03:08 -0700534 except:
535 self.disconnect_servo()
536 raise
Fang Deng5d518f42013-08-02 14:04:32 -0700537
538
Richard Barnette1edbb162016-11-01 11:47:50 -0700539 def repair(self, silent=False):
540 """Attempt to repair servo host.
541
542 @param silent If true, suppress logging in `status.log`.
543 """
Richard Barnetteabbdc252018-07-26 16:57:42 -0700544 message = 'Beginning repair for servo host %s port %s serial %s'
545 message %= (self.hostname, self.servo_port, self.servo_serial)
546 self.record('INFO', None, None, message)
Richard Barnette9a26ad62016-06-10 12:03:08 -0700547 try:
Richard Barnette1edbb162016-11-01 11:47:50 -0700548 self._repair_strategy.repair(self, silent)
Richard Barnette9a26ad62016-06-10 12:03:08 -0700549 except:
550 self.disconnect_servo()
551 raise
Fang Deng5d518f42013-08-02 14:04:32 -0700552
553
Fang Dengd4fe7392013-09-20 12:18:21 -0700554 def has_power(self):
555 """Return whether or not the servo host is powered by PoE."""
556 # TODO(fdeng): See crbug.com/302791
557 # For now, assume all servo hosts in the lab have power.
558 return self.is_in_lab()
559
560
561 def power_cycle(self):
562 """Cycle power to this host via PoE if it is a lab device.
563
Richard Barnette9a26ad62016-06-10 12:03:08 -0700564 @raises AutoservRepairError if it fails to power cycle the
Fang Dengd4fe7392013-09-20 12:18:21 -0700565 servo host.
566
567 """
568 if self.has_power():
569 try:
Garry Wangad4d4fd2019-01-30 17:00:38 -0800570 rpm_client.set_power(self, 'CYCLE')
Fang Dengd4fe7392013-09-20 12:18:21 -0700571 except (socket.error, xmlrpclib.Error,
572 httplib.BadStatusLine,
573 rpm_client.RemotePowerException) as e:
Richard Barnette9a26ad62016-06-10 12:03:08 -0700574 raise hosts.AutoservRepairError(
Garry Wang954f8382019-01-23 13:49:29 -0800575 'Power cycling %s failed: %s' % (self.hostname, e),
576 'power_cycle_via_rpm_failed'
577 )
Fang Dengd4fe7392013-09-20 12:18:21 -0700578 else:
579 logging.info('Skipping power cycling, not a lab device.')
580
581
Dan Shi4d478522014-02-14 13:46:32 -0800582 def get_servo(self):
583 """Get the cached servo.Servo object.
Fang Deng5d518f42013-08-02 14:04:32 -0700584
Dan Shi4d478522014-02-14 13:46:32 -0800585 @return: a servo.Servo object.
Fang Deng5d518f42013-08-02 14:04:32 -0700586 """
Dan Shi4d478522014-02-14 13:46:32 -0800587 return self._servo
588
589
Congbin Guoa1f9cba2018-07-03 11:36:59 -0700590 def close(self):
591 """Stop UART logging and close the host object."""
592 if self._servo:
Congbin Guo2e5e2a22018-07-27 10:32:48 -0700593 # In some cases when we run as lab-tools, the job object is None.
594 if self.job:
595 self._servo.dump_uart_streams(self.job.resultdir)
Congbin Guoa1f9cba2018-07-03 11:36:59 -0700596 self._servo.close()
597
598 super(ServoHost, self).close()
599
600
Richard Barnetteea3e4602016-06-10 12:36:41 -0700601def make_servo_hostname(dut_hostname):
602 """Given a DUT's hostname, return the hostname of its servo.
603
604 @param dut_hostname: hostname of a DUT.
605
606 @return hostname of the DUT's servo.
607
608 """
609 host_parts = dut_hostname.split('.')
610 host_parts[0] = host_parts[0] + '-servo'
611 return '.'.join(host_parts)
612
613
614def servo_host_is_up(servo_hostname):
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700615 """Given a servo host name, return if it's up or not.
Richard Barnetteea3e4602016-06-10 12:36:41 -0700616
617 @param servo_hostname: hostname of the servo host.
618
619 @return True if it's up, False otherwise
620 """
621 # Technically, this duplicates the SSH ping done early in the servo
622 # proxy initialization code. However, this ping ends in a couple
623 # seconds when if fails, rather than the 60 seconds it takes to decide
624 # that an SSH ping has timed out. Specifically, that timeout happens
625 # when our servo DNS name resolves, but there is no host at that IP.
626 logging.info('Pinging servo host at %s', servo_hostname)
627 ping_config = ping_runner.PingConfig(
628 servo_hostname, count=3,
629 ignore_result=True, ignore_status=True)
630 return ping_runner.PingRunner().ping(ping_config).received > 0
631
632
Richard Barnettee519dcd2016-08-15 17:37:17 -0700633def _map_afe_board_to_servo_board(afe_board):
634 """Map a board we get from the AFE to a servo appropriate value.
635
636 Many boards are identical to other boards for servo's purposes.
637 This function makes that mapping.
638
639 @param afe_board string board name received from AFE.
640 @return board we expect servo to have.
641
642 """
643 KNOWN_SUFFIXES = ['-freon', '_freon', '_moblab', '-cheets']
644 BOARD_MAP = {'gizmo': 'panther'}
645 mapped_board = afe_board
646 if afe_board in BOARD_MAP:
647 mapped_board = BOARD_MAP[afe_board]
648 else:
649 for suffix in KNOWN_SUFFIXES:
650 if afe_board.endswith(suffix):
651 mapped_board = afe_board[0:-len(suffix)]
652 break
653 if mapped_board != afe_board:
654 logging.info('Mapping AFE board=%s to %s', afe_board, mapped_board)
655 return mapped_board
656
657
Prathmesh Prabhub4810232018-09-07 13:24:08 -0700658def get_servo_args_for_host(dut_host):
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700659 """Return servo data associated with a given DUT.
Richard Barnetteea3e4602016-06-10 12:36:41 -0700660
Richard Barnetteea3e4602016-06-10 12:36:41 -0700661 @param dut_host Instance of `Host` on which to find the servo
662 attributes.
Prathmesh Prabhuf605dd32018-08-28 17:09:04 -0700663 @return `servo_args` dict with host and an optional port.
Richard Barnetteea3e4602016-06-10 12:36:41 -0700664 """
Prathmesh Prabhucba44292018-08-28 17:44:45 -0700665 info = dut_host.host_info_store.get()
666 servo_args = {k: v for k, v in info.attributes.iteritems()
667 if k in SERVO_ATTR_KEYS}
Richard Barnetteea3e4602016-06-10 12:36:41 -0700668
Prathmesh Prabhucba44292018-08-28 17:44:45 -0700669 if SERVO_PORT_ATTR in servo_args:
670 try:
671 servo_args[SERVO_PORT_ATTR] = int(servo_args[SERVO_PORT_ATTR])
672 except ValueError:
673 logging.error('servo port is not an int: %s',
674 servo_args[SERVO_PORT_ATTR])
675 # Reset servo_args because we don't want to use an invalid port.
676 servo_args.pop(SERVO_HOST_ATTR, None)
677
678 if info.board:
679 servo_args[SERVO_BOARD_ATTR] = _map_afe_board_to_servo_board(info.board)
Nick Sanders2f3c9852018-10-24 12:10:24 -0700680 if info.model:
681 servo_args[SERVO_MODEL_ATTR] = info.model
Prathmesh Prabhu6f5f6362018-09-05 17:20:31 -0700682 return servo_args if SERVO_HOST_ATTR in servo_args else None
Richard Barnetteea3e4602016-06-10 12:36:41 -0700683
684
Prathmesh Prabhuefb1b482018-08-28 17:15:05 -0700685def _tweak_args_for_ssp_moblab(servo_args):
686 if servo_args[SERVO_HOST_ATTR] in ['localhost', '127.0.0.1']:
687 servo_args[SERVO_HOST_ATTR] = _CONFIG.get_config_value(
688 'SSP', 'host_container_ip', type=str, default=None)
689
690
Dan Shi023aae32016-05-25 11:13:01 -0700691def create_servo_host(dut, servo_args, try_lab_servo=False,
Richard Barnette9a26ad62016-06-10 12:03:08 -0700692 try_servo_repair=False):
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700693 """Create a ServoHost object for a given DUT, if appropriate.
Dan Shi4d478522014-02-14 13:46:32 -0800694
Richard Barnette9a26ad62016-06-10 12:03:08 -0700695 This function attempts to create and verify or repair a `ServoHost`
696 object for a servo connected to the given `dut`, subject to various
697 constraints imposed by the parameters:
698 * When the `servo_args` parameter is not `None`, a servo
699 host must be created, and must be checked with `repair()`.
700 * Otherwise, if a servo exists in the lab and `try_lab_servo` is
701 true:
702 * If `try_servo_repair` is true, then create a servo host and
703 check it with `repair()`.
704 * Otherwise, if the servo responds to `ping` then create a
705 servo host and check it with `verify()`.
Fang Denge545abb2014-12-30 18:43:47 -0800706
Richard Barnette9a26ad62016-06-10 12:03:08 -0700707 In cases where `servo_args` was not `None`, repair failure
708 exceptions are passed back to the caller; otherwise, exceptions
Richard Barnette07c2e1d2016-10-26 14:24:28 -0700709 are logged and then discarded. Note that this only happens in cases
710 where we're called from a test (not special task) control file that
711 has an explicit dependency on servo. In that case, we require that
712 repair not write to `status.log`, so as to avoid polluting test
713 results.
714
715 TODO(jrbarnette): The special handling for servo in test control
716 files is a thorn in my flesh; I dearly hope to see it cut out before
717 my retirement.
Richard Barnette9a26ad62016-06-10 12:03:08 -0700718
719 Parameters for a servo host consist of a host name, port number, and
720 DUT board, and are determined from one of these sources, in order of
721 priority:
Richard Barnetteea3e4602016-06-10 12:36:41 -0700722 * Servo attributes from the `dut` parameter take precedence over
723 all other sources of information.
724 * If a DNS entry for the servo based on the DUT hostname exists in
725 the CrOS lab network, that hostname is used with the default
Richard Barnette9a26ad62016-06-10 12:03:08 -0700726 port and the DUT's board.
Richard Barnetteea3e4602016-06-10 12:36:41 -0700727 * If no other options are found, the parameters will be taken
Richard Barnette9a26ad62016-06-10 12:03:08 -0700728 from the `servo_args` dict passed in from the caller.
Richard Barnetteea3e4602016-06-10 12:36:41 -0700729
730 @param dut An instance of `Host` from which to take
731 servo parameters (if available).
732 @param servo_args A dictionary with servo parameters to use if
733 they can't be found from `dut`. If this
734 argument is supplied, unrepaired exceptions
735 from `verify()` will be passed back to the
736 caller.
737 @param try_lab_servo If not true, servo host creation will be
738 skipped unless otherwise required by the
739 caller.
Richard Barnette9a26ad62016-06-10 12:03:08 -0700740 @param try_servo_repair If true, check a servo host with
741 `repair()` instead of `verify()`.
Dan Shi4d478522014-02-14 13:46:32 -0800742
743 @returns: A ServoHost object or None. See comments above.
744
745 """
Richard Barnette07c2e1d2016-10-26 14:24:28 -0700746 servo_dependency = servo_args is not None
Richard Barnette07c2e1d2016-10-26 14:24:28 -0700747 if dut is not None and (try_lab_servo or servo_dependency):
Prathmesh Prabhub4810232018-09-07 13:24:08 -0700748 servo_args_override = get_servo_args_for_host(dut)
Richard Barnetteea3e4602016-06-10 12:36:41 -0700749 if servo_args_override is not None:
Prathmesh Prabhuefb1b482018-08-28 17:15:05 -0700750 if utils.in_moblab_ssp():
751 _tweak_args_for_ssp_moblab(servo_args_override)
Prathmesh Prabhu88bf6052018-08-28 16:21:26 -0700752 logging.debug(
753 'Overriding provided servo_args (%s) with arguments'
754 ' determined from the host (%s)',
755 servo_args,
756 servo_args_override,
757 )
Richard Barnetteea3e4602016-06-10 12:36:41 -0700758 servo_args = servo_args_override
Prathmesh Prabhucba44292018-08-28 17:44:45 -0700759
Richard Barnetteea3e4602016-06-10 12:36:41 -0700760 if servo_args is None:
Prathmesh Prabhu88bf6052018-08-28 16:21:26 -0700761 logging.debug('No servo_args provided, and failed to find overrides.')
Richard Barnetteea3e4602016-06-10 12:36:41 -0700762 return None
Prathmesh Prabhucba44292018-08-28 17:44:45 -0700763 if SERVO_HOST_ATTR not in servo_args:
764 logging.debug('%s attribute missing from servo_args: %s',
765 SERVO_HOST_ATTR, servo_args)
766 return None
Richard Barnette07c2e1d2016-10-26 14:24:28 -0700767 if (not servo_dependency and not try_servo_repair and
Richard Barnette9a26ad62016-06-10 12:03:08 -0700768 not servo_host_is_up(servo_args[SERVO_HOST_ATTR])):
Prathmesh Prabhu88bf6052018-08-28 16:21:26 -0700769 logging.debug('ServoHost is not up.')
Dan Shibbb0cb62014-03-24 17:50:57 -0700770 return None
Prathmesh Prabhu88bf6052018-08-28 16:21:26 -0700771
Prathmesh Prabhuf605dd32018-08-28 17:09:04 -0700772 newhost = ServoHost(
773 is_in_lab=(servo_args
774 and server_utils.host_in_lab(
775 servo_args[SERVO_HOST_ATTR])),
776 **servo_args
777 )
Richard Barnette9a26ad62016-06-10 12:03:08 -0700778 # Note that the logic of repair() includes everything done
779 # by verify(). It's sufficient to call one or the other;
780 # we don't need both.
Richard Barnette07c2e1d2016-10-26 14:24:28 -0700781 if servo_dependency:
782 newhost.repair(silent=True)
Prathmesh Prabhu88bf6052018-08-28 16:21:26 -0700783 return newhost
784
785 if try_servo_repair:
786 try:
787 newhost.repair()
788 except Exception:
789 logging.exception('servo repair failed for %s', newhost.hostname)
Richard Barnette9a26ad62016-06-10 12:03:08 -0700790 else:
791 try:
Prathmesh Prabhu88bf6052018-08-28 16:21:26 -0700792 newhost.verify()
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700793 except Exception:
Prathmesh Prabhu88bf6052018-08-28 16:21:26 -0700794 logging.exception('servo verify failed for %s', newhost.hostname)
Richard Barnette9a26ad62016-06-10 12:03:08 -0700795 return newhost