blob: be1801f90f399e7972fa86dce241e258301a3acc [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
David Spickett8822bbd2023-06-12 14:02:41 +010018import time
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030019from datetime import datetime, timedelta
20# The requests allows HTTP keep-alive which re-uses the same TCP connection
21# to download multiple files.
22import requests
David Spickett55449c62021-12-13 12:57:33 +000023from textwrap import dedent
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030024
David Spickett30a986f2021-04-29 09:37:00 +010025from buildkite_status import get_buildkite_bots_status
26
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030027def ignored(s):
28 return 'ignore' in s and s['ignore']
29def not_ignored(s):
30 return not ignored(s)
31
32
David Spickette88fe592021-03-22 12:25:13 +000033# Returns the parsed json URL or raises an exception
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030034def wget(session, url):
David Spickett8306ed82021-12-06 10:40:36 +000035 got = session.get(url)
36 got.raise_for_status()
37 return got.json()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030038
39
Oliver Stannard91688ff2021-01-07 10:27:27 +000040# Map from buildbot status codes we want to treat as errors to the color they
41# should be shown in. The codes are documented at
42# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
43# and these colors match the suggested ones there.
44RESULT_COLORS = {
45 2: 'red', # Error
46 4: 'purple', # Exception
47 5: 'purple', # Retry
48 6: 'pink', # Cancelled
49}
50
51def get_bot_failing_steps(session, base_url, buildid):
David Spickette88fe592021-03-22 12:25:13 +000052 try:
53 contents = wget(session, "{}/api/v2/builds/{}/steps"
54 .format(base_url, buildid))
55 except requests.exceptions.RequestException:
Oliver Stannard91688ff2021-01-07 10:27:27 +000056 return ""
David Spickette88fe592021-03-22 12:25:13 +000057
Oliver Stannard91688ff2021-01-07 10:27:27 +000058 for step in contents["steps"]:
David Spickett7f18f4d2021-03-22 11:49:17 +000059 if step["results"] in RESULT_COLORS:
Oliver Stannard91688ff2021-01-07 10:27:27 +000060 yield (step["name"], step["results"])
61
62
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030063# Get the status of a individual bot BOT. Returns a dict with the
64# information.
65def get_bot_status(session, bot, base_url, builder_url, build_url):
David Spickette88fe592021-03-22 12:25:13 +000066 try:
67 builds = wget(session,
68 "{}/api/v2/{}/{}/{}"
69 .format(base_url, builder_url, bot, build_url))
70 except requests.exceptions.RequestException as e:
David Spickett7d47cea2022-06-01 14:13:32 +010071 logging.debug(" Couldn't get builds for bot {}!".format(bot))
72 return {'valid': False}
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030073
Oliver Stannard46e99032021-01-05 10:30:56 +000074 reversed_builds = iter(sorted(builds['builds'], key=lambda b: -b["number"]))
David Spickett86f2d472023-06-12 11:21:16 +010075 next_build = None
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030076 for build in reversed_builds:
David Spickett86f2d472023-06-12 11:21:16 +010077 if not build['complete']:
78 next_build = build
79 continue
David Spickett30a986f2021-04-29 09:37:00 +010080
David Spickett86f2d472023-06-12 11:21:16 +010081 time_since = (int(datetime.now().timestamp()) - int(build['complete_at']))
82 duration = int(build['complete_at']) - int(build['started_at'])
83 agent_url = "{}/#/{}/{}".format(base_url, builder_url, build['builderid'])
David Spickett30a986f2021-04-29 09:37:00 +010084
David Spickett86f2d472023-06-12 11:21:16 +010085 status = {
86 'builder_url': agent_url,
87 'number': build['number'],
88 'build_url': "{}/builds/{}".format(agent_url, build['number']),
89 'state': build['state_string'],
90 'time_since': timedelta(seconds=time_since),
91 'duration': timedelta(seconds=duration),
92 'fail': build['state_string'] != 'build successful',
93 'next_in_progress': next_build is not None
94 }
David Spickett7f18f4d2021-03-22 11:49:17 +000095
David Spickett86f2d472023-06-12 11:21:16 +010096 if status['fail']:
97 buildid = build['buildid']
98 status['steps'] = list(get_bot_failing_steps(session, base_url,
99 buildid))
100
101 return status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300102
103
David Spickett85355fa2021-03-22 15:41:41 +0000104# Get status for all bots named in the config
105# Return a dictionary of (base_url, bot name) -> status info
David Spickett30a986f2021-04-29 09:37:00 +0100106def get_buildbot_bots_status(config):
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300107 session = requests.Session()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300108 bot_cache = {}
David Spickettf006c372021-03-22 12:54:12 +0000109
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300110 for server in filter(not_ignored, config):
David Spickett30a986f2021-04-29 09:37:00 +0100111 if server['name'] == "Buildkite":
112 continue
113
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300114 base_url = server['base_url']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300115 logging.debug('Parsing server {}...'.format(server['name']))
116 for builder in server['builders']:
117 logging.debug(' Parsing builders {}...'.format(builder['name']))
118 for bot in builder['bots']:
David Spickett85355fa2021-03-22 15:41:41 +0000119 bot_key = (base_url, bot['name'])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300120 if bot_key in bot_cache:
121 continue
David Spickett85355fa2021-03-22 15:41:41 +0000122
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300123 logging.debug(' Parsing bot {}...'.format(bot['name']))
David Spickett85355fa2021-03-22 15:41:41 +0000124 status = get_bot_status(session, bot['name'], base_url, server['builder_url'],
125 server['build_url'])
David Spickettf2c82dd2021-06-24 10:01:33 +0100126 if status is not None:
David Spickett7d47cea2022-06-01 14:13:32 +0100127 if status.get("valid", True):
128 logging.debug(" Bot status: " + ("FAIL" if status['fail'] else "PASS"))
David Spickettf2c82dd2021-06-24 10:01:33 +0100129 bot_cache[bot_key] = status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300130
David Spickett85355fa2021-03-22 15:41:41 +0000131 return bot_cache
132
133def write_bot_status(config, output_file, bots_status):
134 temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
David Spickettf006c372021-03-22 12:54:12 +0000135
David Spickett55449c62021-12-13 12:57:33 +0000136 temp.write(dedent("""\
David Spickett64161b02022-11-01 09:51:30 +0000137 <!DOCTYPE html>
David Spickett55449c62021-12-13 12:57:33 +0000138 <style>
139 /* Combine the border between cells to prevent 1px gaps
140 in the row background colour. */
141 table, td, th {
142 border-collapse: collapse;
143 }
144 /* Colour every other row in a table body grey. */
145 tbody tr:nth-child(even) td {
146 background-color: #ededed;
147 }
148 </style>"""))
149
David Spickette44441b2023-06-12 12:23:28 +0100150 column_titles = [
151 "Buildbot",
152 "Status",
153 "T Since",
154 "Duration",
155 "Build",
156 "Failing steps",
157 "Build In Progress",
158 ]
159 num_columns = len(column_titles)
160 column_titles_html = "<tr>{}</tr>\n".format(
161 "".join(["<th>{}</th>".format(t) for t in column_titles]))
162
163 # The first table should also say when this was generated.
164 # If we were to put this in its own header only table, it would
165 # not align with the rest because it has no content.
166 first = True
167
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300168 # Dump all servers / bots
169 for server in filter(not_ignored, config):
170 base_url = server['base_url']
171 builder_url = server['builder_url']
172 build_url = server['build_url']
David Spickettec94cc22021-12-13 13:16:05 +0000173
David Spickett55449c62021-12-13 12:57:33 +0000174 temp.write("<table border=0 cellspacing=1 cellpadding=2>\n")
David Spickettec94cc22021-12-13 13:16:05 +0000175 temp.write("<tr><td colspan={}>&nbsp;</td><tr>\n".format(num_columns))
David Spickette44441b2023-06-12 12:23:28 +0100176 if first:
David Spickett8822bbd2023-06-12 14:02:41 +0100177 temp.write("<tr><th colspan={}>Generated {} ({})</td><tr>\n"
178 .format(num_columns, datetime.today().ctime(),
179 time.tzname[time.daylight]))
David Spickette44441b2023-06-12 12:23:28 +0100180 temp.write("<tr><td colspan={}>&nbsp;</td><tr>\n".format(num_columns))
181 first = False
182
183 temp.write("<tr><th colspan={}>{}</td><tr>\n"
184 .format(num_columns, server['name']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300185
186 for builder in server['builders']:
David Spickettec94cc22021-12-13 13:16:05 +0000187 temp.write("<tr><td colspan={}>&nbsp;</td><tr>\n".format(num_columns))
188 temp.write("<tr><th colspan={}>{}</th><tr>\n".format(num_columns, builder['name']))
189 temp.write(column_titles_html)
David Spickett55449c62021-12-13 12:57:33 +0000190 temp.write("<tbody>\n")
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300191 for bot in builder['bots']:
192 temp.write("<tr>\n")
David Spickett7d47cea2022-06-01 14:13:32 +0100193 logging.debug("Writing out status for {}".format(bot['name']))
David Spickettf2c82dd2021-06-24 10:01:33 +0100194 try:
195 status = bots_status[(base_url, bot['name'])]
196 except KeyError:
David Spickettec94cc22021-12-13 13:16:05 +0000197 temp.write(" <td colspan={}>{} is offline!</td>\n</tr>\n"
198 .format(num_columns, bot['name']))
David Spickettf2c82dd2021-06-24 10:01:33 +0100199 continue
David Spickett30a986f2021-04-29 09:37:00 +0100200 else:
201 if not status.get('valid', True):
David Spickettec94cc22021-12-13 13:16:05 +0000202 temp.write(" <td colspan={}>Could not read status for {}!</td>\n</tr>\n"
203 .format(num_columns, bot['name']))
David Spickett30a986f2021-04-29 09:37:00 +0100204 continue
David Spickettf2c82dd2021-06-24 10:01:33 +0100205
David Spickett30a986f2021-04-29 09:37:00 +0100206 temp.write(" <td><a href='{}'>{}</a></td>\n".format(
207 status['builder_url'], bot['name']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300208 temp.write(" <td><font color='{}'>{}</font></td>\n"
209 .format('red' if status['fail'] else 'green',
210 'FAIL' if status['fail'] else 'PASS'))
211 empty_cell=" <td>&nbsp;</td>\n"
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000212 if 'time_since' in status:
David Spickett187f7962022-02-09 12:35:00 +0000213 time_since = status['time_since']
214 # No build should be taking more than a day
215 if time_since > timedelta(hours=24):
216 time_since = "<p style=\"color:red\">{}</p>".format(
217 time_since)
218 else:
219 time_since = str(time_since)
220
221 temp.write(" <td>{}</td>\n".format(time_since))
Maxim Kuvyrkovfdaa4682021-04-14 13:13:14 +0000222 else:
223 temp.write(empty_cell)
224 if 'duration' in status:
225 temp.write(" <td>{}</td>\n".format(status['duration']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300226 else:
227 temp.write(empty_cell)
228 if 'number' in status:
David Spickett30a986f2021-04-29 09:37:00 +0100229 temp.write(" <td><a href='{}'>{}</a></td>\n".format(
230 status['build_url'], status['number']))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300231 else:
232 temp.write(empty_cell)
Oliver Stannard91688ff2021-01-07 10:27:27 +0000233 if 'steps' in status and status['steps']:
234 def render_step(name, result):
235 return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
236 step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
237 temp.write(" <td style=\"text-align:center\">{}</td>\n".format(step_list))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300238 else:
239 temp.write(empty_cell)
David Spickett86f2d472023-06-12 11:21:16 +0100240 if 'next_in_progress' in status:
241 temp.write(" <td>{}</td>\n".format(
242 "Yes" if status['next_in_progress'] else "No"))
243 else:
244 # No value means we don't know either way.
245 temp.write(empty_cell)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300246 temp.write("</tr>\n")
David Spickett55449c62021-12-13 12:57:33 +0000247 temp.write("</tbody>\n")
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300248 temp.write("</table>\n")
249
250 # Move temp to main (atomic change)
251 temp.close()
David Spickett7f18f4d2021-03-22 11:49:17 +0000252 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300253
254
255if __name__ == "__main__":
256 parser = argparse.ArgumentParser()
David Spickettec2166c2021-07-19 14:17:29 +0100257 parser.add_argument('-d', dest='debug', action='store_true',
258 help='show debug log messages')
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300259 parser.add_argument('config_file',
260 help='Bots description in JSON format')
261 parser.add_argument('output_file',
262 help='output HTML path')
263 args = parser.parse_args()
264
265 if args.debug:
266 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
267
268 try:
269 with open(args.config_file, "r") as f:
270 config = json.load(f)
271 except IOError as e:
David Spickett7f18f4d2021-03-22 11:49:17 +0000272 print("error: failed to read {} config file: {}".format(args.config_file, e))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300273 sys.exit(os.EX_CONFIG)
274
David Spickett30a986f2021-04-29 09:37:00 +0100275 status = get_buildbot_bots_status(config)
276 status.update(get_buildkite_bots_status(config))
277 write_bot_status(config, args.output_file, status)