#!/usr/local/bin/perl -w
#
# check_remote_interfaces
#
# by ATonns Mon Sep  9 14:15:23 EDT 2002
#
# monitor the interfaces OID in net-snmpd
#
# $Id: check_remote_interfaces,v 1.13 2003/09/15 17:39:16 atonns Exp atonns $
#
# check_remote_interfaces - monitor the interfaces OID in net-snmpd
# Copyright (C) 2003 - iVillage.com, Anthony Tonns
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# 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.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

# perl setup
use strict;
use Getopt::Long;
use Net::SNMP;
use RRDs;

use lib "/usr/local/nagios/libexec";
use utils qw($TIMEOUT %ERRORS &print_revision &support);
delete @ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

# where to store all RRD files
my $rrd_dir = "/www/apps/nagios/data/ifErrors";

# static variables
my $PROGNAME = "check_remote_interfaces";
my $version = '$Revision: 1.13 $';

# OIDs for the net-snmpd host resources mib
#
# iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable.ifEntry 
my $baseoid = "1.3.6.1.2.1.2.2.1";
my $baselen = length($baseoid) + 1;
# ifDescr
my $descrkey = "2";

# ifAdminStatus
my $adminstatuskey = "7";
# ifAdminStatus linkage from /usr/local/share/snmp/mibs/IF-MIB.txt
my %adminstatuslinkage = (
	1 => "up",
	2 => "down",
	3 => "testing",
);

# ifOperStatus
my $operstatuskey = "8";
# ifOperStatus linkage from /usr/local/share/snmp/mibs/IF-MIB.txt
my %operstatuslinkage = (
	1 => "up",
	2 => "down",
	3 => "testing",
	4 => "unknown",
	5 => "dormant",
	6 => "notPresent",
	7 => "lowerLayerDown",
);

# ifInErrors
my $inerrorskey = "14";
# ifOutErrors
my $outerrorskey = "20";

# skip these interfaces
my %skipif = (
	'lo0' => 1,
);

# auth config stuff
my $username = "XxXxXxXxXxXx";
my $authpass = "XxXxXxXxXxXx";
my $privpass = "XxXxXxXxXxXx";
my $community = "XxXxXxXxXxXx";

################################################################################

# parse args
my ($opt_V,$opt_h,$opt_H,$opt_i,$opt_v,$opt_w,$opt_c);
Getopt::Long::Configure('bundling');
GetOptions(
	"V"   => \$opt_V, "version"    => \$opt_V,
	"h"   => \$opt_h, "help"       => \$opt_h,
	"v+"  => \$opt_v, "verbose+"   => \$opt_v,
	"H=s" => \$opt_H, "hostname=s" => \$opt_H,
	"i=s" => \$opt_i, "interval=s" => \$opt_i,
	"c=s" => \$opt_c, "critical=s" => \$opt_c,
	"w=s" => \$opt_w, "warning=s"  => \$opt_w,
);

# check args
if ( $opt_h )
	{ print_usage(""); }
if ( $opt_V )
	{ print_revision($PROGNAME,$version); clean_exit($ERRORS{OK}); }
if ( ! $opt_H )
	{ print_usage("$PROGNAME: must specify hostname with -H option."); }

my $hostname = $opt_H;
#
my $interval = $1 if ($opt_i =~ /([0-9]+)/);
($interval) || print_usage("$PROGNAME: Invalid interval: $opt_i");
#
my ($w_count,$w_time) = split(',',$opt_w,2);
my ($c_rate,$c_time)  = split(',',$opt_c,2);
map {
	my $input = $_;
	my $output = $1 if ($input =~ /([0-9]+)/);
	($output) || print_usage("$PROGNAME: Invalid threshold: '$input' ");
} ($w_count,$w_time,$c_rate,$c_time);

my $warning_count = $w_count;
my $warning_time  = $w_time;
my $critical_rate = $c_rate;
my $critical_time = $c_time;

