#!/usr/bin/perl -w
#
# check_generic - nagios plugin
#
# Copyright (c) 2007 Matthias Flacke (matthias.flacke at gmx.de)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
#
# TODO
#
#
# $Id$
#
#nagios: -epn
#
use strict;
use Getopt::Long qw(:config no_ignore_case bundling);
BEGIN { eval("use Time::HiRes qw(time)") }
use lib "/usr/local/monitoring/libexec";
use vars qw(
$MYSELF %opt %cmd %rc $command $returncode 
$VERSION $OK $WARNING $CRITICAL $UNKNOWN 
$DETAIL_LIST $DETAIL_RC $DETAIL_STDERR $DETAIL_PERFORMANCE
);

#-------------------------------------------------------------------------------
#--- vars ----------------------------------------------------------------------
#-------------------------------------------------------------------------------
$MYSELF="check_generic";
$VERSION='$Revision$ $Date$ $Author$';
#
#--- RC defines
$OK=0;
$WARNING=1;
$CRITICAL=2;
$UNKNOWN=3;
#
#--- report defines
$DETAIL_LIST=1;
$DETAIL_RC=2;
$DETAIL_STDERR=4;
$DETAIL_PERFORMANCE=8;
#
#--- vars
%cmd=(
	matchlist => [],
);
%rc=(
	label	=> { $OK => "OK", $WARNING => "WARNING", $CRITICAL => "CRITICAL", $UNKNOWN => "UNKNOWN", },
	number	=> { "OK" => $OK, "WARNING" => $WARNING, "CRITICAL" => $CRITICAL, "UNKNOWN" => $UNKNOWN, 
                     "ok" => $OK, "warning" => $WARNING, "critical" => $CRITICAL, "unknown" => $UNKNOWN,
                      "o" => $OK,  "w"      => $WARNING, "c"        => $CRITICAL, "u"       => $UNKNOWN, },
	s2r	=> { 0 => $OK, 2 => $WARNING, 3 => $CRITICAL, 1 => $UNKNOWN, },
	r2s	=> { $OK => 0, $WARNING => 2, $CRITICAL => 3, $UNKNOWN => 1, },
	complement => { $OK => $CRITICAL, $WARNING => $OK, $CRITICAL => $OK, $UNKNOWN => $OK, },
	minimum => { $OK => 0, $WARNING => 1, $CRITICAL => 1, $UNKNOWN => 1, },
	maximum => { $OK => 0, $WARNING => 1, $CRITICAL => 1, $UNKNOWN => 1, },
	list	=> { $OK => [],$WARNING => [],$CRITICAL => [],$UNKNOWN => [], },
	textsev	=> ["ok","unknown","warning","critical"],
	top	=> $OK,
	error	=> [ ],
	starttime => 0.0,
	endtime => 0.0,
);

my %opt=(
	"configfile"	=> "",
	"ignore_rc"	=> 0,
	"libexec"	=> "/usr/local/nagios/libexec",
	"maxage"	=> 24, #hours
	"name"		=> "CHANGEME",
	"performance"	=> undef,
	"report"	=> 15,
	"string"	=> {},
	"timeout"	=> 10,
	"tmpdir"	=> "/usr/tmp/check_generic",
	"type"		=> "scalar",
	"verbose"	=> 0,
	#"ok"		=> "0:0",
	#"warning"	=> "1:1",
	#"critical"	=> "1:1",
	#"unknown"	=> "1:1",
);
	
#-------------------------------------------------------------------------------
#--- subs ----------------------------------------------------------------------
#-------------------------------------------------------------------------------

