1# Copyright 2018 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Functions for reading build information from GoogleStorage. 6 7This module contains functions providing access to basic data about 8ChromeOS builds: 9 * Functions for finding information about the ChromeOS versions 10 currently being served by Omaha for various boards/hardware models. 11 * Functions for finding information about the firmware delivered by 12 any given build of ChromeOS. 13 14The necessary data is stored in JSON files in well-known locations in 15GoogleStorage. 16""" 17 18import json 19import six 20import subprocess 21 22import common 23from autotest_lib.client.common_lib import utils 24from autotest_lib.server import frontend 25 26 27# _OMAHA_STATUS - URI of a file in GoogleStorage with a JSON object 28# summarizing all versions currently being served by Omaha. 29# 30# The principal data is in an array named 'omaha_data'. Each entry 31# in the array contains information relevant to one image being 32# served by Omaha, including the following information: 33# * The board name of the product, as known to Omaha. 34# * The channel associated with the image. 35# * The Chrome and ChromeOS version strings for the image 36# being served. 37# 38_OMAHA_STATUS = 'gs://chromeos-build-release-console/omaha_status.json' 39 40 41# _BUILD_METADATA_PATTERN - Format string for the URI of a file in 42# GoogleStorage with a JSON object that contains metadata about 43# a given build. The metadata includes the version of firmware 44# bundled with the build. 45# 46_BUILD_METADATA_PATTERN = 'gs://chromeos-image-archive/%s/metadata.json' 47 48 49# _FIRMWARE_UPGRADE_DENYLIST - a set of boards that are exempt from 50# automatic stable firmware version assignment. This denylist is 51# here out of an abundance of caution, on the general principle of "if 52# it ain't broke, don't fix it." Specifically, these are old, legacy 53# boards and: 54# * They're working fine with whatever firmware they have in the lab 55# right now. 56# * Because of their age, we can expect that they will never get any 57# new firmware updates in future. 58# * Servo support is spotty or missing, so there's no certainty that 59# DUTs bricked by a firmware update can be repaired. 60# * Because of their age, they are somewhere between hard and 61# impossible to replace. In some cases, they are also already in 62# short supply. 63# 64# N.B. HARDCODED BOARD NAMES ARE EVIL!!! This denylist uses hardcoded 65# names because it's meant to define a list of legacies that will shrivel 66# and die over time. 67# 68# DO NOT ADD TO THIS LIST. If there's a new use case that requires 69# extending the denylist concept, you should find a maintainable 70# solution that deletes this code. 71# 72# TODO(jrbarnette): When any board is past EOL, and removed from the 73# lab, it can be removed from the denylist. When all the boards are 74# past EOL, the denylist should be removed. 75 76_FIRMWARE_UPGRADE_DENYLIST = set([ 77 'butterfly', 78 'daisy', 79 'daisy_skate', 80 'daisy_spring', 81 'lumpy', 82 'parrot', 83 'parrot_ivb', 84 'peach_pi', 85 'peach_pit', 86 'stout', 87 'stumpy', 88 'x86-alex', 89 'x86-mario', 90 'x86-zgb', 91]) 92 93 94def _read_gs_json_data(gs_uri): 95 """Read and parse a JSON file from GoogleStorage. 96 97 This is a wrapper around `gsutil cat` for the specified URI. 98 The standard output of the command is parsed as JSON, and the 99 resulting object returned. 100 101 @param gs_uri URI of the JSON file in GoogleStorage. 102 @return A JSON object parsed from `gs_uri`. 103 """ 104 with open('/dev/null', 'w') as ignore_errors: 105 sp = subprocess.Popen(['gsutil', 'cat', gs_uri], 106 stdout=subprocess.PIPE, 107 stderr=ignore_errors) 108 try: 109 json_object = json.load(sp.stdout) 110 finally: 111 sp.stdout.close() 112 sp.wait() 113 return json_object 114 115 116def _read_build_metadata(board, cros_version): 117 """Read and parse the `metadata.json` file for a build. 118 119 Given the board and version string for a potential CrOS image, 120 find the URI of the build in GoogleStorage, and return a Python 121 object for the associated `metadata.json`. 122 123 @param board Board for the build to be read. 124 @param cros_version Build version string. 125 """ 126 image_path = frontend.format_cros_image_name(board, cros_version) 127 return _read_gs_json_data(_BUILD_METADATA_PATTERN % image_path) 128 129 130def _get_by_key_path(dictdict, key_path): 131 """Traverse a sequence of keys in a dict of dicts. 132 133 The `dictdict` parameter is a dict of nested dict values, and 134 `key_path` a list of keys. 135 136 A single-element key path returns `dictdict[key_path[0]]`, a 137 two-element path returns `dictdict[key_path[0]][key_path[1]]`, and 138 so forth. If any key in the path is not found, return `None`. 139 140 @param dictdict A dictionary of nested dictionaries. 141 @param key_path The sequence of keys to look up in `dictdict`. 142 @return The value found by successive dictionary lookups, or `None`. 143 """ 144 value = dictdict 145 for key in key_path: 146 value = value.get(key) 147 if value is None: 148 break 149 return value 150 151 152def _get_model_firmware_versions(metadata_json, board): 153 """Get the firmware version for all models in a unibuild board. 154 155 @param metadata_json The metadata_json dict parsed from the 156 metadata.json file generated by the build. 157 @param board The board name of the unibuild. 158 @return If the board has no models, return {board: None}. 159 Otherwise, return a dict mapping each model name to its 160 firmware version. 161 """ 162 model_firmware_versions = {} 163 key_path = ['board-metadata', board, 'models'] 164 model_versions = _get_by_key_path(metadata_json, key_path) 165 166 if model_versions is not None: 167 for model, fw_versions in six.iteritems(model_versions): 168 fw_version = (fw_versions.get('main-readwrite-firmware-version') or 169 fw_versions.get('main-readonly-firmware-version')) 170 model_firmware_versions[model] = fw_version 171 else: 172 model_firmware_versions[board] = None 173 174 return model_firmware_versions 175 176 177def get_omaha_version_map(): 178 """Convert omaha versions data to a versions mapping. 179 180 Returns a dictionary mapping board names to the currently preferred 181 version for the Beta channel as served by Omaha. The mappings are 182 provided by settings in the JSON object read from `_OMAHA_STATUS`. 183 184 The board names are the names as known to Omaha: If the board name 185 in the AFE contains '_', the corresponding Omaha name uses '-' 186 instead. The boards mapped may include boards not in the list of 187 managed boards in the lab. 188 189 @return A dictionary mapping Omaha boards to Beta versions. 190 """ 191 def _entry_valid(json_entry): 192 return json_entry['channel'] == 'beta' 193 194 def _get_omaha_data(json_entry): 195 board = json_entry['board']['public_codename'] 196 milestone = json_entry['milestone'] 197 build = json_entry['chrome_os_version'] 198 version = 'R%d-%s' % (milestone, build) 199 return (board, version) 200 201 omaha_status = _read_gs_json_data(_OMAHA_STATUS) 202 return dict(_get_omaha_data(e) for e in omaha_status['omaha_data'] 203 if _entry_valid(e)) 204 205 206def get_omaha_upgrade(omaha_map, board, version): 207 """Get the later of a build in `omaha_map` or `version`. 208 209 Read the Omaha version for `board` from `omaha_map`, and compare it 210 to `version`. Return whichever version is more recent. 211 212 N.B. `board` is the name of a board as known to the AFE. Board 213 names as known to Omaha are different; see 214 `get_omaha_version_map()`, above. This function is responsible 215 for translating names as necessary. 216 217 @param omaha_map Mapping of Omaha board names to preferred builds. 218 @param board Name of the board to look up, as known to the AFE. 219 @param version Minimum version to be accepted. 220 221 @return Returns a ChromeOS version string in standard form 222 R##-####.#.#. Will return `None` if `version` is `None` and 223 no Omaha entry is found. 224 """ 225 omaha_version = omaha_map.get(board.replace('_', '-')) 226 if version is None: 227 return omaha_version 228 if omaha_version is not None: 229 if utils.compare_versions(version, omaha_version) < 0: 230 return omaha_version 231 return version 232 233 234def get_firmware_versions(board, cros_version): 235 """Get the firmware versions for a given board and CrOS version. 236 237 During the CrOS auto-update process, the system will check firmware 238 on the target device, and update that firmware if needed. This 239 function finds the version string of the firmware that would be 240 installed from a given CrOS build. 241 242 A build may have firmware for more than one hardware model, so the 243 returned value is a dictionary mapping models to firmware version 244 strings. 245 246 The returned firmware version value will be `None` if the build 247 isn't found in storage, if there is no firmware found for the build, 248 or if the board is denylisted from firmware updates in the test 249 lab. 250 251 @param board The board for the firmware version to be 252 determined. 253 @param cros_version The CrOS version bundling the firmware. 254 @return A dict mapping from board to firmware version string for 255 non-unibuild board, or a dict mapping from models to firmware 256 versions for a unibuild board (see return type of 257 _get_model_firmware_versions) 258 """ 259 if board in _FIRMWARE_UPGRADE_DENYLIST: 260 return {board: None} 261 try: 262 metadata_json = _read_build_metadata(board, cros_version) 263 unibuild = bool(_get_by_key_path(metadata_json, ['unibuild'])) 264 if unibuild: 265 return _get_model_firmware_versions(metadata_json, board) 266 else: 267 key_path = ['board-metadata', board, 'main-firmware-version'] 268 return {board: _get_by_key_path(metadata_json, key_path)} 269 except Exception as e: 270 # TODO(jrbarnette): If we get here, it likely means that the 271 # build for this board doesn't exist. That can happen if a 272 # board doesn't release on the Beta channel for at least 6 months. 273 # 274 # We can't allow this error to propagate up the call chain 275 # because that will kill assigning versions to all the other 276 # boards that are still OK, so for now we ignore it. Probably, 277 # we should do better. 278 return {board: None} 279