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