• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 xmlrpclib
16import os
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 hosts
22from autotest_lib.client.common_lib import lsbrelease_utils
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.network import ping_runner
26from autotest_lib.client.cros import constants as client_constants
27from autotest_lib.server import afe_utils
28from autotest_lib.server import site_utils as server_utils
29from autotest_lib.server.cros import autoupdater
30from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
31from autotest_lib.server.cros.servo import servo
32from autotest_lib.server.hosts import servo_repair
33from autotest_lib.server.hosts import ssh_host
34from autotest_lib.site_utils.rpm_control_system import rpm_client
35
36try:
37    from chromite.lib import metrics
38except ImportError:
39    metrics = utils.metrics_mock
40
41
42# Names of the host attributes in the database that represent the values for
43# the servo_host and servo_port for a servo connected to the DUT.
44SERVO_HOST_ATTR = 'servo_host'
45SERVO_PORT_ATTR = 'servo_port'
46SERVO_BOARD_ATTR = 'servo_board'
47# Model is inferred from host labels.
48SERVO_MODEL_ATTR = 'servo_model'
49SERVO_SERIAL_ATTR = 'servo_serial'
50SERVO_ATTR_KEYS = (
51        SERVO_BOARD_ATTR,
52        SERVO_HOST_ATTR,
53        SERVO_PORT_ATTR,
54        SERVO_SERIAL_ATTR,
55)
56
57_CONFIG = global_config.global_config
58ENABLE_SSH_TUNNEL_FOR_SERVO = _CONFIG.get_config_value(
59        'CROS', 'enable_ssh_tunnel_for_servo', type=bool, default=False)
60
61AUTOTEST_BASE = _CONFIG.get_config_value(
62        'SCHEDULER', 'drone_installation_directory',
63        default='/usr/local/autotest')
64
65
66class ServoHost(ssh_host.SSHHost):
67    """Host class for a host that controls a servo, e.g. beaglebone."""
68
69    DEFAULT_PORT = int(os.getenv('SERVOD_PORT', '9999'))
70
71    # Timeout for initializing servo signals.
72    INITIALIZE_SERVO_TIMEOUT_SECS = 60
73
74    # Ready test function
75    SERVO_READY_METHOD = 'get_version'
76
77    REBOOT_CMD = 'sleep 1; reboot & sleep 10; reboot -f'
78
79
80    def _initialize(self, servo_host='localhost',
81                    servo_port=DEFAULT_PORT, servo_board=None,
82                    servo_model=None, servo_serial=None, is_in_lab=None,
83                    *args, **dargs):
84        """Initialize a ServoHost instance.
85
86        A ServoHost instance represents a host that controls a servo.
87
88        @param servo_host: Name of the host where the servod process
89                           is running.
90        @param servo_port: Port the servod process is listening on. Defaults
91                           to the SERVOD_PORT environment variable if set,
92                           otherwise 9999.
93        @param servo_board: Board that the servo is connected to.
94        @param servo_model: Model that the servo is connected to.
95        @param is_in_lab: True if the servo host is in Cros Lab. Default is set
96                          to None, for which utils.host_is_in_lab_zone will be
97                          called to check if the servo host is in Cros lab.
98
99        """
100        super(ServoHost, self)._initialize(hostname=servo_host,
101                                           *args, **dargs)
102        self.servo_port = int(servo_port)
103        self.servo_board = servo_board
104        self.servo_model = servo_model
105        self.servo_serial = servo_serial
106        self._servo = None
107        self._repair_strategy = (
108                servo_repair.create_servo_repair_strategy())
109        self._is_localhost = (self.hostname == 'localhost')
110        if self._is_localhost:
111            self._is_in_lab = False
112        elif is_in_lab is None:
113            self._is_in_lab = utils.host_is_in_lab_zone(self.hostname)
114        else:
115            self._is_in_lab = is_in_lab
116
117        # Commands on the servo host must be run by the superuser.
118        # Our account on a remote host is root, but if our target is
119        # localhost then we might be running unprivileged.  If so,
120        # `sudo` will have to be added to the commands.
121        if self._is_localhost:
122            self._sudo_required = utils.system_output('id -u') != '0'
123        else:
124            self._sudo_required = False
125
126
127    def connect_servo(self):
128        """Establish a connection to the servod server on this host.
129
130        Initializes `self._servo` and then verifies that all network
131        connections are working.  This will create an ssh tunnel if
132        it's required.
133
134        As a side effect of testing the connection, all signals on the
135        target servo are reset to default values, and the USB stick is
136        set to the neutral (off) position.
137        """
138        servo_obj = servo.Servo(servo_host=self, servo_serial=self.servo_serial)
139        timeout, _ = retry.timeout(
140                servo_obj.initialize_dut,
141                timeout_sec=self.INITIALIZE_SERVO_TIMEOUT_SECS)
142        if timeout:
143            raise hosts.AutoservVerifyError(
144                    'Servo initialize timed out.')
145        self._servo = servo_obj
146
147
148    def disconnect_servo(self):
149        """Disconnect our servo if it exists.
150
151        If we've previously successfully connected to our servo,
152        disconnect any established ssh tunnel, and set `self._servo`
153        back to `None`.
154        """
155        if self._servo:
156            # N.B. This call is safe even without a tunnel:
157            # rpc_server_tracker.disconnect() silently ignores
158            # unknown ports.
159            self.rpc_server_tracker.disconnect(self.servo_port)
160            self._servo = None
161
162
163    def is_in_lab(self):
164        """Check whether the servo host is a lab device.
165
166        @returns: True if the servo host is in Cros Lab, otherwise False.
167
168        """
169        return self._is_in_lab
170
171
172    def is_localhost(self):
173        """Checks whether the servo host points to localhost.
174
175        @returns: True if it points to localhost, otherwise False.
176
177        """
178        return self._is_localhost
179
180
181    def get_servod_server_proxy(self):
182        """Return a proxy that can be used to communicate with servod server.
183
184        @returns: An xmlrpclib.ServerProxy that is connected to the servod
185                  server on the host.
186        """
187        if ENABLE_SSH_TUNNEL_FOR_SERVO and not self.is_localhost():
188            return self.rpc_server_tracker.xmlrpc_connect(
189                    None, self.servo_port,
190                    ready_test_name=self.SERVO_READY_METHOD,
191                    timeout_seconds=60,
192                    request_timeout_seconds=3600)
193        else:
194            remote = 'http://%s:%s' % (self.hostname, self.servo_port)
195            return xmlrpclib.ServerProxy(remote)
196
197
198    def is_cros_host(self):
199        """Check if a servo host is running chromeos.
200
201        @return: True if the servo host is running chromeos.
202            False if it isn't, or we don't have enough information.
203        """
204        try:
205            result = self.run('grep -q CHROMEOS /etc/lsb-release',
206                              ignore_status=True, timeout=10)
207        except (error.AutoservRunError, error.AutoservSSHTimeout):
208            return False
209        return result.exit_status == 0
210
211
212    def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
213                         connect_timeout=None, alive_interval=None,
214                         alive_count_max=None, connection_attempts=None):
215        """Override default make_ssh_command to use tuned options.
216
217        Tuning changes:
218          - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
219          connection failure. Consistency with remote_access.py.
220
221          - ServerAliveInterval=180; which causes SSH to ping connection every
222          180 seconds. In conjunction with ServerAliveCountMax ensures
223          that if the connection dies, Autotest will bail out quickly.
224
225          - ServerAliveCountMax=3; consistency with remote_access.py.
226
227          - ConnectAttempts=4; reduce flakiness in connection errors;
228          consistency with remote_access.py.
229
230          - UserKnownHostsFile=/dev/null; we don't care about the keys.
231
232          - SSH protocol forced to 2; needed for ServerAliveInterval.
233
234        @param user User name to use for the ssh connection.
235        @param port Port on the target host to use for ssh connection.
236        @param opts Additional options to the ssh command.
237        @param hosts_file Ignored.
238        @param connect_timeout Ignored.
239        @param alive_interval Ignored.
240        @param alive_count_max Ignored.
241        @param connection_attempts Ignored.
242
243        @returns: An ssh command with the requested settings.
244
245        """
246        options = ' '.join([opts, '-o Protocol=2'])
247        return super(ServoHost, self).make_ssh_command(
248            user=user, port=port, opts=options, hosts_file='/dev/null',
249            connect_timeout=30, alive_interval=180, alive_count_max=3,
250            connection_attempts=4)
251
252
253    def _make_scp_cmd(self, sources, dest):
254        """Format scp command.
255
256        Given a list of source paths and a destination path, produces the
257        appropriate scp command for encoding it. Remote paths must be
258        pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost
259        to allow additional ssh options.
260
261        @param sources: A list of source paths to copy from.
262        @param dest: Destination path to copy to.
263
264        @returns: An scp command that copies |sources| on local machine to
265                  |dest| on the remote servo host.
266
267        """
268        command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no '
269                   '-o UserKnownHostsFile=/dev/null -P %d %s "%s"')
270        return command % (self.master_ssh_option,
271                          self.port, ' '.join(sources), dest)
272
273
274    def run(self, command, timeout=3600, ignore_status=False,
275            stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS,
276            connect_timeout=30, ssh_failure_retry_ok=False,
277            options='', stdin=None, verbose=True, args=()):
278        """Run a command on the servo host.
279
280        Extends method `run` in SSHHost. If the servo host is a remote device,
281        it will call `run` in SSHost without changing anything.
282        If the servo host is 'localhost', it will call utils.system_output.
283
284        @param command: The command line string.
285        @param timeout: Time limit in seconds before attempting to
286                        kill the running process. The run() function
287                        will take a few seconds longer than 'timeout'
288                        to complete if it has to kill the process.
289        @param ignore_status: Do not raise an exception, no matter
290                              what the exit code of the command is.
291        @param stdout_tee/stderr_tee: Where to tee the stdout/stderr.
292        @param connect_timeout: SSH connection timeout (in seconds)
293                                Ignored if host is 'localhost'.
294        @param options: String with additional ssh command options
295                        Ignored if host is 'localhost'.
296        @param ssh_failure_retry_ok: when True and ssh connection failure is
297                                     suspected, OK to retry command (but not
298                                     compulsory, and likely not needed here)
299        @param stdin: Stdin to pass (a string) to the executed command.
300        @param verbose: Log the commands.
301        @param args: Sequence of strings to pass as arguments to command by
302                     quoting them in " and escaping their contents if necessary.
303
304        @returns: A utils.CmdResult object.
305
306        @raises AutoservRunError if the command failed.
307        @raises AutoservSSHTimeout SSH connection has timed out. Only applies
308                when servo host is not 'localhost'.
309
310        """
311        run_args = {'command': command, 'timeout': timeout,
312                    'ignore_status': ignore_status, 'stdout_tee': stdout_tee,
313                    'stderr_tee': stderr_tee, 'stdin': stdin,
314                    'verbose': verbose, 'args': args}
315        if self.is_localhost():
316            if self._sudo_required:
317                run_args['command'] = 'sudo -n sh -c "%s"' % utils.sh_escape(
318                        command)
319            try:
320                return utils.run(**run_args)
321            except error.CmdError as e:
322                logging.error(e)
323                raise error.AutoservRunError('command execution error',
324                                             e.result_obj)
325        else:
326            run_args['connect_timeout'] = connect_timeout
327            run_args['options'] = options
328            return super(ServoHost, self).run(**run_args)
329
330
331    def _get_release_version(self):
332        """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
333
334        @returns The version string in lsb-release, under attribute
335                 CHROMEOS_RELEASE_VERSION.
336        """
337        lsb_release_content = self.run(
338                    'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
339        return lsbrelease_utils.get_chromeos_release_version(
340                    lsb_release_content=lsb_release_content)
341
342
343    def get_attached_duts(self, afe):
344        """Gather a list of duts that use this servo host.
345
346        @param afe: afe instance.
347
348        @returns list of duts.
349        """
350        return afe.get_hosts_by_attribute(
351                attribute=SERVO_HOST_ATTR, value=self.hostname)
352
353
354    def get_board(self):
355        """Determine the board for this servo host.
356
357        @returns a string representing this servo host's board.
358        """
359        return lsbrelease_utils.get_current_board(
360                lsb_release_content=self.run('cat /etc/lsb-release').stdout)
361
362
363    def reboot(self, *args, **dargs):
364        """Reboot using special servo host reboot command."""
365        super(ServoHost, self).reboot(reboot_cmd=self.REBOOT_CMD,
366                                      *args, **dargs)
367
368
369    def _maybe_reboot_post_upgrade(self, updater):
370        """Reboot this servo host if an upgrade is waiting.
371
372        If the host has successfully downloaded and finalized a new
373        build, reboot.
374
375        @param updater: a ChromiumOSUpdater instance for checking
376            whether reboot is needed.
377        """
378        if updater.check_update_status() != autoupdater.UPDATER_NEED_REBOOT:
379            return
380
381        if self._needs_synchronized_reboot():
382            logging.info('Servohost requies synchronized reboot, which is no'
383                         ' longer supported. Manually reboot servohost instead.'
384                         ' See crbug/848528')
385            return
386
387        self._reboot_post_upgrade()
388
389
390    def _needs_synchronized_reboot(self):
391        """Does this servohost need synchronized reboot across multiple DUTs"""
392        # TODO(pprabhu) Use HostInfo in this check instead of hitting AFE.
393        afe = frontend_wrappers.RetryingAFE(
394                timeout_min=5, delay_sec=10,
395                server=server_utils.get_global_afe_hostname())
396        dut_list = self.get_attached_duts(afe)
397        return len(dut_list) > 1
398
399
400    def _reboot_post_upgrade(self):
401        """Reboot this servo host because an upgrade is waiting."""
402        logging.info('Rebooting servo host %s from build %s', self.hostname,
403                     self._get_release_version())
404        # Tell the reboot() call not to wait for completion.
405        # Otherwise, the call will log reboot failure if servo does
406        # not come back.  The logged reboot failure will lead to
407        # test job failure.  If the test does not require servo, we
408        # don't want servo failure to fail the test with error:
409        # `Host did not return from reboot` in status.log.
410        self.reboot(fastsync=True, wait=False)
411
412        # We told the reboot() call not to wait, but we need to wait
413        # for the reboot before we continue.  Alas.  The code from
414        # here below is basically a copy of Host.wait_for_restart(),
415        # with the logging bits ripped out, so that they can't cause
416        # the failure logging problem described above.
417        #
418        # The black stain that this has left on my soul can never be
419        # erased.
420        old_boot_id = self.get_boot_id()
421        if not self.wait_down(timeout=self.WAIT_DOWN_REBOOT_TIMEOUT,
422                                warning_timer=self.WAIT_DOWN_REBOOT_WARNING,
423                                old_boot_id=old_boot_id):
424            raise error.AutoservHostError(
425                    'servo host %s failed to shut down.' %
426                    self.hostname)
427        if self.wait_up(timeout=120):
428            logging.info('servo host %s back from reboot, with build %s',
429                            self.hostname, self._get_release_version())
430        else:
431            raise error.AutoservHostError(
432                    'servo host %s failed to come back from reboot.' %
433                    self.hostname)
434
435
436    def update_image(self, wait_for_update=False):
437        """Update the image on the servo host, if needed.
438
439        This method recognizes the following cases:
440          * If the Host is not running Chrome OS, do nothing.
441          * If a previously triggered update is now complete, reboot
442            to the new version.
443          * If the host is processing a previously triggered update,
444            do nothing.
445          * If the host is running a version of Chrome OS different
446            from the default for servo Hosts, trigger an update, but
447            don't wait for it to complete.
448
449        @param wait_for_update If an update needs to be applied and
450            this is true, then don't return until the update is
451            downloaded and finalized, and the host rebooted.
452        @raises dev_server.DevServerException: If all the devservers are down.
453        @raises site_utils.ParseBuildNameException: If the devserver returns
454            an invalid build name.
455        @raises AutoservRunError: If the update_engine_client isn't present on
456            the host, and the host is a cros_host.
457
458        """
459        # servod could be running in a Ubuntu workstation.
460        if not self.is_cros_host():
461            logging.info('Not attempting an update, either %s is not running '
462                         'chromeos or we cannot find enough information about '
463                         'the host.', self.hostname)
464            return
465
466        if lsbrelease_utils.is_moblab():
467            logging.info('Not attempting an update, %s is running moblab.',
468                         self.hostname)
469            return
470
471        target_build = afe_utils.get_stable_cros_image_name(self.get_board())
472        target_build_number = server_utils.ParseBuildName(
473                target_build)[3]
474        # For servo image staging, we want it as more widely distributed as
475        # possible, so that devservers' load can be evenly distributed. So use
476        # hostname instead of target_build as hash.
477        ds = dev_server.ImageServer.resolve(self.hostname,
478                                            hostname=self.hostname)
479        url = ds.get_update_url(target_build)
480
481        updater = autoupdater.ChromiumOSUpdater(update_url=url, host=self)
482        self._maybe_reboot_post_upgrade(updater)
483        current_build_number = self._get_release_version()
484        status = updater.check_update_status()
485        update_pending = True
486        if status in autoupdater.UPDATER_PROCESSING_UPDATE:
487            logging.info('servo host %s already processing an update, update '
488                         'engine client status=%s', self.hostname, status)
489        elif status == autoupdater.UPDATER_NEED_REBOOT:
490            return
491        elif current_build_number != target_build_number:
492            logging.info('Using devserver url: %s to trigger update on '
493                         'servo host %s, from %s to %s', url, self.hostname,
494                         current_build_number, target_build_number)
495            try:
496                ds.stage_artifacts(target_build,
497                                   artifacts=['full_payload'])
498            except Exception as e:
499                logging.error('Staging artifacts failed: %s', str(e))
500                logging.error('Abandoning update for this cycle.')
501            else:
502                try:
503                    updater.trigger_update()
504                except autoupdater.RootFSUpdateError as e:
505                    trigger_download_status = 'failed with %s' % str(e)
506                    metrics.Counter('chromeos/autotest/servo/'
507                                    'rootfs_update_failed').increment()
508                else:
509                    trigger_download_status = 'passed'
510                logging.info('Triggered download and update %s for %s, '
511                             'update engine currently in status %s',
512                             trigger_download_status, self.hostname,
513                             updater.check_update_status())
514        else:
515            logging.info('servo host %s does not require an update.',
516                         self.hostname)
517            update_pending = False
518
519        if update_pending and wait_for_update:
520            logging.info('Waiting for servo update to complete.')
521            self.run('update_engine_client --follow', ignore_status=True)
522
523
524    def verify(self, silent=False):
525        """Update the servo host and verify it's in a good state.
526
527        @param silent   If true, suppress logging in `status.log`.
528        """
529        message = 'Beginning verify for servo host %s port %s serial %s'
530        message %= (self.hostname, self.servo_port, self.servo_serial)
531        self.record('INFO', None, None, message)
532        try:
533            self._repair_strategy.verify(self, silent)
534        except:
535            self.disconnect_servo()
536            raise
537
538
539    def repair(self, silent=False):
540        """Attempt to repair servo host.
541
542        @param silent   If true, suppress logging in `status.log`.
543        """
544        message = 'Beginning repair for servo host %s port %s serial %s'
545        message %= (self.hostname, self.servo_port, self.servo_serial)
546        self.record('INFO', None, None, message)
547        try:
548            self._repair_strategy.repair(self, silent)
549        except:
550            self.disconnect_servo()
551            raise
552
553
554    def has_power(self):
555        """Return whether or not the servo host is powered by PoE."""
556        # TODO(fdeng): See crbug.com/302791
557        # For now, assume all servo hosts in the lab have power.
558        return self.is_in_lab()
559
560
561    def power_cycle(self):
562        """Cycle power to this host via PoE if it is a lab device.
563
564        @raises AutoservRepairError if it fails to power cycle the
565                servo host.
566
567        """
568        if self.has_power():
569            try:
570                rpm_client.set_power(self, 'CYCLE')
571            except (socket.error, xmlrpclib.Error,
572                    httplib.BadStatusLine,
573                    rpm_client.RemotePowerException) as e:
574                raise hosts.AutoservRepairError(
575                        'Power cycling %s failed: %s' % (self.hostname, e),
576                        'power_cycle_via_rpm_failed'
577                )
578        else:
579            logging.info('Skipping power cycling, not a lab device.')
580
581
582    def get_servo(self):
583        """Get the cached servo.Servo object.
584
585        @return: a servo.Servo object.
586        """
587        return self._servo
588
589
590    def close(self):
591        """Close the associated servo and the host object."""
592        if self._servo:
593            # In some cases when we run as lab-tools, the job object is None.
594            if self.job and not self._servo.uart_logs_dir:
595                self._servo.uart_logs_dir = self.job.resultdir
596            self._servo.close()
597
598        super(ServoHost, self).close()
599
600
601def make_servo_hostname(dut_hostname):
602    """Given a DUT's hostname, return the hostname of its servo.
603
604    @param dut_hostname: hostname of a DUT.
605
606    @return hostname of the DUT's servo.
607
608    """
609    host_parts = dut_hostname.split('.')
610    host_parts[0] = host_parts[0] + '-servo'
611    return '.'.join(host_parts)
612
613
614def servo_host_is_up(servo_hostname):
615    """Given a servo host name, return if it's up or not.
616
617    @param servo_hostname: hostname of the servo host.
618
619    @return True if it's up, False otherwise
620    """
621    # Technically, this duplicates the SSH ping done early in the servo
622    # proxy initialization code.  However, this ping ends in a couple
623    # seconds when if fails, rather than the 60 seconds it takes to decide
624    # that an SSH ping has timed out.  Specifically, that timeout happens
625    # when our servo DNS name resolves, but there is no host at that IP.
626    logging.info('Pinging servo host at %s', servo_hostname)
627    ping_config = ping_runner.PingConfig(
628            servo_hostname, count=3,
629            ignore_result=True, ignore_status=True)
630    return ping_runner.PingRunner().ping(ping_config).received > 0
631
632
633def _map_afe_board_to_servo_board(afe_board):
634    """Map a board we get from the AFE to a servo appropriate value.
635
636    Many boards are identical to other boards for servo's purposes.
637    This function makes that mapping.
638
639    @param afe_board string board name received from AFE.
640    @return board we expect servo to have.
641
642    """
643    KNOWN_SUFFIXES = ['-freon', '_freon', '_moblab', '-cheets']
644    BOARD_MAP = {'gizmo': 'panther'}
645    mapped_board = afe_board
646    if afe_board in BOARD_MAP:
647        mapped_board = BOARD_MAP[afe_board]
648    else:
649        for suffix in KNOWN_SUFFIXES:
650            if afe_board.endswith(suffix):
651                mapped_board = afe_board[0:-len(suffix)]
652                break
653    if mapped_board != afe_board:
654        logging.info('Mapping AFE board=%s to %s', afe_board, mapped_board)
655    return mapped_board
656
657
658def get_servo_args_for_host(dut_host):
659    """Return servo data associated with a given DUT.
660
661    @param dut_host   Instance of `Host` on which to find the servo
662                      attributes.
663    @return `servo_args` dict with host and an optional port.
664    """
665    info = dut_host.host_info_store.get()
666    servo_args = {k: v for k, v in info.attributes.iteritems()
667                  if k in SERVO_ATTR_KEYS}
668
669    if SERVO_PORT_ATTR in servo_args:
670        try:
671            servo_args[SERVO_PORT_ATTR] = int(servo_args[SERVO_PORT_ATTR])
672        except ValueError:
673            logging.error('servo port is not an int: %s',
674                          servo_args[SERVO_PORT_ATTR])
675            # Reset servo_args because we don't want to use an invalid port.
676            servo_args.pop(SERVO_HOST_ATTR, None)
677
678    if info.board:
679        servo_args[SERVO_BOARD_ATTR] = _map_afe_board_to_servo_board(info.board)
680    if info.model:
681        servo_args[SERVO_MODEL_ATTR] = info.model
682    return servo_args if SERVO_HOST_ATTR in servo_args else None
683
684
685def _tweak_args_for_ssp_moblab(servo_args):
686    if servo_args[SERVO_HOST_ATTR] in ['localhost', '127.0.0.1']:
687        servo_args[SERVO_HOST_ATTR] = _CONFIG.get_config_value(
688                'SSP', 'host_container_ip', type=str, default=None)
689
690
691def create_servo_host(dut, servo_args, try_lab_servo=False,
692                      try_servo_repair=False):
693    """Create a ServoHost object for a given DUT, if appropriate.
694
695    This function attempts to create and verify or repair a `ServoHost`
696    object for a servo connected to the given `dut`, subject to various
697    constraints imposed by the parameters:
698      * When the `servo_args` parameter is not `None`, a servo
699        host must be created, and must be checked with `repair()`.
700      * Otherwise, if a servo exists in the lab and `try_lab_servo` is
701        true:
702          * If `try_servo_repair` is true, then create a servo host and
703            check it with `repair()`.
704          * Otherwise, if the servo responds to `ping` then create a
705            servo host and check it with `verify()`.
706
707    In cases where `servo_args` was not `None`, repair failure
708    exceptions are passed back to the caller; otherwise, exceptions
709    are logged and then discarded.  Note that this only happens in cases
710    where we're called from a test (not special task) control file that
711    has an explicit dependency on servo.  In that case, we require that
712    repair not write to `status.log`, so as to avoid polluting test
713    results.
714
715    TODO(jrbarnette):  The special handling for servo in test control
716    files is a thorn in my flesh; I dearly hope to see it cut out before
717    my retirement.
718
719    Parameters for a servo host consist of a host name, port number, and
720    DUT board, and are determined from one of these sources, in order of
721    priority:
722      * Servo attributes from the `dut` parameter take precedence over
723        all other sources of information.
724      * If a DNS entry for the servo based on the DUT hostname exists in
725        the CrOS lab network, that hostname is used with the default
726        port and the DUT's board.
727      * If no other options are found, the parameters will be taken
728        from the `servo_args` dict passed in from the caller.
729
730    @param dut            An instance of `Host` from which to take
731                          servo parameters (if available).
732    @param servo_args     A dictionary with servo parameters to use if
733                          they can't be found from `dut`.  If this
734                          argument is supplied, unrepaired exceptions
735                          from `verify()` will be passed back to the
736                          caller.
737    @param try_lab_servo  If not true, servo host creation will be
738                          skipped unless otherwise required by the
739                          caller.
740    @param try_servo_repair  If true, check a servo host with
741                          `repair()` instead of `verify()`.
742
743    @returns: A ServoHost object or None. See comments above.
744
745    """
746    servo_dependency = servo_args is not None
747    if dut is not None and (try_lab_servo or servo_dependency):
748        servo_args_override = get_servo_args_for_host(dut)
749        if servo_args_override is not None:
750            if utils.in_moblab_ssp():
751                _tweak_args_for_ssp_moblab(servo_args_override)
752            logging.debug(
753                    'Overriding provided servo_args (%s) with arguments'
754                    ' determined from the host (%s)',
755                    servo_args,
756                    servo_args_override,
757            )
758            servo_args = servo_args_override
759
760    if servo_args is None:
761        logging.debug('No servo_args provided, and failed to find overrides.')
762        return None
763    if SERVO_HOST_ATTR not in servo_args:
764        logging.debug('%s attribute missing from servo_args: %s',
765                      SERVO_HOST_ATTR, servo_args)
766        return None
767    if (not servo_dependency and not try_servo_repair and
768            not servo_host_is_up(servo_args[SERVO_HOST_ATTR])):
769        logging.debug('ServoHost is not up.')
770        return None
771
772    newhost = ServoHost(
773            is_in_lab=(servo_args
774                       and server_utils.host_in_lab(
775                               servo_args[SERVO_HOST_ATTR])),
776            **servo_args
777    )
778    # Note that the logic of repair() includes everything done
779    # by verify().  It's sufficient to call one or the other;
780    # we don't need both.
781    if servo_dependency:
782        newhost.repair(silent=True)
783        return newhost
784
785    if try_servo_repair:
786        try:
787            newhost.repair()
788        except Exception:
789            logging.exception('servo repair failed for %s', newhost.hostname)
790    else:
791        try:
792            newhost.verify()
793        except Exception:
794            logging.exception('servo verify failed for %s', newhost.hostname)
795    return newhost
796