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