#!/usr/local/bin/perl
############################## check_netapp_df   ##############
# $Id: check_netapp_df.pl,v 1.1.1.1 2007/05/07 06:18:48 jsellens Exp $
# John Sellens (jsellens@syonex.com) made this as a derivative of:
# ############################## check_snmp_storage ##############
# # Version : 1.1
# # Date :  Feb 16 2005
# # Author  : Patrick Proy ( patrick at proy.org)
# # Help : http://www.manubulon.com/nagios/
# # Licence : GPL - http://www.fsf.org/licenses/gpl.txt
# # TODO : better options in snmpv3
# #################################################################
#
# check_netapp_df adds the -a -z options, which lets you do things like
#   check_netapp_df.pl -a -C public -H host -m .
# or
#   check_netapp_df.pl -a -C public -H host -m '/proc|/kern' -e
# and get something that can generally be blindly applied against lots
# of machines, and if the limits creep up and you want to reset them,
# you can add the -z option which resets all the auto limits
#   check_netapp_df.pl -a -C public -H host -m '/proc|/kern' -e -z
#
# help : ./check_netapp_df -h

 
use strict;
use Net::SNMP;
use Getopt::Long;

# Nagios specific

use lib "/usr/local/nagios/libexec";
use utils qw(%ERRORS $TIMEOUT);
#my $TIMEOUT = 15;
#my %ERRORS=('OK'=>0,'WARNING'=>1,'CRITICAL'=>2,'UNKNOWN'=>3,'DEPENDENT'=>4);

# SNMP Datas
# this is enterprises.netapp.netapp1.filesys.dfTable.dfEntry
# .1.3.6.1.4.1.789.1.5.4.1 */
my $dfEntry = ".1.3.6.1.4.1.789.1.5.4.1";
# I'm too lazy to do 64 bit arithmetic so we can use the
# "qdfHighTotalKBytes" and "qdfLowTotalKBytes" OIDs
# so let's just do percent's here


my $index_table = "$dfEntry.1";
my $descr_table = "$dfEntry.2";
my $dpercent_table = "$dfEntry.6.";
my $ipercent_table = "$dfEntry.9.";
my $size_table = '1.3.6.1.2.1.25.2.3.1.5.';
my $used_table = '1.3.6.1.2.1.25.2.3.1.6.';

# Globals

my $Name='check_netapp_df';
my $Version='1.1';

my $Autolimitdir="/tmp/$Name";
my %climits;			# where multiple auto critical limits go
my %wlimits;			# where multiple auto warning limits go
my $writelimits = 0;		# need to write out the limits file?

my $o_host = 	undef; 		# hostname 
my $o_community = undef; 	# community 
my $o_port = 	161; 		# port
my $o_descr = 	undef; 		# description filter 
my $o_warn = 	undef; 		# warning limit 
my $o_crit=	undef; 		# critical limit
my $o_help=	undef; 		# wan't some help ?
my $o_autolimit=undef; 		# use separate initialized limit files
my $o_zaplimits=undef; 		# replace any existing auto limits
my $o_type=	undef;		# pl, pu
my @o_typeok=   ("pu","pl");    # valid values for o_type
my $o_verb=	undef;		# verbose mode
my $o_version=   undef;          # print version
my $o_noreg=	undef;		# Do not use Regexp for name
my $o_index=	undef;		# Parse index instead of description
my $o_negate=	undef;		# Negate the regexp if set
my $o_timeout=  5;            	# Default 5s Timeout
if (defined($TIMEOUT)) {
    $o_timeout = $TIMEOUT;	# more specific default from utils.pm
}
my $o_perf=	undef;		# Output performance data
# SNMP V3 specific
my $o_login=	undef;		# snmp v3 login
my $o_passwd=	undef;		# snmp v3 passwd

# functions

sub p_version { print "$Name version : $Version\n"; }

sub print_usage {
    print "Usage: $Name [-v] -H <host> -C <snmp_community> | (-l login -x passwd) [-p <port>] -m <name in desc_oid> -w <warn_level> -c <crit_level> [-t <timeout>] [-T pl|pu|bl|bu ] [-a] [-r] [-i] [-e] [-z]\n";
}

