Home | History | Annotate | Download | only in stable_images
      1 #!/usr/bin/python
      2 # Copyright 2016 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 """
      7 Automatically update the afe_stable_versions table.
      8 
      9 This command updates the stable repair version for selected boards
     10 in the lab.  For each board, if the version that Omaha is serving
     11 on the Beta channel for the board is more recent than the current
     12 stable version in the AFE database, then the AFE is updated to use
     13 the version on Omaha.
     14 
     15 The upgrade process is applied to every "managed board" in the test
     16 lab.  Generally, a managed board is a board with both spare and
     17 critical scheduling pools.
     18 
     19 See `autotest_lib.site_utils.lab_inventory` for the full definition
     20 of "managed board".
     21 
     22 The command supports a `--dry-run` option that reports changes that
     23 would be made, without making the actual RPC calls to change the
     24 database.
     25 
     26 """
     27 
     28 import argparse
     29 import json
     30 import subprocess
     31 import sys
     32 
     33 import common
     34 from autotest_lib.client.common_lib import utils
     35 from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
     36 from autotest_lib.site_utils import lab_inventory
     37 
     38 
     39 # _OMAHA_STATUS - URI of a file in GoogleStorage with a JSON object
     40 # summarizing all versions currently being served by Omaha.
     41 #
     42 # The principle data is in an array named 'omaha_data'.  Each entry
     43 # in the array contains information relevant to one image being
     44 # served by Omaha, including the following information:
     45 #   * The board name of the product, as known to Omaha.
     46 #   * The channel associated with the image.
     47 #   * The Chrome and Chrome OS version strings for the image
     48 #     being served.
     49 #
     50 _OMAHA_STATUS = 'gs://chromeos-build-release-console/omaha_status.json'
     51 
     52 
     53 # _BUILD_METADATA_PATTERN - Format string for the URI of a file in
     54 # GoogleStorage with a JSON object that contains metadata about
     55 # a given build.  The metadata includes the version of firmware
     56 # bundled with the build.
     57 #
     58 _BUILD_METADATA_PATTERN = 'gs://chromeos-image-archive/%s/metadata.json'
     59 
     60 
     61 # _DEFAULT_BOARD - The distinguished board name used to identify a
     62 # stable version mapping that is used for any board without an explicit
     63 # mapping of its own.
     64 #
     65 # _DEFAULT_VERSION_TAG - A string used to signify that there is no
     66 # mapping for a board, in other words, the board is mapped to the
     67 # default version.
     68 #
     69 _DEFAULT_BOARD = 'DEFAULT'
     70 _DEFAULT_VERSION_TAG = '(default)'
     71 
     72 
     73 # _FIRMWARE_UPGRADE_BLACKLIST - a set of boards that are exempt from
     74 # automatic stable firmware version assignment.  This blacklist is
     75 # here out of an abundance of caution, on the general principle of "if
     76 # it ain't broke, don't fix it."  Specifically, these are old, legacy
     77 # boards and:
     78 #   * They're working fine with whatever firmware they have in the lab
     79 #     right now.  Moreover, because of their age, we can expect that
     80 #     they will never get any new firmware updates in future.
     81 #   * Servo support is spotty or missing, so there's no certainty
     82 #     that DUTs bricked by a firmware update can be repaired.
     83 #   * Because of their age, they are somewhere between hard and
     84 #     impossible to replace.  In some cases, they are also already
     85 #     in short supply.
     86 #
     87 # N.B.  HARDCODED BOARD NAMES ARE EVIL!!!  This blacklist uses hardcoded
     88 # names because it's meant to define a list of legacies that will shrivel
     89 # and die over time.
     90 #
     91 # DO NOT ADD TO THIS LIST.  If there's a new use case that requires
     92 # extending the blacklist concept, you should find a maintainable
     93 # solution that deletes this code.
     94 #
     95 # TODO(jrbarnette):  When any board is past EOL, and removed from the
     96 # lab, it can be removed from the blacklist.  When all the boards are
     97 # past EOL, the blacklist should be removed.
     98 
     99 _FIRMWARE_UPGRADE_BLACKLIST = set([
    100         'butterfly',
    101         'daisy',
    102         'daisy_skate',
    103         'daisy_spring',
    104         'lumpy',
    105         'parrot',
    106         'parrot_ivb',
    107         'peach_pi',
    108         'peach_pit',
    109         'stout',
    110         'stumpy',
    111         'x86-alex',
    112         'x86-mario',
    113         'x86-zgb',
    114     ])
    115 
    116 
    117 def _get_by_key_path(dictdict, key_path):
    118     """
    119     Traverse a sequence of keys in a dict of dicts.
    120 
    121     The `dictdict` parameter is a dict of nested dict values, and
    122     `key_path` a list of keys.
    123 
    124     A single-element key path returns `dictdict[key_path[0]]`, a
    125     two-element path returns `dictdict[key_path[0]][key_path[1]]`, and
    126     so forth.  If any key in the path is not found, return `None`.
    127 
    128     @param dictdict   A dictionary of nested dictionaries.
    129     @param key_path   The sequence of keys to look up in `dictdict`.
    130     @return The value found by successive dictionary lookups, or `None`.
    131     """
    132     value = dictdict
    133     for key in key_path:
    134         value = value.get(key)
    135         if value is None:
    136             break
    137     return value
    138 
    139 
    140 def _get_model_firmware_versions(metadata_json, board):
    141     """
    142     Get the firmware version for each model for a unibuild board.
    143 
    144     @param metadata_json    The metadata_json dict parsed from the metadata.json
    145                             file generated by the build.
    146     @param board            The board name of the unibuild.
    147     @return If no models found for a board, return {board: None}; elase, return
    148             a dict mapping from model name to its upgrade firmware version.
    149     """
    150     model_firmware_versions = {}
    151     key_path = ['board-metadata', board, 'models']
    152     model_versions = _get_by_key_path(metadata_json, key_path)
    153 
    154     if model_versions is not None:
    155         for model, fw_versions in model_versions.iteritems():
    156             fw_version = (fw_versions.get('main-readwrite-firmware-version') or
    157                           fw_versions.get('main-readonly-firmware-version'))
    158             model_firmware_versions[model] = fw_version
    159     else:
    160         model_firmware_versions[board] = None
    161 
    162     return model_firmware_versions
    163 
    164 
    165 def get_firmware_versions(version_map, board, cros_version):
    166     """
    167     Get the firmware versions for a given board and CrOS version.
    168 
    169     Typically, CrOS builds bundle firmware that is installed at update
    170     time. The returned firmware version value will be `None` if the build isn't
    171     found in storage, if there is no firmware found for the build, or if the
    172     board is blacklisted from firmware updates in the test lab.
    173 
    174     @param version_map    An AFE cros version map object; used to
    175                           locate the build in storage.
    176     @param board          The board for the firmware version to be
    177                           determined.
    178     @param cros_version   The CrOS version bundling the firmware.
    179     @return A dict mapping from board to firmware version string for
    180             non-unibuild board, or a dict mapping from models to firmware
    181             versions for a unibuild board (see return type of
    182             _get_model_firmware_versions)
    183     """
    184     if board in _FIRMWARE_UPGRADE_BLACKLIST:
    185         return {board: None}
    186     try:
    187         image_path = version_map.format_image_name(board, cros_version)
    188         uri = _BUILD_METADATA_PATTERN % image_path
    189         metadata_json = _read_gs_json_data(uri)
    190         unibuild = bool(_get_by_key_path(metadata_json, ['unibuild']))
    191         if unibuild:
    192             return _get_model_firmware_versions(metadata_json, board)
    193         else:
    194             key_path = ['board-metadata', board, 'main-firmware-version']
    195             fw_version = _get_by_key_path(metadata_json, key_path)
    196             return {board: fw_version}
    197     except Exception as e:
    198         # TODO(jrbarnette): If we get here, it likely means that
    199         # the repair build for our board doesn't exist.  That can
    200         # happen if a board doesn't release on the Beta channel for
    201         # at least 6 months.
    202         #
    203         # We can't allow this error to propogate up the call chain
    204         # because that will kill assigning versions to all the other
    205         # boards that are still OK, so for now we ignore it.  We
    206         # really should do better.
    207         print ('Failed to get firmware version for board %s: %s.' % (board, e))
    208         return {board: None}
    209 
    210 
    211 class _VersionUpdater(object):
    212     """
    213     Class to report and apply version changes.
    214 
    215     This class is responsible for the low-level logic of applying
    216     version upgrades and reporting them as command output.
    217 
    218     This class exists to solve two problems:
    219      1. To distinguish "normal" vs. "dry-run" modes.  Each mode has a
    220         subclass; methods that perform actual AFE updates are
    221         implemented for the normal mode subclass only.
    222      2. To provide hooks for unit tests.  The unit tests override both
    223         the reporting and modification behaviors, in order to test the
    224         higher level logic that decides what changes are needed.
    225 
    226     Methods meant merely to report changes to command output have names
    227     starting with "report" or "_report".  Methods that are meant to
    228     change the AFE in normal mode have names starting with "_do"
    229     """
    230 
    231     def __init__(self, afe):
    232         image_types = [afe.CROS_IMAGE_TYPE, afe.FIRMWARE_IMAGE_TYPE]
    233         self._version_maps = {
    234             image_type: afe.get_stable_version_map(image_type)
    235                 for image_type in image_types
    236         }
    237         self._cros_map = self._version_maps[afe.CROS_IMAGE_TYPE]
    238         self._selected_map = None
    239 
    240     def select_version_map(self, image_type):
    241         """
    242         Select an AFE version map object based on `image_type`.
    243 
    244         This creates and remembers an AFE version mapper object to be
    245         used for making changes in normal mode.
    246 
    247         @param image_type   Image type parameter for the version mapper
    248                             object.
    249         """
    250         self._selected_map = self._version_maps[image_type]
    251         return self._selected_map
    252 
    253     def announce(self):
    254         """Announce the start of processing to the user."""
    255         pass
    256 
    257     def report(self, message):
    258         """
    259         Report a pre-formatted message for the user.
    260 
    261         The message is printed to stdout, followed by a newline.
    262 
    263         @param message The message to be provided to the user.
    264         """
    265         print message
    266 
    267     def report_default_changed(self, old_default, new_default):
    268         """
    269         Report that the default version mapping is changing.
    270 
    271         This merely reports a text description of the pending change
    272         without executing it.
    273 
    274         @param old_default  The original default version.
    275         @param new_default  The new default version to be applied.
    276         """
    277         self.report('Default %s -> %s' % (old_default, new_default))
    278 
    279     def _report_board_changed(self, board, old_version, new_version):
    280         """
    281         Report a change in one board's assigned version mapping.
    282 
    283         This merely reports a text description of the pending change
    284         without executing it.
    285 
    286         @param board        The board with the changing version.
    287         @param old_version  The original version mapped to the board.
    288         @param new_version  The new version to be applied to the board.
    289         """
    290         template = '    %-22s %s -> %s'
    291         self.report(template % (board, old_version, new_version))
    292 
    293     def report_board_unchanged(self, board, old_version):
    294         """
    295         Report that a board's version mapping is unchanged.
    296 
    297         This reports that a board has a non-default mapping that will be
    298         unchanged.
    299 
    300         @param board        The board that is not changing.
    301         @param old_version  The board's version mapping.
    302         """
    303         self._report_board_changed(board, '(no change)', old_version)
    304 
    305     def _do_set_mapping(self, board, new_version):
    306         """
    307         Change one board's assigned version mapping.
    308 
    309         @param board        The board with the changing version.
    310         @param new_version  The new version to be applied to the board.
    311         """
    312         pass
    313 
    314     def _do_delete_mapping(self, board):
    315         """
    316         Delete one board's assigned version mapping.
    317 
    318         @param board        The board with the version to be deleted.
    319         """
    320         pass
    321 
    322     def set_mapping(self, board, old_version, new_version):
    323         """
    324         Change and report a board version mapping.
    325 
    326         @param board        The board with the changing version.
    327         @param old_version  The original version mapped to the board.
    328         @param new_version  The new version to be applied to the board.
    329         """
    330         self._report_board_changed(board, old_version, new_version)
    331         self._do_set_mapping(board, new_version)
    332 
    333     def upgrade_default(self, new_default):
    334         """
    335         Apply a default version change.
    336 
    337         @param new_default  The new default version to be applied.
    338         """
    339         self._do_set_mapping(_DEFAULT_BOARD, new_default)
    340 
    341     def delete_mapping(self, board, old_version):
    342         """
    343         Delete a board version mapping, and report the change.
    344 
    345         @param board        The board with the version to be deleted.
    346         @param old_version  The board's verson prior to deletion.
    347         """
    348         assert board != _DEFAULT_BOARD
    349         self._report_board_changed(board,
    350                                    old_version,
    351                                    _DEFAULT_VERSION_TAG)
    352         self._do_delete_mapping(board)
    353 
    354 
    355 class _DryRunUpdater(_VersionUpdater):
    356     """Code for handling --dry-run execution."""
    357 
    358     def announce(self):
    359         self.report('Dry run:  no changes will be made.')
    360 
    361 
    362 class _NormalModeUpdater(_VersionUpdater):
    363     """Code for handling normal execution."""
    364 
    365     def _do_set_mapping(self, board, new_version):
    366         self._selected_map.set_version(board, new_version)
    367 
    368     def _do_delete_mapping(self, board):
    369         self._selected_map.delete_version(board)
    370 
    371 
    372 def _read_gs_json_data(gs_uri):
    373     """
    374     Read and parse a JSON file from googlestorage.
    375 
    376     This is a wrapper around `gsutil cat` for the specified URI.
    377     The standard output of the command is parsed as JSON, and the
    378     resulting object returned.
    379 
    380     @return A JSON object parsed from `gs_uri`.
    381     """
    382     with open('/dev/null', 'w') as ignore_errors:
    383         sp = subprocess.Popen(['gsutil', 'cat', gs_uri],
    384                               stdout=subprocess.PIPE,
    385                               stderr=ignore_errors)
    386         try:
    387             json_object = json.load(sp.stdout)
    388         finally:
    389             sp.stdout.close()
    390             sp.wait()
    391     return json_object
    392 
    393 
    394 def _make_omaha_versions(omaha_status):
    395     """
    396     Convert parsed omaha versions data to a versions mapping.
    397 
    398     Returns a dictionary mapping board names to the currently preferred
    399     version for the Beta channel as served by Omaha.  The mappings are
    400     provided by settings in the JSON object `omaha_status`.
    401 
    402     The board names are the names as known to Omaha:  If the board name
    403     in the AFE contains '_', the corresponding Omaha name uses '-'
    404     instead.  The boards mapped may include boards not in the list of
    405     managed boards in the lab.
    406 
    407     @return A dictionary mapping Omaha boards to Beta versions.
    408     """
    409     def _entry_valid(json_entry):
    410         return json_entry['channel'] == 'beta'
    411 
    412     def _get_omaha_data(json_entry):
    413         board = json_entry['board']['public_codename']
    414         milestone = json_entry['milestone']
    415         build = json_entry['chrome_os_version']
    416         version = 'R%d-%s' % (milestone, build)
    417         return (board, version)
    418 
    419     return dict(_get_omaha_data(e) for e in omaha_status['omaha_data']
    420                     if _entry_valid(e))
    421 
    422 
    423 def _get_upgrade_versions(cros_versions, omaha_versions, boards):
    424     """
    425     Get the new stable versions to which we should update.
    426 
    427     The new versions are returned as a tuple of a dictionary mapping
    428     board names to versions, plus a new default board setting.  The
    429     new default is determined as the most commonly used version
    430     across the given boards.
    431 
    432     The new dictionary will have a mapping for every board in `boards`.
    433     That mapping will be taken from `cros_versions`, unless the board has
    434     a mapping in `omaha_versions` _and_ the omaha version is more recent
    435     than the AFE version.
    436 
    437     @param cros_versions    The current board->version mappings in the
    438                             AFE.
    439     @param omaha_versions   The current board->version mappings from
    440                             Omaha for the Beta channel.
    441     @param boards           Set of boards to be upgraded.
    442     @return Tuple of (mapping, default) where mapping is a dictionary
    443             mapping boards to versions, and default is a version string.
    444     """
    445     upgrade_versions = {}
    446     version_counts = {}
    447     afe_default = cros_versions[_DEFAULT_BOARD]
    448     for board in boards:
    449         version = cros_versions.get(board, afe_default)
    450         omaha_version = omaha_versions.get(board.replace('_', '-'))
    451         if (omaha_version is not None and
    452                 utils.compare_versions(version, omaha_version) < 0):
    453             version = omaha_version
    454         upgrade_versions[board] = version
    455         version_counts.setdefault(version, 0)
    456         version_counts[version] += 1
    457     return (upgrade_versions,
    458             max(version_counts.items(), key=lambda x: x[1])[0])
    459 
    460 
    461 def _get_firmware_upgrades(cros_version_map, cros_versions):
    462     """
    463     Get the new firmware versions to which we should update.
    464 
    465     @param cros_version_map An instance of frontend._CrosVersionMap.
    466     @param cros_versions    Current board->cros version mappings in the
    467                             AFE.
    468     @return A dictionary mapping boards/models to firmware upgrade versions.
    469             If the build is unibuild, the key is a model name; else, the key
    470             is a board name.
    471     """
    472     firmware_upgrades = {}
    473     for board, version in cros_versions.iteritems():
    474         firmware_upgrades.update(get_firmware_versions(
    475             cros_version_map, board, version))
    476 
    477     return firmware_upgrades
    478 
    479 
    480 def _apply_cros_upgrades(updater, old_versions, new_versions,
    481                          new_default):
    482     """
    483     Change CrOS stable version mappings in the AFE.
    484 
    485     The input `old_versions` dictionary represents the content of the
    486     `afe_stable_versions` database table; it contains mappings for a
    487     default version, plus exceptions for boards with non-default
    488     mappings.
    489 
    490     The `new_versions` dictionary contains a mapping for every board,
    491     including boards that will be mapped to the new default version.
    492 
    493     This function applies the AFE changes necessary to produce the new
    494     AFE mappings indicated by `new_versions` and `new_default`.  The
    495     changes are ordered so that at any moment, every board is mapped
    496     either according to the old or the new mapping.
    497 
    498     @param updater        Instance of _VersionUpdater responsible for
    499                           making the actual database changes.
    500     @param old_versions   The current board->version mappings in the
    501                           AFE.
    502     @param new_versions   New board->version mappings obtained by
    503                           applying Beta channel upgrades from Omaha.
    504     @param new_default    The new default build for the AFE.
    505     """
    506     old_default = old_versions[_DEFAULT_BOARD]
    507     if old_default != new_default:
    508         updater.report_default_changed(old_default, new_default)
    509     updater.report('Applying stable version changes:')
    510     default_count = 0
    511     for board, new_build in new_versions.items():
    512         if new_build == new_default:
    513             default_count += 1
    514         elif board in old_versions and new_build == old_versions[board]:
    515             updater.report_board_unchanged(board, new_build)
    516         else:
    517             old_build = old_versions.get(board)
    518             if old_build is None:
    519                 old_build = _DEFAULT_VERSION_TAG
    520             updater.set_mapping(board, old_build, new_build)
    521     if old_default != new_default:
    522         updater.upgrade_default(new_default)
    523     for board, new_build in new_versions.items():
    524         if new_build == new_default and board in old_versions:
    525             updater.delete_mapping(board, old_versions[board])
    526     updater.report('%d boards now use the default mapping' %
    527                    default_count)
    528 
    529 
    530 def _apply_firmware_upgrades(updater, old_versions, new_versions):
    531     """
    532     Change firmware version mappings in the AFE.
    533 
    534     The input `old_versions` dictionary represents the content of the
    535     firmware mappings in the `afe_stable_versions` database table.
    536     There is no default version; missing boards simply have no current
    537     version.
    538 
    539     This function applies the AFE changes necessary to produce the new
    540     AFE mappings indicated by `new_versions`.
    541 
    542     TODO(jrbarnette) This function ought to remove any mapping not found
    543     in `new_versions`.  However, in theory, that's only needed to
    544     account for boards that are removed from the lab, and that hasn't
    545     happened yet.
    546 
    547     @param updater        Instance of _VersionUpdater responsible for
    548                           making the actual database changes.
    549     @param old_versions   The current board->version mappings in the
    550                           AFE.
    551     @param new_versions   New board->version mappings obtained by
    552                           applying Beta channel upgrades from Omaha.
    553     """
    554     unchanged = 0
    555     no_version = 0
    556     for board, new_firmware in new_versions.items():
    557         if new_firmware is None:
    558             no_version += 1
    559         elif board not in old_versions:
    560             updater.set_mapping(board, '(nothing)', new_firmware)
    561         else:
    562             old_firmware = old_versions[board]
    563             if new_firmware != old_firmware:
    564                 updater.set_mapping(board, old_firmware, new_firmware)
    565             else:
    566                 unchanged += 1
    567     updater.report('%d boards have no firmware mapping' % no_version)
    568     updater.report('%d boards are unchanged' % unchanged)
    569 
    570 
    571 def _parse_command_line(argv):
    572     """
    573     Parse the command line arguments.
    574 
    575     Create an argument parser for this command's syntax, parse the
    576     command line, and return the result of the ArgumentParser
    577     parse_args() method.
    578 
    579     @param argv Standard command line argument vector; argv[0] is
    580                 assumed to be the command name.
    581     @return Result returned by ArgumentParser.parse_args().
    582 
    583     """
    584     parser = argparse.ArgumentParser(
    585             prog=argv[0],
    586             description='Update the stable repair version for all '
    587                         'boards')
    588     parser.add_argument('-n', '--dry-run', dest='updater_mode',
    589                         action='store_const', const=_DryRunUpdater,
    590                         help='print changes without executing them')
    591     parser.add_argument('extra_boards', nargs='*', metavar='BOARD',
    592                         help='Names of additional boards to be updated.')
    593     arguments = parser.parse_args(argv[1:])
    594     if not arguments.updater_mode:
    595         arguments.updater_mode = _NormalModeUpdater
    596     return arguments
    597 
    598 
    599 def main(argv):
    600     """
    601     Standard main routine.
    602 
    603     @param argv  Command line arguments including `sys.argv[0]`.
    604     """
    605     arguments = _parse_command_line(argv)
    606     afe = frontend_wrappers.RetryingAFE(server=None)
    607     updater = arguments.updater_mode(afe)
    608     updater.announce()
    609     boards = (set(arguments.extra_boards) |
    610               lab_inventory.get_managed_boards(afe))
    611 
    612     cros_version_map = updater.select_version_map(afe.CROS_IMAGE_TYPE)
    613     cros_versions = cros_version_map.get_all_versions()
    614     omaha_versions = _make_omaha_versions(
    615             _read_gs_json_data(_OMAHA_STATUS))
    616     upgrade_versions, new_default = (
    617         _get_upgrade_versions(cros_versions, omaha_versions, boards))
    618     _apply_cros_upgrades(updater, cros_versions,
    619                          upgrade_versions, new_default)
    620 
    621     updater.report('\nApplying firmware updates:')
    622     firmware_version_map = updater.select_version_map(afe.FIRMWARE_IMAGE_TYPE)
    623     fw_versions = firmware_version_map.get_all_versions()
    624     firmware_upgrades = _get_firmware_upgrades(
    625             cros_version_map, upgrade_versions)
    626     _apply_firmware_upgrades(updater, fw_versions, firmware_upgrades)
    627 
    628 
    629 if __name__ == '__main__':
    630     main(sys.argv)
    631