sub process_parameters {

	if (! GetOptions(
		"c|critical=s"	=> \$opt{critical},
		"d|tmpdir=s"	=> \$opt{tmpdir},
		"e|execute=s"	=> \$opt{execute},
		"h|help"	=> \$opt{help},
		"i|ignore_rc"	=> \$opt{ignore_rc},
		"n|name=s"	=> \$opt{name},
		"o|ok=s"	=> \$opt{ok},
		"p|performance=s" => \$opt{performance},
		"r|report:i"	=> \$opt{report},
		"f|false=s"	=> \$opt{false},
		"t|timeout=i"	=> \$opt{timeout},
		"u|unknown=s"	=> \$opt{unknown},
		"v|verbose+"	=> \$opt{verbose},
		"V|version"	=> \$opt{version},
		"w|warning=s"	=> \$opt{warning},
		"y|type=s"	=> \$opt{type},
		)
	) {
		short_usage();
        	return $UNKNOWN;
	}

	if ($opt{version}) {
		print "$MYSELF: v$VERSION\n";
        	return $UNKNOWN;
	}
	if ($opt{help}) {
		short_usage();
        	long_usage();
        	return $UNKNOWN;
	}
	if (!$opt{execute}) {
        	print "$MYSELF error: no commandline specified\n";
		short_usage();
        	return $UNKNOWN;
	} else {
		$cmd{command}=$opt{execute};
		$cmd{name}=$opt{execute};
	}

	if (!$opt{ok} && !$opt{warning} && !$opt{critical} && !$opt{unknown}) {
        	print "$MYSELF error: no evaluation expression specified\n";
		short_usage();
        	return $UNKNOWN;
	} else {
		foreach my $state (reverse @{$rc{textsev}}) {
			if (defined($opt{$state})) {
				$opt{string}=is_string_cmp($opt{$state});
				debug(3,"process_parameters: state $state defined:$opt{$state}, text evaluation:$opt{string}");
			} else {
				debug(3,"process_parameters: state $state not defined");
			}
		}
	}
	if ($opt{false} && (
		$opt{false} ne "o" && $opt{false} ne "OK" &&
		$opt{false} ne "u" && $opt{false} ne "UNKNOWN" &&
		$opt{false} ne "w" && $opt{false} ne "WARNING" &&
		$opt{false} ne "c" && $opt{false} ne "CRITICAL")) {
		print "$MYSELF error: unknown false $opt{false}, should be u|UNKNOWN w|WARNING c|critical o|OK\n";
                short_usage();
                return $UNKNOWN;
	}
	while (!defined($opt{false})) {
		foreach my $state (reverse @{$rc{textsev}}) {
			#print "DEBUG:state:$state opt{state}:$opt{$state}\n";
			if (defined($opt{$state})) {
				$opt{false}=$rc{label}{$rc{complement}{$rc{number}{$state}}};
				last;
			}
		}
	}
	if (! -d $opt{tmpdir}) {
		mkdir $opt{tmpdir} || debug(0,"mkdir $opt{tmpdir} failed:$!");
		if (! -d $opt{tmpdir}) {
			return $UNKNOWN if (defined($opt{delta}));
		}
	}
	if ($opt{type} eq "delta") {
		#--- create tmpfile name from 1. tag 2. command 3. expressions
		$opt{tmpfile}=$opt{name} . "_" . $opt{execute};
		foreach my $state (reverse @{$rc{textsev}}) {
			$opt{tmpfile} .= '_' . $opt{$state} if (defined($opt{$state}));
		}
		$opt{tmpfile}=~s/\W/_/g;
		$opt{tmpfile}="$MYSELF.$opt{tmpfile}.tmp";
		debug(2, "process_parameter: tmpfile:$opt{tmpfile}");
		
		#--- read content of old tmpfile if available
		my $content=readfile("$opt{tmpdir}/$opt{tmpfile}");
		if ($content ne "") {
			chomp $content;
			($cmd{old_timestamp},$cmd{old_output})=split(/\s+/,$content);
			debug(2, "process_parameter: old_timestamp:$cmd{old_timestamp} old_output:$cmd{old_output}");
		}
		#--- remove old files in tmpdir older than one day
		&garbage_collection($opt{maxage});
	}
	debug(2, "verbosity:$opt{verbose}");
	return $OK;
}

