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.stable_images import assign_stable_images 78from 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 99class _NoAFEServoPortError(Exception): 100 """Exception when there is no servo port stored in the AFE.""" 101 102 103class _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 123def _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 129def _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 139def _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 165def _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 246def _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 262def _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 282def _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 301def _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 337def _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 379def _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 423def _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 452def _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 495def _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 531def _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 555def _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 599def _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 606def _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 619def _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 640def _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 682def _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 709def _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 738def 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