#!/usr/pkg/bin/perl
#
# $Header: /home/vikas/src/nocol/perlnocol/hostmon-osclients/RCS/hostmon-client,v 2.6 2000/03/27 06:51:26 vikas Exp $
#
#	hostmon-client.main
#
# Part of the NOCOL monitoring package. Run's system commands on the
# system and prints out system statistics. Output is parsed by the
# 'hostmon' server program which then generates output in the NOCOL format.
# Includes systems specific files automatically i.e for LINUX includes
# hostmon-client.linux etc.  All tests like pstat, df, vmstat are in
# system specific file.
#
#	Copyright 1994, 1997 Vikas Aggarwal, vikas@navya.com
#
# Author:  Vikas Aggarwal,  vikas@navya.com
#
#
# See COPYRIGHT file for full details.
##
#
## Checks the following:
#
#	Uptime =	'uptime' (for reboots)
#	Load average =	'uptime'
#	NFS stats =	'nfsstat' -rc/-m/
#	Swap space =	'pstat or swapon'
#	CPU times =	'iostat'
#	Disk i/o =	'iostat'
#	Page Faults/Swaps =	'vmstat'
#	Disk space =	'df'
#	Network Interface =	'netstat' -m/-s/-i/
#	Mail queue = 	'sendmail -bp' or 'qmail-qread'
#	
##
## Output of this file
##
#	TIME 784419191 secs
#	Uptime 62266 mins
#	Load5 0 load-5min
#	FileTable 13 %used
#	InodeTable 13 %used
#	ProcTable 13 %used
#	SwapSpace 25140 MBytes
#	NFStimeouts 0 %timeouts
#	NFSrequests 7433383 requests
#	DFspace_avail 6 MB /
#	DFspace_%used 71 %full /
#	DFspace_avail 72 MB /usr
#	DFspace_%used 44 %full /usr
#	DFspace_avail 354 MB /export/home
#	DFspace_%used 22 %full /export/home
#	DFinodes_avail 13699 inodes /
#	DFinodes_%used 9 %inodes /
#	DFinodes_avail 70880 inodes /usr
#	DFinodes_%used 6 %inodes /usr
#	DFinodes_avail 251048 inodes /export/home
#	DFinodes_%used 1 %inodes /export/home
#	IOseek 0.0 msps dk0
#	IObw 3.2 %util dk0
#	MemAvm 0 avm
#	MemFree 14784 fre
#	PageIn 0 pi
#	PageOut 0 po
#	CtxtSw  42 rate
#	CPUus 2 %user
#	CPUsy 1 %system
#	CPUidle 97 %idle
#	NetIErr 0 PktRate
#	NetOErr 0 PktRate
#	NetColl 0 PktRate
#	MailQLocal 2 Length
#	MailQDest  4 Length mail.abc.com
#
##
############################
## Variables customization #
############################
#
$sleeptime = 5*60 ;
@monitorlist = ('uptime', 'pstat', 'nfsstat', 'df', 'iostat', 'vmstat',
		'netstat', 'mailstat');

## Following is needed for the 'daemon' part of the client which listens on
#  the following service port for any connection requests. The connections are
#  checked against a permit list and then the entire data file is dumped over.
$NLOG_HOST =  "localhost"  ;	# SET_THIS to the permitted hostmon server host
$SENDMAIL = "sendmail" ;# SET_THIS to location of your 'sendmail'
#$SENDMAIL = "/usr/local/bin/qmail-qread";	# SET_THIS if using qmail

$HOSTMON_SERVICE = "hostmon" unless $HOSTMON_SERVICE ;
$HOSTMON_PORT = 5355 unless $HOSTMON_PORT ;

# add to include path for os specific modules. You must set this to
# the installation directory of hostmon-osclients
($dirname = $0) =~ s@/[^/]*$@@ ;
push(@INC, $dirname, ".", "hostmon-osclients");		# SET_THIS

