blob: 25ca4fe27700ebc3cc03915b0e8295efa92d41f7 [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
David Spickette88fe592021-03-22 12:25:13 +000032# Returns the parsed json URL or raises an exception
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030033def wget(session, url):
David Spickette88fe592021-03-22 12:25:13 +000034 return session.get(url).json()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030035
36
37# Returns a string with the GIT revision usesd on build BUILDID and
38# PREV_BUILDID in the form '<id_buildid>-<id_prev_buildid>'.
39def get_bot_failure_changes(session, base_url, buildid, prev_buildid):
40 def wget_build_rev(bid):
David Spickette88fe592021-03-22 12:25:13 +000041 try:
42 contents = wget(session,
43 "{}/api/v2/builds/{}/changes"
44 .format(base_url, bid))
45 except requests.exceptions.RequestException:
David Spickett7f18f4d2021-03-22 11:49:17 +000046 return None
David Spickette88fe592021-03-22 12:25:13 +000047 changes = contents['changes']
48 if changes:
49 return changes[0]['revision']
50 return None
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030051
David Spickett7f18f4d2021-03-22 11:49:17 +000052 revision = wget_build_rev(buildid)[:GIT_SHORT_LEN]
53 prev_revision = None
54 if prev_buildid is not None:
55 prev_revision = wget_build_rev(prev_buildid)
56
57 if prev_revision is None:
58 return "{}".format(revision)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030059 else:
David Spickett7f18f4d2021-03-22 11:49:17 +000060 return "{}-{}".format(revision, prev_revision[:GIT_SHORT_LEN])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030061
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):
David Spickette88fe592021-03-22 12:25:13 +000075 try:
76 contents = wget(session, "{}/api/v2/builds/{}/steps"
77 .format(base_url, buildid))
78 except requests.exceptions.RequestException:
Oliver Stannard91688ff2021-01-07 10:27:27 +000079 return ""
David Spickette88fe592021-03-22 12:25:13 +000080
Oliver Stannard91688ff2021-01-07 10:27:27 +000081 for step in contents["steps"]:
David Spickett7f18f4d2021-03-22 11:49:17 +000082 if step["results"] in RESULT_COLORS:
Oliver Stannard91688ff2021-01-07 10:27:27 +000083 yield (step["name"], step["results"])
84
85
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030086# Get the status of a individual bot BOT. Returns a dict with the
87# information.
88def get_bot_status(session, bot, base_url, builder_url, build_url):
David Spickette88fe592021-03-22 12:25:13 +000089 try:
90 builds = wget(session,
91 "{}/api/v2/{}/{}/{}"
92 .format(base_url, builder_url, bot, build_url))
93 except requests.exceptions.RequestException as e:
94 return {'fail': True}
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030095
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']:
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030099 delta = int(build['complete_at']) - int(build['started_at'])
David Spickett7f18f4d2021-03-22 11:49:17 +0000100 status = {
101 'builderid': build['builderid'],
102 'number': build['number'],
103 'state': build['state_string'],
104 'time': timedelta(seconds=delta),
105 'fail': build['state_string'] != 'build successful',
106 }
107 if status['fail']:
108 buildid = build['buildid']
109 prev_buildid = next(reversed_builds, None)['buildid']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300110 status['changes'] = get_bot_failure_changes(session, base_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000111 buildid,
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300112 prev_buildid)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000113 status['steps'] = list(get_bot_failing_steps(session, base_url,
David Spickett7f18f4d2021-03-22 11:49:17 +0000114 buildid))
115
116 return status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300117
118
119def bot_status(config_file, output_file):
120 temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
121
122 today = "{}\n".format(datetime.today().ctime())
123
124 session = requests.Session()
125
126 # Get status for all bots
127 bot_cache = {}
128 for server in filter(not_ignored, config):
129 base_url = server['base_url']
130 builder_url = server['builder_url']
131 build_url = server['build_url']
132 logging.debug('Parsing server {}...'.format(server['name']))
133 for builder in server['builders']:
134 logging.debug(' Parsing builders {}...'.format(builder['name']))
135 for bot in builder['bots']:
136 bot_key = "{}/{}".format(base_url, bot['name'])
137 if bot_key in bot_cache:
138 continue
139 logging.debug(' Parsing bot {}...'.format(bot['name']))
140 status = get_bot_status(session, bot['name'], base_url, builder_url,
141 build_url)
142 if not_ignored(bot):
143 fail = 'fail' in status
144 logging.debug(" FAIL" if status['fail'] else " PASS")
145 bot_cache[bot_key] = status
146
147 # Dump all servers / bots
148 for server in filter(not_ignored, config):
149 base_url = server['base_url']
150 builder_url = server['builder_url']
151 build_url = server['build_url']
152 favicon = 'fail.ico' if fail else 'ok.ico'
153 temp.write("<link rel=\"shortcut icon\" href=\"{}\" "
154 "type=\"image/x-icon\"/>\n".format(favicon))
155 temp.write("<table cellspacing=1 cellpadding=2>\n")
156 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
157 temp.write("<tr><th colspan=5>{} @ {}</td><tr>\n"
158 .format(server['name'], today))
159
160 for builder in server['builders']:
161 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
162 temp.write("<tr><th colspan=5>{}</td><tr>\n".format(builder['name']))
163 temp.write("<tr><th>Buildbot</th><th>Status</th><th>Time</th>"
Oliver Stannard91688ff2021-01-07 10:27:27 +0000164 "<th>Build #</th><th>Commits</th><th>Failing steps</th></tr>\n")
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300165 for bot in builder['bots']:
166 temp.write("<tr>\n")
167 status = bot_cache["{}/{}".format(base_url, bot['name'])]
168 url = "{}/#/{}/{}".format(base_url, builder_url, status['builderid'])
169 temp.write(" <td><a href='{}'>{}</a></td>\n".format(url, bot['name']))
170 temp.write(" <td><font color='{}'>{}</font></td>\n"
171 .format('red' if status['fail'] else 'green',
172 'FAIL' if status['fail'] else 'PASS'))
173 empty_cell=" <td>&nbsp;</td>\n"
174 if 'time' in status:
175 temp.write(" <td>{}</td>\n".format(status['time']))
176 else:
177 temp.write(empty_cell)
178 if 'number' in status:
179 build_url = "{}/builds/{}".format(url, status['number'])
180 temp.write(" <td><a href='{}'>{}</a></td>\n".format(build_url, status['number']))
181 else:
182 temp.write(empty_cell)
183 if 'changes' in status:
184 temp.write(" <td>{}</td>\n".format(status['changes']))
185 else:
186 temp.write(empty_cell)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000187 if 'steps' in status and status['steps']:
188 def render_step(name, result):
189 return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
190 step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
191 temp.write(" <td style=\"text-align:center\">{}</td>\n".format(step_list))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300192 else:
193 temp.write(empty_cell)
194 temp.write("</tr>\n")
195 temp.write("</table>\n")
196
197 # Move temp to main (atomic change)
198 temp.close()
David Spickett7f18f4d2021-03-22 11:49:17 +0000199 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300200
201
202if __name__ == "__main__":
203 parser = argparse.ArgumentParser()
204 parser.add_argument('-d', dest='debug', action='store_true')
205 parser.add_argument('config_file',
206 help='Bots description in JSON format')
207 parser.add_argument('output_file',
208 help='output HTML path')
209 args = parser.parse_args()
210
211 if args.debug:
212 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
213
214 try:
215 with open(args.config_file, "r") as f:
216 config = json.load(f)
217 except IOError as e:
David Spickett7f18f4d2021-03-22 11:49:17 +0000218 print("error: failed to read {} config file: {}".format(args.config_file, e))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300219 sys.exit(os.EX_CONFIG)
220
221 bot_status(config, args.output_file)