310 lines
12 KiB
Python
Executable file
310 lines
12 KiB
Python
Executable file
#!/usr/bin/python
|
|
|
|
"""Automatically update the afe_stable_versions table.
|
|
|
|
This command updates the stable repair version for selected boards
|
|
in the lab. For each board, if the version that Omaha is serving
|
|
on the Beta channel for the board is more recent than the current
|
|
stable version in the AFE database, then the AFE is updated to use
|
|
the version on Omaha.
|
|
|
|
The upgrade process is applied to every "managed board" in the test
|
|
lab. Generally, a managed board is a board with both spare and
|
|
critical scheduling pools.
|
|
|
|
See `autotest_lib.site_utils.lab_inventory` for the full definition
|
|
of "managed board".
|
|
|
|
The command accepts two mutually exclusive options determining
|
|
how changes will be handled:
|
|
* With no options, the command will make RPC calls to the AFE to
|
|
update the state according to the rules.
|
|
* With the `--shell-mode` option, the command will print a series
|
|
of `atest` commands that will accomplish the changes.
|
|
* With the `--dry-run` option, the command will perform all normal
|
|
printing, but will skip actual RPC calls to change the database.
|
|
|
|
The `--shell-mode` and `--dry-run` options are mutually exclusive.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
|
|
import common
|
|
from autotest_lib.client.common_lib import utils
|
|
from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
|
|
from autotest_lib.site_utils import lab_inventory
|
|
|
|
|
|
# _OMAHA_STATUS - URI of a file in GoogleStorage with a JSON object
|
|
# summarizing all versions currently being served by Omaha.
|
|
#
|
|
# The principle 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'
|
|
|
|
# _SET_VERSION - `atest` command that will assign a specific board a
|
|
# specific stable version in the AFE.
|
|
#
|
|
# _DELETE_VERSION - `atest` command that will delete a stable version
|
|
# mapping from the AFE.
|
|
#
|
|
# _DEFAULT_BOARD - The distinguished board name used to identify a
|
|
# stable version mapping that is used for any board without an explicit
|
|
# mapping of its own.
|
|
#
|
|
_SET_VERSION = 'atest stable_version modify --board %s --version %s'
|
|
_DELETE_VERSION = ('atest stable_version delete --no-confirmation '
|
|
'--board %s')
|
|
_DEFAULT_BOARD = 'DEFAULT'
|
|
|
|
|
|
# Execution modes:
|
|
#
|
|
# _NORMAL_MODE: no command line options.
|
|
# _DRY_RUN: --dry-run on the command line.
|
|
# _SHELL_MODE: --shell-mode on the command line.
|
|
#
|
|
_NORMAL_MODE = 0
|
|
_DRY_RUN = 1
|
|
_SHELL_MODE = 2
|
|
|
|
|
|
def _get_omaha_board(json_entry):
|
|
"""Get the board name from an 'omaha_data' entry.
|
|
|
|
@param json_entry Deserialized JSON object for one entry of the
|
|
'omaha_data' array.
|
|
@return Returns a version number in the form R##-####.#.#.
|
|
"""
|
|
return json_entry['board']['public_codename']
|
|
|
|
|
|
def _get_omaha_version(json_entry):
|
|
"""Get a Chrome OS version string from an 'omaha_data' entry.
|
|
|
|
@param json_entry Deserialized JSON object for one entry of the
|
|
'omaha_data' array.
|
|
@return Returns a version number in the form R##-####.#.#.
|
|
"""
|
|
milestone = json_entry['chrome_version'].split('.')[0]
|
|
build = json_entry['chrome_os_version']
|
|
return 'R%s-%s' % (milestone, build)
|
|
|
|
|
|
def _get_omaha_versions():
|
|
"""Get the current Beta versions serving on Omaha.
|
|
|
|
Returns a dictionary mapping board names to the currently preferred
|
|
version for the Beta channel as served by Omaha. The board names
|
|
are the names as known to Omaha: If the board name in the AFE has a
|
|
'_', the corresponding Omaha name uses a '-' instead. The boards
|
|
mapped may include boards not in the list of managed boards in the
|
|
lab.
|
|
|
|
The beta channel versions are found by searching the `_OMAHA_STATUS`
|
|
file. That file is calculated by GoldenEye from Omaha. It's
|
|
accurate, but could be out-of-date for a small time window.
|
|
|
|
@return A dictionary mapping Omaha boards to Beta versions.
|
|
"""
|
|
sp = subprocess.Popen(['gsutil', 'cat', _OMAHA_STATUS],
|
|
stdout=subprocess.PIPE)
|
|
omaha_status = json.load(sp.stdout)
|
|
return {_get_omaha_board(e): _get_omaha_version(e)
|
|
for e in omaha_status['omaha_data']
|
|
if e['channel'] == 'beta'}
|
|
|
|
|
|
def _get_upgrade_versions(afe_versions, omaha_versions, boards):
|
|
"""Get the new stable versions to which we should update.
|
|
|
|
The new versions are returned as a tuple of a dictionary mapping
|
|
board names to versions, plus a new default board setting. The
|
|
new default is determined as the most commonly used version
|
|
across the given boards.
|
|
|
|
The new dictionary will have a mapping for every board in `boards`.
|
|
That mapping will be taken from `afe_versions`, unless the board has
|
|
a mapping in `omaha_versions` _and_ the omaha version is more recent
|
|
than the AFE version.
|
|
|
|
@param afe_versions The current board->version mappings in the
|
|
AFE.
|
|
@param omaha_versions The current board->version mappings from
|
|
Omaha for the Beta channel.
|
|
@param boards Set of boards to be upgraded.
|
|
@return Tuple of (mapping, default) where mapping is a dictionary
|
|
mapping boards to versions, and default is a version string.
|
|
"""
|
|
upgrade_versions = {}
|
|
version_counts = {}
|
|
afe_default = afe_versions[_DEFAULT_BOARD]
|
|
for board in boards:
|
|
version = afe_versions.get(board, afe_default)
|
|
omaha_version = omaha_versions.get(board.replace('_', '-'))
|
|
if (omaha_version is not None and
|
|
utils.compare_versions(version, omaha_version) < 0):
|
|
version = omaha_version
|
|
upgrade_versions[board] = version
|
|
version_counts.setdefault(version, 0)
|
|
version_counts[version] += 1
|
|
return (upgrade_versions,
|
|
max(version_counts.items(), key=lambda x: x[1])[0])
|
|
|
|
|
|
def _set_stable_version(afe, mode, board, version):
|
|
"""Call the AFE to change a stable version mapping.
|
|
|
|
Setting the mapping for the distinguished board name
|
|
`_DEFAULT_BOARD` will change the default mapping for any board
|
|
that doesn't have its own mapping.
|
|
|
|
@param afe AFE object for RPC calls.
|
|
@param mode Mode indicating whether to print a shell
|
|
command, call an RPC, or do nothing.
|
|
@param board Update the mapping for this board.
|
|
@param version Update the board to this version.
|
|
"""
|
|
if mode == _SHELL_MODE:
|
|
print _SET_VERSION % (board, version)
|
|
elif mode == _NORMAL_MODE:
|
|
afe.run('set_stable_version', board=board, version=version)
|
|
|
|
|
|
def _delete_stable_version(afe, mode, board):
|
|
"""Call the AFE to delete a stable version mapping.
|
|
|
|
Deleting a mapping causes the board to revert to the current default
|
|
mapping in the AFE.
|
|
|
|
@param afe AFE object for RPC calls.
|
|
@param mode Mode indicating whether to print a shell
|
|
command, call an RPC, or do nothing.
|
|
@param board Delete the mapping for this board.
|
|
"""
|
|
assert board != _DEFAULT_BOARD
|
|
if mode == _SHELL_MODE:
|
|
print _DELETE_VERSION % board
|
|
elif mode == _NORMAL_MODE:
|
|
afe.run('delete_stable_version', board=board)
|
|
|
|
|
|
def _apply_upgrades(afe, mode, afe_versions,
|
|
upgrade_versions, new_default):
|
|
"""Change stable version mappings in the AFE.
|
|
|
|
Update the `afe_stable_versions` database table to have the new
|
|
settings indicated by `upgrade_versions` and `new_default`. Order
|
|
the changes so that at any moment, every board is mapped either
|
|
according to the old or the new mapping.
|
|
|
|
@param afe AFE object for RPC calls.
|
|
@param mode Mode indicating whether the action is to
|
|
print shell commands, do nothing, or
|
|
actually make RPC calls for changes.
|
|
@param afe_versions The current board->version mappings in
|
|
the AFE.
|
|
@param upgrade_versions The current board->version mappings from
|
|
Omaha for the Beta channel.
|
|
@param new_default The new default build for the AFE.
|
|
"""
|
|
old_default = afe_versions[_DEFAULT_BOARD]
|
|
if mode != _SHELL_MODE and new_default != old_default:
|
|
print 'Default %s -> %s' % (old_default, new_default)
|
|
print 'Applying stable version changes:'
|
|
# N.B. The ordering here matters: Any board that will have a
|
|
# non-default stable version must be updated _before_ we change the
|
|
# default mapping, below.
|
|
for board, build in upgrade_versions.items():
|
|
if build == new_default:
|
|
continue
|
|
if board in afe_versions and build == afe_versions[board]:
|
|
if mode == _SHELL_MODE:
|
|
message = '# Leave board %s at %s'
|
|
else:
|
|
message = ' %-22s (no change) -> %s'
|
|
print message % (board, build)
|
|
else:
|
|
if mode != _SHELL_MODE:
|
|
old_build = afe_versions.get(board, '(default)')
|
|
print ' %-22s %s -> %s' % (board, old_build, build)
|
|
_set_stable_version(afe, mode, board, build)
|
|
# At this point, all non-default mappings have been installed.
|
|
# If there's a new default mapping, make that change now, and delete
|
|
# any non-default mappings made obsolete by the update.
|
|
if new_default != old_default:
|
|
_set_stable_version(afe, mode, _DEFAULT_BOARD, new_default)
|
|
for board, build in upgrade_versions.items():
|
|
if board in afe_versions and build == new_default:
|
|
if mode != _SHELL_MODE:
|
|
print (' %-22s %s -> (default)' %
|
|
(board, afe_versions[board]))
|
|
_delete_stable_version(afe, mode, board)
|
|
|
|
|
|
def _parse_command_line(argv):
|
|
"""Parse the command line arguments.
|
|
|
|
Create an argument parser for this command's syntax, parse the
|
|
command line, and return the result of the ArgumentParser
|
|
parse_args() method.
|
|
|
|
@param argv Standard command line argument vector; argv[0] is
|
|
assumed to be the command name.
|
|
@return Result returned by ArgumentParser.parse_args().
|
|
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
prog=argv[0],
|
|
description='Update the stable repair version for all '
|
|
'boards')
|
|
mode_group = parser.add_mutually_exclusive_group()
|
|
mode_group.add_argument('-x', '--shell-mode', dest='mode',
|
|
action='store_const', const=_SHELL_MODE,
|
|
help='print shell commands to make the '
|
|
'changes')
|
|
mode_group.add_argument('-n', '--dry-run', dest='mode',
|
|
action='store_const', const=_DRY_RUN,
|
|
help='print changes without executing them')
|
|
parser.add_argument('extra_boards', nargs='*', metavar='BOARD',
|
|
help='Names of additional boards to be updated.')
|
|
arguments = parser.parse_args(argv[1:])
|
|
if not arguments.mode:
|
|
arguments.mode = _NORMAL_MODE
|
|
return arguments
|
|
|
|
|
|
def main(argv):
|
|
"""Standard main routine.
|
|
|
|
@param argv Command line arguments including `sys.argv[0]`.
|
|
"""
|
|
arguments = _parse_command_line(argv)
|
|
if arguments.mode == _DRY_RUN:
|
|
print 'Dry run; no changes will be made.'
|
|
afe = frontend_wrappers.RetryingAFE(server=None)
|
|
boards = (set(arguments.extra_boards) |
|
|
lab_inventory.get_managed_boards(afe))
|
|
# The 'get_all_stable_versions' RPC returns a dictionary mapping
|
|
# `_DEFAULT_BOARD` to the current default version, plus a set of
|
|
# non-default board -> version mappings.
|
|
afe_versions = afe.run('get_all_stable_versions')
|
|
upgrade_versions, new_default = (
|
|
_get_upgrade_versions(afe_versions,
|
|
_get_omaha_versions(),
|
|
boards))
|
|
_apply_upgrades(afe, arguments.mode, afe_versions,
|
|
upgrade_versions, new_default)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv)
|