# Add to the path. Typically sendmail is under /usr/lib/
$ENV{'PATH'} .= ":/usr/ucb:/usr/bsd:/bin:/usr/bin:/sbin:/usr/sbin" ;
$ENV{'PATH'} .= ":/etc:/usr/etc:/usr/lib:/lib:/slib:/usr/slib" ;

## Define the list of permitted hosts that can connect to the port above
# and get data (the name of the host that runs the hostmon server)
# @permithosts = ("abc.foo.com", "129.1.1.134");
@permithosts = ("$NLOG_HOST") unless @permithosts ;

$debug = 0;                             # set to 1 for debugging output
 
#  The output data filename is "/tmp/<hostname>.hostmon"
#  This must match the name used in the rcp from 'hostmon' 
$dfile = "/tmp/" . `hostname` ; chop $dfile ; $dfile .= ".hostmon" ;
#######################################################################
#
## Following couple of subroutines are the 'daemon' part of the client.
#

# Depending on version of perl, call 'use Socket' or 'require socket.ph'
# From Netnews posting by jrd@cc.usu.edu (Joe Doupnik)
($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 ??';
    }
}

##
##
sub checkconnect {
    local($con, $permit, $junk);
    local($port) ;
    local(@permitaddr, $permitaddr);		# list built by checkconnect()
    local($dfile) = '/etc/motd'  unless $dfile ;
    local($sockaddr) = 'S n a4 x8';

    $ostype= `uname -s -r -m`  unless $ostype;
    if ($ostype =~ /BSD\/OS\s+4/) { $sockaddr = 'x C n a4 x8'; }

    $debug && print STDERR "(debug) telnet read datafile= $dfile\n";

    ## Convert IP address of permitted hosts.
    foreach $permit (@permithosts)
    {
	if ($permit =~ /^\d+/)	# IP address, not name
	{
	    local(@a) = split (/\./, $permit);
	    $permitaddr = pack ('C4', @a);

	    ($junk,$junk,$junk,$junk, @ipaddr) = 
		gethostbyaddr($permitaddr, $AF_INET);
	}
	else {
	    ($junk,$junk,$junk,$junk, $permitaddr) = gethostbyname($permit);
	}

	if ($permitaddr) { 
	    push (@permitaddr, $permitaddr);
	    if ($debug)
	    {
		local (@straddr) = unpack ('C4', $permitaddr);
		print STDERR "(dbg) telnet_permit $permit (@straddr)\n";
	    }
	}
	else {
	    print STDERR "Cannot gethostbyname for $permit, ignoring\n";
	}

    }			# end: foreach


    ## Get a socket
    ($junk, $junk, $proto) = getprotobyname('tcp');
    if (! socket(S, $AF_INET, $SOCK_STREAM, $proto)) {
	print STDERR "socket() call failed: $!\n";
	return (-1);
    }
	
    ## Get port number
    ($junk, $junk, $port) = getservbyname($HOSTMON_SERVICE, 'tcp');
    $port = $HOSTMON_PORT  if ((! $port) || $port !~ /^\d+$/);
    $debug && print STDERR "(dbg) telnet_daemon Port= $port\n" ;

    $here = pack($sockaddr, $AF_INET, $port, "\0\0\0\0");
    
    select(S); $| = 1; select(STDOUT);		# unbuffered socket
    ## Now bind to the socket
    bind (S, $here) || die "bind: $!";
    listen (S, 5) || die "listen: $!";

    ## Listen for connections forever.
    for ($con = 1; ; ++$con)
    {
	local ($remoteaddr, $remote);
	if ($con > 100) { $con = 1; }  # reset
	($debug >1) && print STDERR "(telnet_daemon) Listening for connection: $con\n";
	($remote = accept(NS, S)) || die $!; # waits here for connection

	select(NS); $| = 1; select(STDOUT);	# unbuffered socket
	($af, $port, $remoteaddr) = unpack($sockaddr, $remote);
	@remoteaddr = unpack('C4', $remoteaddr);
	## can also try doing:
	# ($port, $remoteaddr) = sockaddr_in($remote);
	# @remoteaddr = split('\.', inet_ntoa($remoteaddr));
	$debug && print STDERR "(telnet_daemon)connection from @remoteaddr\n" ;

	## Check remote's address
	local ($paddr);
	local ($goodhost) = 0;

	foreach $paddr (@permitaddr)  {
	    if ($paddr eq $remoteaddr) { $goodhost = 1; last; }	# note 'eq'
	}

	if (! $goodhost)  {
	    $debug && print STDERR "(telnet_daemon) rejecting connection from @remoteaddr\n";
	    print NS "Connection not permitted\n" ;
	    close (NS); next ;
	}
	$debug && print STDERR "(telnet_daemon) allowing connection from @remoteaddr\n";

#	while (<NS>) { print "$con: $_"; } # to read from remote site

	if (! -e $dfile || -z $dfile) {sleep 2;} # in case mv-ing over
	if (! -e $dfile || -z $dfile) {close NS; next;} # don't wait
	open (IFILE, $dfile);
	while (<IFILE>) {
	    print NS $_ ;
	}
	close IFILE ;
	close (NS);
    }				# end: forever

}	# end: checkconnect()


