#!/usr/bin/perl
#
## Name:         check_lbnodes.pl - load balance node monitoring program
#
## Author:      Tom Northcutt
## Synopsis:    Checks status of web server load balance nodes, modifies 
#               list of nodes and restarts accordingly.
## Description:
#               This script is designed to be run from a cron job 
#               in order to check load balance nodes status (based on homepage
#               access) and modify the squid load balance list in order to
#               reflect running nodes accordingly.  Additional checks for 
#               homepage modification and backup status are also incorporated.
#               A temporary file is used to keep track of the last 
#               modification of the homepage in question. 
#
# user defined section ###

#file containing list of possible nodes
my $pnodes = "/usr/local/squid/etc/servers-list.txt";

#file containing list of currently running nodes
my $cnodes = "/usr/local/squid/etc/servers-up.dat";

#name of master node 
my $master = "server1";

#how to start/stop squid
my $squid_start = "/etc/rc.d/init.d/squid start";
my $squid_stop = "/etc/rc.d/init.d/squid stop";

#how to restart load balancer (just kill it, it respawns from squid)
my $redir_restart = q[ for i in `ps -ef |grep redir|grep perl| awk '{print $2}'`; do sudo skill -9 $i ; done] ;

# file to log sent mail messages to
my $maillog = "/etc/squid/check_homepage_mail.log";

# email address of person to receive homepage change warnings
# $admin_email = q"root@localhost";
my $admin_email = q(sysadmin@mailserver.localdomain);

# Files containing the previous primary homepage and new page. 
# Used to check for changes.
my $cached_page = "/tmp/cached_homepage.txt";
my $new_page = "/tmp/new_homepage.txt";

# sendmail
my $sendmail = "/usr/lib/sendmail -t";


# END user defined section ###

use Sys::Syslog qw(:DEFAULT setlogsock);
setlogsock "UNIX";

use File::Copy;
use LWP::UserAgent;
my $ua = new LWP::UserAgent;
$ua->agent("AgentName/0.1 " . $ua->agent);
$date = `date`;

# Load in list of potential nodes:
open NODES, "< $pnodes";
my @nodesraw = <NODES>;
close NODES;
  #exclude commented lines
