blob: 68d9e73d8bfc8b514fbda557b8a387fb5ec6c820 [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
16
17from autotest_lib.client.bin import utils
18from autotest_lib.client.common_lib import error
beeps5e8c45a2013-12-17 22:05:11 -080019from autotest_lib.client.common_lib import global_config
Richard Barnette9a26ad62016-06-10 12:03:08 -070020from autotest_lib.client.common_lib import hosts
Dan Shi0942b1d2015-03-31 11:07:00 -070021from autotest_lib.client.common_lib import lsbrelease_utils
beeps5e8c45a2013-12-17 22:05:11 -080022from autotest_lib.client.common_lib.cros import autoupdater
23from 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
Gabe Black1e1c41b2015-02-04 23:55:15 -080025from autotest_lib.client.common_lib.cros.graphite import autotest_stats
Christopher Wileycef1f902014-06-19 11:11:23 -070026from autotest_lib.client.common_lib.cros.network import ping_runner
Hsinyu Chaoe0b08e62015-08-11 10:50:37 +000027from autotest_lib.client.cros import constants as client_constants
Richard Barnettee519dcd2016-08-15 17:37:17 -070028from autotest_lib.server import afe_utils
beeps5e8c45a2013-12-17 22:05:11 -080029from autotest_lib.server import site_utils as server_site_utils
Cheng-Yi Chiang22612862015-08-20 20:39:57 +080030from autotest_lib.server.cros import dnsname_mangler
Simran Basi0739d682015-02-25 16:22:56 -080031from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Richard Barnette9a26ad62016-06-10 12:03:08 -070032from autotest_lib.server.cros.servo import servo
33from autotest_lib.server.hosts import servo_repair
Fang Deng5d518f42013-08-02 14:04:32 -070034from autotest_lib.server.hosts import ssh_host
Fang Dengd4fe7392013-09-20 12:18:21 -070035from autotest_lib.site_utils.rpm_control_system import rpm_client
Fang Deng5d518f42013-08-02 14:04:32 -070036
37
Simran Basi0739d682015-02-25 16:22:56 -080038# Names of the host attributes in the database that represent the values for
39# the servo_host and servo_port for a servo connected to the DUT.
40SERVO_HOST_ATTR = 'servo_host'
41SERVO_PORT_ATTR = 'servo_port'
Richard Barnettee519dcd2016-08-15 17:37:17 -070042SERVO_BOARD_ATTR = 'servo_board'
Simran Basi0739d682015-02-25 16:22:56 -080043
Dan Shi3b2adf62015-09-02 17:46:54 -070044_CONFIG = global_config.global_config
xixuan6cf6d2f2016-01-29 15:29:00 -080045ENABLE_SSH_TUNNEL_FOR_SERVO = _CONFIG.get_config_value(
46 'CROS', 'enable_ssh_tunnel_for_servo', type=bool, default=False)
Simran Basi0739d682015-02-25 16:22:56 -080047
Fang Deng5d518f42013-08-02 14:04:32 -070048
Fang Deng5d518f42013-08-02 14:04:32 -070049class ServoHost(ssh_host.SSHHost):
50 """Host class for a host that controls a servo, e.g. beaglebone."""
51
Richard Barnette9a26ad62016-06-10 12:03:08 -070052 DEFAULT_PORT = 9999
53
Dan Shie5b3c512014-08-21 12:12:09 -070054 # Timeout for initializing servo signals.
55 INITIALIZE_SERVO_TIMEOUT_SECS = 30
Richard Barnette9a26ad62016-06-10 12:03:08 -070056
xixuan6cf6d2f2016-01-29 15:29:00 -080057 # Ready test function
58 SERVO_READY_METHOD = 'get_version'
Fang Deng5d518f42013-08-02 14:04:32 -070059
Gabe Black1e1c41b2015-02-04 23:55:15 -080060 _timer = autotest_stats.Timer('servo_host')
Fang Dengd4fe7392013-09-20 12:18:21 -070061
Fang Deng5d518f42013-08-02 14:04:32 -070062
Richard Barnette17bfc6c2016-08-04 18:41:43 -070063 def _initialize(self, servo_host='localhost',
Richard Barnettee519dcd2016-08-15 17:37:17 -070064 servo_port=DEFAULT_PORT, servo_board=None,
Richard Barnette17bfc6c2016-08-04 18:41:43 -070065 is_in_lab=None, *args, **dargs):
Fang Deng5d518f42013-08-02 14:04:32 -070066 """Initialize a ServoHost instance.
67
68 A ServoHost instance represents a host that controls a servo.
69
70 @param servo_host: Name of the host where the servod process
71 is running.
72 @param servo_port: Port the servod process is listening on.
Dan Shi4d478522014-02-14 13:46:32 -080073 @param is_in_lab: True if the servo host is in Cros Lab. Default is set
74 to None, for which utils.host_is_in_lab_zone will be
75 called to check if the servo host is in Cros lab.
Fang Deng5d518f42013-08-02 14:04:32 -070076
77 """
78 super(ServoHost, self)._initialize(hostname=servo_host,
79 *args, **dargs)
Richard Barnettee519dcd2016-08-15 17:37:17 -070080 self.servo_port = servo_port
81 self.servo_board = servo_board
Richard Barnettee519dcd2016-08-15 17:37:17 -070082 self._servo = None
Richard Barnette9a26ad62016-06-10 12:03:08 -070083 self._repair_strategy = (
84 servo_repair.create_servo_repair_strategy())
Richard Barnettee519dcd2016-08-15 17:37:17 -070085 self._is_localhost = (self.hostname == 'localhost')
86 if self._is_localhost:
87 self._is_in_lab = False
88 elif is_in_lab is None:
Dan Shi4d478522014-02-14 13:46:32 -080089 self._is_in_lab = utils.host_is_in_lab_zone(self.hostname)
90 else:
91 self._is_in_lab = is_in_lab
xixuan6cf6d2f2016-01-29 15:29:00 -080092
Richard Barnettee519dcd2016-08-15 17:37:17 -070093 # Commands on the servo host must be run by the superuser.
94 # Our account on a remote host is root, but if our target is
95 # localhost then we might be running unprivileged. If so,
96 # `sudo` will have to be added to the commands.
Fang Deng5d518f42013-08-02 14:04:32 -070097 if self._is_localhost:
98 self._sudo_required = utils.system_output('id -u') != '0'
99 else:
100 self._sudo_required = False
Richard Barnettee519dcd2016-08-15 17:37:17 -0700101
Richard Barnette9a26ad62016-06-10 12:03:08 -0700102
103 def connect_servo(self):
104 """
105 Establish a connection to the servod server on this host.
106
107 Initializes `self._servo` and then verifies that all network
108 connections are working. This will create an ssh tunnel if
109 it's required.
110
111 As a side effect of testing the connection, all signals on the
112 target servo are reset to default values, and the USB stick is
113 set to the neutral (off) position.
114 """
115 servo_obj = servo.Servo(servo_host=self)
116 timeout, _ = retry.timeout(
117 servo_obj.initialize_dut,
118 timeout_sec=self.INITIALIZE_SERVO_TIMEOUT_SECS)
119 if timeout:
120 raise hosts.AutoservVerifyError(
121 'Servo initialize timed out.')
122 self._servo = servo_obj
123
124
125 def disconnect_servo(self):
126 """
127 Disconnect our servo if it exists.
128
129 If we've previously successfully connected to our servo,
130 disconnect any established ssh tunnel, and set `self._servo`
131 back to `None`.
132 """
133 if self._servo:
134 # N.B. This call is safe even without a tunnel:
135 # rpc_server_tracker.disconnect() silently ignores
136 # unknown ports.
137 self.rpc_server_tracker.disconnect(self.servo_port)
138 self._servo = None
Fang Deng5d518f42013-08-02 14:04:32 -0700139
140
141 def is_in_lab(self):
142 """Check whether the servo host is a lab device.
143
144 @returns: True if the servo host is in Cros Lab, otherwise False.
145
146 """
147 return self._is_in_lab
148
149
150 def is_localhost(self):
151 """Checks whether the servo host points to localhost.
152
153 @returns: True if it points to localhost, otherwise False.
154
155 """
156 return self._is_localhost
157
158
159 def get_servod_server_proxy(self):
Richard Barnette9a26ad62016-06-10 12:03:08 -0700160 """
161 Return a proxy that can be used to communicate with servod server.
Fang Deng5d518f42013-08-02 14:04:32 -0700162
163 @returns: An xmlrpclib.ServerProxy that is connected to the servod
164 server on the host.
Fang Deng5d518f42013-08-02 14:04:32 -0700165 """
Richard Barnette9a26ad62016-06-10 12:03:08 -0700166 if ENABLE_SSH_TUNNEL_FOR_SERVO and not self.is_localhost():
167 return self.rpc_server_tracker.xmlrpc_connect(
168 None, self.servo_port,
169 ready_test_name=self.SERVO_READY_METHOD,
170 timeout_seconds=60)
171 else:
172 remote = 'http://%s:%s' % (self.hostname, self.servo_port)
173 return xmlrpclib.ServerProxy(remote)
Fang Deng5d518f42013-08-02 14:04:32 -0700174
175
Richard Barnette9a26ad62016-06-10 12:03:08 -0700176 def is_cros_host(self):
beeps5e8c45a2013-12-17 22:05:11 -0800177 """Check if a servo host is running chromeos.
178
179 @return: True if the servo host is running chromeos.
180 False if it isn't, or we don't have enough information.
181 """
182 try:
183 result = self.run('grep -q CHROMEOS /etc/lsb-release',
184 ignore_status=True, timeout=10)
185 except (error.AutoservRunError, error.AutoservSSHTimeout):
186 return False
187 return result.exit_status == 0
188
189
Fang Deng5d518f42013-08-02 14:04:32 -0700190 def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
191 connect_timeout=None, alive_interval=None):
192 """Override default make_ssh_command to use tuned options.
193
194 Tuning changes:
195 - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
196 connection failure. Consistency with remote_access.py.
197
198 - ServerAliveInterval=180; which causes SSH to ping connection every
199 180 seconds. In conjunction with ServerAliveCountMax ensures
200 that if the connection dies, Autotest will bail out quickly.
201
202 - ServerAliveCountMax=3; consistency with remote_access.py.
203
204 - ConnectAttempts=4; reduce flakiness in connection errors;
205 consistency with remote_access.py.
206
207 - UserKnownHostsFile=/dev/null; we don't care about the keys.
208
209 - SSH protocol forced to 2; needed for ServerAliveInterval.
210
211 @param user User name to use for the ssh connection.
212 @param port Port on the target host to use for ssh connection.
213 @param opts Additional options to the ssh command.
214 @param hosts_file Ignored.
215 @param connect_timeout Ignored.
216 @param alive_interval Ignored.
217
218 @returns: An ssh command with the requested settings.
219
220 """
221 base_command = ('/usr/bin/ssh -a -x %s -o StrictHostKeyChecking=no'
222 ' -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
223 ' -o ConnectTimeout=30 -o ServerAliveInterval=180'
224 ' -o ServerAliveCountMax=3 -o ConnectionAttempts=4'
225 ' -o Protocol=2 -l %s -p %d')
226 return base_command % (opts, user, port)
227
228
229 def _make_scp_cmd(self, sources, dest):
230 """Format scp command.
231
232 Given a list of source paths and a destination path, produces the
233 appropriate scp command for encoding it. Remote paths must be
234 pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost
235 to allow additional ssh options.
236
237 @param sources: A list of source paths to copy from.
238 @param dest: Destination path to copy to.
239
240 @returns: An scp command that copies |sources| on local machine to
241 |dest| on the remote servo host.
242
243 """
244 command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no '
245 '-o UserKnownHostsFile=/dev/null -P %d %s "%s"')
246 return command % (self.master_ssh_option,
247 self.port, ' '.join(sources), dest)
248
249
250 def run(self, command, timeout=3600, ignore_status=False,
251 stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
252 connect_timeout=30, options='', stdin=None, verbose=True, args=()):
253 """Run a command on the servo host.
254
255 Extends method `run` in SSHHost. If the servo host is a remote device,
256 it will call `run` in SSHost without changing anything.
257 If the servo host is 'localhost', it will call utils.system_output.
258
259 @param command: The command line string.
260 @param timeout: Time limit in seconds before attempting to
261 kill the running process. The run() function
262 will take a few seconds longer than 'timeout'
263 to complete if it has to kill the process.
264 @param ignore_status: Do not raise an exception, no matter
265 what the exit code of the command is.
266 @param stdout_tee/stderr_tee: Where to tee the stdout/stderr.
267 @param connect_timeout: SSH connection timeout (in seconds)
268 Ignored if host is 'localhost'.
269 @param options: String with additional ssh command options
270 Ignored if host is 'localhost'.
271 @param stdin: Stdin to pass (a string) to the executed command.
272 @param verbose: Log the commands.
273 @param args: Sequence of strings to pass as arguments to command by
274 quoting them in " and escaping their contents if necessary.
275
276 @returns: A utils.CmdResult object.
277
278 @raises AutoservRunError if the command failed.
279 @raises AutoservSSHTimeout SSH connection has timed out. Only applies
280 when servo host is not 'localhost'.
281
282 """
283 run_args = {'command': command, 'timeout': timeout,
284 'ignore_status': ignore_status, 'stdout_tee': stdout_tee,
285 'stderr_tee': stderr_tee, 'stdin': stdin,
286 'verbose': verbose, 'args': args}
287 if self.is_localhost():
288 if self._sudo_required:
289 run_args['command'] = 'sudo -n %s' % command
290 try:
291 return utils.run(**run_args)
292 except error.CmdError as e:
293 logging.error(e)
294 raise error.AutoservRunError('command execution error',
295 e.result_obj)
296 else:
297 run_args['connect_timeout'] = connect_timeout
298 run_args['options'] = options
299 return super(ServoHost, self).run(**run_args)
300
301
Richard Barnette9a26ad62016-06-10 12:03:08 -0700302 def _get_release_version(self):
Dan Shi0942b1d2015-03-31 11:07:00 -0700303 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
304
305 @returns The version string in lsb-release, under attribute
306 CHROMEOS_RELEASE_VERSION.
307 """
308 lsb_release_content = self.run(
309 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
310 return lsbrelease_utils.get_chromeos_release_version(
311 lsb_release_content=lsb_release_content)
312
313
Richard Barnette3a7697f2016-04-20 11:33:27 -0700314 def _check_for_reboot(self, updater):
315 """
316 Reboot this servo host if an upgrade is waiting.
317
318 If the host has successfully downloaded and finalized a new
319 build, reboot.
320
321 @param updater: a ChromiumOSUpdater instance for checking
322 whether reboot is needed.
323 @return Return a (status, build) tuple reflecting the
324 update_engine status and current build of the host
325 at the end of the call.
326 """
Richard Barnette9a26ad62016-06-10 12:03:08 -0700327 current_build_number = self._get_release_version()
Richard Barnette3a7697f2016-04-20 11:33:27 -0700328 status = updater.check_update_status()
329 if status == autoupdater.UPDATER_NEED_REBOOT:
330 logging.info('Rebooting beaglebone host %s from build %s',
331 self.hostname, current_build_number)
332 # Tell the reboot() call not to wait for completion.
333 # Otherwise, the call will log reboot failure if servo does
334 # not come back. The logged reboot failure will lead to
335 # test job failure. If the test does not require servo, we
336 # don't want servo failure to fail the test with error:
337 # `Host did not return from reboot` in status.log.
338 reboot_cmd = 'sleep 1 ; reboot & sleep 10; reboot -f',
Richard Barnetteab9769f2016-06-01 15:01:44 -0700339 self.reboot(reboot_cmd=reboot_cmd, fastsync=True, wait=False)
Richard Barnette3a7697f2016-04-20 11:33:27 -0700340
341 # We told the reboot() call not to wait, but we need to wait
342 # for the reboot before we continue. Alas. The code from
343 # here below is basically a copy of Host.wait_for_restart(),
344 # with the logging bits ripped out, so that they can't cause
345 # the failure logging problem described above.
346 #
347 # The black stain that this has left on my soul can never be
348 # erased.
349 old_boot_id = self.get_boot_id()
350 if not self.wait_down(timeout=self.WAIT_DOWN_REBOOT_TIMEOUT,
351 warning_timer=self.WAIT_DOWN_REBOOT_WARNING,
352 old_boot_id=old_boot_id):
353 raise error.AutoservHostError(
354 'servo host %s failed to shut down.' %
355 self.hostname)
356 if self.wait_up(timeout=120):
Richard Barnette9a26ad62016-06-10 12:03:08 -0700357 current_build_number = self._get_release_version()
Richard Barnette3a7697f2016-04-20 11:33:27 -0700358 status = updater.check_update_status()
359 logging.info('servo host %s back from reboot, with build %s',
360 self.hostname, current_build_number)
361 else:
362 raise error.AutoservHostError(
363 'servo host %s failed to come back from reboot.' %
364 self.hostname)
365 return status, current_build_number
366
367
beeps5e8c45a2013-12-17 22:05:11 -0800368 @_timer.decorate
Richard Barnette3a7697f2016-04-20 11:33:27 -0700369 def update_image(self, wait_for_update=False):
beeps5e8c45a2013-12-17 22:05:11 -0800370 """Update the image on the servo host, if needed.
371
J. Richard Barnette84895392015-04-30 12:31:01 -0700372 This method recognizes the following cases:
373 * If the Host is not running Chrome OS, do nothing.
374 * If a previously triggered update is now complete, reboot
375 to the new version.
376 * If the host is processing a previously triggered update,
377 do nothing.
378 * If the host is running a version of Chrome OS different
379 from the default for servo Hosts, trigger an update, but
380 don't wait for it to complete.
beeps5e8c45a2013-12-17 22:05:11 -0800381
Richard Barnette3a7697f2016-04-20 11:33:27 -0700382 @param wait_for_update If an update needs to be applied and
383 this is true, then don't return until the update is
384 downloaded and finalized, and the host rebooted.
beeps5e8c45a2013-12-17 22:05:11 -0800385 @raises dev_server.DevServerException: If all the devservers are down.
386 @raises site_utils.ParseBuildNameException: If the devserver returns
387 an invalid build name.
388 @raises autoupdater.ChromiumOSError: If something goes wrong in the
389 checking update engine client status or applying an update.
390 @raises AutoservRunError: If the update_engine_client isn't present on
391 the host, and the host is a cros_host.
J. Richard Barnette84895392015-04-30 12:31:01 -0700392
beeps5e8c45a2013-12-17 22:05:11 -0800393 """
Dan Shib795b5a2015-09-24 13:26:35 -0700394 # servod could be running in a Ubuntu workstation.
Richard Barnette9a26ad62016-06-10 12:03:08 -0700395 if not self.is_cros_host():
beeps5e8c45a2013-12-17 22:05:11 -0800396 logging.info('Not attempting an update, either %s is not running '
397 'chromeos or we cannot find enough information about '
398 'the host.', self.hostname)
399 return
400
Dan Shib795b5a2015-09-24 13:26:35 -0700401 if lsbrelease_utils.is_moblab():
402 logging.info('Not attempting an update, %s is running moblab.',
403 self.hostname)
404 return
405
Richard Barnette3a7697f2016-04-20 11:33:27 -0700406 board = _CONFIG.get_config_value('CROS', 'servo_board')
J. Richard Barnette84895392015-04-30 12:31:01 -0700407 afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
408 target_version = afe.run('get_stable_version', board=board)
Dan Shi3b2adf62015-09-02 17:46:54 -0700409 build_pattern = _CONFIG.get_config_value(
J. Richard Barnette84895392015-04-30 12:31:01 -0700410 'CROS', 'stable_build_pattern')
411 target_build = build_pattern % (board, target_version)
412 target_build_number = server_site_utils.ParseBuildName(
413 target_build)[3]
beeps5e8c45a2013-12-17 22:05:11 -0800414 ds = dev_server.ImageServer.resolve(self.hostname)
J. Richard Barnette84895392015-04-30 12:31:01 -0700415 url = ds.get_update_url(target_build)
beeps5e8c45a2013-12-17 22:05:11 -0800416
417 updater = autoupdater.ChromiumOSUpdater(update_url=url, host=self)
Richard Barnette3a7697f2016-04-20 11:33:27 -0700418 status, current_build_number = self._check_for_reboot(updater)
419 update_pending = True
beeps5e8c45a2013-12-17 22:05:11 -0800420 if status in autoupdater.UPDATER_PROCESSING_UPDATE:
421 logging.info('servo host %s already processing an update, update '
422 'engine client status=%s', self.hostname, status)
J. Richard Barnette84895392015-04-30 12:31:01 -0700423 elif current_build_number != target_build_number:
beeps5e8c45a2013-12-17 22:05:11 -0800424 logging.info('Using devserver url: %s to trigger update on '
425 'servo host %s, from %s to %s', url, self.hostname,
J. Richard Barnette84895392015-04-30 12:31:01 -0700426 current_build_number, target_build_number)
beeps5e8c45a2013-12-17 22:05:11 -0800427 try:
J. Richard Barnette84895392015-04-30 12:31:01 -0700428 ds.stage_artifacts(target_build,
429 artifacts=['full_payload'])
430 except Exception as e:
431 logging.error('Staging artifacts failed: %s', str(e))
432 logging.error('Abandoning update for this cycle.')
beeps5e8c45a2013-12-17 22:05:11 -0800433 else:
J. Richard Barnette84895392015-04-30 12:31:01 -0700434 try:
Richard Barnette7e53aa02016-05-20 10:49:40 -0700435 # TODO(jrbarnette): This 'touch' is a gross hack
436 # to get us past crbug.com/613603. Once that
437 # bug is resolved, we should remove this code.
438 self.run('touch /home/chronos/.oobe_completed')
J. Richard Barnette84895392015-04-30 12:31:01 -0700439 updater.trigger_update()
440 except autoupdater.RootFSUpdateError as e:
441 trigger_download_status = 'failed with %s' % str(e)
442 autotest_stats.Counter(
443 'servo_host.RootFSUpdateError').increment()
444 else:
445 trigger_download_status = 'passed'
446 logging.info('Triggered download and update %s for %s, '
447 'update engine currently in status %s',
448 trigger_download_status, self.hostname,
449 updater.check_update_status())
beeps5e8c45a2013-12-17 22:05:11 -0800450 else:
451 logging.info('servo host %s does not require an update.',
452 self.hostname)
Richard Barnette3a7697f2016-04-20 11:33:27 -0700453 update_pending = False
454
455 if update_pending and wait_for_update:
456 logging.info('Waiting for servo update to complete.')
457 self.run('update_engine_client --follow', ignore_status=True)
beeps5e8c45a2013-12-17 22:05:11 -0800458
459
Richard Barnette9a26ad62016-06-10 12:03:08 -0700460 def verify(self):
461 """Update the servo host and verify it's in a good state."""
Richard Barnette79d78c42016-05-25 09:31:21 -0700462 # TODO(jrbarnette) Old versions of beaglebone_servo include
Richard Barnette9a26ad62016-06-10 12:03:08 -0700463 # the powerd package. If you touch the .oobe_completed file
464 # (as we do to work around an update_engine problem), then
465 # powerd will eventually shut down the beaglebone for lack
466 # of (apparent) activity. Current versions of
Richard Barnette79d78c42016-05-25 09:31:21 -0700467 # beaglebone_servo don't have powerd, but until we can purge
468 # the lab of the old images, we need to make sure powerd
469 # isn't running.
470 self.run('stop powerd', ignore_status=True)
Richard Barnette9a26ad62016-06-10 12:03:08 -0700471 try:
472 self._repair_strategy.verify(self)
473 except:
474 self.disconnect_servo()
475 raise
Fang Deng5d518f42013-08-02 14:04:32 -0700476
477
Richard Barnette9a26ad62016-06-10 12:03:08 -0700478 def repair(self):
479 """Attempt to repair servo host."""
480 try:
481 self._repair_strategy.repair(self)
482 except:
483 self.disconnect_servo()
484 raise
Fang Deng5d518f42013-08-02 14:04:32 -0700485
486
Fang Dengd4fe7392013-09-20 12:18:21 -0700487 def has_power(self):
488 """Return whether or not the servo host is powered by PoE."""
489 # TODO(fdeng): See crbug.com/302791
490 # For now, assume all servo hosts in the lab have power.
491 return self.is_in_lab()
492
493
494 def power_cycle(self):
495 """Cycle power to this host via PoE if it is a lab device.
496
Richard Barnette9a26ad62016-06-10 12:03:08 -0700497 @raises AutoservRepairError if it fails to power cycle the
Fang Dengd4fe7392013-09-20 12:18:21 -0700498 servo host.
499
500 """
501 if self.has_power():
502 try:
503 rpm_client.set_power(self.hostname, 'CYCLE')
504 except (socket.error, xmlrpclib.Error,
505 httplib.BadStatusLine,
506 rpm_client.RemotePowerException) as e:
Richard Barnette9a26ad62016-06-10 12:03:08 -0700507 raise hosts.AutoservRepairError(
Fang Dengd4fe7392013-09-20 12:18:21 -0700508 'Power cycling %s failed: %s' % (self.hostname, e))
509 else:
510 logging.info('Skipping power cycling, not a lab device.')
511
512
Dan Shi4d478522014-02-14 13:46:32 -0800513 def get_servo(self):
514 """Get the cached servo.Servo object.
Fang Deng5d518f42013-08-02 14:04:32 -0700515
Dan Shi4d478522014-02-14 13:46:32 -0800516 @return: a servo.Servo object.
Fang Deng5d518f42013-08-02 14:04:32 -0700517 """
Dan Shi4d478522014-02-14 13:46:32 -0800518 return self._servo
519
520
Richard Barnetteea3e4602016-06-10 12:36:41 -0700521def make_servo_hostname(dut_hostname):
522 """Given a DUT's hostname, return the hostname of its servo.
523
524 @param dut_hostname: hostname of a DUT.
525
526 @return hostname of the DUT's servo.
527
528 """
529 host_parts = dut_hostname.split('.')
530 host_parts[0] = host_parts[0] + '-servo'
531 return '.'.join(host_parts)
532
533
534def servo_host_is_up(servo_hostname):
535 """
536 Given a servo host name, return if it's up or not.
537
538 @param servo_hostname: hostname of the servo host.
539
540 @return True if it's up, False otherwise
541 """
542 # Technically, this duplicates the SSH ping done early in the servo
543 # proxy initialization code. However, this ping ends in a couple
544 # seconds when if fails, rather than the 60 seconds it takes to decide
545 # that an SSH ping has timed out. Specifically, that timeout happens
546 # when our servo DNS name resolves, but there is no host at that IP.
547 logging.info('Pinging servo host at %s', servo_hostname)
548 ping_config = ping_runner.PingConfig(
549 servo_hostname, count=3,
550 ignore_result=True, ignore_status=True)
551 return ping_runner.PingRunner().ping(ping_config).received > 0
552
553
Richard Barnettee519dcd2016-08-15 17:37:17 -0700554def _map_afe_board_to_servo_board(afe_board):
555 """Map a board we get from the AFE to a servo appropriate value.
556
557 Many boards are identical to other boards for servo's purposes.
558 This function makes that mapping.
559
560 @param afe_board string board name received from AFE.
561 @return board we expect servo to have.
562
563 """
564 KNOWN_SUFFIXES = ['-freon', '_freon', '_moblab', '-cheets']
565 BOARD_MAP = {'gizmo': 'panther'}
566 mapped_board = afe_board
567 if afe_board in BOARD_MAP:
568 mapped_board = BOARD_MAP[afe_board]
569 else:
570 for suffix in KNOWN_SUFFIXES:
571 if afe_board.endswith(suffix):
572 mapped_board = afe_board[0:-len(suffix)]
573 break
574 if mapped_board != afe_board:
575 logging.info('Mapping AFE board=%s to %s', afe_board, mapped_board)
576 return mapped_board
577
578
Richard Barnetteea3e4602016-06-10 12:36:41 -0700579def _get_standard_servo_args(dut_host):
580 """
581 Return servo data associated with a given DUT.
582
583 This checks for the presence of servo host and port attached to the
584 given `dut_host`. This data should be stored in the
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700585 `_afe_host.attributes` field in the provided `dut_host` parameter.
Richard Barnetteea3e4602016-06-10 12:36:41 -0700586
587 @param dut_host Instance of `Host` on which to find the servo
588 attributes.
589 @return A tuple of `servo_args` dict with host and an option port,
590 plus an `is_in_lab` flag indicating whether this in the CrOS
591 test lab, or some different environment.
592 """
593 servo_args = None
594 is_in_lab = False
595 is_ssp_moblab = False
596 if utils.is_in_container():
597 is_moblab = _CONFIG.get_config_value(
598 'SSP', 'is_moblab', type=bool, default=False)
599 is_ssp_moblab = is_moblab
600 else:
601 is_moblab = utils.is_moblab()
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700602 attrs = dut_host._afe_host.attributes
Richard Barnetteea3e4602016-06-10 12:36:41 -0700603 if attrs and SERVO_HOST_ATTR in attrs:
604 servo_host = attrs[SERVO_HOST_ATTR]
605 if (is_ssp_moblab and servo_host in ['localhost', '127.0.0.1']):
606 servo_host = _CONFIG.get_config_value(
607 'SSP', 'host_container_ip', type=str, default=None)
608 servo_args = {SERVO_HOST_ATTR: servo_host}
609 if SERVO_PORT_ATTR in attrs:
Kevin Cheng692e5292016-08-14 00:23:24 -0700610 try:
611 servo_port = attrs[SERVO_PORT_ATTR]
612 servo_args[SERVO_PORT_ATTR] = int(servo_port)
613 except ValueError:
614 logging.error('servo port is not an int: %s', servo_port)
615 # Let's set the servo args to None since we're not creating
616 # the ServoHost object with the proper port now.
617 servo_args = None
Richard Barnetteea3e4602016-06-10 12:36:41 -0700618 is_in_lab = (not is_moblab
619 and utils.host_is_in_lab_zone(servo_host))
620
621 # TODO(jrbarnette): This test to use the default lab servo hostname
622 # is a legacy that we need only until every host in the DB has
623 # proper attributes.
624 elif (not is_moblab and
625 not dnsname_mangler.is_ip_address(dut_host.hostname)):
626 servo_host = make_servo_hostname(dut_host.hostname)
627 is_in_lab = utils.host_is_in_lab_zone(servo_host)
628 if is_in_lab:
629 servo_args = {SERVO_HOST_ATTR: servo_host}
Richard Barnette9a26ad62016-06-10 12:03:08 -0700630 if servo_args is not None:
631 servo_board = afe_utils.get_board(dut_host)
632 if servo_board is not None:
633 servo_board = _map_afe_board_to_servo_board(servo_board)
634 servo_args[SERVO_BOARD_ATTR] = servo_board
Richard Barnetteea3e4602016-06-10 12:36:41 -0700635 return servo_args, is_in_lab
636
637
Dan Shi023aae32016-05-25 11:13:01 -0700638def create_servo_host(dut, servo_args, try_lab_servo=False,
Richard Barnette9a26ad62016-06-10 12:03:08 -0700639 try_servo_repair=False):
Richard Barnetteea3e4602016-06-10 12:36:41 -0700640 """
641 Create a ServoHost object for a given DUT, if appropriate.
Dan Shi4d478522014-02-14 13:46:32 -0800642
Richard Barnette9a26ad62016-06-10 12:03:08 -0700643 This function attempts to create and verify or repair a `ServoHost`
644 object for a servo connected to the given `dut`, subject to various
645 constraints imposed by the parameters:
646 * When the `servo_args` parameter is not `None`, a servo
647 host must be created, and must be checked with `repair()`.
648 * Otherwise, if a servo exists in the lab and `try_lab_servo` is
649 true:
650 * If `try_servo_repair` is true, then create a servo host and
651 check it with `repair()`.
652 * Otherwise, if the servo responds to `ping` then create a
653 servo host and check it with `verify()`.
Fang Denge545abb2014-12-30 18:43:47 -0800654
Richard Barnette9a26ad62016-06-10 12:03:08 -0700655 In cases where `servo_args` was not `None`, repair failure
656 exceptions are passed back to the caller; otherwise, exceptions
657 are logged and then discarded.
658
659 Parameters for a servo host consist of a host name, port number, and
660 DUT board, and are determined from one of these sources, in order of
661 priority:
Richard Barnetteea3e4602016-06-10 12:36:41 -0700662 * Servo attributes from the `dut` parameter take precedence over
663 all other sources of information.
664 * If a DNS entry for the servo based on the DUT hostname exists in
665 the CrOS lab network, that hostname is used with the default
Richard Barnette9a26ad62016-06-10 12:03:08 -0700666 port and the DUT's board.
Richard Barnetteea3e4602016-06-10 12:36:41 -0700667 * If no other options are found, the parameters will be taken
Richard Barnette9a26ad62016-06-10 12:03:08 -0700668 from the `servo_args` dict passed in from the caller.
Richard Barnetteea3e4602016-06-10 12:36:41 -0700669
670 @param dut An instance of `Host` from which to take
671 servo parameters (if available).
672 @param servo_args A dictionary with servo parameters to use if
673 they can't be found from `dut`. If this
674 argument is supplied, unrepaired exceptions
675 from `verify()` will be passed back to the
676 caller.
677 @param try_lab_servo If not true, servo host creation will be
678 skipped unless otherwise required by the
679 caller.
Richard Barnette9a26ad62016-06-10 12:03:08 -0700680 @param try_servo_repair If true, check a servo host with
681 `repair()` instead of `verify()`.
Dan Shi4d478522014-02-14 13:46:32 -0800682
683 @returns: A ServoHost object or None. See comments above.
684
685 """
Richard Barnette9a26ad62016-06-10 12:03:08 -0700686 require_repair = servo_args is not None
Richard Barnetteea3e4602016-06-10 12:36:41 -0700687 is_in_lab = False
Richard Barnette9a26ad62016-06-10 12:03:08 -0700688 if dut is not None and (try_lab_servo or require_repair):
Richard Barnetteea3e4602016-06-10 12:36:41 -0700689 servo_args_override, is_in_lab = _get_standard_servo_args(dut)
690 if servo_args_override is not None:
691 servo_args = servo_args_override
692 if servo_args is None:
693 return None
Richard Barnette9a26ad62016-06-10 12:03:08 -0700694 if (not require_repair and not try_servo_repair and
695 not servo_host_is_up(servo_args[SERVO_HOST_ATTR])):
Dan Shibbb0cb62014-03-24 17:50:57 -0700696 return None
Richard Barnette9a26ad62016-06-10 12:03:08 -0700697 newhost = ServoHost(is_in_lab=is_in_lab, **servo_args)
698 # Note that the logic of repair() includes everything done
699 # by verify(). It's sufficient to call one or the other;
700 # we don't need both.
701 if require_repair:
702 newhost.repair()
703 else:
704 try:
705 if try_servo_repair:
706 newhost.repair()
707 else:
708 newhost.verify()
709 except Exception as e:
710 operation = 'repair' if try_servo_repair else 'verification'
711 logging.exception('Servo %s failed for %s',
712 operation, newhost.hostname)
713 return newhost