| #!/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']: |
| delta = int(build['complete_at']) - int(build['started_at']) |
| status = { |
| 'builderid': build['builderid'], |
| 'number': build['number'], |
| 'state': build['state_string'], |
| 'time': timedelta(seconds=delta), |
| '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 |
| |
| |
| def bot_status(config_file, output_file): |
| temp = tempfile.NamedTemporaryFile(mode='w+', delete=False) |
| |
| today = "{}\n".format(datetime.today().ctime()) |
| |
| session = requests.Session() |
| |
| # Get status for all bots |
| bot_cache = {} |
| for server in filter(not_ignored, config): |
| base_url = server['base_url'] |
| builder_url = server['builder_url'] |
| build_url = server['build_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 = "{}/{}".format(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, builder_url, |
| build_url) |
| if not_ignored(bot): |
| fail = 'fail' in status |
| logging.debug(" FAIL" if status['fail'] else " PASS") |
| bot_cache[bot_key] = status |
| |
| # 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'] |
| favicon = 'fail.ico' if fail else 'ok.ico' |
| temp.write("<link rel=\"shortcut icon\" href=\"{}\" " |
| "type=\"image/x-icon\"/>\n".format(favicon)) |
| temp.write("<table cellspacing=1 cellpadding=2>\n") |
| temp.write("<tr><td colspan=5> </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> </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>Time</th>" |
| "<th>Build #</th><th>Commits</th><th>Failing steps</th></tr>\n") |
| for bot in builder['bots']: |
| temp.write("<tr>\n") |
| status = bot_cache["{}/{}".format(base_url, bot['name'])] |
| 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> </td>\n" |
| if 'time' in status: |
| temp.write(" <td>{}</td>\n".format(status['time'])) |
| 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") |
| |
| # 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') |
| 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) |
| |
| bot_status(config, args.output_file) |