blob: f858ef62fdf59cfc0f3340d9ee048e77fdbcf191 [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
David Spickett30a986f2021-04-29 09:37:00 +010028from buildkite_status import get_buildkite_bots_status
29
David Spickett4f932d12023-06-13 12:32:06 +010030
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030031def ignored(s):
David Spickett4f932d12023-06-13 12:32:06 +010032 return "ignore" in s and s["ignore"]
33
34
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030035def not_ignored(s):
David Spickett4f932d12023-06-13 12:32:06 +010036 return not ignored(s)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030037
38
David Spickette88fe592021-03-22 12:25:13 +000039# Returns the parsed json URL or raises an exception
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030040def wget(session, url):
David Spickett4f932d12023-06-13 12:32:06 +010041 got = session.get(url)
42 got.raise_for_status()
43 return got.json()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030044
45
Oliver Stannard91688ff2021-01-07 10:27:27 +000046# Map from buildbot status codes we want to treat as errors to the color they
47# should be shown in. The codes are documented at
48# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
49# and these colors match the suggested ones there.
50RESULT_COLORS = {
David Spickett4f932d12023-06-13 12:32:06 +010051 2: "red", # Error
52 4: "purple", # Exception
53 5: "purple", # Retry
54 6: "pink", # Cancelled
Oliver Stannard91688ff2021-01-07 10:27:27 +000055}
56
David Spickette88fe592021-03-22 12:25:13 +000057
David Spickett4f932d12023-06-13 12:32:06 +010058def get_bot_failing_steps(session, base_url, buildid):
59 try:
60 contents = wget(session, "{}/api/v2/builds/{}/steps".format(base_url, buildid))
61 except requests.exceptions.RequestException:
62 return ""
63
64 for step in contents["steps"]:
65 if step["results"] in RESULT_COLORS:
66 yield (step["name"], step["results"])
Oliver Stannard91688ff2021-01-07 10:27:27 +000067
68
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030069# Get the status of a individual bot BOT. Returns a dict with the
70# information.
71def get_bot_status(session, bot, base_url, builder_url, build_url):
David Spickett4f932d12023-06-13 12:32:06 +010072 try:
73 builds = wget(
74 session, "{}/api/v2/{}/{}/{}".format(base_url, builder_url, bot, build_url)
75 )
76 except requests.exceptions.RequestException as e:
77 logging.debug(" Couldn't get builds for bot {}!".format(bot))
78 return {"valid": False}
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030079
David Spickett4f932d12023-06-13 12:32:06 +010080 reversed_builds = iter(sorted(builds["builds"], key=lambda b: -b["number"]))
81 next_build = None
82 for build in reversed_builds:
83 if not build["complete"]:
84 next_build = build
85 continue
David Spickett30a986f2021-04-29 09:37:00 +010086
David Spickett4f932d12023-06-13 12:32:06 +010087 time_since = int(datetime.now().timestamp()) - int(build["complete_at"])
88 duration = int(build["complete_at"]) - int(build["started_at"])
89 agent_url = "{}/#/{}/{}".format(base_url, builder_url, build["builderid"])
David Spickett30a986f2021-04-29 09:37:00 +010090
David Spickett4f932d12023-06-13 12:32:06 +010091 status = {
92 "builder_url": agent_url,
93 "number": build["number"],
94 "build_url": "{}/builds/{}".format(agent_url, build["number"]),
95 "state": build["state_string"],
96 "time_since": timedelta(seconds=time_since),
97 "duration": timedelta(seconds=duration),
98 "fail": build["state_string"] != "build successful",
David Spickettf8502432023-07-07 15:52:26 +010099 "next_in_progress": None
100 if next_build is None
101 else "{}/builds/{}".format(agent_url, next_build["number"]),
David Spickett4f932d12023-06-13 12:32:06 +0100102 }
David Spickett7f18f4d2021-03-22 11:49:17 +0000103
David Spickett4f932d12023-06-13 12:32:06 +0100104 if status["fail"]:
105 buildid = build["buildid"]
106 status["steps"] = list(get_bot_failing_steps(session, base_url, buildid))
David Spickett86f2d472023-06-12 11:21:16 +0100107
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000108 # find the start of the failure streak
109 first_fail = build
110 for build in reversed_builds:
111 if build["state_string"] == "build successful":
112 status["first_fail_number"] = first_fail["number"]
113 status["first_fail_url"] = "{}/builds/{}".format(
114 agent_url, first_fail["number"]
115 )
116 fail_since = int(datetime.now().timestamp()) - int(
117 first_fail["complete_at"]
118 )
119 status["fail_since"] = timedelta(seconds=fail_since)
120 break
121 first_fail = build
122 else:
123 pass # fails since forever?
124
David Spickett4f932d12023-06-13 12:32:06 +0100125 return status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300126
127
David Spickett85355fa2021-03-22 15:41:41 +0000128# Get status for all bots named in the config
129# Return a dictionary of (base_url, bot name) -> status info
David Spickett30a986f2021-04-29 09:37:00 +0100130def get_buildbot_bots_status(config):
David Spickett4f932d12023-06-13 12:32:06 +0100131 session = requests.Session()
132 bot_cache = {}
David Spickettf006c372021-03-22 12:54:12 +0000133
David Spickett4f932d12023-06-13 12:32:06 +0100134 for server in filter(not_ignored, config):
135 if server["name"] == "Buildkite":
136 continue
David Spickett30a986f2021-04-29 09:37:00 +0100137
David Spickett4f932d12023-06-13 12:32:06 +0100138 base_url = server["base_url"]
139 logging.debug("Parsing server {}...".format(server["name"]))
140 for builder in server["builders"]:
141 logging.debug(" Parsing builders {}...".format(builder["name"]))
142 for bot in builder["bots"]:
143 bot_key = (base_url, bot["name"])
144 if bot_key in bot_cache:
145 continue
David Spickett85355fa2021-03-22 15:41:41 +0000146
David Spickett4f932d12023-06-13 12:32:06 +0100147 logging.debug(" Parsing bot {}...".format(bot["name"]))
148 status = get_bot_status(
149 session,
150 bot["name"],
151 base_url,
152 server["builder_url"],
153 server["build_url"],
154 )
155 if status is not None:
156 if status.get("valid", True):
157 logging.debug(
158 " Bot status: " + ("FAIL" if status["fail"] else "PASS")
159 )
160 bot_cache[bot_key] = status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300161
David Spickett4f932d12023-06-13 12:32:06 +0100162 return bot_cache
David Spickett85355fa2021-03-22 15:41:41 +0000163
David Spickett82c94b22023-06-12 16:18:33 +0100164
David Spickett85355fa2021-03-22 15:41:41 +0000165def write_bot_status(config, output_file, bots_status):
David Spickett4f932d12023-06-13 12:32:06 +0100166 temp = tempfile.NamedTemporaryFile(mode="w+", delete=False)
David Spickettf006c372021-03-22 12:54:12 +0000167
David Spickett4f932d12023-06-13 12:32:06 +0100168 temp.write(
169 dedent(
170 """\
David Spickett64161b02022-11-01 09:51:30 +0000171 <!DOCTYPE html>
David Spickett55449c62021-12-13 12:57:33 +0000172 <style>
173 /* Combine the border between cells to prevent 1px gaps
174 in the row background colour. */
175 table, td, th {
176 border-collapse: collapse;
177 }
178 /* Colour every other row in a table body grey. */
179 tbody tr:nth-child(even) td {
180 background-color: #ededed;
181 }
David Spickett4f932d12023-06-13 12:32:06 +0100182 </style>"""
183 )
184 )
David Spickett55449c62021-12-13 12:57:33 +0000185
David Spickett4f932d12023-06-13 12:32:06 +0100186 column_titles = [
187 "Buildbot",
188 "Status",
189 "T Since",
190 "Duration",
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000191 "Latest",
David Spickett4f932d12023-06-13 12:32:06 +0100192 "Failing steps",
193 "Build In Progress",
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000194 "1st Failing",
195 "Failing Since",
David Spickett4f932d12023-06-13 12:32:06 +0100196 ]
197 num_columns = len(column_titles)
David Spickette44441b2023-06-12 12:23:28 +0100198
David Spickett4f932d12023-06-13 12:32:06 +0100199 # The first table should also say when this was generated.
200 # If we were to put this in its own header only table, it would
201 # not align with the rest because it has no content.
202 first = True
David Spickette44441b2023-06-12 12:23:28 +0100203
David Spickett4f932d12023-06-13 12:32:06 +0100204 # Dump all servers / bots
205 for server in filter(not_ignored, config):
206 with Table(temp) as table:
207 table.Border(0).Cellspacing(1).Cellpadding(2)
David Spickettec94cc22021-12-13 13:16:05 +0000208
David Spickett4f932d12023-06-13 12:32:06 +0100209 table.AddRow().AddCell().Colspan(num_columns)
David Spickette44441b2023-06-12 12:23:28 +0100210
David Spickett4f932d12023-06-13 12:32:06 +0100211 if first:
212 table.AddRow().AddHeader(
213 "Generated {} ({})".format(
214 datetime.today().ctime(), time.tzname[time.daylight]
215 )
216 ).Colspan(num_columns)
217 table.AddRow().AddCell().Colspan(num_columns)
218 first = False
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300219
David Spickett4f932d12023-06-13 12:32:06 +0100220 table.AddRow().AddHeader(server["name"]).Colspan(num_columns)
David Spickett82c94b22023-06-12 16:18:33 +0100221
David Spickett4f932d12023-06-13 12:32:06 +0100222 for builder in server["builders"]:
223 table.AddRow().AddCell().Colspan(num_columns)
224 table.AddRow().AddHeader(builder["name"]).Colspan(num_columns)
225 title_row = table.AddRow()
226 for title in column_titles:
227 title_row.AddHeader(title)
David Spickett82c94b22023-06-12 16:18:33 +0100228
David Spickett4f932d12023-06-13 12:32:06 +0100229 table.BeginBody()
David Spickett82c94b22023-06-12 16:18:33 +0100230
David Spickett4f932d12023-06-13 12:32:06 +0100231 for bot in builder["bots"]:
232 logging.debug("Writing out status for {}".format(bot["name"]))
David Spickett82c94b22023-06-12 16:18:33 +0100233
David Spickett4f932d12023-06-13 12:32:06 +0100234 row = table.AddRow()
235 base_url = server["base_url"]
236 try:
237 status = bots_status[(base_url, bot["name"])]
238 except KeyError:
239 row.AddCell("{} is offline!".format(bot["name"])).Colspan(
240 num_columns
241 )
242 continue
243 else:
244 if not status.get("valid", True):
245 row.AddCell(
246 "Could not read status for {}!".format(bot["name"])
247 ).Colspan(num_columns)
248 continue
David Spickettf2c82dd2021-06-24 10:01:33 +0100249
David Spickett4f932d12023-06-13 12:32:06 +0100250 row.AddCell(
251 "<a href='{}'>{}</a>".format(status["builder_url"], bot["name"])
252 )
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000253
254 status_cell = row.AddCell()
255 if status["fail"]:
256 status_cell.Style("color:red").Content("FAIL")
257 else:
258 status_cell.Style("color:green").Content("PASS")
David Spickett187f7962022-02-09 12:35:00 +0000259
David Spickett4f932d12023-06-13 12:32:06 +0100260 time_since_cell = row.AddCell()
261 if "time_since" in status:
262 time_since = status["time_since"]
263 # No build should be taking more than a day
264 if time_since > timedelta(hours=24):
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000265 time_since_cell.Style("color:red")
David Spickett4f932d12023-06-13 12:32:06 +0100266 time_since_cell.Content(time_since)
David Spickett82c94b22023-06-12 16:18:33 +0100267
David Spickett4f932d12023-06-13 12:32:06 +0100268 duration_cell = row.AddCell()
269 if "duration" in status:
270 duration_cell.Content(status["duration"])
David Spickett82c94b22023-06-12 16:18:33 +0100271
David Spickett4f932d12023-06-13 12:32:06 +0100272 number_cell = row.AddCell()
273 if "number" in status:
274 number_cell.Content(
275 "<a href='{}'>{}</a>".format(
276 status["build_url"], status["number"]
277 )
278 )
David Spickett82c94b22023-06-12 16:18:33 +0100279
David Spickett4f932d12023-06-13 12:32:06 +0100280 steps_cell = row.AddCell()
281 if "steps" in status and status["steps"]:
David Spickett82c94b22023-06-12 16:18:33 +0100282
David Spickett4f932d12023-06-13 12:32:06 +0100283 def render_step(name, result):
284 return "<font color='{}'>{}</font>".format(
285 RESULT_COLORS[result], name
286 )
David Spickett82c94b22023-06-12 16:18:33 +0100287
David Spickett4f932d12023-06-13 12:32:06 +0100288 step_list = ", ".join(
289 render_step(name, result)
290 for name, result in status["steps"]
291 )
292 steps_cell.Style("text-align:center").Content(step_list)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300293
David Spickett4f932d12023-06-13 12:32:06 +0100294 next_in_progress_cell = row.AddCell()
295 if "next_in_progress" in status:
David Spickettf8502432023-07-07 15:52:26 +0100296 next_build = status["next_in_progress"]
David Spickett4f932d12023-06-13 12:32:06 +0100297 next_in_progress_cell.Content(
David Spickettf8502432023-07-07 15:52:26 +0100298 "No"
299 if next_build is None
300 else "<a href='{}'>Yes</a>".format(next_build)
David Spickett4f932d12023-06-13 12:32:06 +0100301 )
302
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000303 first_fail_cell = row.AddCell()
304 if "first_fail_number" in status:
305 first_fail_cell.Content(
306 "<a href='{}'>{}</a>".format(
307 status["first_fail_url"], status["first_fail_number"]
308 )
309 )
310
311 fail_since_cell = row.AddCell()
312 if "fail_since" in status:
313 fail_since = status["fail_since"]
314 # No build should fail for more than a day
315 if fail_since > timedelta(hours=24):
316 fail_since_cell.Style("color:red")
317 fail_since_cell.Content(fail_since)
318
David Spickett4f932d12023-06-13 12:32:06 +0100319 table.EndBody()
320
321 # Move temp to main (atomic change)
322 temp.close()
323 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300324
325
326if __name__ == "__main__":
David Spickett4f932d12023-06-13 12:32:06 +0100327 parser = argparse.ArgumentParser()
328 parser.add_argument(
329 "-d", dest="debug", action="store_true", help="show debug log messages"
330 )
David Spickett55cc06c2023-07-26 15:29:20 +0100331 parser.add_argument(
332 "--cachefile",
333 required=False,
334 help="Location of bot status data cache file (a pickled Python object). If it exists use it, "
335 "if it does not, read the status from the network and write it to this path.",
336 )
David Spickett4f932d12023-06-13 12:32:06 +0100337 parser.add_argument("config_file", help="Bots description in JSON format")
338 parser.add_argument("output_file", help="output HTML path")
339 args = parser.parse_args()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300340
David Spickett4f932d12023-06-13 12:32:06 +0100341 if args.debug:
342 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300343
David Spickett4f932d12023-06-13 12:32:06 +0100344 try:
345 with open(args.config_file, "r") as f:
346 config = json.load(f)
347 except IOError as e:
348 print("error: failed to read {} config file: {}".format(args.config_file, e))
349 sys.exit(os.EX_CONFIG)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300350
David Spickett55cc06c2023-07-26 15:29:20 +0100351 status = None
352 if args.cachefile and os.path.exists(args.cachefile):
353 logging.debug("Using cache file {}".format(args.cachefile))
354 with open(args.cachefile, "rb") as f:
355 status = pickle.load(f)
356 else:
357 status = get_buildbot_bots_status(config)
358 status.update(get_buildkite_bots_status(config))
359 if args.cachefile:
360 logging.debug("Writing status to cache file {}".format(args.cachefile))
361 with open(args.cachefile, "wb") as f:
362 pickle.dump(status, f)
363
David Spickett4f932d12023-06-13 12:32:06 +0100364 write_bot_status(config, args.output_file, status)