1#!/usr/bin/python2 2# Copyright 2016 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""" 7Automatically update the afe_stable_versions table. 8 9This command updates the stable repair version for selected boards 10in the lab. For each board, if the version that Omaha is serving 11on the Beta channel for the board is more recent than the current 12stable version in the AFE database, then the AFE is updated to use 13the version on Omaha. 14 15The upgrade process is applied to every "managed board" in the test 16lab. Generally, a managed board is a board with both spare and 17critical scheduling pools. 18 19See `autotest_lib.site_utils.lab_inventory` for the full definition 20of "managed board". 21 22The command supports a `--dry-run` option that reports changes that 23would be made, without making the actual RPC calls to change the 24database. 25 26""" 27 28import argparse 29import logging 30 31import common 32from autotest_lib.server.cros.dynamic_suite import frontend_wrappers 33from autotest_lib.site_utils import lab_inventory 34from autotest_lib.site_utils import loglib 35from autotest_lib.site_utils.stable_images import build_data 36from chromite.lib import ts_mon_config 37from chromite.lib import metrics 38 39 40# _DEFAULT_BOARD - The distinguished board name used to identify a 41# stable version mapping that is used for any board without an explicit 42# mapping of its own. 43# 44# _DEFAULT_VERSION_TAG - A string used to signify that there is no 45# mapping for a board, in other words, the board is mapped to the 46# default version. 47# 48_DEFAULT_BOARD = 'DEFAULT' 49_DEFAULT_VERSION_TAG = '(default)' 50 51_METRICS_PREFIX = 'chromeos/autotest/assign_stable_images' 52 53 54class _VersionUpdater(object): 55 """ 56 Class to report and apply version changes. 57 58 This class is responsible for the low-level logic of applying 59 version upgrades and reporting them as command output. 60 61 This class exists to solve two problems: 62 1. To distinguish "normal" vs. "dry-run" modes. Each mode has a 63 subclass; methods that perform actual AFE updates are 64 implemented for the normal mode subclass only. 65 2. To provide hooks for unit tests. The unit tests override both 66 the reporting and modification behaviors, in order to test the 67 higher level logic that decides what changes are needed. 68 69 Methods meant merely to report changes to command output have names 70 starting with "report" or "_report". Methods that are meant to 71 change the AFE in normal mode have names starting with "_do" 72 """ 73 74 def __init__(self, afe, dry_run): 75 """Initialize us. 76 77 @param afe: A frontend.AFE object. 78 @param dry_run: A boolean indicating whether to execute in dry run mode. 79 No updates are persisted to the afe in dry run. 80 """ 81 self._dry_run = dry_run 82 image_types = [afe.CROS_IMAGE_TYPE, afe.FIRMWARE_IMAGE_TYPE] 83 self._version_maps = { 84 image_type: afe.get_stable_version_map(image_type) 85 for image_type in image_types 86 } 87 self._cros_map = self._version_maps[afe.CROS_IMAGE_TYPE] 88 self._selected_map = None 89 90 def select_version_map(self, image_type): 91 """ 92 Select an AFE version map object based on `image_type`. 93 94 This creates and remembers an AFE version mapper object to be 95 used for making changes in normal mode. 96 97 @param image_type Image type parameter for the version mapper 98 object. 99 """ 100 self._selected_map = self._version_maps[image_type] 101 return self._selected_map.get_all_versions() 102 103 def report_default_changed(self, old_default, new_default): 104 """ 105 Report that the default version mapping is changing. 106 107 This merely reports a text description of the pending change 108 without executing it. 109 110 @param old_default The original default version. 111 @param new_default The new default version to be applied. 112 """ 113 logging.debug('Default %s -> %s', old_default, new_default) 114 115 def _report_board_changed(self, board, old_version, new_version): 116 """ 117 Report a change in one board's assigned version mapping. 118 119 This merely reports a text description of the pending change 120 without executing it. 121 122 @param board The board with the changing version. 123 @param old_version The original version mapped to the board. 124 @param new_version The new version to be applied to the board. 125 """ 126 logging.debug(' %-22s %s -> %s', board, old_version, new_version) 127 128 def report_board_unchanged(self, board, old_version): 129 """ 130 Report that a board's version mapping is unchanged. 131 132 This reports that a board has a non-default mapping that will be 133 unchanged. 134 135 @param board The board that is not changing. 136 @param old_version The board's version mapping. 137 """ 138 self._report_board_changed(board, '(no change)', old_version) 139 140 def _do_set_mapping(self, board, new_version): 141 """ 142 Change one board's assigned version mapping. 143 144 @param board The board with the changing version. 145 @param new_version The new version to be applied to the board. 146 """ 147 if self._dry_run: 148 logging.info('DRYRUN: Would have set %s version to %s', 149 board, new_version) 150 else: 151 self._selected_map.set_version(board, new_version) 152 153 def _do_delete_mapping(self, board): 154 """ 155 Delete one board's assigned version mapping. 156 157 @param board The board with the version to be deleted. 158 """ 159 if self._dry_run: 160 logging.info('DRYRUN: Would have deleted version for %s', board) 161 else: 162 self._selected_map.delete_version(board) 163 164 def set_mapping(self, board, old_version, new_version): 165 """ 166 Change and report a board version mapping. 167 168 @param board The board with the changing version. 169 @param old_version The original version mapped to the board. 170 @param new_version The new version to be applied to the board. 171 """ 172 self._report_board_changed(board, old_version, new_version) 173 self._do_set_mapping(board, new_version) 174 175 def upgrade_default(self, new_default): 176 """ 177 Apply a default version change. 178 179 @param new_default The new default version to be applied. 180 """ 181 self._do_set_mapping(_DEFAULT_BOARD, new_default) 182 183 def delete_mapping(self, board, old_version): 184 """ 185 Delete a board version mapping, and report the change. 186 187 @param board The board with the version to be deleted. 188 @param old_version The board's verson prior to deletion. 189 """ 190 assert board != _DEFAULT_BOARD 191 self._report_board_changed(board, 192 old_version, 193 _DEFAULT_VERSION_TAG) 194 self._do_delete_mapping(board) 195 196 197def _get_upgrade_versions(cros_versions, omaha_versions, boards): 198 """ 199 Get the new stable versions to which we should update. 200 201 The new versions are returned as a tuple of a dictionary mapping 202 board names to versions, plus a new default board setting. The 203 new default is determined as the most commonly used version 204 across the given boards. 205 206 The new dictionary will have a mapping for every board in `boards`. 207 That mapping will be taken from `cros_versions`, unless the board has 208 a mapping in `omaha_versions` _and_ the omaha version is more recent 209 than the AFE version. 210 211 @param cros_versions The current board->version mappings in the 212 AFE. 213 @param omaha_versions The current board->version mappings from 214 Omaha for the Beta channel. 215 @param boards Set of boards to be upgraded. 216 @return Tuple of (mapping, default) where mapping is a dictionary 217 mapping boards to versions, and default is a version string. 218 """ 219 upgrade_versions = {} 220 version_counts = {} 221 afe_default = cros_versions[_DEFAULT_BOARD] 222 for board in boards: 223 version = build_data.get_omaha_upgrade( 224 omaha_versions, board, 225 cros_versions.get(board, afe_default)) 226 upgrade_versions[board] = version 227 version_counts.setdefault(version, 0) 228 version_counts[version] += 1 229 return (upgrade_versions, 230 max(version_counts.items(), key=lambda x: x[1])[0]) 231 232 233def _get_firmware_upgrades(cros_versions): 234 """ 235 Get the new firmware versions to which we should update. 236 237 @param cros_versions Current board->cros version mappings in the 238 AFE. 239 @return A dictionary mapping boards/models to firmware upgrade versions. 240 If the build is unibuild, the key is a model name; else, the key 241 is a board name. 242 """ 243 firmware_upgrades = {} 244 for board, version in cros_versions.iteritems(): 245 firmware_upgrades.update( 246 build_data.get_firmware_versions(board, version)) 247 return firmware_upgrades 248 249 250def _apply_cros_upgrades(updater, old_versions, new_versions, 251 new_default): 252 """ 253 Change CrOS stable version mappings in the AFE. 254 255 The input `old_versions` dictionary represents the content of the 256 `afe_stable_versions` database table; it contains mappings for a 257 default version, plus exceptions for boards with non-default 258 mappings. 259 260 The `new_versions` dictionary contains a mapping for every board, 261 including boards that will be mapped to the new default version. 262 263 This function applies the AFE changes necessary to produce the new 264 AFE mappings indicated by `new_versions` and `new_default`. The 265 changes are ordered so that at any moment, every board is mapped 266 either according to the old or the new mapping. 267 268 @param updater Instance of _VersionUpdater responsible for 269 making the actual database changes. 270 @param old_versions The current board->version mappings in the 271 AFE. 272 @param new_versions New board->version mappings obtained by 273 applying Beta channel upgrades from Omaha. 274 @param new_default The new default build for the AFE. 275 """ 276 old_default = old_versions[_DEFAULT_BOARD] 277 if old_default != new_default: 278 updater.report_default_changed(old_default, new_default) 279 logging.info('Applying stable version changes:') 280 default_count = 0 281 for board, new_build in new_versions.items(): 282 if new_build == new_default: 283 default_count += 1 284 elif board in old_versions and new_build == old_versions[board]: 285 updater.report_board_unchanged(board, new_build) 286 else: 287 old_build = old_versions.get(board) 288 if old_build is None: 289 old_build = _DEFAULT_VERSION_TAG 290 updater.set_mapping(board, old_build, new_build) 291 if old_default != new_default: 292 updater.upgrade_default(new_default) 293 for board, new_build in new_versions.items(): 294 if new_build == new_default and board in old_versions: 295 updater.delete_mapping(board, old_versions[board]) 296 logging.info('%d boards now use the default mapping', default_count) 297 298 299def _apply_firmware_upgrades(updater, old_versions, new_versions): 300 """ 301 Change firmware version mappings in the AFE. 302 303 The input `old_versions` dictionary represents the content of the 304 firmware mappings in the `afe_stable_versions` database table. 305 There is no default version; missing boards simply have no current 306 version. 307 308 This function applies the AFE changes necessary to produce the new 309 AFE mappings indicated by `new_versions`. 310 311 TODO(jrbarnette) This function ought to remove any mapping not found 312 in `new_versions`. However, in theory, that's only needed to 313 account for boards that are removed from the lab, and that hasn't 314 happened yet. 315 316 @param updater Instance of _VersionUpdater responsible for 317 making the actual database changes. 318 @param old_versions The current board->version mappings in the 319 AFE. 320 @param new_versions New board->version mappings obtained by 321 applying Beta channel upgrades from Omaha. 322 """ 323 unchanged = 0 324 no_version = 0 325 for board, new_firmware in new_versions.items(): 326 if new_firmware is None: 327 no_version += 1 328 elif board not in old_versions: 329 updater.set_mapping(board, '(nothing)', new_firmware) 330 else: 331 old_firmware = old_versions[board] 332 if new_firmware != old_firmware: 333 updater.set_mapping(board, old_firmware, new_firmware) 334 else: 335 unchanged += 1 336 logging.info('%d boards have no firmware mapping', no_version) 337 logging.info('%d boards are unchanged', unchanged) 338 339 340def _assign_stable_images(arguments): 341 afe = frontend_wrappers.RetryingAFE(server=arguments.web) 342 updater = _VersionUpdater(afe, dry_run=arguments.dry_run) 343 344 cros_versions = updater.select_version_map(afe.CROS_IMAGE_TYPE) 345 omaha_versions = build_data.get_omaha_version_map() 346 upgrade_versions, new_default = ( 347 _get_upgrade_versions(cros_versions, omaha_versions, 348 lab_inventory.get_managed_boards(afe))) 349 _apply_cros_upgrades(updater, cros_versions, 350 upgrade_versions, new_default) 351 352 logging.info('Applying firmware updates.') 353 fw_versions = updater.select_version_map(afe.FIRMWARE_IMAGE_TYPE) 354 firmware_upgrades = _get_firmware_upgrades(upgrade_versions) 355 _apply_firmware_upgrades(updater, fw_versions, firmware_upgrades) 356 357 358def main(): 359 """Standard main routine.""" 360 parser = argparse.ArgumentParser( 361 description='Update the stable repair version for all ' 362 'boards') 363 parser.add_argument('-n', '--dry-run', 364 action='store_true', 365 help='print changes without executing them') 366 loglib.add_logging_options(parser) 367 # TODO(crbug/888046) Make these arguments required once puppet is updated to 368 # pass them in. 369 parser.add_argument('--web', 370 default='cautotest', 371 help='URL to the AFE to update.') 372 373 arguments = parser.parse_args() 374 loglib.configure_logging_with_args(parser, arguments) 375 376 tsmon_args = { 377 'service_name': parser.prog, 378 'indirect': False, 379 'auto_flush': False, 380 } 381 if arguments.dry_run: 382 logging.info('DRYRUN: No changes will be made.') 383 # metrics will be logged to logging stream anyway. 384 tsmon_args['debug_file'] = '/dev/null' 385 386 try: 387 with ts_mon_config.SetupTsMonGlobalState(**tsmon_args): 388 with metrics.SuccessCounter(_METRICS_PREFIX + '/tick', 389 fields={'afe': arguments.web}): 390 _assign_stable_images(arguments) 391 finally: 392 metrics.Flush() 393 394if __name__ == '__main__': 395 main() 396