User:AnomieBOT/source/show-task-status.pl

#!/usr/bin/perl -w

use strict;
use utf8;

binmode STDOUT, ":utf8";

use Cwd;
use File::Basename;
use lib File::Basename::dirname( Cwd::realpath( __FILE__ ) );
use LWP::UserAgent;
use JSON;
use AnomieBOT::API;
use POSIX qw/floor ceil/;

# For scaling sizes
my @sizescale = (
	[ 1024**4, 'T' ],
	[ 1024**3, 'G' ],
	[ 1024**2, 'M' ],
	[ 1024**1, 'K' ],
);

# First, get the list of running Kubernetes jobs.
my $jobs;
if ( -f '/var/run/secrets/kubernetes.io/serviceaccount/namespace' ) {
	# We're running inside a container, hit the API.
	open X, '<:utf8', '/var/run/secrets/kubernetes.io/serviceaccount/namespace' or die "Failed to read namespace: $!\n";
	my $namespace = <X>;
	close X;
	my $ua = LWP::UserAgent->new(
		ssl_opts => {
			SSL_ca_file => "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
			SSL_cert_file => "$ENV{HOME}/.toolskube/client.crt",
			SSL_key_file => "$ENV{HOME}/.toolskube/client.key",
		},
	);
	my $res = $ua->get( "https://kubernetes.default.svc/api/v1/namespaces/$namespace/pods/" );
	if ( $res->is_success ) {
		$jobs = JSON->new->decode( $res->decoded_content );
	}
} else {
	# We're running on the bastion, shell out to kubectl.
	$jobs = JSON->new->decode( scalar `kubectl get pods -o json` );
}
my %running = ( 'unknown' => 1 );
if ( $jobs ) {
	foreach my $job ( @{$jobs->{'items'}} ) {
		my $jobhost = $job->{'metadata'}{'name'};
		$running{$jobhost} = 1 if $job->{'status'}{'phase'} eq 'Running';
	}
}

# Next, fetch the list of running tasks from Redis
my $api = AnomieBOT::API->new("conf.ini", 1, { db => 0 });
my @tasks;

my $now = ftime( time );
my $old = time - 1800;
while(1){
    @tasks = ();
    my ($tasks, $token) = $api->cache->gets( 'joblist' );
    die "No joblist\n" unless ref($tasks) eq 'ARRAY';
    die "No tasks\n" unless @$tasks;

    # Now, query the status of every running task
    my @keep = ();
    for my $task (@$tasks) {
        # Check that the task should still be running
        AnomieBOT::API::load("tasks/$task.pm");
        my $pkg = "tasks::$task";
        my $t=$pkg->new();
        my $a=$t->approved;
        next if $a <= 0;
        push @keep, $task;

        my $status = $api->cache->get( "status:$task" ) // {
            'botnum' => '?',
            'hostname' => 'unknown',
            'status' => 'unknown',
            'lastrun' => 0,
            'nextrun' => 0,
        };
        $status->{'task'} = $task;
        if ( !exists( $running{$status->{'hostname'}} ) ) {
            $status->{'status'} = 'pod missing';
            $status->{'nextrun'} = 0;
        }

        $status->{'sort botnum'} = $status->{'botnum'};

        $status->{'lastrun'} = ftime( $status->{'lastrun'} );
        $status->{'sort nextrun'} = '9999-12-31 23:59:59' unless $status->{'nextrun'};
        $status->{'isold nextrun'} = $status->{'nextrun'} < $old if $status->{'nextrun'};
        $status->{'nextrun'} = ftime( $status->{'nextrun'} );
        push @tasks, $status;
    }

    if ( @keep != @$tasks ) {
        redo unless $api->cache->cas( 'joblist', \@keep, $token );
    }

    last;
}

