blob: b7d265de4c86505336c772e38534d655e38a6839 [file] [log] [blame]
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -03001#!/usr/bin/env python3
2
3# This script greps the JSON files for the buildbots on the LLVM official
4# build master by name and prints an HTML page with the links to the bots
5# and the status.
6#
7# Multiple masters can be used, as well as multiple groups of bots and
8# multiple bots per group, all in a json file. See linaro.json in this
9# repository to have an idea how the config file is.
10
11import sys
12import os
13import argparse
14import json
15import tempfile
16import logging
David Spickettaa155be2021-02-25 14:30:09 +000017import shutil
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030018from datetime import datetime, timedelta
19# The requests allows HTTP keep-alive which re-uses the same TCP connection
20# to download multiple files.
21import requests
22
23# The GIT revision length used on 'Commits' error display.
24GIT_SHORT_LEN=7
25
26def ignored(s):
27 return 'ignore' in s and s['ignore']
28def not_ignored(s):
29 return not ignored(s)
30
31
David Spickette88fe592021-03-22 12:25:13 +000032# Returns the parsed json URL or raises an exception
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030033def wget(session, url):
David Spickette88fe592021-03-22 12:25:13 +000034 return session.get(url).json()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030035
36
37# Returns a string with the GIT revision usesd on build BUILDID and
38# PREV_BUILDID in the form '<id_buildid>-<id_prev_buildid>'.
39def get_bot_failure_changes(session, base_url, buildid, prev_buildid):
40 def wget_build_rev(bid):
David Spickette88fe592021-03-22 12:25:13 +000041 try:
42 contents = wget(session,
43 "{}/api/v2/builds/{}/changes"
44 .format(base_url, bid))
45 except requests.exceptions.RequestException:
David Spickett7f18f4d2021-03-22 11:49:17 +000046 return None
David Spickette88fe592021-03-22 12:25:13 +000047 changes = contents['changes']
48 if changes:
49 return changes[0]['revision']
50 return None
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030051
David Spickett7f18f4d2021-03-22 11:49:17 +000052 revision = wget_build_rev(buildid)[:GIT_SHORT_LEN]
53 prev_revision = None
54 if prev_buildid is not None:
55 prev_revision = wget_build_rev(prev_buildid)
56
57 if prev_revision is None:
58 return "{}".format(revision)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030059 else:
David Spickett7f18f4d2021-03-22 11:49:17 +000060 return "{}-{}".format(revision, prev_revision[:GIT_SHORT_LEN])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030061
62
Oliver Stannard91688ff2021-01-07 10:27:27 +000063# Map from buildbot status codes we want to treat as errors to the color they
64# should be shown in. The codes are documented at
65# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
66# and these colors match the suggested ones there.
67RESULT_COLORS = {
68 2: 'red', # Error
69 4: 'purple', # Exception
70 5: 'purple', # Retry
71 6: 'pink', # Cancelled
72}
73
74def get_bot_failing_steps(session, base_url, buildid):
David Spickette88fe592021-03-22 12:25:13 +000075 try:
76 contents = wget(session, "{}/api/v2/builds/{}/steps"
77 .format(base_url, buildid))
78 except requests.exceptions.RequestException:
Oliver Stannard91688ff2021-01-07 10:27:27 +000079 return ""
David Spickette88fe592021-03-22 12:25:13 +000080
Oliver Stannard91688ff2021-01-07 10:27:27 +000081 for step in contents["steps"]:
David Spickett7f18f4d2021-03-22 11:49:17 +000082 if step["results"] in RESULT_COLORS:
Oliver Stannard91688ff2021-01-07 10:27:27 +000083 yield (step["name"], step["results"])
84
85
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030086# Get the status of a individual bot BOT. Returns a dict with the
87# information.
88def get_bot_status(session, bot, base_url, builder_url, build_url):
David Spickette88fe592021-03-22 12:25:13 +000089 try:
90 builds = wget(session,
91 "{}/api/v2/{}/{}/{}"
92 .format(base_url, builder_url, bot, build_url))
93 except requests.exceptions.RequestException as e:
94 return {'fail': True}
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030095
Oliver Stannard46e99032021-01-05 10:30:56 +000096 reversed_builds = iter(sorted(builds['builds'], key=lambda b: -b["number"]))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030097 for build in reversed_builds:
98 if build['complete']:
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +000099 time_since = (int(datetime.now().timestamp()) - int(build['complete_at']))
100 duration = int(build['complete_at']) - int(build['started_at'])
David Spickett7f18f4d2021-03-22 11:49:17 +0000101 status = {
102 'builderid': build['builderid'],
103 'number': build['number'],
104 'state': build['state_string'],
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000105 'time_since': timedelta(seconds=time_since),
106 'duration': timedelta(seconds=duration),
David Spickett7f18f4d2021-03-22 11:49:17 +0000107 'fail': build['state_string'] != 'build successful',
108 }
109 if status['fail']:
110 buildid = build['buildid']
111 prev_buildid = next(reversed_builds, None)['buildid']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300112 status['changes'] = get_bot_failure_changes(session, base_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000113 buildid,
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300114 prev_buildid)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000115 status['steps'] = list(get_bot_failing_steps(session, base_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000116 buildid))
117
118 return status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300119
120
David Spickett85355fa2021-03-22 15:41:41 +0000121# Get status for all bots named in the config
122# Return a dictionary of (base_url, bot name) -> status info
123def get_bots_status(config):
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300124 session = requests.Session()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300125 bot_cache = {}
David Spickettf006c372021-03-22 12:54:12 +0000126
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300127 for server in filter(not_ignored, config):
128 base_url = server['base_url']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300129 logging.debug('Parsing server {}...'.format(server['name']))
130 for builder in server['builders']:
131 logging.debug(' Parsing builders {}...'.format(builder['name']))
132 for bot in builder['bots']:
David Spickett85355fa2021-03-22 15:41:41 +0000133 bot_key = (base_url, bot['name'])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300134 if bot_key in bot_cache:
135 continue
David Spickett85355fa2021-03-22 15:41:41 +0000136
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300137 logging.debug(' Parsing bot {}...'.format(bot['name']))
David Spickett85355fa2021-03-22 15:41:41 +0000138 status = get_bot_status(session, bot['name'], base_url, server['builder_url'],
139 server['build_url'])
David Spickettf2c82dd2021-06-24 10:01:33 +0100140 if status is not None:
141 logging.debug(" FAIL" if status['fail'] else " PASS")
142 bot_cache[bot_key] = status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300143
David Spickett85355fa2021-03-22 15:41:41 +0000144 return bot_cache
145
146def write_bot_status(config, output_file, bots_status):
147 temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
148 today = "{}\n".format(datetime.today().ctime())
149 # Whether we use the fail favicon or not
150 found_failure = False
David Spickettf006c372021-03-22 12:54:12 +0000151
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300152 # Dump all servers / bots
153 for server in filter(not_ignored, config):
154 base_url = server['base_url']
155 builder_url = server['builder_url']
156 build_url = server['build_url']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300157 temp.write("<table cellspacing=1 cellpadding=2>\n")
158 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
159 temp.write("<tr><th colspan=5>{} @ {}</td><tr>\n"
160 .format(server['name'], today))
161
162 for builder in server['builders']:
163 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
164 temp.write("<tr><th colspan=5>{}</td><tr>\n".format(builder['name']))
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000165 temp.write("<tr><th>Buildbot</th><th>Status</th><th>T Since</th>"
166 "<th>Duration</th><th>Build #</th><th>Commits</th>"
167 "<th>Failing steps</th></tr>\n")
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300168 for bot in builder['bots']:
169 temp.write("<tr>\n")
David Spickettf2c82dd2021-06-24 10:01:33 +0100170 try:
171 status = bots_status[(base_url, bot['name'])]
172 except KeyError:
David Spickettf2d4c482021-06-24 10:07:52 +0100173 temp.write(" <td>{} is offline!</td>\n</tr>\n".format(bot['name']))
David Spickettf2c82dd2021-06-24 10:01:33 +0100174 continue
175
David Spickett85355fa2021-03-22 15:41:41 +0000176 found_failure |= status['fail']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300177 url = "{}/#/{}/{}".format(base_url, builder_url, status['builderid'])
178 temp.write(" <td><a href='{}'>{}</a></td>\n".format(url, bot['name']))
179 temp.write(" <td><font color='{}'>{}</font></td>\n"
180 .format('red' if status['fail'] else 'green',
181 'FAIL' if status['fail'] else 'PASS'))
182 empty_cell=" <td>&nbsp;</td>\n"
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000183 if 'time_since' in status:
184 temp.write(" <td>{}</td>\n".format(status['time_since']))
185 else:
186 temp.write(empty_cell)
187 if 'duration' in status:
188 temp.write(" <td>{}</td>\n".format(status['duration']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300189 else:
190 temp.write(empty_cell)
191 if 'number' in status:
192 build_url = "{}/builds/{}".format(url, status['number'])
193 temp.write(" <td><a href='{}'>{}</a></td>\n".format(build_url, status['number']))
194 else:
195 temp.write(empty_cell)
196 if 'changes' in status:
197 temp.write(" <td>{}</td>\n".format(status['changes']))
198 else:
199 temp.write(empty_cell)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000200 if 'steps' in status and status['steps']:
201 def render_step(name, result):
202 return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
203 step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
204 temp.write(" <td style=\"text-align:center\">{}</td>\n".format(step_list))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300205 else:
206 temp.write(empty_cell)
207 temp.write("</tr>\n")
208 temp.write("</table>\n")
209
David Spickett85355fa2021-03-22 15:41:41 +0000210 temp.write("<link rel=\"shortcut icon\" href=\"{}\" "
211 "type=\"image/x-icon\"/>\n".format(
212 'fail.ico' if found_failure else 'ok.ico'))
213
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300214 # Move temp to main (atomic change)
215 temp.close()
David Spickett7f18f4d2021-03-22 11:49:17 +0000216 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300217
218
219if __name__ == "__main__":
220 parser = argparse.ArgumentParser()
David Spickettec2166c2021-07-19 14:17:29 +0100221 parser.add_argument('-d', dest='debug', action='store_true',
222 help='show debug log messages')
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300223 parser.add_argument('config_file',
224 help='Bots description in JSON format')
225 parser.add_argument('output_file',
226 help='output HTML path')
227 args = parser.parse_args()
228
229 if args.debug:
230 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
231
232 try:
233 with open(args.config_file, "r") as f:
234 config = json.load(f)
235 except IOError as e:
David Spickett7f18f4d2021-03-22 11:49:17 +0000236 print("error: failed to read {} config file: {}".format(args.config_file, e))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300237 sys.exit(os.EX_CONFIG)
238
David Spickett85355fa2021-03-22 15:41:41 +0000239 write_bot_status(config, args.output_file, get_bots_status(config))