• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python2
2# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Install an initial test image on a set of DUTs.
7
8The methods in this module are meant for two nominally distinct use
9cases that share a great deal of code internally.  The first use
10case is for deployment of DUTs that have just been placed in the lab
11for the first time.  The second use case is for use after repairing
12a servo.
13
14Newly deployed DUTs may be in a somewhat anomalous state:
15  * The DUTs are running a production base image, not a test image.
16    By extension, the DUTs aren't reachable over SSH.
17  * The DUTs are not necessarily in the AFE database.  DUTs that
18    _are_ in the database should be locked.  Either way, the DUTs
19    cannot be scheduled to run tests.
20  * The servos for the DUTs need not be configured with the proper
21    overlay.
22
23More broadly, it's not expected that the DUT will be working at the
24start of this operation.  If the DUT isn't working at the end of the
25operation, an error will be reported.
26
27The script performs the following functions:
28  * Configure the servo for the target overlay, and test that the
29    servo is generally in good order.
30  * For the full deployment case, install dev-signed RO firmware
31    from the designated stable test image for the DUTs.
32  * For both cases, use servo to install the stable test image from
33    USB.
34  * If the DUT isn't in the AFE database, add it.
35
36The script imposes these preconditions:
37  * Every DUT has a properly connected servo.
38  * Every DUT and servo have proper DHCP and DNS configurations.
39  * Every servo host is up and running, and accessible via SSH.
40  * There is a known, working test image that can be staged and
41    installed on the target DUTs via servo.
42  * Every DUT has the same board and model.
43  * For the full deployment case, every DUT must be in dev mode,
44    and configured to allow boot from USB with ctrl+U.
45
46The implementation uses the `multiprocessing` module to run all
47installations in parallel, separate processes.
48
49"""
50
51import atexit
52from collections import namedtuple
53import functools
54import json
55import logging
56import multiprocessing
57import os
58import shutil
59import sys
60import tempfile
61import time
62import traceback
63
64import common
65from autotest_lib.client.common_lib import error
66from autotest_lib.client.common_lib import host_states
67from autotest_lib.client.common_lib import time_utils
68from autotest_lib.client.common_lib import utils
69from autotest_lib.client.common_lib.cros import retry
70from autotest_lib.client.common_lib.utils import deprecated
71from autotest_lib.server import afe_utils
72from autotest_lib.server import constants
73from autotest_lib.server import frontend
74from autotest_lib.server import hosts
75from autotest_lib.server.cros.dynamic_suite.constants import VERSION_PREFIX
76from autotest_lib.server.hosts import afe_store
77from autotest_lib.server.hosts import servo_host
78from autotest_lib.server.hosts import servo_constants
79from autotest_lib.site_utils.deployment import cmdvalidate
80from autotest_lib.site_utils.deployment.prepare import dut as preparedut
81from autotest_lib.utils import labellib
82
83
84_LOG_FORMAT = '%(asctime)s | %(levelname)-10s | %(message)s'
85
86_DEFAULT_POOL = constants.Labels.POOL_PREFIX + 'suites'
87
88_LABSTATION_DEFAULT_POOL = constants.Labels.POOL_PREFIX + 'labstation_main'
89
90_DIVIDER = '\n============\n'
91
92_LOG_BUCKET_NAME = 'chromeos-install-logs'
93
94_OMAHA_STATUS = 'gs://chromeos-build-release-console/omaha_status.json'
95
96# Lock reasons we'll pass when locking DUTs, depending on the
97# host's prior state.
98_LOCK_REASON_EXISTING = 'Repairing or deploying an existing host'
99_LOCK_REASON_NEW_HOST = 'Repairing or deploying a new host'
100
101_ReportResult = namedtuple('_ReportResult', ['hostname', 'message'])
102
103
104class InstallFailedError(Exception):
105    """Generic error raised explicitly in this module."""
106
107
108class _NoAFEServoPortError(InstallFailedError):
109    """Exception when there is no servo port stored in the AFE."""
110
111
112class _MultiFileWriter(object):
113
114    """Group file objects for writing at once."""
115
116    @deprecated
117    def __init__(self, files):
118        """Initialize _MultiFileWriter.
119
120        @param files  Iterable of file objects for writing.
121        """
122        self._files = files
123
124    @deprecated
125    def write(self, s):
126        """Write a string to the files.
127
128        @param s  Write this string.
129        """
130        for file in self._files:
131            file.write(s)
132
133
134@deprecated
135def _get_upload_log_path(arguments):
136    return 'gs://{bucket}/{name}'.format(
137        bucket=_LOG_BUCKET_NAME,
138        name=arguments.upload_basename)
139
140
141@deprecated
142def _upload_logs(dirpath, gspath):
143    """Upload report logs to Google Storage.
144
145    @param dirpath  Path to directory containing the logs.
146    @param gspath   Path to GS bucket.
147    """
148    utils.run(['gsutil', 'cp', '-r', '--', dirpath, gspath])
149
150
151@deprecated
152def _get_omaha_build(board):
153    """Get the currently preferred Beta channel build for `board`.
154
155    Open and read through the JSON file provided by GoldenEye that
156    describes what version Omaha is currently serving for all boards
157    on all channels.  Find the entry for `board` on the Beta channel,
158    and return that version string.
159
160    @param board  The board to look up from GoldenEye.
161
162    @return Returns a Chrome OS version string in standard form
163            R##-####.#.#.  Will return `None` if no Beta channel
164            entry is found.
165    """
166    ret = utils.run(['gsutil', 'cat', '--', _OMAHA_STATUS])
167    omaha_status = json.loads(ret.stdout)
168    omaha_board = board.replace('_', '-')
169    for e in omaha_status['omaha_data']:
170        if (e['channel'] == 'beta' and
171                e['board']['public_codename'] == omaha_board):
172            milestone = e['chrome_version'].split('.')[0]
173            build = e['chrome_os_version']
174            return 'R%s-%s' % (milestone, build)
175    return None
176
177
178@deprecated
179def _update_build(afe, report_log, arguments):
180    raise RuntimeError("site_utils.deployment::_update_build is intentionally deleted")
181
182
183@deprecated
184def _create_host(hostname, afe, afe_host):
185    """Create a CrosHost object for the DUT.
186
187    This host object is used to update AFE label information for the DUT, but
188    can not be used for installation image on the DUT. In particular, this host
189    object does not have the servo attribute populated.
190
191    @param hostname  Hostname of the target DUT.
192    @param afe       A frontend.AFE object.
193    @param afe_host  AFE Host object for the DUT.
194    """
195    machine_dict = {
196            'hostname': hostname,
197            'afe_host': afe_host,
198            'host_info_store': afe_store.AfeStore(hostname, afe),
199    }
200    return hosts.create_host(machine_dict)
201
202
203@deprecated
204def _try_lock_host(afe_host):
205    """Lock a host in the AFE, and report whether it succeeded.
206
207    The lock action is logged regardless of success; failures are
208    logged if they occur.
209
210    @param afe_host AFE Host instance to be locked.
211
212    @return `True` on success, or `False` on failure.
213    """
214    try:
215        logging.warning('Locking host now.')
216        afe_host.modify(locked=True,
217                        lock_reason=_LOCK_REASON_EXISTING)
218    except Exception as e:
219        logging.exception('Failed to lock: %s', e)
220        return False
221    return True
222
223
224@deprecated
225def _try_unlock_host(afe_host):
226    """Unlock a host in the AFE, and report whether it succeeded.
227
228    The unlock action is logged regardless of success; failures are
229    logged if they occur.
230
231    @param afe_host AFE Host instance to be unlocked.
232
233    @return `True` on success, or `False` on failure.
234    """
235    try:
236        logging.warning('Unlocking host.')
237        afe_host.modify(locked=False, lock_reason='')
238    except Exception as e:
239        logging.exception('Failed to unlock: %s', e)
240        return False
241    return True
242
243
244@deprecated
245def _update_host_attributes(afe, hostname, host_attrs):
246    """Update the attributes for a given host.
247
248    @param afe          AFE object for RPC calls.
249    @param hostname     Host name of the DUT.
250    @param host_attrs   Dictionary with attributes to be applied to the
251                        host.
252    """
253    s_hostname, s_port, s_serial = _extract_servo_attributes(hostname,
254                                                             host_attrs)
255    afe.set_host_attribute(servo_constants.SERVO_HOST_ATTR,
256                           s_hostname,
257                           hostname=hostname)
258    afe.set_host_attribute(servo_constants.SERVO_PORT_ATTR,
259                           s_port,
260                           hostname=hostname)
261    if s_serial:
262        afe.set_host_attribute(servo_constants.SERVO_SERIAL_ATTR,
263                               s_serial,
264                               hostname=hostname)
265
266
267@deprecated
268def _extract_servo_attributes(hostname, host_attrs):
269    """Extract servo attributes from the host attribute dict, setting defaults.
270
271    @return (servo_hostname, servo_port, servo_serial)
272    """
273    # Grab the servo hostname/port/serial from `host_attrs` if supplied.
274    # For new servo V4 deployments, we require the user to supply the
275    # attributes (because there are no appropriate defaults).  So, if
276    # none are supplied, we assume it can't be V4, and apply the
277    # defaults for servo V3.
278    s_hostname = (host_attrs.get(servo_constants.SERVO_HOST_ATTR) or
279                  servo_host.make_servo_hostname(hostname))
280    s_port = (host_attrs.get(servo_constants.SERVO_PORT_ATTR) or
281              str(servo_host.ServoHost.DEFAULT_PORT))
282    s_serial = host_attrs.get(servo_constants.SERVO_SERIAL_ATTR)
283    return s_hostname, s_port, s_serial
284
285
286@deprecated
287def _wait_for_idle(afe, host_id):
288    """Helper function for `_ensure_host_idle`.
289
290    Poll the host with the given `host_id` via `afe`, waiting for it
291    to become idle.  Run forever; the caller takes care of timing out.
292
293    @param afe        AFE object for RPC calls.
294    @param host_id    Id of the host that's expected to become idle.
295    """
296    while True:
297        afe_host = afe.get_hosts(id=host_id)[0]
298        if afe_host.status in host_states.IDLE_STATES:
299            return
300        # Let's not spam our server.
301        time.sleep(0.2)
302
303
304@deprecated
305def _ensure_host_idle(afe, afe_host):
306    """Abort any special task running on `afe_host`.
307
308    The given `afe_host` is currently locked.  If there's a special task
309    running on the given `afe_host`, abort it, then wait for the host to
310    show up as idle, return whether the operation succeeded.
311
312    @param afe        AFE object for RPC calls.
313    @param afe_host   Host to be aborted.
314
315    @return A true value if the host is idle at return, or a false value
316        if the host wasn't idle after some reasonable time.
317    """
318    # We need to talk to the shard, not the main, for at least two
319    # reasons:
320    #   * The `abort_special_tasks` RPC doesn't forward from the main
321    #     to the shard, and only the shard has access to the special
322    #     tasks.
323    #   * Host status on the main can lag actual status on the shard
324    #     by several minutes.  Only the shard can provide status
325    #     guaranteed to post-date the call to lock the DUT.
326    if afe_host.shard:
327        afe = frontend.AFE(server=afe_host.shard)
328    afe_host = afe.get_hosts(id=afe_host.id)[0]
329    if afe_host.status in host_states.IDLE_STATES:
330        return True
331    afe.run('abort_special_tasks', host_id=afe_host.id, is_active=1)
332    return not retry.timeout(_wait_for_idle, (afe, afe_host.id),
333                             timeout_sec=5.0)[0]
334
335
336@deprecated
337def _get_afe_host(afe, hostname, host_attrs, arguments):
338    """Get an AFE Host object for the given host.
339
340    If the host is found in the database, return the object
341    from the RPC call with the updated attributes in host_attr_dict.
342
343    If no host is found, create one with appropriate servo
344    attributes and the given board label.
345
346    @param afe          AFE object for RPC calls.
347    @param hostname     Host name of the DUT.
348    @param host_attrs   Dictionary with attributes to be applied to the
349                        host.
350    @param arguments    Command line arguments with options.
351
352    @return A tuple of the afe_host, plus a flag. The flag indicates
353            whether the Host should be unlocked if subsequent operations
354            fail.  (Hosts are always unlocked after success).
355    """
356    hostlist = afe.get_hosts([hostname])
357    unlock_on_failure = False
358    if hostlist:
359        afe_host = hostlist[0]
360        if not afe_host.locked:
361            if _try_lock_host(afe_host):
362                unlock_on_failure = True
363            else:
364                raise Exception('Failed to lock host')
365        if not _ensure_host_idle(afe, afe_host):
366            if unlock_on_failure and not _try_unlock_host(afe_host):
367                raise Exception('Failed to abort host, and failed to unlock it')
368            raise Exception('Failed to abort task on host')
369        # This host was pre-existing; if the user didn't supply
370        # attributes, don't update them, because the defaults may
371        # not be correct.
372        if host_attrs and not arguments.labstation:
373            _update_host_attributes(afe, hostname, host_attrs)
374    else:
375        afe_host = afe.create_host(hostname,
376                                   locked=True,
377                                   lock_reason=_LOCK_REASON_NEW_HOST)
378
379        if not arguments.labstation:
380            _update_host_attributes(afe, hostname, host_attrs)
381
382    # Correct board/model label is critical to installation. Always ensure user
383    # supplied board/model matches the AFE information.
384    _ensure_label_in_afe(afe_host, 'board', arguments.board)
385    _ensure_label_in_afe(afe_host, 'model', arguments.model)
386
387    afe_host = afe.get_hosts([hostname])[0]
388    return afe_host, unlock_on_failure
389
390
391@deprecated
392def _ensure_label_in_afe(afe_host, label_name, label_value):
393    """Add the given board label, only if one doesn't already exist.
394
395    @params label_name  name of the label, e.g. 'board', 'model', etc.
396    @params label_value value of the label.
397
398    @raises InstallFailedError if supplied board  is different from existing
399            board in AFE.
400    """
401    if not label_value:
402        return
403
404    labels = labellib.LabelsMapping(afe_host.labels)
405    if label_name not in labels:
406        afe_host.add_labels(['%s:%s' % (label_name, label_value)])
407        return
408
409    existing_value = labels[label_name]
410    if label_value != existing_value:
411        raise InstallFailedError(
412                'provided %s %s does not match the %s %s for host %s' %
413                (label_name, label_value, label_name, existing_value,
414                 afe_host.hostname))
415
416
417@deprecated
418def _create_host_for_installation(host, arguments):
419    """Creates a context manager of hosts.CrosHost object for installation.
420
421    The host object yielded by the returned context manager is agnostic of the
422    infrastructure environment. In particular, it does not have any references
423    to the AFE.
424
425    @param host: A server.hosts.CrosHost object.
426    @param arguments: Parsed commandline arguments for this script.
427
428    @return a context manager which yields hosts.CrosHost object.
429    """
430    info = host.host_info_store.get()
431    s_host, s_port, s_serial = _extract_servo_attributes(host.hostname,
432                                                         info.attributes)
433    return preparedut.create_cros_host(host.hostname, arguments.board,
434                                       arguments.model, s_host, s_port,
435                                       s_serial, arguments.logdir)
436
437
438@deprecated
439def _install_test_image(host, arguments):
440    """Install a test image to the DUT.
441
442    Install a stable test image on the DUT using the full servo
443    repair flow.
444
445    @param host       Host instance for the DUT being installed.
446    @param arguments  Command line arguments with options.
447    """
448    repair_image = _get_cros_repair_image_name(host)
449    logging.info('Using repair image %s', repair_image)
450    if arguments.dry_run:
451        return
452    if arguments.stageusb:
453        try:
454            preparedut.download_image_to_servo_usb(host, repair_image)
455        except Exception as e:
456            logging.exception('Failed to stage image on USB: %s', e)
457            raise Exception('USB staging failed')
458    if arguments.install_test_image:
459        try:
460            preparedut.install_test_image(host)
461        except error.AutoservRunError as e:
462            logging.exception('Failed to install: %s', e)
463            raise Exception('chromeos-install failed')
464    if arguments.install_firmware:
465        try:
466            if arguments.using_servo:
467                logging.debug('Install FW using servo.')
468                preparedut.flash_firmware_using_servo(host, repair_image)
469            else:
470                logging.debug('Install FW by chromeos-firmwareupdate.')
471                preparedut.install_firmware(host)
472        except error.AutoservRunError as e:
473            logging.exception('Firmware update failed: %s', e)
474            msg = '%s failed' % (
475                    'Flashing firmware using servo' if arguments.using_servo
476                    else 'chromeos-firmwareupdate')
477            raise Exception(msg)
478    if arguments.reinstall_test_image:
479        try:
480            preparedut.reinstall_test_image(host)
481        except error.AutoservRunError as e:
482            logging.exception('Failed to install: %s', e)
483            raise Exception('chromeos-install failed')
484    if arguments.install_test_image and arguments.install_firmware:
485        # we need to verify that DUT can successfully boot in to recovery mode
486        # if it's initial deploy.
487        try:
488            preparedut.verify_boot_into_rec_mode(host)
489        except error.AutoservRunError as e:
490            logging.exception('Failed to validate DUT can boot from '
491                              'recovery mode: %s', e)
492            raise Exception('recovery mode validation failed')
493
494
495@deprecated
496def _install_and_update_afe(afe, hostname, host_attrs, arguments):
497    """Perform all installation and AFE updates.
498
499    First, lock the host if it exists and is unlocked.  Then,
500    install the test image on the DUT.  At the end, unlock the
501    DUT, unless the installation failed and the DUT was locked
502    before we started.
503
504    If installation succeeds, make sure the DUT is in the AFE,
505    and make sure that it has basic labels.
506
507    @param afe          AFE object for RPC calls.
508    @param hostname     Host name of the DUT.
509    @param host_attrs   Dictionary with attributes to be applied to the
510                        host.
511    @param arguments    Command line arguments with options.
512    """
513    afe_host, unlock_on_failure = _get_afe_host(afe, hostname, host_attrs,
514                                                arguments)
515    host = None
516    try:
517        host = _create_host(hostname, afe, afe_host)
518        if arguments.labstation:
519            _setup_labstation(host)
520        else:
521            with _create_host_for_installation(host, arguments) as target_host:
522                _install_test_image(target_host, arguments)
523                _update_servo_type_attribute(target_host, host)
524
525        if ((arguments.install_test_image or arguments.reinstall_test_image)
526            and not arguments.dry_run):
527            host.labels.update_labels(host)
528            platform_labels = afe.get_labels(
529                    host__hostname=hostname, platform=True)
530            if not platform_labels:
531                platform = host.get_platform()
532                new_labels = afe.get_labels(name=platform)
533                if not new_labels:
534                    afe.create_label(platform, platform=True)
535                afe_host.add_labels([platform])
536        version = [label for label in afe_host.labels
537                       if label.startswith(VERSION_PREFIX)]
538        if version and not arguments.dry_run:
539            afe_host.remove_labels(version)
540    except Exception as e:
541        if unlock_on_failure and not _try_unlock_host(afe_host):
542            logging.error('Failed to unlock host!')
543        raise
544    finally:
545        if host is not None:
546            host.close()
547
548    if not _try_unlock_host(afe_host):
549        raise Exception('Install succeeded, but failed to unlock the DUT.')
550
551
552@deprecated
553def _install_dut(arguments, host_attr_dict, hostname):
554    """Deploy or repair a single DUT.
555
556    @param arguments       Command line arguments with options.
557    @param host_attr_dict  Dict mapping hostnames to attributes to be
558                           stored in the AFE.
559    @param hostname        Host name of the DUT to install on.
560
561    @return On success, return `None`.  On failure, return a string
562            with an error message.
563    """
564    # In some cases, autotest code that we call during install may
565    # put stuff onto stdout with 'print' statements.  Most notably,
566    # the AFE frontend may print 'FAILED RPC CALL' (boo, hiss).  We
567    # want nothing from this subprocess going to the output we
568    # inherited from our parent, so redirect stdout and stderr, before
569    # we make any AFE calls.  Note that this is reasonable because we're
570    # in a subprocess.
571
572    logpath = os.path.join(arguments.logdir, hostname + '.log')
573    logfile = open(logpath, 'w')
574    sys.stderr = sys.stdout = logfile
575    _configure_logging_to_file(logfile)
576
577    afe = frontend.AFE(server=arguments.web)
578    try:
579        _install_and_update_afe(afe, hostname,
580                                host_attr_dict.get(hostname, {}),
581                                arguments)
582    except Exception as e:
583        logging.exception('Original exception: %s', e)
584        return str(e)
585    return None
586
587
588@deprecated
589def _report_hosts(report_log, heading, host_results_list):
590    """Report results for a list of hosts.
591
592    To improve visibility, results are preceded by a header line,
593    followed by a divider line.  Then results are printed, one host
594    per line.
595
596    @param report_log         File-like object for logging report
597                              output.
598    @param heading            The header string to be printed before
599                              results.
600    @param host_results_list  A list of _ReportResult tuples
601                              to be printed one per line.
602    """
603    if not host_results_list:
604        return
605    report_log.write(heading)
606    report_log.write(_DIVIDER)
607    for result in host_results_list:
608        report_log.write('{result.hostname:30} {result.message}\n'
609                         .format(result=result))
610    report_log.write('\n')
611
612
613@deprecated
614def _report_results(afe, report_log, hostnames, results, arguments):
615    """Gather and report a summary of results from installation.
616
617    Segregate results into successes and failures, reporting
618    each separately.  At the end, report the total of successes
619    and failures.
620
621    @param afe          AFE object for RPC calls.
622    @param report_log   File-like object for logging report output.
623    @param hostnames    List of the hostnames that were tested.
624    @param results      List of error messages, in the same order
625                        as the hostnames.  `None` means the
626                        corresponding host succeeded.
627    @param arguments  Command line arguments with options.
628    """
629    successful_hosts = []
630    success_reports = []
631    failure_reports = []
632    for result, hostname in zip(results, hostnames):
633        if result is None:
634            successful_hosts.append(hostname)
635        else:
636            failure_reports.append(_ReportResult(hostname, result))
637    if successful_hosts:
638        afe.repair_hosts(hostnames=successful_hosts)
639        for h in afe.get_hosts(hostnames=successful_hosts):
640            for label in h.labels:
641                if label.startswith(constants.Labels.POOL_PREFIX):
642                    result = _ReportResult(h.hostname,
643                                           'Host already in %s' % label)
644                    success_reports.append(result)
645                    break
646            else:
647                if arguments.labstation:
648                    target_pool = _LABSTATION_DEFAULT_POOL
649                else:
650                    target_pool = _DEFAULT_POOL
651                h.add_labels([target_pool])
652                result = _ReportResult(h.hostname,
653                                       'Host added to %s' % target_pool)
654                success_reports.append(result)
655    report_log.write(_DIVIDER)
656    _report_hosts(report_log, 'Successes', success_reports)
657    _report_hosts(report_log, 'Failures', failure_reports)
658    report_log.write(
659        'Installation complete:  %d successes, %d failures.\n' %
660        (len(success_reports), len(failure_reports)))
661
662
663@deprecated
664def _clear_root_logger_handlers():
665    """Remove all handlers from root logger."""
666    root_logger = logging.getLogger()
667    for h in root_logger.handlers:
668        root_logger.removeHandler(h)
669
670
671@deprecated
672def _configure_logging_to_file(logfile):
673    """Configure the logging module for `install_duts()`.
674
675    @param log_file  Log file object.
676    """
677    _clear_root_logger_handlers()
678    handler = logging.StreamHandler(logfile)
679    formatter = logging.Formatter(_LOG_FORMAT, time_utils.TIME_FMT)
680    handler.setFormatter(formatter)
681    root_logger = logging.getLogger()
682    root_logger.addHandler(handler)
683
684
685@deprecated
686def _get_used_servo_ports(servo_hostname, afe):
687    """
688    Return a list of used servo ports for the given servo host.
689
690    @param servo_hostname:  Hostname of the servo host to check for.
691    @param afe:             AFE instance.
692
693    @returns a list of used ports for the given servo host.
694    """
695    used_ports = []
696    host_list = afe.get_hosts_by_attribute(
697            attribute=servo_constants.SERVO_HOST_ATTR, value=servo_hostname)
698    for host in host_list:
699        afe_host = afe.get_hosts(hostname=host)
700        if afe_host:
701            servo_port = afe_host[0].attributes.get(servo_constants.SERVO_PORT_ATTR)
702            if servo_port:
703                used_ports.append(int(servo_port))
704    return used_ports
705
706
707@deprecated
708def _get_free_servo_port(servo_hostname, used_servo_ports, afe):
709    """
710    Get a free servo port for the servo_host.
711
712    @param servo_hostname:    Hostname of the servo host.
713    @param used_servo_ports:  Dict of dicts that contain the list of used ports
714                              for the given servo host.
715    @param afe:               AFE instance.
716
717    @returns a free servo port if servo_hostname is non-empty, otherwise an
718        empty string.
719    """
720    used_ports = []
721    servo_port = servo_host.ServoHost.DEFAULT_PORT
722    # If no servo hostname was specified we can assume we're dealing with a
723    # servo v3 or older deployment since the servo hostname can be
724    # inferred from the dut hostname (by appending '-servo' to it).  We only
725    # need to find a free port if we're using a servo v4 since we can use the
726    # default port for v3 and older.
727    if not servo_hostname:
728        return ''
729    # If we haven't checked this servo host yet, check the AFE if other duts
730    # used this servo host and grab the ports specified for them.
731    elif servo_hostname not in used_servo_ports:
732        used_ports = _get_used_servo_ports(servo_hostname, afe)
733    else:
734        used_ports = used_servo_ports[servo_hostname]
735    used_ports.sort()
736    if used_ports:
737        # Range is taken from servod.py in hdctools.
738        start_port = servo_host.ServoHost.DEFAULT_PORT
739        end_port = start_port - 99
740        # We'll choose first port available in descending order.
741        for port in xrange(start_port, end_port - 1, -1):
742            if port not in used_ports:
743              servo_port = port
744              break
745    used_ports.append(servo_port)
746    used_servo_ports[servo_hostname] = used_ports
747    return servo_port
748
749
750@deprecated
751def _get_afe_servo_port(host_info, afe):
752    """
753    Get the servo port from the afe if it matches the same servo host hostname.
754
755    @param host_info   HostInfo tuple (hostname, host_attr_dict).
756
757    @returns Servo port (int) if servo host hostname matches the one specified
758    host_info.host_attr_dict, otherwise None.
759
760    @raises _NoAFEServoPortError: When there is no stored host info or servo
761        port host attribute in the AFE for the given host.
762    """
763    afe_hosts = afe.get_hosts(hostname=host_info.hostname)
764    if not afe_hosts:
765        raise _NoAFEServoPortError
766
767    servo_port = afe_hosts[0].attributes.get(servo_constants.SERVO_PORT_ATTR)
768    afe_servo_host = afe_hosts[0].attributes.get(servo_constants.SERVO_HOST_ATTR)
769    host_info_servo_host = host_info.host_attr_dict.get(
770        servo_constants.SERVO_HOST_ATTR)
771
772    if afe_servo_host == host_info_servo_host and servo_port:
773        return int(servo_port)
774    else:
775        raise _NoAFEServoPortError
776
777
778@deprecated
779def _get_host_attributes(host_info_list, afe):
780    """
781    Get host attributes if a hostname_file was supplied.
782
783    @param host_info_list   List of HostInfo tuples (hostname, host_attr_dict).
784
785    @returns Dict of attributes from host_info_list.
786    """
787    host_attributes = {}
788    # We need to choose servo ports for these hosts but we need to make sure
789    # we don't choose ports already used. We'll store all used ports in a
790    # dict of lists where the key is the servo_host and the val is a list of
791    # ports used.
792    used_servo_ports = {}
793    for host_info in host_info_list:
794        host_attr_dict = host_info.host_attr_dict
795        # If the host already has an entry in the AFE that matches the same
796        # servo host hostname and the servo port is set, use that port.
797        try:
798            host_attr_dict[servo_constants.SERVO_PORT_ATTR] = _get_afe_servo_port(
799                host_info, afe)
800        except _NoAFEServoPortError:
801            host_attr_dict[servo_constants.SERVO_PORT_ATTR] = _get_free_servo_port(
802                host_attr_dict[servo_constants.SERVO_HOST_ATTR], used_servo_ports,
803                afe)
804        host_attributes[host_info.hostname] = host_attr_dict
805    return host_attributes
806
807
808@deprecated
809def _get_cros_repair_image_name(host):
810    """Get the CrOS repair image name for given host.
811
812    @param host: hosts.CrosHost object. This object need not have an AFE
813                 reference.
814    """
815    info = host.host_info_store.get()
816    if not info.board:
817        raise InstallFailedError('Unknown board for given host')
818    return afe_utils.get_stable_cros_image_name_v2(info)
819
820
821@deprecated
822def install_duts(arguments):
823    """Install a test image on DUTs, and deploy them.
824
825    This handles command line parsing for both the repair and
826    deployment commands.  The two operations are largely identical;
827    the main difference is that full deployment includes flashing
828    dev-signed firmware on the DUT prior to installing the test
829    image.
830
831    @param arguments    Command line arguments with options, as
832                        returned by `argparse.Argparser`.
833    """
834    arguments = cmdvalidate.validate_arguments(arguments)
835    if arguments is None:
836        sys.exit(1)
837    sys.stderr.write('Installation output logs in %s\n' % arguments.logdir)
838
839    # Override tempfile.tempdir.  Some of the autotest code we call
840    # will create temporary files that don't get cleaned up.  So, we
841    # put the temp files in our results directory, so that we can
842    # clean up everything at one fell swoop.
843    tempfile.tempdir = tempfile.mkdtemp()
844    atexit.register(shutil.rmtree, tempfile.tempdir)
845
846    # We don't want to distract the user with logging output, so we catch
847    # logging output in a file.
848    logging_file_path = os.path.join(arguments.logdir, 'debug.log')
849    logfile = open(logging_file_path, 'w')
850    _configure_logging_to_file(logfile)
851
852    report_log_path = os.path.join(arguments.logdir, 'report.log')
853    with open(report_log_path, 'w') as report_log_file:
854        report_log = _MultiFileWriter([report_log_file, sys.stdout])
855        afe = frontend.AFE(server=arguments.web)
856        if arguments.dry_run:
857            report_log.write('Dry run - installation and most testing '
858                             'will be skipped.\n')
859        host_attr_dict = _get_host_attributes(arguments.host_info_list, afe)
860        install_pool = multiprocessing.Pool(len(arguments.hostnames))
861        install_function = functools.partial(_install_dut, arguments,
862                                             host_attr_dict)
863        results_list = install_pool.map(install_function, arguments.hostnames)
864        _report_results(afe, report_log, arguments.hostnames, results_list,
865                        arguments)
866
867    if arguments.upload:
868        try:
869            gspath = _get_upload_log_path(arguments)
870            sys.stderr.write('Logs will be uploaded to %s\n' % (gspath,))
871            _upload_logs(arguments.logdir, gspath)
872        except Exception:
873            upload_failure_log_path = os.path.join(arguments.logdir,
874                                                   'gs_upload_failure.log')
875            with open(upload_failure_log_path, 'w') as file_:
876                traceback.print_exc(limit=None, file=file_)
877            sys.stderr.write('Failed to upload logs;'
878                             ' failure details are stored in {}.\n'
879                             .format(upload_failure_log_path))
880
881
882@deprecated
883def _update_servo_type_attribute(host, host_to_update):
884    """Update servo_type attribute for the DUT.
885
886    @param host              A CrOSHost with a initialized servo property.
887    @param host_to_update    A CrOSHost with AfeStore as its host_info_store.
888
889    """
890    info = host_to_update.host_info_store.get()
891    if 'servo_type' not in info.attributes:
892        logging.info("Collecting and adding servo_type attribute.")
893        info.attributes['servo_type'] = host.servo.get_servo_version()
894        host_to_update.host_info_store.commit(info)
895
896
897@deprecated
898def _setup_labstation(host):
899    """Do initial setup for labstation host.
900
901    @param host    A LabstationHost object.
902
903    """
904    try:
905        if not host.is_labstation():
906            raise InstallFailedError('Current OS on host %s is not a labstation'
907                                     ' image.', host.hostname)
908    except AttributeError:
909        raise InstallFailedError('Unable to verify host has a labstation image,'
910                                 ' this can be caused by host is unsshable.')
911
912    try:
913        # TODO: we should setup hwid and serial number for DUT in deploy script
914        #  as well, which is currently obtained from repair job.
915        info = host.host_info_store.get()
916        hwid = host.run('crossystem hwid', ignore_status=True).stdout
917        if hwid:
918            info.attributes['HWID'] = hwid
919
920        serial_number = host.run('vpd -g serial_number',
921                                 ignore_status=True).stdout
922        if serial_number:
923            info.attributes['serial_number'] = serial_number
924        if info != host.host_info_store.get():
925            host.host_info_store.commit(info)
926    except Exception as e:
927        raise InstallFailedError('Failed to get HWID & Serial Number for host'
928                                 ' %s: %s' % (host.hostname, str(e)))
929
930    host.labels.update_labels(host)
931