#!/usr/pkg/bin/perl
# $Header: /home/vikas/src/nocol/perlnocol/RCS/snmpmon-client,v 1.7 1999/11/09 04:28:43 vikas Exp $
#
#	snmpmon-client - perl client for NOCOL to monitor agents via SNMP
#
# SEE 'snmpgeneric' for newer SNMP monitor.
#
# AUTHOR:   Vikas Aggarwal, vikas@naya.com
#
#	Copyright 1994 Vikas Aggarwal, vikas@navya.com
#
######
# Program to poll SNMP variables from various devices. Needs a config file
# of the sort:
#	# DEVICE   cid   type [ type type ... ]
#
#	ctron-res	public  rmon
#	cs500		public  cisco_ts
#	trillian	public  cisco_router rmon
#
# This program gathers data from remote SNMP devices using the CMU snmpwalk
# program. Each device is assigned to one or more categories, and a
# pre-defined set of data is gathered for each category. The output is
# then processed by the 'snmpmon' program for NOCOL output.
#
###
# VARIABLES USED:
#
#	@devicelist	list of devices to monitor
#	@vars{$type}	list of SNMP variables for each type
#	@devvar{$device}	list of variables to monitor for each device
#	@devvarval{$dev, $var}
#			matrix of values of each device & variable.
#			Each point in this matrix is a string (list) that
#			stores the data for all interfaces for the device.
#
#
# Customization:
#
#       Edit '$ping' and its usage for properly 'pinging' a site in main()
#	Also fix the location of the CMU snmpwalk program and the location
#	of the MIB file (both are supplied with the nocol sources).

$nocolroot =  "/usr/pkg"   unless $nocolroot;	# SET_THIS
$etcdir  = "$nocolroot/etc"  unless $etcdir;	# location of config file
$bindir  = "$nocolroot/bin"  unless $bindir;
$ping =  "/sbin/ping" ;				# SET_THIS to ping location

$prog = "$bindir/snmpwalk" ;	        # location of snmpwalk SET_THIS
#$mibfile = "$etcdir/mibII.txt" ;	# location of MIB file SET_THIS
$mibfile = "$etcdir/mib-v2.txt" ;	# location of MIB file SET_THIS

$datadir = "/tmp/snmpmon_data" ; # the snmpmon server expects datafiles here
$dfile = `hostname`; chop $dfile ;
$dfile = "$datadir" . "/" . $dfile . ".snmpmon" ;

if (!$ping) {$ping = `which ping` ; chop $ping; }

$MIB2 = ".iso.org.dod.internet.mgmt.mib-2" ; 
#$MIB2 = ".1.3.6.1.2.1" ;
$MIBENTERPRISE = ".iso.org.dod.internet.private.enterprises" ;
#$MIBENTERPRISE = ".1.3.6.1.4.1"
$MIBRMON = "$MIB2.rmon" ;
#$MIBRMON = "$MIB2.16" ;
$MIBCISCO = "$MIBENTERPRISE.Cisco" ;	# or .9
$MIBCTRON = "$MIBENTERPRISE.cabletron" ;

$debug = 0;				# set to 1 for debugging output
$libdebug = 0;				# set to 1 for debugging output
$prognm = $0;				# save program name
$sleeptime = 10 ;			# in seconds

$cfile = "$prognm" . "-confg" ;
$cfile =~ s,^.*/,, ;		# get basename (strip directory path)
$cfile = "$etcdir" . "/$cfile" ;

#########
-d $datadir || mkdir($datadir, 0700) || die("Cannot create $datadir");
-w $datadir || die("Cannot write to $datadir");
-x $ping || die("Cannot find $ping");
-x $prog || die("Could not find executable $prog, exiting");
-f $mibfile || die("Could not find MIB file $rprog, exiting");
$ENV{"MIBFILE"}= $mibfile ;

@devtypelist = ("system", "rmon", "router", "cisco_router", "cisco_ts") ;

#####  init_$type  routines
sub init_system {
    local ($idx) = "system" ;
    local ($prefix) = "$MIB2.system";
    local (@lvars) = (sysDescr, sysUpTime);

    $debug && print STDERR "(debug) Running init_system\n";
    foreach $i (@lvars) { $vars{$idx} .= "$prefix.$i " };

}				# end init_system()