# set a timeout w/error message
$SIG{'ALRM'} = sub {
        print STDOUT ("$PROGNAME: ERROR: alarm timeout\n");
        clean_exit($ERRORS{UNKNOWN});
};
alarm($TIMEOUT);

# establish a session
my ($session,$error) = Net::SNMP->session(
	-hostname =>		$hostname,
	-version =>		"3",
	-username =>		$username,
	-authprotocol =>	"md5",
	-authpassword =>	$authpass,
	-privpassword =>	$privpass,
	-maxmsgsize =>		1048576,
	-timeout =>		$TIMEOUT,
	-retries =>		3,
);
if ( $error ) {
	print STDOUT "$PROGNAME: session error: $error\n";
	clean_exit($ERRORS{UNKNOWN});
}

# retreive the entire ifTable
my $result = $session->get_table( -baseoid => $baseoid, );
if ( $session->error ) {
	print STDOUT "$PROGNAME: get_table error: ".$session->error."\n";
	$session->close;
	clean_exit($ERRORS{UNKNOWN});
}
$session->close;

my %map;	# from ifDescr to oid instance
my %data;	# data table re-hashed by ifDescr

# create a map based on the final oid for the column with $descrkey
foreach (sort keys %{$result}) {
	my $key = $_;
	my ($column,$instance) = split(/\./, substr($key,$baselen));
	if ( $column eq $descrkey ) {
		$map{$instance} = ${$result}{$key};
	}
}

# NOW reorder all the data in the table based on what was $descrkey's value
foreach (sort keys %{$result}) {
	my $key = $_;
	my ($column,$instance) = split(/\./, substr($key,$baselen));
	if ( $column ne $descrkey ) {
		$data{$map{$instance}}->{$column} = ${$result}{$key};
	}
}

## DEBUG
## let's see it in order now
#foreach (sort keys %data) {
#	my $key = $_;
#	print STDERR "$key ";
#	print STDERR "	" if length($key) < 8;
#	foreach (sort keys %{$data{$key}}) {
#		my $subkey = $_;
#		my $val = $data{$key}->{$subkey};
#		print STDERR " $subkey:$val, ";
#	}
#	print STDERR "\n";
#}

# since we've checked all the sanity beforehand,
# start off assuming all is well
my $state = $ERRORS{OK};

# BUT just in case we'll declare a variable
my @badinterfaces;

