xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 1 | # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Module for ChromeOS & Android build related logic in suite scheduler.""" |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 6 | # pylint: disable=g-bad-import-order |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 7 | |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 8 | from distutils import version |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 9 | import collections |
Xixuan Wu | 244e0ec | 2018-05-23 14:49:55 -0700 | [diff] [blame] | 10 | import json |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 11 | import logging |
| 12 | import re |
| 13 | |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 14 | import apiclient |
| 15 | |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 16 | # Bare branches |
| 17 | BARE_BRANCHES = ['factory', 'firmware'] |
| 18 | |
| 19 | # Definition of os types. |
| 20 | OS_TYPE_CROS = 'cros' |
| 21 | OS_TYPE_BRILLO = 'brillo' |
| 22 | OS_TYPE_ANDROID = 'android' |
| 23 | OS_TYPES = [OS_TYPE_CROS, OS_TYPE_BRILLO, OS_TYPE_ANDROID] |
| 24 | OS_TYPES_LAUNCH_CONTROL = [OS_TYPE_BRILLO, OS_TYPE_ANDROID] |
| 25 | |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 26 | # Launch control build's target's information |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 27 | LaunchControlBuildTargetInfo = collections.namedtuple( |
| 28 | 'LaunchControlBuildTargetInfo', |
| 29 | [ |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 30 | 'target', |
| 31 | 'type', |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 32 | ]) |
| 33 | |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 34 | # ChromeOS build config's information |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 35 | CrOSBuildConfigInfo = collections.namedtuple( |
| 36 | 'CrOSBuildConfigInfo', |
| 37 | [ |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 38 | 'board', |
| 39 | 'type', |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 40 | ]) |
| 41 | |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 42 | # The default build type for fetching latest build. |
Xixuan Wu | 7899db5 | 2018-05-29 14:19:06 -0700 | [diff] [blame] | 43 | _DEFAULT_BUILD_SUFFIX = '-release' |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 44 | |
| 45 | # The default setting of board for fetching latest build. |
| 46 | _DEFAULT_MASTER = 'master' |
| 47 | |
| 48 | # The path for storing the latest build. |
| 49 | _GS_LATEST_MASTER_PATTERN = '%(board)s%(suffix)s/%(name)s' |
| 50 | |
| 51 | # The gs bucket to fetch the latest build. |
| 52 | _GS_BUCKET = 'chromeos-image-archive' |
| 53 | |
| 54 | # The file in Google Storage to fetch the latest build. |
| 55 | _LATEST_MASTER = 'LATEST-master' |
| 56 | |
Xixuan Wu | 244e0ec | 2018-05-23 14:49:55 -0700 | [diff] [blame] | 57 | # The bucket for ChromeOS build release boards. Maintained by GoldenEye. |
| 58 | _GE_RELEASE_BUILD_BUCKET = 'chromeos-build-release-console' |
| 59 | |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 60 | # Special android target to board map. |
| 61 | _ANDROID_TARGET_TO_BOARD_MAP = { |
| 62 | 'seed_l8150': 'gm4g_sprout', |
| 63 | 'bat_land': 'bat' |
| 64 | } |
| 65 | |
| 66 | # CrOS build name patter |
| 67 | _CROS_BUILD_PATTERN = '%(board)s-%(build_type)s/R%(milestone)s-%(manifest)s' |
| 68 | |
| 69 | # Android build name pattern |
| 70 | _ANDROID_BUILD_PATTERN = '%(branch)s/%(target)s/%(build_id)s' |
| 71 | |
| 72 | # The pattern for Launch Control target |
| 73 | _LAUNCH_CONTROL_TARGET_PATTERN = r'(?P<build_target>.+)-(?P<build_type>[^-]+)' |
| 74 | |
| 75 | # The pattern for CrOS build config |
| 76 | _CROS_BUILD_CONFIG_PATTERN = r'-([^-]+)(?:-group)?' |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 77 | |
| 78 | |
| 79 | class NoBuildError(Exception): |
| 80 | """Raised when failing to get the required build from Google Storage.""" |
| 81 | |
| 82 | |
| 83 | class BuildType(object): |
| 84 | """Representing the type of test source build. |
| 85 | |
| 86 | This is used to identify the test source build for testing. |
| 87 | """ |
| 88 | FIRMWARE_RW = 'firmware_rw' |
| 89 | FIRMWARE_RO = 'firmware_ro' |
| 90 | CROS = 'cros' |
| 91 | |
| 92 | |
| 93 | class BuildVersionKey(object): |
| 94 | """Keys referring to the builds to install in run_suites.""" |
| 95 | |
| 96 | CROS_VERSION = 'cros_version' |
| 97 | ANDROID_BUILD_VERSION = 'android_version' |
| 98 | TESTBED_BUILD_VERSION = 'testbed_version' |
| 99 | FW_RW_VERSION = 'fwrw_version' |
| 100 | FW_RO_VERSION = 'fwro_version' |
| 101 | |
| 102 | |
| 103 | class AndroidBuild(collections.namedtuple( |
| 104 | '_AndroidBuildBase', ['branch', 'target', 'build_id']), object): |
| 105 | """Class for constructing android build string.""" |
| 106 | |
| 107 | def __str__(self): |
| 108 | return _ANDROID_BUILD_PATTERN % {'branch': self.branch, |
| 109 | 'target': self.target, |
| 110 | 'build_id': self.build_id} |
| 111 | |
| 112 | |
| 113 | class CrOSBuild(collections.namedtuple( |
| 114 | '_CrOSBuildBase', |
| 115 | ['board', 'build_type', 'milestone', 'manifest']), object): |
| 116 | """Class for constructing ChromeOS build string.""" |
| 117 | |
| 118 | def __str__(self): |
| 119 | return _CROS_BUILD_PATTERN % {'board': self.board, |
| 120 | 'build_type': self.build_type, |
| 121 | 'milestone': self.milestone, |
| 122 | 'manifest': self.manifest} |
| 123 | |
| 124 | |
| 125 | def get_latest_cros_build_from_gs(storage_client, board=None, suffix=None): |
| 126 | """Get latest build for given board from Google Storage. |
| 127 | |
| 128 | Args: |
| 129 | storage_client: a rest_client.StorageRestClient object. |
| 130 | board: the board to fetch latest build. Default is 'master'. |
| 131 | suffix: suffix represents build channel, like '-release'. |
| 132 | Default is '-paladin'. |
| 133 | |
| 134 | Returns: |
| 135 | a ChromeOS version string, e.g. '59.0.000.0'. |
| 136 | |
| 137 | Raises: |
| 138 | HttpError if error happens in interacting with Google Storage. |
| 139 | """ |
| 140 | board = board if board is not None else _DEFAULT_MASTER |
| 141 | suffix = suffix if suffix is not None else _DEFAULT_BUILD_SUFFIX |
| 142 | file_to_check = _GS_LATEST_MASTER_PATTERN % { |
| 143 | 'board': board, |
| 144 | 'suffix': suffix, |
| 145 | 'name': _LATEST_MASTER} |
| 146 | |
| 147 | try: |
| 148 | return storage_client.read_object(_GS_BUCKET, file_to_check) |
| 149 | except apiclient.errors.HttpError as e: |
| 150 | raise NoBuildError( |
| 151 | 'Cannot find latest build for board %s, suffix %s: %s' % |
| 152 | (board, suffix, str(e))) |
| 153 | |
| 154 | |
Xixuan Wu | 244e0ec | 2018-05-23 14:49:55 -0700 | [diff] [blame] | 155 | def get_board_family_mapping_from_gs(storage_client): |
| 156 | """Get board_family to boards mapping from Google Storage. |
| 157 | |
| 158 | Args: |
| 159 | storage_client: a rest_client.StorageRestClient object. |
| 160 | |
| 161 | Returns: |
| 162 | a dictionary of mapping between board family name to boards, e.g. |
| 163 | {'nyan': ['nyan', 'nyan_big', 'nyan_blaze', ..]} |
| 164 | |
| 165 | Raises: |
| 166 | HttpError if error happens in interacting with Google Storage. |
| 167 | """ |
| 168 | try: |
| 169 | boards = storage_client.read_object(_GE_RELEASE_BUILD_BUCKET, 'boards.json') |
| 170 | json_object = json.loads(boards) |
| 171 | board_family = {} |
| 172 | for board in json_object['boards']: |
| 173 | group = board['reference_group'] |
| 174 | if not group: |
| 175 | continue |
| 176 | |
| 177 | # This is to change boards like nyan-blaze to nyan_blaze, which is |
| 178 | # actually used in lab. |
| 179 | board_name = board['public_codename'].replace('-', '_') |
| 180 | if group not in board_family: |
| 181 | board_family[group] = [] |
| 182 | |
| 183 | board_family[group].append(board_name) |
| 184 | |
| 185 | logging.info('Successfully get following board families from GS: %r', |
| 186 | board_family.keys()) |
| 187 | return board_family |
| 188 | except apiclient.errors.HttpError as e: |
| 189 | logging.error('Cannot load boards.json in bucket %s: %s', |
| 190 | _GE_RELEASE_BUILD_BUCKET, str(e)) |
| 191 | raise |
| 192 | |
| 193 | |
Craig Bergstrom | 58263d3 | 2018-04-26 14:11:35 -0600 | [diff] [blame] | 194 | def buildinfo_list_to_branch_build_dict(cros_board_list, buildinfo_list): |
| 195 | """Validate and convert a list of BuildInfo to branch build dict. |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 196 | |
| 197 | Args: |
Xixuan Wu | 6fb1627 | 2017-10-19 13:16:00 -0700 | [diff] [blame] | 198 | cros_board_list: The board list including all CrOS boards. |
Craig Bergstrom | 58263d3 | 2018-04-26 14:11:35 -0600 | [diff] [blame] | 199 | buildinfo_list: A list of BuildInfo objects obtained from CIDB. |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 200 | |
| 201 | Returns: |
Craig Bergstrom | 58263d3 | 2018-04-26 14:11:35 -0600 | [diff] [blame] | 202 | A branch build dict: |
| 203 | key: a tuple of (board, build_type, milestone), like: |
| 204 | ('wolf', 'release', '58') |
| 205 | value: the latest manifest for the given tuple, like: |
| 206 | '9242.0.0'. |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 207 | """ |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 208 | branch_build_dict = {} |
Craig Bergstrom | 58263d3 | 2018-04-26 14:11:35 -0600 | [diff] [blame] | 209 | for build in buildinfo_list: |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 210 | try: |
| 211 | build_config_info = parse_cros_build_config(build.board, |
| 212 | build.build_config) |
| 213 | except ValueError as e: |
| 214 | logging.warning('Failed to parse build config: %s: %s', |
| 215 | build.build_config, e) |
| 216 | continue |
| 217 | |
| 218 | if build.board != build_config_info.board: |
| 219 | logging.warning('Non-matched build_config and board: %s, %s', |
| 220 | build.board, build.board) |
| 221 | continue |
| 222 | |
Xixuan Wu | 6fb1627 | 2017-10-19 13:16:00 -0700 | [diff] [blame] | 223 | if build.board not in cros_board_list: |
| 224 | logging.warning('%s is not a valid CrOS board.', build.board) |
| 225 | continue |
| 226 | |
Xixuan Wu | 8d2f286 | 2018-08-28 16:48:04 -0700 | [diff] [blame] | 227 | build_key = (build.board, build.model, build_config_info.type, |
| 228 | build.milestone) |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 229 | cur_manifest = branch_build_dict.get(build_key) |
| 230 | if cur_manifest is not None: |
| 231 | branch_build_dict[build_key] = max( |
| 232 | [cur_manifest, build.platform], key=version.LooseVersion) |
| 233 | else: |
| 234 | branch_build_dict[build_key] = build.platform |
| 235 | |
| 236 | return branch_build_dict |
| 237 | |
| 238 | |
Craig Bergstrom | 58263d3 | 2018-04-26 14:11:35 -0600 | [diff] [blame] | 239 | def get_cros_builds_since_date_from_db(db_client, cros_board_list, since_date): |
| 240 | """Get branch builds for ChromeOS boards. |
| 241 | |
| 242 | Args: |
| 243 | db_client: a cloud_sql_client.CIDBClient object, to read cidb |
| 244 | build infos. |
| 245 | cros_board_list: The board list including all CrOS boards. |
| 246 | since_date: a datetime.datetime object in UTC to indicate since when CrOS |
| 247 | builds will be fetched. |
| 248 | |
| 249 | Returns: |
| 250 | a two-tuple of branch build dicts. Each branch build dict has |
| 251 | keys and value described below. The first build_dict describes |
| 252 | successful builds and the second describes builds that failed |
| 253 | but met the relaxed success requirement (a successful 'HWTest [sanity]' |
| 254 | stage). |
| 255 | key: a tuple of (board, build_type, milestone), like: |
| 256 | ('wolf', 'release', '58') |
| 257 | value: the latest manifest for the given tuple, like: |
| 258 | '9242.0.0'. |
| 259 | """ |
| 260 | # CIDB use UTC timezone |
| 261 | all_branch_builds = db_client.get_passed_builds_since_date(since_date) |
| 262 | relaxed_builds = db_client.get_relaxed_pased_builds_since_date(since_date) |
| 263 | |
| 264 | branch_builds_dict = buildinfo_list_to_branch_build_dict( |
| 265 | cros_board_list, all_branch_builds) |
| 266 | relaxed_builds_dict = buildinfo_list_to_branch_build_dict( |
Xixuan Wu | 384f913 | 2018-08-28 15:48:26 -0700 | [diff] [blame] | 267 | cros_board_list, relaxed_builds + all_branch_builds) |
Craig Bergstrom | 58263d3 | 2018-04-26 14:11:35 -0600 | [diff] [blame] | 268 | |
| 269 | return branch_builds_dict, relaxed_builds_dict |
| 270 | |
| 271 | |
Xixuan Wu | 5d6063e | 2017-09-05 16:15:07 -0700 | [diff] [blame] | 272 | def get_latest_launch_control_build(android_client, branch, target): |
| 273 | """Get the latest launch control build from Android Build API. |
| 274 | |
| 275 | Args: |
| 276 | android_client: a rest_client.AndroidBuildRestClient object. |
| 277 | branch: the launch control branch. |
| 278 | target: the launch control target. |
| 279 | |
| 280 | Returns: |
| 281 | a string latest launch control build id. |
| 282 | |
| 283 | Raises: |
| 284 | NoBuildError if no latest launch control build is found. |
| 285 | """ |
| 286 | try: |
| 287 | latest_build_id = android_client.get_latest_build_id(branch, target) |
| 288 | if latest_build_id is None: |
| 289 | raise NoBuildError('No latest builds is found.') |
| 290 | |
| 291 | return latest_build_id |
| 292 | except apiclient.errors.HttpError as e: |
| 293 | raise NoBuildError('HttpError happened in getting the latest launch ' |
| 294 | 'control build for ' |
| 295 | '%s,%s: %s' % (branch, target, str(e))) |
| 296 | |
| 297 | |
| 298 | def get_launch_control_builds_by_branch_targets( |
| 299 | android_client, android_board_list, launch_control_branch_targets): |
| 300 | """Get latest launch_control_builds for android boards. |
| 301 | |
| 302 | For every tasks in this event, if it has settings of launch control |
| 303 | branch & target, get the latest launch control build for it. |
| 304 | |
| 305 | Args: |
| 306 | android_client: a rest_client.AndroidBuildRestClient object to |
| 307 | interact with android build API. |
| 308 | android_board_list: a list of Android boards. |
| 309 | launch_control_branch_targets: a dict of branch:targets, see property |
| 310 | launch_control_branch_targets in base_event.py. |
| 311 | |
| 312 | Returns: |
| 313 | a launch control build dict: |
| 314 | key: an android board, like 'shamu'. |
| 315 | value: a list involves the latest builds for each pair |
| 316 | (branch, target) of this board, like: |
| 317 | [u'git_nyc-mr2-release/shamu-userdebug/3844975', |
| 318 | u'git_nyc-mr1-release/shamu-userdebug/3783920'] |
| 319 | """ |
| 320 | launch_control_dict = {} |
| 321 | board_to_builds_dict = {} |
| 322 | for branch, targets in launch_control_branch_targets.iteritems(): |
| 323 | for t in targets: |
| 324 | try: |
| 325 | board = parse_launch_control_target(t).target |
| 326 | except ValueError: |
| 327 | logging.warning( |
| 328 | 'Failed to parse launch control target: %s', t) |
| 329 | continue |
| 330 | |
| 331 | if board not in android_board_list: |
| 332 | continue |
| 333 | |
| 334 | # Use dict here to reduce the times to call AndroidBuild API |
| 335 | if launch_control_dict.get((branch, t)) is None: |
| 336 | try: |
| 337 | build_id = get_latest_launch_control_build( |
| 338 | android_client, branch, t) |
| 339 | except NoBuildError as e: |
| 340 | logging.warning(e) |
| 341 | continue |
| 342 | |
| 343 | build = str(AndroidBuild(branch, t, build_id)) |
| 344 | launch_control_dict[(branch, t)] = build |
| 345 | board_to_builds_dict.setdefault(board, []).append( |
| 346 | build) |
| 347 | |
| 348 | for board, in board_to_builds_dict.iteritems(): |
| 349 | mapped_board = get_board_by_android_target(board) |
| 350 | if mapped_board != board: |
| 351 | logging.debug('Map board %s to %s', board, mapped_board) |
| 352 | if board_to_builds_dict.get(mapped_board) is None: |
| 353 | del board_to_builds_dict[board] |
| 354 | else: |
| 355 | board_to_builds_dict[board] = board_to_builds_dict[ |
| 356 | mapped_board] |
| 357 | |
| 358 | return board_to_builds_dict |
| 359 | |
| 360 | |
| 361 | def parse_launch_control_target(target): |
| 362 | """Parse the build target and type from a Launch Control target. |
| 363 | |
| 364 | The Launch Control target has the format of build_target-build_type, e.g., |
| 365 | shamu-eng or dragonboard-userdebug. This method extracts the build target |
| 366 | and type from the target name. |
| 367 | |
| 368 | Args: |
| 369 | target: Name of a Launch Control target, e.g., shamu-userdebug. |
| 370 | |
| 371 | Returns: |
| 372 | a LaunchControlBuildTargetInfo object whose value is like |
| 373 | (target='shamu', |
| 374 | type='userdebug') |
| 375 | |
| 376 | Raises: |
| 377 | ValueError: if target is not valid. |
| 378 | """ |
| 379 | match = re.match(_LAUNCH_CONTROL_TARGET_PATTERN, target) |
| 380 | if not match: |
| 381 | raise ValueError('target format is not valid') |
| 382 | |
| 383 | return LaunchControlBuildTargetInfo(match.group('build_target'), |
| 384 | match.group('build_type')) |
| 385 | |
| 386 | |
| 387 | def parse_cros_build_config(board, build_config): |
| 388 | """Parse build_type from a given builder for a given board. |
| 389 | |
| 390 | Args: |
| 391 | board: the prefix of a ChromeOS build_config, representing board. |
| 392 | build_config: a ChromeOS build_config name, like 'kevin-release'. |
| 393 | |
| 394 | Returns: |
| 395 | a CrOSBuildConfigInfo object whose value is like |
| 396 | (board='kevin', |
| 397 | type='release') |
| 398 | |
| 399 | Raises: |
| 400 | ValueError: if build_config is in invalid form. |
| 401 | """ |
| 402 | if build_config[0:len(board)] != board: |
| 403 | raise ValueError('build_config cannot be parsed: %s' % build_config) |
| 404 | |
| 405 | match = re.match(_CROS_BUILD_CONFIG_PATTERN, build_config[len(board):]) |
| 406 | if not match: |
| 407 | raise ValueError('build_config %s is not matched %s' % ( |
| 408 | build_config, _CROS_BUILD_CONFIG_PATTERN)) |
| 409 | |
| 410 | return CrOSBuildConfigInfo(board, match.groups()[0]) |
| 411 | |
| 412 | |
| 413 | def get_board_by_android_target(target): |
| 414 | """Map a android target to a android board. |
| 415 | |
| 416 | # Mapping between an android board name and a build target. This is for |
| 417 | # special case handling for certain Android board that the board name and |
| 418 | # build target name does not match. |
| 419 | # This comes from server/site_utils.py in autotest module. |
| 420 | |
| 421 | Args: |
| 422 | target: an android target. |
| 423 | |
| 424 | Returns: |
| 425 | a string android board mapped by ANDROID_TARGET_TO_BOARD_MAP. |
| 426 | """ |
| 427 | return _ANDROID_TARGET_TO_BOARD_MAP.get(target, target) |