#!/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)