#!/usr/bin/perl -w

use Getopt::Std;
use Getopt::Long;
use LWP::UserAgent;
use HTTP::Request::Common qw(GET);
use HTML::TableExtract;
use Nagios::Plugin::Threshold;
use POSIX;
use Switch;
use strict;

# ChangeLog
# 20171022: Initial Version. I'll eventually implement functionality to monitor individual strings and cells.
 
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 - ". 
# Not currently used, but may be implemented in future versions.
my $MAX_MESSAGE_SIZE=512;

# Version and script info.
my $VERSION="1.0";
my $DATE="20171021";
my $SCRIPT_NAME="check_cellwatch.pl";

my ($ok_output, $warn_output, $crit_output, @ok_data, @warn_data, @crit_data, @systems, @threshold_array);
my ($hostname, $timeout, $system, $battery, $warning, $critical, $verbose, $debug, $help, $usage, $version);
GetOptions ("H=s"	=>	\$hostname,
    "timeout=s"		=>	\$timeout,
    "system"		=>	\$system,
    "battery"		=>	\$battery,
    "warning=s"		=>	\$warning,
    "critical=s"	=>	\$critical,
    "verbose"		=>	\$verbose,
    "debug"		=>	\$debug,
    "help"		=>	\$help,
    "usage"		=>	\$help,
    "version"		=>	\$version
);

#### Main Program

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

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

if (! $hostname) {
    print "UKNOWN - You must specify a Cellwatch Monitoring Station to query! Try --help";
    exit(3);
}

if (!($battery) && !($system)) {
   print "UNKNOWN - You must specify either --battery or --system or both. Try --help";
   exit(3);
}

# 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($verbose)) {
   print "UNKNOWN - verbose option is not currently supported";
   exit(3);
}

