blob: e0128d63bac997d1be7b733f70fa1181f6066c75 [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
David Spickett55449c62021-12-13 12:57:33 +000022from textwrap import dedent
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030023
David Spickett30a986f2021-04-29 09:37:00 +010024from buildkite_status import get_buildkite_bots_status
25
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030026# The GIT revision length used on 'Commits' error display.
27GIT_SHORT_LEN=7
28
29def ignored(s):
30 return 'ignore' in s and s['ignore']
31def not_ignored(s):
32 return not ignored(s)
33
34
David Spickette88fe592021-03-22 12:25:13 +000035# Returns the parsed json URL or raises an exception
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030036def wget(session, url):
David Spickett8306ed82021-12-06 10:40:36 +000037 got = session.get(url)
38 got.raise_for_status()
39 return got.json()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030040
41
42# Returns a string with the GIT revision usesd on build BUILDID and
43# PREV_BUILDID in the form '<id_buildid>-<id_prev_buildid>'.
44def get_bot_failure_changes(session, base_url, buildid, prev_buildid):
45 def wget_build_rev(bid):
David Spickette88fe592021-03-22 12:25:13 +000046 try:
47 contents = wget(session,
48 "{}/api/v2/builds/{}/changes"
49 .format(base_url, bid))
50 except requests.exceptions.RequestException:
David Spickett8306ed82021-12-06 10:40:36 +000051 logging.debug(" Couldn't get changes for build {}!".format(buildid))
David Spickett7f18f4d2021-03-22 11:49:17 +000052 return None
David Spickette88fe592021-03-22 12:25:13 +000053 changes = contents['changes']
54 if changes:
55 return changes[0]['revision']
56 return None
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030057
David Spickett7d47cea2022-06-01 14:13:32 +010058 revision = wget_build_rev(buildid)
59 if revision is None:
60 return ""
61 revision = revision[:GIT_SHORT_LEN]
62
David Spickett7f18f4d2021-03-22 11:49:17 +000063 prev_revision = None
64 if prev_buildid is not None:
65 prev_revision = wget_build_rev(prev_buildid)
66
67 if prev_revision is None:
68 return "{}".format(revision)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030069 else:
David Spickett7f18f4d2021-03-22 11:49:17 +000070 return "{}-{}".format(revision, prev_revision[:GIT_SHORT_LEN])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030071
72
Oliver Stannard91688ff2021-01-07 10:27:27 +000073# Map from buildbot status codes we want to treat as errors to the color they
74# should be shown in. The codes are documented at
75# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
76# and these colors match the suggested ones there.
77RESULT_COLORS = {
78 2: 'red', # Error
79 4: 'purple', # Exception
80 5: 'purple', # Retry
81 6: 'pink', # Cancelled
82}
83
84def get_bot_failing_steps(session, base_url, buildid):
David Spickette88fe592021-03-22 12:25:13 +000085 try:
86 contents = wget(session, "{}/api/v2/builds/{}/steps"
87 .format(base_url, buildid))
88 except requests.exceptions.RequestException:
Oliver Stannard91688ff2021-01-07 10:27:27 +000089 return ""
David Spickette88fe592021-03-22 12:25:13 +000090
Oliver Stannard91688ff2021-01-07 10:27:27 +000091 for step in contents["steps"]:
David Spickett7f18f4d2021-03-22 11:49:17 +000092 if step["results"] in RESULT_COLORS:
Oliver Stannard91688ff2021-01-07 10:27:27 +000093 yield (step["name"], step["results"])
94
95
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030096# Get the status of a individual bot BOT. Returns a dict with the
97# information.
98def get_bot_status(session, bot, base_url, builder_url, build_url):
David Spickette88fe592021-03-22 12:25:13 +000099 try:
100 builds = wget(session,
101 "{}/api/v2/{}/{}/{}"
102 .format(base_url, builder_url, bot, build_url))
103 except requests.exceptions.RequestException as e:
David Spickett7d47cea2022-06-01 14:13:32 +0100104 logging.debug(" Couldn't get builds for bot {}!".format(bot))
105 return {'valid': False}
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300106
Oliver Stannard46e99032021-01-05 10:30:56 +0000107 reversed_builds = iter(sorted(builds['builds'], key=lambda b: -b["number"]))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300108 for build in reversed_builds:
109 if build['complete']:
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000110 time_since = (int(datetime.now().timestamp()) - int(build['complete_at']))
111 duration = int(build['complete_at']) - int(build['started_at'])
David Spickett30a986f2021-04-29 09:37:00 +0100112 agent_url = "{}/#/{}/{}".format(base_url, builder_url, build['builderid'])
113
David Spickett7f18f4d2021-03-22 11:49:17 +0000114 status = {
David Spickett30a986f2021-04-29 09:37:00 +0100115 'builder_url': agent_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000116 'number': build['number'],
David Spickett30a986f2021-04-29 09:37:00 +0100117 'build_url': "{}/builds/{}".format(agent_url, build['number']),
David Spickett7f18f4d2021-03-22 11:49:17 +0000118 'state': build['state_string'],
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000119 'time_since': timedelta(seconds=time_since),
120 'duration': timedelta(seconds=duration),
David Spickett7f18f4d2021-03-22 11:49:17 +0000121 'fail': build['state_string'] != 'build successful',
122 }
David Spickett30a986f2021-04-29 09:37:00 +0100123
David Spickett7f18f4d2021-03-22 11:49:17 +0000124 if status['fail']:
125 buildid = build['buildid']
126 prev_buildid = next(reversed_builds, None)['buildid']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300127 status['changes'] = get_bot_failure_changes(session, base_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000128 buildid,
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300129 prev_buildid)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000130 status['steps'] = list(get_bot_failing_steps(session, base_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000131 buildid))
132
133 return status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300134
135
David Spickett85355fa2021-03-22 15:41:41 +0000136# Get status for all bots named in the config
137# Return a dictionary of (base_url, bot name) -> status info
David Spickett30a986f2021-04-29 09:37:00 +0100138def get_buildbot_bots_status(config):
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300139 session = requests.Session()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300140 bot_cache = {}
David Spickettf006c372021-03-22 12:54:12 +0000141
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300142 for server in filter(not_ignored, config):
David Spickett30a986f2021-04-29 09:37:00 +0100143 if server['name'] == "Buildkite":
144 continue
145
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300146 base_url = server['base_url']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300147 logging.debug('Parsing server {}...'.format(server['name']))
148 for builder in server['builders']:
149 logging.debug(' Parsing builders {}...'.format(builder['name']))
150 for bot in builder['bots']:
David Spickett85355fa2021-03-22 15:41:41 +0000151 bot_key = (base_url, bot['name'])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300152 if bot_key in bot_cache:
153 continue
David Spickett85355fa2021-03-22 15:41:41 +0000154
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300155 logging.debug(' Parsing bot {}...'.format(bot['name']))
David Spickett85355fa2021-03-22 15:41:41 +0000156 status = get_bot_status(session, bot['name'], base_url, server['builder_url'],
157 server['build_url'])
David Spickettf2c82dd2021-06-24 10:01:33 +0100158 if status is not None:
David Spickett7d47cea2022-06-01 14:13:32 +0100159 if status.get("valid", True):
160 logging.debug(" Bot status: " + ("FAIL" if status['fail'] else "PASS"))
David Spickettf2c82dd2021-06-24 10:01:33 +0100161 bot_cache[bot_key] = status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300162
David Spickett85355fa2021-03-22 15:41:41 +0000163 return bot_cache
164
165def write_bot_status(config, output_file, bots_status):
166 temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
167 today = "{}\n".format(datetime.today().ctime())
168 # Whether we use the fail favicon or not
169 found_failure = False
David Spickettf006c372021-03-22 12:54:12 +0000170
David Spickett55449c62021-12-13 12:57:33 +0000171 temp.write(dedent("""\
172 <style>
173 /* Combine the border between cells to prevent 1px gaps
174 in the row background colour. */
175 table, td, th {
176 border-collapse: collapse;
177 }
178 /* Colour every other row in a table body grey. */
179 tbody tr:nth-child(even) td {
180 background-color: #ededed;
181 }
182 </style>"""))
183
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300184 # Dump all servers / bots
185 for server in filter(not_ignored, config):
186 base_url = server['base_url']
187 builder_url = server['builder_url']
188 build_url = server['build_url']
David Spickettec94cc22021-12-13 13:16:05 +0000189
190 column_titles = [
191 "Buildbot",
192 "Status",
193 "T Since",
194 "Duration",
195 "Build #",
196 "Commits",
197 "Failing steps"
198 ]
199 num_columns = len(column_titles)
200 column_titles_html = "<tr>{}</tr>\n".format(
201 "".join(["<th>{}</th>".format(t) for t in column_titles]))
202
David Spickett55449c62021-12-13 12:57:33 +0000203 temp.write("<table border=0 cellspacing=1 cellpadding=2>\n")
David Spickettec94cc22021-12-13 13:16:05 +0000204 temp.write("<tr><td colspan={}>&nbsp;</td><tr>\n".format(num_columns))
205 temp.write("<tr><th colspan={}>{} @ {}</td><tr>\n"
206 .format(num_columns, server['name'], today))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300207
208 for builder in server['builders']:
David Spickettec94cc22021-12-13 13:16:05 +0000209 temp.write("<tr><td colspan={}>&nbsp;</td><tr>\n".format(num_columns))
210 temp.write("<tr><th colspan={}>{}</th><tr>\n".format(num_columns, builder['name']))
211 temp.write(column_titles_html)
David Spickett55449c62021-12-13 12:57:33 +0000212 temp.write("<tbody>\n")
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300213 for bot in builder['bots']:
214 temp.write("<tr>\n")
David Spickett7d47cea2022-06-01 14:13:32 +0100215 logging.debug("Writing out status for {}".format(bot['name']))
David Spickettf2c82dd2021-06-24 10:01:33 +0100216 try:
217 status = bots_status[(base_url, bot['name'])]
218 except KeyError:
David Spickettec94cc22021-12-13 13:16:05 +0000219 temp.write(" <td colspan={}>{} is offline!</td>\n</tr>\n"
220 .format(num_columns, bot['name']))
David Spickettf2c82dd2021-06-24 10:01:33 +0100221 continue
David Spickett30a986f2021-04-29 09:37:00 +0100222 else:
223 if not status.get('valid', True):
David Spickettec94cc22021-12-13 13:16:05 +0000224 temp.write(" <td colspan={}>Could not read status for {}!</td>\n</tr>\n"
225 .format(num_columns, bot['name']))
David Spickett30a986f2021-04-29 09:37:00 +0100226 continue
David Spickettf2c82dd2021-06-24 10:01:33 +0100227
David Spickett85355fa2021-03-22 15:41:41 +0000228 found_failure |= status['fail']
David Spickett30a986f2021-04-29 09:37:00 +0100229
230 temp.write(" <td><a href='{}'>{}</a></td>\n".format(
231 status['builder_url'], bot['name']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300232 temp.write(" <td><font color='{}'>{}</font></td>\n"
233 .format('red' if status['fail'] else 'green',
234 'FAIL' if status['fail'] else 'PASS'))
235 empty_cell=" <td>&nbsp;</td>\n"
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000236 if 'time_since' in status:
David Spickett187f7962022-02-09 12:35:00 +0000237 time_since = status['time_since']
238 # No build should be taking more than a day
239 if time_since > timedelta(hours=24):
240 time_since = "<p style=\"color:red\">{}</p>".format(
241 time_since)
242 else:
243 time_since = str(time_since)
244
245 temp.write(" <td>{}</td>\n".format(time_since))
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000246 else:
247 temp.write(empty_cell)
248 if 'duration' in status:
249 temp.write(" <td>{}</td>\n".format(status['duration']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300250 else:
251 temp.write(empty_cell)
252 if 'number' in status:
David Spickett30a986f2021-04-29 09:37:00 +0100253 temp.write(" <td><a href='{}'>{}</a></td>\n".format(
254 status['build_url'], status['number']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300255 else:
256 temp.write(empty_cell)
257 if 'changes' in status:
258 temp.write(" <td>{}</td>\n".format(status['changes']))
259 else:
260 temp.write(empty_cell)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000261 if 'steps' in status and status['steps']:
262 def render_step(name, result):
263 return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
264 step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
265 temp.write(" <td style=\"text-align:center\">{}</td>\n".format(step_list))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300266 else:
267 temp.write(empty_cell)
268 temp.write("</tr>\n")
David Spickett55449c62021-12-13 12:57:33 +0000269 temp.write("</tbody>\n")
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300270 temp.write("</table>\n")
271
David Spickett85355fa2021-03-22 15:41:41 +0000272 temp.write("<link rel=\"shortcut icon\" href=\"{}\" "
273 "type=\"image/x-icon\"/>\n".format(
274 'fail.ico' if found_failure else 'ok.ico'))
275
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300276 # Move temp to main (atomic change)
277 temp.close()
David Spickett7f18f4d2021-03-22 11:49:17 +0000278 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300279
280
281if __name__ == "__main__":
282 parser = argparse.ArgumentParser()
David Spickettec2166c2021-07-19 14:17:29 +0100283 parser.add_argument('-d', dest='debug', action='store_true',
284 help='show debug log messages')
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300285 parser.add_argument('config_file',
286 help='Bots description in JSON format')
287 parser.add_argument('output_file',
288 help='output HTML path')
289 args = parser.parse_args()
290
291 if args.debug:
292 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
293
294 try:
295 with open(args.config_file, "r") as f:
296 config = json.load(f)
297 except IOError as e:
David Spickett7f18f4d2021-03-22 11:49:17 +0000298 print("error: failed to read {} config file: {}".format(args.config_file, e))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300299 sys.exit(os.EX_CONFIG)
300
David Spickett30a986f2021-04-29 09:37:00 +0100301 status = get_buildbot_bots_status(config)
302 status.update(get_buildkite_bots_status(config))
303 write_bot_status(config, args.output_file, status)