blob: 38d9f5fd82891649ebead1f5ce485dffcdff1460 [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
32# Returns the parsed json URL or and error string.
33def wget(session, url):
34 try:
35 req = session.get(url)
36 except requests.exceptions.RequestException as e:
37 return str(e), True
38 return req.json(), False
39
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 Spickett7f18f4d2021-03-22 11:49:17 +000045 contents, err = wget(session,
46 "{}/api/v2/builds/{}/changes"
47 .format(base_url, bid))
48 changes = contents['changes']
49 if err or not changes:
50 return None
51 return changes[0]['revision']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030052
David Spickett7f18f4d2021-03-22 11:49:17 +000053 revision = wget_build_rev(buildid)[:GIT_SHORT_LEN]
54 prev_revision = None
55 if prev_buildid is not None:
56 prev_revision = wget_build_rev(prev_buildid)
57
58 if prev_revision is None:
59 return "{}".format(revision)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030060 else:
David Spickett7f18f4d2021-03-22 11:49:17 +000061 return "{}-{}".format(revision, prev_revision[:GIT_SHORT_LEN])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030062
63
Oliver Stannard91688ff2021-01-07 10:27:27 +000064# Map from buildbot status codes we want to treat as errors to the color they
65# should be shown in. The codes are documented at
66# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
67# and these colors match the suggested ones there.
68RESULT_COLORS = {
69 2: 'red', # Error
70 4: 'purple', # Exception
71 5: 'purple', # Retry
72 6: 'pink', # Cancelled
73}
74
75def get_bot_failing_steps(session, base_url, buildid):
76 contents, err = wget(session, "{}/api/v2/builds/{}/steps"
77 .format(base_url, buildid))
78 if err:
79 return ""
80 for step in contents["steps"]:
David Spickett7f18f4d2021-03-22 11:49:17 +000081 if step["results"] in RESULT_COLORS:
Oliver Stannard91688ff2021-01-07 10:27:27 +000082 yield (step["name"], step["results"])
83
84
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030085# Get the status of a individual bot BOT. Returns a dict with the
86# information.
87def get_bot_status(session, bot, base_url, builder_url, build_url):
David Spickett7f18f4d2021-03-22 11:49:17 +000088 (builds, err) = wget(session,
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030089 "{}/api/v2/{}/{}/{}"
90 .format(base_url, builder_url, bot, build_url))
91 if err:
92 return { 'fail' : err }
93
Oliver Stannard46e99032021-01-05 10:30:56 +000094 reversed_builds = iter(sorted(builds['builds'], key=lambda b: -b["number"]))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030095 for build in reversed_builds:
96 if build['complete']:
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030097 delta = int(build['complete_at']) - int(build['started_at'])
David Spickett7f18f4d2021-03-22 11:49:17 +000098 status = {
99 'builderid': build['builderid'],
100 'number': build['number'],
101 'state': build['state_string'],
102 'time': timedelta(seconds=delta),
103 'fail': build['state_string'] != 'build successful',
104 }
105 if status['fail']:
106 buildid = build['buildid']
107 prev_buildid = next(reversed_builds, None)['buildid']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300108 status['changes'] = get_bot_failure_changes(session, base_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000109 buildid,
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300110 prev_buildid)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000111 status['steps'] = list(get_bot_failing_steps(session, base_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000112 buildid))
113
114 return status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300115
116
117def bot_status(config_file, output_file):
118 temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
119
120 today = "{}\n".format(datetime.today().ctime())
121
122 session = requests.Session()
123
124 # Get status for all bots
125 bot_cache = {}
126 for server in filter(not_ignored, config):
127 base_url = server['base_url']
128 builder_url = server['builder_url']
129 build_url = server['build_url']
130 logging.debug('Parsing server {}...'.format(server['name']))
131 for builder in server['builders']:
132 logging.debug(' Parsing builders {}...'.format(builder['name']))
133 for bot in builder['bots']:
134 bot_key = "{}/{}".format(base_url, bot['name'])
135 if bot_key in bot_cache:
136 continue
137 logging.debug(' Parsing bot {}...'.format(bot['name']))
138 status = get_bot_status(session, bot['name'], base_url, builder_url,
139 build_url)
140 if not_ignored(bot):
141 fail = 'fail' in status
142 logging.debug(" FAIL" if status['fail'] else " PASS")
143 bot_cache[bot_key] = status
144
145 # Dump all servers / bots
146 for server in filter(not_ignored, config):
147 base_url = server['base_url']
148 builder_url = server['builder_url']
149 build_url = server['build_url']
150 favicon = 'fail.ico' if fail else 'ok.ico'
151 temp.write("<link rel=\"shortcut icon\" href=\"{}\" "
152 "type=\"image/x-icon\"/>\n".format(favicon))
153 temp.write("<table cellspacing=1 cellpadding=2>\n")
154 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
155 temp.write("<tr><th colspan=5>{} @ {}</td><tr>\n"
156 .format(server['name'], today))
157
158 for builder in server['builders']:
159 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
160 temp.write("<tr><th colspan=5>{}</td><tr>\n".format(builder['name']))
161 temp.write("<tr><th>Buildbot</th><th>Status</th><th>Time</th>"
Oliver Stannard91688ff2021-01-07 10:27:27 +0000162 "<th>Build #</th><th>Commits</th><th>Failing steps</th></tr>\n")
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300163 for bot in builder['bots']:
164 temp.write("<tr>\n")
165 status = bot_cache["{}/{}".format(base_url, bot['name'])]
166 url = "{}/#/{}/{}".format(base_url, builder_url, status['builderid'])
167 temp.write(" <td><a href='{}'>{}</a></td>\n".format(url, bot['name']))
168 temp.write(" <td><font color='{}'>{}</font></td>\n"
169 .format('red' if status['fail'] else 'green',
170 'FAIL' if status['fail'] else 'PASS'))
171 empty_cell=" <td>&nbsp;</td>\n"
172 if 'time' in status:
173 temp.write(" <td>{}</td>\n".format(status['time']))
174 else:
175 temp.write(empty_cell)
176 if 'number' in status:
177 build_url = "{}/builds/{}".format(url, status['number'])
178 temp.write(" <td><a href='{}'>{}</a></td>\n".format(build_url, status['number']))
179 else:
180 temp.write(empty_cell)
181 if 'changes' in status:
182 temp.write(" <td>{}</td>\n".format(status['changes']))
183 else:
184 temp.write(empty_cell)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000185 if 'steps' in status and status['steps']:
186 def render_step(name, result):
187 return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
188 step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
189 temp.write(" <td style=\"text-align:center\">{}</td>\n".format(step_list))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300190 else:
191 temp.write(empty_cell)
192 temp.write("</tr>\n")
193 temp.write("</table>\n")
194
195 # Move temp to main (atomic change)
196 temp.close()
David Spickett7f18f4d2021-03-22 11:49:17 +0000197 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300198
199
200if __name__ == "__main__":
201 parser = argparse.ArgumentParser()
202 parser.add_argument('-d', dest='debug', action='store_true')
203 parser.add_argument('config_file',
204 help='Bots description in JSON format')
205 parser.add_argument('output_file',
206 help='output HTML path')
207 args = parser.parse_args()
208
209 if args.debug:
210 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
211
212 try:
213 with open(args.config_file, "r") as f:
214 config = json.load(f)
215 except IOError as e:
David Spickett7f18f4d2021-03-22 11:49:17 +0000216 print("error: failed to read {} config file: {}".format(args.config_file, e))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300217 sys.exit(os.EX_CONFIG)
218
219 bot_status(config, args.output_file)