1# 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
20from autotest_lib.client.common_lib import global_config
21from autotest_lib.client.common_lib import lsbrelease_utils
22from autotest_lib.client.common_lib.cros import autoupdater
23from autotest_lib.client.common_lib.cros import dev_server
24from autotest_lib.client.common_lib.cros import retry
25from autotest_lib.client.common_lib.cros.graphite import autotest_stats
26from autotest_lib.client.common_lib.cros.network import ping_runner
27from autotest_lib.client.cros import constants as client_constants
28from autotest_lib.server import site_utils as server_site_utils
29from autotest_lib.server.cros import dnsname_mangler
30from autotest_lib.server.cros.servo import servo
31from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
32from autotest_lib.server.hosts import ssh_host
33from autotest_lib.site_utils.rpm_control_system import rpm_client
34
35
36# Names of the host attributes in the database that represent the values for
37# the servo_host and servo_port for a servo connected to the DUT.
38SERVO_HOST_ATTR = 'servo_host'
39SERVO_PORT_ATTR = 'servo_port'
40
41_CONFIG = global_config.global_config
42
43class ServoHostException(error.AutoservError):
44    """This is the base class for exceptions raised by ServoHost."""
45    pass
46
47
48class ServoHostVerifyFailure(ServoHostException):
49    """Raised when servo verification fails."""
50    pass
51
52
53class ServoHostRepairFailure(ServoHostException):
54    """Raised when a repair method fails to repair a servo host."""
55    pass
56
57
58class ServoHostRepairMethodNA(ServoHostException):
59    """Raised when a repair method is not applicable."""
60    pass
61
62
63class ServoHostRepairTotalFailure(ServoHostException):
64    """Raised if all attempts to repair a servo host fail."""
65    pass
66
67
68def make_servo_hostname(dut_hostname):
69    """Given a DUT's hostname, return the hostname of its servo.
70
71    @param dut_hostname: hostname of a DUT.
72
73    @return hostname of the DUT's servo.
74
75    """
76    host_parts = dut_hostname.split('.')
77    host_parts[0] = host_parts[0] + '-servo'
78    return '.'.join(host_parts)
79
80
81class ServoHost(ssh_host.SSHHost):
82    """Host class for a host that controls a servo, e.g. beaglebone."""
83
84    # Timeout for getting the value of 'pwr_button'.
85    PWR_BUTTON_CMD_TIMEOUT_SECS = 15
86    # Timeout for rebooting servo host.
87    REBOOT_TIMEOUT_SECS = 90
88    HOST_DOWN_TIMEOUT_SECS = 60
89    # Delay after rebooting for servod to become fully functional.
90    REBOOT_DELAY_SECS = 20
91    # Servod process name.
92    SERVOD_PROCESS = 'servod'
93    # Timeout for initializing servo signals.
94    INITIALIZE_SERVO_TIMEOUT_SECS = 30
95
96    _MAX_POWER_CYCLE_ATTEMPTS = 3
97    _timer = autotest_stats.Timer('servo_host')
98
99
100    def _initialize(self, servo_host='localhost', servo_port=9999,
101                    required_by_test=True, is_in_lab=None, *args, **dargs):
102        """Initialize a ServoHost instance.
103
104        A ServoHost instance represents a host that controls a servo.
105
106        @param servo_host: Name of the host where the servod process
107                           is running.
108        @param servo_port: Port the servod process is listening on.
109        @param required_by_test: True if servo is required by test.
110        @param is_in_lab: True if the servo host is in Cros Lab. Default is set
111                          to None, for which utils.host_is_in_lab_zone will be
112                          called to check if the servo host is in Cros lab.
113
114        """
115        super(ServoHost, self)._initialize(hostname=servo_host,
116                                           *args, **dargs)
117        if is_in_lab is None:
118            self._is_in_lab = utils.host_is_in_lab_zone(self.hostname)
119        else:
120            self._is_in_lab = is_in_lab
121        self._is_localhost = (self.hostname == 'localhost')
122        remote = 'http://%s:%s' % (self.hostname, servo_port)
123        self._servod_server = xmlrpclib.ServerProxy(remote)
124        # Commands on the servo host must be run by the superuser. Our account
125        # on Beaglebone is root, but locally we might be running as a
126        # different user. If so - `sudo ' will have to be added to the
127        # commands.
128        if self._is_localhost:
129            self._sudo_required = utils.system_output('id -u') != '0'
130        else:
131            self._sudo_required = False
132        # Create a cache of Servo object. This must be called at the end of
133        # _initialize to make sure all attributes are set.
134        self._servo = None
135        self.required_by_test = required_by_test
136        try:
137            self.verify()
138        except Exception:
139            if required_by_test:
140                if not self.is_in_lab():
141                    raise
142                else:
143                    self.repair()
144
145
146    def is_in_lab(self):
147        """Check whether the servo host is a lab device.
148
149        @returns: True if the servo host is in Cros Lab, otherwise False.
150
151        """
152        return self._is_in_lab
153
154
155    def is_localhost(self):
156        """Checks whether the servo host points to localhost.
157
158        @returns: True if it points to localhost, otherwise False.
159
160        """
161        return self._is_localhost
162
163
164    def get_servod_server_proxy(self):
165        """Return a proxy that can be used to communicate with servod server.
166
167        @returns: An xmlrpclib.ServerProxy that is connected to the servod
168                  server on the host.
169
170        """
171        return self._servod_server
172
173
174    def get_wait_up_processes(self):
175        """Get the list of local processes to wait for in wait_up.
176
177        Override get_wait_up_processes in
178        autotest_lib.client.common_lib.hosts.base_classes.Host.
179        Wait for servod process to go up. Called by base class when
180        rebooting the device.
181
182        """
183        processes = [self.SERVOD_PROCESS]
184        return processes
185
186
187    def _is_cros_host(self):
188        """Check if a servo host is running chromeos.
189
190        @return: True if the servo host is running chromeos.
191            False if it isn't, or we don't have enough information.
192        """
193        try:
194            result = self.run('grep -q CHROMEOS /etc/lsb-release',
195                              ignore_status=True, timeout=10)
196        except (error.AutoservRunError, error.AutoservSSHTimeout):
197            return False
198        return result.exit_status == 0
199
200
201    def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
202                         connect_timeout=None, alive_interval=None):
203        """Override default make_ssh_command to use tuned options.
204
205        Tuning changes:
206          - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
207          connection failure. Consistency with remote_access.py.
208
209          - ServerAliveInterval=180; which causes SSH to ping connection every
210          180 seconds. In conjunction with ServerAliveCountMax ensures
211          that if the connection dies, Autotest will bail out quickly.
212
213          - ServerAliveCountMax=3; consistency with remote_access.py.
214
215          - ConnectAttempts=4; reduce flakiness in connection errors;
216          consistency with remote_access.py.
217
218          - UserKnownHostsFile=/dev/null; we don't care about the keys.
219
220          - SSH protocol forced to 2; needed for ServerAliveInterval.
221
222        @param user User name to use for the ssh connection.
223        @param port Port on the target host to use for ssh connection.
224        @param opts Additional options to the ssh command.
225        @param hosts_file Ignored.
226        @param connect_timeout Ignored.
227        @param alive_interval Ignored.
228
229        @returns: An ssh command with the requested settings.
230
231        """
232        base_command = ('/usr/bin/ssh -a -x %s -o StrictHostKeyChecking=no'
233                        ' -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
234                        ' -o ConnectTimeout=30 -o ServerAliveInterval=180'
235                        ' -o ServerAliveCountMax=3 -o ConnectionAttempts=4'
236                        ' -o Protocol=2 -l %s -p %d')
237        return base_command % (opts, user, port)
238
239
240    def _make_scp_cmd(self, sources, dest):
241        """Format scp command.
242
243        Given a list of source paths and a destination path, produces the
244        appropriate scp command for encoding it. Remote paths must be
245        pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost
246        to allow additional ssh options.
247
248        @param sources: A list of source paths to copy from.
249        @param dest: Destination path to copy to.
250
251        @returns: An scp command that copies |sources| on local machine to
252                  |dest| on the remote servo host.
253
254        """
255        command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no '
256                   '-o UserKnownHostsFile=/dev/null -P %d %s "%s"')
257        return command % (self.master_ssh_option,
258                          self.port, ' '.join(sources), dest)
259
260
261    def run(self, command, timeout=3600, ignore_status=False,
262            stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
263            connect_timeout=30, options='', stdin=None, verbose=True, args=()):
264        """Run a command on the servo host.
265
266        Extends method `run` in SSHHost. If the servo host is a remote device,
267        it will call `run` in SSHost without changing anything.
268        If the servo host is 'localhost', it will call utils.system_output.
269
270        @param command: The command line string.
271        @param timeout: Time limit in seconds before attempting to
272                        kill the running process. The run() function
273                        will take a few seconds longer than 'timeout'
274                        to complete if it has to kill the process.
275        @param ignore_status: Do not raise an exception, no matter
276                              what the exit code of the command is.
277        @param stdout_tee/stderr_tee: Where to tee the stdout/stderr.
278        @param connect_timeout: SSH connection timeout (in seconds)
279                                Ignored if host is 'localhost'.
280        @param options: String with additional ssh command options
281                        Ignored if host is 'localhost'.
282        @param stdin: Stdin to pass (a string) to the executed command.
283        @param verbose: Log the commands.
284        @param args: Sequence of strings to pass as arguments to command by
285                     quoting them in " and escaping their contents if necessary.
286
287        @returns: A utils.CmdResult object.
288
289        @raises AutoservRunError if the command failed.
290        @raises AutoservSSHTimeout SSH connection has timed out. Only applies
291                when servo host is not 'localhost'.
292
293        """
294        run_args = {'command': command, 'timeout': timeout,
295                    'ignore_status': ignore_status, 'stdout_tee': stdout_tee,
296                    'stderr_tee': stderr_tee, 'stdin': stdin,
297                    'verbose': verbose, 'args': args}
298        if self.is_localhost():
299            if self._sudo_required:
300                run_args['command'] = 'sudo -n %s' % command
301            try:
302                return utils.run(**run_args)
303            except error.CmdError as e:
304                logging.error(e)
305                raise error.AutoservRunError('command execution error',
306                                             e.result_obj)
307        else:
308            run_args['connect_timeout'] = connect_timeout
309            run_args['options'] = options
310            return super(ServoHost, self).run(**run_args)
311
312
313    @_timer.decorate
314    def _check_servod(self):
315        """A sanity check of the servod state."""
316        msg_prefix = 'Servod error: %s'
317        error_msg = None
318        try:
319            timeout, _ = retry.timeout(
320                    self._servod_server.get, args=('pwr_button', ),
321                    timeout_sec=self.PWR_BUTTON_CMD_TIMEOUT_SECS)
322            if timeout:
323                error_msg = msg_prefix % 'Request timed out.'
324        except (socket.error, xmlrpclib.Error, httplib.BadStatusLine) as e:
325            error_msg = msg_prefix % e
326        if error_msg:
327            raise ServoHostVerifyFailure(error_msg)
328
329
330    def _check_servo_config(self):
331        """Check if config file exists for servod.
332
333        If servod config file does not exist, there is no need to verify if
334        servo is working. The servo could be attached to a board not supported
335        yet.
336
337        @raises ServoHostVerifyFailure if /var/lib/servod/config does not exist.
338
339        """
340        if self._is_localhost:
341            return
342        try:
343            self.run('test -f /var/lib/servod/config')
344        except (error.AutoservRunError, error.AutoservSSHTimeout) as e:
345            if not self._is_cros_host():
346                logging.info('Ignoring servo config check failure, either %s '
347                             'is not running chromeos or we cannot find enough '
348                             'information about the host.', self.hostname)
349                return
350            raise ServoHostVerifyFailure(
351                    'Servo config file check failed for %s: %s' %
352                    (self.hostname, e))
353
354
355    def _check_servod_status(self):
356        """Check if servod process is running.
357
358        If servod is not running, there is no need to verify if servo is
359        working. Check the process before making any servod call can avoid
360        long timeout that eventually fail any servod call.
361        If the servo host is set to localhost, failure of servod status check
362        will be ignored, as servo call may use ssh tunnel.
363
364        @raises ServoHostVerifyFailure if servod process does not exist.
365
366        """
367        try:
368            pids = [str(int(s)) for s in
369                    self.run('pgrep servod').stdout.strip().split('\n')]
370            logging.info('servod is running, PID=%s', ','.join(pids))
371        except (error.AutoservRunError, error.AutoservSSHTimeout) as e:
372            if self._is_localhost:
373                logging.info('Ignoring servod status check failure. servo host '
374                             'is set to localhost, servo call may use ssh '
375                             'tunnel to go through.')
376            else:
377                raise ServoHostVerifyFailure(
378                        'Servod status check failed for %s: %s' %
379                        (self.hostname, e))
380
381
382    def get_release_version(self):
383        """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
384
385        @returns The version string in lsb-release, under attribute
386                 CHROMEOS_RELEASE_VERSION.
387        """
388        lsb_release_content = self.run(
389                    'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
390        return lsbrelease_utils.get_chromeos_release_version(
391                    lsb_release_content=lsb_release_content)
392
393
394    @_timer.decorate
395    def _update_image(self):
396        """Update the image on the servo host, if needed.
397
398        This method recognizes the following cases:
399          * If the Host is not running Chrome OS, do nothing.
400          * If a previously triggered update is now complete, reboot
401            to the new version.
402          * If the host is processing a previously triggered update,
403            do nothing.
404          * If the host is running a version of Chrome OS different
405            from the default for servo Hosts, trigger an update, but
406            don't wait for it to complete.
407
408        @raises dev_server.DevServerException: If all the devservers are down.
409        @raises site_utils.ParseBuildNameException: If the devserver returns
410            an invalid build name.
411        @raises autoupdater.ChromiumOSError: If something goes wrong in the
412            checking update engine client status or applying an update.
413        @raises AutoservRunError: If the update_engine_client isn't present on
414            the host, and the host is a cros_host.
415
416        """
417        # servod could be running in a Ubuntu workstation.
418        if not self._is_cros_host():
419            logging.info('Not attempting an update, either %s is not running '
420                         'chromeos or we cannot find enough information about '
421                         'the host.', self.hostname)
422            return
423
424        if lsbrelease_utils.is_moblab():
425            logging.info('Not attempting an update, %s is running moblab.',
426                         self.hostname)
427            return
428
429        board = _CONFIG.get_config_value(
430                'CROS', 'servo_board')
431        afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
432        target_version = afe.run('get_stable_version', board=board)
433        build_pattern = _CONFIG.get_config_value(
434                'CROS', 'stable_build_pattern')
435        target_build = build_pattern % (board, target_version)
436        target_build_number = server_site_utils.ParseBuildName(
437                target_build)[3]
438        ds = dev_server.ImageServer.resolve(self.hostname)
439        url = ds.get_update_url(target_build)
440
441        updater = autoupdater.ChromiumOSUpdater(update_url=url, host=self)
442        current_build_number = self.get_release_version()
443        status = updater.check_update_status()
444
445        if status == autoupdater.UPDATER_NEED_REBOOT:
446            logging.info('Rebooting beaglebone host %s with build %s',
447                         self.hostname, current_build_number)
448            kwargs = {
449                'reboot_cmd': 'sleep 1 ; reboot & sleep 10; reboot -f',
450                'fastsync': True,
451                'label': None,
452                'wait': False,
453            }
454            # Do not wait for reboot to complete. Otherwise, self.reboot call
455            # will log reboot failure if servo does not come back. The logged
456            # reboot failure will lead to test job failure. If the test does not
457            # require servo, we don't want servo failure to fail the test with
458            # error: `Host did not return from reboot` in status.log
459            # If servo does not come back after reboot, exception needs to be
460            # raised, so test requires servo should fail.
461            self.reboot(**kwargs)
462            if self.wait_up(timeout=120):
463                current_build_number = self.get_release_version()
464                logging.info('servo host %s back from reboot, with build %s',
465                             self.hostname, current_build_number)
466            else:
467                raise error.AutoservHostError(
468                            'servo host %s failed to come back from reboot.' %
469                             self.hostname)
470
471        if status in autoupdater.UPDATER_PROCESSING_UPDATE:
472            logging.info('servo host %s already processing an update, update '
473                         'engine client status=%s', self.hostname, status)
474        elif current_build_number != target_build_number:
475            logging.info('Using devserver url: %s to trigger update on '
476                         'servo host %s, from %s to %s', url, self.hostname,
477                         current_build_number, target_build_number)
478            try:
479                ds.stage_artifacts(target_build,
480                                   artifacts=['full_payload'])
481            except Exception as e:
482                logging.error('Staging artifacts failed: %s', str(e))
483                logging.error('Abandoning update for this cycle.')
484            else:
485                try:
486                    updater.trigger_update()
487                except autoupdater.RootFSUpdateError as e:
488                    trigger_download_status = 'failed with %s' % str(e)
489                    autotest_stats.Counter(
490                            'servo_host.RootFSUpdateError').increment()
491                else:
492                    trigger_download_status = 'passed'
493                logging.info('Triggered download and update %s for %s, '
494                             'update engine currently in status %s',
495                             trigger_download_status, self.hostname,
496                             updater.check_update_status())
497        else:
498            logging.info('servo host %s does not require an update.',
499                         self.hostname)
500
501
502    def verify_software(self):
503        """Update the servo host and verify it's in a good state.
504
505        It overrides the base class function for verify_software.
506        If an update is available, downloads and applies it. Then verifies:
507            1) Whether basic servo command can run successfully.
508            2) Whether USB is in a good state. crbug.com/225932
509
510        @raises ServoHostVerifyFailure if servo host does not pass the checks.
511
512        """
513        logging.info('Applying an update to the servo host, if necessary.')
514        self._update_image()
515        self._check_servo_config()
516        self._check_servod_status()
517
518        # If servo is already initialized, we don't need to do it again, call
519        # _check_servod should be enough.
520        if self._servo:
521            self._check_servod()
522        else:
523            self._servo = servo.Servo(servo_host=self)
524            timeout, _ = retry.timeout(
525                    self._servo.initialize_dut,
526                    timeout_sec=self.INITIALIZE_SERVO_TIMEOUT_SECS)
527            if timeout:
528                raise ServoHostVerifyFailure('Servo initialize timed out.')
529        logging.info('Sanity checks pass on servo host %s', self.hostname)
530
531
532    def _repair_with_sysrq_reboot(self):
533        """Reboot with magic SysRq key."""
534        self.reboot(timeout=self.REBOOT_TIMEOUT_SECS,
535                    label=None,
536                    down_timeout=self.HOST_DOWN_TIMEOUT_SECS,
537                    reboot_cmd='echo "b" > /proc/sysrq-trigger &',
538                    fastsync=True)
539        time.sleep(self.REBOOT_DELAY_SECS)
540
541
542    def has_power(self):
543        """Return whether or not the servo host is powered by PoE."""
544        # TODO(fdeng): See crbug.com/302791
545        # For now, assume all servo hosts in the lab have power.
546        return self.is_in_lab()
547
548
549    def power_cycle(self):
550        """Cycle power to this host via PoE if it is a lab device.
551
552        @raises ServoHostRepairFailure if it fails to power cycle the
553                servo host.
554
555        """
556        if self.has_power():
557            try:
558                rpm_client.set_power(self.hostname, 'CYCLE')
559            except (socket.error, xmlrpclib.Error,
560                    httplib.BadStatusLine,
561                    rpm_client.RemotePowerException) as e:
562                raise ServoHostRepairFailure(
563                        'Power cycling %s failed: %s' % (self.hostname, e))
564        else:
565            logging.info('Skipping power cycling, not a lab device.')
566
567
568    def _powercycle_to_repair(self):
569        """Power cycle the servo host using PoE.
570
571        @raises ServoHostRepairFailure if it fails to fix the servo host.
572        @raises ServoHostRepairMethodNA if it does not support power.
573
574        """
575        if not self.has_power():
576            raise ServoHostRepairMethodNA('%s does not support power.' %
577                                          self.hostname)
578        logging.info('Attempting repair via PoE powercycle.')
579        failed_cycles = 0
580        self.power_cycle()
581        while not self.wait_up(timeout=self.REBOOT_TIMEOUT_SECS):
582            failed_cycles += 1
583            if failed_cycles >= self._MAX_POWER_CYCLE_ATTEMPTS:
584                raise ServoHostRepairFailure(
585                        'Powercycled host %s %d times; device did not come back'
586                        ' online.' % (self.hostname, failed_cycles))
587            self.power_cycle()
588        logging.info('Powercycling was successful after %d failures.',
589                     failed_cycles)
590        # Allow some time for servod to get started.
591        time.sleep(self.REBOOT_DELAY_SECS)
592
593
594    def repair(self):
595        """Attempt to repair servo host.
596
597        This overrides the base class function for repair.
598        Note if the host is not in Cros Lab, the repair procedure
599        will be skipped.
600
601        @raises ServoHostRepairTotalFailure if all attempts fail.
602
603        """
604        if not self.is_in_lab():
605            logging.warning('Skip repairing servo host %s: Not a lab device.',
606                         self.hostname)
607            return
608        logging.info('Attempting to repair servo host %s.', self.hostname)
609        # Reset the cache to guarantee servo initialization being called later.
610        self._servo = None
611        # TODO(dshi): add self._powercycle_to_repair back to repair_funcs
612        # after crbug.com/336606 is fixed.
613        repair_funcs = [self._repair_with_sysrq_reboot,]
614        errors = []
615        for repair_func in repair_funcs:
616            counter_prefix = 'servo_host_repair.%s.' % repair_func.__name__
617            try:
618                repair_func()
619                self.verify()
620                autotest_stats.Counter(counter_prefix + 'SUCCEEDED').increment()
621                return
622            except ServoHostRepairMethodNA as e:
623                logging.warning('Repair method NA: %s', e)
624                autotest_stats.Counter(counter_prefix + 'RepairNA').increment()
625                errors.append(str(e))
626            except Exception as e:
627                logging.warning('Failed to repair servo: %s', e)
628                autotest_stats.Counter(counter_prefix + 'FAILED').increment()
629                errors.append(str(e))
630        autotest_stats.Counter('servo_host_repair.Full_Repair_Failed'). \
631                increment()
632        raise ServoHostRepairTotalFailure(
633                'All attempts at repairing the servo failed:\n%s' %
634                '\n'.join(errors))
635
636
637    def get_servo(self):
638        """Get the cached servo.Servo object.
639
640        @return: a servo.Servo object.
641        """
642        return self._servo
643
644
645def create_servo_host(dut, servo_args, try_lab_servo=False):
646    """Create a ServoHost object.
647
648    The `servo_args` parameter is a dictionary specifying optional
649    Servo client parameter overrides (i.e. a specific host or port).
650    When specified, the caller requires that an exception be raised
651    unless both the ServoHost and the Servo are successfully
652    created.
653
654    There are three possible cases:
655    1. If the DUT is in the Cros test lab then the ServoHost object
656       is only created for the host in the lab.  Alternate host or
657       port settings in `servo_host` will be ignored.
658    2. When not case 1., but `servo_args` is not `None`, then create
659       a ServoHost object using `servo_args`.
660    3. Otherwise, return `None`.
661
662    When the `try_lab_servo` parameter is false, it indicates that a
663    ServoHost should not be created for a device in the Cros test
664    lab.  The setting of `servo_args` takes precedence over the
665    setting of `try_lab_servo`.
666
667    @param dut: host name of the host that servo connects. It can be used to
668                lookup the servo in test lab using naming convention.
669    @param servo_args: A dictionary that contains args for creating
670                       a ServoHost object,
671                       e.g. {'servo_host': '172.11.11.111',
672                             'servo_port': 9999}.
673                       See comments above.
674    @param try_lab_servo: Boolean. Whether to create ServoHost for a device
675                          in test lab. See above.
676
677    @returns: A ServoHost object or None. See comments above.
678
679    """
680    required_by_test = servo_args is not None
681    if not utils.is_in_container():
682        is_moblab = utils.is_moblab()
683    else:
684        is_moblab = _CONFIG.get_config_value(
685                'SSP', 'is_moblab', type=bool, default=False)
686    if not is_moblab:
687        dut_is_hostname = not dnsname_mangler.is_ip_address(dut)
688        if dut_is_hostname:
689            lab_servo_hostname = make_servo_hostname(dut)
690            is_in_lab = utils.host_is_in_lab_zone(lab_servo_hostname)
691        else:
692            is_in_lab = False
693    else:
694        # Servos on Moblab are not in the actual lab.
695        is_in_lab = False
696        afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
697        hosts = afe.get_hosts(hostname=dut)
698        if hosts and SERVO_HOST_ATTR in hosts[0].attributes:
699            servo_args = {}
700            servo_args[SERVO_HOST_ATTR] = hosts[0].attributes[SERVO_HOST_ATTR]
701            servo_args[SERVO_PORT_ATTR] = hosts[0].attributes.get(
702                    SERVO_PORT_ATTR, 9999)
703            if (utils.is_in_container() and
704                servo_args[SERVO_HOST_ATTR] in ['localhost', '127.0.0.1']):
705                servo_args[SERVO_HOST_ATTR] = _CONFIG.get_config_value(
706                        'SSP', 'host_container_ip', type=str, default=None)
707
708    if not is_in_lab:
709        if not required_by_test:
710            return None
711        return ServoHost(required_by_test=True, is_in_lab=False, **servo_args)
712    elif servo_args is not None or try_lab_servo:
713        # Technically, this duplicates the SSH ping done early in the servo
714        # proxy initialization code.  However, this ping ends in a couple
715        # seconds when if fails, rather than the 60 seconds it takes to decide
716        # that an SSH ping has timed out.  Specifically, that timeout happens
717        # when our servo DNS name resolves, but there is no host at that IP.
718        # TODO(dshi): crbug.com/380773 Remove this ping check once the bug is
719        #             fixed. Autotest should not try to verify servo if servo is
720        #             not required for the test.
721        ping_config = ping_runner.PingConfig(
722                lab_servo_hostname, count=3,
723                ignore_result=True, ignore_status=True)
724        logging.info('Pinging servo at %s', lab_servo_hostname)
725        host_is_up = ping_runner.PingRunner().ping(ping_config).received > 0
726        if host_is_up:
727            return ServoHost(servo_host=lab_servo_hostname, is_in_lab=is_in_lab,
728                             required_by_test=required_by_test)
729    else:
730        return None
731