sub usage_error {
  my ( $msg ) = @_;
  if ( defined( $msg ) && $msg ne "" ) {
    print "$Name:  $msg\n";
  }
  print_usage();
  exit $ERRORS{"UNKNOWN"};
}

sub round ($$) {
    sprintf "%.$_[1]f", $_[0];
}

# Get the alarm signal (just in case snmp timout screws up)
$SIG{'ALRM'} = sub {
     print ("ERROR: General time-out (Alarm signal)\n");
     exit $ERRORS{"UNKNOWN"};
};

sub help {
   print "\nSNMP Disk Monitor for Nagios version ",$Version,"\n";
   print "(c)2004 to my cat Ratoune - Author : Patrick Proy\n\n";
   print_usage();
   print <<EOT;
By default, plugin will monitor %used on drives :
warn if %used > warn and critical if %used > crit
-v, --verbose
   print extra debugging information (and lists all storages)
-h, --help
   print this help message
-H, --hostname=HOST
   name or IP address of host to check
-C, --community=COMMUNITY NAME
   community name for the host's SNMP agent (implies SNMP v1)
-l, --login=LOGIN
   Login for snmpv3 authentication (implies v3 protocol with MD5)
-x, --passwd=PASSWD
   Password for snmpv3 authentication
-p, --port=PORT
   SNMP port (Default 161)
-m, --name=NAME
   Name in description OID (can be mounpoints '/home' or 'Swap Space'...)
   This is treated as a regexp : -m /var will match /var , /var/log, /opt/var ...
   Test it before, because there are known bugs (ex : trailling /)
   No trailing slash for mountpoints !
-a, --autolimit
   Use automated limit files in $Autolimitdir
-z, --zaplimits
   Ignore and replace any pre-defined limits in $Autolimitdir
-r, --noregexp
   Do not use regexp to match NAME in description OID
-i, --index
   Parse index table instead of description table to select storage
-e, --exclude
   Select all storages except the one(s) selected by -m
-T, --type=TYPE
   pl : calculate percent left
   pu : calculate percent used (Default)
-w, --warn=INTEGER
   percent of disk used to generate WARNING state
   you can add the % sign 
-c, --critical=INTEGER
   percent of disk used to generate CRITICAL state
   you can add the % sign 
-f, --perfparse
   Perfparse compatible output
-t, --timeout=INTEGER
   timeout for SNMP in seconds (Default: 5)
-V, --version
   prints version number
Note : 
  with T=pu : OK < warn < crit
  with T=pl : crit < warn < OK
  
  If multiple storage are selected, the worse condition will be returned
  i.e if one disk is critical, the return is critical
 
  example : 
  Browse storage list : <script> -C <community> -H <host> -m <anything> -w 1 -c 2 -v 
  the -m option allows regexp in perl format : 
  Test drive C,F,G,H,I on Windows 	: -m ^[CFGHI]:    
  Test all mounts containing /var      	: -m /var
  Test all mounts under /var      	: -m ^/var
  Test only /var                 	: -m /var -r
  Test all swap spaces			: -m ^Swap
  Test all but swap spaces		: -m ^Swap -e

EOT
}

sub verb { my $t=shift; print $t,"\n" if defined($o_verb) ; }