sub short_usage {
print <<SHORTEOF;

$MYSELF -e <cmdline> -o|u|w|c <expression> [-f false_state] [-n name] [-t timeout] [-r level]
$MYSELF [-h | --help]
$MYSELF [-V | --version]
SHORTEOF
}

sub long_usage {
print <<LONGEOF;

Options:
-e, --execute <cmdline>
   string which contains commands to be executed
   (can be a complete filter chain)
-u|w|c|o, --unknown,warning,critical,ok <expression>
   operator is perl operators, e.g.
      '= n'	- numerically equal
      '< n'	- numerically equal
      '> n'	- numerically equal
      'eq s'	- string equal
      'ne s'	- string non equal
      '=~/s/	- pattern matching
   default: CRITICAL
-f, --false [u|UNKNOWN|w|WARNING|c|CRITICAL|o|OK]
   which state the plugin should become if the expression is false
   default: complement of state
-y, --type [SCALAR,ARRAY,DELTA]
   type of data value
-i, --ignore_rc
	normally the return code of the command executed is taken into account
   use this option to explicitly ignore it, default: $opt{ignore_rc}
-n, --name
   plugin name (shown in output), default: $opt{name}
-t, --timeout
   timeout for one command, default: $opt{timeout}
-d, --tmpdir
   specify directory for tmpfiles, default: $opt{tmpdir}
   (garbage collection for files \'${MYSELF}*\' removes files older than )
-v, --verbose
   increase verbosity (can be called multiple), default: $opt{verbose}
-h, --help
   print detailed help screen
-V, --version
   print version information
LONGEOF

#-s, --state [u|UNKNOWN|w|WARNING|c|CRITICAL|o|OK]
#   which state the plugin should become if the expression is true
#-r, --report <level>
#  specify level of details in output (level is binary coded, just add all options)
#   default: $opt{report}
#      1: mention service names in plugin_output, e.g.
#         "24 plugins checked, 1 critical (http), 0 warning, 0 unknown, 23 ok"
#      2: show STATE in front of each line of plugin output, e.g.
#         "[16] OK system_ssh - SSH OK - OpenSSH_4.4 (protocol 1.99)"
#      4: show STDERR (if any) in each line of plugin output
#      8: show performance data
}

#---
#--- debug output routine
#---
sub debug {
	my ($level,$message)=@_;
	print "$message\n" if ($level <= $opt{verbose});
}

#---
#--- read file and return its contents
#---
sub readfile {
        my ($filename)=@_;
        open(FILE,$filename) || add_error("readfile: error opening $filename:$!") && return "";
        my @lines=<FILE>;
        close(FILE);
        return join("", @lines);
}

#---
#--- write to file
#---
sub writefile {
        my ($filename, $content)=@_;
        open(FILE,">$filename") || add_error("writefile: error opening $filename:$!") && return 0;
        print FILE $content;
        close(FILE);
	return -s $filename;
}

#---
#--- check if expression is string evaluation
#---
sub is_string_cmp {
	my $expression=shift;
	my %stringop=(' lt ',' gt ',' le ',' ge ','\=\~','\!\~',' eq ',' ne ');
	foreach my $key (keys(%stringop)) {
		return 1 if ($expression=~/^\s*$key/);
	}
	return 0;
}
#
#---
sub match_env {
        my ($string, $expr, $wanted_length)=@_;
        my @match=();
        my @len=();
        my $total="";
        my $cute_little_proc="if (\'${string}\'${expr}) { \@match=(\$\`,\$\&,\$\'); }";
        my $rc=eval($cute_little_proc);
	#print "match_env: cute_little_proc:$cute_little_proc\n";
	#print "match_env: string:$string expr:$expr wanted_length:$wanted_length rc eval:$rc eval:$@\n";
        for my $i (0..$#match) {
                $len[$i]=length($match[$i]);
                #print "match $i: $match[$i] ($len[$i])\n";
        }
	return	squeeze($match[0],"right",$wanted_length/3) . 
		squeeze($match[1],"middle",$wanted_length/3) . 
		squeeze($match[2],"left",$wanted_length/3);
}

#---
#--- squeeze string
#---
sub squeeze {
	my ($string,$fromwhere,$num)=@_;

	return "" if (!defined($string));

	my $len=length($string);
	my $replacement="[...]";
	my $rlen=5;  		# length($replacement)

	#--- nothing to squeeze ;-)
	return $string if ($len<=$num);

	if ($fromwhere eq "left") {
		return substr($string,0,$num-$rlen) . $replacement;
	} elsif ($fromwhere eq "middle") {
		return $replacement . substr($string,($len/2)-($num/2)+$rlen,$num-($rlen*2)) . $replacement;
	} elsif ($fromwhere eq "right") {
		return $replacement . substr($string,$num*-1+$rlen);
	} elsif ($fromwhere eq "both") {
		return substr($string,0,$num/2-($rlen/2)) . $replacement . substr($string,($num*-1)/2+($rlen/2));
	} else {
		return "squeeze error: unknown fromwhere parameter $fromwhere\n";
	}
}

#---
#--- taken from Perl Cookbook ;-)
#---
sub is_valid_regex {
    my $pat = shift;
    return eval { "" =~ /$pat/; 1 } || 0;
}

#---
#--- trim input string if found any chars from trim string
#---
sub mytrim {
	my ($src, $trim)=@_;
	return ($src=~/[$trim]*(.*)[$trim]*/) ? $1 : $src;
}

#---
#---
#---
sub mysubst {
	my ($src,$pattern,$substitution)=@_;
	$src=~s/$pattern/$substitution/g;
	return $src;
}

#---
#--- substitute macros a la $HOSTNAME$ from environment
#---
sub substitute_macros {
        my ($input)=@_;
        while ((my $var)=($input=~/\$([A-Z0-9^\$]+)\$/)) {
                $input=~s/\$$var\$/$ENV{"NAGIOS_$var"}/g;
        }
        return $input;
}

#---
#--- add error(s) to global error list
#---
sub add_error {
	push @{$rc{error}}, @_;
}

#---
#--- create unique tmpfile and try to create it
#---
sub get_tmpfile {
	my ($path,$prefix)=@_;
	my $attempt=0;
	my $tmpfile="";
	#--- check existance of path and create it if necessary
	if (! -d $path && ! mkdir($path,0700)) {
		die("get_tmpfile: error creating tmp_path $path:$!");
		return "";
	}
	#--- do 5 attempts to create tmpfile
	while (++$attempt <= 5) {
		my $suffix=int(rand(89999))+10000;
		$tmpfile="$path/$prefix.$suffix";
		next if (-f $tmpfile);
		if (open(TMP,">$tmpfile")) {
			close TMP;
			return $tmpfile;
		}
	}
	die("get_tmpfile: giving up opening $tmpfile:$!");
	return "";
}

#---
#--- remove too old files from $tmpdir
#---
sub garbage_collection {
	my $interval=shift;
	opendir(DIR, $opt{tmpdir}) or die "garbage_collection: cannot open directory $opt{tmpdir}: $!";
	while (defined(my $filename = readdir(DIR))) {
		#--- basic security against weak tmpdirs: delete only files beginning with $MYSELF
		next if ($filename!~/^$MYSELF/);
		my $mtime=(stat("$opt{tmpdir}/$filename"))[9];
		if (time-$mtime>($interval*60*60)) {
			debug(2, sprintf("garbage collection: removing %d hours old $opt{tmpdir}/$filename", (time-$mtime)/(60*60)));
			unlink "$opt{tmpdir}/$filename";
		}
	}
	closedir(DIR);
}

#---
#--- execute $command, return result in %cmd 
#---
sub exec_command {
	my ($cmd)=@_;
	my $tmp_stdout="";
	my $tmp_stderr="";

	#--- execute command with alarm timer to catch timeouts
	$SIG{'ALRM'} = sub { die "timeout" };
	eval {
		alarm($opt{timeout});

		#--- prepare tmpfiles for stdout and stderr
		$tmp_stdout=&get_tmpfile($opt{tmpdir}, "${MYSELF}_stdout_$$");
		$tmp_stderr=&get_tmpfile($opt{tmpdir}, "${MYSELF}_stderr_$$");

		#--- execute command and store stdout/stderr/return code
		`$cmd{command} 1>$tmp_stdout 2>$tmp_stderr`;
		$cmd{rc}=$? >> 8;
		$cmd{timestamp}=time;

		#--- store stdout/stderr and cleanup tmpfiles
		$cmd{output}=readfile($tmp_stdout);
		$cmd{stderr}=readfile($tmp_stderr);
		unlink $tmp_stdout, $tmp_stderr;
		debug(3, "exec_command: raw output:>" . squeeze($cmd{output},"both",80) . "< raw stderr:>" . squeeze($cmd{stderr},"both",80) . "<");

		$cmd{output} .= $cmd{stderr};

		#--- unknown return code? change it explicitly to UNKNOWN
		if (!defined($rc{r2s}{$cmd{rc}})) {
			$cmd{stderr}.=" RC was $cmd{no}{rc}!";
			$cmd{rc}=$UNKNOWN;
		}
		
		#--- remove white chars from output
		chomp $cmd{output};
		$cmd{output}=~s/'//mg;
		#$cmd{output}=~s/\n/\\n/mg;
		#$cmd{output}=mytrim($cmd{output},"\\n\\s");
		#$cmd{stderr}=mytrim($cmd{stderr},"\\n\\s");

		#print "DEBUG output:>$cmd{output}< stderr:>$cmd{stderr}<n";

		alarm(0);
	};
	
	#--- any oddities during command execution?
	if ($@) {
		#--- timeout encountered: store status
		if ($@ =~ /timeout/) {
			$cmd{output}="UNKNOWN - \'$command\' cancelled after timeout ($opt{timeout}s)";
			$cmd{rc}=$UNKNOWN;
		#--- catchall for unknown errors
		} else {
			alarm(0);
       			die "$MYSELF: unexpected exception encountered:$@";
		}
		unlink $tmp_stdout, $tmp_stderr;
	}
	return $cmd{rc};
}

#---
#--- analyze results stored in %cmd
#---
sub do_analysis {
	my ($cmd)=@_;
	#debug(2,"do_analysis: state:$opt{state} false:$opt{false} number{false}:($rc{number}{$opt{false}})");
	my $returncode=$rc{number}{$opt{false}};

	#--- first: check return code
	if ($opt{ignore_rc}) {
		if ($cmd{rc} != 0) {
			debug(2, "do_analysis: ignoring error return code $cmd{rc}");
		}
	} else {
		if ($cmd{rc} != 0) {
			printf "%s UNKNOWN - cmd %s: %s [%s]\n",
				$opt{name},
				# replace | with PIPE to avoid perfdata problems
				mysubst($cmd{command},"\\|","PIPE"),	
				squeeze($cmd{output},"left",80),
				squeeze($cmd{stderr},"left",80);
			return $UNKNOWN;
		}
	} 

	#--- check type
	if ($opt{type} eq "delta") {
		if (defined($cmd{old_timestamp}) && $cmd{old_timestamp} > 0) {
			$cmd{elapsed_seconds}=$cmd{timestamp}-$cmd{old_timestamp};
			$cmd{delta}=$cmd{output}-$cmd{old_output};
			writefile("$opt{tmpdir}/$opt{tmpfile}", "$cmd{timestamp} $cmd{output}");
			debug(2, "do_analysis: elapsed_seconds:$cmd{elapsed_seconds} delta:$cmd{delta}");
			if ($cmd{elapsed_seconds} > 0) {
				$cmd{output}=sprintf "%.2f", $cmd{delta}/$cmd{elapsed_seconds};
			}
		} else {
			writefile("$opt{tmpdir}/$opt{tmpfile}", "$cmd{timestamp} $cmd{output}");
			$cmd{result}=squeeze($cmd{output},"left",80);
			$cmd{match}="[ delta: no previous output available ]";
                	return $UNKNOWN;
		}
	}

	#--- start with no match
	$cmd{match}="none";
	$cmd{matchlist}=[];

	if ($opt{string}) {
		#--- escape newlines in multiline pattern
		$cmd{output}=~s/\n/\\n/mg;
		$cmd{result}=squeeze($cmd{output},"left",50);
	} else {
		#--- remove last newline from numerical patterns
		chomp($cmd{output}) if ($cmd{output}=~/\n$/);
		$cmd{result}=$cmd{output};
	}

	#--- step forward in the order of severity from OK to CRITICAL
	foreach my $severity (@{$rc{textsev}}) {

		if (defined($opt{$severity})) {
			my $expression="\'$cmd{output}\'$opt{$severity}";
			debug(2,"do_analysis: evaluate expression for severity $severity >\'" . squeeze($cmd{output},"left",80) . 
				"\'" . squeeze($opt{$severity},"both",80)."<");
			#--- can be numerical or string evaluation
			if (eval($expression)) {
				$cmd{match}=$opt{$severity};
				push @{$cmd{matchlist}},$severity;
				$returncode=$rc{number}{$severity};
				if ($opt{string}) {
					$cmd{result}="x" . match_env($cmd{output},$opt{$severity},50);
				} else {
					$cmd{result}=$cmd{output};
					#$cmd{result}=squeeze($cmd{output},"both",80);
				}
				debug(2,"do_analysis: eval was successful rc:$returncode result:\'$cmd{result}\' match:\'$cmd{match}\'");
			} else {
				$cmd{result}=squeeze($cmd{output},"both",80);
				debug(2,"do_analysis: eval was *not* successful rc:$returncode severity:$severity expression:>\'" . 
					squeeze($cmd{output},"both",80) . "\'$opt{$severity}<");
			}
			
		}
	}
	return $returncode;
}

#---
#---
#---
sub do_report {
	my $cmd=shift;	

	foreach my $var ('$opt{name}','$rc{rc}','$rc{label}{$rc{rc}}','$cmd{result}','$cmd{match}','$cmd{matchlist}') {
		defined_var("do_report",$var);
	}
	#--- report results
	my $report_output=sprintf "%s %s - result:%s match:%s %s",
		$opt{name},
		$rc{label}{$rc{rc}},
		defined($cmd{result}) ? $cmd{result} : "[...]",
		defined($cmd{match}) ? $cmd{match} : "[...]",
		(@{$cmd{matchlist}}) ? "severities:" . join(',',@{$cmd{matchlist}}) : "";
	#print mysubst($report_output,"\\|","PIPE");
	print $report_output;
	#printf "|%s=%s", $opt{performance}, squeeze($cmd{output},"left",80) if ($opt{performance});
	printf "\n";
}
#
#
#
sub defined_var {
	my ($prefix,$var)=@_;
	if (! eval "defined($var)") {
		debug(0,"$prefix: var $var is not defined");
		return 0;
	}
	return 1;
}

#-------------------------------------------------------------------------------
#--- main ----------------------------------------------------------------------
#-------------------------------------------------------------------------------

#--- parse command line options
if (&process_parameters != $OK) {
	exit $UNKNOWN;
}

#--- initialize timer for overall timeout
$rc{starttime}=time;
$rc{endtime}=$rc{starttime} + $opt{timeout};

#--- execute command
&exec_command(\%cmd);

#--- analyze results
$rc{rc}=&do_analysis(\%cmd);

#--- report
&do_report(\%cmd);

#--- return rc with highest severity
exit $rc{rc};
