Home | History | Annotate | Download | only in deployment
      1 #!/usr/bin/env python
      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 
      8 The methods in this module are meant for two nominally distinct use
      9 cases that share a great deal of code internally.  The first use
     10 case is for deployment of DUTs that have just been placed in the lab
     11 for the first time.  The second use case is for use after repairing
     12 a servo.
     13 
     14 Newly 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     board.
     22 
     23 More broadly, it's not expected that the DUT will be working at the
     24 start of this operation.  If the DUT isn't working at the end of the
     25 operation, an error will be reported.
     26 
     27 The script performs the following functions:
     28   * Configure the servo for the target board, 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 
     36 The 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.
     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 
     46 The implementation uses the `multiprocessing` module to run all
     47 installations in parallel, separate processes.
     48 
     49 """
     50 
     51 import atexit
     52 from collections import namedtuple
     53 import functools
     54 import json
     55 import logging
     56 import multiprocessing
     57 import os
     58 import shutil
     59 import sys
     60 import tempfile
     61 import time
     62 import traceback
     63 
     64 from chromite.lib import gs
     65 
     66 import common
     67 from autotest_lib.client.common_lib import error
     68 from autotest_lib.client.common_lib import host_states
     69 from autotest_lib.client.common_lib import time_utils
     70 from autotest_lib.client.common_lib import utils
     71 from autotest_lib.server import frontend
     72 from autotest_lib.server import hosts
     73 from autotest_lib.server.cros.dynamic_suite.constants import VERSION_PREFIX
     74 from autotest_lib.server.hosts import afe_store
     75 from autotest_lib.server.hosts import servo_host
     76 from autotest_lib.site_utils.deployment import commandline
     77 from autotest_lib.site_utils.stable_images import assign_stable_images
     78 from autotest_lib.site_utils.suite_scheduler.constants import Labels
     79 
     80 
     81 _LOG_FORMAT = '%(asctime)s | %(levelname)-10s | %(message)s'
     82 
     83 _DEFAULT_POOL = Labels.POOL_PREFIX + 'suites'
     84 
     85 _DIVIDER = '\n============\n'
     86 
     87 _LOG_BUCKET_NAME = 'chromeos-install-logs'
     88 
     89 _OMAHA_STATUS = 'gs://chromeos-build-release-console/omaha_status.json'
     90 
     91 # Lock reasons we'll pass when locking DUTs, depending on the
     92 # host's prior state.
     93 _LOCK_REASON_EXISTING = 'Repairing or deploying an existing host'
     94 _LOCK_REASON_NEW_HOST = 'Repairing or deploying a new host'
     95 
     96 _ReportResult = namedtuple('_ReportResult', ['hostname', 'message'])
     97 
     98 
     99 class _NoAFEServoPortError(Exception):
    100     """Exception when there is no servo port stored in the AFE."""
    101 
    102 
    103 class _MultiFileWriter(object):
    104 
    105     """Group file objects for writing at once."""
    106 
    107     def __init__(self, files):
    108         """Initialize _MultiFileWriter.
    109 
    110         @param files  Iterable of file objects for writing.
    111         """
    112         self._files = files
    113 
    114     def write(self, s):
    115         """Write a string to the files.
    116 
    117         @param s  Write this string.
    118         """
    119         for file in self._files:
    120             file.write(s)
    121 
    122 
    123 def _get_upload_log_path(arguments):
    124     return 'gs://{bucket}/{name}'.format(
    125         bucket=_LOG_BUCKET_NAME,
    126         name=commandline.get_default_logdir_name(arguments))
    127 
    128 
    129 def _upload_logs(dirpath, gspath):
    130     """Upload report logs to Google Storage.
    131 
    132     @param dirpath  Path to directory containing the logs.
    133     @param gspath   Path to GS bucket.
    134     """
    135     ctx = gs.GSContext()
    136     ctx.Copy(dirpath, gspath, recursive=True)
    137 
    138 
    139 def _get_omaha_build(board):
    140     """Get the currently preferred Beta channel build for `board`.
    141 
    142     Open and read through the JSON file provided by GoldenEye that
    143     describes what version Omaha is currently serving for all boards
    144     on all channels.  Find the entry for `board` on the Beta channel,
    145     and return that version string.
    146 
    147     @param board  The board to look up from GoldenEye.
    148 
    149     @return Returns a Chrome OS version string in standard form
    150             R##-####.#.#.  Will return `None` if no Beta channel
    151             entry is found.
    152     """
    153     ctx = gs.GSContext()
    154     omaha_status = json.loads(ctx.Cat(_OMAHA_STATUS))
    155     omaha_board = board.replace('_', '-')
    156     for e in omaha_status['omaha_data']:
    157         if (e['channel'] == 'beta' and
    158                 e['board']['public_codename'] == omaha_board):
    159             milestone = e['chrome_version'].split('.')[0]
    160             build = e['chrome_os_version']
    161             return 'R%s-%s' % (milestone, build)
    162     return None
    163 
    164 
    165 def _update_build(afe, report_log, arguments):
    166     """Update the stable_test_versions table.
    167 
    168     This calls the `set_stable_version` RPC call to set the stable
    169     repair version selected by this run of the command.  Additionally,
    170     this updates the stable firmware for the board.  The repair version
    171     is selected from three possible versions:
    172       * The stable test version currently in the AFE database.
    173       * The version Omaha is currently serving as the Beta channel
    174         build.
    175       * The version supplied by the user.
    176     The actual version selected will be whichever of these three is
    177     the most up-to-date version.
    178 
    179     The stable firmware version will be set to whatever firmware is
    180     bundled in the selected repair image.
    181 
    182     This function will log information about the available versions
    183     prior to selection.  After selection the repair and firmware
    184     versions slected will be logged.
    185 
    186     @param afe          AFE object for RPC calls.
    187     @param report_log   File-like object for logging report output.
    188     @param arguments    Command line arguments with options.
    189 
    190     @return Returns the version selected.
    191     """
    192     # Gather the current AFE and Omaha version settings, and report them
    193     # to the user.
    194     cros_version_map = afe.get_stable_version_map(afe.CROS_IMAGE_TYPE)
    195     fw_version_map = afe.get_stable_version_map(afe.FIRMWARE_IMAGE_TYPE)
    196     afe_cros = cros_version_map.get_version(arguments.board)
    197     afe_fw = fw_version_map.get_version(arguments.board)
    198     omaha_cros = _get_omaha_build(arguments.board)
    199     report_log.write('AFE    version is %s.\n' % afe_cros)
    200     report_log.write('Omaha  version is %s.\n' % omaha_cros)
    201     report_log.write('AFE   firmware is %s.\n' % afe_fw)
    202     cros_version = afe_cros
    203     fw_version = afe_fw
    204 
    205     # Check whether we should upgrade the repair build to either
    206     # the Omaha or the user's requested build.  If we do, we must
    207     # also update the firmware version.
    208     if (omaha_cros is not None and
    209              utils.compare_versions(cros_version, omaha_cros) < 0):
    210         cros_version = omaha_cros
    211         fw_version = None
    212     if arguments.build and arguments.build != cros_version:
    213         if utils.compare_versions(cros_version, arguments.build) < 0:
    214             cros_version = arguments.build
    215             fw_version = None
    216         else:
    217             report_log.write('Selected version %s is too old; '
    218                              'using version %s'
    219                              % (arguments.build, cros_version))
    220     if fw_version is None:
    221         fw_version = assign_stable_images.get_firmware_version(
    222                 cros_version_map, arguments.board, cros_version)
    223 
    224     # At this point `cros_version` is our new repair build, and
    225     # `fw_version` is our new target firmware.  Call the AFE back with
    226     # updates as necessary.
    227     if not arguments.nostable:
    228         if cros_version != afe_cros:
    229             cros_version_map.set_version(arguments.board, cros_version)
    230         if fw_version != afe_fw:
    231             if fw_version is not None:
    232                 fw_version_map.set_version(arguments.board,
    233                                            fw_version)
    234             else:
    235                 fw_version_map.delete_version(arguments.board)
    236 
    237     # Report the new state of the world.
    238     report_log.write(_DIVIDER)
    239     report_log.write('Repair version for board %s is now %s.\n' %
    240                      (arguments.board, cros_version))
    241     report_log.write('Firmware       for board %s is now %s.\n' %
    242                      (arguments.board, fw_version))
    243     return cros_version
    244 
    245 
    246 def _create_host(hostname, afe, afe_host):
    247     """Create a CrosHost object for a DUT to be installed.
    248 
    249     @param hostname  Hostname of the target DUT.
    250     @param afe       A frontend.AFE object.
    251     @param afe_host  AFE Host object for the DUT.
    252     """
    253     machine_dict = {
    254             'hostname': hostname,
    255             'afe_host': afe_host,
    256             'host_info_store': afe_store.AfeStore(hostname, afe),
    257     }
    258     servo_args = hosts.CrosHost.get_servo_arguments({})
    259     return hosts.create_host(machine_dict, servo_args=servo_args)
    260 
    261 
    262 def _try_lock_host(afe_host):
    263     """Lock a host in the AFE, and report whether it succeeded.
    264 
    265     The lock action is logged regardless of success; failures are
    266     logged if they occur.
    267 
    268     @param afe_host AFE Host instance to be locked.
    269 
    270     @return `True` on success, or `False` on failure.
    271     """
    272     try:
    273         logging.warning('Locking host now.')
    274         afe_host.modify(locked=True,
    275                         lock_reason=_LOCK_REASON_EXISTING)
    276     except Exception as e:
    277         logging.exception('Failed to lock: %s', e)
    278         return False
    279     return True
    280 
    281 
    282 def _try_unlock_host(afe_host):
    283     """Unlock a host in the AFE, and report whether it succeeded.
    284 
    285     The unlock action is logged regardless of success; failures are
    286     logged if they occur.
    287 
    288     @param afe_host AFE Host instance to be unlocked.
    289 
    290     @return `True` on success, or `False` on failure.
    291     """
    292     try:
    293         logging.warning('Unlocking host.')
    294         afe_host.modify(locked=False, lock_reason='')
    295     except Exception as e:
    296         logging.exception('Failed to unlock: %s', e)
    297         return False
    298     return True
    299 
    300 
    301 def _update_host_attributes(afe, hostname, host_attr_dict):
    302     """Update the attributes for a given host.
    303 
    304     @param afe             AFE object for RPC calls.
    305     @param hostname        Name of the host to be updated.
    306     @param host_attr_dict  Dict of host attributes to store in the AFE.
    307     """
    308     # Let's grab the servo hostname/port/serial from host_attr_dict
    309     # if possible.
    310     host_attr_servo_host = None
    311     host_attr_servo_port = None
    312     host_attr_servo_serial = None
    313     if hostname in host_attr_dict:
    314         host_attr_servo_host = host_attr_dict[hostname].get(
    315                 servo_host.SERVO_HOST_ATTR)
    316         host_attr_servo_port = host_attr_dict[hostname].get(
    317                 servo_host.SERVO_PORT_ATTR)
    318         host_attr_servo_serial = host_attr_dict[hostname].get(
    319                 servo_host.SERVO_SERIAL_ATTR)
    320 
    321     servo_hostname = (host_attr_servo_host or
    322                       servo_host.make_servo_hostname(hostname))
    323     servo_port = (host_attr_servo_port or
    324                   str(servo_host.ServoHost.DEFAULT_PORT))
    325     afe.set_host_attribute(servo_host.SERVO_HOST_ATTR,
    326                            servo_hostname,
    327                            hostname=hostname)
    328     afe.set_host_attribute(servo_host.SERVO_PORT_ATTR,
    329                            servo_port,
    330                            hostname=hostname)
    331     if host_attr_servo_serial:
    332         afe.set_host_attribute(servo_host.SERVO_SERIAL_ATTR,
    333                                host_attr_servo_serial,
    334                                hostname=hostname)
    335 
    336 
    337 def _get_afe_host(afe, hostname, arguments, host_attr_dict):
    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 from which to get the host.
    347     @param hostname        Name of the host to look up or create.
    348     @param arguments       Command line arguments with options.
    349     @param host_attr_dict  Dict of host attributes to store in the AFE.
    350 
    351     @return A tuple of the afe_host, plus a flag. The flag indicates
    352             whether the Host should be unlocked if subsequent operations
    353             fail.  (Hosts are always unlocked after success).
    354     """
    355     hostlist = afe.get_hosts([hostname])
    356     unlock_on_failure = False
    357     if hostlist:
    358         afe_host = hostlist[0]
    359         if not afe_host.locked:
    360             if _try_lock_host(afe_host):
    361                 unlock_on_failure = True
    362             else:
    363                 raise Exception('Failed to lock host')
    364         if afe_host.status not in host_states.IDLE_STATES:
    365             if unlock_on_failure and not _try_unlock_host(afe_host):
    366                 raise Exception('Host is in use, and failed to unlock it')
    367             raise Exception('Host is in use by Autotest')
    368     else:
    369         afe_host = afe.create_host(hostname,
    370                                    locked=True,
    371                                    lock_reason=_LOCK_REASON_NEW_HOST)
    372         afe_host.add_labels([Labels.BOARD_PREFIX + arguments.board])
    373 
    374     _update_host_attributes(afe, hostname, host_attr_dict)
    375     afe_host = afe.get_hosts([hostname])[0]
    376     return afe_host, unlock_on_failure
    377 
    378 
    379 def _install_firmware(host):
    380     """Install dev-signed firmware after removing write-protect.
    381 
    382     At start, it's assumed that hardware write-protect is disabled,
    383     the DUT is in dev mode, and the servo's USB stick already has a
    384     test image installed.
    385 
    386     The firmware is installed by powering on and typing ctrl+U on
    387     the keyboard in order to boot the the test image from USB.  Once
    388     the DUT is booted, we run a series of commands to install the
    389     read-only firmware from the test image.  Then we clear debug
    390     mode, and shut down.
    391 
    392     @param host   Host instance to use for servo and ssh operations.
    393     """
    394     servo = host.servo
    395     # First power on.  We sleep to allow the firmware plenty of time
    396     # to display the dev-mode screen; some boards take their time to
    397     # be ready for the ctrl+U after power on.
    398     servo.get_power_state_controller().power_off()
    399     servo.switch_usbkey('dut')
    400     servo.get_power_state_controller().power_on()
    401     time.sleep(10)
    402     # Dev mode screen should be up now:  type ctrl+U and wait for
    403     # boot from USB to finish.
    404     servo.ctrl_u()
    405     if not host.wait_up(timeout=host.USB_BOOT_TIMEOUT):
    406         raise Exception('DUT failed to boot in dev mode for '
    407                         'firmware update')
    408     # Disable software-controlled write-protect for both FPROMs, and
    409     # install the RO firmware.
    410     for fprom in ['host', 'ec']:
    411         host.run('flashrom -p %s --wp-disable' % fprom,
    412                  ignore_status=True)
    413     host.run('chromeos-firmwareupdate --mode=factory')
    414     # Get us out of dev-mode and clear GBB flags.  GBB flags are
    415     # non-zero because boot from USB was enabled.
    416     host.run('/usr/share/vboot/bin/set_gbb_flags.sh 0',
    417              ignore_status=True)
    418     host.run('crossystem disable_dev_request=1',
    419              ignore_status=True)
    420     host.halt()
    421 
    422 
    423 def _install_test_image(host, arguments):
    424     """Install a test image to the DUT.
    425 
    426     Install a stable test image on the DUT using the full servo
    427     repair flow.
    428 
    429     @param host       Host instance for the DUT being installed.
    430     @param arguments  Command line arguments with options.
    431     """
    432     # Don't timeout probing for the host usb device, there could be a bunch
    433     # of servos probing at the same time on the same servo host.  And
    434     # since we can't pass None through the xml rpcs, use 0 to indicate None.
    435     if not host.servo.probe_host_usb_dev(timeout=0):
    436         raise Exception('No USB stick detected on Servo host')
    437     try:
    438         if not arguments.noinstall:
    439             if not arguments.nostage:
    440                 host.servo.image_to_servo_usb(
    441                         host.stage_image_for_servo())
    442             if arguments.full_deploy:
    443                 _install_firmware(host)
    444             host.servo_install()
    445     except error.AutoservRunError as e:
    446         logging.exception('Failed to install: %s', e)
    447         raise Exception('chromeos-install failed')
    448     finally:
    449         host.close()
    450 
    451 
    452 def _install_and_update_afe(afe, hostname, arguments, host_attr_dict):
    453     """Perform all installation and AFE updates.
    454 
    455     First, lock the host if it exists and is unlocked.  Then,
    456     install the test image on the DUT.  At the end, unlock the
    457     DUT, unless the installation failed and the DUT was locked
    458     before we started.
    459 
    460     If installation succeeds, make sure the DUT is in the AFE,
    461     and make sure that it has basic labels.
    462 
    463     @param afe               AFE object for RPC calls.
    464     @param hostname          Host name of the DUT.
    465     @param arguments         Command line arguments with options.
    466     @param host_attr_dict    Dict of host attributes to store in the AFE.
    467     """
    468     afe_host, unlock_on_failure = _get_afe_host(afe, hostname, arguments,
    469                                                 host_attr_dict)
    470     try:
    471         host = _create_host(hostname, afe, afe_host)
    472         _install_test_image(host, arguments)
    473         host.labels.update_labels(host)
    474         platform_labels = afe.get_labels(
    475                 host__hostname=hostname, platform=True)
    476         if not platform_labels:
    477             platform = host.get_platform()
    478             new_labels = afe.get_labels(name=platform)
    479             if not new_labels:
    480                 afe.create_label(platform, platform=True)
    481             afe_host.add_labels([platform])
    482         version = [label for label in afe_host.labels
    483                        if label.startswith(VERSION_PREFIX)]
    484         if version:
    485             afe_host.remove_labels(version)
    486     except Exception as e:
    487         if unlock_on_failure and not _try_unlock_host(afe_host):
    488             logging.error('Failed to unlock host!')
    489         raise
    490 
    491     if not _try_unlock_host(afe_host):
    492         raise Exception('Install succeeded, but failed to unlock the DUT.')
    493 
    494 
    495 def _install_dut(arguments, host_attr_dict, hostname):
    496     """Deploy or repair a single DUT.
    497 
    498     Implementation note: This function is expected to run in a
    499     subprocess created by a multiprocessing Pool object.  As such,
    500     it can't (shouldn't) write to shared files like `sys.stdout`.
    501 
    502     @param hostname        Host name of the DUT to install on.
    503     @param arguments       Command line arguments with options.
    504     @param host_attr_dict  Dict of host attributes to store in the AFE.
    505 
    506     @return On success, return `None`.  On failure, return a string
    507             with an error message.
    508     """
    509     logpath = os.path.join(arguments.logdir, hostname + '.log')
    510     logfile = open(logpath, 'w')
    511 
    512     # In some cases, autotest code that we call during install may
    513     # put stuff onto stdout with 'print' statements.  Most notably,
    514     # the AFE frontend may print 'FAILED RPC CALL' (boo, hiss).  We
    515     # want nothing from this subprocess going to the output we
    516     # inherited from our parent, so redirect stdout and stderr here,
    517     # before we make any AFE calls.  Note that this does what we
    518     # want only because we're in a subprocess.
    519     sys.stderr = sys.stdout = logfile
    520     _configure_logging_to_file(logfile)
    521 
    522     afe = frontend.AFE(server=arguments.web)
    523     try:
    524         _install_and_update_afe(afe, hostname, arguments, host_attr_dict)
    525     except Exception as e:
    526         logging.exception('Original exception: %s', e)
    527         return str(e)
    528     return None
    529 
    530 
    531 def _report_hosts(report_log, heading, host_results_list):
    532     """Report results for a list of hosts.
    533 
    534     To improve visibility, results are preceded by a header line,
    535     followed by a divider line.  Then results are printed, one host
    536     per line.
    537 
    538     @param report_log         File-like object for logging report
    539                               output.
    540     @param heading            The header string to be printed before
    541                               results.
    542     @param host_results_list  A list of _ReportResult tuples
    543                               to be printed one per line.
    544     """
    545     if not host_results_list:
    546         return
    547     report_log.write(heading)
    548     report_log.write(_DIVIDER)
    549     for result in host_results_list:
    550         report_log.write('{result.hostname:30} {result.message}\n'
    551                          .format(result=result))
    552     report_log.write('\n')
    553 
    554 
    555 def _report_results(afe, report_log, hostnames, results):
    556     """Gather and report a summary of results from installation.
    557 
    558     Segregate results into successes and failures, reporting
    559     each separately.  At the end, report the total of successes
    560     and failures.
    561 
    562     @param afe          AFE object for RPC calls.
    563     @param report_log   File-like object for logging report output.
    564     @param hostnames    List of the hostnames that were tested.
    565     @param results      List of error messages, in the same order
    566                         as the hostnames.  `None` means the
    567                         corresponding host succeeded.
    568     """
    569     successful_hosts = []
    570     success_reports = []
    571     failure_reports = []
    572     for result, hostname in zip(results, hostnames):
    573         if result is None:
    574             successful_hosts.append(hostname)
    575         else:
    576             failure_reports.append(_ReportResult(hostname, result))
    577     if successful_hosts:
    578         afe.reverify_hosts(hostnames=successful_hosts)
    579         for h in afe.get_hosts(hostnames=successful_hosts):
    580             for label in h.labels:
    581                 if label.startswith(Labels.POOL_PREFIX):
    582                     result = _ReportResult(h.hostname,
    583                                            'Host already in %s' % label)
    584                     success_reports.append(result)
    585                     break
    586             else:
    587                 h.add_labels([_DEFAULT_POOL])
    588                 result = _ReportResult(h.hostname,
    589                                        'Host added to %s' % _DEFAULT_POOL)
    590                 success_reports.append(result)
    591     report_log.write(_DIVIDER)
    592     _report_hosts(report_log, 'Successes', success_reports)
    593     _report_hosts(report_log, 'Failures', failure_reports)
    594     report_log.write(
    595         'Installation complete:  %d successes, %d failures.\n' %
    596         (len(success_reports), len(failure_reports)))
    597 
    598 
    599 def _clear_root_logger_handlers():
    600     """Remove all handlers from root logger."""
    601     root_logger = logging.getLogger()
    602     for h in root_logger.handlers:
    603         root_logger.removeHandler(h)
    604 
    605 
    606 def _configure_logging_to_file(logfile):
    607     """Configure the logging module for `install_duts()`.
    608 
    609     @param log_file  Log file object.
    610     """
    611     _clear_root_logger_handlers()
    612     handler = logging.StreamHandler(logfile)
    613     formatter = logging.Formatter(_LOG_FORMAT, time_utils.TIME_FMT)
    614     handler.setFormatter(formatter)
    615     root_logger = logging.getLogger()
    616     root_logger.addHandler(handler)
    617 
    618 
    619 def _get_used_servo_ports(servo_hostname, afe):
    620     """
    621     Return a list of used servo ports for the given servo host.
    622 
    623     @param servo_hostname:  Hostname of the servo host to check for.
    624     @param afe:             AFE instance.
    625 
    626     @returns a list of used ports for the given servo host.
    627     """
    628     used_ports = []
    629     host_list = afe.get_hosts_by_attribute(
    630             attribute=servo_host.SERVO_HOST_ATTR, value=servo_hostname)
    631     for host in host_list:
    632         afe_host = afe.get_hosts(hostname=host)
    633         if afe_host:
    634             servo_port = afe_host[0].attributes.get(servo_host.SERVO_PORT_ATTR)
    635             if servo_port:
    636                 used_ports.append(int(servo_port))
    637     return used_ports
    638 
    639 
    640 def _get_free_servo_port(servo_hostname, used_servo_ports, afe):
    641     """
    642     Get a free servo port for the servo_host.
    643 
    644     @param servo_hostname:    Hostname of the servo host.
    645     @param used_servo_ports:  Dict of dicts that contain the list of used ports
    646                               for the given servo host.
    647     @param afe:               AFE instance.
    648 
    649     @returns a free servo port if servo_hostname is non-empty, otherwise an
    650         empty string.
    651     """
    652     used_ports = []
    653     servo_port = servo_host.ServoHost.DEFAULT_PORT
    654     # If no servo hostname was specified we can assume we're dealing with a
    655     # servo v3 or older deployment since the servo hostname can be
    656     # inferred from the dut hostname (by appending '-servo' to it).  We only
    657     # need to find a free port if we're using a servo v4 since we can use the
    658     # default port for v3 and older.
    659     if not servo_hostname:
    660         return ''
    661     # If we haven't checked this servo host yet, check the AFE if other duts
    662     # used this servo host and grab the ports specified for them.
    663     elif servo_hostname not in used_servo_ports:
    664         used_ports = _get_used_servo_ports(servo_hostname, afe)
    665     else:
    666         used_ports = used_servo_ports[servo_hostname]
    667     used_ports.sort()
    668     if used_ports:
    669         # Range is taken from servod.py in hdctools.
    670         start_port = servo_host.ServoHost.DEFAULT_PORT
    671         end_port = start_port - 99
    672         # We'll choose first port available in descending order.
    673         for port in xrange(start_port, end_port - 1, -1):
    674             if port not in used_ports:
    675               servo_port = port
    676               break
    677     used_ports.append(servo_port)
    678     used_servo_ports[servo_hostname] = used_ports
    679     return servo_port
    680 
    681 
    682 def _get_afe_servo_port(host_info, afe):
    683     """
    684     Get the servo port from the afe if it matches the same servo host hostname.
    685 
    686     @param host_info   HostInfo tuple (hostname, host_attr_dict).
    687 
    688     @returns Servo port (int) if servo host hostname matches the one specified
    689     host_info.host_attr_dict, otherwise None.
    690 
    691     @raises _NoAFEServoPortError: When there is no stored host info or servo
    692         port host attribute in the AFE for the given host.
    693     """
    694     afe_hosts = afe.get_hosts(hostname=host_info.hostname)
    695     if not afe_hosts:
    696         raise _NoAFEServoPortError
    697 
    698     servo_port = afe_hosts[0].attributes.get(servo_host.SERVO_PORT_ATTR)
    699     afe_servo_host = afe_hosts[0].attributes.get(servo_host.SERVO_HOST_ATTR)
    700     host_info_servo_host = host_info.host_attr_dict.get(
    701         servo_host.SERVO_HOST_ATTR)
    702 
    703     if afe_servo_host == host_info_servo_host and servo_port:
    704         return int(servo_port)
    705     else:
    706         raise _NoAFEServoPortError
    707 
    708 
    709 def _get_host_attributes(host_info_list, afe):
    710     """
    711     Get host attributes if a hostname_file was supplied.
    712 
    713     @param host_info_list   List of HostInfo tuples (hostname, host_attr_dict).
    714 
    715     @returns Dict of attributes from host_info_list.
    716     """
    717     host_attributes = {}
    718     # We need to choose servo ports for these hosts but we need to make sure
    719     # we don't choose ports already used. We'll store all used ports in a
    720     # dict of lists where the key is the servo_host and the val is a list of
    721     # ports used.
    722     used_servo_ports = {}
    723     for host_info in host_info_list:
    724         host_attr_dict = host_info.host_attr_dict
    725         # If the host already has an entry in the AFE that matches the same
    726         # servo host hostname and the servo port is set, use that port.
    727         try:
    728             host_attr_dict[servo_host.SERVO_PORT_ATTR] = _get_afe_servo_port(
    729                 host_info, afe)
    730         except _NoAFEServoPortError:
    731             host_attr_dict[servo_host.SERVO_PORT_ATTR] = _get_free_servo_port(
    732                 host_attr_dict[servo_host.SERVO_HOST_ATTR], used_servo_ports,
    733                 afe)
    734         host_attributes[host_info.hostname] = host_attr_dict
    735     return host_attributes
    736 
    737 
    738 def install_duts(argv, full_deploy):
    739     """Install a test image on DUTs, and deploy them.
    740 
    741     This handles command line parsing for both the repair and
    742     deployment commands.  The two operations are largely identical;
    743     the main difference is that full deployment includes flashing
    744     dev-signed firmware on the DUT prior to installing the test
    745     image.
    746 
    747     @param argv         Command line arguments to be parsed.
    748     @param full_deploy  If true, do the full deployment that includes
    749                         flashing dev-signed RO firmware onto the DUT.
    750     """
    751     # Override tempfile.tempdir.  Some of the autotest code we call
    752     # will create temporary files that don't get cleaned up.  So, we
    753     # put the temp files in our results directory, so that we can
    754     # clean up everything in one fell swoop.
    755     tempfile.tempdir = tempfile.mkdtemp()
    756     # MALCOLM:
    757     #   Be comforted.
    758     #   Let's make us med'cines of our great revenge,
    759     #   To cure this deadly grief.
    760     atexit.register(shutil.rmtree, tempfile.tempdir)
    761 
    762     arguments = commandline.parse_command(argv, full_deploy)
    763     if not arguments:
    764         sys.exit(1)
    765     sys.stderr.write('Installation output logs in %s\n' % arguments.logdir)
    766 
    767     # We don't want to distract the user with logging output, so we catch
    768     # logging output in a file.
    769     logging_file_path = os.path.join(arguments.logdir, 'debug.log')
    770     logfile = open(logging_file_path, 'w')
    771     _configure_logging_to_file(logfile)
    772 
    773     report_log_path = os.path.join(arguments.logdir, 'report.log')
    774     with open(report_log_path, 'w') as report_log_file:
    775         report_log = _MultiFileWriter([report_log_file, sys.stdout])
    776         afe = frontend.AFE(server=arguments.web)
    777         current_build = _update_build(afe, report_log, arguments)
    778         host_attr_dict = _get_host_attributes(arguments.host_info_list, afe)
    779         install_pool = multiprocessing.Pool(len(arguments.hostnames))
    780         install_function = functools.partial(_install_dut, arguments,
    781                                              host_attr_dict)
    782         results_list = install_pool.map(install_function, arguments.hostnames)
    783         _report_results(afe, report_log, arguments.hostnames, results_list)
    784 
    785         gspath = _get_upload_log_path(arguments)
    786         report_log.write('Logs will be uploaded to %s\n' % (gspath,))
    787 
    788     try:
    789         _upload_logs(arguments.logdir, gspath)
    790     except Exception as e:
    791         upload_failure_log_path = os.path.join(arguments.logdir,
    792                                                'gs_upload_failure.log')
    793         with open(upload_failure_log_path, 'w') as file:
    794             traceback.print_exc(limit=None, file=file)
    795         sys.stderr.write('Failed to upload logs;'
    796                          ' failure details are stored in {}.\n'
    797                          .format(upload_failure_log_path))
    798