# Now, output. Either plain text for command-line mode, or HTML if passed --html
my $RLdebug = 'true';
if ( grep $_ eq '--html', @ARGV ) {
	print <<EOHTML;
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>AnomieBOT Task Status</title>
<link rel="stylesheet" href="https://tools-static.wmflabs.org/cdnjs/ajax/libs/sortable/0.8.0/css/sortable-theme-minimal.min.css" />
<link rel="stylesheet" href="https://tools-static.wmflabs.org/anomiebot/wikitable.css" />
<script src="https://tools-static.wmflabs.org/cdnjs/ajax/libs/sortable/0.8.0/js/sortable.min.js"></script>
</head>
<body>
<h1>AnomieBOT status</h1>
<p>This page gives the current status of AnomieBOT's various jobs and tasks. For details on each task, see <a href="https://en.wikipedia.org/wiki/User:AnomieBOT/TaskList">User:AnomieBOT/TaskList</a>.</p>
<h2>Tasks</h2>
EOHTML
        print "Report generated $now\n";
        print qq(<table data-sortable class="wikitable tasks">\n);
}

# First, the list of tasks
my @fields = qw/task botnum hostname status lastrun nextrun/;
my %labels = ( task => 'Task', botnum => 'Bot', hostname => 'Host', status => 'Status', lastrun => 'Last run', nextrun => 'Next run' );
my %align  = ( task => '-',    botnum => '',    hostname => '-',    status => '-',      lastrun => '-',        nextrun => '-' );
my %l = ();
print_data( sort { tasksorter($a,$b) } @tasks );

if ( grep $_ eq '--html', @ARGV ) {
	print <<EOHTML;
</table>
<script>if(window.mw){
mw.loader.load(["mediawiki.page.ready"],null,true);
}</script>
</body>
</html>
EOHTML
}


# Utility function to find the max of two numbers
sub max {
	return $_[0] > $_[1] ? $_[0] : $_[1];
}

# Utility function to format a timestamp
sub ftime {
	my $t = shift;
	return '-' unless $t;
	return POSIX::strftime( '%F %T', gmtime $t );
}

# Sorting function for the bot tasks table
sub tasksorter {
	my ($a,$b) = @_;
	my $na = $a->{'botnum'};
	my $nb = $b->{'botnum'};

	return $a->{'task'} cmp $b->{'task'} if $na eq $nb;
	return $na <=> $nb if $na =~ /^\d+/ && $nb =~ /^\d+/;
	return -1 if $na =~ /^\d+/;
	return 1 if $nb =~ /^\d+/;
	return $na cmp $nb;
}

# Simple HTML encoding
sub esc {
	my $s = shift;
	$s=~s/&/&amp;/g;
	$s=~s/</&lt;/g;
	$s=~s/>/&gt;/g;
	$s=~s/"/&quot;/g;
	return $s;
}

# Print the table rows
sub print_data {
	my @rows = @_;

	%l = ();
	for my $row (@rows) {
		for my $k (@fields) {
			$l{$k} = max( $l{$k} // length($labels{$k}), length( $row->{$k} ) );
		}
	}

	my ($make, $pre, $mid, $post);
	if ( grep $_ eq '--html', @ARGV ) {
		$make = sub {
			my ($obj, $k) = @_;
			my $v = esc( $obj->{$k} );
			my $sv = esc( $obj->{"sort $k"} // $obj->{$k} );

			my $td = '<td';
			$td .= $align{$k} eq '' ? ' align="right"' : '';
                        $td .= ' style="background-color:#fcc"' if $obj->{"isold $k"} // 0;
			$td .= qq( data-value="$sv") if $sv ne $v;
			$td .= ">$v</td>";
			return $td;
		};
		($pre, $mid, $post) = ('<tr>', '', '</tr>');
		print "<thead><tr>";
		for my $k (@fields) {
			printf '<th>%s</th>', esc( $labels{$k} );
		}
		print "</tr></thead>\n";
		print "<tbody>\n";
	} else {
		$make = sub {
			my ($obj, $k) = @_;
			return sprintf( "%$align{$k}$l{$k}s", $obj->{$k} );
		};
		($pre, $mid, $post) = ('', '  ', '');
		my @line1 = ();
		my @line2 = ();
		for my $k (@fields) {
			push @line1, $labels{$k} . (" " x ($l{$k} - length($labels{$k})));
			push @line2, "-" x $l{$k};
		}
		print join("  ", @line1) . "\n";
		print join("  ", @line2) . "\n";
	}

	for my $row (@rows) {
		print $pre . join( $mid, map { $make->($row, $_) } @fields) . $post . "\n";
	}

	if ( grep $_ eq '--html', @ARGV ) {
            print "</tbody>\n";
        }
}