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