## Fork and create a telnet daemon that listens on port HOSTMON_PORT
#  Fork twice, and then exit so that the parent is the init process
#  since we dont want any orphans.
#
sub create_telnet_daemon {
    local ($myname) = "$0 (telnet_daemon)";
    local ($tmppidf) = "/tmp/pidxfr" . $$;

    if ($childpid) {
	foreach (`$PSCMD $childpid`) {chop; /$childpid\s+/ && return; }
    }

    if (fork) {			# in parent
	wait ;			# wait for first child to exit...
	open (CHILDPID, "< $tmppidf");
	while (<CHILDPID>)  { $childpid = $_ ; }
	close (CHILDPID); unlink ($tmppidf);
	$debug && print STDERR "(debug) Forked telnet_daemon ($childpid)\n"; 
	return ($childpid) ;
    }				# parent returns

    ## first child process forks the real child and then exits. This
    #  mumbo jumbo prevents a 'orphan' child (zombie processes).
    if (($childpid = fork)) {
	open (CHILDPID, "> $tmppidf");
	print CHILDPID $childpid ; # no newline
	close (CHILDPID);
	exit 0;
    }				# first child exits quickly

    # Child's child process is the real telnet daemon...
    sleep 1 until getppid == 1;
    foreach ('TERM','HUP','KILL','QUIT') { $SIG{$_} = 'DEFAULT'; }
    $0 = "$0 (telnet_daemon)" ;	# change the name shown in 'ps'
    sleep(2);			# allow older daemon to die first
    &checkconnect ;		# never returns
    exit ;			# in case it ever does return

}	# end: create_telnet_daemon()



####################################################################
## Following routines are not doing any 'tests'. They are utility ##
## routines.                                                      ##
####################################################################

