blob: b0c864424c218e995a125dde09b8e937a1623fb1 [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
15import time
16import xmlrpclib
17
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
21from autotest_lib.client.common_lib.cros import autoupdater
22from autotest_lib.client.common_lib.cros import dev_server
Fang Deng5d518f42013-08-02 14:04:32 -070023from autotest_lib.client.common_lib.cros import retry
beeps5e8c45a2013-12-17 22:05:11 -080024from autotest_lib.server import site_utils as server_site_utils
Fang Deng5d518f42013-08-02 14:04:32 -070025from autotest_lib.server.cros.servo import servo
26from autotest_lib.server.hosts import ssh_host
Fang Dengf0ea6142013-10-10 21:43:16 -070027from autotest_lib.site_utils.graphite import stats
Fang Dengd4fe7392013-09-20 12:18:21 -070028from autotest_lib.site_utils.rpm_control_system import rpm_client
Fang Deng5d518f42013-08-02 14:04:32 -070029
30
31class ServoHostException(error.AutoservError):
32 """This is the base class for exceptions raised by ServoHost."""
33 pass
34
35
36class ServoHostVerifyFailure(ServoHostException):
37 """Raised when servo verification fails."""
38 pass
39
40
Fang Dengd4fe7392013-09-20 12:18:21 -070041class ServoHostRepairFailure(ServoHostException):
42 """Raised when a repair method fails to repair a servo host."""
43 pass
44
45
Fang Dengf0ea6142013-10-10 21:43:16 -070046class ServoHostRepairMethodNA(ServoHostException):
47 """Raised when a repair method is not applicable."""
48 pass
49
50
Fang Deng5d518f42013-08-02 14:04:32 -070051class ServoHostRepairTotalFailure(ServoHostException):
52 """Raised if all attempts to repair a servo host fail."""
53 pass
54
55
56def make_servo_hostname(dut_hostname):
57 """Given a DUT's hostname, return the hostname of its servo.
58
59 @param dut_hostname: hostname of a DUT.
60
61 @return hostname of the DUT's servo.
62
63 """
64 host_parts = dut_hostname.split('.')
65 host_parts[0] = host_parts[0] + '-servo'
66 return '.'.join(host_parts)
67
68
69class ServoHost(ssh_host.SSHHost):
70 """Host class for a host that controls a servo, e.g. beaglebone."""
71
72 # Timeout for getting the value of 'pwr_button'.
73 PWR_BUTTON_CMD_TIMEOUT_SECS = 15
74 # Timeout for rebooting servo host.
75 REBOOT_TIMEOUT_SECS = 90
76 HOST_DOWN_TIMEOUT_SECS = 60
77 # Delay after rebooting for servod to become fully functional.
78 REBOOT_DELAY_SECS = 20
79 # Servod process name.
80 SERVOD_PROCESS = 'servod'
81
Fang Dengd4fe7392013-09-20 12:18:21 -070082 _MAX_POWER_CYCLE_ATTEMPTS = 3
beeps5e8c45a2013-12-17 22:05:11 -080083 _timer = stats.Timer('servo_host')
Fang Dengd4fe7392013-09-20 12:18:21 -070084
Fang Deng5d518f42013-08-02 14:04:32 -070085
86 def _initialize(self, servo_host='localhost', servo_port=9999,
Dan Shi4d478522014-02-14 13:46:32 -080087 required_by_test=True, is_in_lab=None, *args, **dargs):
Fang Deng5d518f42013-08-02 14:04:32 -070088 """Initialize a ServoHost instance.
89
90 A ServoHost instance represents a host that controls a servo.
91
92 @param servo_host: Name of the host where the servod process
93 is running.
94 @param servo_port: Port the servod process is listening on.
Dan Shi4d478522014-02-14 13:46:32 -080095 @param required_by_test: True if servo is required by test.
96 @param is_in_lab: True if the servo host is in Cros Lab. Default is set
97 to None, for which utils.host_is_in_lab_zone will be
98 called to check if the servo host is in Cros lab.
Fang Deng5d518f42013-08-02 14:04:32 -070099
100 """
101 super(ServoHost, self)._initialize(hostname=servo_host,
102 *args, **dargs)
Dan Shi4d478522014-02-14 13:46:32 -0800103 if is_in_lab is None:
104 self._is_in_lab = utils.host_is_in_lab_zone(self.hostname)
105 else:
106 self._is_in_lab = is_in_lab
Fang Deng5d518f42013-08-02 14:04:32 -0700107 self._is_localhost = (self.hostname == 'localhost')
108 remote = 'http://%s:%s' % (self.hostname, servo_port)
109 self._servod_server = xmlrpclib.ServerProxy(remote)
110 # Commands on the servo host must be run by the superuser. Our account
111 # on Beaglebone is root, but locally we might be running as a
112 # different user. If so - `sudo ' will have to be added to the
113 # commands.
114 if self._is_localhost:
115 self._sudo_required = utils.system_output('id -u') != '0'
116 else:
117 self._sudo_required = False
Dan Shi4d478522014-02-14 13:46:32 -0800118 # Create a cache of Servo object. This must be called at the end of
119 # _initialize to make sure all attributes are set.
120 self._servo = None
121 try:
122 self.verify()
123 except:
Dan Shi0a1e0592014-03-06 14:36:58 -0800124 if required_by_test and self.is_in_lab():
Dan Shi4d478522014-02-14 13:46:32 -0800125 self.repair_full()
Dan Shi0a1e0592014-03-06 14:36:58 -0800126 else:
127 raise
Fang Deng5d518f42013-08-02 14:04:32 -0700128
129
130 def is_in_lab(self):
131 """Check whether the servo host is a lab device.
132
133 @returns: True if the servo host is in Cros Lab, otherwise False.
134
135 """
136 return self._is_in_lab
137
138
139 def is_localhost(self):
140 """Checks whether the servo host points to localhost.
141
142 @returns: True if it points to localhost, otherwise False.
143
144 """
145 return self._is_localhost
146
147
148 def get_servod_server_proxy(self):
149 """Return a proxy that can be used to communicate with servod server.
150
151 @returns: An xmlrpclib.ServerProxy that is connected to the servod
152 server on the host.
153
154 """
155 return self._servod_server
156
157
158 def get_wait_up_processes(self):
159 """Get the list of local processes to wait for in wait_up.
160
161 Override get_wait_up_processes in
162 autotest_lib.client.common_lib.hosts.base_classes.Host.
163 Wait for servod process to go up. Called by base class when
164 rebooting the device.
165
166 """
167 processes = [self.SERVOD_PROCESS]
168 return processes
169
170
beeps5e8c45a2013-12-17 22:05:11 -0800171 def _is_cros_host(self):
172 """Check if a servo host is running chromeos.
173
174 @return: True if the servo host is running chromeos.
175 False if it isn't, or we don't have enough information.
176 """
177 try:
178 result = self.run('grep -q CHROMEOS /etc/lsb-release',
179 ignore_status=True, timeout=10)
180 except (error.AutoservRunError, error.AutoservSSHTimeout):
181 return False
182 return result.exit_status == 0
183
184
Fang Deng5d518f42013-08-02 14:04:32 -0700185 def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
186 connect_timeout=None, alive_interval=None):
187 """Override default make_ssh_command to use tuned options.
188
189 Tuning changes:
190 - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
191 connection failure. Consistency with remote_access.py.
192
193 - ServerAliveInterval=180; which causes SSH to ping connection every
194 180 seconds. In conjunction with ServerAliveCountMax ensures
195 that if the connection dies, Autotest will bail out quickly.
196
197 - ServerAliveCountMax=3; consistency with remote_access.py.
198
199 - ConnectAttempts=4; reduce flakiness in connection errors;
200 consistency with remote_access.py.
201
202 - UserKnownHostsFile=/dev/null; we don't care about the keys.
203
204 - SSH protocol forced to 2; needed for ServerAliveInterval.
205
206 @param user User name to use for the ssh connection.
207 @param port Port on the target host to use for ssh connection.
208 @param opts Additional options to the ssh command.
209 @param hosts_file Ignored.
210 @param connect_timeout Ignored.
211 @param alive_interval Ignored.
212
213 @returns: An ssh command with the requested settings.
214
215 """
216 base_command = ('/usr/bin/ssh -a -x %s -o StrictHostKeyChecking=no'
217 ' -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
218 ' -o ConnectTimeout=30 -o ServerAliveInterval=180'
219 ' -o ServerAliveCountMax=3 -o ConnectionAttempts=4'
220 ' -o Protocol=2 -l %s -p %d')
221 return base_command % (opts, user, port)
222
223
224 def _make_scp_cmd(self, sources, dest):
225 """Format scp command.
226
227 Given a list of source paths and a destination path, produces the
228 appropriate scp command for encoding it. Remote paths must be
229 pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost
230 to allow additional ssh options.
231
232 @param sources: A list of source paths to copy from.
233 @param dest: Destination path to copy to.
234
235 @returns: An scp command that copies |sources| on local machine to
236 |dest| on the remote servo host.
237
238 """
239 command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no '
240 '-o UserKnownHostsFile=/dev/null -P %d %s "%s"')
241 return command % (self.master_ssh_option,
242 self.port, ' '.join(sources), dest)
243
244
245 def run(self, command, timeout=3600, ignore_status=False,
246 stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
247 connect_timeout=30, options='', stdin=None, verbose=True, args=()):
248 """Run a command on the servo host.
249
250 Extends method `run` in SSHHost. If the servo host is a remote device,
251 it will call `run` in SSHost without changing anything.
252 If the servo host is 'localhost', it will call utils.system_output.
253
254 @param command: The command line string.
255 @param timeout: Time limit in seconds before attempting to
256 kill the running process. The run() function
257 will take a few seconds longer than 'timeout'
258 to complete if it has to kill the process.
259 @param ignore_status: Do not raise an exception, no matter
260 what the exit code of the command is.
261 @param stdout_tee/stderr_tee: Where to tee the stdout/stderr.
262 @param connect_timeout: SSH connection timeout (in seconds)
263 Ignored if host is 'localhost'.
264 @param options: String with additional ssh command options
265 Ignored if host is 'localhost'.
266 @param stdin: Stdin to pass (a string) to the executed command.
267 @param verbose: Log the commands.
268 @param args: Sequence of strings to pass as arguments to command by
269 quoting them in " and escaping their contents if necessary.
270
271 @returns: A utils.CmdResult object.
272
273 @raises AutoservRunError if the command failed.
274 @raises AutoservSSHTimeout SSH connection has timed out. Only applies
275 when servo host is not 'localhost'.
276
277 """
278 run_args = {'command': command, 'timeout': timeout,
279 'ignore_status': ignore_status, 'stdout_tee': stdout_tee,
280 'stderr_tee': stderr_tee, 'stdin': stdin,
281 'verbose': verbose, 'args': args}
282 if self.is_localhost():
283 if self._sudo_required:
284 run_args['command'] = 'sudo -n %s' % command
285 try:
286 return utils.run(**run_args)
287 except error.CmdError as e:
288 logging.error(e)
289 raise error.AutoservRunError('command execution error',
290 e.result_obj)
291 else:
292 run_args['connect_timeout'] = connect_timeout
293 run_args['options'] = options
294 return super(ServoHost, self).run(**run_args)
295
296
297 def _check_servod(self):
298 """A sanity check of the servod state."""
299 msg_prefix = 'Servod error: %s'
300 error_msg = None
301 try:
302 timeout, _ = retry.timeout(
303 self._servod_server.get, args=('pwr_button', ),
304 timeout_sec=self.PWR_BUTTON_CMD_TIMEOUT_SECS)
305 if timeout:
306 error_msg = msg_prefix % 'Request timed out.'
307 except (socket.error, xmlrpclib.Error, httplib.BadStatusLine) as e:
308 error_msg = msg_prefix % e
309 if error_msg:
310 raise ServoHostVerifyFailure(error_msg)
311
312
313 def _check_servo_host_usb(self):
314 """A sanity check of the USB device.
315
316 Sometimes the usb gets wedged due to a kernel bug on the beaglebone.
317 A symptom is the presence of /dev/sda without /dev/sda1. The check
318 here ensures that if /dev/sda exists, /dev/sda1 must also exist.
319 See crbug.com/225932.
320
321 @raises ServoHostVerifyFailure if /dev/sda exists without /dev/sda1 on
322 the beaglebone.
323
324 """
325 try:
326 # The following test exits with a non-zero code
327 # and raises AutoserverRunError if error is detected.
328 self.run('test ! -b /dev/sda -o -b /dev/sda1')
329 except (error.AutoservRunError, error.AutoservSSHTimeout) as e:
330 raise ServoHostVerifyFailure(
331 'USB sanity check on %s failed: %s' % (self.hostname, e))
332
333
beeps5e8c45a2013-12-17 22:05:11 -0800334 @_timer.decorate
335 def _update_image(self):
336 """Update the image on the servo host, if needed.
337
338 This method does nothing for servo hosts that are not running chromeos.
339 If the host is running chromeos, and a newer image is available on the
340 devserver, trigger a download and apply it in the background. If an
341 update has already been downloaded and applied, reboot the servo host
342 into the new image. If update_engine_client is in the process of
343 applying an update that was triggered on a previous invocation, do
344 nothing.
345
346 @raises dev_server.DevServerException: If all the devservers are down.
347 @raises site_utils.ParseBuildNameException: If the devserver returns
348 an invalid build name.
349 @raises autoupdater.ChromiumOSError: If something goes wrong in the
350 checking update engine client status or applying an update.
351 @raises AutoservRunError: If the update_engine_client isn't present on
352 the host, and the host is a cros_host.
353 """
354 #TODO(beeps): Remove this check once all servo hosts are using chromeos.
355 if not self._is_cros_host():
356 logging.info('Not attempting an update, either %s is not running '
357 'chromeos or we cannot find enough information about '
358 'the host.', self.hostname)
359 return
360
361 update_branch = global_config.global_config.get_config_value(
362 'CROS', 'servo_builder')
363 ds = dev_server.ImageServer.resolve(self.hostname)
364 latest_build = ds.get_latest_build_in_server(target=update_branch)
365
366 # We might have just purged all the beaglebone builds on the devserver
367 # after having triggered a download the last time we verified this
368 # beaglebone, so we still need to reboot if necessary.
369 if latest_build is None:
370 logging.debug('Could not find any builds for %s on %s',
371 update_branch, ds.url())
372 url = ds.url()
373 latest_build_number = None
374 else:
375 latest_build = '%s/%s' % (update_branch, latest_build)
376 latest_build_number = server_site_utils.ParseBuildName(
377 latest_build)[3]
378 url = ds.get_update_url(latest_build)
379
380 updater = autoupdater.ChromiumOSUpdater(update_url=url, host=self)
381 current_build_number = updater.get_build_id()
382 status = updater.check_update_status()
383
384 if status == autoupdater.UPDATER_NEED_REBOOT:
385 logging.info('Rebooting beaglebone host %s with build %s',
386 self.hostname, current_build_number)
387 kwargs = {
388 'reboot_cmd': ('((reboot & sleep 10; reboot -f &) '
389 '</dev/null >/dev/null 2>&1 &)'),
390 'fastsync': True,
391 'label': None,
392 'wait': True,
393 }
394 self.reboot(**kwargs)
395 current_build_number = updater.get_build_id()
396 logging.info('servo host %s back from reboot, with build %s',
397 self.hostname, current_build_number)
398
399 if status in autoupdater.UPDATER_PROCESSING_UPDATE:
400 logging.info('servo host %s already processing an update, update '
401 'engine client status=%s', self.hostname, status)
402 elif (latest_build_number and
403 current_build_number != latest_build_number):
404 logging.info('Using devserver url: %s to trigger update on '
405 'servo host %s, from %s to %s', url, self.hostname,
406 current_build_number, latest_build_number)
407 try:
408 updater.trigger_update()
409 except autoupdater.RootFSUpdateError as e:
410 trigger_download_status = 'failed with %s' % str(e)
411 stats.Counter('servo_host.RootFSUpdateError').increment()
412 else:
413 trigger_download_status = 'passed'
414 logging.info('Triggered download and update %s for %s, '
415 'update engine currently in status %s',
416 trigger_download_status, self.hostname,
417 updater.check_update_status())
418 else:
419 logging.info('servo host %s does not require an update.',
420 self.hostname)
421
422
Fang Deng5d518f42013-08-02 14:04:32 -0700423 def verify_software(self):
beeps5e8c45a2013-12-17 22:05:11 -0800424 """Update the servo host and verify it's in a good state.
Fang Deng5d518f42013-08-02 14:04:32 -0700425
426 It overrides the base class function for verify_software.
beeps5e8c45a2013-12-17 22:05:11 -0800427 If an update is available, downloads and applies it. Then verifies:
Fang Deng5d518f42013-08-02 14:04:32 -0700428 1) Whether basic servo command can run successfully.
429 2) Whether USB is in a good state. crbug.com/225932
430
431 @raises ServoHostVerifyFailure if servo host does not pass the checks.
432
433 """
beeps5e8c45a2013-12-17 22:05:11 -0800434 logging.info('Applying an update to the servo host, if necessary.')
435 self._update_image()
436
Fang Deng5d518f42013-08-02 14:04:32 -0700437 logging.info('Verifying servo host %s with sanity checks.',
438 self.hostname)
Fang Deng5d518f42013-08-02 14:04:32 -0700439 self._check_servo_host_usb()
Dan Shi4d478522014-02-14 13:46:32 -0800440 # If servo is already initialized, we don't need to do it again, call
441 # _check_servod should be enough.
442 if self._servo:
443 self._check_servod()
444 else:
445 self._servo = servo.Servo(servo_host=self)
446
Fang Deng5d518f42013-08-02 14:04:32 -0700447 logging.info('Sanity checks pass on servo host %s', self.hostname)
448
449
450 def _repair_with_sysrq_reboot(self):
451 """Reboot with magic SysRq key."""
452 self.reboot(timeout=self.REBOOT_TIMEOUT_SECS,
453 down_timeout=self.HOST_DOWN_TIMEOUT_SECS,
454 reboot_cmd='echo "b" > /proc/sysrq-trigger &',
455 fastsync=True)
456 time.sleep(self.REBOOT_DELAY_SECS)
457
458
Fang Dengd4fe7392013-09-20 12:18:21 -0700459 def has_power(self):
460 """Return whether or not the servo host is powered by PoE."""
461 # TODO(fdeng): See crbug.com/302791
462 # For now, assume all servo hosts in the lab have power.
463 return self.is_in_lab()
464
465
466 def power_cycle(self):
467 """Cycle power to this host via PoE if it is a lab device.
468
469 @raises ServoHostRepairFailure if it fails to power cycle the
470 servo host.
471
472 """
473 if self.has_power():
474 try:
475 rpm_client.set_power(self.hostname, 'CYCLE')
476 except (socket.error, xmlrpclib.Error,
477 httplib.BadStatusLine,
478 rpm_client.RemotePowerException) as e:
479 raise ServoHostRepairFailure(
480 'Power cycling %s failed: %s' % (self.hostname, e))
481 else:
482 logging.info('Skipping power cycling, not a lab device.')
483
484
Fang Deng5d518f42013-08-02 14:04:32 -0700485 def _powercycle_to_repair(self):
Fang Dengd4fe7392013-09-20 12:18:21 -0700486 """Power cycle the servo host using PoE.
487
488 @raises ServoHostRepairFailure if it fails to fix the servo host.
Fang Dengf0ea6142013-10-10 21:43:16 -0700489 @raises ServoHostRepairMethodNA if it does not support power.
Fang Dengd4fe7392013-09-20 12:18:21 -0700490
491 """
492 if not self.has_power():
Fang Dengf0ea6142013-10-10 21:43:16 -0700493 raise ServoHostRepairMethodNA('%s does not support power.' %
494 self.hostname)
Fang Dengd4fe7392013-09-20 12:18:21 -0700495 logging.info('Attempting repair via PoE powercycle.')
496 failed_cycles = 0
497 self.power_cycle()
498 while not self.wait_up(timeout=self.REBOOT_TIMEOUT_SECS):
499 failed_cycles += 1
500 if failed_cycles >= self._MAX_POWER_CYCLE_ATTEMPTS:
501 raise ServoHostRepairFailure(
502 'Powercycled host %s %d times; device did not come back'
503 ' online.' % (self.hostname, failed_cycles))
504 self.power_cycle()
505 logging.info('Powercycling was successful after %d failures.',
506 failed_cycles)
507 # Allow some time for servod to get started.
508 time.sleep(self.REBOOT_DELAY_SECS)
Fang Deng5d518f42013-08-02 14:04:32 -0700509
510
511 def repair_full(self):
512 """Attempt to repair servo host.
513
514 This overrides the base class function for repair.
515 Note if the host is not in Cros Lab, the repair procedure
516 will be skipped.
517
518 @raises ServoHostRepairTotalFailure if all attempts fail.
519
520 """
521 if not self.is_in_lab():
522 logging.warn('Skip repairing servo host %s: Not a lab device.',
523 self.hostname)
524 return
525 logging.info('Attempting to repair servo host %s.', self.hostname)
Dan Shi4d478522014-02-14 13:46:32 -0800526 # Reset the cache to guarantee servo initialization being called later.
527 self._servo = None
Dan Shi0cf92c82014-02-20 15:45:01 -0800528 # TODO(dshi): add self._powercycle_to_repair back to repair_funcs
529 # after crbug.com/336606 is fixed.
530 repair_funcs = [self._repair_with_sysrq_reboot,]
Fang Deng5d518f42013-08-02 14:04:32 -0700531 errors = []
532 for repair_func in repair_funcs:
Fang Dengf0ea6142013-10-10 21:43:16 -0700533 counter_prefix = 'servo_host_repair.%s.' % repair_func.__name__
Fang Deng5d518f42013-08-02 14:04:32 -0700534 try:
535 repair_func()
536 self.verify()
Fang Dengf0ea6142013-10-10 21:43:16 -0700537 stats.Counter(counter_prefix + 'SUCCEEDED').increment()
Fang Deng5d518f42013-08-02 14:04:32 -0700538 return
Fang Dengf0ea6142013-10-10 21:43:16 -0700539 except ServoHostRepairMethodNA as e:
540 logging.warn('Repair method NA: %s', e)
541 stats.Counter(counter_prefix + 'RepairNA').increment()
542 errors.append(str(e))
Fang Deng5d518f42013-08-02 14:04:32 -0700543 except Exception as e:
544 logging.warn('Failed to repair servo: %s', e)
Fang Dengf0ea6142013-10-10 21:43:16 -0700545 stats.Counter(counter_prefix + 'FAILED').increment()
Fang Deng5d518f42013-08-02 14:04:32 -0700546 errors.append(str(e))
Fang Dengf0ea6142013-10-10 21:43:16 -0700547 stats.Counter('servo_host_repair.Full_Repair_Failed').increment()
Fang Deng5d518f42013-08-02 14:04:32 -0700548 raise ServoHostRepairTotalFailure(
549 'All attempts at repairing the servo failed:\n%s' %
550 '\n'.join(errors))
551
552
Dan Shi4d478522014-02-14 13:46:32 -0800553 def get_servo(self):
554 """Get the cached servo.Servo object.
Fang Deng5d518f42013-08-02 14:04:32 -0700555
Dan Shi4d478522014-02-14 13:46:32 -0800556 @return: a servo.Servo object.
Fang Deng5d518f42013-08-02 14:04:32 -0700557 """
Dan Shi4d478522014-02-14 13:46:32 -0800558 return self._servo
559
560
561def create_servo_host(dut, servo_args):
562 """Create a ServoHost object.
563
564 There three possible cases:
565 1) If the DUT is in Cros Lab and has a beaglebone and a servo, then
566 create a ServoHost object pointing to the beaglebone. servo_args
567 is ignored.
568 2) If not case 1) and servo_args is neither None nor empty, then
569 create a ServoHost object using servo_args.
570 3) If neither case 1) or 2) applies, return None.
571 When the servo_args is not None, we assume the servo is required by the
572 test. If servo failed to be verified, we will attempt to repair it. If servo
573 is not required, we will initialize ServoHost without initializing a servo
574 object.
575
576 @param dut: host name of the host that servo connects. It can be used to
577 lookup the servo in test lab using naming convention.
578 @param servo_args: A dictionary that contains args for creating
579 a ServoHost object,
580 e.g. {'servo_host': '172.11.11.111',
581 'servo_port': 9999}.
582 See comments above.
583
584 @returns: A ServoHost object or None. See comments above.
585
586 """
587 lab_servo_hostname = make_servo_hostname(dut)
588 is_in_lab = utils.host_is_in_lab_zone(lab_servo_hostname)
589
590 if is_in_lab:
591 return ServoHost(servo_host=lab_servo_hostname, is_in_lab=is_in_lab,
592 required_by_test=(servo_args is not None))
593 elif servo_args is not None:
594 return ServoHost(required_by_test=True, is_in_lab=False, **servo_args)
595 else:
596 return None