| #!/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) |