#!/usr/bin/perl -w

use Getopt::Std;
use Getopt::Long;
use LWP::UserAgent;
use HTTP::Request::Common qw(GET);
use POSIX;
use Time::Local;
use Text::CSV;
use List::MoreUtils qw(uniq any);
use strict;

my $DEFAULT_TIMEOUT=5;
# Nagios MAX_PLUGIN_OUTPUT_LENGTH is typically 8192. Pushover accepts a message size of 1024. 
# Allow for some extra space for text like 'Events: ' and 'WARNING - ". 
my $MAX_MESSAGE_SIZE=512;

# Allow the clock to skew by 30 seconds.
my $FUZZY_SECONDS=30;

# Version and script info.
my $VERSION="1.0";
my $SCRIPT_NAME="check_spd.pl";

# I spent hours trying to use this as a list of partial matches (like just "Voltage Swell on")
# but failed to get it working properly because some events ended up having regex control 
# characters in them which caused nested quantifiers errors. The only way to resolve this was
# with \Q and \E modifiers but that required matching all the literal strings you see below.
# Long story short, add all the EXACT strings to ignore to this array!
my @events_ignore = ("Voltage Swell on    Ph-A", 
	"Voltage Swell on    Ph-B",
	"Voltage Swell on    Ph-C",
	"Temporary OV on     Ph-A",
	"Temporary OV on     Ph-B",
	"Temporary OV on     Ph-C",
	"Voltage High on     Ph-A",
	"Voltage High on     Ph-B",
	"Voltage High on     Ph-C",
	"Voltage Normal on   Ph-A",
	"Voltage Normal on   Ph-B",
	"Voltage Normal on   Ph-C",
	"Voltage Low on      Ph-A",
	"Voltage Low on      Ph-B",
	"Voltage Low on      Ph-C",
	"Voltage Outage on   Ph-A",
	"Voltage Outage on   Ph-B",
	"Voltage Outage on   Ph-C",
	"Volt Dropout on     Ph-A",
	"Volt Dropout on     Ph-B",
	"Volt Dropout on     Ph-C",
	"Over Voltage on     Ph-A",
	"Over Voltage on     Ph-B",
	"Over Voltage on     Ph-C",
	"Voltage Sag on      Ph-A",
	"Voltage Sag on      Ph-B",
	"Voltage Sag on      Ph-C",
	"Voltage Loss on     Ph-A",
	"Voltage Loss on     Ph-B",
	"Voltage Loss on     Ph-C",
	"XPort Comm. Normal",
	"Battery Status OK",
	"**TVSS NAM V1.15**  M4E",
	"**TVSS NAM V1.11**  M4E",
	"**TVSS NAM V1.07**  M4E",
	"M1 Comm. Normal"
);

# cheezy oversight on my part!
my $not_ok = 0;

my (@output, $return);
my ($hostname, $timeout, $tov, $swell, $overvoltage, $sag, $dropout, $outage, $events, $history, $critical, $verbose, $debug, $help, $version);
GetOptions ("H=s"	=>	\$hostname,
    "timeout=s"		=>	\$timeout,
    "tov"		=>	\$tov,
    "swell"		=>	\$swell,
    "overvoltage"	=>	\$overvoltage,
    "sag"		=>	\$sag,
    "dropout"		=>	\$dropout,
    "outage"		=>	\$outage,
    "events"		=>	\$events,
    "history=s"		=>	\$history,
    "critical"		=>	\$critical,
    "verbose"		=>	\$verbose,
    "debug"		=>	\$debug,
    "help"		=>	\$help,
    "version"		=>	\$version
);

#### Main Program

if ($help) {
    usage();
    exit();
}

if ($version) {
    version();
    exit();
}

if (! $hostname) {
    print "\nYou must specify a Current Technology SPD to query!\n\n";
    usage();
    exit();
}

# The timeout was not specified on the command line.
# We set it now, the 3 minute default is too long.
if (!$timeout) {
    $timeout = $DEFAULT_TIMEOUT;
}

# We got some garbage for the timeout. Fix that now.
elsif (!isdigit($timeout)) {
    $timeout = $DEFAULT_TIMEOUT;
}

if (!(defined($history))) {
   print "UNKNOWN - history is not defined";
   exit(3);
}

if (!(isdigit($history))) {
   print "UNKNOWN - history is not an integer";
   exit(3);
}

