#!/usr/pkg/bin/perl
#
# $Header: /home/vikas/src/nocol/perlnocol/RCS/nocollib.pl,v 1.16 2000/03/28 03:28:43 vikas Exp $
#
# 	nocollib.pl - perl library of NOCOL routines
#
#
# Initially derived from the perl modules submitted by John Wobus, 
# jmwobus@mailbox.syr.edu 9/21/93  (see novellmon, armon)
#
# This rewrite as a NOCOL perl library by Vikas Aggarwal 1994 - 1997
# All current NOCOL development undertaken and maintained by vikas@navya.com
#

## The following is a list of associative arrays used globally to
#  refer to the fields in the EVENT structure.
#	sender, sitename, siteaddr,
#	varname, varval, varthres, varunits,
#	mon, day, hour, min, severity, loglevel, nocop
#
#  Define $libdebug=1 to turn on verbose debugging messages.
#  Might need to fix the '$event_t' description for null padding
#  (since some systems might align structure fields on different
#  boundaries). Use the 'utility/show_nocol_struct_sizes.c' program
#  to see the size of various fields on your system.
#
# Usage:
#
#  require "nocollib.pl";
#
#
# Subroutines:
#
#    unpackevent -	Extracts packed EVENT fields into various arrays
#    packevent -	Converts array fields into a packed EVENT structure
#    readevent -	Read EVENT from open file desc and unpack it.
#    writeevent -	Pack various arrays into EVENT struct and write out.
#    nocol_startup -	Forks off and creates a new PID file, etc.
#    standalone -	Kill earlier running processes and setup signals
#    init_event -	Fill in various arrays with initial (global) values
#    update_event -	Given status, updates array fields and logs if needed
#    eventlog -		Logs event to 'noclogd' daemon
#   
#
####################################################################
# 

#######################
##  Customize these  ##
#######################
$nocolroot =  "/usr/pkg"   unless $nocolroot;	# SET_THIS
$etcdir  = "$nocolroot/etc"  unless $etcdir;	# location of config file
$piddir  =  "/var/run"   unless $piddir;	# SET_THIS
$bindir  = "$nocolroot/bin"  unless $bindir;
$datadir = "/var/run/data"   unless $datadir;	# output data file

push(@INC, $bindir); push(@INC, $etcdir); # add to search paths for 'require'
$ENV{'PATH'} .= ":$bindir";             # add to path for snmpwalk, etc.

$NLOG_HOST =  "localhost"  ;	# Used for logging to noclogd, SET_THIS
$NLOG_SERVICE = "noclog" ;		# if port is in /etc/services
$NLOG_PORT = 5354 unless $NLOG_PORT;	# default port if NOT in /etc/services

$HOSTMON_SERVICE = "hostmon" unless $HOSTMON_SERVICE; # for 'hostmon'
$HOSTMON_PORT = 5355 unless $HOSTMON_PORT; # used if not in /etc/services

$ping =  "/sbin/ping" ;				# SET_THIS to ping location
$rpcping = "$bindir/rpcping" unless $rpcping; # SET_THIS, used by 'hostmon'

## Need to pad the following for pack() using 'x' nulls (actual length is 195,
#  but C compilers have to align on word? boundary).
#  event_t is the EVENT structure format, notice the nulls for ascii strings
#  The last 'x' is a padding to align depending on machine architecture.
#  Use the utility/show_nocol_struct_sizes to see the size on your arch.
#  NOTE : Use 'A' instead of 'a' as unpacking 'a' will not remove any
#  trailing blanks or spaces whereas in case of 'A' it does.
$event_t =  "a11 x a15 x a127 x x4 a15 x L L a7 x CCCC C C C x1"  ;	# SET_THIS
$levent = length( pack($event_t, 0, '') );	    # sizeof structure

if ($libdebug) {
    print STDERR "(libdebug) Sizeof event= $levent\n" ;
    print STDERR "\tNocol Dir= $nocolroot, LOGhost= $NLOG_HOST\n" ;
}

######################
##  GLOBAL DEFINES  ## Extracted from nocol.h
######################


$E_CRITICAL = 1;
$E_ERROR    = 2;
$E_WARNING  = 3;
$E_INFO     = 4;

$n_UP          = 0x01;
$n_DOWN        = 0x02;
$n_UNKNOWN     = 0x04;
$n_TEST        = 0x08;
$n_NODISPLAY   = 0x10;

