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

=head1 NAME

zenus_ - Munin plugin to monitor the usage of a Zen Internet Broadband account

=head1 APPLICABLE SYSTEMS

Any (Though most likely those connected to a Zen Internet Broadband connection)

=head1 CONFIGURATION

This plugin requires the C<zenus> module to be installed. This can be fetched
from L<http://www.rachaelandtom.info/zenus>.

Tip: To see if it's already setup correctly, just run this plugin
with the parameter 'autoconf'. If you get a "yes", everything should
work like a charm already.

This configuration section shows the defaults of the plugin:

	[zenus_*]
		env.user
		env.pass
		env.account
		env.tick	60

You must supply C<env.user> and C<env.pass>. These will be your PORTAL username and password, NOT your ADSL account details.

You may either specify the account to report through C<env.account> (e.g. zen12345@zen) or by naming the symlink in your plugins directory (e.g. zenus_zen12345_zen). If no account is specified, the default account will be chosen.

C<env.tick> specifies how often (in minutes) the data will be refreshed from upstream. This is to avoid hitting the Zen servers every five minutes. Data is cached between runs.

=head1 INTERPRETATION

The plugin shows the amount of data downloaded and uploaded since the beginning of the chargable period (i.e. the calendar month). A thick line shows the current download allowance (planned plus banked). A thinner line shows the estimated amount remaining at the end of the month. If you are estimated to exceed your allowance before the end of the month, a second line appears showing the rate at which you're downloading beyond the sustainable rate.

Warnings are sent if your estimated allowance drops below 25% of your cap and if the overspeed rate rises above zero. Critical warnings are sent if your estimated allowance drops below 10% of your cap.

=head1 MAGIC MARKERS

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

=head1 AUTHOR

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

=cut

use strict;
use warnings;

use lib $ENV{'MUNIN_LIBDIR'};
use Munin::Plugin;
use Data::Dump qw(pp);
use Time::Local;

my $ret = undef;

# Load modules like so
if ( !eval "require zenus;" ) {
    $ret =
      "Could not load zenus. Get it from http://www.rachaelandtom.info/zenus\n";
    $ret .= "(BTW, \@INC is: " . join( ', ', @INC ) . ")\n";
}

my $CAN_LOG = 1;
if ( !eval "require Log::Log4perl;" ) {
    $CAN_LOG = 0;
}
elsif ( !eval "require Log::Dispatch::FileRotate;" ) {
    $CAN_LOG = 0;
}

my $USER = $ENV{user};
my $PASS = $ENV{pass};
my $ACCT = $ENV{account} || "";
my $TICK = $ENV{tick} || 60;      # minutes

my @name_fields = split /_/, $0;
if ( scalar @name_fields == 3 ) {
    if ( $name_fields[1] eq '' or $name_fields[2] eq '' ) {
        print "Misconfigured symlink. See Documentation\n";
        exit 1;
    }
    else {
        $ACCT = $name_fields[1] . '@' . $name_fields[2];
    }
}

# If there are more or less than 3 components to the filename,
# we just carry on with the default account

if ($CAN_LOG) {
    my $loggerconf = q(
		log4perl.rootLogger 				= DEBUG, LOG1
		log4perl.appender.LOG1 				= Log::Dispatch::FileRotate
		log4perl.appender.LOG1.filename		= /var/log/munin/zenus.log
		log4perl.appender.LOG1.mode			= append
		log4perl.appender.LOG1.DatePattern	= yyyy-ww
		log4perl.appender.LOG1.layout		= Log::Log4perl::Layout::PatternLayout
		log4perl.appender.LOG1.layout.ConversionPattern = %d %p %c: %m%n
	);
    Log::Log4perl::init( \$loggerconf );
    our $logger = Log::Log4perl->get_logger($ACCT);
} else {
	our $logger = "";
}

my $lastread;