sub init_rmon {
    local ($idx) = "rmon" ;
    local ($prefix) = "$MIBRMON.statistics.etherStatsTable.etherStatsEntry";
    local (@lvars) = (etherStatsOctets, etherStatsPkts,
		      etherStatsCRCAlignErrors,
		      etherStatsUndersizePkts, etherStatsOversizePkts,
		      etherStatsCollisions) ;

    $debug && print STDERR "(debug) Running init_rmon\n";
    foreach $i (@lvars) { $vars{$idx} .= "$prefix.$i " };

}				# end init_rmon()


sub init_router {
    local ($idx) = "router" ;
    local ($prefix) = "$MIB2.interfaces.ifTable.ifEntry" ;
    local (@lvars) = (ifDescr , ifSpeed, ifOutQLen,
		      ifInOctets,  ifInUcastPkts, ifOutOctets, ifOutUcastPkts,
		      ifInErrors, ifOutErrors);

    $debug && print STDERR "(debug) Running init_router\n";
    foreach $i (@lvars) { $vars{$idx} .= "$prefix.$i " };
}

sub init_cisco_router {
    local ($idx) = "cisco_router" ;
    local ($prefix) = "$MIBCISCO.local.linterfaces.lifTable.lifEntry";
    local (@lvars) = (locIfCarTrans, locIfCollisions, locIfInAbort,
		      locIfInCRC, locIfInRunts, locIfResets, locIfReliab);

    $debug && print STDERR "(debug) Running init_cisco_router\n";
    foreach $i (@lvars) { $vars{$idx} .= "$prefix.$i " };
}

sub init_cisco_ts {
    local ($idx) = "cisco_ts" ;
    local ($prefix) = "$MIBCISCO.local.lts.ltsLineTable.ltsLineEntry";
    local (@lvars) = (tsLineActive, tsLineType);

    $debug && print STDERR "(debug) Running init_cisco_ts\n";
    foreach $i (@lvars) { $vars{$idx} .= "$prefix.$i " };
}

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

#####
#####  procdata_$type
#####

##
# Print out values of monitored system variables...
sub procdata_system {
    local ($dev) = @_ ;
    local ($prevtime, $var);
    local ($d) = &toindex($dev);
    local ($dtime) = $testtime - $prevtime{$d};

    $debug && 
	print STDERR "(debug) Running procdata_system for device $dev\n";
    foreach $var ( split(/[ ,\t\n]+/, $vars{"system"}) ) {
	local ($v) = &toindex($var) ;
	local ($varalias) = ( $var =~ /^.*(\.[^.]+)$/ ) ;
	$varalias = "SYS" . $varalias ;
	print "VARIABLE $varalias VALUE\n";
	if ($var =~ /sysUpTime/) { #  (844388487) 97 days, 17:31:24
	    local ($upsecs) = ( $nvarval{$v} =~ /^\s*\((\d+)\)\s*/ );
	    printf " %d\n", int($upsecs / 100) ; # convert TICKS into secs
	}
	else {
	    foreach (split(/[\t\n]/, $nvarval{$v})) {
		print " $_\n" ;
	    }
	}
	$devvar_val{$d, $v} = "$nvarval{$v}" ; # store new values
    }
}

##
# Monitor collisions, etc per sec.