sub check_options {
    Getopt::Long::Configure ("bundling");
    GetOptions(
   	'v'	=> \$o_verb,		'verbose'	=> \$o_verb,
        'h'     => \$o_help,    	'help'        	=> \$o_help,
        'H:s'   => \$o_host,		'hostname:s'	=> \$o_host,
        'p:i'   => \$o_port,   		'port:i'	=> \$o_port,
        'C:s'   => \$o_community,	'community:s'	=> \$o_community,
        'l:s'   => \$o_login,           'login:s'       => \$o_login,
        'x:s'   => \$o_passwd,          'passwd:s'      => \$o_passwd,
        'c:s'   => \$o_crit,    	'critical:s'	=> \$o_crit,
        'w:s'   => \$o_warn,    	'warn:s'	=> \$o_warn,
 	't:i'   => \$o_timeout,       	'timeout:i'     => \$o_timeout,
        'm:s'   => \$o_descr,		'name:s'	=> \$o_descr,
	'T:s'	=> \$o_type,		'type:s'	=> \$o_type,
        'a'     => \$o_autolimit,       'autolimit'     => \$o_autolimit,
        'z'     => \$o_zaplimits,       'zaplimits'     => \$o_zaplimits,
        'r'     => \$o_noreg,           'noregexp'      => \$o_noreg,
        'i'     => \$o_index,          	'index'      	=> \$o_index,
        'e'     => \$o_negate,         	'exclude'    	=> \$o_negate,
        'V'     => \$o_version,         'version'       => \$o_version,
	'f'	=> \$o_perf,		'perfparse'	=> \$o_perf
    );
    if (defined($o_help) ) { help(); exit $ERRORS{"UNKNOWN"}};
    if (defined($o_version) ) { p_version(); exit $ERRORS{"UNKNOWN"}};
    # check snmp information
    if ( !defined($o_community) && (!defined($o_login) || !defined($o_passwd)) )
        { usage_error( "Missing snmp login info!" ); }
    # Check types
    if ( !defined($o_type) ) { $o_type="pu" ;}
    if ( ! grep( /^$o_type$/ ,@o_typeok) ) { usage_error( "Invalid type for -T" ); }
    # Check compulsory attributes
    if ( ! defined($o_host) )
	{ usage_error( "host name not specified" ); }
    if ( ! defined($o_descr) )
	{ usage_error( "-m option not specified" ); }
    if ( !defined($o_autolimit) && defined($o_zaplimits) )
	{ usage_error( "-z requires -a" ); }
    if ( defined($o_autolimit) && ( defined($o_warn) || defined($o_crit) ) )
	{ usage_error( "-a may not be used with -c or -w" ); }
    if ( !defined($o_autolimit) && ( !defined($o_warn) || !defined($o_crit) ) )
	{ usage_error( "limits not specified -- -a or -w and -c" ); }
    if ( defined($o_autolimit) && $o_type ne "pu" )
	{ usage_error( "-a may only (currently) be used with type 'pu'" ); }
    # Check for positive numbers
    if (($o_warn < 0) || ($o_crit < 0))
	{ usage_error( " warn and critical > 0" ); }
    # Get rid of % sign
    $o_warn =~ s/\%//; 
    $o_crit =~ s/\%//;
    # Check warning and critical values
    if ( $o_type eq 'pu' ) {
	if ($o_warn > $o_crit)
	    { usage_error( " warn must be <= crit if type=".$o_type ); }
    }
    if ( $o_type eq 'pl' ) {
	if ($o_warn < $o_crit)
	    { usage_error( " warn must be >= crit if type=".$o_type ); }
    }
    if ( ($o_warn < 0 ) || ($o_crit < 0 ))
	{ usage_error( "warn and crit must be >= 0" ); }
    if ( ( $o_type eq 'pl' ) || ( $o_type eq 'pu' )) {
        if ( ($o_warn > 100 ) || ($o_crit > 100 ))
	  { usage_error( "warn and crit percent must be <= 100" ); }
    } 
}


sub minimum {
  my ( $a, $b ) = @_;
  return( $a<$b ? $a : $b );
}


########## MAIN #######

check_options();

my $limitfile = "$Autolimitdir/${o_host}_${o_port}";
if ( $o_autolimit ) {
  # see if we have existing limits we can slurp in
  verb( "checking for $limitfile" );
  if ( -f $limitfile ) {
    verb( "need to read limits from $limitfile" );
    if ( open( IN, "<$limitfile" ) ) {
      my $line;
      while ( $line = <IN> ) {
	chomp( $line );
	# $line =~ /^(.*)!pu:(.*)!pu:(.*)$/;
	# my ( $key, $warn, $crit ) = ( $1, $2, $3 );
	if ( my ( $key, $warn, $crit ) = $line =~ /^(.*)!pu:(.*)!pu:(.*)$/ ) {
	verb( "key '$key' warn '$warn' crit '$crit'" );
	$wlimits{$key} = $warn;
	$climits{$key} = $crit;
	} else {
	  warn( "$Name:  $limitfile on $line" );
	}
      }
      close( IN );
    } else {
      warn( "Warning: could not read $limitfile: $!" );
    }
  } else {
    $writelimits = 1;
  }
}

# set timeout alarm
verb("timeout defined : $o_timeout + 10");
alarm ($o_timeout+10);

