| #!/usr/bin/env python |
| # Copyright 2015 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Install an initial test image on a set of DUTs. |
| |
| The methods in this module are meant for two nominally distinct use |
| cases that share a great deal of code internally. The first use |
| case is for deployment of DUTs that have just been placed in the lab |
| for the first time. The second use case is for use after repairing |
| a servo. |
| |
| Newly deployed DUTs may be in a somewhat anomalous state: |
| * The DUTs are running a production base image, not a test image. |
| By extension, the DUTs aren't reachable over SSH. |
| * The DUTs are not necessarily in the AFE database. DUTs that |
| _are_ in the database should be locked. Either way, the DUTs |
| cannot be scheduled to run tests. |
| * The servos for the DUTs need not be configured with the proper |
| overlay. |
| |
| More broadly, it's not expected that the DUT will be working at the |
| start of this operation. If the DUT isn't working at the end of the |
| operation, an error will be reported. |
| |
| The script performs the following functions: |
| * Configure the servo for the target overlay, and test that the |
| servo is generally in good order. |
| * For the full deployment case, install dev-signed RO firmware |
| from the designated stable test image for the DUTs. |
| * For both cases, use servo to install the stable test image from |
| USB. |
| * If the DUT isn't in the AFE database, add it. |
| |
| The script imposes these preconditions: |
| * Every DUT has a properly connected servo. |
| * Every DUT and servo have proper DHCP and DNS configurations. |
| * Every servo host is up and running, and accessible via SSH. |
| * There is a known, working test image that can be staged and |
| installed on the target DUTs via servo. |
| * Every DUT has the same board and model. |
| * For the full deployment case, every DUT must be in dev mode, |
| and configured to allow boot from USB with ctrl+U. |
| |
| The implementation uses the `multiprocessing` module to run all |
| installations in parallel, separate processes. |
| |
| """ |
| |
| import atexit |
| from collections import namedtuple |
| import functools |
| import json |
| import logging |
| import multiprocessing |
| import os |
| import shutil |
| import sys |
| import tempfile |
| import time |
| import traceback |
| |
| from chromite.lib import gs |
| |
| import common |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib import host_states |
| from autotest_lib.client.common_lib import time_utils |
| from autotest_lib.client.common_lib import utils |
| from autotest_lib.client.common_lib.cros import retry |
| from autotest_lib.server import afe_utils |
| from autotest_lib.server import constants |
| from autotest_lib.server import frontend |
| from autotest_lib.server import hosts |
| from autotest_lib.server.cros.dynamic_suite.constants import VERSION_PREFIX |
| from autotest_lib.server.hosts import afe_store |
| from autotest_lib.server.hosts import servo_host |
| from autotest_lib.site_utils.deployment import cmdvalidate |
| from autotest_lib.site_utils.deployment.prepare import dut as preparedut |
| from autotest_lib.site_utils.stable_images import build_data |
| from autotest_lib.utils import labellib |
| |
| |
| _LOG_FORMAT = '%(asctime)s | %(levelname)-10s | %(message)s' |
| |
| _DEFAULT_POOL = constants.Labels.POOL_PREFIX + 'suites' |
| |
| _DIVIDER = '\n============\n' |
| |
| _LOG_BUCKET_NAME = 'chromeos-install-logs' |
| |
| _OMAHA_STATUS = 'gs://chromeos-build-release-console/omaha_status.json' |
| |
| # Lock reasons we'll pass when locking DUTs, depending on the |
| # host's prior state. |
| _LOCK_REASON_EXISTING = 'Repairing or deploying an existing host' |
| _LOCK_REASON_NEW_HOST = 'Repairing or deploying a new host' |
| |
| _ReportResult = namedtuple('_ReportResult', ['hostname', 'message']) |
| |
| |
| class InstallFailedError(Exception): |
| """Generic error raised explicitly in this module.""" |
| |
| |
| class _NoAFEServoPortError(InstallFailedError): |
| """Exception when there is no servo port stored in the AFE.""" |
| |
| |
| class _MultiFileWriter(object): |
| |
| """Group file objects for writing at once.""" |
| |
| def __init__(self, files): |
| """Initialize _MultiFileWriter. |
| |
| @param files Iterable of file objects for writing. |
| """ |
| self._files = files |
| |
| def write(self, s): |
| """Write a string to the files. |
| |
| @param s Write this string. |
| """ |
| for file in self._files: |
| file.write(s) |
| |
| |
| def _get_upload_log_path(arguments): |
| return 'gs://{bucket}/{name}'.format( |
| bucket=_LOG_BUCKET_NAME, |
| name=arguments.upload_basename) |
| |
| |
| def _upload_logs(dirpath, gspath): |
| """Upload report logs to Google Storage. |
| |
| @param dirpath Path to directory containing the logs. |
| @param gspath Path to GS bucket. |
| """ |
| ctx = gs.GSContext() |
| ctx.Copy(dirpath, gspath, recursive=True) |
| |
| |
| def _get_omaha_build(board): |
| """Get the currently preferred Beta channel build for `board`. |
| |
| Open and read through the JSON file provided by GoldenEye that |
| describes what version Omaha is currently serving for all boards |
| on all channels. Find the entry for `board` on the Beta channel, |
| and return that version string. |
| |
| @param board The board to look up from GoldenEye. |
| |
| @return Returns a Chrome OS version string in standard form |
| R##-####.#.#. Will return `None` if no Beta channel |
| entry is found. |
| """ |
| ctx = gs.GSContext() |
| omaha_status = json.loads(ctx.Cat(_OMAHA_STATUS)) |
| omaha_board = board.replace('_', '-') |
| for e in omaha_status['omaha_data']: |
| if (e['channel'] == 'beta' and |
| e['board']['public_codename'] == omaha_board): |
| milestone = e['chrome_version'].split('.')[0] |
| build = e['chrome_os_version'] |
| return 'R%s-%s' % (milestone, build) |
| return None |
| |
| |
| def _update_build(afe, report_log, arguments): |
| """Update the stable_test_versions table. |
| |
| This calls the `set_stable_version` RPC call to set the stable |
| repair version selected by this run of the command. Additionally, |
| this updates the stable firmware for the board. The repair version |
| is selected from three possible versions: |
| * The stable test version currently in the AFE database. |
| * The version Omaha is currently serving as the Beta channel |
| build. |
| * The version supplied by the user. |
| The actual version selected will be whichever of these three is |
| the most up-to-date version. |
| |
| The stable firmware version will be set to whatever firmware is |
| bundled in the selected repair image. If the selected repair image bundles |
| firmware for more than one model, then the firmware for every model in the |
| build will be updated. |
| |
| This function will log information about the available versions |
| prior to selection. After selection the repair and firmware |
| versions slected will be logged. |
| |
| @param afe AFE object for RPC calls. |
| @param report_log File-like object for logging report output. |
| @param arguments Command line arguments with options. |
| |
| @return Returns the version selected. |
| """ |
| # Gather the current AFE and Omaha version settings, and report them |
| # to the user. |
| cros_version_map = afe.get_stable_version_map(afe.CROS_IMAGE_TYPE) |
| fw_version_map = afe.get_stable_version_map(afe.FIRMWARE_IMAGE_TYPE) |
| afe_cros = cros_version_map.get_version(arguments.board) |
| afe_fw = fw_version_map.get_version(arguments.board) |
| omaha_cros = _get_omaha_build(arguments.board) |
| report_log.write('AFE version is %s.\n' % afe_cros) |
| report_log.write('Omaha version is %s.\n' % omaha_cros) |
| report_log.write('AFE firmware is %s.\n' % afe_fw) |
| cros_version = afe_cros |
| |
| # Check whether we should upgrade the repair build to either |
| # the Omaha or the user's requested build. If we do, we must |
| # also update the firmware version. |
| if (omaha_cros is not None |
| and (cros_version is None or |
| utils.compare_versions(cros_version, omaha_cros) < 0)): |
| cros_version = omaha_cros |
| if arguments.build and arguments.build != cros_version: |
| if (cros_version is None |
| or utils.compare_versions(cros_version, arguments.build) < 0): |
| cros_version = arguments.build |
| else: |
| report_log.write('Selected version %s is too old; ' |
| 'using version %s' |
| % (arguments.build, cros_version)) |
| |
| afe_fw_versions = {arguments.board: afe_fw} |
| fw_versions = build_data.get_firmware_versions( |
| arguments.board, cros_version) |
| # At this point `cros_version` is our new repair build, and |
| # `fw_version` is our new target firmware. Call the AFE back with |
| # updates as necessary. |
| if not arguments.dry_run: |
| if cros_version != afe_cros: |
| cros_version_map.set_version(arguments.board, cros_version) |
| |
| if fw_versions != afe_fw_versions: |
| for model, fw_version in fw_versions.iteritems(): |
| if fw_version is not None: |
| fw_version_map.set_version(model, fw_version) |
| else: |
| fw_version_map.delete_version(model) |
| |
| # Report the new state of the world. |
| report_log.write(_DIVIDER) |
| report_log.write('Repair CrOS version for board %s is now %s.\n' % |
| (arguments.board, cros_version)) |
| for model, fw_version in fw_versions.iteritems(): |
| report_log.write('Firmware version for model %s is now %s.\n' % |
| (model, fw_version)) |
| return cros_version |
| |
| |
| def _create_host(hostname, afe, afe_host): |
| """Create a CrosHost object for the DUT. |
| |
| This host object is used to update AFE label information for the DUT, but |
| can not be used for installation image on the DUT. In particular, this host |
| object does not have the servo attribute populated. |
| |
| @param hostname Hostname of the target DUT. |
| @param afe A frontend.AFE object. |
| @param afe_host AFE Host object for the DUT. |
| """ |
| machine_dict = { |
| 'hostname': hostname, |
| 'afe_host': afe_host, |
| 'host_info_store': afe_store.AfeStore(hostname, afe), |
| } |
| return hosts.create_host(machine_dict) |
| |
| |
| def _try_lock_host(afe_host): |
| """Lock a host in the AFE, and report whether it succeeded. |
| |
| The lock action is logged regardless of success; failures are |
| logged if they occur. |
| |
| @param afe_host AFE Host instance to be locked. |
| |
| @return `True` on success, or `False` on failure. |
| """ |
| try: |
| logging.warning('Locking host now.') |
| afe_host.modify(locked=True, |
| lock_reason=_LOCK_REASON_EXISTING) |
| except Exception as e: |
| logging.exception('Failed to lock: %s', e) |
| return False |
| return True |
| |
| |
| def _try_unlock_host(afe_host): |
| """Unlock a host in the AFE, and report whether it succeeded. |
| |
| The unlock action is logged regardless of success; failures are |
| logged if they occur. |
| |
| @param afe_host AFE Host instance to be unlocked. |
| |
| @return `True` on success, or `False` on failure. |
| """ |
| try: |
| logging.warning('Unlocking host.') |
| afe_host.modify(locked=False, lock_reason='') |
| except Exception as e: |
| logging.exception('Failed to unlock: %s', e) |
| return False |
| return True |
| |
| |
| def _update_host_attributes(afe, hostname, host_attrs): |
| """Update the attributes for a given host. |
| |
| @param afe AFE object for RPC calls. |
| @param hostname Host name of the DUT. |
| @param host_attrs Dictionary with attributes to be applied to the |
| host. |
| """ |
| s_hostname, s_port, s_serial = _extract_servo_attributes(hostname, |
| host_attrs) |
| afe.set_host_attribute(servo_host.SERVO_HOST_ATTR, |
| s_hostname, |
| hostname=hostname) |
| afe.set_host_attribute(servo_host.SERVO_PORT_ATTR, |
| s_port, |
| hostname=hostname) |
| if s_serial: |
| afe.set_host_attribute(servo_host.SERVO_SERIAL_ATTR, |
| s_serial, |
| hostname=hostname) |
| |
| |
| def _extract_servo_attributes(hostname, host_attrs): |
| """Extract servo attributes from the host attribute dict, setting defaults. |
| |
| @return (servo_hostname, servo_port, servo_serial) |
| """ |
| # Grab the servo hostname/port/serial from `host_attrs` if supplied. |
| # For new servo V4 deployments, we require the user to supply the |
| # attributes (because there are no appropriate defaults). So, if |
| # none are supplied, we assume it can't be V4, and apply the |
| # defaults for servo V3. |
| s_hostname = (host_attrs.get(servo_host.SERVO_HOST_ATTR) or |
| servo_host.make_servo_hostname(hostname)) |
| s_port = (host_attrs.get(servo_host.SERVO_PORT_ATTR) or |
| str(servo_host.ServoHost.DEFAULT_PORT)) |
| s_serial = host_attrs.get(servo_host.SERVO_SERIAL_ATTR) |
| return s_hostname, s_port, s_serial |
| |
| |
| def _wait_for_idle(afe, host_id): |
| """Helper function for `_ensure_host_idle`. |
| |
| Poll the host with the given `host_id` via `afe`, waiting for it |
| to become idle. Run forever; the caller takes care of timing out. |
| |
| @param afe AFE object for RPC calls. |
| @param host_id Id of the host that's expected to become idle. |
| """ |
| while True: |
| afe_host = afe.get_hosts(id=host_id)[0] |
| if afe_host.status in host_states.IDLE_STATES: |
| return |
| # Let's not spam our server. |
| time.sleep(0.2) |
| |
| |
| def _ensure_host_idle(afe, afe_host): |
| """Abort any special task running on `afe_host`. |
| |
| The given `afe_host` is currently locked. If there's a special task |
| running on the given `afe_host`, abort it, then wait for the host to |
| show up as idle, return whether the operation succeeded. |
| |
| @param afe AFE object for RPC calls. |
| @param afe_host Host to be aborted. |
| |
| @return A true value if the host is idle at return, or a false value |
| if the host wasn't idle after some reasonable time. |
| """ |
| # We need to talk to the shard, not the master, for at least two |
| # reasons: |
| # * The `abort_special_tasks` RPC doesn't forward from the master |
| # to the shard, and only the shard has access to the special |
| # tasks. |
| # * Host status on the master can lag actual status on the shard |
| # by several minutes. Only the shard can provide status |
| # guaranteed to post-date the call to lock the DUT. |
| if afe_host.shard: |
| afe = frontend.AFE(server=afe_host.shard) |
| afe_host = afe.get_hosts(id=afe_host.id)[0] |
| if afe_host.status in host_states.IDLE_STATES: |
| return True |
| afe.run('abort_special_tasks', host_id=afe_host.id, is_active=1) |
| return not retry.timeout(_wait_for_idle, (afe, afe_host.id), |
| timeout_sec=5.0)[0] |
| |
| |
| def _get_afe_host(afe, hostname, host_attrs, arguments): |
| """Get an AFE Host object for the given host. |
| |
| If the host is found in the database, return the object |
| from the RPC call with the updated attributes in host_attr_dict. |
| |
| If no host is found, create one with appropriate servo |
| attributes and the given board label. |
| |
| @param afe AFE object for RPC calls. |
| @param hostname Host name of the DUT. |
| @param host_attrs Dictionary with attributes to be applied to the |
| host. |
| @param arguments Command line arguments with options. |
| |
| @return A tuple of the afe_host, plus a flag. The flag indicates |
| whether the Host should be unlocked if subsequent operations |
| fail. (Hosts are always unlocked after success). |
| """ |
| hostlist = afe.get_hosts([hostname]) |
| unlock_on_failure = False |
| if hostlist: |
| afe_host = hostlist[0] |
| if not afe_host.locked: |
| if _try_lock_host(afe_host): |
| unlock_on_failure = True |
| else: |
| raise Exception('Failed to lock host') |
| if not _ensure_host_idle(afe, afe_host): |
| if unlock_on_failure and not _try_unlock_host(afe_host): |
| raise Exception('Failed to abort host, and failed to unlock it') |
| raise Exception('Failed to abort task on host') |
| # This host was pre-existing; if the user didn't supply |
| # attributes, don't update them, because the defaults may |
| # not be correct. |
| if host_attrs: |
| _update_host_attributes(afe, hostname, host_attrs) |
| else: |
| afe_host = afe.create_host(hostname, |
| locked=True, |
| lock_reason=_LOCK_REASON_NEW_HOST) |
| _update_host_attributes(afe, hostname, host_attrs) |
| |
| # Correct board/model label is critical to installation. Always ensure user |
| # supplied board/model matches the AFE information. |
| _ensure_label_in_afe(afe_host, 'board', arguments.board) |
| _ensure_label_in_afe(afe_host, 'model', arguments.model) |
| |
| afe_host = afe.get_hosts([hostname])[0] |
| return afe_host, unlock_on_failure |
| |
| |
| def _ensure_label_in_afe(afe_host, label_name, label_value): |
| """Add the given board label, only if one doesn't already exist. |
| |
| @params label_name name of the label, e.g. 'board', 'model', etc. |
| @params label_value value of the label. |
| |
| @raises InstallFailedError if supplied board is different from existing |
| board in AFE. |
| """ |
| if not label_value: |
| return |
| |
| labels = labellib.LabelsMapping(afe_host.labels) |
| if label_name not in labels: |
| afe_host.add_labels(['%s:%s' % (label_name, label_value)]) |
| return |
| |
| existing_value = labels[label_name] |
| if label_value != existing_value: |
| raise InstallFailedError( |
| 'provided %s %s does not match the %s %s for host %s' % |
| (label_name, label_value, label_name, existing_value, |
| afe_host.hostname)) |
| |
| |
| def _create_host_for_installation(host, arguments): |
| """Creates a context manager of hosts.CrosHost object for installation. |
| |
| The host object yielded by the returned context manager is agnostic of the |
| infrastructure environment. In particular, it does not have any references |
| to the AFE. |
| |
| @param host: A server.hosts.CrosHost object. |
| @param arguments: Parsed commandline arguments for this script. |
| |
| @return a context manager which yields hosts.CrosHost object. |
| """ |
| info = host.host_info_store.get() |
| s_host, s_port, s_serial = _extract_servo_attributes(host.hostname, |
| info.attributes) |
| return preparedut.create_host(host.hostname, arguments.board, |
| arguments.model, s_host, s_port, s_serial, |
| arguments.logdir) |
| |
| |
| def _install_test_image(host, arguments): |
| """Install a test image to the DUT. |
| |
| Install a stable test image on the DUT using the full servo |
| repair flow. |
| |
| @param host Host instance for the DUT being installed. |
| @param arguments Command line arguments with options. |
| """ |
| repair_image = _get_cros_repair_image_name(host) |
| logging.info('Using repair image %s', repair_image) |
| if arguments.dry_run: |
| return |
| if arguments.stageusb: |
| try: |
| preparedut.download_image_to_servo_usb(host, repair_image) |
| except Exception as e: |
| logging.exception('Failed to stage image on USB: %s', e) |
| raise Exception('USB staging failed') |
| if arguments.install_firmware: |
| try: |
| if arguments.using_servo: |
| logging.debug('Install FW using servo.') |
| preparedut.flash_firmware_using_servo(host, repair_image) |
| else: |
| logging.debug('Install FW by chromeos-firmwareupdate.') |
| preparedut.install_firmware(host) |
| except error.AutoservRunError as e: |
| logging.exception('Firmware update failed: %s', e) |
| msg = '%s failed' % ( |
| 'Flashing firmware using servo' if arguments.using_servo |
| else 'chromeos-firmwareupdate') |
| raise Exception(msg) |
| if arguments.install_test_image: |
| try: |
| preparedut.install_test_image(host) |
| except error.AutoservRunError as e: |
| logging.exception('Failed to install: %s', e) |
| raise Exception('chromeos-install failed') |
| |
| |
| def _install_and_update_afe(afe, hostname, host_attrs, arguments): |
| """Perform all installation and AFE updates. |
| |
| First, lock the host if it exists and is unlocked. Then, |
| install the test image on the DUT. At the end, unlock the |
| DUT, unless the installation failed and the DUT was locked |
| before we started. |
| |
| If installation succeeds, make sure the DUT is in the AFE, |
| and make sure that it has basic labels. |
| |
| @param afe AFE object for RPC calls. |
| @param hostname Host name of the DUT. |
| @param host_attrs Dictionary with attributes to be applied to the |
| host. |
| @param arguments Command line arguments with options. |
| """ |
| afe_host, unlock_on_failure = _get_afe_host(afe, hostname, host_attrs, |
| arguments) |
| host = None |
| try: |
| host = _create_host(hostname, afe, afe_host) |
| with _create_host_for_installation(host, arguments) as host_to_install: |
| _install_test_image(host_to_install, arguments) |
| _update_servo_type_attribute(host_to_install, host) |
| |
| if arguments.install_test_image and not arguments.dry_run: |
| host.labels.update_labels(host) |
| platform_labels = afe.get_labels( |
| host__hostname=hostname, platform=True) |
| if not platform_labels: |
| platform = host.get_platform() |
| new_labels = afe.get_labels(name=platform) |
| if not new_labels: |
| afe.create_label(platform, platform=True) |
| afe_host.add_labels([platform]) |
| version = [label for label in afe_host.labels |
| if label.startswith(VERSION_PREFIX)] |
| if version and not arguments.dry_run: |
| afe_host.remove_labels(version) |
| except Exception as e: |
| if unlock_on_failure and not _try_unlock_host(afe_host): |
| logging.error('Failed to unlock host!') |
| raise |
| finally: |
| if host is not None: |
| host.close() |
| |
| if not _try_unlock_host(afe_host): |
| raise Exception('Install succeeded, but failed to unlock the DUT.') |
| |
| |
| def _install_dut(arguments, host_attr_dict, hostname): |
| """Deploy or repair a single DUT. |
| |
| @param arguments Command line arguments with options. |
| @param host_attr_dict Dict mapping hostnames to attributes to be |
| stored in the AFE. |
| @param hostname Host name of the DUT to install on. |
| |
| @return On success, return `None`. On failure, return a string |
| with an error message. |
| """ |
| # In some cases, autotest code that we call during install may |
| # put stuff onto stdout with 'print' statements. Most notably, |
| # the AFE frontend may print 'FAILED RPC CALL' (boo, hiss). We |
| # want nothing from this subprocess going to the output we |
| # inherited from our parent, so redirect stdout and stderr, before |
| # we make any AFE calls. Note that this is reasonable because we're |
| # in a subprocess. |
| |
| logpath = os.path.join(arguments.logdir, hostname + '.log') |
| logfile = open(logpath, 'w') |
| sys.stderr = sys.stdout = logfile |
| _configure_logging_to_file(logfile) |
| |
| afe = frontend.AFE(server=arguments.web) |
| try: |
| _install_and_update_afe(afe, hostname, |
| host_attr_dict.get(hostname, {}), |
| arguments) |
| except Exception as e: |
| logging.exception('Original exception: %s', e) |
| return str(e) |
| return None |
| |
| |
| def _report_hosts(report_log, heading, host_results_list): |
| """Report results for a list of hosts. |
| |
| To improve visibility, results are preceded by a header line, |
| followed by a divider line. Then results are printed, one host |
| per line. |
| |
| @param report_log File-like object for logging report |
| output. |
| @param heading The header string to be printed before |
| results. |
| @param host_results_list A list of _ReportResult tuples |
| to be printed one per line. |
| """ |
| if not host_results_list: |
| return |
| report_log.write(heading) |
| report_log.write(_DIVIDER) |
| for result in host_results_list: |
| report_log.write('{result.hostname:30} {result.message}\n' |
| .format(result=result)) |
| report_log.write('\n') |
| |
| |
| def _report_results(afe, report_log, hostnames, results): |
| """Gather and report a summary of results from installation. |
| |
| Segregate results into successes and failures, reporting |
| each separately. At the end, report the total of successes |
| and failures. |
| |
| @param afe AFE object for RPC calls. |
| @param report_log File-like object for logging report output. |
| @param hostnames List of the hostnames that were tested. |
| @param results List of error messages, in the same order |
| as the hostnames. `None` means the |
| corresponding host succeeded. |
| """ |
| successful_hosts = [] |
| success_reports = [] |
| failure_reports = [] |
| for result, hostname in zip(results, hostnames): |
| if result is None: |
| successful_hosts.append(hostname) |
| else: |
| failure_reports.append(_ReportResult(hostname, result)) |
| if successful_hosts: |
| afe.repair_hosts(hostnames=successful_hosts) |
| for h in afe.get_hosts(hostnames=successful_hosts): |
| for label in h.labels: |
| if label.startswith(constants.Labels.POOL_PREFIX): |
| result = _ReportResult(h.hostname, |
| 'Host already in %s' % label) |
| success_reports.append(result) |
| break |
| else: |
| h.add_labels([_DEFAULT_POOL]) |
| result = _ReportResult(h.hostname, |
| 'Host added to %s' % _DEFAULT_POOL) |
| success_reports.append(result) |
| report_log.write(_DIVIDER) |
| _report_hosts(report_log, 'Successes', success_reports) |
| _report_hosts(report_log, 'Failures', failure_reports) |
| report_log.write( |
| 'Installation complete: %d successes, %d failures.\n' % |
| (len(success_reports), len(failure_reports))) |
| |
| |
| def _clear_root_logger_handlers(): |
| """Remove all handlers from root logger.""" |
| root_logger = logging.getLogger() |
| for h in root_logger.handlers: |
| root_logger.removeHandler(h) |
| |
| |
| def _configure_logging_to_file(logfile): |
| """Configure the logging module for `install_duts()`. |
| |
| @param log_file Log file object. |
| """ |
| _clear_root_logger_handlers() |
| handler = logging.StreamHandler(logfile) |
| formatter = logging.Formatter(_LOG_FORMAT, time_utils.TIME_FMT) |
| handler.setFormatter(formatter) |
| root_logger = logging.getLogger() |
| root_logger.addHandler(handler) |
| |
| |
| def _get_used_servo_ports(servo_hostname, afe): |
| """ |
| Return a list of used servo ports for the given servo host. |
| |
| @param servo_hostname: Hostname of the servo host to check for. |
| @param afe: AFE instance. |
| |
| @returns a list of used ports for the given servo host. |
| """ |
| used_ports = [] |
| host_list = afe.get_hosts_by_attribute( |
| attribute=servo_host.SERVO_HOST_ATTR, value=servo_hostname) |
| for host in host_list: |
| afe_host = afe.get_hosts(hostname=host) |
| if afe_host: |
| servo_port = afe_host[0].attributes.get(servo_host.SERVO_PORT_ATTR) |
| if servo_port: |
| used_ports.append(int(servo_port)) |
| return used_ports |
| |
| |
| def _get_free_servo_port(servo_hostname, used_servo_ports, afe): |
| """ |
| Get a free servo port for the servo_host. |
| |
| @param servo_hostname: Hostname of the servo host. |
| @param used_servo_ports: Dict of dicts that contain the list of used ports |
| for the given servo host. |
| @param afe: AFE instance. |
| |
| @returns a free servo port if servo_hostname is non-empty, otherwise an |
| empty string. |
| """ |
| used_ports = [] |
| servo_port = servo_host.ServoHost.DEFAULT_PORT |
| # If no servo hostname was specified we can assume we're dealing with a |
| # servo v3 or older deployment since the servo hostname can be |
| # inferred from the dut hostname (by appending '-servo' to it). We only |
| # need to find a free port if we're using a servo v4 since we can use the |
| # default port for v3 and older. |
| if not servo_hostname: |
| return '' |
| # If we haven't checked this servo host yet, check the AFE if other duts |
| # used this servo host and grab the ports specified for them. |
| elif servo_hostname not in used_servo_ports: |
| used_ports = _get_used_servo_ports(servo_hostname, afe) |
| else: |
| used_ports = used_servo_ports[servo_hostname] |
| used_ports.sort() |
| if used_ports: |
| # Range is taken from servod.py in hdctools. |
| start_port = servo_host.ServoHost.DEFAULT_PORT |
| end_port = start_port - 99 |
| # We'll choose first port available in descending order. |
| for port in xrange(start_port, end_port - 1, -1): |
| if port not in used_ports: |
| servo_port = port |
| break |
| used_ports.append(servo_port) |
| used_servo_ports[servo_hostname] = used_ports |
| return servo_port |
| |
| |
| def _get_afe_servo_port(host_info, afe): |
| """ |
| Get the servo port from the afe if it matches the same servo host hostname. |
| |
| @param host_info HostInfo tuple (hostname, host_attr_dict). |
| |
| @returns Servo port (int) if servo host hostname matches the one specified |
| host_info.host_attr_dict, otherwise None. |
| |
| @raises _NoAFEServoPortError: When there is no stored host info or servo |
| port host attribute in the AFE for the given host. |
| """ |
| afe_hosts = afe.get_hosts(hostname=host_info.hostname) |
| if not afe_hosts: |
| raise _NoAFEServoPortError |
| |
| servo_port = afe_hosts[0].attributes.get(servo_host.SERVO_PORT_ATTR) |
| afe_servo_host = afe_hosts[0].attributes.get(servo_host.SERVO_HOST_ATTR) |
| host_info_servo_host = host_info.host_attr_dict.get( |
| servo_host.SERVO_HOST_ATTR) |
| |
| if afe_servo_host == host_info_servo_host and servo_port: |
| return int(servo_port) |
| else: |
| raise _NoAFEServoPortError |
| |
| |
| def _get_host_attributes(host_info_list, afe): |
| """ |
| Get host attributes if a hostname_file was supplied. |
| |
| @param host_info_list List of HostInfo tuples (hostname, host_attr_dict). |
| |
| @returns Dict of attributes from host_info_list. |
| """ |
| host_attributes = {} |
| # We need to choose servo ports for these hosts but we need to make sure |
| # we don't choose ports already used. We'll store all used ports in a |
| # dict of lists where the key is the servo_host and the val is a list of |
| # ports used. |
| used_servo_ports = {} |
| for host_info in host_info_list: |
| host_attr_dict = host_info.host_attr_dict |
| # If the host already has an entry in the AFE that matches the same |
| # servo host hostname and the servo port is set, use that port. |
| try: |
| host_attr_dict[servo_host.SERVO_PORT_ATTR] = _get_afe_servo_port( |
| host_info, afe) |
| except _NoAFEServoPortError: |
| host_attr_dict[servo_host.SERVO_PORT_ATTR] = _get_free_servo_port( |
| host_attr_dict[servo_host.SERVO_HOST_ATTR], used_servo_ports, |
| afe) |
| host_attributes[host_info.hostname] = host_attr_dict |
| return host_attributes |
| |
| |
| def _get_cros_repair_image_name(host): |
| """Get the CrOS repair image name for given host. |
| |
| @param host: hosts.CrosHost object. This object need not have an AFE |
| reference. |
| """ |
| info = host.host_info_store.get() |
| if not info.board: |
| raise InstallFailedError('Unknown board for given host') |
| return afe_utils.get_stable_cros_image_name(info.board) |
| |
| |
| def install_duts(arguments): |
| """Install a test image on DUTs, and deploy them. |
| |
| This handles command line parsing for both the repair and |
| deployment commands. The two operations are largely identical; |
| the main difference is that full deployment includes flashing |
| dev-signed firmware on the DUT prior to installing the test |
| image. |
| |
| @param arguments Command line arguments with options, as |
| returned by `argparse.Argparser`. |
| """ |
| arguments = cmdvalidate.validate_arguments(arguments) |
| if arguments is None: |
| sys.exit(1) |
| sys.stderr.write('Installation output logs in %s\n' % arguments.logdir) |
| |
| # Override tempfile.tempdir. Some of the autotest code we call |
| # will create temporary files that don't get cleaned up. So, we |
| # put the temp files in our results directory, so that we can |
| # clean up everything at one fell swoop. |
| tempfile.tempdir = tempfile.mkdtemp() |
| atexit.register(shutil.rmtree, tempfile.tempdir) |
| |
| # We don't want to distract the user with logging output, so we catch |
| # logging output in a file. |
| logging_file_path = os.path.join(arguments.logdir, 'debug.log') |
| logfile = open(logging_file_path, 'w') |
| _configure_logging_to_file(logfile) |
| |
| report_log_path = os.path.join(arguments.logdir, 'report.log') |
| with open(report_log_path, 'w') as report_log_file: |
| report_log = _MultiFileWriter([report_log_file, sys.stdout]) |
| afe = frontend.AFE(server=arguments.web) |
| if arguments.dry_run: |
| report_log.write('Dry run - installation and most testing ' |
| 'will be skipped.\n') |
| current_build = _update_build(afe, report_log, arguments) |
| host_attr_dict = _get_host_attributes(arguments.host_info_list, afe) |
| install_pool = multiprocessing.Pool(len(arguments.hostnames)) |
| install_function = functools.partial(_install_dut, arguments, |
| host_attr_dict) |
| results_list = install_pool.map(install_function, arguments.hostnames) |
| _report_results(afe, report_log, arguments.hostnames, results_list) |
| |
| if arguments.upload: |
| try: |
| gspath = _get_upload_log_path(arguments) |
| sys.stderr.write('Logs will be uploaded to %s\n' % (gspath,)) |
| _upload_logs(arguments.logdir, gspath) |
| except Exception: |
| upload_failure_log_path = os.path.join(arguments.logdir, |
| 'gs_upload_failure.log') |
| with open(upload_failure_log_path, 'w') as file_: |
| traceback.print_exc(limit=None, file=file_) |
| sys.stderr.write('Failed to upload logs;' |
| ' failure details are stored in {}.\n' |
| .format(upload_failure_log_path)) |
| |
| |
| def _update_servo_type_attribute(host, host_to_update): |
| """Update servo_type attribute for the DUT. |
| |
| @param host A CrOSHost with a initialized servo property. |
| @param host_to_update A CrOSHost with AfeStore as its host_info_store. |
| |
| """ |
| info = host_to_update.host_info_store.get() |
| if 'servo_type' not in info.attributes: |
| logging.info("Collecting and adding servo_type attribute.") |
| info.attributes['servo_type'] = host.servo.get_servo_version() |
| host_to_update.host_info_store.commit(info) |