blob: cf136f6e481a976bfa7e4c9172a5b1d96477f04a [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):
45 if bid != -1:
46 contents, err = wget(session,
47 "{}/api/v2/builds/{}/changes"
48 .format(base_url, bid))
49 if err or len(contents['changes']) == 0:
50 return ""
51 return contents['changes'][0]['revision']
52 return ""
53
54 revision = wget_build_rev(buildid)
55 prev_revision = wget_build_rev(prev_buildid)
56 if not prev_revision:
57 return "{:.{width}}".format(revision, width=GIT_SHORT_LEN)
58 else:
59 return "{:.{width}}-{:.{width}}".format(revision, prev_revision,
60 width=GIT_SHORT_LEN)
61
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):
75 contents, err = wget(session, "{}/api/v2/builds/{}/steps"
76 .format(base_url, buildid))
77 if err:
78 return ""
79 for step in contents["steps"]:
80 if step["results"] in RESULT_COLORS.keys():
81 yield (step["name"], step["results"])
82
83
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030084# Get the status of a individual bot BOT. Returns a dict with the
85# information.
86def get_bot_status(session, bot, base_url, builder_url, build_url):
87 (contents, err) = wget(session,
88 "{}/api/v2/{}/{}/{}"
89 .format(base_url, builder_url, bot, build_url))
90 if err:
91 return { 'fail' : err }
92
93 builds = contents
94
95 status = {}
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']:
99 status['builderid'] = build['builderid']
100 status['number'] = build['number']
101 status['state'] = build['state_string']
102 delta = int(build['complete_at']) - int(build['started_at'])
103 status['time'] = str(timedelta(seconds=delta))
104 if build['state_string'] != 'build successful':
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300105 status['fail'] = True
106
107 try:
108 prev_buildid = next(reversed_builds)['buildid']
109 except StopIteration:
110 prev_buildid = -1;
111 status['changes'] = get_bot_failure_changes(session, base_url,
112 build['buildid'],
113 prev_buildid)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000114 status['steps'] = list(get_bot_failing_steps(session, base_url,
115 build['buildid']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300116 else:
117 status['fail'] = False
118 break
119 return status
120
121
122def bot_status(config_file, output_file):
123 temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
124
125 today = "{}\n".format(datetime.today().ctime())
126
127 session = requests.Session()
128
129 # Get status for all bots
130 bot_cache = {}
131 for server in filter(not_ignored, config):
132 base_url = server['base_url']
133 builder_url = server['builder_url']
134 build_url = server['build_url']
135 logging.debug('Parsing server {}...'.format(server['name']))
136 for builder in server['builders']:
137 logging.debug(' Parsing builders {}...'.format(builder['name']))
138 for bot in builder['bots']:
139 bot_key = "{}/{}".format(base_url, bot['name'])
140 if bot_key in bot_cache:
141 continue
142 logging.debug(' Parsing bot {}...'.format(bot['name']))
143 status = get_bot_status(session, bot['name'], base_url, builder_url,
144 build_url)
145 if not_ignored(bot):
146 fail = 'fail' in status
147 logging.debug(" FAIL" if status['fail'] else " PASS")
148 bot_cache[bot_key] = status
149
150 # Dump all servers / bots
151 for server in filter(not_ignored, config):
152 base_url = server['base_url']
153 builder_url = server['builder_url']
154 build_url = server['build_url']
155 favicon = 'fail.ico' if fail else 'ok.ico'
156 temp.write("<link rel=\"shortcut icon\" href=\"{}\" "
157 "type=\"image/x-icon\"/>\n".format(favicon))
158 temp.write("<table cellspacing=1 cellpadding=2>\n")
159 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
160 temp.write("<tr><th colspan=5>{} @ {}</td><tr>\n"
161 .format(server['name'], today))
162
163 for builder in server['builders']:
164 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
165 temp.write("<tr><th colspan=5>{}</td><tr>\n".format(builder['name']))
166 temp.write("<tr><th>Buildbot</th><th>Status</th><th>Time</th>"
Oliver Stannard91688ff2021-01-07 10:27:27 +0000167 "<th>Build #</th><th>Commits</th><th>Failing steps</th></tr>\n")
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300168 for bot in builder['bots']:
169 temp.write("<tr>\n")
170 status = bot_cache["{}/{}".format(base_url, bot['name'])]
171 url = "{}/#/{}/{}".format(base_url, builder_url, status['builderid'])
172 temp.write(" <td><a href='{}'>{}</a></td>\n".format(url, bot['name']))
173 temp.write(" <td><font color='{}'>{}</font></td>\n"
174 .format('red' if status['fail'] else 'green',
175 'FAIL' if status['fail'] else 'PASS'))
176 empty_cell=" <td>&nbsp;</td>\n"
177 if 'time' in status:
178 temp.write(" <td>{}</td>\n".format(status['time']))
179 else:
180 temp.write(empty_cell)
181 if 'number' in status:
182 build_url = "{}/builds/{}".format(url, status['number'])
183 temp.write(" <td><a href='{}'>{}</a></td>\n".format(build_url, status['number']))
184 else:
185 temp.write(empty_cell)
186 if 'changes' in status:
187 temp.write(" <td>{}</td>\n".format(status['changes']))
188 else:
189 temp.write(empty_cell)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000190 if 'steps' in status and status['steps']:
191 def render_step(name, result):
192 return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
193 step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
194 temp.write(" <td style=\"text-align:center\">{}</td>\n".format(step_list))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300195 else:
196 temp.write(empty_cell)
197 temp.write("</tr>\n")
198 temp.write("</table>\n")
199
200 # Move temp to main (atomic change)
201 temp.close()
David Spickettaa155be2021-02-25 14:30:09 +0000202 shutil.move(temp.name, sys.argv[2])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300203
204
205if __name__ == "__main__":
206 parser = argparse.ArgumentParser()
207 parser.add_argument('-d', dest='debug', action='store_true')
208 parser.add_argument('config_file',
209 help='Bots description in JSON format')
210 parser.add_argument('output_file',
211 help='output HTML path')
212 args = parser.parse_args()
213
214 if args.debug:
215 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
216
217 try:
218 with open(args.config_file, "r") as f:
219 config = json.load(f)
220 except IOError as e:
221 print("error: failed to read {} config file: {}".format(sys.argv[1], e))
222 sys.exit(os.EX_CONFIG)
223
224 bot_status(config, args.output_file)