if (defined($debug)) {
   print "UNKNOWN - debug option is not currently supported";
   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 ($battery && ($warning || $critical)) {
   &set_battery_thresholds;
}
elsif ($battery) {
   print "UNKNOWN - when querying battery stats you must supply either a warning and/or a critical threshold";
   exit(3);
}

&fetch_system_status;

# Build outputs
for (my $i=0 ; $i <=scalar(@systems) ; $i++) {
  
   if ($system && $i == 0) {
      if (defined($crit_data[$i])) { $crit_output .= $systems[$i] . $crit_data[$i] ; }
      if (defined($warn_data[$i])) { $warn_output .= $systems[$i] . $warn_data[$i] ;}
      if (defined($ok_data[$i])) { $ok_output .= $systems[$i] . $ok_data[$i] ;}
   }
   if ($battery && $i > 0) {
      if (defined($crit_data[$i])) { $crit_output .= $systems[$i] . $crit_data[$i] ; }
      if (defined($warn_data[$i])) { $warn_output .= $systems[$i] . $warn_data[$i] ;}
      if (defined($ok_data[$i])) { $ok_output .= $systems[$i] . $ok_data[$i] ;}
   }
}

if (defined($crit_output)) {
   $crit_output =~ s/\s+$//;
   if (defined($warn_output)) {
      $warn_output =~ s/\s+$//;
      print "CRITICAL - $crit_output WARNING - $warn_output";
      exit(2);
   }
   else {
      print "CRITICAL - $crit_output";
      exit(2);
   }
}

if (defined($warn_output)) {
   $warn_output =~ s/\s+$//;
   print "WARNING - $warn_output";
   exit(1);
}

if (defined($ok_output)) {
   $ok_output =~ s/\s+$//;
   print "OK - $ok_output";
   exit(0);
}

print "UKNOWN - No data received internally. This is a bug and should never happen.";
exit(3);

### Functions

sub usage {
    print "$SCRIPT_NAME -H <ip address> [--timeout <seconds>] (--battery | --system) \n\n";
    print "Options:\n";
    print "\t-H\n";
    print "\t   IP Address for Cellwatch monitoring station. 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--system\n";
    print "\t   Evaluate the Overall system table\n\n";
    print "\t--battery\n";
    print "\t   Evaluate the battery table for all battery systems\n\n";
    print "\t--warning\n";
    print "\t   Enter a standard Nagios warning threshold to be used with --battery\n\n";
    print "\t--critical\n";
    print "\t   Enter a standard Nagios critical threshold to be used with --battery\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, $DATE\n\n";
}

sub set_battery_thresholds {
   my (@warn, @crit);
   # Check to see if they are multi-part
   if (defined($warning) && (!($warning =~ /,/))) { print "UNKNOWN - A multi-part warning threshold is needed volts,amps"; exit(3); }
   if (defined($critical) && (!($critical =~ /,/))) { print "UNKNOWN - A multi-part critical threshold is needed volts,amps"; exit(3); }
   # split up the multi-part into an array
   if (defined($warning)) { @warn = split(',', $warning); } 
   if (defined($critical)) { @crit = split(',', $critical); }

   # Both WARNING and CRITICAL thresholds need to be set.
   if (defined($warning) && defined($critical)) {
      push(@threshold_array, Nagios::Plugin::Threshold->new(warning => $warn[0], critical => $crit[0]));
      push(@threshold_array, Nagios::Plugin::Threshold->new(warning => $warn[1], critical => $crit[2]));
   }

   # Just a CRITICAL is defined
   elsif (defined($critical)) {
      push(@threshold_array, Nagios::Plugin::Threshold->new(critical => $crit[0]));
      push(@threshold_array, Nagios::Plugin::Threshold->new(critical => $crit[1]));
   }

   # Just a WARNING is defined
   elsif (defined($warning)) {
      push(@threshold_array, Nagios::Plugin::Threshold->new(warning => $warn[0]));
      push(@threshold_array, Nagios::Plugin::Threshold->new(warning => $warn[1]));
   }
}
   
         
sub fetch_system_status {

   # 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/System";

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

   # If we recieved data from the meter, continue to process it.
   if ($response->is_success) {
      my $data = $response->content;
      $systems[0]="System ";

      # This is the first table from the webpage. Using both depth/count and headers is probably too specific. 
      my $system_status = HTML::TableExtract->new( depth => 0, count => 1, headers => [qw(Voltage Current Temperature Ohmic-value)] );
      $system_status->parse($data);

      # There should be only one table returned from the query, so that's the one we look at. 
      if (defined($system_status->first_table_found())) {
         my $system_status_table = $system_status->first_table_found();

         # I didn't bother to determine what the other possible values there are other than OK. Everything other than OK is a warning.
         # You can replace $warn_data with $crit_data if you need to.
         if ($system_status_table->cell(0,0) eq "OK") { $ok_data[0] .= "Voltage:" . $system_status_table->cell(0,0) . " "; } else { $warn_data[0] .= "Voltage:" . $system_status_table->cell(0,0) . " "; }
         if ($system_status_table->cell(0,1) eq "OK") { $ok_data[0] .= "Current:" . $system_status_table->cell(0,1) . " "; } else { $warn_data[0] .= "Current:" . $system_status_table->cell(0,1) . " "; }
         if ($system_status_table->cell(0,2) eq "OK") { $ok_data[0] .= "Temperature:" . $system_status_table->cell(0,2) . " "; } else { $warn_data[0] .= "Temperature:" . $system_status_table->cell(0,2) . " "; }
         if ($system_status_table->cell(0,3) eq "OK") { $ok_data[0] .= "Ohmic-Value:" . $system_status_table->cell(0,3) . " "; } else { $warn_data[0] .= "Ohmic-Value:" . $system_status_table->cell(0,3) . " "; }
      }
      else {
         print "UNKNOWN - Overall system status table not found!";
         exit(3);
      }

      if ($battery) {
         # Again, this is very specific.
         my $battery_status = HTML::TableExtract->new( depth => 0, count => 2, headers => [qw(Battery Voltage Current Temperature Ohmic-value Volts Amps)] );
         $battery_status->parse($data);
         # Again, should only be one table anyway.
         if (defined($battery_status->first_table_found())) {
            my $battery_status_table = $battery_status->first_table_found();
            my $i=1;
            foreach my $row ($battery_status_table->rows) {

               push(@systems, @$row[0] . " ");

               # Populate the OKs or whatever other text as WARNING, same as in --system
               if (@$row[1] eq "OK") { $ok_data[$i] .= "Voltage:" . @$row[1] . " "; } else { $warn_data[$i] .= "Voltage:" . @$row[1] . " "; }
               if (@$row[2] eq "OK") { $ok_data[$i] .= "Current:" . @$row[2] . " "; } else { $warn_data[$i] .= "Current:" . @$row[2] . " "; }
               if (@$row[3] eq "OK") { $ok_data[$i] .= "Temperature:" . @$row[3] . " "; } else { $warn_data[$i] .= "Temperature:" . @$row[3] . " "; }
               if (@$row[4] eq "OK") { $ok_data[$i] .= "Ohmic-Value:" . @$row[4] . " "; } else { $warn_data[$i] .= "Ohmic-Value:" . @$row[4] . " "; }
               
               # Apply some thresholds to the Volts and Amps
               switch ($threshold_array[0]->get_status(@$row[5])) {
                  case 0 { $ok_data[$i] .= "Volts:" . @$row[5] . " "}
                  case 1 { $warn_data[$i] .= "Volts:" . @$row[5] . " "; }
                  case 2 { $crit_data[$i] .= "Volts:" . @$row[5] . " "; }
               }

               switch ($threshold_array[1]->get_status(@$row[6])) {
                  case 0 { $ok_data[$i] .= "Amps:" . @$row[6] . " "}
                  case 1 { $warn_data[$i] .= "Amps:" . @$row[6] . " "; }
                  case 2 { $crit_data[$i] .= "Amps:" . @$row[6] . " "; }
               }
 
               $i++;
            } 
         }
         else {
            print "UNKNOWN - Battery status table not found!";
            exit(3);
         }
      }
   }
      
   # 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);
   }
}
      

