blob: 9bf60d7a365ef9f2974692c4ffe1b0557c8d24b0 [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
David Spickett82c94b22023-06-12 16:18:33 +010024from make_table import Table
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030025
David Spickett30a986f2021-04-29 09:37:00 +010026from buildkite_status import get_buildkite_bots_status
27
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030028def ignored(s):
29 return 'ignore' in s and s['ignore']
30def not_ignored(s):
31 return not ignored(s)
32
33
David Spickette88fe592021-03-22 12:25:13 +000034# Returns the parsed json URL or raises an exception
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030035def wget(session, url):
David Spickett8306ed82021-12-06 10:40:36 +000036 got = session.get(url)
37 got.raise_for_status()
38 return got.json()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030039
40
Oliver Stannard91688ff2021-01-07 10:27:27 +000041# Map from buildbot status codes we want to treat as errors to the color they
42# should be shown in. The codes are documented at
43# https://docs.buildbot.net/latest/developer/results.html#build-result-codes,
44# and these colors match the suggested ones there.
45RESULT_COLORS = {
46 2: 'red', # Error
47 4: 'purple', # Exception
48 5: 'purple', # Retry
49 6: 'pink', # Cancelled
50}
51
52def get_bot_failing_steps(session, base_url, buildid):
David Spickette88fe592021-03-22 12:25:13 +000053 try:
54 contents = wget(session, "{}/api/v2/builds/{}/steps"
55 .format(base_url, buildid))
56 except requests.exceptions.RequestException:
Oliver Stannard91688ff2021-01-07 10:27:27 +000057 return ""
David Spickette88fe592021-03-22 12:25:13 +000058
Oliver Stannard91688ff2021-01-07 10:27:27 +000059 for step in contents["steps"]:
David Spickett7f18f4d2021-03-22 11:49:17 +000060 if step["results"] in RESULT_COLORS:
Oliver Stannard91688ff2021-01-07 10:27:27 +000061 yield (step["name"], step["results"])
62
63
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030064# Get the status of a individual bot BOT. Returns a dict with the
65# information.
66def get_bot_status(session, bot, base_url, builder_url, build_url):
David Spickette88fe592021-03-22 12:25:13 +000067 try:
68 builds = wget(session,
69 "{}/api/v2/{}/{}/{}"
70 .format(base_url, builder_url, bot, build_url))
71 except requests.exceptions.RequestException as e:
David Spickett7d47cea2022-06-01 14:13:32 +010072 logging.debug(" Couldn't get builds for bot {}!".format(bot))
73 return {'valid': False}
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030074
Oliver Stannard46e99032021-01-05 10:30:56 +000075 reversed_builds = iter(sorted(builds['builds'], key=lambda b: -b["number"]))
David Spickett86f2d472023-06-12 11:21:16 +010076 next_build = None
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -030077 for build in reversed_builds:
David Spickett86f2d472023-06-12 11:21:16 +010078 if not build['complete']:
79 next_build = build
80 continue
David Spickett30a986f2021-04-29 09:37:00 +010081
David Spickett86f2d472023-06-12 11:21:16 +010082 time_since = (int(datetime.now().timestamp()) - int(build['complete_at']))
83 duration = int(build['complete_at']) - int(build['started_at'])
84 agent_url = "{}/#/{}/{}".format(base_url, builder_url, build['builderid'])
David Spickett30a986f2021-04-29 09:37:00 +010085
David Spickett86f2d472023-06-12 11:21:16 +010086 status = {
87 'builder_url': agent_url,
88 'number': build['number'],
89 'build_url': "{}/builds/{}".format(agent_url, build['number']),
90 'state': build['state_string'],
91 'time_since': timedelta(seconds=time_since),
92 'duration': timedelta(seconds=duration),
93 'fail': build['state_string'] != 'build successful',
94 'next_in_progress': next_build is not None
95 }
David Spickett7f18f4d2021-03-22 11:49:17 +000096
David Spickett86f2d472023-06-12 11:21:16 +010097 if status['fail']:
98 buildid = build['buildid']
99 status['steps'] = list(get_bot_failing_steps(session, base_url,
100 buildid))
101
102 return status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300103
104
David Spickett85355fa2021-03-22 15:41:41 +0000105# Get status for all bots named in the config
106# Return a dictionary of (base_url, bot name) -> status info
David Spickett30a986f2021-04-29 09:37:00 +0100107def get_buildbot_bots_status(config):
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300108 session = requests.Session()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300109 bot_cache = {}
David Spickettf006c372021-03-22 12:54:12 +0000110
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300111 for server in filter(not_ignored, config):
David Spickett30a986f2021-04-29 09:37:00 +0100112 if server['name'] == "Buildkite":
113 continue
114
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300115 base_url = server['base_url']
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300116 logging.debug('Parsing server {}...'.format(server['name']))
117 for builder in server['builders']:
118 logging.debug(' Parsing builders {}...'.format(builder['name']))
119 for bot in builder['bots']:
David Spickett85355fa2021-03-22 15:41:41 +0000120 bot_key = (base_url, bot['name'])
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300121 if bot_key in bot_cache:
122 continue
David Spickett85355fa2021-03-22 15:41:41 +0000123
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300124 logging.debug(' Parsing bot {}...'.format(bot['name']))
David Spickett85355fa2021-03-22 15:41:41 +0000125 status = get_bot_status(session, bot['name'], base_url, server['builder_url'],
126 server['build_url'])
David Spickettf2c82dd2021-06-24 10:01:33 +0100127 if status is not None:
David Spickett7d47cea2022-06-01 14:13:32 +0100128 if status.get("valid", True):
129 logging.debug(" Bot status: " + ("FAIL" if status['fail'] else "PASS"))
David Spickettf2c82dd2021-06-24 10:01:33 +0100130 bot_cache[bot_key] = status
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300131
David Spickett85355fa2021-03-22 15:41:41 +0000132 return bot_cache
133
David Spickett82c94b22023-06-12 16:18:33 +0100134
David Spickett85355fa2021-03-22 15:41:41 +0000135def write_bot_status(config, output_file, bots_status):
136 temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
David Spickettf006c372021-03-22 12:54:12 +0000137
David Spickett55449c62021-12-13 12:57:33 +0000138 temp.write(dedent("""\
David Spickett64161b02022-11-01 09:51:30 +0000139 <!DOCTYPE html>
David Spickett55449c62021-12-13 12:57:33 +0000140 <style>
141 /* Combine the border between cells to prevent 1px gaps
142 in the row background colour. */
143 table, td, th {
144 border-collapse: collapse;
145 }
146 /* Colour every other row in a table body grey. */
147 tbody tr:nth-child(even) td {
148 background-color: #ededed;
149 }
150 </style>"""))
151
David Spickette44441b2023-06-12 12:23:28 +0100152 column_titles = [
153 "Buildbot",
154 "Status",
155 "T Since",
156 "Duration",
157 "Build",
158 "Failing steps",
159 "Build In Progress",
160 ]
161 num_columns = len(column_titles)
David Spickette44441b2023-06-12 12:23:28 +0100162
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):
David Spickett82c94b22023-06-12 16:18:33 +0100170 with Table(temp) as table:
171 table.Border(0).Cellspacing(1).Cellpadding(2)
David Spickettec94cc22021-12-13 13:16:05 +0000172
David Spickett82c94b22023-06-12 16:18:33 +0100173 table.AddRow().AddCell().Colspan(num_columns)
David Spickette44441b2023-06-12 12:23:28 +0100174
David Spickett82c94b22023-06-12 16:18:33 +0100175 if first:
176 table.AddRow().AddHeader("Generated {} ({})".format(
177 datetime.today().ctime(), time.tzname[time.daylight])).Colspan(num_columns)
178 table.AddRow().AddCell().Colspan(num_columns)
179 first = False
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300180
David Spickett82c94b22023-06-12 16:18:33 +0100181 table.AddRow().AddHeader(server['name']).Colspan(num_columns)
182
183 for builder in server['builders']:
184 table.AddRow().AddCell().Colspan(num_columns)
185 table.AddRow().AddHeader(builder['name']).Colspan(num_columns)
186 title_row = table.AddRow()
187 for title in column_titles:
188 title_row.AddHeader(title)
189
190 table.BeginBody()
191
192 for bot in builder['bots']:
193 logging.debug("Writing out status for {}".format(bot['name']))
194
195 row = table.AddRow()
196 base_url = server['base_url']
197 try:
198 status = bots_status[(base_url, bot['name'])]
199 except KeyError:
200 row.AddCell("{} is offline!".format(bot['name'])).Colspan(num_columns)
201 continue
202 else:
203 if not status.get('valid', True):
204 row.AddCell("Could not read status for {}!".format(
205 bot['name'])).Colspan(num_columns)
David Spickett30a986f2021-04-29 09:37:00 +0100206 continue
David Spickettf2c82dd2021-06-24 10:01:33 +0100207
David Spickett82c94b22023-06-12 16:18:33 +0100208 row.AddCell("<a href='{}'>{}</a>".format(status['builder_url'], bot['name']))
209 row.AddCell("<font color='{}'>{}</font>"
210 .format('red' if status['fail'] else 'green',
211 'FAIL' if status['fail'] else 'PASS'))
David Spickett187f7962022-02-09 12:35:00 +0000212
David Spickett82c94b22023-06-12 16:18:33 +0100213 time_since_cell = row.AddCell()
214 if 'time_since' in status:
215 time_since = status['time_since']
216 # No build should be taking more than a day
217 if time_since > timedelta(hours=24):
218 time_since = "<p style=\"color:red\">{}</p>".format(
219 time_since)
220 else:
221 time_since = str(time_since)
222
223 time_since_cell.Content(time_since)
224
225 duration_cell = row.AddCell()
226 if 'duration' in status:
227 duration_cell.Content(status['duration'])
228
229 number_cell = row.AddCell()
230 if 'number' in status:
231 number_cell.Content("<a href='{}'>{}</a>".format(
232 status['build_url'], status['number']))
233
234 steps_cell = row.AddCell()
235 if 'steps' in status and status['steps']:
236 def render_step(name, result):
237 return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
238 step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
239 steps_cell.Style("text-align:center").Content(step_list)
240
241 next_in_progress_cell = row.AddCell()
242 if 'next_in_progress' in status:
243 next_in_progress_cell.Content(
244 "Yes" if status['next_in_progress'] else "No")
245
246 table.EndBody()
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300247
248 # Move temp to main (atomic change)
249 temp.close()
David Spickett7f18f4d2021-03-22 11:49:17 +0000250 shutil.move(temp.name, output_file)
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300251
252
253if __name__ == "__main__":
254 parser = argparse.ArgumentParser()
David Spickettec2166c2021-07-19 14:17:29 +0100255 parser.add_argument('-d', dest='debug', action='store_true',
256 help='show debug log messages')
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300257 parser.add_argument('config_file',
258 help='Bots description in JSON format')
259 parser.add_argument('output_file',
260 help='output HTML path')
261 args = parser.parse_args()
262
263 if args.debug:
264 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
265
266 try:
267 with open(args.config_file, "r") as f:
268 config = json.load(f)
269 except IOError as e:
David Spickett7f18f4d2021-03-22 11:49:17 +0000270 print("error: failed to read {} config file: {}".format(args.config_file, e))
Adhemerval Zanellad3e8c482020-10-12 11:31:48 -0300271 sys.exit(os.EX_CONFIG)
272
David Spickett30a986f2021-04-29 09:37:00 +0100273 status = get_buildbot_bots_status(config)
274 status.update(get_buildkite_bots_status(config))
275 write_bot_status(config, args.output_file, status)