aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdhemerval Zanella <adhemerval.zanella@linaro.org>2020-10-12 11:31:48 -0300
committerAdhemerval Zanella <adhemerval.zanella@linaro.org>2020-10-14 18:17:35 +0000
commitd3e8c4824078cd503f0e5cfd7d0ad0169e8956b7 (patch)
tree268c744bfadec75c8f8f88b492acf22e8b3589b2
parent099326258a4230d1d91a2aa6f7b38d6c031c4c17 (diff)
downloadlinaro-scripts-d3e8c4824078cd503f0e5cfd7d0ad0169e8956b7.tar.gz
Move bot-status to python and adapt it to buildbot v2
The perl syntax was giving me headaches, so I rewrote it in python3. It has exactly the same functionalities, even the debug logging (which is not enabled through command line). The resulting HMTL should be slight different placement, but with the same expected informations. The 'Comments' fields might result in truncated information because some bots returns a long string with all the resulting information instead of a more simple status. I also added a small optimization to use HTTP Keep-alive (through the python requests library) to avoid open multiple connections to fetch each json file. Change-Id: I2bc1e400ac8eabdac876bae7cc2f2344d9fdaa1d
-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 }
]
},
{