xref: /aosp_15_r20/external/autotest/site_utils/stable_images/build_data.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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