$varname       = "UNSET" unless  $varname;
$varunits      = "UNSET" unless  $varunits;
$sleepint      = 5*60    unless  $sleepint;

## these variables are needed by eventlog()
$nlogfd = undef;
$nlogclosetime = 0;		# used for deciding when to reopen logfile


###################################################################
# Support routines for Networking
###################################################################

##
# Create a connected socket to the remote host.
#	newSocket ("cisco-gw.abc.com", 23, 'tcp')
# Dies on failure.
sub newSocket {
    local ($host, $port, $sproto) = @_ ;
    local ($type, $proto, $haddr);

    $sproto = 'tcp' unless $sproto;

    # Depending on version of perl, call 'use Socket' or 'require socket.ph'
    # From Netnews posting by jrd@cc.usu.edu (Joe Doupnik)
    local ($AF_INET, $SOCK_STREAM, $SOCK_DGRAM) = (2, 1, 2); # default values
    if ( $] =~ /^5\.\d+$/ ) {        # perl v5
	# print STDERR "debug: Check for perl5 passed...\n";
	eval "use Socket";
	$AF_INET = &Socket'PF_INET;	#'
        $SOCK_STREAM = &Socket'SOCK_STREAM;	#'
        $SOCK_DGRAM  = &Socket'SOCK_DGRAM;	#'
    }
    else {  # perl v4
	eval {require "socket.ph"} || eval {require "sys/socket.ph"} ;
	if ( defined (&main'PF_INET) ) {	#')) {
	    # print STDERR "debug: found sys/socket.ph\n";
	    $AF_INET = &main'PF_INET;	#'
            $SOCK_STREAM = &main'SOCK_STREAM;	#';
            $SOCK_DGRAM = &main'SOCK_DGRAM;	#';
        }
        # Solaris, need to run h2ph on include/socket.h to create socket.ph
	elsif (`uname -s -r -m` =~ /SunOS\s+5/) {
            require "sys/socket.ph";        # Gives error and exits
            die 'Did you forget to run h2ph ??';
        }
    }

    if ($port == 0 || $port =~ /\D/) {
      ($junk,$junk,$port) = getservbyname($port, $sproto);
    }
    if (! $port) {
      print STDERR "No port number (is entry needed in /etc/services ?)\n";
      return undef;
    }
    if ($sproto =~ /tcp/) { $type = $SOCK_STREAM;}
    else { $type = $SOCK_DGRAM };

    # Do equivalent of a inet_ntoa()
    if ($host =~ /^\d/) {
	local ( @n ) = split(/\./, $host);
	$haddr = pack('C4', @n);
    }
    else { ($junk,$junk,$junk,$junk, $haddr) = gethostbyname($host); }

    if (! $haddr) {
      print STDERR "Unknown host (no address)- $host\n";
      return undef;
    }

    unless ($ostype) {$ostype= `uname -s -r -m`; chop ($ostype); }
    if ($ostype =~ /BSD\/OS\s+4/) { 	# bsdi 4.0 changed socket struct
      $paddr = pack ('x C n a4 x8', $AF_INET, $port, $haddr);
    }
    else { $paddr = pack ('S n a4 x8', $AF_INET, $port, $haddr); }

    ($junk, $junk, $proto) = getprotobyname($sproto);
    socket(SOCK, $AF_INET, $type, $proto) || die "socket: $!";
    if (! connect(SOCK, $paddr)) {
	close SOCK;
	print STDERR "newSocket() connect failed: $!\n";
	return undef;
    }

    return (SOCK);
}

########################################################################
###    NOCOL subroutines
########################################################################
##

##
# Given an index (to the list of associative arrays that are globally
# declared), and an event structure, it unpacks and fills in the arrays
# with the values from the event structure.

sub unpackevent {
    local($i, $event) = @_ ;		# index and event
    local($vpad, $uevent_t);
    # $uevent_t is required because in the original event_t
    # we have used 'a' for packing to put NULL padding.  This
    # is required for C programs to read this structure.  But
    # while unpacking we have to use 'A' so that it removes
    # any trailing spaces or NULLs.  So we are converting 
    # 'a' to 'A'.
    ($uevent_t = $event_t) =~ tr/a/A/; 

    ($sender{$i},$sitename{$i},$siteaddr{$i},
     $varname{$i},$varvalue{$i},$varthres{$i},$varunits{$i},
     $mon{$i},$day{$i},$hour{$i},$min{$i},
     $severity{$i},$loglevel{$i},$nocop{$i}, $vpad)
	=unpack($uevent_t, $event);
    
    ($libdebug > 0) && print "(libdebug) unpackevent: $i\n";
}