my @nodeslist;
foreach $i (@nodesraw){
    ($i =~ /^#/) ? next : push (@nodeslist,$i);
}

#load in list of current nodes:
open NODESOLD, "< $cnodes";
my @nodesold = <NODESOLD>;
close NODESOLD;

#debugging
my $debug;
if ($ARGV[0] =~ /debug/){$debug = 1 || 0};


##MAIN ###
my @nodesnew;
foreach $i (@nodeslist){    
    chop $i;
    #check homepage on each node, modify if necessary
    my $req1 = new HTTP::Request GET => "http://$i.localdomain/index.html";
    $res1 = $ua->request($req1);
    my $res_success1 = $res1->is_success();
    if ($res_success1){push(@nodesnew, $i)};

    ##check master node homepage for changes
    #write new web page to a file        
    if ($i eq "$master"){
	check_hp(\$req1);
    }
}

##check nodes
#if full node list nodes is the total list of nodes
if (@nodesnew == @nodeslist &&  @nodesnew == @nodesold){
    print "all full node list nodes are up\n" if $debug;
}elsif(@nodesold == @nodesnew){
    print "node(s) still down\nfull node list:\n@nodeslist\n\ncurrently up:\n@nodesnew\n" if $debug;
    mail_admin("node(s) still down","node(s) still down\nfull node list:\n@nodeslist\n\ncurrently up:\n@nodesnew\nprevious:\n@nodesold");
}elsif(@nodesnew > @nodesold){
    print "a node came back up\nfull node list:\n@nodeslist\n\ncurrently up:\n@nodesnew\n\nprevious:\n@nodesold\n" if $debug;
    update_nodes(\@nodesnew, 1);
    mail_admin("node(s) came back up","a node came back up\nfull node list:\n@nodeslist\n\ncurrently up:\n@nodesnew\n");
}elsif(@nodesnew < @nodesold){
    print "a node went down\n\nfull node list:\n@nodeslist\n\ncurrently up:\n@nodesnew\n\nprevious:\n@nodesold\n" if $debug;
    update_nodes(\@nodesnew, 1);
    mail_admin("a node went down","a node went down\nfull node list:\n@nodeslist\n\ncurrently up:\n@nodesnew\n");

}else{
    print "didn't catch this case\n";
}


##ENDMAIN ###



## SUBROUTINES ###


##(array_diff ARRAY1, ARRAY2)
# compare two lists and return the differences
# that are in ARRAY1 but not in ARRAY2, rerun
# function with ARRAY2, ARRAY1 for other differences
sub array_diff{
    my ($list1, $list2) = @_;
    
    my @diff;
    foreach $i (@$list1){
	my $match = 0;
	foreach $j (@$list2){
	    if ("$i" eq "$j"){$match = 1;next;}
	}
	if($match == 0){
	    push(@diff, "$i");
	}
    }
    return @diff;
} 

##(get_pid PROCESS_NAME)
# Given a process name, this routine returns the process id.
# If the process isn't running, returns 0;
sub get_pid {
  my ($process_name) = @_;

  my @ps_info = `/bin/ps -C $process_name`;
  $null = shift(@ps_info); #first line is ps header info
  my $ps_elem = @ps_info; #num elements in @it #icky perl
  
  my $ps_line = $ps_info[--$ps_elem];
  $ps_line =~ s/^ +//; #ps output varies, remove all leading whitespace

  #in case server forked, process last line 
  my ($pid, @null) = split(/ /, $ps_line);
  return $pid if $pid;
  return 0;
}


## (mail_admin SUBJECT, MESSAGE)
# Send a message to the system administrator leting them know of
# special circumstances. 
sub mail_admin {
  my ($subject,$message) = @_;
  
  open(MAIL, "| $sendmail")
    or die "PROXY-ALERT: Can't fork for sendmail: $!\n";
  
  print MAIL <<"ENDMAIL";
From: $admin_email
To: $admin_email
Subject: $subject

$message
ENDMAIL
  
   close(MAIL);		# close the pipe

  #log this event
  open (MAILLOG, ">> $maillog") 
    or die "PROXY-ALERT: Couldn't open the maillog: $maillog : $!\n";
  print MAILLOG "\n" . "-"x70 . "\n";
  print MAILLOG "$subject\n$message\n";
  close(MAILLOG);
}


##(syslog_msg MESSAGE, PRIORITY)
# Send syslog(3) a message to enter into the system logs appropriately.
# PRIORITY can be one of: debug, info, notice, warning, err, crit, alert
# or emerg (in reverse order of precedence, INFO is default).  
sub syslog_msg{
  my $message = shift;
  my $priority = shift || "info";

  openlog('check_homepage', 'cons,pid', 'user');
  syslog("$priority", "$message");
  closelog();
  return 1;
}


##(check_hp) 
# Check for homepage changes and report info to administrator
#
sub check_hp{
    my $req = shift;
    my $req1 = $$req;

    print "checking the homepage for $master\n" if $debug;
    open (NPAGE, "> $new_page")
	or mail_admin("PROXY-ERROR: can't open $new_page",
		      "$date - unable to open the file \
                           $new_page for writing")
	and die "SQUID-ALERT: can't open $new_page: $!\n";
	
    print NPAGE $res1->content;
    close NPAGE;

    #check for cached page                            
    if  (! -e "$cached_page"){
	mail_admin("PROXY-ERROR: no cached page: $cached_page",
		   "$date - $cached_page doesn't exist");
	
	copy("$new_page", "$cached_page")
	  or die "PROXY-ALERT: couldn't copy $new_page to $cached_page: $!\n";

	die "PROXY-ALERT: $cached_page doesn't exist, created new.: $!\n";
    }

    #compare new and cached pages                     
    $difference = `diff $cached_page $new_page`;
    if($difference){

	mail_admin("PROXY-ALERT: - webserver homepage has changed",
		   "$date The following changes were detected:\n$difference");
	
	syslog_msg("webserver homepage has changed, see $maillog");
	
	copy("$new_page", "$cached_page")
	    or die "PROXY-ALERT: couldn't copy $new_page to$cached_page: $!\n" ;
    }
}

##(update_nodes NODE_ARRAY, RESTART)                                    
# Generates a new node list file, then optionally restarts either squid
# server entirely or just the redir script.  RESTART option takes a
# value of 0,1,2.  0) does not restart a server, 1) restarts redir.pl
# only, 2) restarts squid entirely.

sub update_nodes{
    my $upnodes = shift;
    my $restart = shift || 0;

    #write new nodelist to file
    open NODESOLD, "> $cnodes";
    print "updating nodes:\n" if $debug;
    foreach $i (@$upnodes){
	print NODESOLD "$i\n";
    }
    close NODESOLD;

    if ($restart == 1){
	print "restarting load balance scripts\n" if $debug;
	`$redir_restart`;
    }elsif($restart == 2){
	print "restarting squid\n" if $debug;
	`$squid_stop`;
	`$squid_start`;
    }
}



1;

=head1 NAME

check_lbnodes.pl - proxy/load balance monitoring program

=head1 AUTHOR

Tom Northcutt


=head1 SYNOPSIS

Checks status of web server and load balance nodes, removes unavailable nodes from the balancer, and reports on homepage changes


=head1 DESCRIPTION

This script is designed to be run from a cron job in order to check
load balancing node status (based on homepage access) remove
unavailable nodes from the system accordingly.  Additional checks for
homepage modification and backup status are also incorporated.  A
temporary file is used to keep track of the last modification of the
homepage in question.  


=head1 SUBROUTINES

=item B<sub get_pid(PROCESS_NAME)>

 Given a process name, this routine returns the process id.
 If the process isn't running, returns 0;

=item B<sub mail_admin(SUBJECT, MESSAGE)>

 Send a message to the system administrator leting them know of
 special circumstances. 

=item B<sub syslog_msg(MESSAGE, PRIORITY)>

 Send syslog(3) a message to enter into the system logs appropriately.
 PRIORITY can be one of: debug, info, notice, warning, err, crit, alert
 or emerg (in reverse order of precedence, INFO is default).  

=item B<sub update_nodes(NODE_ARRAY, RESTART)>

 Generates a new node list file, then optionally restarts either squid
 server entirely or just the redir script.  RESTART option takes
 a value of 0,1,2.  0) does not restart a server, 1) restarts redir.pl
 only, 2) restarts squid entirely.

=item B<sub check_hp(REQUEST_OBJECT)>              

 Check for homepage changes and report info to administrator.  Takes a 
 lwp request object as input.


=cut