# Connect to host
my ($session,$error);
if ( defined($o_login) && defined($o_passwd)) {
  # SNMPv3 login
  verb("SNMPv3 login");
  ($session, $error) = Net::SNMP->session(
      -hostname         => $o_host,
      -version          => '3',
      -username         => $o_login,
      -authpassword     => $o_passwd,
      -authprotocol     => 'md5',
      -privpassword     => $o_passwd,
      -timeout   	=> $o_timeout
   );
} else {
  # SNMPV1 login
  ($session, $error) = Net::SNMP->session(
     -hostname  => $o_host,
     -community => $o_community,
     -port      => $o_port,
     -timeout   => $o_timeout
  );
}

if (!defined($session)) {
   printf("ERROR: %s.\n", $error);
   exit $ERRORS{"UNKNOWN"};
}

my $resultat=undef;
if (defined ($o_index)){
  $resultat = $session->get_table( 
	Baseoid => $index_table 
  );
} else {
  $resultat = $session->get_table( 
	Baseoid => $descr_table 
  );
}

if (!defined($resultat)) {
   printf("ERROR: Description/Index table : %s.\n", $session->error);
   $session->close;
   exit $ERRORS{"UNKNOWN"};
}


my @tindex = undef;
my @doids = undef;
my @ioids = undef;
my @descr = undef;
my $num_int = 0;
my $count_oid = 0;
my $test = undef;
my $perf_out=	undef;
# Select storage by regexp of exact match
# and put the oid to query in an array

verb("Filter : $o_descr");

foreach my $key ( keys %$resultat) {
   verb("OID : $key, Desc : $$resultat{$key}");
   # test by regexp or exact match / include or exclude
   if (defined($o_negate)) {
     $test = defined($o_noreg)
                ? $$resultat{$key} ne $o_descr
                : $$resultat{$key} !~ /$o_descr/;
   } else {
     $test = defined($o_noreg)
                ? $$resultat{$key} eq $o_descr
                : $$resultat{$key} =~ /$o_descr/;
   }  
  if ($test) {
     # get the index number of the interface
     my @oid_list = split (/\./,$key);
     $tindex[$num_int] = pop (@oid_list);
     if ( $test ) {	# i.e. we haven't excluded it as "abnormal"
       # get the full description
       $descr[$num_int]=$$resultat{$key};
       # put the oid in an array
       $doids[$count_oid]=$dpercent_table . $tindex[$num_int];
       $ioids[$count_oid]=$ipercent_table . $tindex[$num_int];
       $count_oid++;

       verb("Name : $descr[$num_int], Index : $tindex[$num_int]");
       $num_int++;
     }
  }
}
verb("storages selected : $num_int");
if ( $num_int == 0 ) { print "Unknown storage : $o_descr : ERROR\n" ; exit $ERRORS{"UNKNOWN"};}

my $dresult = $session->get_request(
   Varbindlist => \@doids
);

if (!defined($dresult)) { printf("ERROR: dSize table :%s.\n", $session->error); $session->close;
   my $i;
   for ($i=0;$i<$count_oid;$i++) {
     printf( "getting '%s'\n", $doids[$i] );
   }
   exit $ERRORS{"UNKNOWN"};
}

my $iresult = $session->get_request(
   Varbindlist => \@ioids
);

if (!defined($iresult)) { printf("ERROR: iSize table :%s.\n", $session->error); $session->close;
   my $i;
   for ($i=0;$i<$count_oid;$i++) {
     printf( "getting '%s'\n", $ioids[$i] );
   }
   exit $ERRORS{"UNKNOWN"};
}

$session->close;

# Only a few ms left...
alarm(0);