##
# Given an index (to the list of associative arrays that are globally
# declared), it creates an EVENT structure and returns it.
# Note the 'vpad' to align the structure on an even byte boundary.
sub packevent {
    local($i) = @_ ;		#index to associative arrays
    local($event) ;
    local($vpad) = "" ;
    $event = pack($event_t, 
		  $sender{$i},$sitename{$i},$siteaddr{$i},
		  $varname{$i},$varvalue{$i},$varthres{$i},$varunits{$i},
		  $mon{$i},$day{$i},$hour{$i},$min{$i},
		  $severity{$i},$loglevel{$i},$nocop{$i}, $vpad) ;
    
    return($event) ;
}

##
## Subroutines to read and write from the datafile.
#
sub readevent {
    local($fd, $index) = @_ ;	# open file descriptor & index
    local($bl,$event);
    
    $index = 'TEMP' unless $index ;		# use TEMP storage by default
    if ($bl = read($fd, $event, $levent)) {
	&unpackevent($index, $event);		# fills in the various arrays
    }
    else { 
	$debug && print STDERR "(dbg)readevent():No data(Read returned $bl)\n";
    }

    return($bl);
}

sub writeevent {
    local($fd, $index) = @_ ;	# open file descriptor & index
    local($event) = &packevent($index);
    print $fd $event ;
}

##
#  printevent()  to display the event arrays given an index 
sub printevent {
    local($i) = @_ ;
    
    print "
      SENDER= $sender{$i}, $sitename{$i},$siteaddr{$i},
      VAR= $varname{$i},$varvalue{$i},$varthres{$i},$varunits{$i},
      DATE= $mon{$i},$day{$i},$hour{$i},$min{$i},
      SEV= $severity{$i},$loglevel{$i}, $nocop{$i}\n" ;
    
}

##	gettime()
#
# If the global variable '$eventime' is set, then use its value (the
# 'dotest' subroutine could have set this.
sub gettime {
    local($mon,$day,$hour,$min,@timesecs);

    if ($eventtime > 0) {@timesecs = localtime($eventtime); }
    else {@timesecs=localtime(time); }
    $mon=substr("00".(1+$timesecs[4]), -2);
    $day=substr("00".$timesecs[3], -2);
    $hour=substr("00".$timesecs[2], -2);
    $min=substr("00".$timesecs[1], -2);
    return($mon,$day,$hour,$min);
}


