blob: b7d265de4c86505336c772e38534d655e38a6839 [file] [log] [blame]
#!/usr/bin/env python3
# This script greps the JSON files for the buildbots on the LLVM official
# build master by name and prints an HTML page with the links to the bots
# and the status.
#
# Multiple masters can be used, as well as multiple groups of bots and
# multiple bots per group, all in a json file. See linaro.json in this
# repository to have an idea how the config file is.
import sys
import os
import argparse
import json
import tempfile
import logging
import shutil
from datetime import datetime, timedelta
# The requests allows HTTP keep-alive which re-uses the same TCP connection
# to download multiple files.
import requests
# The GIT revision length used on 'Commits' error display.
GIT_SHORT_LEN=7
def ignored(s):
return 'ignore' in s and s['ignore']
def not_ignored(s):
return not ignored(s)
# Returns the parsed json URL or raises an exception
def wget(session, url):
return session.get(url).json()
# Returns a string with the GIT revision usesd on build BUILDID and
# PREV_BUILDID in the form '<id_buildid>-<id_prev_buildid>'.
def get_bot_failure_changes(session, base_url, buildid, prev_buildid):
def wget_build_rev(bid):
try:
contents = wget(session,
"{}/api/v2/builds/{}/changes"
.format(base_url, bid))
except requests.exceptions.RequestException:
return None
changes = contents['changes']
if changes:
return changes[0]['revision']
return None
revision = wget_build_rev(buildid)[:GIT_SHORT_LEN]
prev_revision = None
if prev_buildid is not None:
prev_revision = wget_build_rev(prev_buildid)
if prev_revision is None:
return "{}".format(revision)
else:
return "{}-{}".format(revision, prev_revision[:GIT_SHORT_LEN])
# Map from buildbot status codes we want to treat as errors to the color they
# should be shown in. The codes are documented at
# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
# and these colors match the suggested ones there.
RESULT_COLORS = {
2: 'red', # Error
4: 'purple', # Exception
5: 'purple', # Retry
6: 'pink', # Cancelled
}
def get_bot_failing_steps(session, base_url, buildid):
try:
contents = wget(session, "{}/api/v2/builds/{}/steps"
.format(base_url, buildid))
except requests.exceptions.RequestException:
return ""
for step in contents["steps"]:
if step["results"] in RESULT_COLORS:
yield (step["name"], step["results"])
# Get the status of a individual bot BOT. Returns a dict with the
# information.
def get_bot_status(session, bot, base_url, builder_url, build_url):
try:
builds = wget(session,
"{}/api/v2/{}/{}/{}"
.format(base_url, builder_url, bot, build_url))
except requests.exceptions.RequestException as e:
return {'fail': True}
reversed_builds = iter(sorted(builds['builds'], key=lambda b: -b["number"]))
for build in reversed_builds:
if build['complete']:
time_since = (int(datetime.now().timestamp()) - int(build['complete_at']))
duration = int(build['complete_at']) - int(build['started_at'])
status = {
'builderid': build['builderid'],
'number': build['number'],
'state': build['state_string'],
'time_since': timedelta(seconds=time_since),
'duration': timedelta(seconds=duration),
'fail': build['state_string'] != 'build successful',
}
if status['fail']:
buildid = build['buildid']
prev_buildid = next(reversed_builds, None)['buildid']
status['changes'] = get_bot_failure_changes(session, base_url,
buildid,
prev_buildid)
status['steps'] = list(get_bot_failing_steps(session, base_url,
buildid))
return status
# Get status for all bots named in the config
# Return a dictionary of (base_url, bot name) -> status info
def get_bots_status(config):
session = requests.Session()
bot_cache = {}
for server in filter(not_ignored, config):
base_url = server['base_url']
logging.debug('Parsing server {}...'.format(server['name']))
for builder in server['builders']:
logging.debug(' Parsing builders {}...'.format(builder['name']))
for bot in builder['bots']:
bot_key = (base_url, bot['name'])
if bot_key in bot_cache:
continue
logging.debug(' Parsing bot {}...'.format(bot['name']))
status = get_bot_status(session, bot['name'], base_url, server['builder_url'],
server['build_url'])
if status is not None:
logging.debug(" FAIL" if status['fail'] else " PASS")
bot_cache[bot_key] = status
return bot_cache
def write_bot_status(config, output_file, bots_status):
temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
today = "{}\n".format(datetime.today().ctime())
# Whether we use the fail favicon or not
found_failure = False
# Dump all servers / bots
for server in filter(not_ignored, config):
base_url = server['base_url']
builder_url = server['builder_url']
build_url = server['build_url']
temp.write("<table cellspacing=1 cellpadding=2>\n")
temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
temp.write("<tr><th colspan=5>{} @ {}</td><tr>\n"
.format(server['name'], today))
for builder in server['builders']:
temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
temp.write("<tr><th colspan=5>{}</td><tr>\n".format(builder['name']))
temp.write("<tr><th>Buildbot</th><th>Status</th><th>T Since</th>"
"<th>Duration</th><th>Build #</th><th>Commits</th>"
"<th>Failing steps</th></tr>\n")
for bot in builder['bots']:
temp.write("<tr>\n")
try:
status = bots_status[(base_url, bot['name'])]
except KeyError:
temp.write(" <td>{} is offline!</td>\n</tr>\n".format(bot['name']))
continue
found_failure |= status['fail']
url = "{}/#/{}/{}".format(base_url, builder_url, status['builderid'])
temp.write(" <td><a href='{}'>{}</a></td>\n".format(url, bot['name']))
temp.write(" <td><font color='{}'>{}</font></td>\n"
.format('red' if status['fail'] else 'green',
'FAIL' if status['fail'] else 'PASS'))
empty_cell=" <td>&nbsp;</td>\n"
if 'time_since' in status:
temp.write(" <td>{}</td>\n".format(status['time_since']))
else:
temp.write(empty_cell)
if 'duration' in status:
temp.write(" <td>{}</td>\n".format(status['duration']))
else:
temp.write(empty_cell)
if 'number' in status:
build_url = "{}/builds/{}".format(url, status['number'])
temp.write(" <td><a href='{}'>{}</a></td>\n".format(build_url, status['number']))
else:
temp.write(empty_cell)
if 'changes' in status:
temp.write(" <td>{}</td>\n".format(status['changes']))
else:
temp.write(empty_cell)
if 'steps' in status and status['steps']:
def render_step(name, result):
return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
temp.write(" <td style=\"text-align:center\">{}</td>\n".format(step_list))
else:
temp.write(empty_cell)
temp.write("</tr>\n")
temp.write("</table>\n")
temp.write("<link rel=\"shortcut icon\" href=\"{}\" "
"type=\"image/x-icon\"/>\n".format(
'fail.ico' if found_failure else 'ok.ico'))
# Move temp to main (atomic change)
temp.close()
shutil.move(temp.name, output_file)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-d', dest='debug', action='store_true',
help='show debug log messages')
parser.add_argument('config_file',
help='Bots description in JSON format')
parser.add_argument('output_file',
help='output HTML path')
args = parser.parse_args()
if args.debug:
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
try:
with open(args.config_file, "r") as f:
config = json.load(f)
except IOError as e:
print("error: failed to read {} config file: {}".format(args.config_file, e))
sys.exit(os.EX_CONFIG)
write_bot_status(config, args.output_file, get_bots_status(config))