1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright 2019 The ChromiumOS Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Modifies a tryjob based off of arguments.""" 8 9 10import argparse 11import enum 12import json 13import os 14import sys 15 16import chroot 17import failure_modes 18import get_llvm_hash 19import update_chromeos_llvm_hash 20import update_packages_and_run_tests 21import update_tryjob_status 22 23 24class ModifyTryjob(enum.Enum): 25 """Options to modify a tryjob.""" 26 27 REMOVE = "remove" 28 RELAUNCH = "relaunch" 29 ADD = "add" 30 31 32def GetCommandLineArgs(): 33 """Parses the command line for the command line arguments.""" 34 35 # Default path to the chroot if a path is not specified. 36 cros_root = os.path.expanduser("~") 37 cros_root = os.path.join(cros_root, "chromiumos") 38 39 # Create parser and add optional command-line arguments. 40 parser = argparse.ArgumentParser( 41 description="Removes, relaunches, or adds a tryjob." 42 ) 43 44 # Add argument for the JSON file to use for the update of a tryjob. 45 parser.add_argument( 46 "--status_file", 47 required=True, 48 help="The absolute path to the JSON file that contains the tryjobs used " 49 "for bisecting LLVM.", 50 ) 51 52 # Add argument that determines what action to take on the revision specified. 53 parser.add_argument( 54 "--modify_tryjob", 55 required=True, 56 choices=[modify_tryjob.value for modify_tryjob in ModifyTryjob], 57 help="What action to perform on the tryjob.", 58 ) 59 60 # Add argument that determines which revision to search for in the list of 61 # tryjobs. 62 parser.add_argument( 63 "--revision", 64 required=True, 65 type=int, 66 help="The revision to either remove or relaunch.", 67 ) 68 69 # Add argument for other change lists that want to run alongside the tryjob. 70 parser.add_argument( 71 "--extra_change_lists", 72 type=int, 73 nargs="+", 74 help="change lists that would like to be run alongside the change list " 75 "of updating the packages", 76 ) 77 78 # Add argument for custom options for the tryjob. 79 parser.add_argument( 80 "--options", 81 required=False, 82 nargs="+", 83 help="options to use for the tryjob testing", 84 ) 85 86 # Add argument for the builder to use for the tryjob. 87 parser.add_argument( 88 "--builder", help="builder to use for the tryjob testing" 89 ) 90 91 # Add argument for a specific chroot path. 92 parser.add_argument( 93 "--chroot_path", 94 default=cros_root, 95 help="the path to the chroot (default: %(default)s)", 96 ) 97 98 # Add argument for whether to display command contents to `stdout`. 99 parser.add_argument( 100 "--verbose", 101 action="store_true", 102 help="display contents of a command to the terminal " 103 "(default: %(default)s)", 104 ) 105 106 args_output = parser.parse_args() 107 108 if not os.path.isfile( 109 args_output.status_file 110 ) or not args_output.status_file.endswith(".json"): 111 raise ValueError( 112 'File does not exist or does not ending in ".json" ' 113 ": %s" % args_output.status_file 114 ) 115 116 if ( 117 args_output.modify_tryjob == ModifyTryjob.ADD.value 118 and not args_output.builder 119 ): 120 raise ValueError("A builder is required for adding a tryjob.") 121 elif ( 122 args_output.modify_tryjob != ModifyTryjob.ADD.value 123 and args_output.builder 124 ): 125 raise ValueError( 126 "Specifying a builder is only available when adding a " "tryjob." 127 ) 128 129 return args_output 130 131 132def GetCLAfterUpdatingPackages( 133 packages, 134 git_hash, 135 svn_version, 136 chroot_path, 137 patch_metadata_file, 138 svn_option, 139): 140 """Updates the packages' LLVM_NEXT.""" 141 142 change_list = update_chromeos_llvm_hash.UpdatePackages( 143 packages=packages, 144 manifest_packages=[], 145 llvm_variant=update_chromeos_llvm_hash.LLVMVariant.next, 146 git_hash=git_hash, 147 svn_version=svn_version, 148 chroot_path=chroot_path, 149 mode=failure_modes.FailureModes.DISABLE_PATCHES, 150 git_hash_source=svn_option, 151 extra_commit_msg=None, 152 ) 153 154 print("\nSuccessfully updated packages to %d" % svn_version) 155 print("Gerrit URL: %s" % change_list.url) 156 print("Change list number: %d" % change_list.cl_number) 157 158 return change_list 159 160 161def CreateNewTryjobEntryForBisection( 162 cl, extra_cls, options, builder, chroot_path, cl_url, revision 163): 164 """Submits a tryjob and adds additional information.""" 165 166 # Get the tryjob results after submitting the tryjob. 167 # Format of 'tryjob_results': 168 # [ 169 # { 170 # 'link' : [TRYJOB_LINK], 171 # 'buildbucket_id' : [BUILDBUCKET_ID], 172 # 'extra_cls' : [EXTRA_CLS_LIST], 173 # 'options' : [EXTRA_OPTIONS_LIST], 174 # 'builder' : [BUILDER_AS_A_LIST] 175 # } 176 # ] 177 tryjob_results = update_packages_and_run_tests.RunTryJobs( 178 cl, extra_cls, options, [builder], chroot_path 179 ) 180 print("\nTryjob:") 181 print(tryjob_results[0]) 182 183 # Add necessary information about the tryjob. 184 tryjob_results[0]["url"] = cl_url 185 tryjob_results[0]["rev"] = revision 186 tryjob_results[0][ 187 "status" 188 ] = update_tryjob_status.TryjobStatus.PENDING.value 189 tryjob_results[0]["cl"] = cl 190 191 return tryjob_results[0] 192 193 194def AddTryjob( 195 packages, 196 git_hash, 197 revision, 198 chroot_path, 199 patch_metadata_file, 200 extra_cls, 201 options, 202 builder, 203 verbose, 204 svn_option, 205): 206 """Submits a tryjob.""" 207 208 update_chromeos_llvm_hash.verbose = verbose 209 210 change_list = GetCLAfterUpdatingPackages( 211 packages, 212 git_hash, 213 revision, 214 chroot_path, 215 patch_metadata_file, 216 svn_option, 217 ) 218 219 tryjob_dict = CreateNewTryjobEntryForBisection( 220 change_list.cl_number, 221 extra_cls, 222 options, 223 builder, 224 chroot_path, 225 change_list.url, 226 revision, 227 ) 228 229 return tryjob_dict 230 231 232def PerformTryjobModification( 233 revision, 234 modify_tryjob, 235 status_file, 236 extra_cls, 237 options, 238 builder, 239 chroot_path, 240 verbose, 241): 242 """Removes, relaunches, or adds a tryjob. 243 244 Args: 245 revision: The revision associated with the tryjob. 246 modify_tryjob: What action to take on the tryjob. 247 Ex: ModifyTryjob.REMOVE, ModifyTryjob.RELAUNCH, ModifyTryjob.ADD 248 status_file: The .JSON file that contains the tryjobs. 249 extra_cls: Extra change lists to be run alongside tryjob 250 options: Extra options to pass into 'cros tryjob'. 251 builder: The builder to use for 'cros tryjob'. 252 chroot_path: The absolute path to the chroot (used by 'cros tryjob' when 253 relaunching a tryjob). 254 verbose: Determines whether to print the contents of a command to `stdout`. 255 """ 256 257 # Format of 'bisect_contents': 258 # { 259 # 'start': [START_REVISION_OF_BISECTION] 260 # 'end': [END_REVISION_OF_BISECTION] 261 # 'jobs' : [ 262 # {[TRYJOB_INFORMATION]}, 263 # {[TRYJOB_INFORMATION]}, 264 # ..., 265 # {[TRYJOB_INFORMATION]} 266 # ] 267 # } 268 with open(status_file) as tryjobs: 269 bisect_contents = json.load(tryjobs) 270 271 if not bisect_contents["jobs"] and modify_tryjob != ModifyTryjob.ADD: 272 sys.exit("No tryjobs in %s" % status_file) 273 274 tryjob_index = update_tryjob_status.FindTryjobIndex( 275 revision, bisect_contents["jobs"] 276 ) 277 278 # 'FindTryjobIndex()' returns None if the tryjob was not found. 279 if tryjob_index is None and modify_tryjob != ModifyTryjob.ADD: 280 raise ValueError( 281 "Unable to find tryjob for %d in %s" % (revision, status_file) 282 ) 283 284 # Determine the action to take based off of 'modify_tryjob'. 285 if modify_tryjob == ModifyTryjob.REMOVE: 286 del bisect_contents["jobs"][tryjob_index] 287 288 print("Successfully deleted the tryjob of revision %d" % revision) 289 elif modify_tryjob == ModifyTryjob.RELAUNCH: 290 # Need to update the tryjob link and buildbucket ID. 291 tryjob_results = update_packages_and_run_tests.RunTryJobs( 292 bisect_contents["jobs"][tryjob_index]["cl"], 293 bisect_contents["jobs"][tryjob_index]["extra_cls"], 294 bisect_contents["jobs"][tryjob_index]["options"], 295 bisect_contents["jobs"][tryjob_index]["builder"], 296 chroot_path, 297 ) 298 299 bisect_contents["jobs"][tryjob_index][ 300 "status" 301 ] = update_tryjob_status.TryjobStatus.PENDING.value 302 bisect_contents["jobs"][tryjob_index]["link"] = tryjob_results[0][ 303 "link" 304 ] 305 bisect_contents["jobs"][tryjob_index][ 306 "buildbucket_id" 307 ] = tryjob_results[0]["buildbucket_id"] 308 309 print( 310 "Successfully relaunched the tryjob for revision %d and updated " 311 "the tryjob link to %s" % (revision, tryjob_results[0]["link"]) 312 ) 313 elif modify_tryjob == ModifyTryjob.ADD: 314 # Tryjob exists already. 315 if tryjob_index is not None: 316 raise ValueError( 317 "Tryjob already exists (index is %d) in %s." 318 % (tryjob_index, status_file) 319 ) 320 321 # Make sure the revision is within the bounds of the start and end of the 322 # bisection. 323 elif bisect_contents["start"] < revision < bisect_contents["end"]: 324 325 patch_metadata_file = "PATCHES.json" 326 327 ( 328 git_hash, 329 revision, 330 ) = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption(revision) 331 332 tryjob_dict = AddTryjob( 333 update_chromeos_llvm_hash.DEFAULT_PACKAGES, 334 git_hash, 335 revision, 336 chroot_path, 337 patch_metadata_file, 338 extra_cls, 339 options, 340 builder, 341 verbose, 342 revision, 343 ) 344 345 bisect_contents["jobs"].append(tryjob_dict) 346 347 print("Successfully added tryjob of revision %d" % revision) 348 else: 349 raise ValueError("Failed to add tryjob to %s" % status_file) 350 else: 351 raise ValueError( 352 'Invalid "modify_tryjob" option provided: %s' % modify_tryjob 353 ) 354 355 with open(status_file, "w") as update_tryjobs: 356 json.dump( 357 bisect_contents, update_tryjobs, indent=4, separators=(",", ": ") 358 ) 359 360 361def main(): 362 """Removes, relaunches, or adds a tryjob.""" 363 364 chroot.VerifyOutsideChroot() 365 366 args_output = GetCommandLineArgs() 367 368 PerformTryjobModification( 369 args_output.revision, 370 ModifyTryjob(args_output.modify_tryjob), 371 args_output.status_file, 372 args_output.extra_change_lists, 373 args_output.options, 374 args_output.builder, 375 args_output.chroot_path, 376 args_output.verbose, 377 ) 378 379 380if __name__ == "__main__": 381 main() 382