# -*- perl -*-
#
# $Revision: 1.5 $

###
#
#  Infortrend SNMP Query-based nagios check
#  Jacob Ela, Nov 2008
#  ---
#  Monitors the A16S-G2130 connected to (new)bio via snmp.
#  Probably would work for other Infortrend enclosures, but would need to confirm
#  that the snmp oid's are the same and may need tweaks if some of these statuses aren't
#  present.
#
#  Could also use some tidying.

#  Updated 1-5-2011 to downgrade logical drive warning to notable - see comments
#  Also worth noting is that the number of logical drives is hardcoded - if the raid
#  configuration ever changes on this, it'll probably break the monitoring script.  woops!
#  good (bad) thing its a one off special case enclosure..

package infortrend;

require Exporter;
@ISA = qw(Exporter);
@EXPORT_OK = qw(&print_usage &print_version &service_test);
use lib $main::runtimedir;
use csl_utils qw($TIMEOUT %ERRORS);

sub print_usage ();
sub print_version ();
sub service_test (%);

use strict;
use Net::SNMP;

# nagios flags
my $warning_flag;
my $critical_flag;
my $spare_drive_flag;

# default config constants
my $VERBOSE = 0;
my $TIMEOUT = 5;

my $HOST; # set in the nagios check

# Drive status definitions
my $DRIVE_STATUS = ".1.3.6.1.4.1.1714.1.1.6.1.11.";
my $NUM_DRIVES = 16;
my %DRIVE_STATUS_CODES = (
  "0" => "WARNING: New Drive",
  "1" => "OK",
  "2" => "WARNING: Used Drive",
  "3" => "Spare Drive",
  "4" => "WARNING: Drive Initializing",
  "5" => "WARNING: Drive Rebuilding",
  "6" => "WARNING: Adding drive to LD",
  "9" => "Chassis Spare Drive",
  "17" => "WARNING: Cloning drive",
  "18" => "WARNING: Valid Clone",
  "19" => "WARNING: Copying drive",
  "63" => "WARNING: Drive Absent",
  "252" => "CRITICAL: Missing Global Spare",
  "253" => "CRITICAL: Missing Spare",
  "254" => "CRITICAL: Missing Drive",
  "255" => "CRITICAL: Failed Drive",
  "ok" => 1 );

# logical drive status definitions
# 1-5-11: Changing the Initializing message to notable.  The array has very rarely over the 
# past couple years cleared the 3rd drive to ok on snmp despite running fine and the web
# interface not complaining.  it's a blight on nagios to see it all the time, so we'll
# downgrade the monitoring warning because we can't change what the array is reporting.
my $LD_STATUS = ".1.3.6.1.4.1.1714.1.1.2.1.6.";
my $NUM_LDS = 3;
my %LD_STATUS_CODES = (
  "0" => "OK",
  "1" => "WARNING: Rebuilding",
  "2" => "Notable: Initializing",
  "3" => "CRITICAL: Degraded",
  "4" => "CRITICAL: Dead",
  "5" => "CRITICAL: Invalid",
  "6" => "CRITICAL: Incomplete",
  "7" => "CRITICAL: Drive Missing",
  "ok" => 0);

# ludev statuses (psus, temps, fans)
my $LUDEV_STATUS = ".1.3.6.1.4.1.1714.1.1.9.1.13.";
# ludev is tricky, since statuses change based on which ludev object we're looking at
# and they're all bit flags.  we can define what each bit means if set with a hash of hash.
my %PSU_STATUS = (
  "0" => "CRITICAL: PSU Malfunctioning",
  "6" => "CRITICAL: PSU Off",
  "7" => "CRITICAL: PSU Not Present"
);

my %FAN_STATUS = (
  "0" => "CRITICAL: Fan Malfunctioning",
  "6" => "CRITICAL: Fan Off",
  "7" => "CRITICAL: Fan Not Present"
);

my %TEMP_BIT_STATUS = (
  "0" => "CRITICAL: Temp Sensor Malfunctioning",
  "6" => "CRITICAL: Temp Sensor Not Activated",
  "7" => "CRITICAL: Temp Sensor Not Present"
);

# Temp sensors also have three bits to indicate temp ranges.
my %TEMP_RANGE_STATUS = (
  "0" => "OK",
  "2" => "WARNING: Cold Temp Warning",
  "3" => "WARNING: Hot Temp Warning",
  "4" => "CRITICAL: Cold Temp Limit Exceeded",
  "5" => "CRITICAL: Hot Temp Limit Exceeded"
);