__END__

=head1 NAME

check_cellwatch - Pull and evaluate data from two tables displayed on the main Cellwatch
                  Monitoring system webpage at http://IP/System
=head1 VERSION

This documentation refers to check_cellwatch version 1.0

=head1 APPLICATION REQUIREMENTS

 Several standard Perl libraries are required for this program to function.
 LWP::UserAgent, HTTP::Request::Common, use HTML::TableExtract, 
 Nagios::Plugin::Threshold, and POSIX 

=head1 USAGE

 check_cellwatch.pl -H <ip address> --timeout <seconds> (--battery | --system)

=head1 REQUIRED ARGUMENTS

 --battery or --system. --battery requires --warning and/or --critical. 

=head1 EXAMPLES

 $ check_cellwatch.pl -H X.X.X.X --system --battery --warning 500:560,-10:5 --critical 400:580,-15:10
 
 Check both the Overall system table and the Battery table. If all values in the system table
 are "OK", the plugin will return those elements as OK. If any are something other than OK,
 the plugin will return just those elements as a WARNING. 
 The same goes for the --battery option, with the exception of the "Volts" and "Amps" columns.
 Thresholds are applied to the integer values returned from those cells. In the above example
 if the voltage is outside the range 500-560 a WARNING is returned, and if the voltage is 
 outside the range 400-580 a CRITICAL condition is returned. Similar logic is applied to the 
 amps, having a WARNING range of -10 to 5 and a CRITICAL range -15 to 10. 

 In my case, this would return:

 OK - System Voltage:OK Current:OK Temperature:OK Ohmic-Value:OK Battery: XYZ UPS-1 Voltage:OK \
 Current:OK Temperature:OK Ohmic-Value:OK Volts:515.6 Amps:-3 Battery: EH093BAA09 UPS-2 \
 Voltage:OK Current:OK Temperature:OK Ohmic-Value:OK Volts:514.4 Amps:-5

 For testing I would do something like this:
 $ check_cellwatch.pl -H X.X.X.X --system --battery --warning 500:560,-10:5 --critical 515:580,-15:10

 Which would return:

 CRITICAL - Battery: XYZ UPS-2 Volts:514.4

 See: https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT
 For Nagios threshold examples

=head1 BUGS, LIMITATIONS, INCOMPATIBILITES

 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) 2017 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.