# Some code I stole to check IP address validity.
# Seems to work! I don't want to bother with DNS.
if( $hostname =~ m/^(\d\d?\d?)\.(\d\d?\d?)\.(\d\d?\d?)\.(\d\d?\d?)$/ ) {

    if(! ($1 <= 255 && $2 <= 255 && $3 <= 255 && $4 <= 255)) {
	print "UNKNOWN - IP address is invalid!\n";
	exit(3);
    }
}
else {
    print "UNKNOWN - IP address is invalid!\n";
    exit(3);
}

if (!(defined($tov) || defined($swell) || defined($overvoltage) || defined($sag) || defined($dropout) || defined($outage) || defined($events))) {
   print "UNKNOWN - No metrics defined to monitor. See --usage";
   exit(3);
}

# Find the epoch time of history to evaluate
my $timehist = (time() - ($history*60));

if (defined($tov)) {
   $return = &fetch_records_csv("m3_records_tov_download.csv");
   push(@output, "TOV: $return");
}
if (defined($swell)) {
   $return = &fetch_records_csv("m3_records_swell_download.csv");
   push(@output, "SWELL: $return");
}
if (defined($overvoltage)) {
   $return = &fetch_records_csv("m3_records_ov_download.csv");
   push(@output, "OVERVOLTAGE: $return");
}
if (defined($sag)) {
   $return = &fetch_records_csv("m3_records_sag_download.csv");
   push(@output, "SAG: $return");
}
if (defined($dropout)) {
   $return = &fetch_records_csv("m3_records_dropout_download.csv");
   push(@output, "DROPOUT: $return"); 
}
if (defined($outage)) {
   $return = &fetch_records_csv("m3_records_outage_download.csv");
   push(@output, "OUTAGE: $return");
}
if (defined($events)) {
   my %events = &fetch_events_csv();
   $return = &process_events(%events);
   push(@output, "EVENTS: $return");
}

if (defined($critical) && $not_ok) {
   print "CRITICAL - @output";
   exit(2);
}

if ($not_ok) {
   print "WARNING - @output";
   exit(1);
}

print "OK - @output";
exit(0);

### Functions

sub usage {
    print "$SCRIPT_NAME --hostname <ip address> [--timeout <seconds>]\n\n";
    print "Options:\n";
    print "\t--hostname\n";
    print "\t   IP Address for Current Technology Surge Protective Device. DNS is not supported\n\n";
    print "\t--timeout\n";
    print "\t   Specify timeout for LWP call. Defaults to 5 seconds\n\n";
    print "\t--tov\n";
    print "\t   Look for temporary overvoltage records\n\n";
    print "\t--swell\n";
    print "\t   Look for voltage swell records\n\n";
    print "\t--overvoltage\n";
    print "\t   Look for over voltage records\n\n";
    print "\t--sag\n";
    print "\t   Look for voltage sag records\n\n";
    print "\t--dropout\n";
    print "\t   Look for voltage dropout records\n\n";
    print "\t--outage\n";
    print "\t   Look for voltage outage records\n\n";
    print "\t--events\n";
    print "\t   Look through the events log for major issues\n\n";
    print "\t--history\n";
    print "\t   Specify the history (in minutes) to evaluate from the records\n\n";
    print "\t--help\n";
    print "\t   This message.\n\n";
    print "\t--version\n";
    print "\t   Version information.\n\n";
    print "\t--verbose\n";
    print "\t   Not used at this time.\n\n";
    print "\t--debug\n";
    print "\t   Not used at this time\n\n";
}

sub version {
    print "\n$SCRIPT_NAME version $VERSION Written by Eric Schoeller, 20150330\n\n";
}

sub process_events {
   my %events = @_;
   my %current_events;
   foreach my $event_ref (sort keys %events) {
      # Strip out any tailing spaces. This is very important!
      ${$events{$event_ref}}[1] =~ s/\s*$//g;
      # Determine if this event should be filtered.
      if (!(/\Q${$events{$event_ref}}[1]\E/ ~~ @events_ignore)) {
         $current_events{${$events{$event_ref}}[0]} = ${$events{$event_ref}}[1];
      }
   }
   if (keys(%current_events) < 1) {
      return("No Events");
   }
   else {
      $not_ok = 1;
      my $events_output = '';
      for my $epoch ( sort(keys %current_events) ) {
            my ($sec,$min,$hour,$day,$month,$year) = (localtime($epoch))[0,1,2,3,4,5];
            my $date_string = sprintf '%02d:%02d:%02d %d/%d/%d', $hour, $min, $sec, $month+1, $day, $year+1900;
            # Eliminate any excessive spaces in the event text.
            $current_events{$epoch} =~ s/\s{2,}/ /g;
            # Recursively build the event output string
            $events_output = $current_events{$epoch} . " (" . $date_string . ")" . " " . $events_output;
      }
      if (length($events_output) > $MAX_MESSAGE_SIZE) {
         return(substr($events_output, 0, $MAX_MESSAGE_SIZE) . " - TRUNCATED");
      }
      # Otherwise just return. Could be wrapped in an else() but isn't necessary.
      return($events_output);
   }

}