## 	nocol_startup()
# Forks and creates the pid file. Does some preliminary stuff also.
#
sub nocol_startup {
    $prognm = $0 unless $prognm; # set to the name of program
    @me=split(/\//,$prognm); $me=pop(@me);

    #$piddir=join("/",@me); if ($piddir eq "") {$piddir=$etcdir;}
    $piddir=$piddir unless $piddir;
    $cfile="$etcdir/$me-confg" unless $cfile;
    $datafile="$datadir/$me-output" unless $datafile;

    $sender= $me unless $sender;	# filled in the EVENT sender
    # get OS, revision, arch
    unless ($ostype) { $ostype= `uname -s -r -m`; chop ($ostype); }

    ($libdebug > 1) && print STDERR "(libdebug) me= $me, sender= $sender\n" ;
    if ( (!$debug) && ($p= fork))  {print "$p\n"; exit;}
    &standalone($me, $piddir);
}    

##	standalone()
# Checks to see if PIDFILE exists and kills any earlier running process.
# Created new PIDFILE and sets up signal handlers. Incidentally, pidfile
# cannot be local since it is unlinked by the signal handler routines.
#
sub standalone {
    local($me,$dir)=@_;
    local($localhost)=`hostname`; chop ($localhost);
    local($pid,$host);
    local($PSCMD) = "/bin/ps";	# cmd to see process by giving a pid

    local ($status) = scalar grep(/usage/i, `$PSCMD 1 2>&1`);
    if ($status >= 1) { $PSCMD = "/bin/ps -p" ;}	# hope no other flags needed
    
    $pidfile= "$dir/$me.pid";	# cannot be local()

    if(open(PID,"<$pidfile"))
    {
	chop($pid=<PID>); chop($host=<PID>); close(PID);
	if ("$host" ne "$localhost") {
	    die("Program probably running on $host, else delete $pidfile\n");
	}
	
	if($pid =~ /^\d+$/)
	{
	    for (1..2) {	# try killing twice
		foreach (`$PSCMD $pid`) {
		    chop;
		    if(/$pid.*$me/){kill(9,$pid); print "killing process $pid\n"; }
		}			# end: foreach()
	    }			# end: for(1..2)
	}
    }				# end: open(PID...)
    if (open(PID,">$pidfile")){print PID "$$\n$localhost\n"; close(PID);}
    else {die ("standalone: cannot create $pidfile, $!\n") } ;

    $SIG{"QUIT"} = clean_out ;
    $SIG{"TERM"} = clean_out ;
    $SIG{"INT"}  = clean_out ;
    $SIG{"USR1"} = toggle_libdebug ;
}

sub clean_out {
   unlink $pidfile ;
   unlink $datafile ; 
   die "Terminating on signal\n" ;
}

sub toggle_libdebug {

    $libdebug = ++$libdebug % 3 ; # increment to 2, then back to 0
    if ($libdebug) { print STDERR "(nocollib): libdebug = $libdebug\n"; }
}


## Fill in default values for the item arrays. Inserts the following 'global'
#  variables:   $sender  $varname  $varunits
#  Gets the sitename/siteaddr from the args passed to it.
sub init_event {
    local($sitename, $siteaddr, $i) = @_ ;	# index into assoc arrays
    
    if ($libdebug) {
	print STDERR "(libdebug) init_event: Inserting index= $i, Sitename= $sitename, Siteaddr= $siteaddr\n"; }

    $siteaddr = '-' if ($siteaddr eq '');
    if ($sender eq "") {die("init_event: Bad sender= $sender\n"); }

    $sender{$i} = $sender ;
    $sitename{$i} = $sitename ;
    $siteaddr{$i} = "$siteaddr";

    $varname{$i}  = $varname ;
    $varvalue{$i} = 0 ;
    $varthres{$i} = 0 ;
    $varunits{$i} = $varunits ;
    
    ($mon{$i},$day{$i},$hour{$i},$min{$i}) = &gettime ;
    
    $severity{$i} = $E_INFO;
    $loglevel{$i} = $E_INFO;
    $nocop{$i}    = $nocop{$i} | $n_UNKNOWN ;
    
} # end init_event()

##	calc_status()
# Useful to extract the status and maximum severity in monitors which
# have 3 thresholds. Given the three thresholds and the value, it returns
# the 'status, thres, maxseverity'.
# Handle increasing or decreasing threshold values.
sub calc_status {
    local ($val, $warnt, $errt, $critt) = @_ ; # the value and 3 thresholds
    local ($status) = 1;
    local ($gt) = 1 ;	#  test if value *greater* than thresholds

    if ($critt < $warnt) { $gt = 0 ;} # test if value *less* than thres
    elsif ($critt == $warnt && $critt == 0) { $gt = 0 ;}

    if ( ($gt && $val >= $warnt) || ($gt == 0 && $val <= $warnt) ) 
       { $status = 0; }		# value exceeds warning threshold

    if ( ($gt && $val >= $critt) || ($gt == 0 && $val <= $critt) ) 
       { return ($status, $critt, $E_CRITICAL); }
    elsif ( ($gt && $val >= $errt) || ($gt == 0 && $val <= $errt) )
       { return ($status, $errt,  $E_ERROR); }
    elsif ( ($gt && $val >= $warnt) || ($gt == 0 && $val <= $warnt) )
       { return ($status, $warnt, $E_WARNING); }
    else    
       { return ($status, $warnt, $E_INFO); } # $status should be UP

}	# end calc_status
    

##	update_event()
# Given index to events, status (0 or 1), new value, max severity to escalate
# to, this subroutine will update the various associative arrays and log the
# event also (if the state has changed).
sub update_event {
    local($i, $status, $value, $maxsev) = @_ ;	# $i is index to arrays

    if ($libdebug) {
	print STDERR "(libdebug) update_event: ITEM $i, stat=$status, maxsev=$maxsev, val= $value\n";
    }
    if ($maxsev < $E_CRITICAL) { $maxsev = $E_CRITICAL; }

    $varvalue{$i} = $value ;

    if ($status)	# status is UP
    {
	($mon{$i},$day{$i},$hour{$i},$min{$i}) = &gettime ;
	if (!($nocop{$i} & $n_UP))			# recent state change
	{
	    $nocop{$i} = $nocop{$i} & ~($n_UP | $n_DOWN | $n_UNKNOWN) | $n_UP;
	    $loglevel{$i} = $severity{$i} ;	# loglevel set to old level
	    $severity{$i} = $E_INFO;
	    &eventlog(&packevent($i)) ;		# log to noclogd daemon
	}
    }
    else		# status is DOWN, escalate severity
    {
	local($oseverity) = $severity{$i} ;
	# escalate severity
	$severity{$i}= ($severity{$i} > $maxsev) ? ($severity{$i} - 1) : $maxsev;

	$nocop{$i}= $nocop{$i} & ~($n_UP | $n_DOWN | $n_UNKNOWN) | $n_DOWN;

	if ($oseverity != $severity{$i}) # severity change
	{
	    ($mon{$i},$day{$i},$hour{$i},$min{$i}) = &gettime ;
	    # smaller severity is more severe... set  that as loglevel
	    $loglevel{$i}= $severity{$i} < $oseverity ? $severity{$i}:$oseverity;
	    &eventlog(&packevent($i));
	}
    }
    
    if ($libdebug) {print STDERR "(libdebug) update_event:\n\t";&printevent($i);}
    
}  # end update_event()

##
##	eventlog()
# Send the event to NLOG_HOST using UDP.
#

sub eventlog {
    local($event) = @_ ;
    local($RETRY_REOPEN) = 1*60 ; # try re-opening logging socket every minute
    
    if (! defined($nlogfd))
    {
	local($lport);

	# do not try again until RETRY_REOPEN secs
	if ((time  - $nlogclosetime) < $RETRY_REOPEN) { return (-1) ; }

	## Get local port number
	($junk, $junk, $lport) = getservbyname($NLOG_SERVICE, 'udp');
	$lport = $NLOG_PORT  unless $lport ;

	$nlogfd = &newSocket($NLOG_HOST, $lport, 'udp');
	if (! defined($nlogfd)) {
	    print STDERR "eventlog- could not connect to $NLOG_HOST\n";
	    $nlogclosetime = time ;	# record time of last attempt
	    return (-1);
	}

	select( (select($nlogfd), $| = 1)[0] ); # make socket unbuffered
        ($libdebug > 0) && print STDERR "(libdebug) eventlog: socket opened\n";
    }  	# end: if(socket not open)
    
    ## Here only if socket has been opened and have to write out EVENT.
    if (! defined(send($nlogfd, $event, 0)) )
    {
      print STDERR ("(libdebug) eventlog send() error: $!");
      close($nlogfd);
      undef $nlogfd ;
      # Try to reopen and resend, since its important. use $nlogclosetime
      # a flag to prevent endless loop
      if ($nlogclosetime != 1) {
	$nlogclosetime = 1;	# prevent looping
	eventlog($event);
	$nlogclosetime = 0;
      }
      return(-1) if (! defined($nlogfd));
    }

    return (0);
}	# eventlog()


##
##
# Standard main.. just create subroutines in your new
# nocol monitor and call nocol_main from your monitors main()
# Subroutines needed are:
#	readconf 	- should define @items
#	dotest		- shout set $status, $value, $maxseverity
# For varying thresholds of severity, you will have to set the values
# of the global variables '$maxseverity' and '$varthres{$index=$item}'
# in your dotest().
#
sub nocol_main {

    &nocol_startup;
    &readconf;
    
    foreach $item (@items) {
	local ($host, $addr, $junk) = split( /\t/, $item );
	&init_event ($host, $addr, $item);	# fill in initial values
    }

    while (1)
    {
	local ($stime, $deltatime);

	$stime = time;		# time starting tests

	foreach $item (@items) {
	    local ($host, $addr, $junk) = split( /\t/, $item );
	    local ($status, $value) = &dotest($host, $addr);

	    if ($status < 0)	# bad status, probably some error
	    {
		print STDERR "$me: dotest failed for $item... skipping\n";
		next;
	    }
	    else {&update_event($item, $status, $value, $maxseverity);}
	}
	open(OEVENTS,">$datafile");
	foreach $item (@items)
	{
	    if(!$forget{$item})		# maintained by calling monitor
	    {
		&writeevent(OEVENTS, $item);
	    }
	}
	close(OEVENTS);

	$deltatime = time - $stime;		# time to do tests

	if ($sleepint > $deltatime) { sleep($sleepint - $deltatime); }

    }				# end: while(1)
    
}	# end nocol_main()

##
#
##

1;		# needed