if ( $o_autolimit ) {
  my $i;
  # should we ignore and replace any existing limits?
  if ( $o_zaplimits ) {
    undef %climits;
    undef %wlimits;
  }
  # do we need to recalculate any limits?  e.g. Have any new filesystems?
  for ($i=0;$i<$num_int;$i++) {
    # We assume $o_type = pu here because I'm too lazy to put all the
    # code in to make it handle all the possible variations.  Though,
    # if we only record pu limits, we could always translate those numbers
    # into the current preferred type.  But again, I'm too lazy (for now).
    my $pu = 0;
    my $name = $descr[$i];
    $pu = $$dresult{$dpercent_table . $tindex[$i]};
    # if one is missing, recalculate both, so we are sure they are consistent
    if ( !defined( $wlimits{$name} )
      || !defined( $climits{$name} ) ) {
      verb( "need to set limits for $name, currently $pu" );
      # critical is 3/4's of the way from here to 100%
      $climits{$name} = minimum( 99, int( (300 + $pu) / 4 + 0.5 ) );
      # warning is halfway from here to 100%, and less than critical
      $wlimits{$name} = minimum( $climits{$name} - 1,
	minimum( 98, int( (100 + $pu) / 2 + 0.5 ) ) );
      $writelimits = 1;
    }
  }
  if ( $writelimits ) {
    verb( "need to write out new $limitfile" );
    if ( ! -d $Autolimitdir ) {
	mkdir( $Autolimitdir );
    }
    if ( open( OUT, ">$limitfile" ) ) {
      foreach my $key ( keys %wlimits ) {
	# we hope we never have a device with ! in its name
	printf OUT "%s!pu:%d!pu:%d\n", $key, $wlimits{$key}, $climits{$key};
      }
      close( OUT );
    } else {
      warn( "Warning: could not write $limitfile: $!" );
    }
  }
}


my $i=undef;
# we store the messages away, so we can print criticals first, so they
# show up at the start of the message (especially for a pager message)
my @okmsgs = ();
my @critmsgs = ();
my @warnmsgs = ();
my $warn_state=0;
my $crit_state=0;
my ($msg);
for ($i=0;$i<$num_int;$i++) {
  # if we have per-target limits use them - if -a, this should be true
  my $crit = defined($climits{$descr[$i]})
    ? $climits{$descr[$i]} : $o_crit;
  my $warn = defined($wlimits{$descr[$i]})
    ? $wlimits{$descr[$i]} : $o_warn;
  verb("dpercent :  $$dresult{$dpercent_table . $tindex[$i]}");
  verb("ipercent : $$iresult{$ipercent_table . $tindex[$i]}");
  my $pu=undef;
  $pu = $$dresult{$dpercent_table . $tindex[$i]};
  my $pl = 100 - $pu;
  # add a ',' if some data exists in $perf_out
  $perf_out .= " " if (defined ($perf_out)) ;
  ##### Ouputs 
  ##### TODO : subs "," with something
  if ($o_type eq "pu") {
    $msg = sprintf("%s: %.0f %%used",$descr[$i],$pu);
    if ($pu >= $crit) {
      $crit_state=1;
      push( @critmsgs, $msg . " >$crit" );
    } elsif ($pu >= $warn) {
      $warn_state=1;
      push( @warnmsgs, $msg . " >$warn" );
    } else {
      push( @okmsgs, $msg . " <$warn" );
    }
  }
  
  if ($o_type eq 'pl') {
    $msg=sprintf ("%s: %.0f %%left",$descr[$i],$pl);
    if ($pl <= $crit) {
      $crit_state=1;
      push( @critmsgs, $msg . " <$crit" );
    } elsif ($pl <= $warn) {
      $warn_state=1;
      push( @warnmsgs, $msg . " <$warn" );
    } else {
      push( @okmsgs, $msg . " >$warn" );
    }
  }
  $descr[$i] =~ s/'/_/g; 
  $perf_out .= "'".$descr[$i] . "'="
      . $$dresult{$dpercent_table . $tindex[$i]} . ";"
      . round($warn,0) . ";" . round($crit,0)
      . "0;100";		# min; max
}
if ( @critmsgs > 0 ) {
  print( "CRITICAL: " );
  while ( @critmsgs > 0 ) {
    print( shift( @critmsgs ) . "; " );
  }
}
if ( @warnmsgs > 0 ) {
  print( "WARNING: " );
  while ( @warnmsgs > 0 ) {
    print( shift( @warnmsgs ) . "; " );
  }
}
if ( @okmsgs > 0 ) {
  print( "OK: " );
  while ( @okmsgs > 0 ) {
    print( shift( @okmsgs ) . "; " );
  }
}

verb ("Perf data : $perf_out");

if ( defined($o_perf) ) {
  print( " | $perf_out" );
}
print( "\n" );

if ( $crit_state == 1) {
     exit $ERRORS{"CRITICAL"};
    }
if ( $warn_state == 1) {
     exit $ERRORS{"WARNING"};
   }
exit $ERRORS{"OK"};

