blob: a030d3eaa063e970e286aeb35170cb7461bf0741 [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 Spickett55cc06c2023-07-26 15:29:20 +010017import pickle
David Spickettaa155be2021-02-25 14:30:09 +000018import shutil
David Spickett8822bbd2023-06-12 14:02:41 +010019import time
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030020from datetime import datetime, timedelta
David Spickett4f932d12023-06-13 12:32:06 +010021
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030022# The requests allows HTTP keep-alive which re-uses the same TCP connection
23# to download multiple files.
24import requests
David Spickett55449c62021-12-13 12:57:33 +000025from textwrap import dedent
David Spickett82c94b22023-06-12 16:18:33 +010026from make_table import Table
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030027
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030028def ignored(s):
David Spickett4f932d12023-06-13 12:32:06 +010029 return "ignore" in s and s["ignore"]
30
31
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030032def not_ignored(s):
David Spickett4f932d12023-06-13 12:32:06 +010033 return not ignored(s)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030034
35
David Spickette88fe592021-03-22 12:25:13 +000036# Returns the parsed json URL or raises an exception
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030037def wget(session, url):
David Spickett4f932d12023-06-13 12:32:06 +010038 got = session.get(url)
39 got.raise_for_status()
40 return got.json()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030041
42
Oliver Stannard91688ff2021-01-07 10:27:27 +000043# Map from buildbot status codes we want to treat as errors to the color they
44# should be shown in. The codes are documented at
45# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
46# and these colors match the suggested ones there.
47RESULT_COLORS = {
David Spickett4f932d12023-06-13 12:32:06 +010048 2: "red", # Error
49 4: "purple", # Exception
50 5: "purple", # Retry
51 6: "pink", # Cancelled
Oliver Stannard91688ff2021-01-07 10:27:27 +000052}
53
David Spickette88fe592021-03-22 12:25:13 +000054
David Spickett4f932d12023-06-13 12:32:06 +010055def get_bot_failing_steps(session, base_url, buildid):
56 try:
57 contents = wget(session, "{}/api/v2/builds/{}/steps".format(base_url, buildid))
58 except requests.exceptions.RequestException:
59 return ""
60
61 for step in contents["steps"]:
62 if step["results"] in RESULT_COLORS:
63 yield (step["name"], step["results"])
Oliver Stannard91688ff2021-01-07 10:27:27 +000064
65
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030066# Get the status of a individual bot BOT. Returns a dict with the
67# information.
68def get_bot_status(session, bot, base_url, builder_url, build_url):
David Spickett4f932d12023-06-13 12:32:06 +010069 try:
70 builds = wget(
71 session, "{}/api/v2/{}/{}/{}".format(base_url, builder_url, bot, build_url)
72 )
73 except requests.exceptions.RequestException as e:
74 logging.debug(" Couldn't get builds for bot {}!".format(bot))
75 return {"valid": False}
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030076
David Spickett4f932d12023-06-13 12:32:06 +010077 reversed_builds = iter(sorted(builds["builds"], key=lambda b: -b["number"]))
78 next_build = None
79 for build in reversed_builds:
80 if not build["complete"]:
81 next_build = build
82 continue
David Spickett30a986f2021-04-29 09:37:00 +010083
David Spickett4f932d12023-06-13 12:32:06 +010084 time_since = int(datetime.now().timestamp()) - int(build["complete_at"])
85 duration = int(build["complete_at"]) - int(build["started_at"])
86 agent_url = "{}/#/{}/{}".format(base_url, builder_url, build["builderid"])
David Spickett30a986f2021-04-29 09:37:00 +010087
David Spickett4f932d12023-06-13 12:32:06 +010088 status = {
89 "builder_url": agent_url,
90 "number": build["number"],
91 "build_url": "{}/builds/{}".format(agent_url, build["number"]),
92 "state": build["state_string"],
93 "time_since": timedelta(seconds=time_since),
94 "duration": timedelta(seconds=duration),
95 "fail": build["state_string"] != "build successful",
David Spickettf8502432023-07-07 15:52:26 +010096 "next_in_progress": None
97 if next_build is None
98 else "{}/builds/{}".format(agent_url, next_build["number"]),
David Spickett4f932d12023-06-13 12:32:06 +010099 }
David Spickett7f18f4d2021-03-22 11:49:17 +0000100
David Spickett4f932d12023-06-13 12:32:06 +0100101 if status["fail"]:
102 buildid = build["buildid"]
103 status["steps"] = list(get_bot_failing_steps(session, base_url, buildid))
David Spickett86f2d472023-06-12 11:21:16 +0100104
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000105 # find the start of the failure streak
106 first_fail = build
107 for build in reversed_builds:
108 if build["state_string"] == "build successful":
109 status["first_fail_number"] = first_fail["number"]
110 status["first_fail_url"] = "{}/builds/{}".format(
111 agent_url, first_fail["number"]
112 )
113 fail_since = int(datetime.now().timestamp()) - int(
114 first_fail["complete_at"]
115 )
116 status["fail_since"] = timedelta(seconds=fail_since)
117 break
118 first_fail = build
119 else:
120 pass # fails since forever?
121
David Spickett4f932d12023-06-13 12:32:06 +0100122 return status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300123
124
David Spickett85355fa2021-03-22 15:41:41 +0000125# Get status for all bots named in the config
126# Return a dictionary of (base_url, bot name) -> status info
David Spickett30a986f2021-04-29 09:37:00 +0100127def get_buildbot_bots_status(config):
David Spickett4f932d12023-06-13 12:32:06 +0100128 session = requests.Session()
129 bot_cache = {}
David Spickettf006c372021-03-22 12:54:12 +0000130
David Spickett4f932d12023-06-13 12:32:06 +0100131 for server in filter(not_ignored, config):
David Spickett4f932d12023-06-13 12:32:06 +0100132 base_url = server["base_url"]
133 logging.debug("Parsing server {}...".format(server["name"]))
134 for builder in server["builders"]:
135 logging.debug(" Parsing builders {}...".format(builder["name"]))
136 for bot in builder["bots"]:
137 bot_key = (base_url, bot["name"])
138 if bot_key in bot_cache:
139 continue
David Spickett85355fa2021-03-22 15:41:41 +0000140
David Spickett4f932d12023-06-13 12:32:06 +0100141 logging.debug(" Parsing bot {}...".format(bot["name"]))
142 status = get_bot_status(
143 session,
144 bot["name"],
145 base_url,
146 server["builder_url"],
147 server["build_url"],
148 )
149 if status is not None:
150 if status.get("valid", True):
151 logging.debug(
152 " Bot status: " + ("FAIL" if status["fail"] else "PASS")
153 )
154 bot_cache[bot_key] = status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300155
David Spickett4f932d12023-06-13 12:32:06 +0100156 return bot_cache
David Spickett85355fa2021-03-22 15:41:41 +0000157
David Spickett82c94b22023-06-12 16:18:33 +0100158
David Spickett85355fa2021-03-22 15:41:41 +0000159def write_bot_status(config, output_file, bots_status):
David Spickett4f932d12023-06-13 12:32:06 +0100160 temp = tempfile.NamedTemporaryFile(mode="w+", delete=False)
David Spickettf006c372021-03-22 12:54:12 +0000161
David Spickett4f932d12023-06-13 12:32:06 +0100162 temp.write(
163 dedent(
164 """\
David Spickett64161b02022-11-01 09:51:30 +0000165 <!DOCTYPE html>
David Spickett55449c62021-12-13 12:57:33 +0000166 <style>
167 /* Combine the border between cells to prevent 1px gaps
168 in the row background colour. */
169 table, td, th {
170 border-collapse: collapse;
171 }
172 /* Colour every other row in a table body grey. */
173 tbody tr:nth-child(even) td {
174 background-color: #ededed;
175 }
David Spickett4f932d12023-06-13 12:32:06 +0100176 </style>"""
177 )
178 )
David Spickett55449c62021-12-13 12:57:33 +0000179
David Spickett4f932d12023-06-13 12:32:06 +0100180 column_titles = [
181 "Buildbot",
182 "Status",
183 "T Since",
184 "Duration",
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000185 "Latest",
David Spickett4f932d12023-06-13 12:32:06 +0100186 "Failing steps",
187 "Build In Progress",
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000188 "1st Failing",
189 "Failing Since",
David Spickett4f932d12023-06-13 12:32:06 +0100190 ]
191 num_columns = len(column_titles)
David Spickette44441b2023-06-12 12:23:28 +0100192
David Spickett4f932d12023-06-13 12:32:06 +0100193 # The first table should also say when this was generated.
194 # If we were to put this in its own header only table, it would
195 # not align with the rest because it has no content.
196 first = True
David Spickette44441b2023-06-12 12:23:28 +0100197
David Spickett4f932d12023-06-13 12:32:06 +0100198 # Dump all servers / bots
199 for server in filter(not_ignored, config):
200 with Table(temp) as table:
201 table.Border(0).Cellspacing(1).Cellpadding(2)
David Spickettec94cc22021-12-13 13:16:05 +0000202
David Spickett4f932d12023-06-13 12:32:06 +0100203 table.AddRow().AddCell().Colspan(num_columns)
David Spickette44441b2023-06-12 12:23:28 +0100204
David Spickett4f932d12023-06-13 12:32:06 +0100205 if first:
206 table.AddRow().AddHeader(
207 "Generated {} ({})".format(
208 datetime.today().ctime(), time.tzname[time.daylight]
209 )
210 ).Colspan(num_columns)
211 table.AddRow().AddCell().Colspan(num_columns)
212 first = False
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300213
David Spickett4f932d12023-06-13 12:32:06 +0100214 table.AddRow().AddHeader(server["name"]).Colspan(num_columns)
David Spickett82c94b22023-06-12 16:18:33 +0100215
David Spickett4f932d12023-06-13 12:32:06 +0100216 for builder in server["builders"]:
217 table.AddRow().AddCell().Colspan(num_columns)
218 table.AddRow().AddHeader(builder["name"]).Colspan(num_columns)
219 title_row = table.AddRow()
220 for title in column_titles:
221 title_row.AddHeader(title)
David Spickett82c94b22023-06-12 16:18:33 +0100222
David Spickett4f932d12023-06-13 12:32:06 +0100223 table.BeginBody()
David Spickett82c94b22023-06-12 16:18:33 +0100224
David Spickett4f932d12023-06-13 12:32:06 +0100225 for bot in builder["bots"]:
226 logging.debug("Writing out status for {}".format(bot["name"]))
David Spickett82c94b22023-06-12 16:18:33 +0100227
David Spickett4f932d12023-06-13 12:32:06 +0100228 row = table.AddRow()
229 base_url = server["base_url"]
230 try:
231 status = bots_status[(base_url, bot["name"])]
232 except KeyError:
233 row.AddCell("{} is offline!".format(bot["name"])).Colspan(
234 num_columns
235 )
236 continue
237 else:
238 if not status.get("valid", True):
239 row.AddCell(
240 "Could not read status for {}!".format(bot["name"])
241 ).Colspan(num_columns)
242 continue
David Spickettf2c82dd2021-06-24 10:01:33 +0100243
David Spickett4f932d12023-06-13 12:32:06 +0100244 row.AddCell(
245 "<a href='{}'>{}</a>".format(status["builder_url"], bot["name"])
246 )
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000247
248 status_cell = row.AddCell()
249 if status["fail"]:
250 status_cell.Style("color:red").Content("FAIL")
251 else:
252 status_cell.Style("color:green").Content("PASS")
David Spickett187f7962022-02-09 12:35:00 +0000253
David Spickett4f932d12023-06-13 12:32:06 +0100254 time_since_cell = row.AddCell()
255 if "time_since" in status:
256 time_since = status["time_since"]
257 # No build should be taking more than a day
258 if time_since > timedelta(hours=24):
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000259 time_since_cell.Style("color:red")
David Spickett4f932d12023-06-13 12:32:06 +0100260 time_since_cell.Content(time_since)
David Spickett82c94b22023-06-12 16:18:33 +0100261
David Spickett4f932d12023-06-13 12:32:06 +0100262 duration_cell = row.AddCell()
263 if "duration" in status:
264 duration_cell.Content(status["duration"])
David Spickett82c94b22023-06-12 16:18:33 +0100265
David Spickett4f932d12023-06-13 12:32:06 +0100266 number_cell = row.AddCell()
267 if "number" in status:
268 number_cell.Content(
269 "<a href='{}'>{}</a>".format(
270 status["build_url"], status["number"]
271 )
272 )
David Spickett82c94b22023-06-12 16:18:33 +0100273
David Spickett4f932d12023-06-13 12:32:06 +0100274 steps_cell = row.AddCell()
275 if "steps" in status and status["steps"]:
David Spickett82c94b22023-06-12 16:18:33 +0100276
David Spickett4f932d12023-06-13 12:32:06 +0100277 def render_step(name, result):
278 return "<font color='{}'>{}</font>".format(
279 RESULT_COLORS[result], name
280 )
David Spickett82c94b22023-06-12 16:18:33 +0100281
David Spickett4f932d12023-06-13 12:32:06 +0100282 step_list = ", ".join(
283 render_step(name, result)
284 for name, result in status["steps"]
285 )
286 steps_cell.Style("text-align:center").Content(step_list)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300287
David Spickett4f932d12023-06-13 12:32:06 +0100288 next_in_progress_cell = row.AddCell()
289 if "next_in_progress" in status:
David Spickettf8502432023-07-07 15:52:26 +0100290 next_build = status["next_in_progress"]
David Spickett4f932d12023-06-13 12:32:06 +0100291 next_in_progress_cell.Content(
David Spickettf8502432023-07-07 15:52:26 +0100292 "No"
293 if next_build is None
294 else "<a href='{}'>Yes</a>".format(next_build)
David Spickett4f932d12023-06-13 12:32:06 +0100295 )
296
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000297 first_fail_cell = row.AddCell()
298 if "first_fail_number" in status:
299 first_fail_cell.Content(
300 "<a href='{}'>{}</a>".format(
301 status["first_fail_url"], status["first_fail_number"]
302 )
303 )
304
305 fail_since_cell = row.AddCell()
306 if "fail_since" in status:
307 fail_since = status["fail_since"]
308 # No build should fail for more than a day
309 if fail_since > timedelta(hours=24):
310 fail_since_cell.Style("color:red")
311 fail_since_cell.Content(fail_since)
312
David Spickett4f932d12023-06-13 12:32:06 +0100313 table.EndBody()
314
315 # Move temp to main (atomic change)
316 temp.close()
317 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300318
319
320if __name__ == "__main__":
David Spickett4f932d12023-06-13 12:32:06 +0100321 parser = argparse.ArgumentParser()
322 parser.add_argument(
323 "-d", dest="debug", action="store_true", help="show debug log messages"
324 )
David Spickett55cc06c2023-07-26 15:29:20 +0100325 parser.add_argument(
326 "--cachefile",
327 required=False,
328 help="Location of bot status data cache file (a pickled Python object). If it exists use it, "
329 "if it does not, read the status from the network and write it to this path.",
330 )
David Spickett4f932d12023-06-13 12:32:06 +0100331 parser.add_argument("config_file", help="Bots description in JSON format")
332 parser.add_argument("output_file", help="output HTML path")
333 args = parser.parse_args()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300334
David Spickett4f932d12023-06-13 12:32:06 +0100335 if args.debug:
336 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300337
David Spickett4f932d12023-06-13 12:32:06 +0100338 try:
339 with open(args.config_file, "r") as f:
340 config = json.load(f)
341 except IOError as e:
342 print("error: failed to read {} config file: {}".format(args.config_file, e))
343 sys.exit(os.EX_CONFIG)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300344
David Spickett55cc06c2023-07-26 15:29:20 +0100345 status = None
346 if args.cachefile and os.path.exists(args.cachefile):
347 logging.debug("Using cache file {}".format(args.cachefile))
348 with open(args.cachefile, "rb") as f:
349 status = pickle.load(f)
350 else:
351 status = get_buildbot_bots_status(config)
David Spickett55cc06c2023-07-26 15:29:20 +0100352 if args.cachefile:
353 logging.debug("Writing status to cache file {}".format(args.cachefile))
354 with open(args.cachefile, "wb") as f:
355 pickle.dump(status, f)
356
David Spickett4f932d12023-06-13 12:32:06 +0100357 write_bot_status(config, args.output_file, status)