#!/usr/bin/python # 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. """Command for viewing and changing software version assignments. Usage: stable_version [ -w SERVER ] [ -n ] [ -t TYPE ] stable_version [ -w SERVER ] [ -n ] [ -t TYPE ] BOARD/MODEL stable_version [ -w SERVER ] [ -n ] -t TYPE -d BOARD/MODEL stable_version [ -w SERVER ] [ -n ] -t TYPE BOARD/MODEL VERSION Available options: -w SERVER | --web SERVER Used to specify an alternative server for the AFE RPC interface. -n | --dry-run When specified, the command reports what would be done, but makes no changes. -t TYPE | --type TYPE Specifies the type of version mapping to use. This option is required for operations to change or delete mappings. When listing mappings, the option may be omitted, in which case all mapping types are listed. -d | --delete Delete the mapping for the given board or model argument. Command arguments: BOARD/MODEL When specified, indicates the board or model to use as a key when listing, changing, or deleting mappings. VERSION When specified, indicates that the version name should be assigned to the given board or model. With no arguments, the command will list all available mappings of all types. The `--type` option will restrict the listing to only mappings of the given type. With only a board or model specified (and without the `--delete` option), will list all mappings for the given board or model. The `--type` option will restrict the listing to only mappings of the given type. With the `--delete` option, will delete the mapping for the given board or model. The `--type` option is required in this case. With both a board or model and a version specified, will assign the version to the given board or model. The `--type` option is required in this case. """ import argparse import os import sys import common from autotest_lib.server import frontend from autotest_lib.site_utils.stable_images import build_data class _CommandError(Exception): """Exception to indicate an error in command processing.""" class _VersionMapHandler(object): """An internal class to wrap data for version map operations. This is a simple class to gather in one place data associated with higher-level command line operations. @property _description A string description used to describe the image type when printing command output. @property _dry_run Value of the `--dry-run` command line operation. @property _afe AFE RPC object. @property _version_map AFE version map object for the image type. """ # Subclasses are required to redefine both of these to a string with # an appropriate value. TYPE = None DESCRIPTION = None def __init__(self, afe, dry_run): self._afe = afe self._dry_run = dry_run self._version_map = afe.get_stable_version_map(self.TYPE) @property def _description(self): return self.DESCRIPTION def _format_key_data(self, key): return '%-10s %-12s' % (self._description, key) def _format_operation(self, opname, key): return '%-9s %s' % (opname, self._format_key_data(key)) def get_mapping(self, key): """Return the mapping for `key`. @param key Board or model key to use for look up. """ return self._version_map.get_version(key) def print_all_mappings(self): """Print all mappings in `self._version_map`""" print '%s version mappings:' % self._description mappings = self._version_map.get_all_versions() if not mappings: return key_list = mappings.keys() key_width = max(12, len(max(key_list, key=len))) format = '%%-%ds %%s' % key_width for k in sorted(key_list): print format % (k, mappings[k]) def print_mapping(self, key): """Print the mapping for `key`. Prints a single mapping for the board/model specified by `key`. Print nothing if no mapping exists. @param key Board or model key to use for look up. """ version = self.get_mapping(key) if version is not None: print '%s %s' % (self._format_key_data(key), version) def set_mapping(self, key, new_version): """Change the mapping for `key`, and report the action. The mapping for the board or model specifed by `key` is set to `new_version`. The setting is reported to the user as added, changed, or unchanged based on the current mapping in the AFE. This operation honors `self._dry_run`. @param key Board or model key for assignment. @param new_version Version to be assigned to `key`. """ old_version = self.get_mapping(key) if old_version is None: print '%s -> %s' % ( self._format_operation('Adding', key), new_version) elif old_version != new_version: print '%s -> %s to %s' % ( self._format_operation('Updating', key), old_version, new_version) else: print '%s -> %s' % ( self._format_operation('Unchanged', key), old_version) if not self._dry_run and old_version != new_version: self._version_map.set_version(key, new_version) def delete_mapping(self, key): """Delete the mapping for `key`, and report the action. The mapping for the board or model specifed by `key` is removed from `self._version_map`. The change is reported to the user. Requests to delete non-existent keys are ignored. This operation honors `self._dry_run`. @param key Board or model key to be deleted. """ version = self.get_mapping(key) if version is not None: print '%s -> %s' % ( self._format_operation('Delete', key), version) if not self._dry_run: self._version_map.delete_version(key) else: print self._format_operation('Unmapped', key) class _FirmwareVersionMapHandler(_VersionMapHandler): TYPE = frontend.AFE.FIRMWARE_IMAGE_TYPE DESCRIPTION = 'Firmware' class _CrOSVersionMapHandler(_VersionMapHandler): TYPE = frontend.AFE.CROS_IMAGE_TYPE DESCRIPTION = 'Chrome OS' def set_mapping(self, board, version): """Assign the Chrome OS mapping for the given board. This function assigns the given Chrome OS version to the given board. Additionally, for any model with firmware bundled in the assigned build, that model will be assigned the firmware version found for it in the build. @param board Chrome OS board to be assigned a new version. @param version New Chrome OS version to be assigned to the board. """ new_version = build_data.get_omaha_upgrade( build_data.get_omaha_version_map(), board, version) if new_version != version: print 'Force %s version from Omaha: %-12s -> %s' % ( self._description, board, new_version) super(_CrOSVersionMapHandler, self).set_mapping(board, new_version) fw_versions = build_data.get_firmware_versions(board, new_version) fw_handler = _FirmwareVersionMapHandler(self._afe, self._dry_run) for model, fw_version in fw_versions.iteritems(): if fw_version is not None: fw_handler.set_mapping(model, fw_version) def delete_mapping(self, board): """Delete the Chrome OS mapping for the given board. This function handles deletes the Chrome OS version mapping for the given board. Additionally, any R/W firmware mapping that existed because of the OS mapping will be deleted as well. @param board Chrome OS board to be deleted from the mapping. """ version = self.get_mapping(board) super(_CrOSVersionMapHandler, self).delete_mapping(board) fw_versions = build_data.get_firmware_versions(board, version) fw_handler = _FirmwareVersionMapHandler(self._afe, self._dry_run) for model in fw_versions.iterkeys(): fw_handler.delete_mapping(model) class _FAFTVersionMapHandler(_VersionMapHandler): TYPE = frontend.AFE.FAFT_IMAGE_TYPE DESCRIPTION = 'FAFT' _IMAGE_TYPE_CLASSES = [ _CrOSVersionMapHandler, _FirmwareVersionMapHandler, _FAFTVersionMapHandler, ] _ALL_IMAGE_TYPES = [cls.TYPE for cls in _IMAGE_TYPE_CLASSES] _IMAGE_TYPE_HANDLERS = {cls.TYPE: cls for cls in _IMAGE_TYPE_CLASSES} def _create_version_map_handler(image_type, afe, dry_run): return _IMAGE_TYPE_HANDLERS[image_type](afe, dry_run) def _requested_mapping_handlers(afe, image_type): """Iterate through the image types for a listing operation. When listing all mappings, or when listing by board, the listing can be either for all available image types, or just for a single type requested on the command line. This function takes the value of the `-t` option, and yields a `_VersionMapHandler` object for either the single requested type, or for all of the types. @param afe AFE RPC interface object; created from SERVER. @param image_type Argument to the `-t` option. A non-empty string indicates a single image type; value of `None` indicates all types. """ if image_type: yield _create_version_map_handler(image_type, afe, True) else: for cls in _IMAGE_TYPE_CLASSES: yield cls(afe, True) def list_all_mappings(afe, image_type): """List all mappings in the AFE. This function handles the following syntax usage case: stable_version [-w SERVER] [-t TYPE] @param afe AFE RPC interface object; created from SERVER. @param image_type Argument to the `-t` option. """ need_newline = False for handler in _requested_mapping_handlers(afe, image_type): if need_newline: print handler.print_all_mappings() need_newline = True def list_mapping_by_key(afe, image_type, key): """List all mappings for the given board or model. This function handles the following syntax usage case: stable_version [-w SERVER] [-t TYPE] BOARD/MODEL @param afe AFE RPC interface object; created from SERVER. @param image_type Argument to the `-t` option. @param key Value of the BOARD/MODEL argument. """ for handler in _requested_mapping_handlers(afe, image_type): handler.print_mapping(key) def _validate_set_mapping(arguments): """Validate syntactic requirements to assign a mapping. The given arguments specified assigning version to be assigned to a board or model; check the arguments for errors that can't be discovered by `ArgumentParser`. Errors are reported by raising `_CommandError`. @param arguments `Namespace` object returned from argument parsing. """ if not arguments.type: raise _CommandError('The -t/--type option is required to assign a ' 'version') if arguments.type == _FirmwareVersionMapHandler.TYPE: msg = ('Cannot assign %s versions directly; ' 'must assign the %s version instead.') descriptions = (_FirmwareVersionMapHandler.DESCRIPTION, _CrOSVersionMapHandler.DESCRIPTION) raise _CommandError(msg % descriptions) def set_mapping(afe, image_type, key, version, dry_run): """Assign a version mapping to the given board or model. This function handles the following syntax usage case: stable_version [-w SERVER] [-n] -t TYPE BOARD/MODEL VERSION @param afe AFE RPC interface object; created from SERVER. @param image_type Argument to the `-t` option. @param key Value of the BOARD/MODEL argument. @param key Value of the VERSION argument. @param dry_run Whether the `-n` option was supplied. """ if dry_run: print 'Dry run; no mappings will be changed.' handler = _create_version_map_handler(image_type, afe, dry_run) handler.set_mapping(key, version) def _validate_delete_mapping(arguments): """Validate syntactic requirements to delete a mapping. The given arguments specified the `-d` / `--delete` option; check the arguments for errors that can't be discovered by `ArgumentParser`. Errors are reported by raising `_CommandError`. @param arguments `Namespace` object returned from argument parsing. """ if arguments.key is None: raise _CommandError('Must specify BOARD_OR_MODEL argument ' 'with -d/--delete') if arguments.version is not None: raise _CommandError('Cannot specify VERSION argument with ' '-d/--delete') if not arguments.type: raise _CommandError('-t/--type required with -d/--delete option') def delete_mapping(afe, image_type, key, dry_run): """Delete the version mapping for the given board or model. This function handles the following syntax usage case: stable_version [-w SERVER] [-n] -t TYPE -d BOARD/MODEL @param afe AFE RPC interface object; created from SERVER. @param image_type Argument to the `-t` option. @param key Value of the BOARD/MODEL argument. @param dry_run Whether the `-n` option was supplied. """ if dry_run: print 'Dry run; no mappings will be deleted.' handler = _create_version_map_handler(image_type, afe, dry_run) handler.delete_mapping(key) def _parse_args(argv): """Parse the given arguments according to the command syntax. @param argv Full argument vector, with argv[0] being the command name. """ parser = argparse.ArgumentParser( prog=os.path.basename(argv[0]), description='Set and view software version assignments') parser.add_argument('-w', '--web', default=None, metavar='SERVER', help='Specify the AFE to query.') parser.add_argument('-n', '--dry-run', action='store_true', help='Report what would be done without making ' 'changes.') parser.add_argument('-t', '--type', default=None, choices=_ALL_IMAGE_TYPES, help='Specify type of software image to be assigned.') parser.add_argument('-d', '--delete', action='store_true', help='Delete the BOARD_OR_MODEL argument from the ' 'mappings.') parser.add_argument('key', nargs='?', metavar='BOARD_OR_MODEL', help='Board, model, or other key for which to get or ' 'set a version') parser.add_argument('version', nargs='?', metavar='VERSION', help='Version to be assigned') return parser.parse_args(argv[1:]) def _dispatch_command(afe, arguments): if arguments.delete: _validate_delete_mapping(arguments) delete_mapping(afe, arguments.type, arguments.key, arguments.dry_run) elif arguments.key is None: list_all_mappings(afe, arguments.type) elif arguments.version is None: list_mapping_by_key(afe, arguments.type, arguments.key) else: _validate_set_mapping(arguments) set_mapping(afe, arguments.type, arguments.key, arguments.version, arguments.dry_run) def main(argv): """Standard main routine. @param argv Command line arguments including `sys.argv[0]`. """ arguments = _parse_args(argv) afe = frontend.AFE(server=arguments.web) try: _dispatch_command(afe, arguments) except _CommandError as exc: print >>sys.stderr, 'Error: %s' % str(exc) sys.exit(1) if __name__ == '__main__': try: main(sys.argv) except KeyboardInterrupt: pass