sub save_data {
    my $hashref = shift;
    my $update_lastread = shift || 1;

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

        $lastread = time if $update_lastread;
        print "# Updating LastRead to " . localtime($lastread) . "\n";

        my @save_vector;
        push @save_vector, $lastread;

        # Push the hash values on to the array
        foreach ( keys %$hashref ) {
            push @save_vector, $_ . '¬' . $hashref->{$_};
        }

        $::logger->info(
            "Saving Data (LastRead is " . localtime($lastread) . "\n" )
          if $CAN_LOG;

        #Go!
        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 $hashref;
    foreach (@save_vector) {
        my ( $key, $value ) = split /¬/;
        $hashref->{$key} = $value;
    }
    my $force_save = 0;
    if ( !defined $lastread or time >= ( $lastread + ( $TICK * 60 ) ) ) {

        # Data is stale
        print "# Data is " . ( time - $lastread ) . " seconds old\n";
        $::logger->info( "Data is " . ( time - $lastread ) . " seconds old\n" )
          if $CAN_LOG;

        if ( exists $hashref->{backoff} ) {
            if ( time <= $hashref->{backoff} ) {

                # We're in a back-off period
                print "# Back-off in effect\n";
                $::logger->info("Back-off in effect\n") if $CAN_LOG;
                exit 0;
            }
            else {
                # We've just come out of a back-off period
                print "# Back-off ends\n";
                $::logger->info("Back-off ends\n") if $CAN_LOG;
                delete $hashref->{backoff};
            }
        }
        elsif ( !defined $lastread ) {

            # Initial run. Carry on
            print "# Initial Run\n";
            my $force_save = 1;
        }
        elsif ( time >= ( $lastread + ( 120 * 60 ) ) ) {

            # Data is VERY Stale. Assume a login issue and
            # back-off for 24 hours.
            print "# Starting Back-Off\n";
            $hashref->{backoff} = time + ( 24 * 60 * 60 );
            $::logger->info(
                "Backing off until " . localtime( $hashref->{backoff} ) . "\n" )
              if $CAN_LOG;
            save_data( $hashref, 0 );
            exit 1;
        }

        print "# REFRESHING DATA\n";
        $::logger->info("Refreshing Data\n") if $CAN_LOG;
        my $temphash;
        eval {
            zenus::login( $USER, $PASS );

            (
                $temphash->{name},  $temphash->{used},
                $temphash->{avail}, $temphash->{per},
                $temphash->{uploadAmount}
            ) = zenus::getUsage($ACCT);
            (
                $temphash->{dayofmonth}, $temphash->{permonth},
                $temphash->{avedaily},   $temphash->{maxdaily},
                $temphash->{daysremain}, $temphash->{estusage},
                $temphash->{daysinmonth}
              )
              = zenus::estimateRemaining( $temphash->{used},
                $temphash->{avail} );
            $hashref = $temphash;
            1;

            # If zenus threw an error we won't copy the data over,
            #  so we still use the cached data.
        } or do {
            print "# Zenus Error $@\n";
        };
    }
    else {
        print "# Using existing data\n";
        $::logger->info("Using existing data\n") if $CAN_LOG;
    }
    save_data( $hashref, 1 ) if $force_save;
    return $hashref;

}

if ( defined $ARGV[0] and $ARGV[0] eq "autoconf" ) {
    if ($ret) {
        print "no ($ret)\n";
        exit 1;
    }
    print "yes\n";
    exit 0;
}

if ( defined $ARGV[0] and $ARGV[0] eq "suggest" ) {
    if ( defined $USER and defined $PASS ) {
        zenus::login( $USER, $PASS );
        for my $account ( zenus::getAccounts() ) {
            if ( defined $account ) {
                $account =~ s/\@/_/g;
                print "$account\n";
            }
        }
    }
    exit 0;
}

if ( defined $ARGV[0] and $ARGV[0] eq "config" ) {
    if ($ret) {
        print $ret;
        exit 1;
    }
    my $data    = load_data();
    my $datestr = scalar( localtime($lastread) );
    print <<EOF;
graph_args --base 1000
graph_vlabel GBytes out (-) / in (+)
graph_category Network
graph_title Zen Broadband Usage ($ACCT)
graph_info Usage of your Zen Broadband account: $data->{name}
upload.label Uploaded
upload.type GAUGE
upload.info Amount of data uploaded (No upper limit)
upload.draw AREA
upload.graph no
upload.colour 00CC00EE
download.label Transfer
download.type GAUGE
download.draw AREA
download.extinfo Last Read was $datestr
download.negative upload
download.colour 00CC00EE
EOF
    if ( defined $data->{avail} and $data->{avail} != 0 ) {
        my $cap  = sprintf( "%.3f", $data->{avail} );
        my $warn = sprintf( "%.3f", $cap * 0.25 );
        my $crit = sprintf( "%.3f", $cap * 0.1 );
        print <<EOF;
download.info Amount of data downloaded (Limit is $cap GB)
allowance.label Allowance
allowance.type GAUGE
allowance.info Amount of data you are allowed to download this month
allowance.draw LINE2
remaining.label Est'd Remaining
remaining.type GAUGE
remaining.info Estimated amount of data transfer remaining at the end of the month
remaining.draw LINE1
remaining.min 0
remaining.max $cap
remaining.warning $warn:
remaining.critical $crit:
overrate.label Overspeed Rate
overrate.type GAUGE
overrate.info Rate at which you're downloading beyond the sustainable rate
overrate.draw LINE1
overrate.min 0
overrate.warning 0:
EOF
    }
    else {
        # Probably an unlimited contract
        print "download.info Amount of data downloaded\n";
    }

    save_data($data);
    exit 0;
}

my $data = load_data();
print "upload.value " . $data->{uploadAmount} . "\n";
print "download.value " . $data->{used} . "\n";
if ( defined $data->{avail} and $data->{avail} != 0 ) {
    print "allowance.value " . $data->{avail} . "\n";
    my $remain = $data->{avail} - $data->{estusage};
    $remain = 0 if $remain < 0;
    print "remaining.value " . $remain . "\n";
    my $overrate = $data->{avedaily} - $data->{maxdaily};
    $overrate = 0 if $overrate < 0;
    print "overrate.value " . $overrate . "\n";
}
save_data($data);
exit 0;