sub fetch_events_csv {

   # See fetch_records_csv for comments. Data processing was slightly different,
   # the Text::CSV method did not work here.
   my %events;
   my $ua = LWP::UserAgent->new();
   $ua->timeout($timeout);
   my $req = GET "http://$hostname/m3_events_download.csv";
   my $response = $ua->request($req);
   if ($response->is_success) {
      my $data = $response->content;
      my @events_array = split(/\n/, $data);
      foreach my $event (@events_array) {
         my @fields = split(/,/, $event);
         $fields[0] =~ s/ //g;
         if (isdigit($fields[0])) {
            my @day = split(/-/,$fields[2]);
            my @time = split(/:/,$fields[3]);
            my $epoch = timelocal($time[2],$time[1],$time[0],$day[2],($day[1]-1),$day[0]);
            # Not a great place for this ... but I am a stickler for keeping these damn things 
            # from having a date of 2020 ... But allow FUZZY_SECONDS of clock skew
            if ($epoch > (time+$FUZZY_SECONDS)) {
               print "CRITICAL - SPD date/time is set in the future! " . "(" . localtime($epoch) .")" . " Reset ASAP.";
               exit(2);
            }
            if ($epoch > $timehist) {
               push @{ $events{$fields[0]} }, $epoch, $fields[1];
            }
         }
      }
      return(%events);
   }
      
   # Our LWP call did not return a response. Exit with UNKNWON status and indicate the LWP error.
   else {
      print "UNKNOWN - " . $response->status_line ;
      exit(3);
   }
}
      
