blob: 40630e52471c627ed7af3f32b423bb3217e6db69 [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
17from datetime import datetime, timedelta
18# The requests allows HTTP keep-alive which re-uses the same TCP connection
19# to download multiple files.
20import requests
21
22# The GIT revision length used on 'Commits' error display.
23GIT_SHORT_LEN=7
24
25def ignored(s):
26 return 'ignore' in s and s['ignore']
27def not_ignored(s):
28 return not ignored(s)
29
30
31# Returns the parsed json URL or and error string.
32def wget(session, url):
33 try:
34 req = session.get(url)
35 except requests.exceptions.RequestException as e:
36 return str(e), True
37 return req.json(), False
38
39
40# Returns a string with the GIT revision usesd on build BUILDID and
41# PREV_BUILDID in the form '<id_buildid>-<id_prev_buildid>'.
42def get_bot_failure_changes(session, base_url, buildid, prev_buildid):
43 def wget_build_rev(bid):
44 if bid != -1:
45 contents, err = wget(session,
46 "{}/api/v2/builds/{}/changes"
47 .format(base_url, bid))
48 if err or len(contents['changes']) == 0:
49 return ""
50 return contents['changes'][0]['revision']
51 return ""
52
53 revision = wget_build_rev(buildid)
54 prev_revision = wget_build_rev(prev_buildid)
55 if not prev_revision:
56 return "{:.{width}}".format(revision, width=GIT_SHORT_LEN)
57 else:
58 return "{:.{width}}-{:.{width}}".format(revision, prev_revision,
59 width=GIT_SHORT_LEN)
60
61
Oliver Stannard91688ff2021-01-07 10:27:27 +000062# Map from buildbot status codes we want to treat as errors to the color they
63# should be shown in. The codes are documented at
64# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
65# and these colors match the suggested ones there.
66RESULT_COLORS = {
67 2: 'red', # Error
68 4: 'purple', # Exception
69 5: 'purple', # Retry
70 6: 'pink', # Cancelled
71}
72
73def get_bot_failing_steps(session, base_url, buildid):
74 contents, err = wget(session, "{}/api/v2/builds/{}/steps"
75 .format(base_url, buildid))
76 if err:
77 return ""
78 for step in contents["steps"]:
79 if step["results"] in RESULT_COLORS.keys():
80 yield (step["name"], step["results"])
81
82
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030083# Get the status of a individual bot BOT. Returns a dict with the
84# information.
85def get_bot_status(session, bot, base_url, builder_url, build_url):
86 (contents, err) = wget(session,
87 "{}/api/v2/{}/{}/{}"
88 .format(base_url, builder_url, bot, build_url))
89 if err:
90 return { 'fail' : err }
91
92 builds = contents
93
94 status = {}
Oliver Stannard46e99032021-01-05 10:30:56 +000095 reversed_builds = iter(sorted(builds['builds'], key=lambda b: -b["number"]))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030096 for build in reversed_builds:
97 if build['complete']:
98 status['builderid'] = build['builderid']
99 status['number'] = build['number']
100 status['state'] = build['state_string']
101 delta = int(build['complete_at']) - int(build['started_at'])
102 status['time'] = str(timedelta(seconds=delta))
103 if build['state_string'] != 'build successful':
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300104 status['fail'] = True
105
106 try:
107 prev_buildid = next(reversed_builds)['buildid']
108 except StopIteration:
109 prev_buildid = -1;
110 status['changes'] = get_bot_failure_changes(session, base_url,
111 build['buildid'],
112 prev_buildid)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000113 status['steps'] = list(get_bot_failing_steps(session, base_url,
114 build['buildid']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300115 else:
116 status['fail'] = False
117 break
118 return status
119
120
121def bot_status(config_file, output_file):
122 temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
123
124 today = "{}\n".format(datetime.today().ctime())
125
126 session = requests.Session()
127
128 # Get status for all bots
129 bot_cache = {}
130 for server in filter(not_ignored, config):
131 base_url = server['base_url']
132 builder_url = server['builder_url']
133 build_url = server['build_url']
134 logging.debug('Parsing server {}...'.format(server['name']))
135 for builder in server['builders']:
136 logging.debug(' Parsing builders {}...'.format(builder['name']))
137 for bot in builder['bots']:
138 bot_key = "{}/{}".format(base_url, bot['name'])
139 if bot_key in bot_cache:
140 continue
141 logging.debug(' Parsing bot {}...'.format(bot['name']))
142 status = get_bot_status(session, bot['name'], base_url, builder_url,
143 build_url)
144 if not_ignored(bot):
145 fail = 'fail' in status
146 logging.debug(" FAIL" if status['fail'] else " PASS")
147 bot_cache[bot_key] = status
148
149 # Dump all servers / bots
150 for server in filter(not_ignored, config):
151 base_url = server['base_url']
152 builder_url = server['builder_url']
153 build_url = server['build_url']
154 favicon = 'fail.ico' if fail else 'ok.ico'
155 temp.write("<link rel=\"shortcut icon\" href=\"{}\" "
156 "type=\"image/x-icon\"/>\n".format(favicon))
157 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']))
165 temp.write("<tr><th>Buildbot</th><th>Status</th><th>Time</th>"
Oliver Stannard91688ff2021-01-07 10:27:27 +0000166 "<th>Build #</th><th>Commits</th><th>Failing steps</th></tr>\n")
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300167 for bot in builder['bots']:
168 temp.write("<tr>\n")
169 status = bot_cache["{}/{}".format(base_url, bot['name'])]
170 url = "{}/#/{}/{}".format(base_url, builder_url, status['builderid'])
171 temp.write(" <td><a href='{}'>{}</a></td>\n".format(url, bot['name']))
172 temp.write(" <td><font color='{}'>{}</font></td>\n"
173 .format('red' if status['fail'] else 'green',
174 'FAIL' if status['fail'] else 'PASS'))
175 empty_cell=" <td>&nbsp;</td>\n"
176 if 'time' in status:
177 temp.write(" <td>{}</td>\n".format(status['time']))
178 else:
179 temp.write(empty_cell)
180 if 'number' in status:
181 build_url = "{}/builds/{}".format(url, status['number'])
182 temp.write(" <td><a href='{}'>{}</a></td>\n".format(build_url, status['number']))
183 else:
184 temp.write(empty_cell)
185 if 'changes' in status:
186 temp.write(" <td>{}</td>\n".format(status['changes']))
187 else:
188 temp.write(empty_cell)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000189 if 'steps' in status and status['steps']:
190 def render_step(name, result):
191 return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
192 step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
193 temp.write(" <td style=\"text-align:center\">{}</td>\n".format(step_list))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300194 else:
195 temp.write(empty_cell)
196 temp.write("</tr>\n")
197 temp.write("</table>\n")
198
199 # Move temp to main (atomic change)
200 temp.close()
201 os.rename(temp.name, sys.argv[2])
202
203
204if __name__ == "__main__":
205 parser = argparse.ArgumentParser()
206 parser.add_argument('-d', dest='debug', action='store_true')
207 parser.add_argument('config_file',
208 help='Bots description in JSON format')
209 parser.add_argument('output_file',
210 help='output HTML path')
211 args = parser.parse_args()
212
213 if args.debug:
214 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
215
216 try:
217 with open(args.config_file, "r") as f:
218 config = json.load(f)
219 except IOError as e:
220 print("error: failed to read {} config file: {}".format(sys.argv[1], e))
221 sys.exit(os.EX_CONFIG)
222
223 bot_status(config, args.output_file)