blob: 2536fa462d6b5a4e1907fb97be6ad904b3fa354c [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
17from datetime import datetime, timedelta
18# The requests allows HTTP keep-alive which re-uses the same TCP connection
19# to download multiple files.
20import requests
21
22# The GIT revision length used on 'Commits' error display.
23GIT_SHORT_LEN=7
24
25def ignored(s):
26 return 'ignore' in s and s['ignore']
27def not_ignored(s):
28 return not ignored(s)
29
30
31# Returns the parsed json URL or and error string.
32def wget(session, url):
33 try:
34 req = session.get(url)
35 except requests.exceptions.RequestException as e:
36 return str(e), True
37 return req.json(), False
38
39
40# Returns a string with the GIT revision usesd on build BUILDID and
41# PREV_BUILDID in the form '<id_buildid>-<id_prev_buildid>'.
42def get_bot_failure_changes(session, base_url, buildid, prev_buildid):
43 def wget_build_rev(bid):
44 if bid != -1:
45 contents, err = wget(session,
46 "{}/api/v2/builds/{}/changes"
47 .format(base_url, bid))
48 if err or len(contents['changes']) == 0:
49 return ""
50 return contents['changes'][0]['revision']
51 return ""
52
53 revision = wget_build_rev(buildid)
54 prev_revision = wget_build_rev(prev_buildid)
55 if not prev_revision:
56 return "{:.{width}}".format(revision, width=GIT_SHORT_LEN)
57 else:
58 return "{:.{width}}-{:.{width}}".format(revision, prev_revision,
59 width=GIT_SHORT_LEN)
60
61
62# Get the status of a individual bot BOT. Returns a dict with the
63# information.
64def get_bot_status(session, bot, base_url, builder_url, build_url):
65 (contents, err) = wget(session,
66 "{}/api/v2/{}/{}/{}"
67 .format(base_url, builder_url, bot, build_url))
68 if err:
69 return { 'fail' : err }
70
71 builds = contents
72
73 status = {}
74 reversed_builds = reversed(builds['builds'])
75 for build in reversed_builds:
76 if build['complete']:
77 status['builderid'] = build['builderid']
78 status['number'] = build['number']
79 status['state'] = build['state_string']
80 delta = int(build['complete_at']) - int(build['started_at'])
81 status['time'] = str(timedelta(seconds=delta))
82 if build['state_string'] != 'build successful':
83 status['comments'] = build['state_string']
84 status['fail'] = True
85
86 try:
87 prev_buildid = next(reversed_builds)['buildid']
88 except StopIteration:
89 prev_buildid = -1;
90 status['changes'] = get_bot_failure_changes(session, base_url,
91 build['buildid'],
92 prev_buildid)
93 else:
94 status['fail'] = False
95 break
96 return status
97
98
99def bot_status(config_file, output_file):
100 temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
101
102 today = "{}\n".format(datetime.today().ctime())
103
104 session = requests.Session()
105
106 # Get status for all bots
107 bot_cache = {}
108 for server in filter(not_ignored, config):
109 base_url = server['base_url']
110 builder_url = server['builder_url']
111 build_url = server['build_url']
112 logging.debug('Parsing server {}...'.format(server['name']))
113 for builder in server['builders']:
114 logging.debug(' Parsing builders {}...'.format(builder['name']))
115 for bot in builder['bots']:
116 bot_key = "{}/{}".format(base_url, bot['name'])
117 if bot_key in bot_cache:
118 continue
119 logging.debug(' Parsing bot {}...'.format(bot['name']))
120 status = get_bot_status(session, bot['name'], base_url, builder_url,
121 build_url)
122 if not_ignored(bot):
123 fail = 'fail' in status
124 logging.debug(" FAIL" if status['fail'] else " PASS")
125 bot_cache[bot_key] = status
126
127 # Dump all servers / bots
128 for server in filter(not_ignored, config):
129 base_url = server['base_url']
130 builder_url = server['builder_url']
131 build_url = server['build_url']
132 favicon = 'fail.ico' if fail else 'ok.ico'
133 temp.write("<link rel=\"shortcut icon\" href=\"{}\" "
134 "type=\"image/x-icon\"/>\n".format(favicon))
135 temp.write("<table cellspacing=1 cellpadding=2>\n")
136 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
137 temp.write("<tr><th colspan=5>{} @ {}</td><tr>\n"
138 .format(server['name'], today))
139
140 for builder in server['builders']:
141 temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
142 temp.write("<tr><th colspan=5>{}</td><tr>\n".format(builder['name']))
143 temp.write("<tr><th>Buildbot</th><th>Status</th><th>Time</th>"
144 "<th>Build #</th><th>Commits</th><th>Comments</th></tr>\n")
145 for bot in builder['bots']:
146 temp.write("<tr>\n")
147 status = bot_cache["{}/{}".format(base_url, bot['name'])]
148 url = "{}/#/{}/{}".format(base_url, builder_url, status['builderid'])
149 temp.write(" <td><a href='{}'>{}</a></td>\n".format(url, bot['name']))
150 temp.write(" <td><font color='{}'>{}</font></td>\n"
151 .format('red' if status['fail'] else 'green',
152 'FAIL' if status['fail'] else 'PASS'))
153 empty_cell=" <td>&nbsp;</td>\n"
154 if 'time' in status:
155 temp.write(" <td>{}</td>\n".format(status['time']))
156 else:
157 temp.write(empty_cell)
158 if 'number' in status:
159 build_url = "{}/builds/{}".format(url, status['number'])
160 temp.write(" <td><a href='{}'>{}</a></td>\n".format(build_url, status['number']))
161 else:
162 temp.write(empty_cell)
163 if 'changes' in status:
164 temp.write(" <td>{}</td>\n".format(status['changes']))
165 else:
166 temp.write(empty_cell)
167 if status['fail']:
168 temp.write(" <td>{:.30}</td>\n".format(status['comments']))
169 else:
170 temp.write(empty_cell)
171 temp.write("</tr>\n")
172 temp.write("</table>\n")
173
174 # Move temp to main (atomic change)
175 temp.close()
176 os.rename(temp.name, sys.argv[2])
177
178
179if __name__ == "__main__":
180 parser = argparse.ArgumentParser()
181 parser.add_argument('-d', dest='debug', action='store_true')
182 parser.add_argument('config_file',
183 help='Bots description in JSON format')
184 parser.add_argument('output_file',
185 help='output HTML path')
186 args = parser.parse_args()
187
188 if args.debug:
189 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
190
191 try:
192 with open(args.config_file, "r") as f:
193 config = json.load(f)
194 except IOError as e:
195 print("error: failed to read {} config file: {}".format(sys.argv[1], e))
196 sys.exit(os.EX_CONFIG)
197
198 bot_status(config, args.output_file)