sub fetch_records_csv {

   # define a hash which will later hold array references to CSV data.
   # http://www.perlmonks.org/?node_id=686151
   my %records;
 
   # Fire up a new user agent
   my $ua = LWP::UserAgent->new();

   # Set the timeout. The default of 3 minutes is too long!
   $ua->timeout($timeout);

   # Build the HTTP GET request
   my $req = GET "http://$hostname/$_[0]";

   # Set the response
   my $response = $ua->request($req);

   # If we recieved data from the meter, continue to process it.
   if ($response->is_success) {

      # LWP returns data in a huge single line with some sort of newline character
      # that I cannot filter/break for. Following a suggestion online I use Text::CSV
      # (yes, sorry for the additional module requirement) to sort through the mess,
      # and for whatever reason it's only possible by passing it a file handle.
      # http://stackoverflow.com/questions/3851905/how-can-i-parse-downloaded-csv-data-with-perl
      # Don't ask me why!!!
      my $data = $response->content;
      open my $fh, '<', \$data or die $!;
      my $csv = Text::CSV->new({ sep_char => ',' });
      while (my $row = $csv->getline($fh)) {
         my @fields = @{ $row };
         # The only way for isdigit to work correctly on $fields[0] (RecNum) is to strip out any spaces.
         $fields[0] =~ s/ //g;      
         # First line of CSV data is always text, so we ignore there here, and potentially any other garbage.
         if (isdigit($fields[0])) { 
            my @day = split(/-/,$fields[4]);
            my @time = split(/:/,$fields[5]);
            # Epoch conversion. Note ($day[1]-1), since timelocal wants months 0-11, not 1-12.
            my $epoch = timelocal($time[2],$time[1],$time[0],$day[2],($day[1]-1),$day[0]);
            # Not a great place for this ... but I am a stickler for keeping these damn things 
            # from having a date of 2020 ... but allow FUZZY_SECONDS of clock skew
            if ($epoch > (time+$FUZZY_SECONDS)) {
               print "CRITICAL - SPD date/time is set in the future! " . "(" . localtime($epoch) .")" . " Reset ASAP.";
               exit(2);
            }
            # If the current event occurred within our desired history range, we process it.
            if ($epoch > $timehist) {
               # Clean up the fields, remove any leading spaces
               $fields[1] =~ s/^\s*//g;
               $fields[2] =~ s/^\s*//g;
               $fields[3] =~ s/^\s*//g;
               # Populate the records hash with an array for each recorded event. 
               # The RecNum will serve as the key, however it is likely never really used.
               push @{ $records{$fields[0]} }, $fields[1], $fields[2], $fields[3], $epoch;
            }
         }
      }
   }

   # Our LWP call did not return a response. Exit with UNKNWON status and indicate the LWP error.
   else {
      print "UNKNOWN - " . $response->status_line ;
      exit(3);
   }

   # Proceed with data processing, our LWP call succeeded. 
   if (keys(%records) < 1) {
      return("No Events");
   }
   else {
      $not_ok = 1;
      # For all the records collect which phases the events occurred on.
      my @phases;
      foreach my $record_ref (sort keys %records) {
         push(@phases, ${$records{$record_ref}}[2]);
      }
      # Strip out all the duplicate phases with uniq(), then sort them to alphabetical order
      # join the array into a single comma separated scalar variable.
      my $flat_phases = join(",", (sort(uniq(@phases))));

      # Only 3 records across 3 phases. This is could be a full 3-phase event
      if ((keys(%records) == 3) && (scalar(uniq(@phases)) == 3) ) {
         # Pull in the voltage and duration data and see if they all match up.
         my (@volts,@duration);
         foreach my $record_ref (sort keys %records) {
            push(@duration, ${$records{$record_ref}}[1]);
            push(@volts, ${$records{$record_ref}}[0]);
         }
         # Voltage and Duration are all equal. This is a full 3phase event, report accordingly.
         if ( (scalar(uniq(@duration)) == 1) && (scalar(uniq(@volts)) == 1) ) {
            return($volts[0] . " volts, " . $duration[0] . ", Phases " . $flat_phases);
         }
      }
      # For more than 1 event, which do not meet full 3phase criteria. Report both differently 
      # given the number of unique phases affected.
      #if (scalar(uniq(@phases)) > 1) {
      #   return(keys(%records) . " Events on Phases " . $flat_phases);
      #} 
      #else {
      #   return(keys(%records) . " Events on Phase " . $flat_phases);
      #}
      my $events_output = '';
      for my $record_ref ( sort(keys %records) ) {
            my ($sec,$min,$hour,$day,$month,$year) = (localtime(${$records{$record_ref}}[3]))[0,1,2,3,4,5];
            my $date_string = sprintf '%02d:%02d:%02d %d/%d/%d', $hour, $min, $sec, $month+1, $day, $year+1900;
            # Eliminate any excessive spaces in the event text.
            ${$records{$record_ref}}[1] =~ s/\s{2,}/ /g;
            # Recursively build the event output string
            $events_output = ${$records{$record_ref}}[0] . " volts on Phase " . ${$records{$record_ref}}[2] . " for " . ${$records{$record_ref}}[1] . " (" . $date_string .") " . $events_output;
      }
      if (length($events_output) > $MAX_MESSAGE_SIZE) {
         return(substr($events_output, 0, $MAX_MESSAGE_SIZE) . " - TRUNCATED");
      }
      # Otherwise just return. Could be wrapped in an else() but isn't necessary.
      return($events_output);
   }
}



__END__

=head1 NAME

check_spd - Scrape logs from a Thomas and Betts Current Technology Surge Protective Device
            and report appropriately on events of interest

=head1 VERSION

This documentation refers to check_spd version 1.0

=head1 APPLICATION REQUIREMENTS

 Several standard Perl libraries are required for this program to function.
 LWP::UserAgent, HTTP::Request::Common and POSIX.

=head1 USAGE

 check_spd.pl --hostname <ip address> --timeout <seconds>

=head1 REQUIRED ARGUMENTS

 Only the hostname is required. Timeout will default to 5 seconds

=head1 INCOMPATIBILITIES

 None. See Bugs.

=head1 BUGS AND LIMITATIONS

 None.
 If you experience any problems please contact me. (eric.schoeller <at> coloradoDOTedu)

=head1 AUTHOR

Eric Schoeller (eric.schoeller <at> coloradoDOTedu)

=head1 LICENCE AND COPYRIGHT

 Copyright (c) 2015 Eric Schoeller (eric.schoeller <at> coloradoDOTedu).
 All rights reserved.

 This module is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License.
 See L<http://www.fsf.org/licensing/licenses/gpl.html>.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