# Assign the lu objects to their bit status hashes
my %LUDEV_STATUS_HASHES = (
  "1" => \%TEMP_BIT_STATUS, 
  "2" => \%TEMP_BIT_STATUS,
  "3" => \%TEMP_BIT_STATUS,
  "8" => \%PSU_STATUS,
  "9" => \%PSU_STATUS,
  "10" => \%FAN_STATUS,
  "11" => \%FAN_STATUS,
  "12" => \%FAN_STATUS,
  "13" => \%FAN_STATUS,
  "14" => \%TEMP_BIT_STATUS
);

my %LUDEV_NAMES = (
  "1" => "CPU Temp Sensor",
  "2" => "Board1 Temp Sensor",
  "3" => "Board2 Temp Sensor",
  "8" => "PSU0",
  "9" => "PSU1",
  "10" => "Cooling fan 0",
  "11" => "Cooling fan 1",
  "12" => "Cooling fan 2",
  "13" => "Cooling fan 3",
  "14" => "Middle Backplane Inner Temp 0"
);

# subs to help decode bit flags
sub dec2bin {
    my $str = unpack("B32", pack("N", shift));
    return $str;
}

sub bin2dec {
    return unpack("N", pack("B32", substr("0" x 32 . shift, -32)));
}

# decode status and set flags if need be
sub decode {
  my $code_set = shift;
  my $code = shift;
  my $ludev_hash;
  my $message;
  
  if ($code_set eq $DRIVE_STATUS) {
    $message = $DRIVE_STATUS_CODES{$code};
  }elsif ($code_set eq $LD_STATUS) {
    $message = $LD_STATUS_CODES{$code};
  }elsif ($code_set eq $LUDEV_STATUS) {
    $ludev_hash = shift;
    $message = $ludev_hash->{$code};
  }
  
  unless ($message) {
    $message = "CRITICAL: Unknown SNMP code $code returned # ";
    $critical_flag = 1;
    return $message;
  }

  if ($message =~ /CRITICAL/) {
    $critical_flag = 1;
  }elsif ($message =~ /WARNING/) {
    $warning_flag = 1;
  }

  return $message;
}

# quitting
sub quit_check {
  unless ($spare_drive_flag) {
    print ("CRITICAL: No Spare Drive Present # ");
    $critical_flag = 1;
  }

  if ($critical_flag) {
    return $ERRORS{CRITICAL};
  }elsif ($warning_flag) {
    return $ERRORS{WARNING};
  }else {
    unless ($VERBOSE) {print "Enclosure OK";}
    return $ERRORS{OK};
  }
}

# timeouts, since net::smtp doesnt seem to timeout if the host doens't respond,
# despite the documentation.  I bet it's actually me, but this sure is easy.
$SIG{'ALRM'} = sub {
     print ("CRITICAL: No snmp response from $HOST ($TIMEOUT sec. alarm timeout) # ");
     $critical_flag = 1;
     quit_check;
};

sub print_version () {
    my $version = '$Revision: 1.5 $';
    $version =~ s/^\$Revision: //;
    $version =~ s/ \$\s*$//;

    print "infortrend.pm - $version\n";
    return $ERRORS{'OK'};
}

sub print_usage () {

    print "
Usage:  Check Infortrend disk chassis status.
Options:
    -H <hostname>
    -t <seconds> check timeout value
    --verbose
    --help
    --version
    --usage
";
    return $ERRORS{'OK'};
}


#############
# check begins

