blob: eed4eb0e4b2cb71383daba2629654778af8ea81a [file] [log] [blame]
#!/usr/bin/python2
# 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