# check all the interfaces
INT: foreach (sort keys %data) {
	my $ifDescr = $_;
	if ( exists($skipif{$ifDescr}) ) {
		next INT;
	}
	my $mystate = $ERRORS{OK};

	# get the current data
	my $ifAdminStatus = $data{$ifDescr}->{$adminstatuskey};
	my $ifOperStatus  = $data{$ifDescr}->{$operstatuskey};
	my $ifInErrors    = $data{$ifDescr}->{$inerrorskey};
	my $ifOutErrors   = $data{$ifDescr}->{$outerrorskey};
# DEBUG
#	print STDERR "$ifDescr: $ifAdminStatus - $ifOperStatus - $ifInErrors - $ifOutErrors\n";

	# if it's not OK, signal critical
	if ( $ifOperStatus != $ifAdminStatus ) {
			$mystate = $ERRORS{CRITICAL};
	}

	my ($warnstart,$warnstep,$warnnames,$warndata,
		$critstart,$critstep,$critnames,$critdata) =
			get_data($hostname,$ifDescr,$ifInErrors,$ifOutErrors);

# DEBUG
#	use Dumpvalue;
#	my $dumper = new Dumpvalue;
#	$dumper->set(globPrint => 1);

#
# Example code from RRDs perldoc
#
#  my ($start,$step,$names,$data) = RRDs::fetch ... 
#  print "Start:       ", scalar localtime($start), " ($start)\n";
#  print "Step size:   $step seconds\n";
#  print "DS names:    ", join (", ", @$names)."\n";
#  print "Data points: ", $#$data + 1, "\n";
#  print "Data:\n";
#  foreach my $line (@$data) {
#    print "  ", scalar localtime($start), " ($start) ";
#    $start += $step;
#    foreach my $val (@$line) {
#      printf "%12.1f ", $val;
#    }
#    print "\n";
#  }

# DEBUG
#	print STDERR "DS names:    ", join (", ", @$warnnames)."\n" if $ifDescr eq "hme0";
#	print STDERR "warning data: " if $ifDescr eq "hme0";
#	$dumper->dumpValue($warndata) if $ifDescr eq "hme0";

	# warning is based on pure count
	my $warnifInErrors  = 0;
	my $warnifOutErrors = 0;
	foreach (@$warndata) {
		my $line = $_;
# DEBUG
#		print STDERR "time:  ". scalar localtime($warnstart). " ($warnstart) \n" if $ifDescr eq "hme0";
#		$warnstart += $warnstep;

		if ( ${$line}[0] ) {
				my $in = ${$line}[0] * $warnstep;
# DEBUG
#				print STDERR "in: $in\n" if $ifDescr eq "hme0";
				$warnifInErrors  += $in;
		}
		if ( ${$line}[1] ) {
				my $out = ${$line}[1] * $warnstep;
# DEBUG
#				print STDERR "out: $out\n"if $ifDescr eq "hme0";
				$warnifOutErrors  += $out;
		}
	}

	# if any errors, signal warning
	if ( $warnifInErrors > $warning_count || $warnifOutErrors > $warning_count ) {
			$mystate = $ERRORS{WARNING} if $mystate == $ERRORS{OK};
	}

# DEBUG
#	print STDERR "DS names:    ", join (", ", @$critnames)."\n" if $ifDescr eq "hme0";
#	print STDERR "critical data: " if $ifDescr eq "hme0";
#	$dumper->dumpValue($critdata) if $ifDescr eq "hme0";

	my $critifInErrors_cnt  = 0;
	my $critifOutErrors_cnt = 0;
	foreach (@$critdata) {
		my $line = $_;
# DEBUG
#		print STDERR "time:  ". scalar localtime($critstart). " ($critstart) \n" if $ifDescr eq "hme0";
#		$critstart += $critstep;

		if ( ${$line}[0] ) {
				my $in = ${$line}[0];
				$in *= $critstep;
# DEBUG
#				print STDERR "in: $in\n" if $ifDescr eq "hme0";
				$critifInErrors_cnt  += $in;
		}
		if ( ${$line}[1] ) {
				my $out = ${$line}[1];
				$out *= $critstep;
# DEBUG
#				print STDERR "out: $out\n" if $ifDescr eq "hme0";
				$critifOutErrors_cnt  += $out;
		}
	}
	# express in errors per minute
	my $critifInErrors  = $critifInErrors_cnt  / $critical_time;
	my $critifOutErrors = $critifOutErrors_cnt / $critical_time;

	# if any errors, signal warning
	if ( $critifInErrors > $critical_rate || $critifOutErrors > $critical_rate ) {
			$mystate = $ERRORS{CRITICAL};
	}

# DEBUG
#	print STDERR "interface => $ifDescr, adminstatus => $ifAdminStatus, operstatus => $ifOperStatus, inerrors => $warnifInErrors, outerrors => $warnifOutErrors, inerrorsrate => $critifInErrors, outerrorsrate => $critifOutErrors\n";
	if ( $mystate != $ERRORS{OK} ) {
		push(	@badinterfaces,
			{	interface => $ifDescr,
				adminstatus => $ifAdminStatus,
				operstatus => $ifOperStatus,
				inerrors => $warnifInErrors,
				outerrors => $warnifOutErrors,
				inerrorsrate => $critifInErrors,
				outerrorsrate => $critifOutErrors,
			}
		);
		# if it's OK, it can be changed to show error
		if ( $state == $ERRORS{OK} ) {
			$state = $mystate;
		}
		# if it's WARNING, it can be escalated to CRITICAL
		elsif ( $state == $ERRORS{WARNING} && $mystate == $ERRORS{CRITICAL} ) {
			$state = $mystate;
		}
	}
}

