1#!/usr/bin/env python2 2"""Generate summary report for ChromeOS toolchain waterfalls.""" 3 4from __future__ import print_function 5 6import argparse 7import datetime 8import getpass 9import json 10import os 11import re 12import shutil 13import sys 14import time 15 16from cros_utils import command_executer 17 18# All the test suites whose data we might want for the reports. 19TESTS = (('bvt-inline', 'HWTest [bvt-inline]'), ('bvt-cq', 'HWTest [bvt-cq]'), 20 ('security', 'HWTest [security]')) 21 22# The main waterfall builders, IN THE ORDER IN WHICH WE WANT THEM 23# LISTED IN THE REPORT. 24WATERFALL_BUILDERS = [ 25 'amd64-llvm-next-toolchain', 26 'arm-llvm-next-toolchain', 27 'arm64-llvm-next-toolchain', 28] 29 30DATA_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-report-data/' 31ARCHIVE_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-reports/' 32DOWNLOAD_DIR = '/tmp/waterfall-logs' 33MAX_SAVE_RECORDS = 7 34BUILD_DATA_FILE = '%s/build-data.txt' % DATA_DIR 35LLVM_ROTATING_BUILDER = 'llvm_next_toolchain' 36ROTATING_BUILDERS = [LLVM_ROTATING_BUILDER] 37 38# For int-to-string date conversion. Note, the index of the month in this 39# list needs to correspond to the month's integer value. i.e. 'Sep' must 40# be as MONTHS[9]. 41MONTHS = [ 42 '', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 43 'Nov', 'Dec' 44] 45 46DAYS_PER_MONTH = { 47 1: 31, 48 2: 28, 49 3: 31, 50 4: 30, 51 5: 31, 52 6: 30, 53 7: 31, 54 8: 31, 55 9: 30, 56 10: 31, 57 11: 31, 58 12: 31 59} 60 61 62def format_date(int_date, use_int_month=False): 63 """Convert an integer date to a string date. YYYYMMDD -> YYYY-MMM-DD""" 64 65 if int_date == 0: 66 return 'today' 67 68 tmp_date = int_date 69 day = tmp_date % 100 70 tmp_date = tmp_date / 100 71 month = tmp_date % 100 72 year = tmp_date / 100 73 74 if use_int_month: 75 date_str = '%d-%02d-%02d' % (year, month, day) 76 else: 77 month_str = MONTHS[month] 78 date_str = '%d-%s-%d' % (year, month_str, day) 79 return date_str 80 81 82def EmailReport(report_file, report_type, date, email_to): 83 """Emails the report to the approprite address.""" 84 subject = '%s Waterfall Summary report, %s' % (report_type, date) 85 sendgmr_path = '/google/data/ro/projects/gws-sre/sendgmr' 86 command = ('%s --to=%s --subject="%s" --body_file=%s' % 87 (sendgmr_path, email_to, subject, report_file)) 88 command_executer.GetCommandExecuter().RunCommand(command) 89 90 91def GetColor(status): 92 """Given a job status string, returns appropriate color string.""" 93 if status.strip() == 'pass': 94 color = 'green ' 95 elif status.strip() == 'fail': 96 color = ' red ' 97 elif status.strip() == 'warning': 98 color = 'orange' 99 else: 100 color = ' ' 101 return color 102 103 104def GenerateWaterfallReport(report_dict, waterfall_type, date): 105 """Write out the actual formatted report.""" 106 107 filename = 'waterfall_report.%s_waterfall.%s.txt' % (waterfall_type, date) 108 109 date_string = '' 110 report_list = report_dict.keys() 111 112 with open(filename, 'w') as out_file: 113 # Write Report Header 114 out_file.write('\nStatus of %s Waterfall Builds from %s\n\n' % 115 (waterfall_type, date_string)) 116 out_file.write(' \n') 117 out_file.write( 118 ' Build bvt- ' 119 ' bvt-cq ' 120 ' security \n') 121 out_file.write( 122 ' status inline ' 123 ' \n') 124 125 # Write daily waterfall status section. 126 for builder in report_list: 127 build_dict = report_dict[builder] 128 buildbucket_id = build_dict['buildbucket_id'] 129 overall_status = build_dict['status'] 130 if 'bvt-inline' in build_dict.keys(): 131 inline_status = build_dict['bvt-inline'] 132 else: 133 inline_status = ' ' 134 if 'bvt-cq' in build_dict.keys(): 135 cq_status = build_dict['bvt-cq'] 136 else: 137 cq_status = ' ' 138 if 'security' in build_dict.keys(): 139 security_status = build_dict['security'] 140 else: 141 security_status = ' ' 142 inline_color = GetColor(inline_status) 143 cq_color = GetColor(cq_status) 144 security_color = GetColor(security_status) 145 146 out_file.write( 147 '%26s %4s %6s %6s %6s\n' % 148 (builder, overall_status, inline_color, cq_color, security_color)) 149 if waterfall_type == 'main': 150 out_file.write(' build url: https://cros-goldeneye.corp.google.com/' 151 'chromeos/healthmonitoring/buildDetails?buildbucketId=%s' 152 '\n' % buildbucket_id) 153 else: 154 out_file.write(' build url: https://ci.chromium.org/p/chromeos/' 155 'builds/b%s \n' % buildbucket_id) 156 report_url = ('https://logs.chromium.org/v/?s=chromeos%2Fbuildbucket%2F' 157 'cr-buildbucket.appspot.com%2F' + buildbucket_id + 158 '%2F%2B%2Fsteps%2FReport%2F0%2Fstdout') 159 out_file.write('\n report status url: %s\n' % report_url) 160 out_file.write('\n') 161 162 print('Report generated in %s.' % filename) 163 return filename 164 165 166def GetTryjobData(date, rotating_builds_dict): 167 """Read buildbucket id and board from stored file. 168 169 buildbot_test_llvm.py, when it launches the rotating builders, 170 records the buildbucket_id and board for each launch in a file. 171 This reads that data out of the file so we can find the right 172 tryjob data. 173 """ 174 175 date_str = format_date(date, use_int_month=True) 176 fname = '%s.builds' % date_str 177 filename = os.path.join(DATA_DIR, 'rotating-builders', fname) 178 179 if not os.path.exists(filename): 180 print('Cannot find file: %s' % filename) 181 print('Unable to generate rotating builder report for date %d.' % date) 182 return 183 184 with open(filename, 'r') as in_file: 185 lines = in_file.readlines() 186 187 for line in lines: 188 l = line.strip() 189 parts = l.split(',') 190 if len(parts) != 2: 191 print('Warning: Illegal line in data file.') 192 print('File: %s' % filename) 193 print('Line: %s' % l) 194 continue 195 buildbucket_id = parts[0] 196 board = parts[1] 197 rotating_builds_dict[board] = buildbucket_id 198 199 return 200 201 202def GetRotatingBuildData(date, report_dict, chromeos_root, board, 203 buildbucket_id, ce): 204 """Gets rotating builder job results via 'cros buildresult'.""" 205 path = os.path.join(chromeos_root, 'chromite') 206 save_dir = os.getcwd() 207 date_str = format_date(date, use_int_month=True) 208 os.chdir(path) 209 210 command = ( 211 'cros buildresult --buildbucket-id %s --report json' % buildbucket_id) 212 _, out, _ = ce.RunCommandWOutput(command) 213 tmp_dict = json.loads(out) 214 results = tmp_dict[buildbucket_id] 215 216 board_dict = dict() 217 board_dict['buildbucket_id'] = buildbucket_id 218 stages_results = results['stages'] 219 for test in TESTS: 220 key1 = test[0] 221 key2 = test[1] 222 if key2 in stages_results: 223 board_dict[key1] = stages_results[key2] 224 board_dict['status'] = results['status'] 225 report_dict[board] = board_dict 226 os.chdir(save_dir) 227 return 228 229 230def GetMainWaterfallData(date, report_dict, chromeos_root, ce): 231 """Gets main waterfall job results via 'cros buildresult'.""" 232 path = os.path.join(chromeos_root, 'chromite') 233 save_dir = os.getcwd() 234 date_str = format_date(date, use_int_month=True) 235 os.chdir(path) 236 for builder in WATERFALL_BUILDERS: 237 command = ('cros buildresult --build-config %s --date %s --report json' % 238 (builder, date_str)) 239 _, out, _ = ce.RunCommandWOutput(command) 240 tmp_dict = json.loads(out) 241 builder_dict = dict() 242 for k in tmp_dict.keys(): 243 buildbucket_id = k 244 results = tmp_dict[k] 245 246 builder_dict['buildbucket_id'] = buildbucket_id 247 builder_dict['status'] = results['status'] 248 stages_results = results['stages'] 249 for test in TESTS: 250 key1 = test[0] 251 key2 = test[1] 252 builder_dict[key1] = stages_results[key2] 253 report_dict[builder] = builder_dict 254 os.chdir(save_dir) 255 return 256 257 258# Check for prodaccess. 259def CheckProdAccess(): 260 """Verifies prodaccess is current.""" 261 status, output, _ = command_executer.GetCommandExecuter().RunCommandWOutput( 262 'prodcertstatus') 263 if status != 0: 264 return False 265 # Verify that status is not expired 266 if 'expires' in output: 267 return True 268 return False 269 270 271def ValidDate(date): 272 """Ensures 'date' is a valid date.""" 273 min_year = 2018 274 275 tmp_date = date 276 day = tmp_date % 100 277 tmp_date = tmp_date / 100 278 month = tmp_date % 100 279 year = tmp_date / 100 280 281 if day < 1 or month < 1 or year < min_year: 282 return False 283 284 cur_year = datetime.datetime.now().year 285 if year > cur_year: 286 return False 287 288 if month > 12: 289 return False 290 291 if month == 2 and cur_year % 4 == 0 and cur_year % 100 != 0: 292 max_day = 29 293 else: 294 max_day = DAYS_PER_MONTH[month] 295 296 if day > max_day: 297 return False 298 299 return True 300 301 302def ValidOptions(parser, options): 303 """Error-check the options passed to this script.""" 304 too_many_options = False 305 if options.main: 306 if options.rotating: 307 too_many_options = True 308 309 if too_many_options: 310 parser.error('Can only specify one of --main, --rotating.') 311 312 if not os.path.exists(options.chromeos_root): 313 parser.error( 314 'Invalid chromeos root. Cannot find: %s' % options.chromeos_root) 315 316 email_ok = True 317 if options.email and options.email.find('@') == -1: 318 email_ok = False 319 parser.error('"%s" is not a valid email address; it must contain "@..."' % 320 options.email) 321 322 valid_date = ValidDate(options.date) 323 324 return not too_many_options and valid_date and email_ok 325 326 327def Main(argv): 328 """Main function for this script.""" 329 parser = argparse.ArgumentParser() 330 parser.add_argument( 331 '--main', 332 dest='main', 333 default=False, 334 action='store_true', 335 help='Generate report only for main waterfall ' 336 'builders.') 337 parser.add_argument( 338 '--rotating', 339 dest='rotating', 340 default=False, 341 action='store_true', 342 help='Generate report only for rotating builders.') 343 parser.add_argument( 344 '--date', 345 dest='date', 346 required=True, 347 type=int, 348 help='The date YYYYMMDD of waterfall report.') 349 parser.add_argument( 350 '--email', 351 dest='email', 352 default='', 353 help='Email address to use for sending the report.') 354 parser.add_argument( 355 '--chromeos_root', 356 dest='chromeos_root', 357 required=True, 358 help='Chrome OS root in which to run chroot commands.') 359 360 options = parser.parse_args(argv) 361 362 if not ValidOptions(parser, options): 363 return 1 364 365 main_only = options.main 366 rotating_only = options.rotating 367 date = options.date 368 369 prod_access = CheckProdAccess() 370 if not prod_access: 371 print('ERROR: Please run prodaccess first.') 372 return 373 374 waterfall_report_dict = dict() 375 rotating_report_dict = dict() 376 377 ce = command_executer.GetCommandExecuter() 378 if not rotating_only: 379 GetMainWaterfallData(date, waterfall_report_dict, options.chromeos_root, ce) 380 381 if not main_only: 382 rotating_builds_dict = dict() 383 GetTryjobData(date, rotating_builds_dict) 384 if len(rotating_builds_dict.keys()) > 0: 385 for board in rotating_builds_dict.keys(): 386 buildbucket_id = rotating_builds_dict[board] 387 GetRotatingBuildData(date, rotating_report_dict, options.chromeos_root, 388 board, buildbucket_id, ce) 389 390 if options.email: 391 email_to = options.email 392 else: 393 email_to = getpass.getuser() 394 395 if waterfall_report_dict and not rotating_only: 396 main_report = GenerateWaterfallReport(waterfall_report_dict, 'main', date) 397 398 EmailReport(main_report, 'Main', format_date(date), email_to) 399 shutil.copy(main_report, ARCHIVE_DIR) 400 if rotating_report_dict and not main_only: 401 rotating_report = GenerateWaterfallReport(rotating_report_dict, 'rotating', 402 date) 403 404 EmailReport(rotating_report, 'Rotating', format_date(date), email_to) 405 shutil.copy(rotating_report, ARCHIVE_DIR) 406 407 408if __name__ == '__main__': 409 Main(sys.argv[1:]) 410 sys.exit(0) 411