aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--monitor/README.txt2
-rwxr-xr-xmonitor/bot-status243
-rwxr-xr-xmonitor/bot-status.py198
-rwxr-xr-xmonitor/install.sh26
-rw-r--r--monitor/linaro.json8
5 files changed, 212 insertions, 265 deletions
diff --git a/monitor/README.txt b/monitor/README.txt
index 652c4eb..4faef1a 100644
--- a/monitor/README.txt
+++ b/monitor/README.txt
@@ -79,11 +79,13 @@ Each buildbot has four columns:
* Status: Can only be "PASS" or "FAIL", but contains additional information
if it fails, ex. "slave lost" or "build stage 1" or "test-suite". These are
the name of the stages that failed.
+ * Time: the total time spent in build reported as HH:MM:SS.
* Build number: The build number, to help identify if there is a change from
a specific number. Not very useful, but there just for reference.
* Commit range: The range of commits that were tested on that build. This is
very helpful to identify if a slow bot is failing because it hasn't yet
reached the commit range on a fast bot that is passing, or not.
+ * Comments: The reported status string in the case of a failure.
LLVM Masters
diff --git a/monitor/bot-status b/monitor/bot-status
deleted file mode 100755
index d1a4986..0000000
--- a/monitor/bot-status
+++ /dev/null
@@ -1,243 +0,0 @@
-#!/usr/bin/env perl
-
-# This script greps the JSON files for the buildbots on
-# the LLVM official build master by name and prints an
-# HTML page with the links to the bots and the status.
-#
-# Multiple masters can be used, as well as multiple groups of bots
-# and multiple bots per group, all in a json file. See linaro.json
-# in this repository to have an idea how the config file is.
-#
-# Module JSON needs to be installed, either from cpan or packages.
-
-push @INC, `dirname $0`;
-
-use strict;
-use warnings;
-# Core modules
-use File::Temp qw/tempfile/;
-use File::Copy;
-# This is not part of core, but you really *need* it.
-use JSON;
-# This can be replaced by `wget/curl`
-use LWP;
-use LWP::UserAgent;
-# We don't have DateTime everywhere...
-my $date = `date`;
-# DEBUG
-my $DEBUG = 0;
-
-######################################################### Initialisation
-# Option checking
-my $syntax = "$0 config-file.json output-file.html\n";
-die $syntax unless (scalar @ARGV == 2);
-# Read config file
-my ($config, $error) = &read_file($ARGV[0]);
-die $error if ($error);
-($config, $error) = &decode($config);
-die $error if ($error);
-
-# Setup HTML output file
-my $output = $ARGV[1];
-my ($temp, $tempname) = tempfile();
-
-
-######################################################### Main Logic
-# Get status for all bots
-my %bot_cache;
-my $fail = 0;
-foreach my $server (@$config) {
- next if (defined $server->{'ignore'} and $server->{'ignore'} eq "true");
- my ($BASE_URL, $BUILDER_URL, $BUILD_URL) =
- ($server->{'base_url'}, $server->{'builder_url'}, $server->{'build_url'});
- &debug("Parsing server ".$server->{'name'}."...\n");
- foreach my $builder (@{$server->{'builders'}}) {
- &debug(" Parsing builder ".$builder->{'name'}."...\n");
- foreach my $bot (@{$builder->{'bots'}}) {
- &debug(" Parsing bot ".$bot->{'name'}."...\n");
- next if defined $bot_cache{$bot->{'name'}};
- my $status = &get_status($bot->{'name'}, $BASE_URL, $BUILDER_URL, $BUILD_URL);
- if (!defined $bot->{'ignore'} or $bot->{'ignore'} ne "true") {
- $fail = 1 if ($status->{'fail'});
- } else {
- &debug(" Ignoring...\n");
- }
- &debug($status->{'fail'} ? " FAIL\n" : " PASS\n");
- $bot_cache{$BASE_URL.'/'.$bot->{'name'}} = $status;
- }
- }
-}
-
-# Dump all servers / bots
-foreach my $server (@$config) {
- next if (defined $server->{'ignore'} and $server->{'ignore'} eq "true");
- my ($BASE_URL, $BUILDER_URL, $BUILD_URL) =
- ($server->{'base_url'}, $server->{'builder_url'}, $server->{'build_url'});
- # Favicon
- my $favicon = $fail ? "fail.ico" : "ok.ico";
- print $temp "<link rel=\"shortcut icon\" href=\"$favicon\" type=\"image/x-icon\"/>\n";
- # Header
- print $temp "<table cellspacing=1 cellpadding=2>\n";
- print $temp "<tr><td colspan=5>&nbsp;</td><tr>\n";
- print $temp "<tr><th colspan=5>$server->{'name'} @ $date</td><tr>\n";
- ## Main loop
- foreach my $builder (@{$server->{'builders'}}) {
- print $temp "<tr><td colspan=5>&nbsp;</td><tr>\n";
- print $temp "<tr><th colspan=5>$builder->{'name'}</td><tr>\n";
- print $temp "<tr><th>Buildbot</th><th>Status</th><th>Comments</th>".
- "<th>Build #</th><th>Commits</th><th>Time (minutes)</th></tr>\n";
- foreach my $bot (@{$builder->{'bots'}}) {
- print $temp "<tr>\n";
- my $status = $bot_cache{$BASE_URL.'/'.$bot->{'name'}};
- my $url = "$BASE_URL/$BUILDER_URL/$bot->{'name'}";
- print $temp " <td><a href='$url'>$bot->{'name'}</a></td>\n";
- if ($status->{'fail'}) {
- print $temp " <td><font color='red'>FAIL</font></td>\n".
- " <td>$status->{'fail'}</td>\n";
- } else {
- print $temp " <td><font color='green'>PASS</font></td>\n".
- " <td>&nbsp;</td>\n";
- }
- if (defined $status->{'build'}) {
- my $build_url = $url."/builds/".$status->{'build'};
- print $temp " <td><a href='$build_url'>$status->{'build'}</a></td>\n";
- } else {
- print $temp " <td>&nbsp;</td>\n";
- }
- if (defined $status->{'from'} and
- defined $status->{'to'}) {
- print $temp " <td>$status->{'from'}-$status->{'to'}</td>\n";
- } else {
- print $temp " <td>&nbsp;</td>\n";
- }
- if (defined $status->{'time'}) {
- my $time = sprintf("%.0f", $status->{'time'} / 60);
- print $temp " <td>$time</td>\n";
- } else {
- print $temp " <td>&nbsp;</td>\n";
- }
- print $temp "</tr>\n";
- }
- }
- # Footer
- print $temp "</table>\n";
-}
-close $temp;
-
-# Move temp to main (atomic change)
-move($tempname, $output);
-exit;
-
-######################################################### Subs
-
-# GET STATUS: get the status of an individual bot
-# (botname, base url, builder url, build url) -> (status)
-sub get_status() {
- my ($bot, $BASE_URL, $BUILDER_URL, $BUILD_URL) = @_;
- my ($err, $contents, $json);
- my %status;
-
- # Get buildbot main JSON
- ($contents, $err) = wget("$BASE_URL/json/$BUILDER_URL/$bot");
- $status{'fail'} = $err;
- return \%status if $err;
- ($json, $err) = decode($contents);
- $status{'fail'} = $err;
- return \%status if $err;
-
- # Find recent builds
- my $cached_builds = scalar @{$json->{'cachedBuilds'}};
- my $running_builds = scalar @{$json->{'currentBuilds'}};
- my $last_build = $json->{'cachedBuilds'}[$cached_builds - $running_builds - 1];
- return \%status if (not defined $last_build);
-
- # Get most recent build
- ($contents, $err) = wget("$BASE_URL/json/$BUILDER_URL/$bot/$BUILD_URL/$last_build");
- $status{'fail'} = $err;
- return \%status if $err;
- ($json, $err) = decode($contents);
- $status{'fail'} = $err;
- return \%status if $err;
-
- # Build number
- $status{'build'} = $json->{'number'};
-
- # Status of the last build
- # "text" : [ "build", "successful" ],
- # "text" : [ "failed", "svn-llvm" ],
- my $failed = 0;
- foreach (@{$json->{'text'}}) {
- $status{'fail'} .= $_." " if ($failed);
- $failed = 1 if (/failed|exception/);
- }
- $status{'fail'} =~ s/ $//;
-
- # Commit range. All LLVM repositories are in git now, so truncate the hashes
- # to 8 characters for display.
- my @commits = @{$json->{'sourceStamp'}->{'changes'}};
- my $first_rev = $commits[0]->{'revision'};
- my $last_rev = $commits[-1]->{'revision'};
- $status{'from'} = substr($first_rev, 0, 8);
- $status{'to'} = substr($last_rev, 0, 8);
-
- # Elapsed time of the last build.
- $status{'time'} = $json->{'times'}[1] - $json->{'times'}[0];
-
- return \%status;
-}
-
-# WGET: uses LWP to get an URL, returns contents (or error).
-# (url) -> (contents, error)
-sub wget() {
- my ($url) = @_;
- my ($contents, $error) = ("", "");
-
- my $ua = LWP::UserAgent->new;
- $ua->agent("LLVM BotMonitor/0.1");
- my $req = HTTP::Request->new(GET => $url);
- my $res = $ua->request($req);
-
- if ($res->is_success) {
- $contents = $res->content;
- } else {
- $error = $res->status_line;
- }
- return ($contents, $error);
-}
-
-# READ FILE: Reads a local file, returns contents
-# (filename) -> (contents)
-sub read_file() {
- my ($file) = @_;
- my ($contents, $error) = ("", "");
- if (open FH, $file) {
- while (<FH>) { $contents .= $_; }
- close FH;
- } else {
- $error = "Can't open config file $file: $!";
- }
- return ($contents, $error);
-}
-
-# DECODE: Reads contents, returns JSON output (or error)
-# (contents) -> (JSON, error)
-sub decode() {
- my ($contents) = @_;
- my ($json, $error) = ("", "");
- eval { $json = decode_json($contents); };
- if ($@) {
- if ($DEBUG) {
- $error = $@;
- } else {
- $error = "JSON error";
- }
- }
- return ($json, $error);
-}
-
-# DEBUG: Prints debug messages if debug enabled
-# (msg) -> ()
-sub debug () {
- my ($msg) = @_;
- print STDERR $msg if ($DEBUG);
-}
diff --git a/monitor/bot-status.py b/monitor/bot-status.py
new file mode 100755
index 0000000..2536fa4
--- /dev/null
+++ b/monitor/bot-status.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+
+# This script greps the JSON files for the buildbots on the LLVM official
+# build master by name and prints an HTML page with the links to the bots
+# and the status.
+#
+# Multiple masters can be used, as well as multiple groups of bots and
+# multiple bots per group, all in a json file. See linaro.json in this
+# repository to have an idea how the config file is.
+
+import sys
+import os
+import argparse
+import json
+import tempfile
+import logging
+from datetime import datetime, timedelta
+# The requests allows HTTP keep-alive which re-uses the same TCP connection
+# to download multiple files.
+import requests
+
+# The GIT revision length used on 'Commits' error display.
+GIT_SHORT_LEN=7
+
+def ignored(s):
+ return 'ignore' in s and s['ignore']
+def not_ignored(s):
+ return not ignored(s)
+
+
+# Returns the parsed json URL or and error string.
+def wget(session, url):
+ try:
+ req = session.get(url)
+ except requests.exceptions.RequestException as e:
+ return str(e), True
+ return req.json(), False
+
+
+# Returns a string with the GIT revision usesd on build BUILDID and
+# PREV_BUILDID in the form '<id_buildid>-<id_prev_buildid>'.
+def get_bot_failure_changes(session, base_url, buildid, prev_buildid):
+ def wget_build_rev(bid):
+ if bid != -1:
+ contents, err = wget(session,
+ "{}/api/v2/builds/{}/changes"
+ .format(base_url, bid))
+ if err or len(contents['changes']) == 0:
+ return ""
+ return contents['changes'][0]['revision']
+ return ""
+
+ revision = wget_build_rev(buildid)
+ prev_revision = wget_build_rev(prev_buildid)
+ if not prev_revision:
+ return "{:.{width}}".format(revision, width=GIT_SHORT_LEN)
+ else:
+ return "{:.{width}}-{:.{width}}".format(revision, prev_revision,
+ width=GIT_SHORT_LEN)
+
+
+# Get the status of a individual bot BOT. Returns a dict with the
+# information.
+def get_bot_status(session, bot, base_url, builder_url, build_url):
+ (contents, err) = wget(session,
+ "{}/api/v2/{}/{}/{}"
+ .format(base_url, builder_url, bot, build_url))
+ if err:
+ return { 'fail' : err }
+
+ builds = contents
+
+ status = {}
+ reversed_builds = reversed(builds['builds'])
+ for build in reversed_builds:
+ if build['complete']:
+ status['builderid'] = build['builderid']
+ status['number'] = build['number']
+ status['state'] = build['state_string']
+ delta = int(build['complete_at']) - int(build['started_at'])
+ status['time'] = str(timedelta(seconds=delta))
+ if build['state_string'] != 'build successful':
+ status['comments'] = build['state_string']
+ status['fail'] = True
+
+ try:
+ prev_buildid = next(reversed_builds)['buildid']
+ except StopIteration:
+ prev_buildid = -1;
+ status['changes'] = get_bot_failure_changes(session, base_url,
+ build['buildid'],
+ prev_buildid)
+ else:
+ status['fail'] = False
+ break
+ return status
+
+
+def bot_status(config_file, output_file):
+ temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
+
+ today = "{}\n".format(datetime.today().ctime())
+
+ session = requests.Session()
+
+ # Get status for all bots
+ bot_cache = {}
+ for server in filter(not_ignored, config):
+ base_url = server['base_url']
+ builder_url = server['builder_url']
+ build_url = server['build_url']
+ logging.debug('Parsing server {}...'.format(server['name']))
+ for builder in server['builders']:
+ logging.debug(' Parsing builders {}...'.format(builder['name']))
+ for bot in builder['bots']:
+ bot_key = "{}/{}".format(base_url, bot['name'])
+ if bot_key in bot_cache:
+ continue
+ logging.debug(' Parsing bot {}...'.format(bot['name']))
+ status = get_bot_status(session, bot['name'], base_url, builder_url,
+ build_url)
+ if not_ignored(bot):
+ fail = 'fail' in status
+ logging.debug(" FAIL" if status['fail'] else " PASS")
+ bot_cache[bot_key] = status
+
+ # Dump all servers / bots
+ for server in filter(not_ignored, config):
+ base_url = server['base_url']
+ builder_url = server['builder_url']
+ build_url = server['build_url']
+ favicon = 'fail.ico' if fail else 'ok.ico'
+ temp.write("<link rel=\"shortcut icon\" href=\"{}\" "
+ "type=\"image/x-icon\"/>\n".format(favicon))
+ temp.write("<table cellspacing=1 cellpadding=2>\n")
+ temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
+ temp.write("<tr><th colspan=5>{} @ {}</td><tr>\n"
+ .format(server['name'], today))
+
+ for builder in server['builders']:
+ temp.write("<tr><td colspan=5>&nbsp;</td><tr>\n")
+ temp.write("<tr><th colspan=5>{}</td><tr>\n".format(builder['name']))
+ temp.write("<tr><th>Buildbot</th><th>Status</th><th>Time</th>"
+ "<th>Build #</th><th>Commits</th><th>Comments</th></tr>\n")
+ for bot in builder['bots']:
+ temp.write("<tr>\n")
+ status = bot_cache["{}/{}".format(base_url, bot['name'])]
+ url = "{}/#/{}/{}".format(base_url, builder_url, status['builderid'])
+ temp.write(" <td><a href='{}'>{}</a></td>\n".format(url, bot['name']))
+ temp.write(" <td><font color='{}'>{}</font></td>\n"
+ .format('red' if status['fail'] else 'green',
+ 'FAIL' if status['fail'] else 'PASS'))
+ empty_cell=" <td>&nbsp;</td>\n"
+ if 'time' in status:
+ temp.write(" <td>{}</td>\n".format(status['time']))
+ else:
+ temp.write(empty_cell)
+ if 'number' in status:
+ build_url = "{}/builds/{}".format(url, status['number'])
+ temp.write(" <td><a href='{}'>{}</a></td>\n".format(build_url, status['number']))
+ else:
+ temp.write(empty_cell)
+ if 'changes' in status:
+ temp.write(" <td>{}</td>\n".format(status['changes']))
+ else:
+ temp.write(empty_cell)
+ if status['fail']:
+ temp.write(" <td>{:.30}</td>\n".format(status['comments']))
+ else:
+ temp.write(empty_cell)
+ temp.write("</tr>\n")
+ temp.write("</table>\n")
+
+ # Move temp to main (atomic change)
+ temp.close()
+ os.rename(temp.name, sys.argv[2])
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-d', dest='debug', action='store_true')
+ parser.add_argument('config_file',
+ help='Bots description in JSON format')
+ parser.add_argument('output_file',
+ help='output HTML path')
+ args = parser.parse_args()
+
+ if args.debug:
+ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+ try:
+ with open(args.config_file, "r") as f:
+ config = json.load(f)
+ except IOError as e:
+ print("error: failed to read {} config file: {}".format(sys.argv[1], e))
+ sys.exit(os.EX_CONFIG)
+
+ bot_status(config, args.output_file)
diff --git a/monitor/install.sh b/monitor/install.sh
index 7f503a5..beceec4 100755
--- a/monitor/install.sh
+++ b/monitor/install.sh
@@ -15,7 +15,7 @@
# Full path
BASE=$(readlink -fn -- "$0")
BASE=$(dirname "$BASE")
-if [ ! -x "$BASE/bot-status" ]; then
+if [ ! -x "$BASE/bot-status.py" ]; then
echo "Make sure the install script is in the monitor directory"
exit 1
fi
@@ -42,29 +42,19 @@ if [ $MANY -eq 1 ]; then
JSON=""
fi
-# Checking for required Perl modules
-if ! perl -v > /dev/null; then
- echo "Please, install Perl"
- exit 1
-fi
-if ! perl -e "File::Temp" > /dev/null; then
- echo "Please, install Perl's File module"
- exit 1
+# Checking for required Python3 modules
+if ! python3 --version > /dev/null; then
+ echo 'Python3 missing'
fi
-if ! perl -e "use JSON" > /dev/null; then
- echo "Please, install Perl's JSON module"
- exit 1
-fi
-if ! perl -e "use LWP" > /dev/null; then
- echo "Please, install Perl's LWP module"
- exit 1
+if ! python3 -c 'import requests' > /dev/null; then
+ echo 'python3-requests module missing'
fi
############################# Install
# Creates bin for bot-status
mkdir -p "$ROOT/bin"
-ln -sf "$BASE/bot-status" "$ROOT/bin/bot-status"
+ln -sf "$BASE/bot-status.py" "$ROOT/bin/bot-status.py"
if [ "$JSON" != "" ]; then
ln -sf "$BASE/$JSON" "$ROOT/bin/$JSON"
fi
@@ -99,5 +89,5 @@ echo
# Crontab
echo " * To run the application every five minutes, add this line to your crontab:"
-echo " */5 * * * * $ROOT/bin/bot-status $ROOT/bin/$JSON $ROOT/html/index.html"
+echo " */5 * * * * $ROOT/bin/bot-status.py $ROOT/bin/$JSON $ROOT/html/index.html"
echo
diff --git a/monitor/linaro.json b/monitor/linaro.json
index 1c19ead..ae83e28 100644
--- a/monitor/linaro.json
+++ b/monitor/linaro.json
@@ -87,7 +87,7 @@
},
{
"name": "Cross-Compilation Bots",
- "ignore" : "true",
+ "ignore" : true,
"bots": [
]
},
@@ -107,7 +107,7 @@
"builders": [
{
"name": "Benchmarking Bots",
- "ignore": "true",
+ "ignore": true,
"bots": [
]
}
@@ -118,12 +118,12 @@
"base_url": "http://buildmaster.tcwglab.linaro.org",
"builder_url": "builders",
"build_url": "builds",
- "ignore" : "true",
+ "ignore" : true,
"builders": [
{
"name": "Benchmarking Bots",
"bots": [
- { "name": "clang-native-arm-lnt-perf", "ignore": "true" }
+ { "name": "clang-native-arm-lnt-perf", "ignore": true }
]
},
{