##
# Define a boolean variable for testing OS types easily in the above
# routines. Also set the $osfile variable
sub osinit {
    $ostype= `uname -s -r -m` ; chop $ostype; # OS, revision, arch
    $osfile = "hostmon-client.";
    $debug && print STDERR "OSTYPE = $ostype\n";
    $PSCMD = "/bin/ps";	# ps command to allow pid on cmd line. Autoset below


    # set boolean values for OS's
    if    ($ostype =~ /AIX/)		{$osfile .= "aix";}
    elsif ($ostype =~ /FreeBSD\s+2\.[2345]+/)	{$osfile .= "freebsd";}
    elsif ($ostype =~ /FreeBSD\s+[34]+/)	{$osfile .= "freebsd"; }
    elsif ($ostype =~ /FreeBSD/)	{$osfile .= "bsdi"; } # older versions
    elsif ($ostype =~ /BSDI/)		{$osfile .= "bsdi"; }
    elsif ($ostype =~ /BSD\/OS/)	{$osfile .= "bsdi"; }
    elsif ($ostype =~ /NetBSD/)		{$osfile .= "bsdi"; }
    elsif ($ostype =~ /SunOS\s+4/)	{$osfile .= "sunos4";}
    elsif ($ostype =~ /SunOS\s+5/)	{$osfile .= "solaris2";}
    elsif ($ostype =~ /OSF1/)		{$osfile .= "osf1";}
    elsif ($ostype =~ /ULTRIX/)		{$osfile .= "ultrix";}
    elsif ($ostype =~ /Linux/)		{$osfile .= "linux";}
    elsif ($ostype =~ /HP-UX/)		{$osfile .= "hpux";}
    elsif ($ostype =~ /IRIX\ 4/)	{$osfile .= "irix4";}
    elsif ($ostype =~ /IRIX\ 5/)	{$osfile .= "irix5";}
    elsif ($ostype =~ /IRIX(64)?\ 6/)	{$osfile .= "irix6";}
    else  {print STDERR "hostmon-client:FATAL, OS $ostype is unknown\n"; die;}
    #$debug && 
    print "OSTYPE : $ostype\nLoading File : $osfile\n";
    
    ## force units to 1024 blocks for df and vmstat for SVR4
    # $ENV{'BLOCKSIZE'} = "1024";

    #if ($ostype =~ /SunOS\s+5/) { $PSCMD = "/bin/ps -p"; }
    local ($status) = grep(/usage/i, `$PSCMD 1 2>&1`);
    if ($status == 1) { $PSCMD = "/bin/ps -f -p" ;}
}

sub standalone {
    local (@me) =split(/\//,$0); 
    $me = pop(@me);		# not local, since needed by exit routine
    local ($p);

    if (!$debug) { if ($p=fork) {print "$p\n"; exit;} }
    local($pidfile)= "/tmp/$me.pid";

    if(open(PID,"<$pidfile"))
    {
	local ($pid) = <PID>; chop $pid ; close(PID);
        if($pid =~ /^\d+$/)
        {
	    foreach (`$PSCMD $pid`) {
		chop;
		if(/$pid.*$me/) {
		    kill('TERM',$pid); print "($$) killing process $pid\n";
		    foreach (1..3) {
			if (-e $pidfile) {sleep 1;}
			else { break; }
		    }
		    kill(9, $pid); print " killing process $pid\n";
		}
	    }		# end: foreach()
	}
    }                   # end: open(PID...)

    if (open(PID,">$pidfile")){print PID "$$\n"; close(PID);}
    else {die ("standalone: cannot create $pidfile, $!\n") ; }

    foreach ('TERM','HUP','INT','KILL','QUIT') {
	$SIG{$_} = clean_out_onexit;
    }

}	# end standalone()


## 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 . '.tmp') ;	# delete temporary file
    unlink ("/tmp/$me.pid");	# pid filename
    die "($$) Terminating on signal\n" ;
}	# clean_out_onexit()

