1#!/usr/bin/python3 2# Copyright 2018 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"""Command for viewing and changing software version assignments. 7 8Usage: 9 stable_version [ -w SERVER ] [ -n ] [ -t TYPE ] 10 stable_version [ -w SERVER ] [ -n ] [ -t TYPE ] BOARD/MODEL 11 stable_version [ -w SERVER ] [ -n ] -t TYPE -d BOARD/MODEL 12 stable_version [ -w SERVER ] [ -n ] -t TYPE BOARD/MODEL VERSION 13 14Available options: 15-w SERVER | --web SERVER 16 Used to specify an alternative server for the AFE RPC interface. 17 18-n | --dry-run 19 When specified, the command reports what would be done, but makes no 20 changes. 21 22-t TYPE | --type TYPE 23 Specifies the type of version mapping to use. This option is 24 required for operations to change or delete mappings. When listing 25 mappings, the option may be omitted, in which case all mapping types 26 are listed. 27 28-d | --delete 29 Delete the mapping for the given board or model argument. 30 31Command arguments: 32BOARD/MODEL 33 When specified, indicates the board or model to use as a key when 34 listing, changing, or deleting mappings. 35 36VERSION 37 When specified, indicates that the version name should be assigned 38 to the given board or model. 39 40With no arguments, the command will list all available mappings of all 41types. The `--type` option will restrict the listing to only mappings of 42the given type. 43 44With only a board or model specified (and without the `--delete` 45option), will list all mappings for the given board or model. The 46`--type` option will restrict the listing to only mappings of the given 47type. 48 49With the `--delete` option, will delete the mapping for the given board 50or model. The `--type` option is required in this case. 51 52With both a board or model and a version specified, will assign the 53version to the given board or model. The `--type` option is required in 54this case. 55""" 56 57from __future__ import print_function 58 59import argparse 60import os 61import sys 62 63import common 64from autotest_lib.server import frontend 65from autotest_lib.site_utils.stable_images import build_data 66 67 68class _CommandError(Exception): 69 """Exception to indicate an error in command processing.""" 70 71 72class _VersionMapHandler(object): 73 """An internal class to wrap data for version map operations. 74 75 This is a simple class to gather in one place data associated 76 with higher-level command line operations. 77 78 @property _description A string description used to describe the 79 image type when printing command output. 80 @property _dry_run Value of the `--dry-run` command line 81 operation. 82 @property _afe AFE RPC object. 83 @property _version_map AFE version map object for the image type. 84 """ 85 86 # Subclasses are required to redefine both of these to a string with 87 # an appropriate value. 88 TYPE = None 89 DESCRIPTION = None 90 91 def __init__(self, afe, dry_run): 92 self._afe = afe 93 self._dry_run = dry_run 94 self._version_map = afe.get_stable_version_map(self.TYPE) 95 96 @property 97 def _description(self): 98 return self.DESCRIPTION 99 100 def _format_key_data(self, key): 101 return '%-10s %-12s' % (self._description, key) 102 103 def _format_operation(self, opname, key): 104 return '%-9s %s' % (opname, self._format_key_data(key)) 105 106 def get_mapping(self, key): 107 """Return the mapping for `key`. 108 109 @param key Board or model key to use for look up. 110 """ 111 return self._version_map.get_version(key) 112 113 def print_all_mappings(self): 114 """Print all mappings in `self._version_map`""" 115 print('%s version mappings:' % self._description) 116 mappings = self._version_map.get_all_versions() 117 if not mappings: 118 return 119 key_list = mappings.keys() 120 key_width = max(12, len(max(key_list, key=len))) 121 format = '%%-%ds %%s' % key_width 122 for k in sorted(key_list): 123 print(format % (k, mappings[k])) 124 125 def print_mapping(self, key): 126 """Print the mapping for `key`. 127 128 Prints a single mapping for the board/model specified by 129 `key`. Print nothing if no mapping exists. 130 131 @param key Board or model key to use for look up. 132 """ 133 version = self.get_mapping(key) 134 if version is not None: 135 print('%s %s' % (self._format_key_data(key), version)) 136 137 def set_mapping(self, key, new_version): 138 """Change the mapping for `key`, and report the action. 139 140 The mapping for the board or model specifed by `key` is set 141 to `new_version`. The setting is reported to the user as 142 added, changed, or unchanged based on the current mapping in 143 the AFE. 144 145 This operation honors `self._dry_run`. 146 147 @param key Board or model key for assignment. 148 @param new_version Version to be assigned to `key`. 149 """ 150 old_version = self.get_mapping(key) 151 if old_version is None: 152 print('%s -> %s' % ( 153 self._format_operation('Adding', key), new_version)) 154 elif old_version != new_version: 155 print('%s -> %s to %s' % ( 156 self._format_operation('Updating', key), 157 old_version, new_version)) 158 else: 159 print('%s -> %s' % ( 160 self._format_operation('Unchanged', key), old_version)) 161 if not self._dry_run and old_version != new_version: 162 self._version_map.set_version(key, new_version) 163 164 def delete_mapping(self, key): 165 """Delete the mapping for `key`, and report the action. 166 167 The mapping for the board or model specifed by `key` is removed 168 from `self._version_map`. The change is reported to the user. 169 170 Requests to delete non-existent keys are ignored. 171 172 This operation honors `self._dry_run`. 173 174 @param key Board or model key to be deleted. 175 """ 176 version = self.get_mapping(key) 177 if version is not None: 178 print('%s -> %s' % ( 179 self._format_operation('Delete', key), version)) 180 if not self._dry_run: 181 self._version_map.delete_version(key) 182 else: 183 print(self._format_operation('Unmapped', key)) 184 185 186class _FirmwareVersionMapHandler(_VersionMapHandler): 187 TYPE = frontend.AFE.FIRMWARE_IMAGE_TYPE 188 DESCRIPTION = 'Firmware' 189 190 191class _CrOSVersionMapHandler(_VersionMapHandler): 192 TYPE = frontend.AFE.CROS_IMAGE_TYPE 193 DESCRIPTION = 'ChromeOS' 194 195 def set_mapping(self, board, version): 196 """Assign the ChromeOS mapping for the given board. 197 198 This function assigns the given ChromeOS version to the given 199 board. Additionally, for any model with firmware bundled in the 200 assigned build, that model will be assigned the firmware version 201 found for it in the build. 202 203 @param board ChromeOS board to be assigned a new version. 204 @param version New ChromeOS version to be assigned to the 205 board. 206 """ 207 new_version = build_data.get_omaha_upgrade( 208 build_data.get_omaha_version_map(), board, version) 209 if new_version != version: 210 print('Force %s version from Omaha: %-12s -> %s' % ( 211 self._description, board, new_version)) 212 super(_CrOSVersionMapHandler, self).set_mapping(board, new_version) 213 fw_versions = build_data.get_firmware_versions(board, new_version) 214 fw_handler = _FirmwareVersionMapHandler(self._afe, self._dry_run) 215 for model, fw_version in fw_versions.iteritems(): 216 if fw_version is not None: 217 fw_handler.set_mapping(model, fw_version) 218 219 def delete_mapping(self, board): 220 """Delete the ChromeOS mapping for the given board. 221 222 This function handles deletes the ChromeOS version mapping for the 223 given board. Additionally, any R/W firmware mapping that existed 224 because of the OS mapping will be deleted as well. 225 226 @param board ChromeOS board to be deleted from the mapping. 227 """ 228 version = self.get_mapping(board) 229 super(_CrOSVersionMapHandler, self).delete_mapping(board) 230 fw_versions = build_data.get_firmware_versions(board, version) 231 fw_handler = _FirmwareVersionMapHandler(self._afe, self._dry_run) 232 for model in fw_versions.iterkeys(): 233 fw_handler.delete_mapping(model) 234 235 236class _FAFTVersionMapHandler(_VersionMapHandler): 237 TYPE = frontend.AFE.FAFT_IMAGE_TYPE 238 DESCRIPTION = 'FAFT' 239 240 241_IMAGE_TYPE_CLASSES = [ 242 _CrOSVersionMapHandler, 243 _FirmwareVersionMapHandler, 244 _FAFTVersionMapHandler, 245] 246_ALL_IMAGE_TYPES = [cls.TYPE for cls in _IMAGE_TYPE_CLASSES] 247_IMAGE_TYPE_HANDLERS = {cls.TYPE: cls for cls in _IMAGE_TYPE_CLASSES} 248 249 250def _create_version_map_handler(image_type, afe, dry_run): 251 return _IMAGE_TYPE_HANDLERS[image_type](afe, dry_run) 252 253 254def _requested_mapping_handlers(afe, image_type): 255 """Iterate through the image types for a listing operation. 256 257 When listing all mappings, or when listing by board, the listing can 258 be either for all available image types, or just for a single type 259 requested on the command line. 260 261 This function takes the value of the `-t` option, and yields a 262 `_VersionMapHandler` object for either the single requested type, or 263 for all of the types. 264 265 @param afe AFE RPC interface object; created from SERVER. 266 @param image_type Argument to the `-t` option. A non-empty string 267 indicates a single image type; value of `None` 268 indicates all types. 269 """ 270 if image_type: 271 yield _create_version_map_handler(image_type, afe, True) 272 else: 273 for cls in _IMAGE_TYPE_CLASSES: 274 yield cls(afe, True) 275 276 277def list_all_mappings(afe, image_type): 278 """List all mappings in the AFE. 279 280 This function handles the following syntax usage case: 281 282 stable_version [-w SERVER] [-t TYPE] 283 284 @param afe AFE RPC interface object; created from SERVER. 285 @param image_type Argument to the `-t` option. 286 """ 287 need_newline = False 288 for handler in _requested_mapping_handlers(afe, image_type): 289 if need_newline: 290 print() 291 handler.print_all_mappings() 292 need_newline = True 293 294 295def list_mapping_by_key(afe, image_type, key): 296 """List all mappings for the given board or model. 297 298 This function handles the following syntax usage case: 299 300 stable_version [-w SERVER] [-t TYPE] BOARD/MODEL 301 302 @param afe AFE RPC interface object; created from SERVER. 303 @param image_type Argument to the `-t` option. 304 @param key Value of the BOARD/MODEL argument. 305 """ 306 for handler in _requested_mapping_handlers(afe, image_type): 307 handler.print_mapping(key) 308 309 310def _validate_set_mapping(arguments): 311 """Validate syntactic requirements to assign a mapping. 312 313 The given arguments specified assigning version to be assigned to 314 a board or model; check the arguments for errors that can't be 315 discovered by `ArgumentParser`. Errors are reported by raising 316 `_CommandError`. 317 318 @param arguments `Namespace` object returned from argument parsing. 319 """ 320 if not arguments.type: 321 raise _CommandError('The -t/--type option is required to assign a ' 322 'version') 323 if arguments.type == _FirmwareVersionMapHandler.TYPE: 324 msg = ('Cannot assign %s versions directly; ' 325 'must assign the %s version instead.') 326 descriptions = (_FirmwareVersionMapHandler.DESCRIPTION, 327 _CrOSVersionMapHandler.DESCRIPTION) 328 raise _CommandError(msg % descriptions) 329 330 331def set_mapping(afe, image_type, key, version, dry_run): 332 """Assign a version mapping to the given board or model. 333 334 This function handles the following syntax usage case: 335 336 stable_version [-w SERVER] [-n] -t TYPE BOARD/MODEL VERSION 337 338 @param afe AFE RPC interface object; created from SERVER. 339 @param image_type Argument to the `-t` option. 340 @param key Value of the BOARD/MODEL argument. 341 @param key Value of the VERSION argument. 342 @param dry_run Whether the `-n` option was supplied. 343 """ 344 if dry_run: 345 print('Dry run; no mappings will be changed.') 346 handler = _create_version_map_handler(image_type, afe, dry_run) 347 handler.set_mapping(key, version) 348 349 350def _validate_delete_mapping(arguments): 351 """Validate syntactic requirements to delete a mapping. 352 353 The given arguments specified the `-d` / `--delete` option; check 354 the arguments for errors that can't be discovered by 355 `ArgumentParser`. Errors are reported by raising `_CommandError`. 356 357 @param arguments `Namespace` object returned from argument parsing. 358 """ 359 if arguments.key is None: 360 raise _CommandError('Must specify BOARD_OR_MODEL argument ' 361 'with -d/--delete') 362 if arguments.version is not None: 363 raise _CommandError('Cannot specify VERSION argument with ' 364 '-d/--delete') 365 if not arguments.type: 366 raise _CommandError('-t/--type required with -d/--delete option') 367 368 369def delete_mapping(afe, image_type, key, dry_run): 370 """Delete the version mapping for the given board or model. 371 372 This function handles the following syntax usage case: 373 374 stable_version [-w SERVER] [-n] -t TYPE -d BOARD/MODEL 375 376 @param afe AFE RPC interface object; created from SERVER. 377 @param image_type Argument to the `-t` option. 378 @param key Value of the BOARD/MODEL argument. 379 @param dry_run Whether the `-n` option was supplied. 380 """ 381 if dry_run: 382 print('Dry run; no mappings will be deleted.') 383 handler = _create_version_map_handler(image_type, afe, dry_run) 384 handler.delete_mapping(key) 385 386 387def _parse_args(argv): 388 """Parse the given arguments according to the command syntax. 389 390 @param argv Full argument vector, with argv[0] being the command 391 name. 392 """ 393 parser = argparse.ArgumentParser( 394 prog=os.path.basename(argv[0]), 395 description='Set and view software version assignments') 396 parser.add_argument('-w', '--web', default=None, 397 metavar='SERVER', 398 help='Specify the AFE to query.') 399 parser.add_argument('-n', '--dry-run', action='store_true', 400 help='Report what would be done without making ' 401 'changes.') 402 parser.add_argument('-t', '--type', default=None, 403 choices=_ALL_IMAGE_TYPES, 404 help='Specify type of software image to be assigned.') 405 parser.add_argument('-d', '--delete', action='store_true', 406 help='Delete the BOARD_OR_MODEL argument from the ' 407 'mappings.') 408 parser.add_argument('key', nargs='?', metavar='BOARD_OR_MODEL', 409 help='Board, model, or other key for which to get or ' 410 'set a version') 411 parser.add_argument('version', nargs='?', metavar='VERSION', 412 help='Version to be assigned') 413 return parser.parse_args(argv[1:]) 414 415 416def _dispatch_command(afe, arguments): 417 if arguments.delete: 418 _validate_delete_mapping(arguments) 419 delete_mapping(afe, arguments.type, arguments.key, 420 arguments.dry_run) 421 elif arguments.key is None: 422 list_all_mappings(afe, arguments.type) 423 elif arguments.version is None: 424 list_mapping_by_key(afe, arguments.type, arguments.key) 425 else: 426 _validate_set_mapping(arguments) 427 set_mapping(afe, arguments.type, arguments.key, 428 arguments.version, arguments.dry_run) 429 430 431def main(argv): 432 """Standard main routine. 433 434 @param argv Command line arguments including `sys.argv[0]`. 435 """ 436 arguments = _parse_args(argv) 437 afe = frontend.AFE(server=arguments.web) 438 try: 439 _dispatch_command(afe, arguments) 440 except _CommandError as exc: 441 print('Error: %s' % str(exc), file=sys.stderr) 442 sys.exit(1) 443 444 445if __name__ == '__main__': 446 try: 447 main(sys.argv) 448 except KeyboardInterrupt: 449 pass 450