#!/usr/bin/perl
#vim:ts=4
###########################################################################
#
# check_summary
# Steve Shipway, 2005.  Calculate a value based on a function of other status
# values.
#
# check_summary [ -f file | -e 'script' ] [-z] [-x] [-s] [-d] [-S status.log]
#
###########################################################################

use strict;
use vars qw/$opt_d $opt_f $opt_e $opt_x $opt_z $opt_s $opt_S $opt_h/;
use Getopt::Std;
use Text::ParseWords;

my($DEBUG) = 0;

my($STATUSLOG) = '/u02/nagios/log/status.log';
my($SCRIPT)    = '@cluster( /% )'; # default script: all servers summary

my($STATUS ) = 3;
my($MESSAGE) = "Null script";

my(%hosts) = ();
my(%services) = ();
my($usedowntime) = 1; # ignore things in downtime
my($usedisabled) = 1; # ignore things with notify disabled
my($usesoft    ) = 1; # use things in soft states

my(%map) = ( 'OK'=>0, 'WARN'=>1, 'WARNING'=>1, 'CRIT'=>2, 'CRITICAL'=>2,
	'UNKNOWN'=>3, 'UNK'=>3, 'PENDING'=>4,
	'UP'=>0, 'DOWN'=>2, 'UNREACHABLE'=>3,
	0=>0, 1=>1, 2=>2, 3=>3 );

###########################################################################
sub usage {
	print "check_summary [-x][-s][-d][-h][-z] [-f file | -e 'script'] [-S status.log]\n\n";
	print "-x : Do not ignore hosts/services with notifications disabled\n";
	print "-z : Do not ignore hosts/services in scheduled downtime\n";
	print "-s : Ignore services in a soft alert state\n";
	print "-h : Show this help text\n";
	print "-d : Enable debug mode. Can help track down script errors.\n";
	print "-S : Give location of status.log file (default $STATUSLOG)\n";
	print "\n";
	print "Script syntax:\n";
	print "Script must resolve to a single value, else it is an error.\n";
	print "Script can contain functions, hosts, services, and literals.\n";
	print "You must quote spaces, or escape with a backstroke.\n";
	print "Literals are of the form:\n     status:text\nfor status of OK,WARN,CRIT,UNKNOWN\n";
	print "Hosts are of the form:\n     /hostname\nwhere hostname can be a regexp preceeded by a %\n";
	print "Services are of the form:\n     /hostname/servicedesc\nwhere either or both of hostname and servicedesc can be regexps preceeded by %\n";
	print "Functions are of the form:\n     \@functionname ( argumentlist )\nwhere argumentlist is a list made up of one or more literals, hosts, services,\nor functions.  The functionname can be any of:\n";
	print "  \@max : Take the maximum value of the statuses in the list\n";
	print "  \@min : Take the minimum value of the statuses in the list\n";
	print "  \@cluster : OK if all are OK, critical if all are critical, else warning\n";
	print "  \@avg : Mean average status, rounded to nearest\n";
	print "  \@median : Median status\n";
	print "  \@map : Change text to the text of item with the same status as the first\n          item in the list.  Status is the status of the first item.  Use this\n          to rewrite the text after calculations.\n";
	print "\n";
	print "Example script:\n";
	print ' @map ( @max ( @cluster( /%webserver ) @cluster( /%dbserver ) /router ) "OK:All fine" "WARN:Performance degraded" "CRIT:Service unavailable" )';
	print "\n\n";
	print "check_summary created by Steve Shipway http://www.steveshipway.org/software\n";
	exit(3);
}
###########################################################################
sub error($) {
	print $_[0]."\n";
	exit 3;
}

sub readstatus() {
	open LOG, "<$STATUSLOG" or error("Unable to open status log");
	while ( <LOG> ) {
		if( /^............ HOST;([^;]+);([^;]+);\d+;\d+;\d+;\d+;\d+;\d+;\d+;\d+;(\d);\d+;\d+;\d+;\d+;.+;(\d+);\d+;\d+;(.*)$/ ) {
			$hosts{$1} = { state=>$map{$2}, enabled=>$3, downtime=>$4, output=>$5 };
			print "Added host $1: ".$map{$2}." $5\n" if($DEBUG);
			next;
		}
		if( /^............ SERVICE;([^;]+);([^;]+);([^;]+);(\d+)\/(\d+);[^;]+;\d+;\d+;[^;]+;\d+;\d+;\d+;\d+;\d+;[^;]+;\d+;\d+;\d+;\d+;\d+;\d+;(\d);\d+;\d+;\d+;\d+;[^;]+;(\d+);\d+;\d+;\d+;(.*)$/ ) {
			$services{$1}{$2} = { state=>$map{$3}, enabled=>$6, downtime=>$7, output=>$8, try=>$4 , count=>$5, soft=>0  };
			$services{$1}{$2}{soft} = 1 if( $3 ne 'OK' and $4<$5 );
			next;
		}
	}
	close LOG;
	# Should we also parse the hostgroups file here?
}

