#!/usr/bin/env python2
|
"""Generate summary report for ChromeOS toolchain waterfalls."""
|
|
from __future__ import print_function
|
|
import argparse
|
import datetime
|
import getpass
|
import json
|
import os
|
import re
|
import shutil
|
import sys
|
import time
|
|
from cros_utils import command_executer
|
|
# All the test suites whose data we might want for the reports.
|
TESTS = (('bvt-inline', 'HWTest [bvt-inline]'), ('bvt-cq', 'HWTest [bvt-cq]'),
|
('security', 'HWTest [security]'))
|
|
# The main waterfall builders, IN THE ORDER IN WHICH WE WANT THEM
|
# LISTED IN THE REPORT.
|
WATERFALL_BUILDERS = [
|
'amd64-llvm-next-toolchain',
|
'arm-llvm-next-toolchain',
|
'arm64-llvm-next-toolchain',
|
]
|
|
DATA_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-report-data/'
|
ARCHIVE_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-reports/'
|
DOWNLOAD_DIR = '/tmp/waterfall-logs'
|
MAX_SAVE_RECORDS = 7
|
BUILD_DATA_FILE = '%s/build-data.txt' % DATA_DIR
|
LLVM_ROTATING_BUILDER = 'llvm_next_toolchain'
|
ROTATING_BUILDERS = [LLVM_ROTATING_BUILDER]
|
|
# For int-to-string date conversion. Note, the index of the month in this
|
# list needs to correspond to the month's integer value. i.e. 'Sep' must
|
# be as MONTHS[9].
|
MONTHS = [
|
'', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
|
'Nov', 'Dec'
|
]
|
|
DAYS_PER_MONTH = {
|
1: 31,
|
2: 28,
|
3: 31,
|
4: 30,
|
5: 31,
|
6: 30,
|
7: 31,
|
8: 31,
|
9: 30,
|
10: 31,
|
11: 31,
|
12: 31
|
}
|
|
|
def format_date(int_date, use_int_month=False):
|
"""Convert an integer date to a string date. YYYYMMDD -> YYYY-MMM-DD"""
|
|
if int_date == 0:
|
return 'today'
|
|
tmp_date = int_date
|
day = tmp_date % 100
|
tmp_date = tmp_date / 100
|
month = tmp_date % 100
|
year = tmp_date / 100
|
|
if use_int_month:
|
date_str = '%d-%02d-%02d' % (year, month, day)
|
else:
|
month_str = MONTHS[month]
|
date_str = '%d-%s-%d' % (year, month_str, day)
|
return date_str
|
|
|
def EmailReport(report_file, report_type, date, email_to):
|
"""Emails the report to the approprite address."""
|
subject = '%s Waterfall Summary report, %s' % (report_type, date)
|
sendgmr_path = '/google/data/ro/projects/gws-sre/sendgmr'
|
command = ('%s --to=%s --subject="%s" --body_file=%s' %
|
(sendgmr_path, email_to, subject, report_file))
|
command_executer.GetCommandExecuter().RunCommand(command)
|
|
|
def GetColor(status):
|
"""Given a job status string, returns appropriate color string."""
|
if status.strip() == 'pass':
|
color = 'green '
|
elif status.strip() == 'fail':
|
color = ' red '
|
elif status.strip() == 'warning':
|
color = 'orange'
|
else:
|
color = ' '
|
return color
|
|
|
def GenerateWaterfallReport(report_dict, waterfall_type, date):
|
"""Write out the actual formatted report."""
|
|
filename = 'waterfall_report.%s_waterfall.%s.txt' % (waterfall_type, date)
|
|
date_string = ''
|
report_list = report_dict.keys()
|
|
with open(filename, 'w') as out_file:
|
# Write Report Header
|
out_file.write('\nStatus of %s Waterfall Builds from %s\n\n' %
|
(waterfall_type, date_string))
|
out_file.write(' \n')
|
out_file.write(
|
' Build bvt- '
|
' bvt-cq '
|
' security \n')
|
out_file.write(
|
' status inline '
|
' \n')
|
|
# Write daily waterfall status section.
|
for builder in report_list:
|
build_dict = report_dict[builder]
|
buildbucket_id = build_dict['buildbucket_id']
|
overall_status = build_dict['status']
|
if 'bvt-inline' in build_dict.keys():
|
inline_status = build_dict['bvt-inline']
|
else:
|
inline_status = ' '
|
if 'bvt-cq' in build_dict.keys():
|
cq_status = build_dict['bvt-cq']
|
else:
|
cq_status = ' '
|
if 'security' in build_dict.keys():
|
security_status = build_dict['security']
|
else:
|
security_status = ' '
|
inline_color = GetColor(inline_status)
|
cq_color = GetColor(cq_status)
|
security_color = GetColor(security_status)
|
|
out_file.write(
|
'%26s %4s %6s %6s %6s\n' %
|
(builder, overall_status, inline_color, cq_color, security_color))
|
if waterfall_type == 'main':
|
out_file.write(' build url: https://cros-goldeneye.corp.google.com/'
|
'chromeos/healthmonitoring/buildDetails?buildbucketId=%s'
|
'\n' % buildbucket_id)
|
else:
|
out_file.write(' build url: https://ci.chromium.org/p/chromeos/'
|
'builds/b%s \n' % buildbucket_id)
|
report_url = ('https://logs.chromium.org/v/?s=chromeos%2Fbuildbucket%2F'
|
'cr-buildbucket.appspot.com%2F' + buildbucket_id +
|
'%2F%2B%2Fsteps%2FReport%2F0%2Fstdout')
|
out_file.write('\n report status url: %s\n' % report_url)
|
out_file.write('\n')
|
|
print('Report generated in %s.' % filename)
|
return filename
|
|
|
def GetTryjobData(date, rotating_builds_dict):
|
"""Read buildbucket id and board from stored file.
|
|
buildbot_test_llvm.py, when it launches the rotating builders,
|
records the buildbucket_id and board for each launch in a file.
|
This reads that data out of the file so we can find the right
|
tryjob data.
|
"""
|
|
date_str = format_date(date, use_int_month=True)
|
fname = '%s.builds' % date_str
|
filename = os.path.join(DATA_DIR, 'rotating-builders', fname)
|
|
if not os.path.exists(filename):
|
print('Cannot find file: %s' % filename)
|
print('Unable to generate rotating builder report for date %d.' % date)
|
return
|
|
with open(filename, 'r') as in_file:
|
lines = in_file.readlines()
|
|
for line in lines:
|
l = line.strip()
|
parts = l.split(',')
|
if len(parts) != 2:
|
print('Warning: Illegal line in data file.')
|
print('File: %s' % filename)
|
print('Line: %s' % l)
|
continue
|
buildbucket_id = parts[0]
|
board = parts[1]
|
rotating_builds_dict[board] = buildbucket_id
|
|
return
|
|
|
def GetRotatingBuildData(date, report_dict, chromeos_root, board,
|
buildbucket_id, ce):
|
"""Gets rotating builder job results via 'cros buildresult'."""
|
path = os.path.join(chromeos_root, 'chromite')
|
save_dir = os.getcwd()
|
date_str = format_date(date, use_int_month=True)
|
os.chdir(path)
|
|
command = (
|
'cros buildresult --buildbucket-id %s --report json' % buildbucket_id)
|
_, out, _ = ce.RunCommandWOutput(command)
|
tmp_dict = json.loads(out)
|
results = tmp_dict[buildbucket_id]
|
|
board_dict = dict()
|
board_dict['buildbucket_id'] = buildbucket_id
|
stages_results = results['stages']
|
for test in TESTS:
|
key1 = test[0]
|
key2 = test[1]
|
if key2 in stages_results:
|
board_dict[key1] = stages_results[key2]
|
board_dict['status'] = results['status']
|
report_dict[board] = board_dict
|
os.chdir(save_dir)
|
return
|
|
|
def GetMainWaterfallData(date, report_dict, chromeos_root, ce):
|
"""Gets main waterfall job results via 'cros buildresult'."""
|
path = os.path.join(chromeos_root, 'chromite')
|
save_dir = os.getcwd()
|
date_str = format_date(date, use_int_month=True)
|
os.chdir(path)
|
for builder in WATERFALL_BUILDERS:
|
command = ('cros buildresult --build-config %s --date %s --report json' %
|
(builder, date_str))
|
_, out, _ = ce.RunCommandWOutput(command)
|
tmp_dict = json.loads(out)
|
builder_dict = dict()
|
for k in tmp_dict.keys():
|
buildbucket_id = k
|
results = tmp_dict[k]
|
|
builder_dict['buildbucket_id'] = buildbucket_id
|
builder_dict['status'] = results['status']
|
stages_results = results['stages']
|
for test in TESTS:
|
key1 = test[0]
|
key2 = test[1]
|
builder_dict[key1] = stages_results[key2]
|
report_dict[builder] = builder_dict
|
os.chdir(save_dir)
|
return
|
|
|
# Check for prodaccess.
|
def CheckProdAccess():
|
"""Verifies prodaccess is current."""
|
status, output, _ = command_executer.GetCommandExecuter().RunCommandWOutput(
|
'prodcertstatus')
|
if status != 0:
|
return False
|
# Verify that status is not expired
|
if 'expires' in output:
|
return True
|
return False
|
|
|
def ValidDate(date):
|
"""Ensures 'date' is a valid date."""
|
min_year = 2018
|
|
tmp_date = date
|
day = tmp_date % 100
|
tmp_date = tmp_date / 100
|
month = tmp_date % 100
|
year = tmp_date / 100
|
|
if day < 1 or month < 1 or year < min_year:
|
return False
|
|
cur_year = datetime.datetime.now().year
|
if year > cur_year:
|
return False
|
|
if month > 12:
|
return False
|
|
if month == 2 and cur_year % 4 == 0 and cur_year % 100 != 0:
|
max_day = 29
|
else:
|
max_day = DAYS_PER_MONTH[month]
|
|
if day > max_day:
|
return False
|
|
return True
|
|
|
def ValidOptions(parser, options):
|
"""Error-check the options passed to this script."""
|
too_many_options = False
|
if options.main:
|
if options.rotating:
|
too_many_options = True
|
|
if too_many_options:
|
parser.error('Can only specify one of --main, --rotating.')
|
|
if not os.path.exists(options.chromeos_root):
|
parser.error(
|
'Invalid chromeos root. Cannot find: %s' % options.chromeos_root)
|
|
email_ok = True
|
if options.email and options.email.find('@') == -1:
|
email_ok = False
|
parser.error('"%s" is not a valid email address; it must contain "@..."' %
|
options.email)
|
|
valid_date = ValidDate(options.date)
|
|
return not too_many_options and valid_date and email_ok
|
|
|
def Main(argv):
|
"""Main function for this script."""
|
parser = argparse.ArgumentParser()
|
parser.add_argument(
|
'--main',
|
dest='main',
|
default=False,
|
action='store_true',
|
help='Generate report only for main waterfall '
|
'builders.')
|
parser.add_argument(
|
'--rotating',
|
dest='rotating',
|
default=False,
|
action='store_true',
|
help='Generate report only for rotating builders.')
|
parser.add_argument(
|
'--date',
|
dest='date',
|
required=True,
|
type=int,
|
help='The date YYYYMMDD of waterfall report.')
|
parser.add_argument(
|
'--email',
|
dest='email',
|
default='',
|
help='Email address to use for sending the report.')
|
parser.add_argument(
|
'--chromeos_root',
|
dest='chromeos_root',
|
required=True,
|
help='Chrome OS root in which to run chroot commands.')
|
|
options = parser.parse_args(argv)
|
|
if not ValidOptions(parser, options):
|
return 1
|
|
main_only = options.main
|
rotating_only = options.rotating
|
date = options.date
|
|
prod_access = CheckProdAccess()
|
if not prod_access:
|
print('ERROR: Please run prodaccess first.')
|
return
|
|
waterfall_report_dict = dict()
|
rotating_report_dict = dict()
|
|
ce = command_executer.GetCommandExecuter()
|
if not rotating_only:
|
GetMainWaterfallData(date, waterfall_report_dict, options.chromeos_root, ce)
|
|
if not main_only:
|
rotating_builds_dict = dict()
|
GetTryjobData(date, rotating_builds_dict)
|
if len(rotating_builds_dict.keys()) > 0:
|
for board in rotating_builds_dict.keys():
|
buildbucket_id = rotating_builds_dict[board]
|
GetRotatingBuildData(date, rotating_report_dict, options.chromeos_root,
|
board, buildbucket_id, ce)
|
|
if options.email:
|
email_to = options.email
|
else:
|
email_to = getpass.getuser()
|
|
if waterfall_report_dict and not rotating_only:
|
main_report = GenerateWaterfallReport(waterfall_report_dict, 'main', date)
|
|
EmailReport(main_report, 'Main', format_date(date), email_to)
|
shutil.copy(main_report, ARCHIVE_DIR)
|
if rotating_report_dict and not main_only:
|
rotating_report = GenerateWaterfallReport(rotating_report_dict, 'rotating',
|
date)
|
|
EmailReport(rotating_report, 'Rotating', format_date(date), email_to)
|
shutil.copy(rotating_report, ARCHIVE_DIR)
|
|
|
if __name__ == '__main__':
|
Main(sys.argv[1:])
|
sys.exit(0)
|