You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
10 KiB
278 lines
10 KiB
# Copyright 2018 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.
|
|
|
|
"""Functions for reading build information from GoogleStorage.
|
|
|
|
This module contains functions providing access to basic data about
|
|
Chrome OS builds:
|
|
* Functions for finding information about the Chrome OS versions
|
|
currently being served by Omaha for various boards/hardware models.
|
|
* Functions for finding information about the firmware delivered by
|
|
any given build of Chrome OS.
|
|
|
|
The necessary data is stored in JSON files in well-known locations in
|
|
GoogleStorage.
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
|
|
import common
|
|
from autotest_lib.client.common_lib import utils
|
|
from autotest_lib.server import frontend
|
|
|
|
|
|
# _OMAHA_STATUS - URI of a file in GoogleStorage with a JSON object
|
|
# summarizing all versions currently being served by Omaha.
|
|
#
|
|
# The principal data is in an array named 'omaha_data'. Each entry
|
|
# in the array contains information relevant to one image being
|
|
# served by Omaha, including the following information:
|
|
# * The board name of the product, as known to Omaha.
|
|
# * The channel associated with the image.
|
|
# * The Chrome and Chrome OS version strings for the image
|
|
# being served.
|
|
#
|
|
_OMAHA_STATUS = 'gs://chromeos-build-release-console/omaha_status.json'
|
|
|
|
|
|
# _BUILD_METADATA_PATTERN - Format string for the URI of a file in
|
|
# GoogleStorage with a JSON object that contains metadata about
|
|
# a given build. The metadata includes the version of firmware
|
|
# bundled with the build.
|
|
#
|
|
_BUILD_METADATA_PATTERN = 'gs://chromeos-image-archive/%s/metadata.json'
|
|
|
|
|
|
# _FIRMWARE_UPGRADE_DENYLIST - a set of boards that are exempt from
|
|
# automatic stable firmware version assignment. This denylist is
|
|
# here out of an abundance of caution, on the general principle of "if
|
|
# it ain't broke, don't fix it." Specifically, these are old, legacy
|
|
# boards and:
|
|
# * They're working fine with whatever firmware they have in the lab
|
|
# right now.
|
|
# * Because of their age, we can expect that they will never get any
|
|
# new firmware updates in future.
|
|
# * Servo support is spotty or missing, so there's no certainty that
|
|
# DUTs bricked by a firmware update can be repaired.
|
|
# * Because of their age, they are somewhere between hard and
|
|
# impossible to replace. In some cases, they are also already in
|
|
# short supply.
|
|
#
|
|
# N.B. HARDCODED BOARD NAMES ARE EVIL!!! This denylist uses hardcoded
|
|
# names because it's meant to define a list of legacies that will shrivel
|
|
# and die over time.
|
|
#
|
|
# DO NOT ADD TO THIS LIST. If there's a new use case that requires
|
|
# extending the denylist concept, you should find a maintainable
|
|
# solution that deletes this code.
|
|
#
|
|
# TODO(jrbarnette): When any board is past EOL, and removed from the
|
|
# lab, it can be removed from the denylist. When all the boards are
|
|
# past EOL, the denylist should be removed.
|
|
|
|
_FIRMWARE_UPGRADE_DENYLIST = set([
|
|
'butterfly',
|
|
'daisy',
|
|
'daisy_skate',
|
|
'daisy_spring',
|
|
'lumpy',
|
|
'parrot',
|
|
'parrot_ivb',
|
|
'peach_pi',
|
|
'peach_pit',
|
|
'stout',
|
|
'stumpy',
|
|
'x86-alex',
|
|
'x86-mario',
|
|
'x86-zgb',
|
|
])
|
|
|
|
|
|
def _read_gs_json_data(gs_uri):
|
|
"""Read and parse a JSON file from GoogleStorage.
|
|
|
|
This is a wrapper around `gsutil cat` for the specified URI.
|
|
The standard output of the command is parsed as JSON, and the
|
|
resulting object returned.
|
|
|
|
@param gs_uri URI of the JSON file in GoogleStorage.
|
|
@return A JSON object parsed from `gs_uri`.
|
|
"""
|
|
with open('/dev/null', 'w') as ignore_errors:
|
|
sp = subprocess.Popen(['gsutil', 'cat', gs_uri],
|
|
stdout=subprocess.PIPE,
|
|
stderr=ignore_errors)
|
|
try:
|
|
json_object = json.load(sp.stdout)
|
|
finally:
|
|
sp.stdout.close()
|
|
sp.wait()
|
|
return json_object
|
|
|
|
|
|
def _read_build_metadata(board, cros_version):
|
|
"""Read and parse the `metadata.json` file for a build.
|
|
|
|
Given the board and version string for a potential CrOS image,
|
|
find the URI of the build in GoogleStorage, and return a Python
|
|
object for the associated `metadata.json`.
|
|
|
|
@param board Board for the build to be read.
|
|
@param cros_version Build version string.
|
|
"""
|
|
image_path = frontend.format_cros_image_name(board, cros_version)
|
|
return _read_gs_json_data(_BUILD_METADATA_PATTERN % image_path)
|
|
|
|
|
|
def _get_by_key_path(dictdict, key_path):
|
|
"""Traverse a sequence of keys in a dict of dicts.
|
|
|
|
The `dictdict` parameter is a dict of nested dict values, and
|
|
`key_path` a list of keys.
|
|
|
|
A single-element key path returns `dictdict[key_path[0]]`, a
|
|
two-element path returns `dictdict[key_path[0]][key_path[1]]`, and
|
|
so forth. If any key in the path is not found, return `None`.
|
|
|
|
@param dictdict A dictionary of nested dictionaries.
|
|
@param key_path The sequence of keys to look up in `dictdict`.
|
|
@return The value found by successive dictionary lookups, or `None`.
|
|
"""
|
|
value = dictdict
|
|
for key in key_path:
|
|
value = value.get(key)
|
|
if value is None:
|
|
break
|
|
return value
|
|
|
|
|
|
def _get_model_firmware_versions(metadata_json, board):
|
|
"""Get the firmware version for all models in a unibuild board.
|
|
|
|
@param metadata_json The metadata_json dict parsed from the
|
|
metadata.json file generated by the build.
|
|
@param board The board name of the unibuild.
|
|
@return If the board has no models, return {board: None}.
|
|
Otherwise, return a dict mapping each model name to its
|
|
firmware version.
|
|
"""
|
|
model_firmware_versions = {}
|
|
key_path = ['board-metadata', board, 'models']
|
|
model_versions = _get_by_key_path(metadata_json, key_path)
|
|
|
|
if model_versions is not None:
|
|
for model, fw_versions in model_versions.iteritems():
|
|
fw_version = (fw_versions.get('main-readwrite-firmware-version') or
|
|
fw_versions.get('main-readonly-firmware-version'))
|
|
model_firmware_versions[model] = fw_version
|
|
else:
|
|
model_firmware_versions[board] = None
|
|
|
|
return model_firmware_versions
|
|
|
|
|
|
def get_omaha_version_map():
|
|
"""Convert omaha versions data to a versions mapping.
|
|
|
|
Returns a dictionary mapping board names to the currently preferred
|
|
version for the Beta channel as served by Omaha. The mappings are
|
|
provided by settings in the JSON object read from `_OMAHA_STATUS`.
|
|
|
|
The board names are the names as known to Omaha: If the board name
|
|
in the AFE contains '_', the corresponding Omaha name uses '-'
|
|
instead. The boards mapped may include boards not in the list of
|
|
managed boards in the lab.
|
|
|
|
@return A dictionary mapping Omaha boards to Beta versions.
|
|
"""
|
|
def _entry_valid(json_entry):
|
|
return json_entry['channel'] == 'beta'
|
|
|
|
def _get_omaha_data(json_entry):
|
|
board = json_entry['board']['public_codename']
|
|
milestone = json_entry['milestone']
|
|
build = json_entry['chrome_os_version']
|
|
version = 'R%d-%s' % (milestone, build)
|
|
return (board, version)
|
|
|
|
omaha_status = _read_gs_json_data(_OMAHA_STATUS)
|
|
return dict(_get_omaha_data(e) for e in omaha_status['omaha_data']
|
|
if _entry_valid(e))
|
|
|
|
|
|
def get_omaha_upgrade(omaha_map, board, version):
|
|
"""Get the later of a build in `omaha_map` or `version`.
|
|
|
|
Read the Omaha version for `board` from `omaha_map`, and compare it
|
|
to `version`. Return whichever version is more recent.
|
|
|
|
N.B. `board` is the name of a board as known to the AFE. Board
|
|
names as known to Omaha are different; see
|
|
`get_omaha_version_map()`, above. This function is responsible
|
|
for translating names as necessary.
|
|
|
|
@param omaha_map Mapping of Omaha board names to preferred builds.
|
|
@param board Name of the board to look up, as known to the AFE.
|
|
@param version Minimum version to be accepted.
|
|
|
|
@return Returns a Chrome OS version string in standard form
|
|
R##-####.#.#. Will return `None` if `version` is `None` and
|
|
no Omaha entry is found.
|
|
"""
|
|
omaha_version = omaha_map.get(board.replace('_', '-'))
|
|
if version is None:
|
|
return omaha_version
|
|
if omaha_version is not None:
|
|
if utils.compare_versions(version, omaha_version) < 0:
|
|
return omaha_version
|
|
return version
|
|
|
|
|
|
def get_firmware_versions(board, cros_version):
|
|
"""Get the firmware versions for a given board and CrOS version.
|
|
|
|
During the CrOS auto-update process, the system will check firmware
|
|
on the target device, and update that firmware if needed. This
|
|
function finds the version string of the firmware that would be
|
|
installed from a given CrOS build.
|
|
|
|
A build may have firmware for more than one hardware model, so the
|
|
returned value is a dictionary mapping models to firmware version
|
|
strings.
|
|
|
|
The returned firmware version value will be `None` if the build
|
|
isn't found in storage, if there is no firmware found for the build,
|
|
or if the board is denylisted from firmware updates in the test
|
|
lab.
|
|
|
|
@param board The board for the firmware version to be
|
|
determined.
|
|
@param cros_version The CrOS version bundling the firmware.
|
|
@return A dict mapping from board to firmware version string for
|
|
non-unibuild board, or a dict mapping from models to firmware
|
|
versions for a unibuild board (see return type of
|
|
_get_model_firmware_versions)
|
|
"""
|
|
if board in _FIRMWARE_UPGRADE_DENYLIST:
|
|
return {board: None}
|
|
try:
|
|
metadata_json = _read_build_metadata(board, cros_version)
|
|
unibuild = bool(_get_by_key_path(metadata_json, ['unibuild']))
|
|
if unibuild:
|
|
return _get_model_firmware_versions(metadata_json, board)
|
|
else:
|
|
key_path = ['board-metadata', board, 'main-firmware-version']
|
|
return {board: _get_by_key_path(metadata_json, key_path)}
|
|
except Exception as e:
|
|
# TODO(jrbarnette): If we get here, it likely means that the
|
|
# build for this board doesn't exist. That can happen if a
|
|
# board doesn't release on the Beta channel for at least 6 months.
|
|
#
|
|
# We can't allow this error to propagate up the call chain
|
|
# because that will kill assigning versions to all the other
|
|
# boards that are still OK, so for now we ignore it. Probably,
|
|
# we should do better.
|
|
return {board: None}
|