#!/usr/bin/perl
#
# Monitor external programs via SNMP
# (based on netsnmp-freespace.monitor)
#
# Modified Feb 2002 by Dan Urist <durist@world.std.com>
# Changes: added -C config file option; cleaned up code
#
# This script monitors one or more external programs run by the UCD
# SNMP agent. Specific programs to monitor can be specified with the
# "-N" option; these are zero-indexed in the order they appear in the
# monitored host's snmpd.conf file. Default is to monitor all.
#
# The summary output line will be of the form "host:name[,host:name]"
# where "name" is the name of the failing program (the "extNames"
# field as defined in snmpd.conf; not the path to the program). The
# detail lines will contain full error text from the failing program
# and the error value it returned.
#
# The script will exit with 0 for success, 1 for an extNames program 
# failure and 2 for an SNMP error.
#
# BUGS AND LIMITATIONS: This is designed to handle programs that only
# return one line of output via snmpd; that is, with simple programs
# run via the "sh" or "exec" directives in the snmpd.conf file and NOT
# with programs run by "exec" and returning data in their own MIB
# tables. Actually, I've only gotten the "sh" directive to work with
# ucd-snmp-4.2.1 under Solaris. Also note that when given an external
# program number that doesn't exist on the monitored host, the script
# will return the output for program number 0 and will not report an
# error (see FIXME note below). In some situations (e.g. sending v. 1
# request to a host configured only to respond to v. 3) the script will
# fail silently, because the SNMP module doesn't report an error.
#
#
#    Copyright (C) 2001 Daniel J. Urist <durist@world.std.com>
#
#    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
#
use SNMP;
use Getopt::Std;

$ENV{'MIBS'} = "UCD-SNMP-MIB";

getopts("hN:" . &SNMPconfig("getopts"));

my $VERSION = "0.3";
if( $opt_h || (scalar @ARGV == 0) ){
  print join("\n",
	     "$0 Version $VERSION by Daniel J. Urist <durist\@world.std.com>",
	     "\n",
	     );
  print "Usage: $0 OPTIONS host [host ...]\n";
  print "Options:\n";

  print join("\n\t",
	     "\t-h                    # Usage",
	     "[-N num[,num...]]]    # Program numbers to look for",
	     &SNMPconfig("usage"), "\n");
  exit 2;
}

# Get SNMP options
my %SNMPARGS = &SNMPconfig;

my @Extprognums = split(",", $opt_N) if defined $opt_N;
my $RETVAL = 0;
my %Failures;
my %Longerr;
my $Session;

foreach $host (@ARGV) {
  $Session = new SNMP::Session(
			       DestHost => $host,
			       %SNMPARGS,
			      );
  unless( defined($Session) ) {
    $RETVAL = ($RETVAL == 1) ? 1 : 2;
    $Failures{"$host session error"} = "";
    $Longerr{"$host could not get SNMP session"} = "";
    next;
  }

  my $ext;
  my $v;
  my @q;
  # We are monitoring specific programs

  # FIXME If $ext is out of range, i.e. for example, if $ext is 2 and
  # there are only programs numbered 0 and 1, the returned value is
  # for program number 0. It seems to me we should get an SNMP error
  # back, but we don't; I suspect this is a bug in the SNMP module
  # since passing a bogus index to snmpwalk gives a "No Such Instance"
  # error. Unfortunately I also can't find a way to retrieve the index
  # number from the returned data, so I have nothing with which to
  # compare it and flag the error.

  if( defined(@Extprognums) ){
    foreach $ext (@Extprognums){
      $v = new SNMP::Varbind (["extIndex", $ext]);
      $Session->getnext($v);
      @q = $Session->get ([
			      ["extNames", $v->iid],	# 0
			      ["extCommand", $v->iid],  # 1
			      ["extResult", $v->iid],	# 2
			      ["extOutput", $v->iid],	# 3
			      ["extErrFix", $v->iid],	# 4
			     ]);

      if($q[2] != 0){
	$RETVAL = 1;
	$Failures{$host .":" . $q[0]} = "";
	$Longerr{$host .":" . $q[0] . " exited with code: " . $q[2] . ", error: \"" . $q[3] . "\""} = "";
      }
      if ($Session->{"ErrorStr"}) {
	$RETVAL = ($RETVAL == 1) ? 1 : 2;
	$Failures{$host} = "";
	$Longerr{"$host returned an SNMP error: " . $Session->{"ErrorStr"}} = "";
	last;
      }
    }
  }
  # We are monitoring all programs
  else{
    $v = new SNMP::Varbind (["extIndex"]);
    $Session->getnext($v);
    while (!$Session->{"ErrorStr"} && $v->tag eq "extIndex") {
      @q = $Session->get ([
			      ["extNames", $v->iid],	# 0
			      ["extCommand", $v->iid],  # 1
			      ["extResult", $v->iid],	# 2
			      ["extOutput", $v->iid],	# 3
			      ["extErrFix", $v->iid],	# 4
			     ]);

      if($q[2] != 0){
	$RETVAL = 1;
	$Failures{$host .":" . $q[0]} = "";
	$Longerr{$host .":" . $q[0] . " exited with code: " . $q[2] . ", error: \"" . $q[3] . "\""} = "";
      }
      if ($Session->{"ErrorStr"}) {
	$RETVAL = ($RETVAL == 1) ? 1 : 2;
	$Failures{$host} = "";
	$Longerr{"$host returned an SNMP error: " . $Session->{"ErrorStr"}} = "";
	last;
      }
      $Session->getnext($v);
    }
  }
}

