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