sub procdata_rmon {
    local ($dev) = @_ ;
    local ($prevtime, $var, $i);
    local ($d) = &toindex($dev); # strip off hyphens,  etc.
    local ($dtime) = $testtime - $prevtime{$d};

    $debug &&
	print STDERR "(debug) Running procdata_rmon for device $dev\n";
    foreach $var ( split(/[ ,\t\n]+/, $vars{"rmon"}) ) {
	local ($v) = &toindex($var) ;

	local ($size, $deltaval) =
	    &vector_calc($nvarval{$v} , "-" , $devvar_val{$d,$v} );

	if ($size != 0)
	{
	    local ($junk, $deltarate) =
		&vector_calc($deltaval, "/", $dtime);
	    
	    # shorten the prefix
	    local ($varalias) = ( $var =~ /^.*(\.[^.]+)$/ ) ;
	    $varalias = "RMON" . $varalias ;
	    print "VARIABLE $varalias RATE-sec\n" ;
	    local ($i) = 0;
	    foreach (split(/[\t\n]/, $deltarate)) {
		++$i; 
		print " $_ COMMENT Net_$i\n" ;
	    }
	    if ($var =~ /Octets/) # extract bandwidth utilized
	    {
		$i = 0;
		$varalias =~ s/Octets/BW/ ;
		print "VARIABLE $varalias VALUE\n" ;
		foreach (split(/[\t\n]/, $deltarate)) {
		    ++$i;
		    printf " %d COMMENT Net_$i\n",
		    int(($_ * 8 * 100) / 10000000); # percentage Mbits per sec
		}
	    }
	}
#	else { $debug && print STDERR "Varying array lengths, skipping ...\n" }

	$devvar_val{$d, $v} = "$nvarval{$v}" ; # store new values
	    
    }	# end:  foreach($var)
}

##
# Monitor errors, packet rate, bandwidth utilization

sub procdata_router {
    local ($dev) = @_ ;
    local ($prevtime, $var);
    local ($d) = &toindex($dev);
    local ($dtime) = $testtime - $prevtime{$d};
    local (@ifDescr, @ifSpeed);

    $debug && 
	print STDERR "(debug) Running procdata_cisco_router for device $dev\n";
    foreach $var ( split(/[ ,\t\n]+/, $vars{"router"}) ) {
	local ($v) = &toindex($var) ;

	# Print values for some variables, deltas for some...
	if ($var =~ /ifDescr/) {@ifDescr = split(/[\t\n]/, $nvarval{$v}) ;}
	if ($var =~ /ifSpeed/) {@ifSpeed = split(/[\t\n]/, $nvarval{$v}) ;}
	elsif ($var =~ /ifOutQLen/) # need value, not rate
	{
	    local ($varalias) = ( $var =~ /^.*(\.[^.]+)$/ ) ;
	    $varalias = "RTR" . $varalias ;
	    print "VARIABLE $varalias VALUE\n" ;
	    local ($i) = 0;
	    foreach (split(/[\t\n]/, $nvarval{$v})) {
		print " $_ COMMENT $ifDescr[$i++]\n" ;
	    }
	}
	elsif ($var =~ /Octets|Pkts|Errors/)		# extract rates
	{
	    local ($size, $deltaval) =
		&vector_calc($nvarval{$v} , "-" , $devvar_val{$d,$v} );

	    if ($size != 0)
	    {
		local ($junk, $deltarate) =
		    &vector_calc($deltaval, "/", $dtime);
	    
		# shorten the prefix
		local ($varalias) = ( $var =~ /^.*(\.[^.]+)$/ ) ;
		$varalias = "RTR" . $varalias ;
		print "VARIABLE $varalias RATE-sec\n" ;
		local ($i) = 0;
		foreach (split(/[\t\n]/, $deltarate)) {
		    print " $_ COMMENT $ifDescr[$i++]\n" ;
		}
		if ($var =~ /Octets/) # extract bandwidth utilized
		{
		    $i = 0;
		    $varalias =~ s/Octets/BW/ ;
		    print "VARIABLE $varalias VALUE\n" ;
		    foreach (split(/[\t\n]/, $deltarate)) {
			printf " %d COMMENT $ifDescr[$i]\n",
			int(($_ * 8 * 100) / $ifSpeed[$i++]); # percentage
		    }
		}
	    }	# end if(size != 0)
	}

	$devvar_val{$d, $v} = "$nvarval{$v}" ; # store new values
	    
    }		# foreach $var
}

##
# Monitor reliability of interfaces, error rate