# print text for the humans
my $statetxt;
foreach (keys(%ERRORS)) {
	my $key = $_;
	$statetxt=$key if ( $state == $ERRORS{$key} );
}

# the almighty output
print STDOUT "Interfaces $statetxt";
if ( $state != $ERRORS{OK} ) {
	print STDOUT " - ";
	while($#badinterfaces != -1) {
		my $int = shift(@badinterfaces);
		print STDOUT "$$int{interface}: " .
			"AdminStatus: ". $adminstatuslinkage{${$int}{adminstatus}} . ", " .
			"OperStatus: ".  $operstatuslinkage{${$int}{operstatus}} . ", " .
			"InErrCnt: ".    sprintf("%4.2f",${$int}{inerrors}) . ", " .
			"OutErrCnt: ".   sprintf("%4.2f",${$int}{outerrors}) . ", ",
			"InErrRate: ".   sprintf("%4.2f",${$int}{inerrorsrate}) . ", " .
			"OutErrRate: ".  sprintf("%4.2f",${$int}{outerrorsrate}) . ", ";
	}
}
print STDOUT "\n";
clean_exit($state);

################################################################################

# how does this work again?
sub print_usage {
	my ($msg) = @_;
	if ( $msg ) { print STDOUT "$msg\n\n"; }
	print_revision($PROGNAME,$version);
	print STDOUT "Usage: $PROGNAME -H hostname " .
					" -i seconds -w count,seconds -c rate,minutes\n";
	print STDOUT "Usage: $PROGNAME --hostname=hostname " .
					" --interval=seconds --warning=count,seconds --critical=rate,minutes\n";
	print STDOUT "       ".' ' x length($PROGNAME) .
				     " [-v|--verbose -V|--version -h|--help]\n";
	clean_exit($ERRORS{UNKNOWN});
}

sub clean_exit {
	my($state)=@_;
	alarm(0);
	exit($state);
} 

sub get_data {
	my ($hostname,$ifDescr,$ifInErrors,$ifOutErrors) = @_;
	my $ERR;

	my $filename = "$rrd_dir/$hostname-$ifDescr.rrd";
	if ( ! -f $filename ) {
		my $oneday = 60 * 60 * 24;
		my $howmanyperday = $oneday / $interval;
		RRDs::create ( $filename,
			"--step", "$interval",
			"DS:ifInErrors:DERIVE:$interval:0:U",
			"DS:ifOutErrors:DERIVE:$interval:0:U",
			"RRA:AVERAGE:0.5:1:$howmanyperday",
			"RRA:AVERAGE:0.5:1:$howmanyperday",
		);
		$ERR=RRDs::error;
			die "ERROR while creating $filename: $ERR\n" if $ERR;
	}

	my $now = time;
	my $rrd_data = "$now:$ifInErrors:$ifOutErrors";
# DEBUG
	RRDs::update ( $filename, $rrd_data );
	$ERR=RRDs::error;
		die "ERROR while updating $filename: $ERR\n" if $ERR;

	# $warning_time is in minutes
	my $warning_start  = $now - $warning_time;
	# $critical_time is in minutes
	my $critical_start = $now - ($critical_time*60);

	my ($warnstart,$warnstep,$warnnames,$warndata) =  RRDs::fetch ( $filename, "AVERAGE",
		"--start", $warning_start,
	);
	$ERR=RRDs::error;
		die "ERROR while fetching from $filename: $ERR\n" if $ERR;

	my ($critstart,$critstep,$critnames,$critdata) =  RRDs::fetch ( $filename, "AVERAGE",
		"--start", $critical_start,
	);
	$ERR=RRDs::error;
		die "ERROR while fetching from $filename: $ERR\n" if $ERR;

	return ($warnstart,$warnstep,$warnnames,$warndata,$critstart,$critstep,$critnames,$critdata)
}
