#!/usr/bin/perl -w # use nagios embedded perl # nagios: -epn # COPYRIGHT: # # This software is Copyright (c) 2008 NETWAYS GmbH, Tobias Redel # # # (Except where explicitly superseded by other copyright notices) # # # LICENSE: # # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from http://www.fsf.org. # # This work 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., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.fsf.org. # # # CONTRIBUTION SUBMISSION POLICY: # # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to NETWAYS GmbH.) # # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # this Software, to NETWAYS GmbH, you confirm that # you are the copyright holder for those contributions and you grant # NETWAYS GmbH a nonexclusive, worldwide, irrevocable, # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. # # Nagios and the Nagios logo are registered trademarks of Ethan Galstad. =head1 NAME check_cache - A Nagios Plugin wrapper with caching function =head1 SYNOPSIS check_cache -H -C -S -p [-m] [-e] [-u] [-t] [-c] [-v] [] [-h] [-V] [--print-sample-config] A Nagios Plugin wrapper with caching function =head1 OPTIONS =over =item -H|--host IP address or DNS name of target host =item -C|--command Nagios Plugin (command), (example: -C '/usr/local/nagios/libexec/check_users -w 2 -c 3') =item -S|--service Service Name (example: -S 'check_users' ) =item -p|--period Caching Period in minutes (example: -p 5) =item -m|--mode Caching mode, dynamic or static? (example: -m static). If not defined, mode will be dynamic =item -e|--execute Path to 'remote executor' (example: -e /usr/local/nagios/libexec/check_by_ssh). If not defined, executor path will be /usr/local/nagios/libexec/check_by_ssh =item -u|--username Login on remote host with user =item -t|--tmpdir Path to temporary caching dir. If not defined, dir will be /tmp =item -c|--config Path to config file =item -v|--verbose Verbose mode. If no logfile is specified, verbose output will be printend to STDOUT =item -h|--help print help page =item -V|--version print plugin version =item --print-sample-config print sample configuration file =cut use strict; use Getopt::Long qw(:config no_ignore_case bundling); use Pod::Usage; use XML::Simple; use Data::Dumper; use Fcntl ':flock'; # version string my $version = '0.4'; # define states our @state = ('OK', 'WARNING', 'CRITICAL', 'UNKNOWN'); # get command-line parameters my ($hostname, $command, $service, $period, $mode, $execute, $username, $tmpdir, $configfile, $verbose, $help, $showVersion, $print_sample_config); GetOptions( "H|host=s" => \$hostname, "C|command=s" => \$command, "S|service=s" => \$service, "p|period=s" => \$period, "m|mode=s" => \$mode, "e|execute=s" => \$execute, "u|username=s" => \$username, "t|tmpdir=s" => \$tmpdir, "c|config=s" => \$configfile, "v|verbose:s" => \$verbose, "h|help" => \$help, "V|version" => \$showVersion, "print-sample-config" => \$print_sample_config ); # * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # # help and version page and sample config... # * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # # should print version? if (defined $showVersion) { print $version."\n"; exit 0; } # should print sample config? print_sample_config() if defined $print_sample_config; # should print help? if ($help || !$hostname || !$command || !$service || !$period) { pod2usage(1); } # * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # # let's go! # * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # # first of all - process config file my $CONFIG; if (defined $configfile) { read_config($configfile); foreach(keys %{$CONFIG}) { eval "\$$_=\$CONFIG->{\$_};"; beVerbose("CONFIGFILE", "Set \$$_ to '$CONFIG->{$_}'"); } } # auto complement $mode = "dynamic" if !defined $mode; $tmpdir = "/tmp" if !defined $tmpdir; $execute = "/usr/local/nagios/libexec/check_by_ssh" if !defined $execute; # define variables my $cache; my $cache2; my $cache_file_path = $tmpdir."/".$hostname.".cache"; my $check_queue; my $return_item; my $calc; my %FILELOCK_HANDLES; # check executable if (! -f $execute) { LeavePlugin("1", "can't find executable '$execute'"); } # save value for plugin exit $return_item = $service; # is cache file available? if (! -f $cache_file_path) { # not available => create open(FILE,">$cache_file_path") || die LeavePlugin("3", "Can't create cache file: $!"); close(FILE); beVerbose("FILE CREATE", $cache_file_path); } # set lock level 1 (period lock) fileLock('1', 'lock', $period); # read xml cache file if not empty beVerbose("XML CACHE", "read cache file"); if (-s $cache_file_path >= 3) { $cache = XMLin($cache_file_path, keyattr => { service => 'name' }, forcearray => ['service']); } # command already cached? if (!defined $cache->{'service'}->{$service}) { # not found in cache file... # so... add ourselves to queue add2Queue($service, $command, $period, ''); # search all related services => mode? (static/dynamic) searchRelatedObjects($service, $mode); # execute executeCommand(); # save in cache file writeCacheFile(); } else { # found in cache file... # so... calc validity $calc = calcValidity($cache->{'service'}->{$service}->{'executiontime'}); # execute stuff if cache entry is not valid any more or command has changed if (($calc >= $cache->{'service'}->{$service}->{'period'}) || $command ne $cache->{'service'}->{$service}->{'command'}) { add2Queue($service, $command, $period, ''); searchRelatedObjects($service, $mode); executeCommand(); writeCacheFile(); } else { # otherwise, check if returncode of cache entry == 0 if ($cache->{'service'}->{$service}->{'returncode'} != 0) { add2Queue($service, $command, $period, $cache->{'service'}->{$service}->{'returncode'}); searchRelatedObjects($service, $mode); searchOnlyWarningsAndErrors(); executeCommand(); writeCacheFile(); } } } # unlock lock level 1 (period lock) fileLock('1', 'unlock', $period); # return and leave LeavePlugin($return_item); # * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # # functions... # * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # sub print_sample_config { print "\n"; print "# --------------------------------------------------\n"; print "# check_cache sample configfile\n"; print "# --------------------------------------------------\n"; print "\n"; print "# Caching mode, dynamic or static? (example: -m static). If not defined, mode will be dynamic\n"; print "# mode = dynamic\n"; print "\n"; print "# Login on remote host with user \n"; print "# username = nagios\n"; print "\n"; print "# Path to temporary caching dir. If not defined, dir will be /tmp\n"; print "# tmpdir = /tmp\n"; print "\n"; print "# Path to 'remote executor' - If not defined, executor path will be /usr/local/nagios/libexec/check_by_ssh\n"; print "# execute = /usr/local/nagios/libexec/check_by_ssh\n"; print "\n"; exit 0; } sub beVerbose { my $type = shift; my $text = shift; if (defined $verbose) { # generate message my $message = localtime(time)." | Verbose: $type: $text\n"; # should write log to file or STDOUT? if ($verbose ne "") { open(LOGF,">>$verbose") || die $!; print LOGF $message; close(LOGF); } else { print $message; } } } sub LeavePlugin { formatCacheResults("decode"); if (!$_[1]) { print $cache->{'service'}->{$_[0]}->{'lastresult'}."\n"; exit $cache->{'service'}->{$_[0]}->{'returncode'}; } else { my $exitCode = $_[0]; my $comment = $_[1]; print $state[$exitCode]." - $comment\n"; exit $exitCode; } } sub read_config { my $configfile = shift; my $config; # read configfile open(CF,'<'.$configfile) or die "Can't open configfile: $!"; read(CF, $config, -s $configfile); close(CF); beVerbose("CONFIGFILE", "read $configfile"); # split lines my @lines = split(/\015\012|\012|\015/,$config); foreach(@lines) { next if($_ =~ /^\s*#/); next if($_ !~ /^\s*\S+\s*=.*$/); my ($key,$value) = split(/=/,$_,2); # Remove whitespaces at the beginning and at the end $key =~ s/^\s+//g; $key =~ s/\s+$//g; $value =~ s/^\s+//g; $value =~ s/\s+$//g; beVerbose("CONFIGFILE", "Found entry '$key' with value '$value'"); $CONFIG->{$key} = $value; } } sub formatCacheResults { my $type = shift; my @cache = shift; foreach(keys %{$cache->{'service'}}) { if ($type eq "encode" || $type eq "ENCODE") { $cache->{'service'}->{$_}->{'lastresult'} =~s/\n/\\n/g; } if ($type eq "decode" || $type eq "DECODE") { $cache->{'service'}->{$_}->{'lastresult'} =~s/\\n/\n/g; } } } sub calcValidity { my $value = shift; my $timestamp = time(); my $calc; $calc = $timestamp - $value; $calc = $calc/60; return $calc; } sub createCacheObject { my $hostname = shift; my $timestamp = localtime(time); $cache->{'hostname'} = $hostname; $cache->{'created'} = $timestamp; } sub add2Queue { my $service_name = shift; my $command = shift; my $period = shift; my $returncode = shift; $check_queue->{$service_name}->{'command'} = $command; $check_queue->{$service_name}->{'period'} = $period; $check_queue->{$service_name}->{'returncode'} = $returncode; $check_queue->{$service_name}->{'result'} = defined; beVerbose("CMD QUEUE", "added : ".$command); } sub searchRelatedObjects { my $service_name = shift; my $mode = shift; my $timestamp = time(); foreach(keys %{$cache->{'service'}}) { # Don't include ourselves again next if ($_ eq $service_name); # dynamic mode... # (search criteria) # # 1) get all services listen in cache file # 2) build a subset of all services minor or equal '$period' # 3) build a subset of subset 2 where service's cache entry is not valid any more if ($mode eq "dynamic") { # calculate validity of each service $calc = calcValidity($cache->{'service'}->{$_}->{'executiontime'}); # if service has the same or a lower validity AND is not valid any more if ($cache->{'service'}->{$_}->{'period'} <= $period && $calc >= $cache->{'service'}->{$_}->{'period'}) { beVerbose("CMD QUEUE", " - - - following item is related - - - "); add2Queue($_, $cache->{'service'}->{$_}->{'command'}, $cache->{'service'}->{$_}->{'period'}, $cache->{'service'}->{$_}->{'returncode'}); } } # static mode... # (search criteria) # # 1) get all services listen in cache file # 2) get a subset of all services equal to '$period' if ($mode eq "static") { beVerbose("CMD QUEUE", " - - - following item is related - - -"); add2Queue($_, $cache->{'service'}->{$_}->{'command'}, $cache->{'service'}->{$_}->{'period'}, $cache->{'service'}->{$_}->{'returncode'}) if $cache->{'service'}->{$_}->{'period'} == $period; } } } sub searchOnlyWarningsAndErrors { foreach(keys %{$check_queue}) { if ($check_queue->{$_}->{'returncode'} == 0) { delete $check_queue->{$_}; beVerbose("CMD QUEUE", "deleted: $_"); } } } sub fileLock { my $level = shift; my $type = shift; my $period = shift; my $lock_path; # set file $lock_path = $cache_file_path.".".$period if $level == 1; $lock_path = $cache_file_path if $level == 2; # lock mode if ($type eq "lock" || $type eq "LOCK") { open my $FH, '>', $lock_path; flock($FH, LOCK_EX); beVerbose("FILE LOCK", $lock_path); $FILELOCK_HANDLES{$lock_path} = $FH; } # unlock mode if ($type eq "unlock" || $type eq "UNLOCK") { flock($FILELOCK_HANDLES{$lock_path}, LOCK_UN); beVerbose("FILE UNLOCK", $lock_path); close $FILELOCK_HANDLES{$lock_path}; } } sub writeCacheFile { formatCacheResults("encode"); # adjust $cache (delete empty vars) foreach(keys %{$cache->{'service'}}) { foreach my $val (keys %{$cache->{'service'}->{$_}}) { delete $cache->{'service'}->{$_}->{$val} if !defined $cache->{'service'}->{$_}->{$val}; } } # generate XML my $xml = XMLout($cache, RootName => 'cachefile'); # open file and save fileLock('2', 'lock'); open(FILE,">$cache_file_path") || die $!; print FILE $xml; beVerbose("XML CACHE", "write cache file"); close(FILE); fileLock('2', 'unlock'); } sub executeCommand { # add 'executor' to cmd var my $cmd = $execute; # add username to cmd var (if needed) $cmd .= " -l ".$username if defined $username; # add hostname to cmd var $cmd .= " -H ".$hostname; my @cached_services; # build -C Options foreach(keys %{$check_queue}) { push(@cached_services, $_); my $quoted_cmd = $check_queue->{$_}->{'command'}; $quoted_cmd =~ s/'/'"'"'/g; $cmd .= " -C '".$quoted_cmd."'"; } # execute... beVerbose("CMD EXEC", $cmd); my $result = qx($cmd); # one or more executed commands? if ((scalar @cached_services) == 1) { my $result_code = $?; chomp($result); # format return codes... if ($result_code == 256) { $result_code = 1; } if ($result_code == 512) { $result_code = 2; } # set up $cache for writin createCacheObject($hostname); $cache->{'service'}->{$cached_services[0]}->{'name'} = $cached_services[0]; $cache->{'service'}->{$cached_services[0]}->{'command'} = $check_queue->{$cached_services[0]}->{'command'}; $cache->{'service'}->{$cached_services[0]}->{'period'} = $check_queue->{$cached_services[0]}->{'period'}; $cache->{'service'}->{$cached_services[0]}->{'executiontime'} = time(); $cache->{'service'}->{$cached_services[0]}->{'executiontimehr'} = localtime(time); $cache->{'service'}->{$cached_services[0]}->{'lastresult'} = $result; $cache->{'service'}->{$cached_services[0]}->{'returncode'} = $result_code; } else { createCacheObject($hostname); # split return values and sort messages in @results_messages and return codes in @results_codes; my ($message_to_add, @results_messages, @results_codes); my @results = split(/\n/, $result); foreach(@results) { if ($_ =~ /^STATUS CODE(.*)[0-9]/) { chomp($message_to_add); push(@results_messages, $message_to_add); $message_to_add = ""; $message_to_add .= $_; push(@results_codes, $message_to_add); $message_to_add = ""; } else { $message_to_add .= $_."\n"; } } for (my $i=0; $i < (scalar @cached_services); $i++) { # split again for return code :( my $return_code = (split(/ /, $results_codes[$i]))[2]; $cache->{'service'}->{$cached_services[$i]}->{'name'} = $cached_services[$i]; $cache->{'service'}->{$cached_services[$i]}->{'command'} = $check_queue->{$cached_services[$i]}->{'command'}; $cache->{'service'}->{$cached_services[$i]}->{'period'} = $check_queue->{$cached_services[$i]}->{'period'}; $cache->{'service'}->{$cached_services[$i]}->{'executiontime'} = time(); $cache->{'service'}->{$cached_services[$i]}->{'executiontimehr'} = localtime(time); $cache->{'service'}->{$cached_services[$i]}->{'lastresult'} = $results_messages[$i]; $cache->{'service'}->{$cached_services[$i]}->{'returncode'} = $return_code; } } }