blob: 6e37defc9aa17849fa0619a654a226cc99ddf43f [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 )
David Spickett52555292023-12-07 11:28:52 +0000113 # Occasionaly we find a finished build without complete_at,
114 # it may be an intermitent issue on Buildbot's side.
115 complete_at = first_fail.get("complete_at")
116 if complete_at is not None:
117 fail_since = int(datetime.now().timestamp()) - int(complete_at)
118 status["fail_since"] = timedelta(seconds=fail_since)
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000119 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):
David Spickett4f932d12023-06-13 12:32:06 +0100134 base_url = server["base_url"]
135 logging.debug("Parsing server {}...".format(server["name"]))
136 for builder in server["builders"]:
137 logging.debug(" Parsing builders {}...".format(builder["name"]))
138 for bot in builder["bots"]:
139 bot_key = (base_url, bot["name"])
140 if bot_key in bot_cache:
141 continue
David Spickett85355fa2021-03-22 15:41:41 +0000142
David Spickett4f932d12023-06-13 12:32:06 +0100143 logging.debug(" Parsing bot {}...".format(bot["name"]))
144 status = get_bot_status(
145 session,
146 bot["name"],
147 base_url,
148 server["builder_url"],
149 server["build_url"],
150 )
151 if status is not None:
152 if status.get("valid", True):
153 logging.debug(
154 " Bot status: " + ("FAIL" if status["fail"] else "PASS")
155 )
156 bot_cache[bot_key] = status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300157
David Spickett4f932d12023-06-13 12:32:06 +0100158 return bot_cache
David Spickett85355fa2021-03-22 15:41:41 +0000159
David Spickett82c94b22023-06-12 16:18:33 +0100160
David Spickett85355fa2021-03-22 15:41:41 +0000161def write_bot_status(config, output_file, bots_status):
David Spickett4f932d12023-06-13 12:32:06 +0100162 temp = tempfile.NamedTemporaryFile(mode="w+", delete=False)
David Spickettf006c372021-03-22 12:54:12 +0000163
David Spickett4f932d12023-06-13 12:32:06 +0100164 temp.write(
165 dedent(
166 """\
David Spickett64161b02022-11-01 09:51:30 +0000167 <!DOCTYPE html>
David Spickett55449c62021-12-13 12:57:33 +0000168 <style>
169 /* Combine the border between cells to prevent 1px gaps
170 in the row background colour. */
171 table, td, th {
172 border-collapse: collapse;
173 }
174 /* Colour every other row in a table body grey. */
175 tbody tr:nth-child(even) td {
176 background-color: #ededed;
177 }
David Spickett4f932d12023-06-13 12:32:06 +0100178 </style>"""
179 )
180 )
David Spickett55449c62021-12-13 12:57:33 +0000181
David Spickett4f932d12023-06-13 12:32:06 +0100182 column_titles = [
183 "Buildbot",
184 "Status",
185 "T Since",
186 "Duration",
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000187 "Latest",
David Spickett4f932d12023-06-13 12:32:06 +0100188 "Failing steps",
189 "Build In Progress",
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000190 "1st Failing",
191 "Failing Since",
David Spickett4f932d12023-06-13 12:32:06 +0100192 ]
193 num_columns = len(column_titles)
David Spickette44441b2023-06-12 12:23:28 +0100194
David Spickett4f932d12023-06-13 12:32:06 +0100195 # The first table should also say when this was generated.
196 # If we were to put this in its own header only table, it would
197 # not align with the rest because it has no content.
198 first = True
David Spickette44441b2023-06-12 12:23:28 +0100199
David Spickett4f932d12023-06-13 12:32:06 +0100200 # Dump all servers / bots
201 for server in filter(not_ignored, config):
202 with Table(temp) as table:
203 table.Border(0).Cellspacing(1).Cellpadding(2)
David Spickettec94cc22021-12-13 13:16:05 +0000204
David Spickett4f932d12023-06-13 12:32:06 +0100205 table.AddRow().AddCell().Colspan(num_columns)
David Spickette44441b2023-06-12 12:23:28 +0100206
David Spickett4f932d12023-06-13 12:32:06 +0100207 if first:
208 table.AddRow().AddHeader(
209 "Generated {} ({})".format(
210 datetime.today().ctime(), time.tzname[time.daylight]
211 )
212 ).Colspan(num_columns)
213 table.AddRow().AddCell().Colspan(num_columns)
214 first = False
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300215
David Spickett4f932d12023-06-13 12:32:06 +0100216 table.AddRow().AddHeader(server["name"]).Colspan(num_columns)
David Spickett82c94b22023-06-12 16:18:33 +0100217
David Spickett4f932d12023-06-13 12:32:06 +0100218 for builder in server["builders"]:
219 table.AddRow().AddCell().Colspan(num_columns)
220 table.AddRow().AddHeader(builder["name"]).Colspan(num_columns)
221 title_row = table.AddRow()
222 for title in column_titles:
223 title_row.AddHeader(title)
David Spickett82c94b22023-06-12 16:18:33 +0100224
David Spickett4f932d12023-06-13 12:32:06 +0100225 table.BeginBody()
David Spickett82c94b22023-06-12 16:18:33 +0100226
David Spickett4f932d12023-06-13 12:32:06 +0100227 for bot in builder["bots"]:
228 logging.debug("Writing out status for {}".format(bot["name"]))
David Spickett82c94b22023-06-12 16:18:33 +0100229
David Spickett4f932d12023-06-13 12:32:06 +0100230 row = table.AddRow()
231 base_url = server["base_url"]
232 try:
233 status = bots_status[(base_url, bot["name"])]
234 except KeyError:
235 row.AddCell("{} is offline!".format(bot["name"])).Colspan(
236 num_columns
237 )
238 continue
239 else:
240 if not status.get("valid", True):
241 row.AddCell(
242 "Could not read status for {}!".format(bot["name"])
243 ).Colspan(num_columns)
244 continue
David Spickettf2c82dd2021-06-24 10:01:33 +0100245
David Spickett4f932d12023-06-13 12:32:06 +0100246 row.AddCell(
247 "<a href='{}'>{}</a>".format(status["builder_url"], bot["name"])
248 )
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000249
250 status_cell = row.AddCell()
251 if status["fail"]:
252 status_cell.Style("color:red").Content("FAIL")
253 else:
254 status_cell.Style("color:green").Content("PASS")
David Spickett187f7962022-02-09 12:35:00 +0000255
David Spickett4f932d12023-06-13 12:32:06 +0100256 time_since_cell = row.AddCell()
257 if "time_since" in status:
258 time_since = status["time_since"]
259 # No build should be taking more than a day
260 if time_since > timedelta(hours=24):
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000261 time_since_cell.Style("color:red")
David Spickett4f932d12023-06-13 12:32:06 +0100262 time_since_cell.Content(time_since)
David Spickett82c94b22023-06-12 16:18:33 +0100263
David Spickett4f932d12023-06-13 12:32:06 +0100264 duration_cell = row.AddCell()
265 if "duration" in status:
266 duration_cell.Content(status["duration"])
David Spickett82c94b22023-06-12 16:18:33 +0100267
David Spickett4f932d12023-06-13 12:32:06 +0100268 number_cell = row.AddCell()
269 if "number" in status:
270 number_cell.Content(
271 "<a href='{}'>{}</a>".format(
272 status["build_url"], status["number"]
273 )
274 )
David Spickett82c94b22023-06-12 16:18:33 +0100275
David Spickett4f932d12023-06-13 12:32:06 +0100276 steps_cell = row.AddCell()
277 if "steps" in status and status["steps"]:
David Spickett82c94b22023-06-12 16:18:33 +0100278
David Spickett4f932d12023-06-13 12:32:06 +0100279 def render_step(name, result):
280 return "<font color='{}'>{}</font>".format(
281 RESULT_COLORS[result], name
282 )
David Spickett82c94b22023-06-12 16:18:33 +0100283
David Spickett4f932d12023-06-13 12:32:06 +0100284 step_list = ", ".join(
285 render_step(name, result)
286 for name, result in status["steps"]
287 )
288 steps_cell.Style("text-align:center").Content(step_list)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300289
David Spickett4f932d12023-06-13 12:32:06 +0100290 next_in_progress_cell = row.AddCell()
291 if "next_in_progress" in status:
David Spickettf8502432023-07-07 15:52:26 +0100292 next_build = status["next_in_progress"]
David Spickett4f932d12023-06-13 12:32:06 +0100293 next_in_progress_cell.Content(
David Spickettf8502432023-07-07 15:52:26 +0100294 "No"
295 if next_build is None
296 else "<a href='{}'>Yes</a>".format(next_build)
David Spickett4f932d12023-06-13 12:32:06 +0100297 )
298
Antoine Moynault0cbe02e2023-06-21 08:13:57 +0000299 first_fail_cell = row.AddCell()
300 if "first_fail_number" in status:
301 first_fail_cell.Content(
302 "<a href='{}'>{}</a>".format(
303 status["first_fail_url"], status["first_fail_number"]
304 )
305 )
306
307 fail_since_cell = row.AddCell()
308 if "fail_since" in status:
309 fail_since = status["fail_since"]
310 # No build should fail for more than a day
311 if fail_since > timedelta(hours=24):
312 fail_since_cell.Style("color:red")
313 fail_since_cell.Content(fail_since)
314
David Spickett4f932d12023-06-13 12:32:06 +0100315 table.EndBody()
316
317 # Move temp to main (atomic change)
318 temp.close()
319 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300320
321
322if __name__ == "__main__":
David Spickett4f932d12023-06-13 12:32:06 +0100323 parser = argparse.ArgumentParser()
324 parser.add_argument(
325 "-d", dest="debug", action="store_true", help="show debug log messages"
326 )
David Spickett55cc06c2023-07-26 15:29:20 +0100327 parser.add_argument(
328 "--cachefile",
329 required=False,
330 help="Location of bot status data cache file (a pickled Python object). If it exists use it, "
331 "if it does not, read the status from the network and write it to this path.",
332 )
David Spickett4f932d12023-06-13 12:32:06 +0100333 parser.add_argument("config_file", help="Bots description in JSON format")
334 parser.add_argument("output_file", help="output HTML path")
335 args = parser.parse_args()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300336
David Spickett4f932d12023-06-13 12:32:06 +0100337 if args.debug:
338 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300339
David Spickett4f932d12023-06-13 12:32:06 +0100340 try:
341 with open(args.config_file, "r") as f:
342 config = json.load(f)
343 except IOError as e:
344 print("error: failed to read {} config file: {}".format(args.config_file, e))
345 sys.exit(os.EX_CONFIG)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300346
David Spickett55cc06c2023-07-26 15:29:20 +0100347 status = None
348 if args.cachefile and os.path.exists(args.cachefile):
349 logging.debug("Using cache file {}".format(args.cachefile))
350 with open(args.cachefile, "rb") as f:
351 status = pickle.load(f)
352 else:
353 status = get_buildbot_bots_status(config)
David Spickett55cc06c2023-07-26 15:29:20 +0100354 if args.cachefile:
355 logging.debug("Writing status to cache file {}".format(args.cachefile))
356 with open(args.cachefile, "wb") as f:
357 pickle.dump(status, f)
358
David Spickett4f932d12023-06-13 12:32:06 +0100359 write_bot_status(config, args.output_file, status)