xref: /aosp_15_r20/external/autotest/site_utils/stable_images/stable_version.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1#!/usr/bin/python3
2# Copyright 2018 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"""Command for viewing and changing software version assignments.
7
8Usage:
9    stable_version [ -w SERVER ] [ -n ] [ -t TYPE ]
10    stable_version [ -w SERVER ] [ -n ] [ -t TYPE ] BOARD/MODEL
11    stable_version [ -w SERVER ] [ -n ] -t TYPE -d BOARD/MODEL
12    stable_version [ -w SERVER ] [ -n ] -t TYPE BOARD/MODEL VERSION
13
14Available options:
15-w SERVER | --web SERVER
16    Used to specify an alternative server for the AFE RPC interface.
17
18-n | --dry-run
19    When specified, the command reports what would be done, but makes no
20    changes.
21
22-t TYPE | --type TYPE
23    Specifies the type of version mapping to use.  This option is
24    required for operations to change or delete mappings.  When listing
25    mappings, the option may be omitted, in which case all mapping types
26    are listed.
27
28-d | --delete
29    Delete the mapping for the given board or model argument.
30
31Command arguments:
32BOARD/MODEL
33    When specified, indicates the board or model to use as a key when
34    listing, changing, or deleting mappings.
35
36VERSION
37    When specified, indicates that the version name should be assigned
38    to the given board or model.
39
40With no arguments, the command will list all available mappings of all
41types.  The `--type` option will restrict the listing to only mappings of
42the given type.
43
44With only a board or model specified (and without the `--delete`
45option), will list all mappings for the given board or model.  The
46`--type` option will restrict the listing to only mappings of the given
47type.
48
49With the `--delete` option, will delete the mapping for the given board
50or model.  The `--type` option is required in this case.
51
52With both a board or model and a version specified, will assign the
53version to the given board or model.  The `--type` option is required in
54this case.
55"""
56
57from __future__ import print_function
58
59import argparse
60import os
61import sys
62
63import common
64from autotest_lib.server import frontend
65from autotest_lib.site_utils.stable_images import build_data
66
67
68class _CommandError(Exception):
69    """Exception to indicate an error in command processing."""
70
71
72class _VersionMapHandler(object):
73    """An internal class to wrap data for version map operations.
74
75    This is a simple class to gather in one place data associated
76    with higher-level command line operations.
77
78    @property _description  A string description used to describe the
79                            image type when printing command output.
80    @property _dry_run      Value of the `--dry-run` command line
81                            operation.
82    @property _afe          AFE RPC object.
83    @property _version_map  AFE version map object for the image type.
84    """
85
86    # Subclasses are required to redefine both of these to a string with
87    # an appropriate value.
88    TYPE = None
89    DESCRIPTION = None
90
91    def __init__(self, afe, dry_run):
92        self._afe = afe
93        self._dry_run = dry_run
94        self._version_map = afe.get_stable_version_map(self.TYPE)
95
96    @property
97    def _description(self):
98        return self.DESCRIPTION
99
100    def _format_key_data(self, key):
101        return '%-10s %-12s' % (self._description, key)
102
103    def _format_operation(self, opname, key):
104        return '%-9s %s' % (opname, self._format_key_data(key))
105
106    def get_mapping(self, key):
107        """Return the mapping for `key`.
108
109        @param key  Board or model key to use for look up.
110        """
111        return self._version_map.get_version(key)
112
113    def print_all_mappings(self):
114        """Print all mappings in `self._version_map`"""
115        print('%s version mappings:' % self._description)
116        mappings = self._version_map.get_all_versions()
117        if not mappings:
118            return
119        key_list = mappings.keys()
120        key_width = max(12, len(max(key_list, key=len)))
121        format = '%%-%ds  %%s' % key_width
122        for k in sorted(key_list):
123            print(format % (k, mappings[k]))
124
125    def print_mapping(self, key):
126        """Print the mapping for `key`.
127
128        Prints a single mapping for the board/model specified by
129        `key`.  Print nothing if no mapping exists.
130
131        @param key  Board or model key to use for look up.
132        """
133        version = self.get_mapping(key)
134        if version is not None:
135            print('%s  %s' % (self._format_key_data(key), version))
136
137    def set_mapping(self, key, new_version):
138        """Change the mapping for `key`, and report the action.
139
140        The mapping for the board or model specifed by `key` is set
141        to `new_version`.  The setting is reported to the user as
142        added, changed, or unchanged based on the current mapping in
143        the AFE.
144
145        This operation honors `self._dry_run`.
146
147        @param key          Board or model key for assignment.
148        @param new_version  Version to be assigned to `key`.
149        """
150        old_version = self.get_mapping(key)
151        if old_version is None:
152            print('%s -> %s' % (
153                self._format_operation('Adding', key), new_version))
154        elif old_version != new_version:
155            print('%s -> %s to %s' % (
156                self._format_operation('Updating', key),
157                old_version, new_version))
158        else:
159            print('%s -> %s' % (
160                self._format_operation('Unchanged', key), old_version))
161        if not self._dry_run and old_version != new_version:
162            self._version_map.set_version(key, new_version)
163
164    def delete_mapping(self, key):
165        """Delete the mapping for `key`, and report the action.
166
167        The mapping for the board or model specifed by `key` is removed
168        from `self._version_map`.  The change is reported to the user.
169
170        Requests to delete non-existent keys are ignored.
171
172        This operation honors `self._dry_run`.
173
174        @param key  Board or model key to be deleted.
175        """
176        version = self.get_mapping(key)
177        if version is not None:
178            print('%s -> %s' % (
179                self._format_operation('Delete', key), version))
180            if not self._dry_run:
181                self._version_map.delete_version(key)
182        else:
183            print(self._format_operation('Unmapped', key))
184
185
186class _FirmwareVersionMapHandler(_VersionMapHandler):
187    TYPE = frontend.AFE.FIRMWARE_IMAGE_TYPE
188    DESCRIPTION = 'Firmware'
189
190
191class _CrOSVersionMapHandler(_VersionMapHandler):
192    TYPE = frontend.AFE.CROS_IMAGE_TYPE
193    DESCRIPTION = 'ChromeOS'
194
195    def set_mapping(self, board, version):
196        """Assign the ChromeOS mapping for the given board.
197
198        This function assigns the given ChromeOS version to the given
199        board.  Additionally, for any model with firmware bundled in the
200        assigned build, that model will be assigned the firmware version
201        found for it in the build.
202
203        @param board    ChromeOS board to be assigned a new version.
204        @param version  New ChromeOS version to be assigned to the
205                        board.
206        """
207        new_version = build_data.get_omaha_upgrade(
208            build_data.get_omaha_version_map(), board, version)
209        if new_version != version:
210            print('Force %s version from Omaha:  %-12s -> %s' % (
211                self._description, board, new_version))
212        super(_CrOSVersionMapHandler, self).set_mapping(board, new_version)
213        fw_versions = build_data.get_firmware_versions(board, new_version)
214        fw_handler = _FirmwareVersionMapHandler(self._afe, self._dry_run)
215        for model, fw_version in fw_versions.iteritems():
216            if fw_version is not None:
217                fw_handler.set_mapping(model, fw_version)
218
219    def delete_mapping(self, board):
220        """Delete the ChromeOS mapping for the given board.
221
222        This function handles deletes the ChromeOS version mapping for the
223        given board.  Additionally, any R/W firmware mapping that existed
224        because of the OS mapping will be deleted as well.
225
226        @param board    ChromeOS board to be deleted from the mapping.
227        """
228        version = self.get_mapping(board)
229        super(_CrOSVersionMapHandler, self).delete_mapping(board)
230        fw_versions = build_data.get_firmware_versions(board, version)
231        fw_handler = _FirmwareVersionMapHandler(self._afe, self._dry_run)
232        for model in fw_versions.iterkeys():
233            fw_handler.delete_mapping(model)
234
235
236class _FAFTVersionMapHandler(_VersionMapHandler):
237    TYPE = frontend.AFE.FAFT_IMAGE_TYPE
238    DESCRIPTION = 'FAFT'
239
240
241_IMAGE_TYPE_CLASSES = [
242    _CrOSVersionMapHandler,
243    _FirmwareVersionMapHandler,
244    _FAFTVersionMapHandler,
245]
246_ALL_IMAGE_TYPES = [cls.TYPE for cls in _IMAGE_TYPE_CLASSES]
247_IMAGE_TYPE_HANDLERS = {cls.TYPE: cls for cls in _IMAGE_TYPE_CLASSES}
248
249
250def _create_version_map_handler(image_type, afe, dry_run):
251    return _IMAGE_TYPE_HANDLERS[image_type](afe, dry_run)
252
253
254def _requested_mapping_handlers(afe, image_type):
255    """Iterate through the image types for a listing operation.
256
257    When listing all mappings, or when listing by board, the listing can
258    be either for all available image types, or just for a single type
259    requested on the command line.
260
261    This function takes the value of the `-t` option, and yields a
262    `_VersionMapHandler` object for either the single requested type, or
263    for all of the types.
264
265    @param afe          AFE RPC interface object; created from SERVER.
266    @param image_type   Argument to the `-t` option.  A non-empty string
267                        indicates a single image type; value of `None`
268                        indicates all types.
269    """
270    if image_type:
271        yield _create_version_map_handler(image_type, afe, True)
272    else:
273        for cls in _IMAGE_TYPE_CLASSES:
274            yield cls(afe, True)
275
276
277def list_all_mappings(afe, image_type):
278    """List all mappings in the AFE.
279
280    This function handles the following syntax usage case:
281
282        stable_version [-w SERVER] [-t TYPE]
283
284    @param afe          AFE RPC interface object; created from SERVER.
285    @param image_type   Argument to the `-t` option.
286    """
287    need_newline = False
288    for handler in _requested_mapping_handlers(afe, image_type):
289        if need_newline:
290            print()
291        handler.print_all_mappings()
292        need_newline = True
293
294
295def list_mapping_by_key(afe, image_type, key):
296    """List all mappings for the given board or model.
297
298    This function handles the following syntax usage case:
299
300        stable_version [-w SERVER] [-t TYPE] BOARD/MODEL
301
302    @param afe          AFE RPC interface object; created from SERVER.
303    @param image_type   Argument to the `-t` option.
304    @param key          Value of the BOARD/MODEL argument.
305    """
306    for handler in _requested_mapping_handlers(afe, image_type):
307        handler.print_mapping(key)
308
309
310def _validate_set_mapping(arguments):
311    """Validate syntactic requirements to assign a mapping.
312
313    The given arguments specified assigning version to be assigned to
314    a board or model; check the arguments for errors that can't be
315    discovered by `ArgumentParser`.  Errors are reported by raising
316    `_CommandError`.
317
318    @param arguments  `Namespace` object returned from argument parsing.
319    """
320    if not arguments.type:
321        raise _CommandError('The -t/--type option is required to assign a '
322                            'version')
323    if arguments.type == _FirmwareVersionMapHandler.TYPE:
324        msg = ('Cannot assign %s versions directly; '
325               'must assign the %s version instead.')
326        descriptions = (_FirmwareVersionMapHandler.DESCRIPTION,
327                        _CrOSVersionMapHandler.DESCRIPTION)
328        raise _CommandError(msg % descriptions)
329
330
331def set_mapping(afe, image_type, key, version, dry_run):
332    """Assign a version mapping to the given board or model.
333
334    This function handles the following syntax usage case:
335
336        stable_version [-w SERVER] [-n] -t TYPE BOARD/MODEL VERSION
337
338    @param afe          AFE RPC interface object; created from SERVER.
339    @param image_type   Argument to the `-t` option.
340    @param key          Value of the BOARD/MODEL argument.
341    @param key          Value of the VERSION argument.
342    @param dry_run      Whether the `-n` option was supplied.
343    """
344    if dry_run:
345        print('Dry run; no mappings will be changed.')
346    handler = _create_version_map_handler(image_type, afe, dry_run)
347    handler.set_mapping(key, version)
348
349
350def _validate_delete_mapping(arguments):
351    """Validate syntactic requirements to delete a mapping.
352
353    The given arguments specified the `-d` / `--delete` option; check
354    the arguments for errors that can't be discovered by
355    `ArgumentParser`.  Errors are reported by raising `_CommandError`.
356
357    @param arguments  `Namespace` object returned from argument parsing.
358    """
359    if arguments.key is None:
360        raise _CommandError('Must specify BOARD_OR_MODEL argument '
361                            'with -d/--delete')
362    if arguments.version is not None:
363        raise _CommandError('Cannot specify VERSION argument with '
364                            '-d/--delete')
365    if not arguments.type:
366        raise _CommandError('-t/--type required with -d/--delete option')
367
368
369def delete_mapping(afe, image_type, key, dry_run):
370    """Delete the version mapping for the given board or model.
371
372    This function handles the following syntax usage case:
373
374        stable_version [-w SERVER] [-n] -t TYPE -d BOARD/MODEL
375
376    @param afe          AFE RPC interface object; created from SERVER.
377    @param image_type   Argument to the `-t` option.
378    @param key          Value of the BOARD/MODEL argument.
379    @param dry_run      Whether the `-n` option was supplied.
380    """
381    if dry_run:
382        print('Dry run; no mappings will be deleted.')
383    handler = _create_version_map_handler(image_type, afe, dry_run)
384    handler.delete_mapping(key)
385
386
387def _parse_args(argv):
388    """Parse the given arguments according to the command syntax.
389
390    @param argv   Full argument vector, with argv[0] being the command
391                  name.
392    """
393    parser = argparse.ArgumentParser(
394        prog=os.path.basename(argv[0]),
395        description='Set and view software version assignments')
396    parser.add_argument('-w', '--web', default=None,
397                        metavar='SERVER',
398                        help='Specify the AFE to query.')
399    parser.add_argument('-n', '--dry-run', action='store_true',
400                        help='Report what would be done without making '
401                             'changes.')
402    parser.add_argument('-t', '--type', default=None,
403                        choices=_ALL_IMAGE_TYPES,
404                        help='Specify type of software image to be assigned.')
405    parser.add_argument('-d', '--delete', action='store_true',
406                        help='Delete the BOARD_OR_MODEL argument from the '
407                             'mappings.')
408    parser.add_argument('key', nargs='?', metavar='BOARD_OR_MODEL',
409                        help='Board, model, or other key for which to get or '
410                             'set a version')
411    parser.add_argument('version', nargs='?', metavar='VERSION',
412                        help='Version to be assigned')
413    return parser.parse_args(argv[1:])
414
415
416def _dispatch_command(afe, arguments):
417    if arguments.delete:
418        _validate_delete_mapping(arguments)
419        delete_mapping(afe, arguments.type, arguments.key,
420                       arguments.dry_run)
421    elif arguments.key is None:
422        list_all_mappings(afe, arguments.type)
423    elif arguments.version is None:
424        list_mapping_by_key(afe, arguments.type, arguments.key)
425    else:
426        _validate_set_mapping(arguments)
427        set_mapping(afe, arguments.type, arguments.key,
428                    arguments.version, arguments.dry_run)
429
430
431def main(argv):
432    """Standard main routine.
433
434    @param argv  Command line arguments including `sys.argv[0]`.
435    """
436    arguments = _parse_args(argv)
437    afe = frontend.AFE(server=arguments.web)
438    try:
439        _dispatch_command(afe, arguments)
440    except _CommandError as exc:
441        print('Error: %s' % str(exc), file=sys.stderr)
442        sys.exit(1)
443
444
445if __name__ == '__main__':
446    try:
447        main(sys.argv)
448    except KeyboardInterrupt:
449        pass
450