blob: 16b63c1c22eb22817b9c39818bc99d5fae9d13f3 [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
David Spickett4f932d12023-06-13 12:32:06 +010020
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030021# The requests allows HTTP keep-alive which re-uses the same TCP connection
22# to download multiple files.
23import requests
David Spickett55449c62021-12-13 12:57:33 +000024from textwrap import dedent
David Spickett82c94b22023-06-12 16:18:33 +010025from make_table import Table
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030026
David Spickett30a986f2021-04-29 09:37:00 +010027from buildkite_status import get_buildkite_bots_status
28
David Spickett4f932d12023-06-13 12:32:06 +010029
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030030def ignored(s):
David Spickett4f932d12023-06-13 12:32:06 +010031 return "ignore" in s and s["ignore"]
32
33
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030034def not_ignored(s):
David Spickett4f932d12023-06-13 12:32:06 +010035 return not ignored(s)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030036
37
David Spickette88fe592021-03-22 12:25:13 +000038# Returns the parsed json URL or raises an exception
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030039def wget(session, url):
David Spickett4f932d12023-06-13 12:32:06 +010040 got = session.get(url)
41 got.raise_for_status()
42 return got.json()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030043
44
Oliver Stannard91688ff2021-01-07 10:27:27 +000045# Map from buildbot status codes we want to treat as errors to the color they
46# should be shown in. The codes are documented at
47# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
48# and these colors match the suggested ones there.
49RESULT_COLORS = {
David Spickett4f932d12023-06-13 12:32:06 +010050 2: "red", # Error
51 4: "purple", # Exception
52 5: "purple", # Retry
53 6: "pink", # Cancelled
Oliver Stannard91688ff2021-01-07 10:27:27 +000054}
55
David Spickette88fe592021-03-22 12:25:13 +000056
David Spickett4f932d12023-06-13 12:32:06 +010057def get_bot_failing_steps(session, base_url, buildid):
58 try:
59 contents = wget(session, "{}/api/v2/builds/{}/steps".format(base_url, buildid))
60 except requests.exceptions.RequestException:
61 return ""
62
63 for step in contents["steps"]:
64 if step["results"] in RESULT_COLORS:
65 yield (step["name"], step["results"])
Oliver Stannard91688ff2021-01-07 10:27:27 +000066
67
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030068# Get the status of a individual bot BOT. Returns a dict with the
69# information.
70def get_bot_status(session, bot, base_url, builder_url, build_url):
David Spickett4f932d12023-06-13 12:32:06 +010071 try:
72 builds = wget(
73 session, "{}/api/v2/{}/{}/{}".format(base_url, builder_url, bot, build_url)
74 )
75 except requests.exceptions.RequestException as e:
76 logging.debug(" Couldn't get builds for bot {}!".format(bot))
77 return {"valid": False}
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030078
David Spickett4f932d12023-06-13 12:32:06 +010079 reversed_builds = iter(sorted(builds["builds"], key=lambda b: -b["number"]))
80 next_build = None
81 for build in reversed_builds:
82 if not build["complete"]:
83 next_build = build
84 continue
David Spickett30a986f2021-04-29 09:37:00 +010085
David Spickett4f932d12023-06-13 12:32:06 +010086 time_since = int(datetime.now().timestamp()) - int(build["complete_at"])
87 duration = int(build["complete_at"]) - int(build["started_at"])
88 agent_url = "{}/#/{}/{}".format(base_url, builder_url, build["builderid"])
David Spickett30a986f2021-04-29 09:37:00 +010089
David Spickett4f932d12023-06-13 12:32:06 +010090 status = {
91 "builder_url": agent_url,
92 "number": build["number"],
93 "build_url": "{}/builds/{}".format(agent_url, build["number"]),
94 "state": build["state_string"],
95 "time_since": timedelta(seconds=time_since),
96 "duration": timedelta(seconds=duration),
97 "fail": build["state_string"] != "build successful",
David Spickettf8502432023-07-07 15:52:26 +010098 "next_in_progress": None
99 if next_build is None
100 else "{}/builds/{}".format(agent_url, next_build["number"]),
David Spickett4f932d12023-06-13 12:32:06 +0100101 }
David Spickett7f18f4d2021-03-22 11:49:17 +0000102
David Spickett4f932d12023-06-13 12:32:06 +0100103 if status["fail"]:
104 buildid = build["buildid"]
105 status["steps"] = list(get_bot_failing_steps(session, base_url, buildid))
David Spickett86f2d472023-06-12 11:21:16 +0100106
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000107 # find the start of the failure streak
108 first_fail = build
109 for build in reversed_builds:
110 if build["state_string"] == "build successful":
111 status["first_fail_number"] = first_fail["number"]
112 status["first_fail_url"] = "{}/builds/{}".format(
113 agent_url, first_fail["number"]
114 )
115 fail_since = int(datetime.now().timestamp()) - int(
116 first_fail["complete_at"]
117 )
118 status["fail_since"] = timedelta(seconds=fail_since)
119 break
120 first_fail = build
121 else:
122 pass # fails since forever?
123
David Spickett4f932d12023-06-13 12:32:06 +0100124 return status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300125
126
David Spickett85355fa2021-03-22 15:41:41 +0000127# Get status for all bots named in the config
128# Return a dictionary of (base_url, bot name) -> status info
David Spickett30a986f2021-04-29 09:37:00 +0100129def get_buildbot_bots_status(config):
David Spickett4f932d12023-06-13 12:32:06 +0100130 session = requests.Session()
131 bot_cache = {}
David Spickettf006c372021-03-22 12:54:12 +0000132
David Spickett4f932d12023-06-13 12:32:06 +0100133 for server in filter(not_ignored, config):
134 if server["name"] == "Buildkite":
135 continue
David Spickett30a986f2021-04-29 09:37:00 +0100136
David Spickett4f932d12023-06-13 12:32:06 +0100137 base_url = server["base_url"]
138 logging.debug("Parsing server {}...".format(server["name"]))
139 for builder in server["builders"]:
140 logging.debug(" Parsing builders {}...".format(builder["name"]))
141 for bot in builder["bots"]:
142 bot_key = (base_url, bot["name"])
143 if bot_key in bot_cache:
144 continue
David Spickett85355fa2021-03-22 15:41:41 +0000145
David Spickett4f932d12023-06-13 12:32:06 +0100146 logging.debug(" Parsing bot {}...".format(bot["name"]))
147 status = get_bot_status(
148 session,
149 bot["name"],
150 base_url,
151 server["builder_url"],
152 server["build_url"],
153 )
154 if status is not None:
155 if status.get("valid", True):
156 logging.debug(
157 " Bot status: " + ("FAIL" if status["fail"] else "PASS")
158 )
159 bot_cache[bot_key] = status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300160
David Spickett4f932d12023-06-13 12:32:06 +0100161 return bot_cache
David Spickett85355fa2021-03-22 15:41:41 +0000162
David Spickett82c94b22023-06-12 16:18:33 +0100163
David Spickett85355fa2021-03-22 15:41:41 +0000164def write_bot_status(config, output_file, bots_status):
David Spickett4f932d12023-06-13 12:32:06 +0100165 temp = tempfile.NamedTemporaryFile(mode="w+", delete=False)
David Spickettf006c372021-03-22 12:54:12 +0000166
David Spickett4f932d12023-06-13 12:32:06 +0100167 temp.write(
168 dedent(
169 """\
David Spickett64161b02022-11-01 09:51:30 +0000170 <!DOCTYPE html>
David Spickett55449c62021-12-13 12:57:33 +0000171 <style>
172 /* Combine the border between cells to prevent 1px gaps
173 in the row background colour. */
174 table, td, th {
175 border-collapse: collapse;
176 }
177 /* Colour every other row in a table body grey. */
178 tbody tr:nth-child(even) td {
179 background-color: #ededed;
180 }
David Spickett4f932d12023-06-13 12:32:06 +0100181 </style>"""
182 )
183 )
David Spickett55449c62021-12-13 12:57:33 +0000184
David Spickett4f932d12023-06-13 12:32:06 +0100185 column_titles = [
186 "Buildbot",
187 "Status",
188 "T Since",
189 "Duration",
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000190 "Latest",
David Spickett4f932d12023-06-13 12:32:06 +0100191 "Failing steps",
192 "Build In Progress",
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000193 "1st Failing",
194 "Failing Since",
David Spickett4f932d12023-06-13 12:32:06 +0100195 ]
196 num_columns = len(column_titles)
David Spickette44441b2023-06-12 12:23:28 +0100197
David Spickett4f932d12023-06-13 12:32:06 +0100198 # The first table should also say when this was generated.
199 # If we were to put this in its own header only table, it would
200 # not align with the rest because it has no content.
201 first = True
David Spickette44441b2023-06-12 12:23:28 +0100202
David Spickett4f932d12023-06-13 12:32:06 +0100203 # Dump all servers / bots
204 for server in filter(not_ignored, config):
205 with Table(temp) as table:
206 table.Border(0).Cellspacing(1).Cellpadding(2)
David Spickettec94cc22021-12-13 13:16:05 +0000207
David Spickett4f932d12023-06-13 12:32:06 +0100208 table.AddRow().AddCell().Colspan(num_columns)
David Spickette44441b2023-06-12 12:23:28 +0100209
David Spickett4f932d12023-06-13 12:32:06 +0100210 if first:
211 table.AddRow().AddHeader(
212 "Generated {} ({})".format(
213 datetime.today().ctime(), time.tzname[time.daylight]
214 )
215 ).Colspan(num_columns)
216 table.AddRow().AddCell().Colspan(num_columns)
217 first = False
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300218
David Spickett4f932d12023-06-13 12:32:06 +0100219 table.AddRow().AddHeader(server["name"]).Colspan(num_columns)
David Spickett82c94b22023-06-12 16:18:33 +0100220
David Spickett4f932d12023-06-13 12:32:06 +0100221 for builder in server["builders"]:
222 table.AddRow().AddCell().Colspan(num_columns)
223 table.AddRow().AddHeader(builder["name"]).Colspan(num_columns)
224 title_row = table.AddRow()
225 for title in column_titles:
226 title_row.AddHeader(title)
David Spickett82c94b22023-06-12 16:18:33 +0100227
David Spickett4f932d12023-06-13 12:32:06 +0100228 table.BeginBody()
David Spickett82c94b22023-06-12 16:18:33 +0100229
David Spickett4f932d12023-06-13 12:32:06 +0100230 for bot in builder["bots"]:
231 logging.debug("Writing out status for {}".format(bot["name"]))
David Spickett82c94b22023-06-12 16:18:33 +0100232
David Spickett4f932d12023-06-13 12:32:06 +0100233 row = table.AddRow()
234 base_url = server["base_url"]
235 try:
236 status = bots_status[(base_url, bot["name"])]
237 except KeyError:
238 row.AddCell("{} is offline!".format(bot["name"])).Colspan(
239 num_columns
240 )
241 continue
242 else:
243 if not status.get("valid", True):
244 row.AddCell(
245 "Could not read status for {}!".format(bot["name"])
246 ).Colspan(num_columns)
247 continue
David Spickettf2c82dd2021-06-24 10:01:33 +0100248
David Spickett4f932d12023-06-13 12:32:06 +0100249 row.AddCell(
250 "<a href='{}'>{}</a>".format(status["builder_url"], bot["name"])
251 )
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000252
253 status_cell = row.AddCell()
254 if status["fail"]:
255 status_cell.Style("color:red").Content("FAIL")
256 else:
257 status_cell.Style("color:green").Content("PASS")
David Spickett187f7962022-02-09 12:35:00 +0000258
David Spickett4f932d12023-06-13 12:32:06 +0100259 time_since_cell = row.AddCell()
260 if "time_since" in status:
261 time_since = status["time_since"]
262 # No build should be taking more than a day
263 if time_since > timedelta(hours=24):
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000264 time_since_cell.Style("color:red")
David Spickett4f932d12023-06-13 12:32:06 +0100265 time_since_cell.Content(time_since)
David Spickett82c94b22023-06-12 16:18:33 +0100266
David Spickett4f932d12023-06-13 12:32:06 +0100267 duration_cell = row.AddCell()
268 if "duration" in status:
269 duration_cell.Content(status["duration"])
David Spickett82c94b22023-06-12 16:18:33 +0100270
David Spickett4f932d12023-06-13 12:32:06 +0100271 number_cell = row.AddCell()
272 if "number" in status:
273 number_cell.Content(
274 "<a href='{}'>{}</a>".format(
275 status["build_url"], status["number"]
276 )
277 )
David Spickett82c94b22023-06-12 16:18:33 +0100278
David Spickett4f932d12023-06-13 12:32:06 +0100279 steps_cell = row.AddCell()
280 if "steps" in status and status["steps"]:
David Spickett82c94b22023-06-12 16:18:33 +0100281
David Spickett4f932d12023-06-13 12:32:06 +0100282 def render_step(name, result):
283 return "<font color='{}'>{}</font>".format(
284 RESULT_COLORS[result], name
285 )
David Spickett82c94b22023-06-12 16:18:33 +0100286
David Spickett4f932d12023-06-13 12:32:06 +0100287 step_list = ", ".join(
288 render_step(name, result)
289 for name, result in status["steps"]
290 )
291 steps_cell.Style("text-align:center").Content(step_list)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300292
David Spickett4f932d12023-06-13 12:32:06 +0100293 next_in_progress_cell = row.AddCell()
294 if "next_in_progress" in status:
David Spickettf8502432023-07-07 15:52:26 +0100295 next_build = status["next_in_progress"]
David Spickett4f932d12023-06-13 12:32:06 +0100296 next_in_progress_cell.Content(
David Spickettf8502432023-07-07 15:52:26 +0100297 "No"
298 if next_build is None
299 else "<a href='{}'>Yes</a>".format(next_build)
David Spickett4f932d12023-06-13 12:32:06 +0100300 )
301
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000302 first_fail_cell = row.AddCell()
303 if "first_fail_number" in status:
304 first_fail_cell.Content(
305 "<a href='{}'>{}</a>".format(
306 status["first_fail_url"], status["first_fail_number"]
307 )
308 )
309
310 fail_since_cell = row.AddCell()
311 if "fail_since" in status:
312 fail_since = status["fail_since"]
313 # No build should fail for more than a day
314 if fail_since > timedelta(hours=24):
315 fail_since_cell.Style("color:red")
316 fail_since_cell.Content(fail_since)
317
David Spickett4f932d12023-06-13 12:32:06 +0100318 table.EndBody()
319
320 # Move temp to main (atomic change)
321 temp.close()
322 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300323
324
325if __name__ == "__main__":
David Spickett4f932d12023-06-13 12:32:06 +0100326 parser = argparse.ArgumentParser()
327 parser.add_argument(
328 "-d", dest="debug", action="store_true", help="show debug log messages"
329 )
330 parser.add_argument("config_file", help="Bots description in JSON format")
331 parser.add_argument("output_file", help="output HTML path")
332 args = parser.parse_args()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300333
David Spickett4f932d12023-06-13 12:32:06 +0100334 if args.debug:
335 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300336
David Spickett4f932d12023-06-13 12:32:06 +0100337 try:
338 with open(args.config_file, "r") as f:
339 config = json.load(f)
340 except IOError as e:
341 print("error: failed to read {} config file: {}".format(args.config_file, e))
342 sys.exit(os.EX_CONFIG)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300343
David Spickett4f932d12023-06-13 12:32:06 +0100344 status = get_buildbot_bots_status(config)
345 status.update(get_buildkite_bots_status(config))
346 write_bot_status(config, args.output_file, status)