sub procdata_cisco_router {
    local ($dev) = @_ ;
    local ($prevtime, $var);
    local ($d) = &toindex($dev);
    local ($dtime) = $testtime - $prevtime{$d};
    local ($i);

    $debug && 
	print STDERR "(debug) Running procdata_cisco_router for device $dev\n";
    foreach $var ( split(/[ ,\t\n]+/, $vars{"cisco_router"}) ) {
	local ($v) = &toindex($var) ;

	if ($var =~ /IfReliab/) # no deltas for interface reliability
	{
	    local ($varalias) = ( $var =~ /^.*(\.[^.]+)$/ ) ;
	    $varalias = "CISCO" . $varalias ;
	    print "VARIABLE $varalias VALUE\n";
	    $i = 0;
	    foreach (split(/[\t\n]/, $nvarval{$v})) {
		++$i;
		print " $_ COMMENT Iface_$i\n" ;
	    }
	}
	else			# extract rates
	{
	    local ($size, $deltaval) =
		&vector_calc($nvarval{$v} , "-" , $devvar_val{$d,$v} );

	    if ($size != 0)
	    {
		$dtime = ($dtime / 60); # use minutes for small incr values
		local ($junk, $deltarate) =
		    &vector_calc($deltaval, "/", $dtime);
	    
		# shorten the prefix
		local ($varalias) = ( $var =~ /^.*(\.[^.]+)$/ ) ;
		$varalias = "CISCO" . $varalias ;
		print "VARIABLE $varalias RATE-min\n";
		$i = 0;
		foreach (split(/[\t\n]/, $deltarate)) {
		    ++$i;
		    print " $_ COMMENT Iface_$i\n" ;
		}
	    }
	}

	$devvar_val{$d, $v} = "$nvarval{$v}" ; # store new values

    }				# foreach $var
}

##
# Monitor number of lines in use for cisco terminal server.
# A little different than the other procdata_ modules above... it compares
# which of the TTY line types are active and adds them up. Does not 
# differentiate between dialin TTY lines and TTY lines used for other
# purposes- this can be added easily (look at the line descriptions and
# take that into account). Can perhaps also tweak to look at TTY line
# ranges (within the same terminal server).

