blob: 5a54299d207b32295c16a51654b0a66f25d9e484 [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
David Spickett30a986f2021-04-29 09:37:00 +010023from buildkite_status import get_buildkite_bots_status
24
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030025# The GIT revision length used on 'Commits' error display.
26GIT_SHORT_LEN=7
27
28def ignored(s):
29 return 'ignore' in s and s['ignore']
30def not_ignored(s):
31 return not ignored(s)
32
33
David Spickette88fe592021-03-22 12:25:13 +000034# Returns the parsed json URL or raises an exception
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030035def wget(session, url):
David Spickett8306ed82021-12-06 10:40:36 +000036 got = session.get(url)
37 got.raise_for_status()
38 return got.json()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030039
40
41# Returns a string with the GIT revision usesd on build BUILDID and
42# PREV_BUILDID in the form '<id_buildid>-<id_prev_buildid>'.
43def get_bot_failure_changes(session, base_url, buildid, prev_buildid):
44 def wget_build_rev(bid):
David Spickette88fe592021-03-22 12:25:13 +000045 try:
46 contents = wget(session,
47 "{}/api/v2/builds/{}/changes"
48 .format(base_url, bid))
49 except requests.exceptions.RequestException:
David Spickett8306ed82021-12-06 10:40:36 +000050 logging.debug(" Couldn't get changes for build {}!".format(buildid))
David Spickett7f18f4d2021-03-22 11:49:17 +000051 return None
David Spickette88fe592021-03-22 12:25:13 +000052 changes = contents['changes']
53 if changes:
54 return changes[0]['revision']
55 return None
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030056
David Spickett7f18f4d2021-03-22 11:49:17 +000057 revision = wget_build_rev(buildid)[:GIT_SHORT_LEN]
58 prev_revision = None
59 if prev_buildid is not None:
60 prev_revision = wget_build_rev(prev_buildid)
61
62 if prev_revision is None:
63 return "{}".format(revision)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030064 else:
David Spickett7f18f4d2021-03-22 11:49:17 +000065 return "{}-{}".format(revision, prev_revision[:GIT_SHORT_LEN])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030066
67
Oliver Stannard91688ff2021-01-07 10:27:27 +000068# Map from buildbot status codes we want to treat as errors to the color they
69# should be shown in. The codes are documented at
70# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
71# and these colors match the suggested ones there.
72RESULT_COLORS = {
73 2: 'red', # Error
74 4: 'purple', # Exception
75 5: 'purple', # Retry
76 6: 'pink', # Cancelled
77}
78
79def get_bot_failing_steps(session, base_url, buildid):
David Spickette88fe592021-03-22 12:25:13 +000080 try:
81 contents = wget(session, "{}/api/v2/builds/{}/steps"
82 .format(base_url, buildid))
83 except requests.exceptions.RequestException:
Oliver Stannard91688ff2021-01-07 10:27:27 +000084 return ""
David Spickette88fe592021-03-22 12:25:13 +000085
Oliver Stannard91688ff2021-01-07 10:27:27 +000086 for step in contents["steps"]:
David Spickett7f18f4d2021-03-22 11:49:17 +000087 if step["results"] in RESULT_COLORS:
Oliver Stannard91688ff2021-01-07 10:27:27 +000088 yield (step["name"], step["results"])
89
90
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030091# Get the status of a individual bot BOT. Returns a dict with the
92# information.
93def get_bot_status(session, bot, base_url, builder_url, build_url):
David Spickette88fe592021-03-22 12:25:13 +000094 try:
95 builds = wget(session,
96 "{}/api/v2/{}/{}/{}"
97 .format(base_url, builder_url, bot, build_url))
98 except requests.exceptions.RequestException as e:
99 return {'fail': True}
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300100
Oliver Stannard46e99032021-01-05 10:30:56 +0000101 reversed_builds = iter(sorted(builds['builds'], key=lambda b: -b["number"]))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300102 for build in reversed_builds:
103 if build['complete']:
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000104 time_since = (int(datetime.now().timestamp()) - int(build['complete_at']))
105 duration = int(build['complete_at']) - int(build['started_at'])
David Spickett30a986f2021-04-29 09:37:00 +0100106 agent_url = "{}/#/{}/{}".format(base_url, builder_url, build['builderid'])
107
David Spickett7f18f4d2021-03-22 11:49:17 +0000108 status = {
David Spickett30a986f2021-04-29 09:37:00 +0100109 'builder_url': agent_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000110 'number': build['number'],
David Spickett30a986f2021-04-29 09:37:00 +0100111 'build_url': "{}/builds/{}".format(agent_url, build['number']),
David Spickett7f18f4d2021-03-22 11:49:17 +0000112 'state': build['state_string'],
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000113 'time_since': timedelta(seconds=time_since),
114 'duration': timedelta(seconds=duration),
David Spickett7f18f4d2021-03-22 11:49:17 +0000115 'fail': build['state_string'] != 'build successful',
116 }
David Spickett30a986f2021-04-29 09:37:00 +0100117
David Spickett7f18f4d2021-03-22 11:49:17 +0000118 if status['fail']:
119 buildid = build['buildid']
120 prev_buildid = next(reversed_builds, None)['buildid']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300121 status['changes'] = get_bot_failure_changes(session, base_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000122 buildid,
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300123 prev_buildid)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000124 status['steps'] = list(get_bot_failing_steps(session, base_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000125 buildid))
126
127 return status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300128
129
David Spickett85355fa2021-03-22 15:41:41 +0000130# Get status for all bots named in the config
131# Return a dictionary of (base_url, bot name) -> status info
David Spickett30a986f2021-04-29 09:37:00 +0100132def get_buildbot_bots_status(config):
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300133 session = requests.Session()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300134 bot_cache = {}
David Spickettf006c372021-03-22 12:54:12 +0000135
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300136 for server in filter(not_ignored, config):
David Spickett30a986f2021-04-29 09:37:00 +0100137 if server['name'] == "Buildkite":
138 continue
139
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300140 base_url = server['base_url']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300141 logging.debug('Parsing server {}...'.format(server['name']))
142 for builder in server['builders']:
143 logging.debug(' Parsing builders {}...'.format(builder['name']))
144 for bot in builder['bots']:
David Spickett85355fa2021-03-22 15:41:41 +0000145 bot_key = (base_url, bot['name'])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300146 if bot_key in bot_cache:
147 continue
David Spickett85355fa2021-03-22 15:41:41 +0000148
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300149 logging.debug(' Parsing bot {}...'.format(bot['name']))
David Spickett85355fa2021-03-22 15:41:41 +0000150 status = get_bot_status(session, bot['name'], base_url, server['builder_url'],
151 server['build_url'])
David Spickettf2c82dd2021-06-24 10:01:33 +0100152 if status is not None:
David Spickett8306ed82021-12-06 10:40:36 +0000153 logging.debug(" Bot status: " + ("FAIL" if status['fail'] else "PASS"))
David Spickettf2c82dd2021-06-24 10:01:33 +0100154 bot_cache[bot_key] = status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300155
David Spickett85355fa2021-03-22 15:41:41 +0000156 return bot_cache
157
158def write_bot_status(config, output_file, bots_status):
159 temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
160 today = "{}\n".format(datetime.today().ctime())
161 # Whether we use the fail favicon or not
162 found_failure = False
David Spickettf006c372021-03-22 12:54:12 +0000163
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300164 # Dump all servers / bots
165 for server in filter(not_ignored, config):
166 base_url = server['base_url']
167 builder_url = server['builder_url']
168 build_url = server['build_url']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300169 temp.write("<table cellspacing=1 cellpadding=2>\n")
170 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
171 temp.write("<tr><th colspan=5>{} @ {}</td><tr>\n"
172 .format(server['name'], today))
173
174 for builder in server['builders']:
175 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
176 temp.write("<tr><th colspan=5>{}</td><tr>\n".format(builder['name']))
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000177 temp.write("<tr><th>Buildbot</th><th>Status</th><th>T Since</th>"
178 "<th>Duration</th><th>Build #</th><th>Commits</th>"
179 "<th>Failing steps</th></tr>\n")
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300180 for bot in builder['bots']:
181 temp.write("<tr>\n")
David Spickettf2c82dd2021-06-24 10:01:33 +0100182 try:
183 status = bots_status[(base_url, bot['name'])]
184 except KeyError:
David Spickettf2d4c482021-06-24 10:07:52 +0100185 temp.write(" <td>{} is offline!</td>\n</tr>\n".format(bot['name']))
David Spickettf2c82dd2021-06-24 10:01:33 +0100186 continue
David Spickett30a986f2021-04-29 09:37:00 +0100187 else:
188 if not status.get('valid', True):
189 temp.write(" <td>Could not read status for {}!</td>\n</tr>\n".format(bot['name']))
190 continue
David Spickettf2c82dd2021-06-24 10:01:33 +0100191
David Spickett85355fa2021-03-22 15:41:41 +0000192 found_failure |= status['fail']
David Spickett30a986f2021-04-29 09:37:00 +0100193
194 temp.write(" <td><a href='{}'>{}</a></td>\n".format(
195 status['builder_url'], bot['name']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300196 temp.write(" <td><font color='{}'>{}</font></td>\n"
197 .format('red' if status['fail'] else 'green',
198 'FAIL' if status['fail'] else 'PASS'))
199 empty_cell=" <td>&nbsp;</td>\n"
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000200 if 'time_since' in status:
201 temp.write(" <td>{}</td>\n".format(status['time_since']))
202 else:
203 temp.write(empty_cell)
204 if 'duration' in status:
205 temp.write(" <td>{}</td>\n".format(status['duration']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300206 else:
207 temp.write(empty_cell)
208 if 'number' in status:
David Spickett30a986f2021-04-29 09:37:00 +0100209 temp.write(" <td><a href='{}'>{}</a></td>\n".format(
210 status['build_url'], status['number']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300211 else:
212 temp.write(empty_cell)
213 if 'changes' in status:
214 temp.write(" <td>{}</td>\n".format(status['changes']))
215 else:
216 temp.write(empty_cell)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000217 if 'steps' in status and status['steps']:
218 def render_step(name, result):
219 return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
220 step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
221 temp.write(" <td style=\"text-align:center\">{}</td>\n".format(step_list))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300222 else:
223 temp.write(empty_cell)
224 temp.write("</tr>\n")
225 temp.write("</table>\n")
226
David Spickett85355fa2021-03-22 15:41:41 +0000227 temp.write("<link rel=\"shortcut icon\" href=\"{}\" "
228 "type=\"image/x-icon\"/>\n".format(
229 'fail.ico' if found_failure else 'ok.ico'))
230
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300231 # Move temp to main (atomic change)
232 temp.close()
David Spickett7f18f4d2021-03-22 11:49:17 +0000233 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300234
235
236if __name__ == "__main__":
237 parser = argparse.ArgumentParser()
David Spickettec2166c2021-07-19 14:17:29 +0100238 parser.add_argument('-d', dest='debug', action='store_true',
239 help='show debug log messages')
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300240 parser.add_argument('config_file',
241 help='Bots description in JSON format')
242 parser.add_argument('output_file',
243 help='output HTML path')
244 args = parser.parse_args()
245
246 if args.debug:
247 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
248
249 try:
250 with open(args.config_file, "r") as f:
251 config = json.load(f)
252 except IOError as e:
David Spickett7f18f4d2021-03-22 11:49:17 +0000253 print("error: failed to read {} config file: {}".format(args.config_file, e))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300254 sys.exit(os.EX_CONFIG)
255
David Spickett30a986f2021-04-29 09:37:00 +0100256 status = get_buildbot_bots_status(config)
257 status.update(get_buildkite_bots_status(config))
258 write_bot_status(config, args.output_file, status)