if (scalar keys %Failures) {
    print join (", ", sort keys %Failures), "\n", "\n";
    print join ("\n", sort keys %Longerr), "\n";
}

exit $RETVAL;


#
# Manage the standard SNMP options
# Arguments are same as netsnmp utils
#
# If called with "getopts", returns a string for "getopts"
# If called with "usage", returns an array of usage information
# Otherwise, returns a hash of SNMP config vars
#
# Overloading this sub like this is kinda hoakey,
# but keeps everything in one place
sub SNMPconfig {
  my($action) = @_;

  if($action eq "getopts"){
    return "C:t:r:p:v:u:l:A:e:E:n:a:x:X:";
  }
  elsif($action eq "usage"){
    return(
	   "[-C configfile]       # SNMP vars config file",
	   "[-t Timeout]          # Timeout in ms (default: 1000000)",
	   "[-r Retries]          # Retries before failure (default: 5)",
	   "[-p RemotePort]       # Remote UDP port (default 161)",
	   "[-v Version]          # 1,2,2c or 3 (default: 1)",
	   "[-c Community]        # v.1,2,2c Community Name (default: public)",
	   "[-u SecName]          # v.3 Security Name (default: initial)",
	   "[-l SecLevel]         # v.3 Security Level (default: noAuthNoPriv)",
	   "[-A AuthPass]         # v.3 Authentication Passphrase (default: none)",
	   "[-e SecEngineId]      # v.3 security engineID (default: none)",
	   "[-E ContextEngineId]  # v.3 context engineID (default: none)",
	   "[-n Context]          # v.3 context name (default: none)",
	   "[-a AuthProto]        # authentication protocol (MD5|SHA; default MD5)",
	   "[-x PrivProto]        # privacy protocol (DES)",
	   "[-X PrivPass]         # privacy passphrase (default: none)",
	  );
  }

  # Read config file
  my %Conf;
  if($opt_C){
    unless( open(CONF, $opt_C) ){
      print "$0: Could not open config file $opt_C\n";
      exit 2;
    }
    my $line;
    my @fields;
    foreach $line (<CONF>){
      chomp $line;
      @fields = split(/=/, $line);
      $Conf{ lc $fields[0] } = $fields[1];
    }
    close CONF;
  }

  my %SNMPARGS;

  # Common options
  $SNMPARGS{Timeout} = $opt_t || $Conf{timeout} || 1000000;
  $SNMPARGS{Retries} = $opt_r || $Conf{retries} || 5;
  $SNMPARGS{RemotePort} = $opt_p || $Conf{remoteport} || 161;
  $SNMPARGS{Version} = $opt_v || $Conf{version} || 1;

  # v. 3 options
  if ($SNMPARGS{Version} eq "3"){
    $SNMPARGS{SecName} = $opt_u || $Conf{secname} || 'initial';
    $SNMPARGS{SecLevel} = $opt_l || $Conf{seclevel} || 'noAuthNoPriv';
    $SNMPARGS{AuthPass} = $opt_A || $Conf{authpass} || '';
    $SNMPARGS{SecEngineId} = $opt_e || $Conf{secengineid} || '';
    $SNMPARGS{ContextEngineId} = $opt_E || $Conf{contextengineid} || '';
    $SNMPARGS{Context} = $opt_n || $Conf{context} || '';
    $SNMPARGS{AuthProto} = $opt_a || $Conf{authproto} || '';
    $SNMPARGS{PrivProto} = $opt_x || $Conf{privproto} || '';
    $SNMPARGS{PrivPass} = $opt_X || $Conf{privpass} || '';
  }
  # v. 1,2 options
  else{
    $SNMPARGS{Community} = $opt_c || $Conf{community} || 'public';
  }

  return %SNMPARGS;
}