sub service_test (%) {
  my %options = @_;
  
  $HOST = $options{"opt_H"};
  $VERBOSE = ($options{"verbose"} || $VERBOSE);
  $TIMEOUT = ($options{"opt_t"} || $TIMEOUT);

  if ((!$HOST) || ($HOST eq "")) {
    print "\nHost name not specified.\n";
    &print_usage();
    return $ERRORS{CRITICAL};
  }

  $warning_flag = $critical_flag = $spare_drive_flag = 0;

  alarm($TIMEOUT); # arm the timeout

  my ($session, $error) = Net::SNMP->session(
  	-hostname => $HOST
  );

  if (!defined($session)) {
    printf("CRITICAL: Error: %s. # ", $error);
    $critical_flag = 1;
    quit_check;
  }

  my $result;
  my $querystring;

  ####
  # get all the drive status codes and store in %drive_status_results by bay
  #####

  my %drive_status_results;

  for(my $i = 1; $i <= $NUM_DRIVES; $i++) {
    $querystring = $DRIVE_STATUS . $i;
    $result = $session->get_request(
      -varbindlist => [$querystring]
    );
    $drive_status_results{$i} = $result->{$querystring};
  }

  ###
  # get all of the logical drive status codes and store in %ld_status_results by id
  ####

  my %ld_status_results;

  for (my $i = 1; $i <= $NUM_LDS; $i++) {
    $querystring = $LD_STATUS . $i;
    $result = $session->get_request(
      -varbindlist => [$querystring]
    );
    $ld_status_results{$i} = $result->{$querystring};
  }

  ###
  # get the ludev codes we care about and put them in %ludev_status_results by id
  ####

  my %ludev_status_results;

  foreach my $object_number (keys %LUDEV_STATUS_HASHES) {
    $querystring = $LUDEV_STATUS . $object_number;
    $result = $session->get_request(
      -varbindlist => [$querystring]
    );
    $ludev_status_results{$object_number} = $result->{$querystring};
  }

  # close the session, we have what we need
  $session->close;

  alarm(0); # cancel the timeout

  ###
  # Start looking at the data we recieved
  ####
 
  # First, drive bay statuses
  foreach my $fact (sort {$a <=> $b} keys %drive_status_results) {
    my $foo = $drive_status_results{$fact};
    #if ($drive_status_results{$fact} == $DRIVE_STATUS_CODES{"ok"}) {
    if (($foo == 1) || ($foo == 3) || ($foo == 9)) { # OK, Spare, Chassis Spare
      if (($foo == 3) || ($foo == 9)) { $spare_drive_flag = 1; }
      if ($VERBOSE) { print "Bay$fact: OK # "; }
    } else {
      my $message = decode($DRIVE_STATUS, $foo);
      print "Bay$fact: $message # ";
    }
  }

  # Next, statuses on the logical drives
  foreach my $fact (sort {$a <=> $b} keys %ld_status_results) {
    my $foo = $ld_status_results{$fact};
    if ($foo == $LD_STATUS_CODES{"ok"}) {
      if ($VERBOSE) { print "Logical Drive $fact: OK # "; }
    } else {
      $foo = dec2bin($foo); # turns foo into a binary string
      $foo = substr($foo, -8); # get the last 8 chars
      my $status_code = substr($foo, 0, 3);
      my $offline_status = substr($foo, -1);
      $status_code = bin2dec($status_code); # back to decimal
      my $message = decode($LD_STATUS, $status_code);
      print "Logical Drive $fact: $message";
      if ($offline_status) {
        $critical_flag = 1;
        print ", CRITICAL: Off-line";
      }
      print " # ";
    }
  }

  # and finally, the ludev statuses
  foreach my $fact (sort {$a <=> $b} keys %ludev_status_results) {
    # most of these are bit flags, but temps have an integer inside of it for ranges.
    my $foo = $ludev_status_results{$fact};
    my $messagestring = $LUDEV_NAMES{$fact} . ":";
    if ($foo == 0) { 
      if ($VERBOSE) { print $messagestring . " OK # "; }
    } else {
      # its got something to say, but to decode it we need to know what type it is
      # we may as well get the binary string we'll need for all of them anyway too
      my %applicable_hash = %{$LUDEV_STATUS_HASHES{$fact}};
      $foo = dec2bin($foo);
      $foo = substr($foo, -8);
      if (%applicable_hash == %TEMP_BIT_STATUS) {
        # temp sensor, so in addition to the other bits we want the middle integer
        my $status_code = substr($foo, 1, 3);
        $status_code = bin2dec($status_code);
        $messagestring .= " " . decode($LUDEV_STATUS, $status_code, \%TEMP_RANGE_STATUS);
      }
      my $bar;
      # go through each defined bit and if set, concat what it means to the message
      foreach my $bit_position (keys %applicable_hash) {
        $bar = substr($foo, $bit_position, 1);
        if ($bar eq "1") {
          $messagestring .= " " . decode($LUDEV_STATUS, $bit_position, \%applicable_hash);
        }
      }
    
      print $messagestring . " # "; 
    }
  }

  quit_check;
}