##
##	mailstat
#  Show total local mail queue length and top 5 'down' hosts
#  Uses sendmail or qmail
sub mailstat
{
    local ($qsize, $host) = (0, "");
    local (%NTo);
    local ($cmd) = "$SENDMAIL -bp";

    if ($cmd !~ /sendmail/) { $cmd = "$SENDMAIL"; }

    #open(STDERR, ">&STDOUT")		|| die "Can't dup stdout";
    open(CMD, "$cmd |")	|| return (0);  # dont exit if cmd not found

    # qmail code by  thomas.erskine-dated-7c0d9090a295911d@crc.ca
    if ($cmd =~ /qmail/) {
      while (<CMD>) {
	$debug && print STDERR "(debug)mailstat: $_" ;
	if (/^\S/) { ++$qsize; }
	else {
	  chop;
	  if (/(local|remote)\s+(\S+)/) {
	    if ($1 eq 'local') { $tolocal++; }
	    elsif ($1 eq 'remote') { $toremote++; }
	    $addr = $2;
	  }
	  else { next; }	# shouldn't happen, but let's not die
	  ($user,$host) = split(/[@]/,$addr);
	  $host =~ y/A-Z/a-z/; 	# canonicalize lower
	  $host =~ tr/<>//d;	# watch angle brackets
	  $host =~ s/\s*\(.*$//;	# strip off comments
	  $NTo{$host} += 1;
	}
      }
      print "MailQ2Local $tolocal Addrs\n";	# unique to qmail
      print "MailQ2Remote $toremote Addrs\n";
      
    }	# if qmail
    else	# sendmail
    {
      while (<CMD>) {
	$debug && print STDERR "(debug)mailstat: $_" ;
	next if (/^[A-Z]+\d+\s+/); # From: this 
	next if (/deferred|warning|timeout/i);
	if (/Mail Queue/) {	# Mail Queue (152 requests)
	  ($x,$y,$paren,$qsize) = split(/[ \(]/);
	} elsif (/[@]/) {
	  chop;
	  ($user,$host) = split(/[@]/);
	  $host =~ y/A-Z/a-z/; 	# canonicalize lower
	  $host =~ s/>//g;	# watch angle brackets
	  $host =~ s/^\s*//;	# remove leading whitespace
	  $NTo{$host} += 1;
	}
      }
    }	# if sendmail

    close (CMD);
    
    # to sort the associative array by queue length
    local(@hosts) = keys %NTo;
    @hosts = sort byqlen @hosts;	# high values at the top (0,1...)

    print "MailQLocal $qsize Length\n";
    if (@hosts != 0) {
      for (@hosts[0..4])	 # printing only top 5 hosts
      {
	if ($_) { printf "MailQDest %d Length %s\n", $NTo{$_}, $_ ; }
      }
    }
    else	 # blank out the previous list
    {
      printf "MailQDest %d Length %s\n", 0, "";
    }
    #$NTo{"Local_QLen"} = $qsize;	# entire queue length on localhost
}

## byqlen
#
# This is a simple function which compares two element of arry $NTo
# and returns 0, 1, or -1. (equal, greater or less than).
# It is required by 'sort' in sub mailstat()
sub byqlen  {

    $NTo{$b} <=> $NTo{$a}
}

###
### main
###    

if ($debug) {
    print STDERR "(debug) MonitorList= @monitorlist\n" ;
    print STDERR "(debug) Outputfile= $dfile\n";
}

&osinit;			# OS specific initialization

require $osfile;                # include OS specific file
eval "require 'local'";		# any local site-specific special file

&standalone ;			# create telnet_daemon before calling this ??
#defined (&create_telnet_daemon) &&  &create_telnet_daemon ;
&create_telnet_daemon;

$passno = 1;			# keep track of pass number in loop
$tmpfile = "$dfile" . ".tmp" ;	# create temporary output file
$chek_dpass = int(3600 /$sleeptime);	# check telnet daemon every hour
$restart_dpass = int(24*3600 /$sleeptime);	#restart every 24 hours

while (1)
{
    $stime = time;

    open (DFILE, "> $tmpfile") ; 
    select(DFILE);			# select default output file

    print "TIME $stime secs\n";	        # needed in the output
    foreach $s (@monitorlist) { &$s ;}	# call the subroutines.
    close (DFILE) ;
    $debug && print STDOUT "Moving $tmpfile to $dfile\n";
    `mv $tmpfile $dfile` ;

    # Check the 'telnet_daemon' every chek_dpass passes.
    if ((++$passno % $chek_dpass) == 0) {
	defined (&create_telnet_daemon) && &create_telnet_daemon;
    }

    $deltatime = time - $stime;              # time to do tests
    $debug  && print STDERR "(dbg) sleep for= $sleeptime - $deltatime\n";
    if ($sleeptime > $deltatime) { sleep($sleeptime - $deltatime) ;}

    ## restart to avoid the memory leaks.
    if (($passno % $restart_dpass) == 0) {
	$passno = 0;
	if ( -x $0 ) { $childpid && kill ('TERM', $childpid); sleep 5;
		       $childpid && kill ('TERM', $childpid); sleep 5;
		       exec $0 ; # restart
	}
    }
}	# end while(1)

####################  END OF FILE hostmon-client.main   ##################
