#!/usr/bin/perl
# -*- cperl -*-

=head1 NAME

boinc_credit - Munin plugin to monitor BOINC credit for a user

=head1 APPLICABLE SYSTEMS

Any

=head1 CONFIGURATION

All that should be needed is to add the following to your config:

   [boinc_credit]
   env.cpid 1234abcd....

Where the value is your Cross Project ID (CPID).

=head1 MAGIC MARKERS

  #%# family=auto contrib
  #%# capabilities=autoconf

=head1 VERSION

1.0

=head1 AUTHOR

Paul Saunders <darac+munin@darac.org.uk>

=cut

use strict;
use warnings;

use lib $ENV{'MUNIN_LIBDIR'};
use Munin::Plugin;

my $CPID     = $ENV{cpid};
my $STATSURL = $ENV{statsurl}
  || "http://boinc.netsoft-online.com/get_user.php?cpid=$CPID";
my $TICK = $ENV{tick} || 60;    # minutes

my $ret;
if ( !eval "require XML::Simple;" ) {
    $ret += "Could not load XML::Simple; ";
}
if ( !eval "require LWP::Simple;" ) {
    $ret += "Could not load LWP::Simple; ";
}

if ( defined $ARGV[0] and $ARGV[0] eq 'autoconf' ) {

    # Can't auto configure at the moment.
    # At least, until we can calculate CPID
    print "no\n";
    exit 0;
}

my $lastread;

sub save_data {
    my @projdata = @_;

    # Do we need to save this data?
    if ( !defined $lastread or time >= $lastread + ( $TICK * 60 ) ) {
        $lastread = time;

        my @save_vector;
        push @save_vector, $lastread;
        foreach (@projdata) {

            # Serialise the hash
            my @tempbuf;
            foreach my $key ( keys %{$_} ) {
                push @tempbuf, $key . '¬' . $_->{$key};
            }
            push @save_vector, join( '^^', @tempbuf );
        }
        save_state(@save_vector);
    }
}

sub load_data {

    # Bring the data back in
    my @save_vector = restore_state();

    # Read the timestamp, Do we need to refresh the data?
    $lastread = shift @save_vector;

    my @projarray;
    foreach (@save_vector) {
        my $hashref;
        foreach ( split /\^\^/ ) {
            my ( $key, $value ) = split /¬/;
            $hashref->{$key} = $value;
        }
        push @projarray, $hashref;
    }

    if ( !defined $lastread or time >= ( $lastread + ( $TICK * 60 ) ) ) {

        # Data is stale

        eval {

            # Fetch the XML
            my $content;
            unless ( defined( $content = LWP::Simple::get $STATSURL) ) {
                die "Could not get $STATSURL";
            }
            my $xmlref = XML::Simple::XMLin( $content, ForceArray => 1 );

            my @temparray;
            foreach ( @{ $xmlref->{project} } ) {
                my $temphash;
                $temphash->{name}   = $_->{name}[0];
                $temphash->{id}     = $_->{project_id}[0];
                $temphash->{credit} = $_->{total_credit}[0];
                $temphash->{creditfract} =
                  $_->{total_credit}[0] / $xmlref->{total_credit}[0];
                $temphash->{totalcredit} = $xmlref->{total_credit}[0];
                $temphash->{rank}        = $_->{project_rank_total_credit}[0];

                push @temparray, $temphash;
            }

            # If the above threw an error, we won't overwrite the old data
            @projarray = @temparray;

            1;
        } or do {
            print $@;
          }
    }
    return @projarray;
}