##############################################################################
sub printlist {
	my($rv);
	$rv = "";
	foreach ( @_ ) { if( ref $_ ) { $rv.=$_->[0]; } else { $rv.=$_; };
		$rv .=", "; }
	return $rv;
}
##############################################################################
my(@script) = ();
sub hwildcard {
	my($p) = $_[0];
	my(@rv) = ();

	return ($p) if( $p !~ /^[:\%]/ ); # not a pattern
	
	# hostgroups *TO BE DONE*

	# wildcards
	$p =~ s/^\%//;  $p = "." if(!$p);
print "Host wildcard [$p]\n" if($DEBUG);
	foreach ( keys %hosts ) { push @rv,$_ if( /$p/ ); }
#print "Return: ".(join ",",@rv)."\n" if($DEBUG);
	return (@rv);
}
sub swildcard {
	my($h,$p) = @_;
	my(@rv) = ();

	return ($p) if( $p !~ /^\%/ ); # not a pattern
	$p =~ s/^\%//; $p = "." if(!$p);
print "Service wildcard [$p] for host [$h]\n" if($DEBUG);
	foreach ( keys %{$services{$h}} ) { 
		print "Checking $_\n" if($DEBUG);
		if( $_=~/$p/ ){ push @rv,$_ ; print "OK\n" if($DEBUG); }
	}
	return (@rv);
}
sub expand {
	my($h,$s);
	my($hp,$sp);
	my(@newscript) = ();
	foreach ( @script ) {
		next if( /^[\s,]*$/ ); # ignore blank tokens
		if( /^['"]/ ) { s/^['"]//; s/['"]$//; } else { 
			s/^[\s,]+//; s/[\s,]+$//; }
		if( /^\)/ ) {
			while( s/\)// ) { push @newscript, ")"; }
			next;
		}
		if( /^[@\(]/ ) { push @newscript, $_; next; } # function
		if( /^\/(.*)\/(.*)/ ) {
			# host and service
			($hp,$sp) = ($1,$2);
print "Expanding service /$hp/$sp\n" if($DEBUG);
			foreach $h ( hwildcard( $hp ) ) {
				if( ! defined $hosts{$h} ) { error "Unrecognised host $h"; }
			foreach $s ( swildcard( $h, $sp ) ) {
				if( ! defined $services{$h}{$s} ) { error "Unrecognised service $h/$s"; }
				push @newscript, [ $services{$h}{$s}{state}, $services{$h}{$s}{output} ]
					unless( ($usedowntime and $services{$h}{$s}{downtime})
						or ($usedisabled and !$services{$h}{$s}{enabled}) 
						or (!$usesoft and $services{$h}{$s}{soft})
					);
			}
			}
			next;
		}
		if( /^\/(.*)/ ) {
			# host 
			$hp = $1;
print "Expanding host /$hp\n" if($DEBUG);
			foreach $h ( hwildcard( $hp ) ) {
				if( ! defined $hosts{$h} ) { error "Unrecognised host $h"; }
				push @newscript, [ $hosts{$h}{state}, $hosts{$h}{output} ]
					unless( ($usedowntime and $hosts{$h}{downtime})
						or ($usedisabled and !$hosts{$h}{enabled}) );
			}
			next;
		}
		# if we are here, then its a literal
		if( /(.*):(.*)/ ) {
			if(! defined $map{$1} ) { error "Bad state $1"; }
			push @newscript, [ $map{$1}, $2 ];
			next;
		}
		error "Cannot parse script ($_)";
		last;
	}

	@script = @newscript;
}
sub dofunction($) { # process the function at index
	my($idx) = $_[0];
	my(@params) = ();
	my($lstart, $lend);
	my(@rv) = ();
	my($fn, $rv, $output);
	my($t,$n,$min,$max,$err);

	# first, find the end of the parameter list.
	if($script[$idx] !~ /^@/) { error "Not a function: ".$script[$idx]; }
	if($script[$idx+1] !~ /^\(/) { error "Expected bracket: ".$script[$idx]; }
	$lstart = $idx+2;
	$lend = $lstart;
	while( $lend <= $#script and $script[$lend]!~/^\)/) { $lend++; }
	if( $lend > $#script ) { error "Expected closing bracket missing."; }
	$lend--;

	@params = @script[$lstart..$lend] if( $lstart <= $lend );
	$fn = $script[$idx];
if($DEBUG) {
print "Script: ".(printlist @script)."\n";
print "IDX: $idx  param=$lstart .. $lend\n";
print "Function: $fn\n";
print "Params: ".(printlist @params)."\n";
}
	# check parameters
	foreach (@params) {
		if( ! ref $_ ) { error "Unexpected constant $_"; }
	}

	($rv, $output) = ( $map{UNKNOWN}, "Cannot determine result" );
	if( $fn =~ /\@max/i ) {
		($rv,$output) = (0,"");
		foreach ( @params ) {
			if( $_->[0] >= $rv ) { 	
				$rv = $_->[0]; 
				$output.="<BR>" if($output);
				$output .= $_->[1]; 
			}
		}
		$output = "All parameters OK" if(!$output);
	} elsif( $fn =~ /\@min/i ) {
		($rv,$output) = (3,"");
		foreach ( @params ) {
			if( $_->[0] <= $rv ) { 
				$rv = $_->[0]; 
				$output .= "<BR>" if($output);
				$output .= $_->[1]; 
			}
		}
		$output = "All parameters OK" if(!$rv);
		$output = "All values unknown" if(!$output);
	} elsif( $fn =~ /\@av(era)?g/i ) {
		($t,$n)=(0,0);
		foreach( @params ) { $t += $_->[0]; $n++; }
		if( $n < 1 ) {
			($rv,$output) = (3,"No items in list");
		} else {
			$t = int( $t/$n + 0.500001 );
			($rv, $output) = ( $t, "Average status" );
		}
	} elsif( $fn =~ /\@median/i ) {
		($rv,$output)=(3,"Function not yet written");
	} elsif( $fn =~ /\@clus/i ) {
		($rv,$output) = (0,"All items OK");
		$min = 3; $max = 0;
		$err = "";
		foreach ( @params ) {
			if( $_->[0] < $min ) { $min = $_->[0]; }
			if( $_->[0] > $max ) { $max = $_->[0]; }
			if( $_->[0] > 0 ) {
				$err .= "<BR>" if($err);
				$err .= $_->[1];
			}
		}
		if( $min == 3 ) {
			($rv,$output) = (3, "All status unknown");
		} elsif( $min == 2 ) {
			($rv,$output) = (2, "System Critial:<BR>$err");
		} elsif( $max > 0 ) {
			($rv,$output) = (1, "System performance degraded:<BR>$err");
		}
	} elsif( $fn =~ /\@map/i ) {
		# change message of first to match message of subsequent item(s) with 
		# the same status.
		$err = "";
		if(!@params) {
			($rv,$output) = (3, "No parameters given");
		} else {
			($rv, $output) = @{$params[0]}; 
			shift @params;
			foreach ( @params ) {
				if( $_->[0] == $rv ) {
					$err .= "<BR>" if($err);
					$err .= $_->[1];
				}
			}
			$output = $err if($err);
		}
	} else { error "Unknown function $fn"; }

	# Create the new script array
	@rv = @script[0..($idx-1)] if($idx>0);
	push @rv, [$rv,$output];
	push @rv, @script[($lend+2)..$#script] if(($lend+1)<$#script);
	@script = @rv;
if($DEBUG) {
print "AFTER Script: ".(printlist @script)."\n";
}
}

sub parsescript {
	my($done) = 0;
	my($idx);

	@script = quotewords( '[\s,()]+', 'delimiters', $SCRIPT );
	expand;
	do {
		$done = 1;
		$idx = $#script; # evaluate functions right to left
		while( $idx >= 0 ) {
			if( !ref $script[$idx] and $script[$idx]=~/^@/ ) {
				$done = 0; # keep going
				last;
			}
			$idx--;
		}
		dofunction($idx) if(!$done);
	} while(!$done);
	if( $#script == 0 ) {
		($STATUS,$MESSAGE) = @{$script[0]};
	} else {
		($STATUS,$MESSAGE) = 
			( $map{UNKNOWN}, "Script did not reduce to one value" );
	}
}
##############################################################################

getopts( "df:e:xzshS:" );
usage if($opt_h);
$usedowntime = 0 if($opt_z);
$usedisabled = 0 if($opt_x);
$usesoft     = 0 if($opt_s);
$STATUSLOG = $opt_S if($opt_S);
$SCRIPT    = $opt_e if($opt_e);
$DEBUG = 1 if($opt_d);
if($opt_f) {
	error("Cannot read script file") if(! -r $opt_f);
	open SF, "<$opt_f" or error("$opt_f: $!");
	$SCRIPT = "";
	while (<SF>) { s/#.*$//; $SCRIPT .= " $_"; }
	close SF;
}

readstatus;
parsescript;

print "$MESSAGE\n";
print "status $STATUS\n" if($DEBUG);
exit $STATUS;