sub procdata_cisco_ts {
    local ($dev) = @_;
    local ($d) = &toindex($dev);
    local ($acount, $tcount, @active, @termtype);

    $debug && 
	print STDERR "(debug) Running procdata_cisco_ts for device $dev\n";

    foreach $i ( split(/[ ,\t\n]+/, $vars{"cisco_ts"}) ) {
	if ($i =~ /tsLineActive/) {
	    @active = split(/[\t\n]+/, $nvarval{&toindex($i)} ); }
	if ($i =~ /tsLineType/)   {
	    @termtype = split(/[\t\n]+/, $nvarval{&toindex($i)} ) ;}
    }

    if ($#active < 0 || $#termtype < 0 || $#active != $#termtype)
    { 
	print STDERR "cisco_ts: ERROR, no. active($#active) != no. types($#termtype)\n" ;
	return;
    }
    $debug &&
	print STDERR "(debug): active list= $#active, type list= $#termtype\n";

    foreach $i (0..$#active) {
	if ($termtype[$i] =~ /\(3\)/ ) { # TTY terminal, not VTY
	    $tcount++;
	    $acount+=$active[$i]; # Since '$active' is either 0 or 1 anyway
	    if ($debug > 1) {
		print STDERR "(debug): Index=$i Type=$termtype[$i] Active=$active[$i]\n";
	    }
	}
    }

    $debug && print STDERR "(debug)Active/Total terminals: $acount/$tcount\n";

    print "VARIABLE CISCO.tsLineActive VALUE\n $acount\n";

}	# end: procdata_cisco_ts

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

###
### Utility routines
###

## Strip hyphens, etc from a string suitable for an index for an
#  array.
#
sub toindex {
    local ($i) = @_ ;
    $i  =~ s/[\.-]//g ;		# strip off all hyphens, dots, etc.

    return ($i);
}

## Do an operation on two vectors (lists)
#
sub vector_calc {
    local ($a, $op, $b) = @_ ;	# three strings
    local (@veca) = split(/[\t\n]+/, $a);
    local (@vecb) = split(/[\t\n]+/, $b);
    local ($result) = "" ;	# string, not list
    local ($i) = 0;

    if ($debug > 1) {print STDERR "vector_calc: To do $a $op $b\n" ; }
    # Remember that perl array indexes start from 0 (none = -1)
    if ($#veca < 0 || $#vecb < 0)  {return (0, @result); }
    
    # extend the single valued vector to length of longer vector
    if ($#veca == 0 && $#vecb != 0) {
	local ($x) = pop(@veca);
	foreach $i (0..$#vecb) {push(@veca, $x); }
    }
    if ($#vecb == 0 && $#veca != 0) {
	local ($x) = pop(@vecb);
	foreach $i (0..$#veca) {push(@vecb, $x); }
    }
    if ($#veca != $#vecb) {	# oh well, cannot do the operation
	$debug && print STDERR "Varying array lengths, skipping...\n" ;
	return (0, @result);
    }

    foreach $i (0..$#veca) {
	local ($tval) =  eval "int ($veca[$i] $op $vecb[$i])" ;
	$result .= "$tval\t" ;
    }

    if ($debug > 1) {print STDERR "vector_calc: result= $result\n" ;}
    return ($i + 1, $result);
}

##
# Read lines of the form:
#	<device> <cid> <type> <type> ...
# Create a long string of variables to be monitored for each device and
# store in @devvar{$device}
sub readconfig {
    local ($d, $typ, $v);

    open(CONFIG,"< $cfile") || die("Couldnt find $cfile, exiting");
    while(<CONFIG>)
    {
	chop;
	if(/^\s*\#/) {next;}   # skip comments
	if(/^\s*$/) {next;}   # skip blank lines
	if ( /^POLLINTERVAL\s(\d+)/i )  { $sleeptime = $1; next ;}

	if ( /^\s*(\S+)\s+(\S+)\s+(\S+.*$)/ )
	{	
	    push (@devicelist, "$1");
	    $d = &toindex("$1");
	    $cid{$d}=  "$2";
	    # remaining are device types
	    foreach $typ (split(/[ ,\t\n]+/, $3)) {
#		$debug && print STDERR "Dev types $typ\n";
		if ($istype{$typ}) {
		    $isdevtypes{$d, $typ} = 1;
		    foreach $v ( split(/[ ,\t\n]+/, $vars{$typ}) )  {
#			$debug && print STDERR " $v\n";
			$devvar{$d} .= "$v " ;
		    }
		}
	    }
	}
	else { print "Ignoring illegal line: $_\n" ; }

    }	# end while(CONFIG)
    close (CONFIG);

    if ($debug > 1) {
	foreach $d (@devicelist) {
	    $d = &toindex($d);
	    print STDERR "(debug) readconfig Device $d, variables = $devvar{$d}\n";
	}
    }
}


##
# ping a host to check if it is up and running. Might need some
# tweaking to adjust for different 'ping' styles on different systems.
# Return 0 if down, 1 if up.
# Can handle the following syntaxes so far:
#     ping host pktsize count           # HPUX & Ultrix
#     ping -s host pktsize count        # SunOS & Solaris
#     ping -c count -s pktsize host     # all others ?
sub doping {
    local ($rhost) = @_ ;
    local ($ping) = "ping" unless $ping;
    local ($value) = 0 ;	          # 1 for up, 0 for down

    $ostype= `uname -s -r -m`  unless $ostype;	# OS, revision, arch

    # PING output= 4 packets transmitted, 3 packets received, 25% packet loss

    if ($ping =~ /multiping/) {
	open(CMD, "$ping -s 100 -c 4 $rhost |");
    }
    elsif ($ostype =~ /HP-UX/ || $ostype =~ /ULTRIX/) {
	open(CMD, "$ping $rhost 100 4 |");
    }
    elsif ($ostype =~ /SunOS\s+4/ || $ostype =~ /SunOS\s+5/) {
	open(CMD, "$ping -s $rhost 100 4 |");
    }
    else {
	open (CMD, "$ping -s 100 -c 4 $rhost |");
    }

    while (<CMD>) {
	if ( /\s+(\d+)%\s+packet\s+loss/) { 
	    if ($1 < 50) { $value = 1; } # if 1 lost, then 25%
	    last;
	}
    }		# end: while(CMD)
    close (CMD);

    $debug && print STDERR "(dbg) doping return for $rhost =$value\n" ;
    return ($value);

}	# end doping()

## Die on getting a terminate signal. Clean up data files.
##
sub clean_out_onexit {
 
    if ($childpid)              # kill the telnet_daemon
    {
        kill ('TERM', $childpid) ; sleep 1; kill (9, $childpid);
        print STDERR "($$) killing telnet_daemon ($childpid)\n";
    }
 
    unlink ($dfile) ;
    unlink ($dfile . ".$$") ;  # delete temporary file
    die "($$) Terminating on signal\n" ;

}	# clean_out_onexit()

###
###  main
###

$cfile = shift || $cfile;
if ("$cfile" eq "") {die("No config file, exiting") ; }
elsif ($debug) {print STDERR "(debug) Config file is $cfile\n"; }

foreach $t (@devtypelist) { 
    eval "if (defined (&init_$t) == 1) {&init_$t; \$istype{\$t} = 1 ; }" ;
}

&readconfig ;

if ($#devicelist < 0) {die ("No devices to monitor, exiting")};
if ($dfile =~ /\S/) { 
    select (DFILE);		# select default output file
    foreach ('TERM','HUP','INT','KILL','QUIT') {
	$SIG{$_} = 'clean_out_onexit';
    }
}
 
 

# Output of Check-active SNMP command is:
#    Name: .iso.org.dod........lts.ltsLineTable.lts.LineEntry.tsLineActive.0
#    INTEGER: 1
local ($dev, $var, $typ);
while (1) {
    if ($dfile =~ /\S/) { open(DFILE, "> $dfile.$$"); }	# output data file
    $testtime = time . " " . `date` ; chop $testtime; print "TIME $testtime" ; 
    foreach $dev (@devicelist) {
	next if ($dev =~ /^\s*$/);
	if (&doping($dev) <= 0) { # device is dead
	    print STDERR "Host $dev not responding to ping, skipping\n";
	    next;
	}
	$debug && print STDERR "(debug) Monitoring device $dev\n";
	print "\nDEVICE $dev\n" ;
	$testtime = time ;
	local($d) = &toindex($dev) ;

	foreach $var ( split(/[ ,\t\n]+/, $devvar{$d}) ) {
	    next if ($var =~ /^\s*$/);
	    local ($v) = &toindex($var);
	    local ($cmd) = "$prog $dev $cid{$d} $var";

	    $nvarval{$v} = "" ; # init to null string
	    if ($debug > 1) {print STDERR "  (debug) running $cmd\n";}
	    open (CMD, "$cmd |") ;
	    $_ = <CMD> ;	# first line indicates error from snmpwalk
	    if (! /^Name:/ ) {
		print STDERR "snmpwalk ERROR for $dev/$var: $_\n" ;
		close (CMD);
		next;		# skip
	    }
	    while (<CMD>) {
#	    if ($debug > 1) {print STDERR "(debug) CMD output= $_";}
		next if (/^Name:/);
		chop;
#		if (/^\s*\S+:(\D*|\s*)(\d+)(\D*|\s*)$/) { # extract number
		if (/^[A-Z]+\s*\D+:\s*(\S+.*)\s*$/) { # any non-space value
		    local ($val) = $1;
		    $nvarval{$v} .= "$val\t"; # use tab as field separators
		}
	    }
	    close (CMD);
	    $debug && print STDERR "Stored nvarval{var} = $nvarval{$v}\n";
	}			# foreach $var

	$testtime = $testtime + ( (time - $testtime) / 2 ); # take average

	# process each type of variable for the device
	foreach $typ (@devtypelist) { 
	    if ($isdevtypes{$d, $typ}) {
		eval "defined (&procdata_$typ) && &procdata_$typ(\$dev)" ;
	    }
	}

	$prevtime{$d} = $testtime ;
    }				# foreach $device
    print "-----\n" ;		# indicate end of all sites.
    if ($dfile =~ /\S/) { close(DFILE); `mv $dfile.$$ $dfile`; }

    $debug && print STDERR "Sleeping for $sleeptime...\n";
    sleep $sleeptime ;
}				# end while(1)