# Project Colours from http://boinc.netsoft-online.com/e107_plugins/forum/forum_viewtopic.php?3
sub rgb($$$) {
    return sprintf( '%02x%02x%02x', shift, shift, shift );
}
my %project_colour = (
    'climatepredition.net'           => rgb( 0,   139, 69  ),
    'Predictor@Home'                 => rgb( 135, 206, 235 ),
    'SETI@home'                      => rgb( 65,  105, 225 ),
    'Einstein@Home'                  => rgb( 255, 165, 0   ),
    'Rosetta@home'                   => rgb( 238, 130, 238 ),
    'PrimeGrid'                      => rgb( 205, 197, 191 ),
    'LHC@home'                       => rgb( 255, 127, 80  ),
    'World Community Grid'           => rgb( 250, 128, 114 ),
    'BURP'                           => rgb( 0,   255, 127 ),
    'SZTAKI Desktop Grid'            => rgb( 205, 79,  57  ),
    'uFluids'                        => rgb( 0,   0,   0   ),
    'SIMAP'                          => rgb( 143, 188, 143 ),
    'Folding@Home'                   => rgb( 153, 50,  204 ),
    'MalariaControl'                 => rgb( 30,  144, 255 ),
    'The Lattice Project'            => rgb( 0,   100, 0   ),
    'Pirates@Home'                   => rgb( 127, 255, 0   ),
    'BBC Climate Change Experiment'  => rgb( 205, 173, 0   ),
    'Leiden Classical'               => rgb( 140, 34,  34  ),
    'SETI@home Beta'                 => rgb( 152, 245, 255 ),
    'RALPH@Home'                     => rgb( 250, 240, 230 ),
    'QMC@HOME'                       => rgb( 144, 238, 144 ),
    'XtremLab'                       => rgb( 130, 130, 130 ),
    'HashClash'                      => rgb( 255, 105, 180 ),
    'cpdn seasonal'                  => rgb( 255, 255, 255 ),
    'Chess960@Home Alpha'            => rgb( 165, 42,  42  ),
    'vtu@home'                       => rgb( 255, 0,   0   ),
    'LHC@home alpha'                 => rgb( 205, 133, 63  ),
    'TANPAKU'                        => rgb( 189, 183, 107 ),
    'other'                          => rgb( 255, 193, 37  ),
    'Rectilinear Crossing Number'    => rgb( 83,  134, 139 ),
    'Nano-Hive@Home'                 => rgb( 193, 205, 193 ),
    'Spinhenge@home'                 => rgb( 255, 240, 245 ),
    'RieselSieve'                    => rgb( 205, 183, 158 ),
    'Project Neuron'                 => rgb( 139, 58,  98  ),
    'RenderFarm@Home'                => rgb( 210, 105, 30  ),
    'Docking@Home'                   => rgb( 178, 223, 238 ),
    'proteins@home'                  => rgb( 0,   0,   255 ),
    'DepSpid'                        => rgb( 139, 90,  43  ),
    'ABC@home'                       => rgb( 222, 184, 135 ),
    'BOINC alpha test'               => rgb( 245, 245, 220 ),
    'WEP-M+2'                        => rgb( 0,   250, 154 ),
    'Zivis Superordenador Ciudadano' => rgb( 255, 239, 219 ),
    'SciLINC'                        => rgb( 240, 248, 255 ),
    'APS@Home'                       => rgb( 205, 91,  69  ),
    'PS3GRID'                        => rgb( 0,   139, 139 ),
    'Superlink@Technion'             => rgb( 202, 255, 112 ),
    'BRaTS@Home'                     => rgb( 255, 106, 106 ),
    'Cosmology@Home'                 => rgb( 240, 230, 140 ),
    'SHA 1 Collision Search'         => rgb( 255, 250, 205 ),
);

if ( defined $ARGV[0] and $ARGV[0] eq 'config' ) {
    if ($ret) {
        print $ret;
        exit 1;
    }
    my @projdata = load_data();
    print <<EOF;
graph_args --base 1000 --logarithmic
graph_vlabel Cobblestones
graph_category htc
graph_title BOINC Total Credit
EOF
    foreach ( sort { $a->{id} <=> $b->{id} } @projdata ) {
        my $fieldname = 'proj' . $_->{id};
        print <<EOF;
$fieldname.label $_->{name}
$fieldname.type GAUGE
$fieldname.info Total Credit for project $_->{name}
EOF
        if ( exists $project_colour{ $_->{name} } ) {
            print "$fieldname.colour $project_colour{$_->{name}}\n";
        }
    }
    save_data(@projdata);
    exit 0;
}

my @projdata = load_data();
foreach ( sort { $a->{id} <=> $b->{id} } @projdata ) {
    my $fieldname = 'proj' . $_->{id};
    print "$fieldname.value $_->{credit}\n";
    printf "$fieldname.extinfo %.2f%% of Total Credit (%.2f out of %.2f)\n",
      $_->{creditfract} * 100, $_->{credit}, $_->{totalcredit};
}
save_data(@projdata);
exit 0;
