#! /usr/bin/perl -w # nagios: -epn package Devel::TraceMethods; use strict; use vars '$VERSION'; $VERSION = '1.00'; sub import { my $package = shift; while (@_) { my $traced = shift; my $logger = ref $_[0] eq 'CODE' && defined &{ $_[0] } ? shift : undef; _wrap_symbol( $traced, $logger ); } } sub _wrap_symbol { my ($traced, $logger) = @_; my $src; # get the calling package symbol table name { no strict 'refs'; $src = \%{ $traced . '::' }; } # loop through all symbols in calling package, looking for subs for my $symbol ( keys %$src ) { # get all code references, make sure they're valid my $sub = *{ $src->{$symbol} }{CODE}; next unless defined $sub and defined &$sub; # save all other slots of the typeglob my @slots; for my $slot (qw( SCALAR ARRAY HASH IO FORMAT )) { my $elem = *{ $src->{$symbol} }{$slot}; next unless defined $elem; push @slots, $elem; } # clear out the source glob undef $src->{$symbol}; # replace the sub in the source $src->{$symbol} = sub { my @args = @_; _log_call->( name => "${traced}::$symbol", logger => $logger, args => [ @_ ] ); return $sub->(@_); }; # replace the other slot elements for my $elem (@slots) { $src->{$symbol} = $elem; } } } { my $logger = sub { require Carp; Carp::carp( join ', ', @_ ) }; # set a callback sub for logging sub callback { # should allow this to be a class method :) shift if @_ > 1; my $coderef = shift; unless( ref($coderef) eq 'CODE' and defined(&$coderef) ) { require Carp; Carp::croak( "$coderef is not a code reference!" ); } $logger = $coderef; } # where logging actually happens sub _log_call { my %args = @_; my $log_sub = $args{logger} || $logger; $log_sub->( $args{name}, @{ $args{args} }); } } # # Logfile::Config::Tivoli.pm - Tivoli Config Module # # Purpose: Provide a convenient way for loading # tivoli config files and # return it as hash structure # package Nagios::Tivoli::Config::Logfile; use strict; sub new { my($this, $param ) = @_; my $class = ref($this) || $this; my $self = { formatfile => '', # format file with tivoli format definitions, # can be an array of files formatstring => '', # format file content as string severity_mappings => {}, max_continuation_lines => 0, # in case there are %n in among the patterns line_buffer => [], # for continuation lines line_buffer_size => 0, }; bless $self, $class; $self->set_severity_mapping('fatal', 2); $self->set_severity_mapping('critical', 2); $self->set_severity_mapping('severe', 2); $self->set_severity_mapping('warning', 1); $self->set_severity_mapping('minor', 1); $self->set_severity_mapping('harmless', 0); $self->set_severity_mapping('unknown', 0); # parse parameter if (ref($param) eq "HASH") { for my $key (keys %{$param}) { if (!defined $self->{lc $key}) { printf STDERR "unrecognized parameter: %s\n", $key; return undef; } else { if (ref($param->{$key}) eq 'HASH') { $self->merge_hash($self->{$key}, $param->{$key}); } else { $self->{lc $key} = $param->{$key}; } } } } elsif (ref($param) eq "") { $self->{formatfile} = $param; } else { printf STDERR "formatfile is a required parameter\n"; } if ((!defined $self->{formatfile} || $self->{formatfile} eq '') && (!defined $self->{formatstring} || $self->{formatstring} eq '')) { printf STDERR "please either specify formatfile or formatstring\n"; return undef; } if (defined $self->{formatstring} and $self->{formatstring} ne '') { $self->{_formatstring} = $self->{formatstring}; } else { $self->{_formatstring} = $self->_read($self->{formatfile}); } if (! $self->{_formatstring}) { return undef; } foreach (keys %{$self->{tivolimapping}}) { $self->set_severity_mapping($_, $self->{tivolimapping}->{$_}); } if ($self->_parse) { #Data::Dumper::Dumper($self->{formats}); return $self; } else { printf STDERR ("parsing failed, see previous messages..."); return undef; } } sub _read { my $self = shift; my $filename = shift; my $content; if (ref($filename) eq 'ARRAY') { for my $file (@{$filename}) { $content .= $self->_read($file); } } else { if (open FMT, $filename) { while() { $content .= $_; } close FMT; } else { printf STDERR "unable to read file %s: %s\n", $filename, $!; return undef; } } return($content); } sub _parse { my $self = shift; my $format; my $lineno = 0; for my $line (split /\n/, $self->{_formatstring}) { $lineno++; chomp $line; $line = $1 if $line =~ /^\s*(.*?)\s*$/; next if $line =~ m/^\/\//; next if $line eq ""; if ($line =~ m/^FORMAT/) { my($name, $follows, $followname) = $line =~ m/^FORMAT\s+(.*?)\s*(|FOLLOWS\s+(.*?))$/; $format= Nagios::Tivoli::Config::Logfile::Format->new({ name => $name, lineno => $lineno, severity_mappings => $self->{severity_mappings}, }); if (defined $followname) { my @follows = split /\s*,\s*/, $followname; for my $follow (@follows) { if (my $follow_format = $self->get_format_by_name($follow)) { $format->inherit($follow_format); } } $format->{follows} = \@follows; } } elsif ($line =~ m/^END/) { if (!defined $format) { printf STDERR "found format end without beginning\n"; return 0; } if (!defined $format->{pattern}) { if (!exists $format->{follows}) { printf STDERR "found format without pattern\n"; return 0; } } $self->add_format($format); } elsif (defined $format) { if (!defined $format->{pattern}) { # %s Specifies a variable string. # %t Specifies a variable date of the form 'MMM DD hh:mm:ss' # %s+ Specifies one or more variable strings that # are separated by spaces. # %s* Specifies zero or more strings separated by white space. # %n Specifies a new line (CR). # This applies only to the following adapters: # tecad_logfile_aix4-r1, tecad_logfile_hpux10, # tecad_logfile_linux_ix86, tecad_logfile_linux-ppc, # tecad_logfile_linux-s390, tecad_logfile_solaris2, # and tecad_win. $format->{tiv_pattern} = $line; $format->{patternlines} = 0; if ($line =~ /%n/) { $format->{patternlines}++ while $line =~ /%n/g; $format->{pattern} = [map { $self->translate_pattern($_) } split /%n/, $line]; $self->{max_continuation_lines} = $format->{patternlines} unless $format->{patternlines} <= $self->{max_continuation_lines}; } else { $format->{pattern} = $self->translate_pattern($line); } } elsif ($line =~ m/^-(.*?)\s+(.*)$/i) { $format->add_variable($1, $2); } elsif ($line =~ m/^(.*?)\s+"*(.*?)"*\s*$/) { $format->add_slot($1, $2); } } else { printf STDERR "%s is outside of a format definition\n", $line; return 0; } } return 1; } sub translate_pattern { my $self = shift; my $tiv_pattern = shift; $tiv_pattern =~ s/\\/\\\\/g; # quote \ $tiv_pattern =~ s/\(/\\(/g; # quote ( $tiv_pattern =~ s/\)/\\)/g; # quote ) $tiv_pattern =~ s/%\[\d+\]s/%s/g; # replace %[2]s with just %s $tiv_pattern =~ s/\[/\\[/g; # quote [ $tiv_pattern =~ s/\]/\\]/g; # quote ] $tiv_pattern =~ s/\?/\\?/g; # quote ? $tiv_pattern =~ s/\|/\\|/g; # quote | $tiv_pattern =~ s/\-/\\-/g; # quote - #$tiv_pattern =~ s/%s\+/\(.+?\)/g; # %s+ becomes .+? #$tiv_pattern =~ s/%s\*/\(.*?\)/g; # %s* becomes .*? #$tiv_pattern =~ s/%s/\(\[^\\s\]+?\)/g; # %s becomes [^\s]+? $tiv_pattern =~ s/%s\+/\([^\\s]*?.+[^\\s]*?\)/g; # %s+ becomes [^\s]*?.+[^\s]*? $tiv_pattern =~ s/%s\*\s*$/\(.*\)/g; # last %s* becomes .* eats the rest $tiv_pattern =~ s/%s\*/\(.*?\)/g; # %s* becomes .*? eats as much as necessary $tiv_pattern =~ s/%s/\(\[^\\s\]+\)/g; # %s becomes [^\s]+? #$tiv_pattern =~ s/%n/\\n/g; # %n becomes \n $tiv_pattern =~ s/[ ]+/\\s\+/g; # blanks become \s+ $tiv_pattern =~ s/%n//g; # %n becomes \n $tiv_pattern =~ s/%t/\(\\w\{3\}\\s+\\d\{1,2\}\\s+\\d\{1,2\}\:\\d\{1,2\}\:\\d\{1,2\}\)/g; return $tiv_pattern; } sub match { my $self = shift; my $line = shift; if ($self->{line_buffer_size} < $self->{max_continuation_lines} + 1) { push(@{$self->{line_buffer}}, $line); $self->{line_buffer_size}++; } else { shift @{$self->{line_buffer}}; push(@{$self->{line_buffer}}, $line); } #printf STDERR "try: %s\n", $line; foreach my $format (reverse @{$self->{'formats'}}) { next if ! $format->{can_match}; #if (($format->{name} ne '*DISCARD*') && # (! $format->has_slots() || ! $format->get_slot('severity'))) { # next; # ungueltiges format #} my @matches = (); #printf STDERR "format %s\n", $format->{name}; #printf STDERR "match /%s/\n", $format->{pattern}; if (my @matches = $self->match_pattern($line, $format)) { my $hit = Nagios::Tivoli::Config::Logfile::Hit->new({ format => $format, logline => $line, matches => \@matches, format_mappings => $self->{format_mappings}, severity_mappings => $self->{severity_mappings}, }); #printf STDERR "hit: %s\n", $line; if ($format->{name} eq '*DISCARD*') { #printf STDERR "discard: %s %s\n", $line, Data::Dumper::Dumper($hit); last; } else { #printf STDERR "hit2: %s // %s\n", $hit->{subject}, $format->{name}; return({ exit_code => $hit->get_nagios_severity(), severity => $hit->{severity}, format_name => $hit->{format_name}, subject => $hit->{subject}, logline => $line, slots => $hit->{slots}, }); } } } #printf STDERR "mis: %s\n", $line; return({ exit_code => $self->get_severity_mapping('HARMLESS'), severity => 'HARMLESS', format_name => 'NO MATCHING RULE', subject => 'NO MATCHING RULE', logline => $line, slots => { }, }); } sub match_pattern { my $self = shift; my $line = shift; my $format = shift; my $pattern = $format->{pattern}; if (ref($pattern) eq 'ARRAY') { my @all_matches = (); # my $patterns = scalar(@{$pattern}); if ($patterns > $self->{line_buffer_size}) { # zu wenig zeilen vorhanden return (); } else { my $startidx = $self->{line_buffer_size} - $patterns; my $idx = 0; while ($idx < $patterns) { # pattern[$idx] matched ${$self->{line_buffer}}[$startidx + $idx] ? if (my @matches = ${$self->{line_buffer}}[$startidx + $idx] =~ /$pattern->[$idx]/) { $idx++; push(@all_matches, @matches); } else { last; } } if ($idx == $patterns) { return @all_matches; } else { return (); } } } else { #my @matches = $line =~ /$pattern/; my @matches = $format->{matchfunc}($line); return @matches; } } # inherit # # copy variable and slot definitions of a followed format to the current format # sub inherit { my $self = shift; my $ancestor = shift; $self->merge_hash($self->{variables}, $ancestor->{variables}); $self->merge_hash($self->{slots}, $ancestor->{slots}); } # get_severity_mapping # # get the numerical nagios level for a tivoli level # sub get_severity_mapping { my $self = shift; my $tivoli_severity = lc shift; return $self->{severity_mappings}->{$tivoli_severity}; } # set_severity_mapping # # set the numerical nagios level for a tivoli level # sub set_severity_mapping { my $self = shift; my $tivoli_severity = lc shift; my $nagios_severity = shift; $self->{severity_mappings}->{$tivoli_severity} = $nagios_severity; } # set_format_mappings # # set runtime values for LABEL, DEFAULT,... # sub set_format_mappings { my $self = shift; my %mappings = @_; foreach (keys %mappings) { $self->{format_mappings}->{$_} = $mappings{$_}; } } sub add_format { my $self = shift; my $format = shift; if (($format->{name} ne '*DISCARD*') && (! $format->has_slots() || ! $format->get_slot('severity'))) { #printf STDERR "FORMAT %s skipped\n", $format->{name}; $format->{can_match} = 0; } else { $format->{can_match} = 1; } push(@{$self->{formats}}, $format); } sub get_format_by_name { my $self = shift; my $name = shift; foreach (@{$self->{formats}}) { return $_ if $_->{name} eq $name; } return undef; } sub merge_hash { my $self = shift; my $hash1 = shift; my $hash2 = shift; for my $key (keys %{$hash2}) { $hash1->{$key} = $hash2->{$key}; } return($hash1); } package Nagios::Tivoli::Config::Logfile::Format; use strict; use warnings; use Carp; use vars qw(@ISA); @ISA = qw(Nagios::Tivoli::Config::Logfile); sub new { my($this, $param ) = @_; my $class = ref($this) || $this; my $self = { name => '', lineno => 0, slots => {}, variables => {}, severity_mappings => {}, }; bless $self, $class; if (ref($param) eq "HASH") { for my $key (keys %{$param}) { if (!defined $self->{lc $key}) { carp("unrecognized parameter: $key"); } else { if (ref($param->{$key}) eq 'HASH') { $self->merge_hash($self->{$key}, $param->{$key}); } else { $self->{lc $key} = $param->{$key}; } } } } if (!defined $self->{name}) { die "please either specify formatfile or formatstring"; } $self->add_match_closure(); return $self; } sub add_slot { my $self = shift; my $slot = shift; my $value = shift; $self->{slots}->{$slot} = $value; } sub get_slot { my $self = shift; my $slot = shift; return $self->{slots}->{$slot}; } sub has_slots { my $self = shift; return scalar (keys %{$self->{slots}}); } sub add_variable { my $self = shift; my $variable = shift; my $value = shift; $self->{variables}->{$variable} = $value; } sub get_variable { my $self = shift; my $variable = shift; return $self->{variables}->{$variable}; } sub has_variables { my $self = shift; return scalar (keys %{$self->{variables}}); } sub add_match_closure { my $self = shift; # creates a function which keeps the compiled version of self->pattern $self->{matchfunc} = eval "sub { local \$_ = shift; return m/\$self->{pattern}/o; }"; } package Nagios::Tivoli::Config::Logfile::Hit; use strict; use warnings; use Carp; use vars qw(@ISA); @ISA = qw(Nagios::Tivoli::Config::Logfile::Format); sub new { my($this, $param ) = @_; my $class = ref($this) || $this; my $self = { format => $param->{format}, logline => $param->{logline}, format_mappings => $param->{format_mappings}, severity_mappings => $param->{severity_mappings}, matches => {}, variables => {}, slots => {}, }; bless $self, $class; my $matchcnt = 1; map { $self->{matches}->{$matchcnt++} = $_; } @{$param->{matches}}; $self->init(); return $self; } sub init { my $self = shift; $self->{severity} = $self->{format}->{slots}->{severity}; $self->{format_name} = $self->{format}->{name}; $self->merge_hash($self->{variables}, $self->{format}->{variables}); $self->merge_hash($self->{slots}, $self->{format}->{slots}); # resolve pattern groups in internal variables foreach my $var (keys %{$self->{variables}}) { if ($self->{variables}->{$var} =~ /^\$(\d+)/) { if (defined $self->{matches}->{$1}) { $self->{variables}->{$var} = $self->{matches}->{$1}; } else { printf STDERR "cannot replace \$%d in var %s\n", $1, $var; } } } # resolve pattern groups and format reserved words in slots foreach my $slot (keys %{$self->{slots}}) { if ($self->{slots}->{$slot} =~ /^\$(\d+)/) { if (defined $self->{matches}->{$1}) { $self->{slots}->{$slot} = $self->{matches}->{$1}; } else { printf STDERR "cannot replace \$%d in slot %s\n", $1, $slot; } } elsif ($self->{slots}->{$slot} eq 'DEFAULT') { if ($slot eq 'hostname') { $self->{slots}->{$slot} = $self->{format_mappings}->{hostname}; } elsif ($slot eq 'fqhostname') { $self->{slots}->{$slot} = $self->{format_mappings}->{fqhostname}; } elsif ($slot eq 'origin') { $self->{slots}->{$slot} = $self->{format_mappings}->{origin}; } else { $self->{slots}->{$slot} = 'check_logfiles'; } } elsif ($self->{slots}->{$slot} eq 'LABEL') { $self->{slots}->{$slot} = $self->{format_mappings}->{LABEL}; } elsif ($self->{slots}->{$slot} eq 'FILENAME') { $self->{slots}->{$slot} = $self->{format_mappings}->{FILENAME}; } else { } } foreach my $slot (keys %{$self->{slots}}) { if ($self->{slots}->{$slot} =~ /PRINTF/i) { $self->{slots}->{$slot} = $self->printf($self->{slots}->{$slot}); } } $self->{subject} = $self->{slots}->{msg} || $self->{logline}; #delete $self->{slots}->{msg}; } sub printf { my $self = shift; my $text = shift; my @printf = $text =~ m/printf\("(.*?)"\s*,\s*(.*)\)/i; my $result = $text; my @replacements; for my $key (split /\s*,\s*/, $printf[1]) { if (defined $self->{variables}->{$key}) { push @replacements, $self->{variables}->{$key}; } elsif (defined $self->{slots}->{$key}) { push @replacements, $self->{slots}->{$key}; } else { print STDERR "$key not found\n"; push @replacements, ''; } } eval { $result = sprintf($printf[0], @replacements); }; return($result); } sub get_nagios_severity { my $self = shift; return $self->get_severity_mapping($self->{slots}->{severity}); } package Nagios::CheckLogfiles; use strict; use IO::File; use File::Basename; use File::Spec; use Cwd; use Data::Dumper; #use Net::Domain qw(hostname hostdomain hostfqdn); use Socket; use POSIX qw(strftime); use IPC::Open2; use constant GZIP => '/bin/gzip'; my $ERROR_OK = 0; my $ERROR_WARNING = 1; my $ERROR_CRITICAL = 2; my $ERROR_UNKNOWN = 3; our $ExitCode = $ERROR_OK; our $ExitMsg = "OK"; my(%ERRORS, $TIMEOUT); %ERRORS = ( OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3 ); $TIMEOUT = 60; $| = 1; eval "require Win32;"; #eval "require Net::Domain qw(hostname hostdomain hostfqdn);"; eval "require Net::Domain;"; sub new { my $class = shift; my $params = shift; my $self = bless {} , $class; return $self->init($params); } # # Read a hash with parameters # sub init { my $self = shift; my $params = shift; my($sec, $min, $hour, $mday, $mon, $year) = (localtime)[0, 1, 2, 3, 4, 5]; $year += 1900; $mon += 1; $self->{tracefile} = $self->system_tempdir().'/check_logfiles.trace'; $self->{trace} = -e $self->{tracefile} ? 1 : 0; $self->{seekfilesdir} = $params->{seekfilesdir} || '/var/tmp/check_logfiles'; $self->{protocolsdir} = $params->{protocolsdir} || '/tmp'; $self->{scriptpath} = $params->{scriptpath} || '/bin:/sbin:/usr/bin:/usr/sbin'; $self->{protocolretention} = ($params->{protocolretention} || 7) * 24 * 3600; $self->{macros} = $params->{macros}; $self->{timeout} = $params->{timeout}; $self->{pidfile} = $params->{pidfile} || '/var/run/check_logfiles.pid'; $self->{perfdata} = ""; $self->{searches} = []; $self->{selectedsearches} = $params->{selectedsearches} || []; $self->{dynamictag} = $params->{dynamictag} || ""; $self->{cmdlinemacros} = $params->{cmdlinemacros} || {}; $self->{reset} = $params->{reset} || 0; $self->default_options({ prescript => 1, smartprescript => 0, supersmartprescript => 0, postscript => 1, smartpostscript => 0, supersmartpostscript => 0, report => 'short', maxlength => 4096, seekfileerror => 'critical', }); if ($params->{cfgfile}) { if (ref($params->{cfgfile}) eq "ARRAY") { # multiple cfgfiles found in a config dir my @tmp_searches = (); $self->{cfgbase} = $params->{cfgbase} || "check_logfiles"; foreach my $cfgfile (@{$params->{cfgfile}}) { $self->{cfgfile} = $cfgfile; if (! $self->init_from_file()) { return undef; } push(@tmp_searches, @{$self->{searches}}); $self->{searches} = []; } my %seen = (); # newer searches replace searches with the same tag @tmp_searches = reverse map { if (! exists $seen{$_->{tag}}) { $seen{$_->{tag}}++; $_; } else { (); } } reverse @tmp_searches; $self->{searches} = \@tmp_searches; my $uniqueseekfile = undef; my $uniqueprotocolfile = undef; foreach (@{$self->{searches}}) { $_->{cfgbase} = "check_logfiles"; next if $_->{tag} eq "prescript"; next if $_->{tag} eq "postscript"; $_->construct_seekfile(); } #$self->{cfgbase} = (split /\./, basename($params->{cfgfile}->[0]))[0]; $self->{cfgbase} = "check_logfiles"; } elsif ($params->{cfgfile} =~ /%0A/) { # this must be an encoded flat file $self->{cfgfile} = $params->{cfgfile}; $self->{cfgbase} = "flatfile"; if (! $self->init_from_file()) { return undef; } } else { $self->{cfgfile} = $params->{cfgfile}; $self->{cfgbase} = (split /\./, basename($self->{cfgfile}))[0]; if (! $self->init_from_file()) { return undef; } } # if there is a dynamictag parameter then replace template names with # template_dynamictagtag if (scalar(@{$self->{selectedsearches}})) { @{$self->{searches}} = map { my $srch = $_; if (grep {/^$srch->{tag}$/} @{$self->{selectedsearches}}) { $srch; } elsif (grep {/^$srch->{tag}$/} map { $_.'_'.$self->{dynamictag} } @{$self->{selectedsearches}}) { $srch; } elsif ($srch->{tag} eq "prescript") { $srch; } elsif ($srch->{tag} eq "postscript") { $srch; } else { $self->trace("skipping non-selected search %s", $srch->{tag}); (); } } @{$self->{searches}}; } } else { $self->{cfgbase} = $params->{cfgbase} || "check_logfiles"; # first the global options (from the commandline in this case) $self->refresh_options($params->{options}); $self->init_macros; foreach (@{$params->{searches}}) { $_->{seekfilesdir} = $self->{seekfilesdir}; $_->{scriptpath} = $self->{scriptpath}; %{$_->{macros}} = %{$self->{macros}}; $_->{tracefile} = $self->{tracefile}; $_->{cfgbase} = $self->{cfgbase}; if (my $search = Nagios::CheckLogfiles::Search->new($_)) { # maybe override default search options with global ones (ex. report) $search->refresh_options($self->get_options('report,seekfileerror')); push(@{$self->{searches}}, $search); } else { $ExitCode = $ERROR_UNKNOWN; $ExitMsg = sprintf "cannot create %s search %s", $_->{type}, $_->{tag}; return undef; } } } if (defined(&Win32::GetShortPathName) && ($^O =~ /Win/)) { # if this is true windows (not cygwin) and if the path exists # then transform it to a short form. undef if path does not exist. if (my $tmpshortpath = &Win32::GetShortPathName($self->{protocolsdir})) { $self->{protocolsdir} = $tmpshortpath; } } if ($self->get_option('report') !~ /^(long|short|html)$/) { $ExitCode = $ERROR_UNKNOWN; $ExitMsg = sprintf "UNKNOWN - output must be short, long or html"; return undef; } $self->{protocolfile} = sprintf "%s/%s.protocol-%04d-%02d-%02d-%02d-%02d-%02d", $self->{protocolsdir}, $self->{cfgbase}, $year, $mon, $mday, $hour, $min, $sec; $self->{protocololdfiles} = sprintf "%s/%s.protocol-*-*-*-*-*-*", $self->{protocolsdir}, $self->{cfgbase}; $self->{protocolfh} = new IO::File; $self->{protocolwritten} = 0; $self->{allerrors} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 }; # if parameters update if (@{$self->{searches}}) { $self->{exitcode} = $ExitCode; $self->{exitmessage} = $ExitMsg; return $self; } else { $ExitCode = $ERROR_UNKNOWN; $ExitMsg = sprintf "UNKNOWN - configuration incomplete"; return undef; } } sub init_from_file { my $self = shift; my $fullcfgfile; # # variables from the config file. # our($seekfilesdir, $protocolsdir, $scriptpath, $protocolretention, $prescript, $prescriptparams ,$prescriptstdin, $prescriptdelay, $postscript, $postscriptparams, $postscriptstdin, $postscriptdelay, @searches, @logs, $tracefile, $options, $report, $timeout, $pidfile); our $MACROS = {}; if ($^O =~ /MSWin/) { $ENV{HOME} = $ENV{USERPROFILE}; } if ($self->{cfgbase} eq "flatfile") { $self->{cfgfile} =~ s/\%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg; eval $self->{cfgfile}; if ($@) { $ExitCode = $ERROR_UNKNOWN; $ExitMsg = sprintf "UNKNOWN - syntax error %s", (split(/\n/, $@))[0]; return undef; } } else { if (-f $self->{cfgfile}) { $fullcfgfile = $self->{cfgfile}; } elsif (-f $self->{cfgfile}.'.cfg') { $fullcfgfile = $self->{cfgfile}.'.cfg'; } elsif (-f $ENV{HOME}.'/'.$self->{cfgfile}) { $fullcfgfile = $ENV{HOME}.'/'.$self->{cfgfile}; } elsif (-f $ENV{HOME}.'/'.$self->{cfgfile}.'.cfg') { $fullcfgfile = $ENV{HOME}.'/'.$self->{cfgfile}.'.cfg'; } else { $ExitCode = $ERROR_UNKNOWN; $ExitMsg = sprintf "UNKNOWN - can not load configuration file %s", $self->{cfgfile}; return undef; } $fullcfgfile = File::Spec->rel2abs($fullcfgfile) unless File::Spec->file_name_is_absolute($fullcfgfile); eval { require $fullcfgfile; }; if ($@) { $ExitCode = $ERROR_UNKNOWN; $ExitMsg = sprintf "UNKNOWN - syntax error %s", (split(/\n/, $@))[0]; return undef; } } $self->{tracefile} = $tracefile if $tracefile; $self->{trace} = -e $self->{tracefile} ? 1 : 0; # already done one level above $self->{cfgbase} = (split /\./, basename($self->{cfgfile}))[0]; $self->{seekfilesdir} = $seekfilesdir if $seekfilesdir; $self->{protocolsdir} = $protocolsdir if $protocolsdir; $self->{scriptpath} = $scriptpath if $scriptpath; $self->{protocolretention} = ($protocolretention * 24 * 3600) if $protocolretention; $self->{prescript} = $prescript if $prescript; $self->{prescriptparams} = $prescriptparams if $prescriptparams; $self->{prescriptstdin} = $prescriptstdin if $prescriptstdin; $self->{prescriptdelay} = $prescriptdelay if $prescriptdelay; $self->{postscript} = $postscript if $postscript; $self->{postscriptparams} = $postscriptparams if $postscriptparams; $self->{postscriptstdin} = $postscriptstdin if $postscriptstdin; $self->{postscriptdelay} = $postscriptdelay if $postscriptdelay; $self->{macros} = $MACROS if $MACROS; $self->{timeout} = $timeout if $timeout; $self->{pidfile} = $pidfile if $pidfile; $self->{privatestate} = {}; $self->init_macros; $self->refresh_options($options); if (@logs) { # # Since version 1.4 the what/where-array is called @searches. # To stay compatible, @logs is still recognized. # @searches = @logs; } if ($self->{options}->{prescript}) { $_->{scriptpath} = $self->{scriptpath}; %{$_->{macros}} = %{$self->{macros}}; $_->{tracefile} = $self->{tracefile}; $_->{cfgbase} = $self->{cfgbase}; $_->{script} = $self->{prescript}; $_->{scriptparams} = $self->{prescriptparams}; $_->{scriptstdin} = $self->{prescriptstdin}; $_->{scriptdelay} = $self->{prescriptdelay}; $_->{options} = sprintf "%s%sscript", $self->{options}->{supersmartprescript} ? "super" : "", $self->{options}->{smartprescript} ? "smart" : ""; $_->{privatestate} = $self->{privatestate}; my $search = Nagios::CheckLogfiles::Search::Prescript->new($_); push(@{$self->{searches}}, $search); } foreach (@searches) { $_->{seekfilesdir} = $self->{seekfilesdir}; $_->{scriptpath} = $self->{scriptpath}; %{$_->{macros}} = %{$self->{macros}}; $_->{tracefile} = $self->{tracefile}; $_->{cfgbase} = $self->{cfgbase}; if ((exists $_->{template}) && ! $self->{dynamictag}) { # skip templates if they cannot be tagged next; } $_->{dynamictag} = $self->{dynamictag}; if (my $search = Nagios::CheckLogfiles::Search->new($_)) { $search->refresh_options($self->get_options('report,seekfileerror')); push(@{$self->{searches}}, $search); $_->{privatestate}->{$search->{tag}} = $search->{privatestate}; } else { $ExitCode = $ERROR_UNKNOWN; $ExitMsg = sprintf "cannot create %s search %s", $_->{type}, $_->{tag}; return undef; } } if ($self->{options}->{postscript}) { $_->{scriptpath} = $self->{scriptpath}; %{$_->{macros}} = %{$self->{macros}}; $_->{tracefile} = $self->{tracefile}; $_->{cfgbase} = $self->{cfgbase}; $_->{script} = $self->{postscript}; $_->{scriptparams} = $self->{postscriptparams}; $_->{scriptstdin} = $self->{postscriptstdin}; $_->{scriptdelay} = $self->{postscriptdelay}; $_->{options} = sprintf "%s%sscript", $self->{options}->{supersmartpostscript} ? "super" : "", $self->{options}->{smartpostscript} ? "smart" : ""; $_->{privatestate} = $self->{privatestate}; my $search = Nagios::CheckLogfiles::Search::Postscript->new($_); push(@{$self->{searches}}, $search); } return $self; } sub run { my $self = shift; if ($self->{reset}) { foreach my $search (@{$self->{searches}}) { if ($search->{tag} ne "prescript" && $search->{tag} ne "postscript") { $search->rewind(); } } return $self; } foreach my $search (@{$self->{searches}}) { if (1) { # there will be a timesrunningout variable if ($search->{tag} eq "postscript") { $search->{macros}->{CL_SERVICESTATEID} = $self->{exitcode}; $search->{macros}->{CL_SERVICEOUTPUT} = $self->{exitmessage}; $search->{macros}->{CL_LONGSERVICEOUTPUT} = $self->{long_exitmessage} || $self->{exitmessage}; $search->{macros}->{CL_SERVICEPERFDATA} = $self->{perfdata}; $search->{macros}->{CL_PROTOCOLFILE} = $self->{protocolfile}; if ($search->{options}->{supersmartscript}) { # # Throw away everything found so far. Supersmart postscripts # have the last word. # $self->reset_result(); } } $search->run(); if (($search->{tag} eq "prescript") && ($search->{options}->{supersmartscript}) && ($search->{exitcode} > 0)) { # # Prepare for a premature end. A failed supersmart prescript # will abort the whole script. # $self->reset_result(); $self->trace("failed supersmart prescript. aborting..."); } $_->{privatestate}->{$search->{tag}} = $search->{privatestate}; if ($search->{options}->{protocol}) { if (scalar(@{$search->{matchlines}->{CRITICAL}}) || scalar(@{$search->{matchlines}->{WARNING}}) || scalar(@{$search->{matchlines}->{UNKNOWN}})) { if ($self->{protocolfh}->open($self->{protocolfile}, "a")) { foreach (qw(CRITICAL WARNING UNKNOWN)) { if (@{$search->{matchlines}->{$_}}) { $self->{protocolfh}->print(sprintf "%s Errors in %s (tag %s)\n", $_, $search->{logbasename}, $search->{tag}); foreach (@{$search->{matchlines}->{$_}}) { $self->{protocolfh}->printf("%s\n", $_); } } } $self->{protocolfh}->close(); $self->{protocolwritten} = 1; } } } if ($search->{options}->{count}) { foreach (qw(OK WARNING CRITICAL UNKNOWN)) { $self->{allerrors}->{$_} += scalar(@{$search->{matchlines}->{$_}}); if ($search->{lastmsg}->{$_}) { $self->{lastmsg}->{$_} = $search->{lastmsg}->{$_}; } } } $self->formulate_result(); if (($search->{tag} eq "prescript") && ($search->{options}->{supersmartscript}) && ($search->{exitcode} > 0)) { # # Failed supersmart prescript. I'm out... # last; } elsif (($search->{tag} eq "postscript") && ($search->{options}->{supersmartscript})) { my $codestr = {reverse %ERRORS}->{$search->{exitcode}}; ($self->{exitmessage}, $self->{perfdata}) = split(/\|/, $search->{lastmsg}->{$codestr}, 2); $self->{exitcode} = $search->{exitcode}; } } } $self->cleanup_protocols(); return $self; } sub formulate_result { my $self = shift; # # create the summary from all information collected so far # $self->{hint} = sprintf "(%s", join(", ", grep { $_ } ($self->{allerrors}->{CRITICAL} ? sprintf "%d errors", $self->{allerrors}->{CRITICAL} : undef, $self->{allerrors}->{WARNING} ? sprintf "%d warnings", $self->{allerrors}->{WARNING} : undef, $self->{allerrors}->{UNKNOWN} ? sprintf "%d unknown", $self->{allerrors}->{UNKNOWN} : undef)); if ($self->{protocolwritten}) { $self->{hint} .= sprintf " in %s)", basename($self->{protocolfile}); } else { $self->{hint} .= ")"; } foreach my $level qw(CRITICAL WARNING UNKNOWN OK) { $self->{exitcode} = $ERRORS{$level}; if (($level ne "OK") && ($self->{allerrors}->{$level})) { $self->{exitmessage} = sprintf "%s - %s - %s %s", $level, $self->{hint}, $self->{lastmsg}->{$level}, ($self->{allerrors}->{$level} == 1 ? "" : "..."); last; } else { $self->{exitmessage} = sprintf "OK - no errors or warnings"; } } $self->{perfdata} = join (" ", map { $_->formulate_perfdata(); if ($_->{perfdata}) {$_->{perfdata}} else {()} } @{$self->{searches}}); if ($self->get_option('report') ne "short") { $self->formulate_long_result(); } } sub formulate_long_result { my $self = shift; my $maxlength = $self->get_option('maxlength'); $self->{long_exitmessage} = ""; my $prefix = ($self->get_option('report') eq "html") ? "" : ""; my $suffix = ($self->get_option('report') eq "html") ? "
" : ""; my $messagelen = length($prefix) + length($suffix) + length($self->{exitmessage}); my $line = ""; foreach my $search (@{$self->{searches}}) { if (scalar(@{$search->{matchlines}->{CRITICAL}}) || scalar(@{$search->{matchlines}->{WARNING}}) || scalar(@{$search->{matchlines}->{UNKNOWN}})) { if ($self->get_option('report') eq "html") { $line = sprintf "tag %s", ((scalar(@{$search->{matchlines}->{CRITICAL}}) && "CRITICAL") || (scalar(@{$search->{matchlines}->{WARNING}}) && "WARNING") || (scalar(@{$search->{matchlines}->{UNKNOWN}}) && "UNKNOWN")), $search->{tag}; } else { $line = sprintf "tag %s %s\n", $search->{tag}, ((scalar(@{$search->{matchlines}->{CRITICAL}}) && "CRITICAL") || (scalar(@{$search->{matchlines}->{WARNING}}) && "WARNING") || (scalar(@{$search->{matchlines}->{UNKNOWN}}) && "UNKNOWN")); } if ($messagelen + length($line) < $maxlength) { $self->{long_exitmessage} .= $line; $messagelen += length($line); } else { last; } foreach my $level qw(CRITICAL WARNING UNKNOWN) { foreach my $message (@{$search->{matchlines}->{$level}}) { if ($self->get_option('report') eq "html") { $message =~ s//>/g; $line = sprintf "%s", $level, $message; } else { $line = sprintf "%s\n", $message; } if ($messagelen + length($line) < $maxlength) { $self->{long_exitmessage} .= $line; $messagelen += length($line); } else { last; } } } } } if ($self->{long_exitmessage}) { $self->{long_exitmessage} = sprintf "%s%s%s\n", $prefix, $self->{long_exitmessage}, $suffix; } } sub reset_result { my $self = shift; $self->{allerrors} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 }; } sub reset { my $self = shift; $self->{allerrors} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 }; foreach my $level qw(OK CRITICAL WARNING UNKNOWN) { $self->{lastmsg}->{$level} = ""; } foreach my $search (@{$self->{searches}}) { $search->reset(); } } sub cleanup_protocols { my $self = shift; # # cleanup old protocol files # # if ($self->{protocololdfiles} =~ /[^\\][ ]/) { # because Core::glob splits the argument on whitespace $self->{protocololdfiles} =~ s/( )/\\$1/g; } foreach my $oldprotocolfile (glob "$self->{protocololdfiles}") { if ((stat $oldprotocolfile)[9] < (time - $self->{protocolretention})) { $self->trace("deleting old protocol %s", $oldprotocolfile); unlink $oldprotocolfile; } } } sub init_macros { my $self = shift; my($sec, $min, $hour, $mday, $mon, $year) = (localtime)[0, 1, 2, 3, 4, 5]; my $cw = $^O =~ /MSWin/ ? 0 : strftime("%V", $sec, $min, $hour, $mday, $mon, $year, -1, -1, -1); $year += 1900; $mon += 1; # # Set default values for the built-in macros. # my $DEFAULTMACROS = { CL_SERVICEDESC => $self->{cfgbase}, CL_DATE_YYYY => sprintf("%04d", $year), CL_DATE_YY => substr($year,2,2), CL_DATE_MM => sprintf("%02d", $mon), CL_DATE_DD => sprintf("%02d", $mday), CL_DATE_HH => sprintf("%02d", $hour), CL_DATE_MI => sprintf("%02d", $min), CL_DATE_SS => sprintf("%02d", $sec), CL_DATE_TIMESTAMP => sprintf("%10d", time), CL_DATE_CW => sprintf("%02d", $cw), CL_NSCA_SERVICEDESC => $self->{cfgbase}, CL_NSCA_HOST_ADDRESS => "127.0.0.1", CL_NSCA_PORT => 5667, CL_NSCA_TO_SEC => 10, CL_NSCA_CONFIG_FILE => "/usr/local/nagios/etc/send_nsca.cfg", }; if (defined(&Win32::LoginName)) { $DEFAULTMACROS->{CL_USERNAME} = &Win32::LoginName(); $DEFAULTMACROS->{CL_HAS_WIN32} = 1; } else { $DEFAULTMACROS->{CL_USERNAME} = scalar getpwuid $>; $DEFAULTMACROS->{CL_HAS_WIN32} = 0; } if (defined(&Net::Domain::hostname)) { $DEFAULTMACROS->{CL_HOSTNAME} = &Net::Domain::hostname(); $DEFAULTMACROS->{CL_DOMAIN} = &Net::Domain::hostdomain(); $DEFAULTMACROS->{CL_FQDN} = &Net::Domain::hostfqdn(); $DEFAULTMACROS->{CL_HAS_NET_DOMAIN} = 1; } else { $DEFAULTMACROS->{CL_HOSTNAME} = POSIX::uname(); $DEFAULTMACROS->{CL_DOMAIN} = "localdomain"; $DEFAULTMACROS->{CL_FQDN} = POSIX::uname().'.'.'localdomain'; $DEFAULTMACROS->{CL_HAS_NET_DOMAIN} = 0; } #printf STDERR "%s\n", Data::Dumper::Dumper($DEFAULTMACROS); $DEFAULTMACROS->{CL_IPADDRESS} = scalar gethostbyname($DEFAULTMACROS->{CL_HOSTNAME}) ? inet_ntoa(scalar gethostbyname($DEFAULTMACROS->{CL_HOSTNAME})) : '127.0.0.1'; # # Add self-defined macros to the defaultmacros structure or overwrite # already defined macros. # if ($self->{macros}) { foreach (keys %{$self->{macros}}) { $DEFAULTMACROS->{$_} = $self->{macros}->{$_}; } } # # Add self-defined macros from the command line # --macro CL_KAAS="so a kaas" --macro CL_SCHMARRN="so a schmarrn" # if ($self->{cmdlinemacros}) { foreach (keys %{$self->{cmdlinemacros}}) { $DEFAULTMACROS->{$_} = $self->{cmdlinemacros}->{$_}; } } # # Escape the most commonly used special characters so they will no longer # be treated like special characters in a pattern. # $self->{macros} = $DEFAULTMACROS; return $self; } # # Resolve macros in a string. # If a second parameter is given, then this string is meant as a regular expression. # Escape special characters accordingly. # sub resolve_macros { my $self = shift; my $pstring = shift; while ($$pstring =~ /\$(.+?)\$/g) { my $maybemacro = $1; if (exists $self->{macros}->{$maybemacro}) { my $macro = $self->{macros}->{$maybemacro}; $$pstring =~ s/\$$maybemacro\$/$macro/; } } } sub resolve_macros_in_pattern { my $self = shift; my $pstring = shift; while ($$pstring =~ /\$(.+?)\$/g) { # das alte bleibt hier stehen als denkmal der schande #while ($$pstring =~ /.*\$(\w+)\$.*/g) { my $maybemacro = $1; if (exists $self->{macros}->{$maybemacro}) { my $macro = $self->{macros}->{$maybemacro}; # # Escape the most commonly used special characters so they will no longer # be treated like special characters in a pattern. # $macro =~ s|/|\\/|g; $macro =~ s|\-|\\-|g; $macro =~ s|\.|\\.|g; $$pstring =~ s/\$$maybemacro\$/$macro/; } } } sub default_options { my $self = shift; my $defaults = shift; while (my($key, $value) = each %{$defaults}) { $self->{options}->{$key} = $value; } } sub set_options { my $self = shift; my $options = shift; while (my($key, $value) = each %{$options}) { $self->{options}->{$key} = $value if $value; } } sub set_option { my $self = shift; my $option = shift; my $value = shift; $self->{options}->{$option} = $value if defined $value; } sub get_option { my $self = shift; my $option = shift; return exists $self->{options}->{$option} ? $self->{options}->{$option} : undef; } sub get_options { my $self = shift; my $list = shift; if (! $list) { return $self->{options}; } else { my %h = map {($_, $self->{options}->{$_})} split(',', $list); return \%h; } } sub refresh_options { my $self = shift; my $options = shift; if ($options) { if (ref($options) eq 'HASH') { # already as hash foreach my $option (keys %{$options}) { my $optarg = $options->{$option}; foreach my $defoption (keys %{$self->{options}}) { if ($option eq $defoption) { $self->{options}->{$defoption} = $optarg; } } } } else { # comes as string foreach my $option (split /,/, $options) { my $optarg = undef; $option =~ s/^\s+//; $option =~ s/\s+$//; if ($option =~ /(.*)=(.*)/) { $option = $1; $optarg = $2; $optarg =~ s/^"//; $optarg =~ s/"$//; $optarg =~ s/^'//; $optarg =~ s/'$//; } foreach my $defoption (keys %{$self->{options}}) { if ($option eq $defoption) { if ($optarg) { # example: sticky=3600,syslogclient="winhost1.dom" $self->{options}->{$defoption} = $optarg; } else { $self->{options}->{$defoption} = 1; } } elsif ($option eq 'no'.$defoption) { $self->{options}->{$defoption} = 0; } } } } } # reset [smart][pre|post]script options if no script should be called foreach my $option (qw(script prescript postscript)) { if (exists $self->{options}->{'supersmart'.$option}) { $self->{options}->{'smart'.$option} = 1 if $self->{options}->{'supersmart'.$option}; } if (exists $self->{options}->{'smart'.$option}) { $self->{options}->{$option} = 1 if $self->{options}->{'smart'.$option}; } if (exists $self->{options}->{$option}) { if (($self->{options}->{$option}) && ! exists $self->{$option}) { $self->{options}->{$option} = 0; $self->{options}->{'smart'.$option} = 0; $self->{options}->{'supersmart'.$option} = 0; } } } if ($self->{options}->{sticky}) { if ($self->{options}->{sticky} > 1) { $self->{maxstickytime} = $self->{options}->{sticky}; $self->{options}->{sticky} = 1; } else { # durch mehrmaliges refresh (seitens des CheckLogfiles-Objekts kann maxstickytime # zerschossen werden if (! exists $self->{maxstickytime} || $self->{maxstickytime} == 0) { $self->{maxstickytime} = 3600 * 24 * 365 * 10; } } } if ($self->{options}->{syslogclient}) { # $self->{prefilter} = $self->{options}->{syslogclient}; } } sub trace { my $self = shift; my $format = shift; $self->{tracebuffer} = [] unless exists $self->{tracebuffer}; push(@{$self->{tracebuffer}}, @_); if ($self->{verbose}) { printf("%s: ", scalar localtime); printf($format, @_); } if ($self->{trace}) { my $logfh = new IO::File; $logfh->autoflush(1); if ($logfh->open($self->{tracefile}, "a")) { $logfh->printf("%s: ", scalar localtime); $logfh->printf($format, @_); $logfh->printf("\n"); $logfh->close(); } } } sub action { my $self = shift; my $script = shift; my $scriptparams = shift; my $scriptstdin = shift; my $scriptdelay = shift; my $smart = shift; my $privatestate = shift; my $success = 0; my $rc = 0; my $exitvalue; my $signalnum; my $dumpedcore; my $output; my $pid = 0; my $wait = 0; my $strerror = (qw(OK WARNING CRITICAL UNKNOWN)) [$self->{macros}->{CL_SERVICESTATEID}]; my $cmd; my @stdinformat = (); foreach my $macro (keys %{$self->{macros}}) { my $envmacro = $macro; if ($envmacro =~ /^CL_/) { $envmacro =~ s/^CL_/CHECK_LOGFILES_/; } else { $envmacro = "CHECK_LOGFILES_".$macro; } $ENV{$envmacro} = $self->{macros}->{$macro}; } $ENV{CHECK_LOGFILES_SERVICESTATE} = qw(OK WARNING CRITICAL UNKNOWN) [$ENV{CHECK_LOGFILES_SERVICESTATEID}]; if (ref $script eq "CODE") { $self->trace("script is of type %s", ref $script); if (ref($scriptparams) eq "ARRAY") { foreach (@{$scriptparams}) { $self->resolve_macros(\$_) if $_; } } my $stdoutvar; *SAVEOUT = *STDOUT; eval { our $CHECK_LOGFILES_PRIVATESTATE = $privatestate; open OUT ,'>',\$stdoutvar; *STDOUT = *OUT; $exitvalue = &{$script}($scriptparams, $scriptstdin); }; *STDOUT = *SAVEOUT; if ($@) { $output = $@; $success = 0; $rc = -1; $self->trace("script said: %s", $output); } else { #$output = $stdoutvar || ""; $output = defined $stdoutvar ? $stdoutvar : ""; chomp $output; $self->trace("script said: %s", $output); if ($smart) { if (($exitvalue =~ /^\d/) && ($exitvalue >= 0 && $exitvalue <= 3)) { $success = 1; $rc = $exitvalue; $self->trace("script %s exits with code %d", $script, $rc); } else { $success = 1; $rc = -4; $self->trace("script %s failed for unknown reasons", $script); } } else { $success = 1; $rc = $exitvalue; $output = $self->{macros}->{CL_SERVICEOUTPUT}; } } } else { my $pathsep = ($^O =~ /MSWin/) ? ';' : ':'; foreach my $dir (split(/$pathsep/, $self->{scriptpath})) { if ( -x $dir.'/'.$script ) { $self->trace(sprintf "found script in %s/%s", $dir, $script); $cmd = sprintf "%s/%s", $dir, $script; if ($^O =~ /MSWin/) { $cmd =~ s/\//\\/g; if ($cmd =~ /\s/) { if (defined(&Win32::GetShortPathName)) { $cmd = &Win32::GetShortPathName($cmd); } else { $cmd = sprintf "\"%s\"", $cmd; } } } else { # need to escape blanks if ($cmd =~ /\s/) { $cmd =~ s/([ ])/\\$1/g; } } last; } } if ($cmd) { if (defined $scriptparams) { $self->resolve_macros(\$scriptparams); $cmd = sprintf "%s %s", $cmd, $scriptparams; } $self->trace(sprintf "execute %s", $cmd); if (defined $scriptstdin) { my $pid = 0; my $wait = 0; my $maxlines = 100; if (! ref($scriptstdin eq "ARRAY")) { $scriptstdin = [$scriptstdin]; } foreach (@{$scriptstdin}) { $self->resolve_macros(\$_); } @stdinformat = @{$scriptstdin}; # if the format string was defined using single quotes, the escape # characters must be expanded. $stdinformat[0] =~ s/\\t/\t/g; $stdinformat[0] =~ s/\\n/\n/g; $SIG{'PIPE'} = sub {}; $SIG{'CHLD'} = sub {}; my($chld_out, $chld_in); $pid = open2($chld_out, $chld_in, $cmd); $self->trace("stdin is <trace(@stdinformat); $self->trace("EOF"); $chld_in->printf(@stdinformat); $chld_in->close(); $output = $chld_out->getline() || ""; while ($maxlines-- > 0) { # sucking the remaining output to avoid sigpipe $chld_out->getline() || last; } chomp $output; $chld_out->flush(); $chld_out->close(); if ($^O =~ /MSWin/) { # unfortunately waitpid in rare cases returns -1 on windows $wait = wait; } else { $wait = waitpid $pid, 0; } $exitvalue = $? >> 8; $signalnum = $? & 127; $dumpedcore = $? & 128; if (($signalnum == 13) && ($maxlines < 0)) { $signalnum = 0; # the script printed more than the allowed 100 lines of output. # closing the descriptor $chld_out caused a SIGPIPE which will # be accepted here. } } else { my @output = `$cmd`; # find the first non-empty line @output = map { chomp; $_; } grep !/^$/, @output; $output = $output[0] || ""; $exitvalue = $? >> 8; $signalnum = $? & 127; $dumpedcore = $? & 128; } $self->trace("script said: %s", $output); if ($wait != $pid) { $success = 0; $rc = -5; $self->trace("wait %d != %d", $wait, $pid); } elsif ($signalnum) { $success = 0; $rc = -2; $self->trace("script %s received signal %d", $script, $signalnum); $self->trace("script %s exits with code %d", $script, $rc); } elsif ($dumpedcore) { $success = 0; $rc = -3; $self->trace("script %s failed with core dump", $script); } elsif ($smart) { if ($exitvalue >= 0 && $exitvalue <= 3) { $success = 1; $rc = $exitvalue; $self->trace("script %s exits with code %d", $script, $rc); } else { $success = 0; $rc = -4; $self->trace("script %s failed for unknown reasons", $script); } } else { $success = 1; $rc = $exitvalue; $output = $self->{macros}->{CL_SERVICEOUTPUT}; } } else { $self->trace(sprintf "could not find %s", $script); $success = 0; $rc = -1; } } if ($scriptdelay) { $self->trace(sprintf "sleeping for %d seconds", $scriptdelay); sleep $scriptdelay; } map { /^CHECK_LOGFILES/ && delete $ENV{$_}; } keys %{$ENV}; if($output) { # remove ticks in case the script was badly programmed # this is ugly and should be left to the scripts author $output =~ s/^"//; $output =~ s/"$//g; } return ($success, $rc, $output) } sub getfilefingerprint { my $self = shift; my $file = shift; if (-f $file) { if ($self->get_option('randominode')) { return "00:00"; } elsif ($^O eq "MSWin32") { my $magic; if (ref $file) { my $pos = $file->tell(); $file->seek(0, 0); $magic = $file->getline() || "this_was_an_empty_file"; $file->seek(0, $pos); } else { my $fh = new IO::File; $fh->open($file, "r"); $magic = $fh->getline() || "this_was_an_empty_file"; $fh->close(); } if ($self->{options}->{encoding}) { $magic =~ tr/\x80-\xFF//d; $magic =~ tr/\x00-\x1F//d; } $self->trace("magic: %s", $magic); #return(md5_base64($magic)); return(unpack("H*", $magic)); # use the creation time as unique identifier # haaaahaaaaaa win32 creation time is a good joke # google for "tunneling" return sprintf "0:%d", (stat $file)[10]; #return "0:0"; } else { return sprintf "%d:%d", (stat $file)[0], (stat $file)[1]; } } else { return "0:0"; } } sub getfilesize { my $self = shift; my $file = shift; return (-f $file) ? (stat $file)[7] : 0; } sub getfileisreadable { my $self = shift; my $file = shift; if ($^O =~ /MSWin/) { # -r is not reliable when working with cacls my $fh = new IO::File; if ($fh->open($file, "r")) { $fh->close(); return 1; } else { return undef; } } elsif (-r $file) { return 1; } else { use filetest 'access'; $self->trace("stat (%s) failed, try access instead", $file); if (-r $file) { return 1; } else { # i'm catholic. i believe in miracles. my $fh = new IO::File; if ($fh->open($file, "r")) { $fh->close(); return 1; } else { return 0; } } } } sub old_getfileisreadable { my $self = shift; my $file = shift; my $fh = new IO::File; if ($^O =~ /MSWin/) { if ($fh->open($file, "r")) { $fh->close(); return 1; } else { return undef; } } elsif (($^O eq "linux") || ($^O eq "cygwin")) { if (! -r $file) { use filetest 'access'; $self->trace("stat (%s) failed, try access instead", $file); return -r $file; } return -r $file; } else { return -r $file; } } sub system_tempdir { my $self = shift; if ($^O =~ /MSWin/) { return $ENV{TEMP} if defined $ENV{TEMP}; return $ENV{TMP} if defined $ENV{TMP}; return File::Spec->catfile($ENV{windir}, 'Temp') if defined $ENV{windir}; return 'C:\Temp'; } else { return "/tmp"; } } sub run_as_daemon { my $self = shift; my $delay = shift; if ($^O =~ /MSWin/) { if ($ENV{PROMPT}) { # i was called from a shell # vielleicht irgendwas mit detach die "not yet implemented"; } else { eval "require Win32::Daemon;"; if (defined(&Win32::Daemon::StartService)) { import Win32::Daemon; my $svc_callback = sub { my( $event, $context ) = @_; # # entgegen der DRECKSDOKU enthaelt $event NICHT den Status # $event = Win32::Daemon::State(); $context->{last_event} = $event; if ($event == SERVICE_RUNNING()) { # main loop $self->trace("Entering main loop"); do { $self->run(); $self->trace(sprintf "%s%s\n%s", $self->{exitmessage}, $self->{perfdata} ? "|".$self->{perfdata} : "", $self->{long_exitmessage} ? $self->{long_exitmessage}."\n" : ""); $self->reset(); foreach (1..$delay) { if (Win32::Daemon::State() == SERVICE_RUNNING()) { sleep 1; } else { last; } } } while(Win32::Daemon::State() == SERVICE_RUNNING()); $self->trace("Leaving main loop"); } elsif ($event == SERVICE_START_PENDING()) { # Initialization code $self->trace("Service initialized"); $context->{last_state} = SERVICE_RUNNING(); Win32::Daemon::State(SERVICE_RUNNING()); } elsif ($event == SERVICE_PAUSE_PENDING()) { $self->trace("Service makes a break"); $context->{last_state} = SERVICE_PAUSED(); Win32::Daemon::State(SERVICE_PAUSED()); } elsif ($event == SERVICE_CONTINUE_PENDING()) { $self->trace("Service continues"); $context->{last_state} = SERVICE_RUNNING(); Win32::Daemon::State(SERVICE_RUNNING()); } elsif ($event == SERVICE_STOP_PENDING()) { $self->trace("Service stops"); $context->{last_state} = SERVICE_STOPPED(); $self->trace("Daemon exiting..."); Win32::Daemon::State(SERVICE_STOPPED()); Win32::Daemon::StopService(); } else { # Take care of unhandled states by setting the State() # to whatever the last state was we set... $self->trace("Service got an unhandled call"); Win32::Daemon::State( $context->{last_state} ); } return(); }; Win32::Daemon::RegisterCallbacks($svc_callback); my %context = ( count => 0, start_time => time(), keep_going => 0, make_a_break => 0, ); # Start the service passing in a context and # indicating to callback using the "Running" event # every 2000 milliseconds (2 seconds). Win32::Daemon::StartService(\%context, 2000); } else { die "omeiomeiomei nix Win32::Daemon"; } } } else { chdir '/'; die "cannot detach from controlling terminal" unless POSIX::setsid(); exit if (fork()); exit if (fork()); open STDIN, '+>/dev/null'; open STDOUT, '+>&STDIN'; open STDERR, '+>&STDIN'; my $keep_going = 1; # $self->{pidfile}; $self->trace(sprintf "Daemon running with pid %d", $$); foreach my $signal (qw(HUP INT TERM QUIT)) { $SIG{$signal} = sub { $self->trace("Caught SIG%s: exiting gracefully", $signal); $keep_going = 0; }; } $self->trace("Entering main loop"); do { $self->run(); $self->trace(sprintf "%s%s\n%s", $self->{exitmessage}, $self->{perfdata} ? "|".$self->{perfdata} : "", $self->{long_exitmessage} ? $self->{long_exitmessage}."\n" : ""); $self->reset(); foreach (1..$delay) { if ($keep_going) { sleep 1; } else { last; } } } while($keep_going); $self->trace("Daemon exiting..."); } } sub install_windows_service { my $self = shift; my $servicename = shift || 'check_logfiles'; my $cfgfile = shift; my $username = shift; my $password = shift; if ($^O =~ /MSWin/) { eval "require Win32::Daemon;"; if (defined(&Win32::Daemon::StartService)) { import Win32::Daemon; my $fullpath = Win32::GetFullPathName($0); my ($cwd, $base, $ext) = ( $fullpath =~ /^(.*\\)(.*)\.(.*)$/ ) [0..2] ; my $servicepath = ($ext eq 'exe') ? "\"$fullpath\"" : "\"$^X\""; my $serviceparameters = ($ext eq 'exe') ? "--daemon --config \"$cfgfile\"" : " \"$fullpath\" --daemon --config \"$cfgfile\""; my $service = { machine => '', name => $servicename, display => $servicename, path => $servicepath, parameters => $serviceparameters, user => ($username || ''), password => ($password || ''), description => 'This is the Nagios plugin check_logfiles', }; if (Win32::Daemon::CreateService($service)) { $self->{exitmessage} = 'Successfully added service'; $self->{exitcode} = 0; } else { $self->{exitmessage} = 'Failed to add service: '. Win32::FormatMessage(Win32::Daemon::GetLastError()); $self->{exitcode} = 3; } } else { die "nix Win32::Daemon, nix Service, nix install"; } } else { $self->{exitmessage} = 'You just installed a Windows service on a Unix machine. Good luck.'; $self->{exitcode} = 0; } } sub deinstall_windows_service { my $self = shift; my $servicename = shift || 'check_logfiles'; if ($^O =~ /MSWin/) { eval "require Win32::Daemon;"; if (defined(&Win32::Daemon::StartService)) { import Win32::Daemon; if (Win32::Daemon::DeleteService('', $servicename)) { $self->{exitmessage} = 'Successfully deinstalled service'; $self->{exitcode} = 0; } else { $self->{exitmessage} = 'Failed to deinstall service: '. Win32::FormatMessage(Win32::Daemon::GetLastError()); $self->{exitcode} = 3; } } } else { $self->{exitmessage} = 'Congrats. You just deinstalled a Windows service on a Unix machine.'; $self->{exitcode} = 0; } } package Nagios::CheckLogfiles::Search; use strict; use Exporter; use File::Basename; use POSIX qw(SSIZE_MAX); #use Unicode::Normalize; #use Encode; use vars qw(@ISA); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; @ISA = qw(Nagios::CheckLogfiles); sub new { my $self = bless {}, shift; my $params = shift; $self->{tag} = $params->{tag} || 'default'; $self->{template} = $params->{template} if $params->{template}; $self->{dynamictag} = $params->{dynamictag} if $params->{dynamictag}; if (exists $self->{template} && exists $self->{dynamictag}) { $self->{tag} = $self->{template}.'_'.$self->{dynamictag}; } else { $self->{tag} = $params->{tag} || 'default'; } $self->{type} = $params->{type}; $self->{logfile} = $params->{logfile}; $self->{rotation} = $params->{rotation}; $self->{script} = $params->{script}; $self->{scriptparams} = $params->{scriptparams}; $self->{scriptstdin} = $params->{scriptstdin}; $self->{scriptdelay} = $params->{scriptdelay}; $self->{cfgbase} = $params->{cfgbase} || "check_logfiles"; $self->{seekfilesdir} = $params->{seekfilesdir} || $self->system_tempdir(); $self->{archivedir} = $params->{archivedir}; $self->{scriptpath} = $params->{scriptpath}; $self->{macros} = $params->{macros}; $self->{tracefile} = $params->{tracefile}; $self->{prefilter} = $params->{prefilter}; $self->{trace} = -e $self->{tracefile} ? 1 : 0; if (exists $params->{tivolipatterns}) { my $tivoliparams = { }; my $tivolipatterns = []; my $tivoliformatfiles = []; my $tivoliformatstrings = []; if (ref($params->{tivolipatterns}) ne 'ARRAY') { $tivolipatterns = [$params->{tivolipatterns}]; } else { push(@{$tivolipatterns}, @{$params->{tivolipatterns}}); } foreach my $pattern (@{$tivolipatterns}) { if (scalar(@{[split /\n/, $pattern]}) == 1) { push(@{$tivoliparams->{formatfile}}, $pattern); } else { #push(@{$tivoliparams->{formatstring}}, $pattern); # erstmal nur skalar moeglich $tivoliparams->{formatstring} = $pattern; } } if (exists $params->{tivolimapping}) { foreach (keys %{$params->{tivolimapping}}) { $tivoliparams->{severity_mappings}->{lc $_} = 0 if $params->{tivolimapping}->{$_} =~ /(?i)ok/; $tivoliparams->{severity_mappings}->{lc $_} = 1 if $params->{tivolimapping}->{$_} =~ /(?i)warning/; $tivoliparams->{severity_mappings}->{lc $_} = 2 if $params->{tivolimapping}->{$_} =~ /(?i)critical/; $tivoliparams->{severity_mappings}->{lc $_} = 3 if $params->{tivolimapping}->{$_} =~ /(?i)unknown/; $tivoliparams->{severity_mappings}->{lc $_} = $params->{tivolimapping}->{$_} if $params->{tivolimapping}->{$_} =~ /\d/; } } if ($self->{tivoli}->{object} = Nagios::Tivoli::Config::Logfile->new( $tivoliparams )) { } else { die "could not create tivoli object from $params->{tivolipatterns}"; } } if (! $self->{type}) { if ($self->{rotation}) { $self->{type} = "rotating"; } else { $self->{type} = "simple"; } } $self->{privatestate} = {}; my $class = sprintf "Nagios::CheckLogfiles::Search::%s", join "::", map { (uc substr($_, 0, 1)).substr($_, 1); } split(/::/, $self->{type}); bless $self, $class; if (! $self->can("init")) { # # Maybe $class was not defined in this file. Try to find # the external module. # my $module = $class.".pm"; $module =~ s/::/\//g; foreach (@INC) { if (-f $_."/$module") { require $module; bless $self, $class; last; } } } if ($self->can("init")) { if ($self->init($params)) { return $self; } else { return undef; } } else { return undef; } } # # Read a hash with parameters # sub init { my $self = shift; my $params = shift; $self->{laststate} = {}; $self->{relevantfiles} = []; $self->{preliminaryfilter} = { SKIP => [], NEED => [] }; $self->{matchlines} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] }; $self->{lastmsg} = { OK => "", WARNING => "", CRITICAL => "", UNKNOWN => "" }; $self->{patterns} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] }; $self->{patternfuncs} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] }; $self->{negpatterns} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] }; $self->{negpatterncnt} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] }; $self->{exceptions} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] }; $self->{threshold} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 }; $self->{thresholdcnt} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 }; $self->{hasinversepat} = 0; $self->{likeavirgin} = 0; $self->{linesread} = 0; $self->{linenumber} = 0; # used for context $self->{perfdata} = ""; $self->{max_readsize} = 1024 * 1024 * 128; # sysread can only read SSIZE_MAX bytes in one operation. # this is often (1024 * 1024 * 1024 * 2) - 1 = 2GB - 1 # if we need to read from a non-seekable filehandle more than this # amount of data, then we have to perform multiple reads. # because the $bytes variable must hold the result of such a read and # its size is limited by available memory, it is divided by 16 # so each read request does not overburden the sysread call and # does not inflate the process to more than 128MB # # options # $self->default_options({ script => 0, smartscript => 0, supersmartscript => 0, protocol => 1, count => 1, syslogserver => 0, logfilenocry => 1, perfdata => 1, case => 1, sticky => 0, syslogclient => 0, savethresholdcount => 1, encoding => 0, maxlength => 0, lookback => 0, context => 0, allyoucaneat => 0, randominode => 0, preferredlevel => 0, warningthreshold => 0, criticalthreshold => 0, unknownthreshold => 0, report => 'short', seekfileerror => 'critical', archivedirregexp => 0, }); $self->refresh_options($params->{options}); # # Dynamic logfile names may contain macros. # if (exists $self->{template} && exists $self->{dynamictag}) { $self->{macros}->{CL_TAG} = $self->{dynamictag}; $self->{macros}->{CL_TEMPLATE} = $self->{template}; } else { $self->resolve_macros(\$self->{tag}); $self->{macros}->{CL_TAG} = $self->{tag}; } $self->{logfile_before_resolving} = $self->{logfile}; $self->resolve_macros(\$self->{logfile}); $self->{macros}->{CL_LOGFILE} = $self->{logfile}; $self->{logbasename} = basename($self->{logfile}); $self->{archivedir} = exists $params->{archivedir} ? $params->{archivedir} : dirname($self->{logfile}); $self->resolve_macros(\$self->{archivedir}); # # Preliminary filter # if ($self->{prefilter}) { my $pattern = $self->{prefilter}; $self->resolve_macros_in_pattern(\$pattern); $pattern = '(?i)'.$pattern unless $self->{options}->{case}; $self->addfilter(1, $pattern); } if ($self->{options}->{syslogclient}) { my $pattern = $self->{options}->{syslogclient}; $self->resolve_macros_in_pattern(\$pattern); $pattern = '(?i)'.$pattern unless $self->{options}->{case}; $self->addfilter(1, $pattern); } if ($self->{options}->{syslogserver}) { my $pattern = '($CL_HOSTNAME$|localhost)'; $self->resolve_macros_in_pattern(\$pattern); $pattern = '(?i)'.$pattern unless $self->{options}->{case}; $self->addfilter(1, $pattern); } # # the guy who begged me for the encoding option never wrote me a mail again. # this means for me, encoding works perfect. if it does not work for you # then it's not my problem. # if ($self->{options}->{encoding}) { #require Encode qw(encode decode); require Encode; } # # Setup the structure describing what to search for. # foreach my $level qw(OK CRITICAL WARNING UNKNOWN) { # # if a single pattern was given as a scalar, force it into an array # and resolve macros. # if (exists $params->{(lc $level).'patterns'}) { if (ref($params->{(lc $level).'patterns'}) ne 'ARRAY') { $self->{patterns}->{$level} = [$params->{(lc $level).'patterns'}]; } else { push(@{$self->{patterns}->{$level}}, @{$params->{(lc $level).'patterns'}}); } foreach my $pattern (@{$self->{patterns}->{$level}}) { $self->resolve_macros_in_pattern(\$pattern); } # # separate the pattern arrays. patterns beginning with a "!" will raise # an error if they cannot be found. # this type of pattern also needs a counter for the matches because after # scanning the logfiles we must also check for a "not-found" condition. # @{$self->{negpatterns}->{$level}} = map { if (substr($_, 0, 1) eq "!") { push(@{$self->{negpatterncnt}->{$level}}, 0); substr($_, 1) } else { () } } @{$self->{patterns}->{$level}}; if (scalar(@{$self->{negpatterns}->{$level}})) { $self->{hasinversepat} = 1; @{$self->{patterns}->{$level}} = map { if (substr($_, 0, 1) ne "!") { $_ } else { () } } @{$self->{patterns}->{$level}}; } # # prepend the patterns with (?i) if the case insensitivity option is set # if (! $self->{options}->{case}) { foreach my $pattern (@{$self->{patterns}->{$level}}) { $pattern = '(?i)'.$pattern; } foreach my $pattern (@{$self->{negpatterns}->{$level}}) { $pattern = '(?i)'.$pattern; } } # # ignore the match unless a minimum of threshold occurrances were found # if ((! $self->{options}->{(lc $level).'threshold'}) && ($params->{(lc $level).'threshold'})) { $self->{options}->{(lc $level).'threshold'} = $params->{(lc $level).'threshold'}; } if ($self->{options}->{(lc $level).'threshold'}) { $self->{threshold}->{$level} = $self->{options}->{(lc $level).'threshold'} - 1; } else { $self->{threshold}->{$level} = 0; } foreach my $pattern (@{$self->{patterns}->{$level}}) { push(@{$self->{patternfuncs}->{$level}}, eval "sub { local \$_ = shift; return m/\$pattern/o; }"); } } if (exists $params->{(lc $level).'exceptions'}) { if (ref($params->{(lc $level).'exceptions'}) ne 'ARRAY') { $self->{exceptions}->{$level} = [$params->{(lc $level).'exceptions'}]; } else { push(@{$self->{exceptions}->{$level}}, @{$params->{(lc $level).'exceptions'}}); } foreach my $pattern (@{$self->{exceptions}->{$level}}) { $self->resolve_macros_in_pattern(\$pattern); } if (! $self->{options}->{case}) { foreach my $pattern (@{$self->{exceptions}->{$level}}) { $pattern = '(?i)'.$pattern; } } } } foreach my $level qw(CRITICAL WARNING UNKNOWN) { foreach my $pattern (@{$self->{negpatterns}->{$level}}) { push(@{$self->{negpatterncnt}->{$level}}, 0); } } if (exists $self->{tivoli}->{object}) { $self->{patterns} = { OK => [], WARNING => [], CRITICAL => ['.*'], UNKNOWN => [] }; push(@{$self->{patternfuncs}->{OK}}, sub { return undef; }); push(@{$self->{patternfuncs}->{WARNING}}, sub { return undef; }); push(@{$self->{patternfuncs}->{UNKNOWN}}, sub { return undef; }); push(@{$self->{patternfuncs}->{CRITICAL}}, eval "sub { local \$_ = shift; return m/.*/o; }"); $self->{tivoli}->{object}->set_format_mappings( hostname => $self->{macros}->{CL_HOSTNAME}, fqhostname => $self->{macros}->{CL_FQDN}, origin => $self->{macros}->{CL_IPADDRESS}, FILENAME => (ref($self) eq 'Nagios::CheckLogfiles::Search::Eventlog') ? 'EventLog' : $self->{macros}->{CL_LOGFILE}, # oder SysLogD LABEL => $self->{macros}->{CL_HOSTNAME}, # NON-TME ); } $self->construct_seekfile(); $self->{NH_detection} = ($^O =~ /MSWin/) ? 0 : 1; return $self; } sub construct_seekfile { my $self = shift; # since 2.0 the complete path to the logfile is mapped to the seekfilename if ($self->{logfile} ne $self->{logfile_before_resolving}) { $self->{seekfilebase} = $self->{logfile_before_resolving}; $self->{seekfilebase} =~ s/\$/_/g; } else { $self->{seekfilebase} = $self->{logfile}; } $self->{seekfilebase} =~ s/\//_/g; $self->{seekfilebase} =~ s/\\/_/g; $self->{seekfilebase} =~ s/:/_/g; $self->{seekfilebase} =~ s/\s/_/g; $self->{seekfiletag} = $self->{tag}; $self->{seekfiletag} =~ s/\//_/g; $self->{seekfile} = sprintf "%s/%s.%s.%s", $self->{seekfilesdir}, $self->{cfgbase}, $self->{seekfilebase}, $self->{tag} eq "default" ? "seek" : $self->{seekfiletag}; $self->{pre3seekfile} = sprintf "/tmp/%s.%s.%s", $self->{cfgbase}, $self->{seekfilebase}, $self->{tag} eq "default" ? "seek" : $self->{seekfiletag}; $self->{pre2seekfile} = sprintf "%s/%s.%s.%s", $self->{seekfilesdir}, $self->{cfgbase}, $self->{logbasename}, $self->{tag} eq "default" ? "seek" : $self->{seekfiletag}; } sub force_cfgbase { # this is for the -F option. after initialization the seek/protocolfiles # must be reset to cfgbase of the base configfile is used my $self = shift; $self->{cfgbase} = shift; $self->construct_seekfile(); } sub prepare { my $self = shift; return $self; } sub finish { my $self = shift; return $self; } sub rewind { my $self = shift; $self->loadstate(); foreach (keys %{$self->{laststate}}) { $self->{newstate}->{$_} = $self->{laststate}->{$_}; } $self->addevent(0, "reset"); $self->{newstate}->{logoffset} = 0; $self->{newstate}->{logtime} = 0; $self->savestate(); return $self; } sub run { my $self = shift; $self->trace(sprintf "==================== %s ==================", $self->{logfile}); $self->prepare(); $self->loadstate(); $self->analyze_situation(); if ($self->{logrotated} || $self->{logmodified} || $self->{hasinversepat}) { # be lazy and examine files only if necessary $self->collectfiles(); } if ($self->{hasinversepat} || scalar(@{$self->{relevantfiles}})) { $self->scan(); } else { $self->trace("nothing to do"); # $state keeps the old values foreach (keys %{$self->{laststate}}) { $self->{newstate}->{$_} = $self->{laststate}->{$_}; } $self->trace("keeping %s", $self->{newstate}->{servicestateid}) if $self->{newstate}->{servicestateid}; # maybe this was the 1st time } $self->savestate(); $self->finish(); $self->formulate_perfdata(); } =item loadstate() Load the last session's state. The state is defined by - the position where the last search stopped - the time when the logfile was last touched then. - device and inode of the logfile (since version 1.4) If there is no state file, then this must be the first run of check_logfiles. In this case take the current file length as the stop position, so nothing will actually be done. =cut sub loadstate { my $self = shift; if (-f $self->{seekfile}) { $self->{likeavirgin} = 0; $self->trace(sprintf "found seekfile %s", $self->{seekfile}); our $state = {}; #eval { do $self->{seekfile}; #}; if ($@) { # found a seekfile with the old syntax $self->trace(sprintf "seekfile has old format %s", $@); my $seekfh = new IO::File; $seekfh->open($self->{seekfile}, "r"); $self->{laststate} = { logoffset => $seekfh->getline() || 0, logtime => $seekfh->getline() || 0, devino => $seekfh->getline(), logfile => $self->{logfile}, }; chomp $self->{laststate}->{logoffset} if $self->{laststate}->{logoffset}; chomp $self->{laststate}->{logtime} if $self->{laststate}->{logtime}; chomp $self->{laststate}->{devino} if $self->{laststate}->{devino}; $seekfh->close(); } else { # found a new format seekfile $self->{laststate} = $state; } if (! $self->{laststate}->{logfile}) { $self->{laststate}->{logfile} = $self->{logfile}; } if (! $self->{laststate}->{devino}) { # upgrade vom < 1.4 on the fly $self->{laststate}->{devino} = $self->getfilefingerprint($self->{logfile}); } if (! $self->{laststate}->{servicestateid}) { $self->{laststate}->{servicestateid} = 0; } if (! $self->{laststate}->{serviceoutput}) { $self->{laststate}->{serviceoutput} = "OK"; } foreach my $level qw(CRITICAL WARNING UNKNOWN) { if (exists $self->{laststate}->{thresholdcnt}->{$level}) { $self->{thresholdcnt}->{$level} = $self->{laststate}->{thresholdcnt}->{$level}; } } $self->trace("LS lastlogfile = %s", $self->{laststate}->{logfile}); $self->trace("LS lastoffset = %u / lasttime = %d (%s) / inode = %s", $self->{laststate}->{logoffset}, $self->{laststate}->{logtime}, scalar localtime($self->{laststate}->{logtime}), $self->{laststate}->{devino}); } else { $self->trace("try pre2seekfile %s instead", $self->{pre2seekfile}); if (-f $self->{pre2seekfile}) { $self->trace("pre-2.0 seekfile %s found. rename it to %s", $self->{pre2seekfile}, $self->{seekfile}); mkdir $self->{seekfilesdir} if ! -d $self->{seekfilesdir}; rename $self->{pre2seekfile}, $self->{seekfile}; $self->trace("and call load_state again"); $self->loadstate() if -f $self->{seekfile}; return $self; } $self->trace("try pre3seekfile %s instead", $self->{pre3seekfile}); if (-f $self->{pre3seekfile}) { $self->trace("pre-3.0 seekfile %s found. rename it to %s", $self->{pre3seekfile}, $self->{seekfile}); mkdir $self->{seekfilesdir} if ! -d $self->{seekfilesdir}; rename $self->{pre3seekfile}, $self->{seekfile}; $self->trace("and call load_state again"); $self->loadstate() if -f $self->{seekfile}; return $self; } $self->{likeavirgin} = 1; $self->trace("no seekfile %s found", $self->{seekfile}); if (-e $self->{logfile}) { $self->trace(sprintf "but logfile %s found", $self->{logfile}); # Fake a "the logfile was not touched" situation. $self->trace('eat all you can') if $self->{options}->{allyoucaneat}; $self->{laststate} = { logoffset => ($self->{options}->{allyoucaneat} ? 0 : $self->getfilesize($self->{logfile})), logtime => (stat $self->{logfile})[10], devino => $self->getfilefingerprint($self->{logfile}), logfile => $self->{logfile}, servicestateid => 0, serviceoutput => "OK", }; } else { $self->trace("and no logfile found"); # This is true virginity $self->{laststate} = { logoffset => 0, logtime => 0, devino => "0:0", logfile => $self->{logfile}, servicestateid => 0, serviceoutput => "OK", }; } $self->trace("ILS lastlogfile = %s", $self->{laststate}->{logfile}); $self->trace("ILS lastoffset = %u / lasttime = %d (%s) / inode = %s", $self->{laststate}->{logoffset}, $self->{laststate}->{logtime}, scalar localtime($self->{laststate}->{logtime}), $self->{laststate}->{devino}); } if (exists $self->{laststate}->{privatestate}) { $self->{privatestate} = $self->{laststate}->{privatestate}; $self->trace("found private state %s", Data::Dumper::Dumper($self->{privatestate})); } if (! $self->{laststate}->{runcount}) { $self->{laststate}->{runcount} = 1; } else { $self->{laststate}->{runcount}++; } if (! $self->{laststate}->{runtime}) { $self->{laststate}->{runtime} = 0; } $self->{privatestate}->{lastruntime} = $self->{laststate}->{runtime}; $self->{privatestate}->{runcount} = $self->{laststate}->{runcount}; $self->{privatestate}->{logfile} = $self->{macros}->{CL_LOGFILE}; $self->{macros}->{CL_LAST_RUNTIME} = $self->{privatestate}->{lastruntime}; $self->{macros}->{CL_RUN_COUNT} = $self->{privatestate}->{runcount}; return $self; } =item savestate() Save a session's state. We need this for the next run of check_logfiles. Here we remember, how far we read the logfile, when it was last modified and what it's inode was. =cut sub savestate { my $self = shift; my $seekfh = new IO::File; my $now = time; $self->searchresult(); # calculate servicestateid and serviceoutput if ($self->{options}->{sticky}) { if ($self->get_option('report') ne 'short') { $self->{newstate}->{matchlines} = $self->{matchlines}; } if ($self->{laststate}->{servicestateid}) { $self->trace("an error level of %s is sticking at me", $self->{laststate}->{servicestateid}); $self->trace("and now i have %s", $self->{newstate}->{servicestateid}); if ($self->{newstate}->{servicestateid}) { $self->{newstate}->{laststicked} = $now; $self->trace("refresh laststicked"); # dont forget to count the sticky error if ($self->get_option('report') ne 'short') { foreach my $level (qw(OK WARNING CRITICAL UNKNOWN)) { my $servicestateid = {'OK'=>0,'WARNING'=>1,'CRITICAL'=>2,'UNKNOWN'=>3}->{$level}; foreach my $event ( reverse @{$self->{laststate}->{matchlines}->{$level}}) { $self->addfirstevent($servicestateid, $event); } } } else { $self->addfirstevent($self->{laststate}->{servicestateid}, $self->{laststate}->{serviceoutput}); } if (($self->{newstate}->{servicestateid} == 1) && ($self->{laststate}->{servicestateid} == 2)) { # if this was a warning and we already have a sticky critical # save the critical as the sticky exitcode $self->{newstate}->{servicestateid} = $self->{laststate}->{servicestateid}; # and keep the critical message as output $self->{newstate}->{serviceoutput} = $self->{laststate}->{serviceoutput}; } } else { if ($self->{options}->{sticky} > 1) { # we had a stick error, then an ok pattern and no new error $self->trace("sticky error was resetted"); $self->{newstate}->{laststicked} = 0; $self->{newstate}->{servicestateid} = 0; $self->{newstate}->{serviceoutput} = ""; if ($self->get_option('report') ne 'short') { delete $self->{newstate}->{matchlines}; } } else { # newstate is 0 because nothing happened in this scan # after maxstickytime do not carry on with this error. if (($now - $self->{laststate}->{laststicked}) > $self->{maxstickytime}) { $self->trace("maxstickytime %d expired", $self->{maxstickytime}); $self->{newstate}->{laststicked} = 0; $self->{newstate}->{servicestateid} = 0; $self->{newstate}->{serviceoutput} = ""; if ($self->get_option('report') ne 'short') { delete $self->{newstate}->{matchlines}; } } else { $self->{newstate}->{laststicked} = $self->{laststate}->{laststicked}; $self->{newstate}->{servicestateid} = $self->{laststate}->{servicestateid}; $self->{newstate}->{serviceoutput} = $self->{laststate}->{serviceoutput}; $self->trace("stay sticky until %s", scalar localtime ($self->{newstate}->{laststicked} + $self->{maxstickytime})); if ($self->get_option('report') ne 'short') { foreach my $level (qw(OK WARNING CRITICAL UNKNOWN)) { my $servicestateid = {'OK'=>0,'WARNING'=>1,'CRITICAL'=>2,'UNKNOWN'=>3}->{$level}; foreach my $event ( reverse @{$self->{laststate}->{matchlines}->{$level}}) { $self->addfirstevent($servicestateid, $event); } } } else { $self->addevent($self->{newstate}->{servicestateid}, $self->{newstate}->{serviceoutput}); } } } } } else { $self->trace("no sticky error from last run"); if ($self->{newstate}->{servicestateid}) { $self->{newstate}->{laststicked} = $now; $self->trace("stick until %s", scalar localtime ($self->{newstate}->{laststicked} + $self->{maxstickytime})); } } } # save threshold counts if a threshold exists for a level if ($self->{options}->{savethresholdcount}) { foreach my $level qw(CRITICAL WARNING UNKNOWN) { if ($self->{threshold}->{$level}) { $self->{newstate}->{thresholdcnt}->{$level} = $self->{thresholdcnt}->{$level}; } } } $self->{newstate}->{tag} = $self->{tag}; $self->{newstate}->{privatestate} = $self->{privatestate}; $self->{newstate}->{runcount} = $self->{laststate}->{runcount}; $self->{newstate}->{runtime} = $now; mkdir $self->{seekfilesdir} if ! -d $self->{seekfilesdir}; if ($seekfh->open($self->{seekfile}, "w")) { my $dumpstate = Data::Dumper->new([$self->{newstate}], [qw(state)]); #printf("save %s\n", $dumpstate->Dump()); $dumpstate = Data::Dumper->new([$self->{newstate}], [qw(state)]); $seekfh->printf("%s\n", $dumpstate->Dump()); $seekfh->printf("\n1;\n"); $seekfh->close(); $self->trace("keeping position %u and time %d (%s) for inode %s in mind", $self->{newstate}->{logoffset}, $self->{newstate}->{logtime}, scalar localtime($self->{newstate}->{logtime}), $self->{newstate}->{devino}); } else { $self->{options}->{count} = 1; $self->addevent($self->get_option('seekfileerror'), sprintf "cannot write status file %s! check your filesystem (permissions/usage/integrity) and disk devices", $self->{seekfile}); } return $self; } sub formulate_perfdata { my $self = shift; if ($self->{options}->{perfdata}) { if (exists $self->{template} && $self->{dynamictag}) { $self->{perftag} = $self->{template}; } else { $self->{perftag} = $self->{tag}; } $self->{perfdata} = sprintf "%s_lines=%d %s_warnings=%d %s_criticals=%d %s_unknowns=%d", $self->{perftag}, $self->{linesread}, $self->{perftag}, scalar(@{$self->{matchlines}->{WARNING}}), $self->{perftag}, scalar(@{$self->{matchlines}->{CRITICAL}}), $self->{perftag}, scalar(@{$self->{matchlines}->{UNKNOWN}}); } } sub addevent { my $self = shift; my $level = shift; my $errormessage = shift; if (! defined $errormessage || $errormessage eq '') { $errormessage = '_(null)_'; } if ($self->{options}->{maxlength}) { $errormessage = substr $errormessage, 0, $self->{options}->{maxlength}; } if ($level =~ /^\d/) { $level = (qw(OK WARNING CRITICAL UNKNOWN))[$level]; } else { $level = uc $level; } push(@{$self->{matchlines}->{$level}}, $errormessage); $self->{lastmsg}->{$level} = ${$self->{matchlines}->{$level}}[$#{$self->{matchlines}->{$level}}]; } sub update_context { my $self = shift; my $follow = shift; my $line = shift; } sub addfirstevent { my $self = shift; my $level = shift; my $errormessage = shift; if ($level =~ /^\d/) { $level = (qw(OK WARNING CRITICAL UNKNOWN))[$level]; } unshift(@{$self->{matchlines}->{$level}}, $errormessage); $self->{lastmsg}->{$level} = ${$self->{matchlines}->{$level}}[$#{$self->{matchlines}->{$level}}]; } # # Read through all files found during analyze_situation and compare # the contents with patterns declared critical or warning or.... # sub scan { my $self = shift; my $actionfailed = 0; my $resetted = 0; my $needfilter = scalar(@{$self->{preliminaryfilter}->{NEED}}); my $skipfilter = scalar(@{$self->{preliminaryfilter}->{SKIP}}); foreach my $logfile (@{$self->{relevantfiles}}) { $self->trace("moving to position %u in %s", $self->{laststate}->{logoffset}, $logfile->{filename}); if ($logfile->{seekable}) { $logfile->{fh}->seek($self->{laststate}->{logoffset}, 0); } else { my $buf; my $needtoread; $logfile->{offset} = 0; if ($self->{laststate}->{logoffset} > $self->{max_readsize}) { $needtoread = $self->{max_readsize}; $self->trace("i cannot sysread %u bytes. begin with %u bytes", $self->{laststate}->{logoffset}, $needtoread); } else { $needtoread = $self->{laststate}->{logoffset}; } while ($logfile->{offset} < $self->{laststate}->{logoffset}) { $self->trace("i start at offset %u", $logfile->{offset}); my $bytes = $logfile->{fh}->sysread($buf, $needtoread); if (! defined $bytes) { $self->trace("read error at position %u", $logfile->{offset}); last; } elsif ($bytes == 0) { # this should not happen, but at least it is an exit # from an endless loop. $self->trace("i read %d bytes. looks like EOF at position %u", $bytes, $logfile->{offset}); last; } else { $self->trace("i read %d bytes", $bytes); $logfile->{offset} += $bytes; if (($self->{laststate}->{logoffset} - $logfile->{offset}) > $self->{max_readsize}) { $needtoread = $self->{max_readsize}; $self->trace("i cannot sysread %u bytes. continue with %u bytes", $self->{laststate}->{logoffset} - $logfile->{offset}, $needtoread); } else { $needtoread = $self->{laststate}->{logoffset} - $logfile->{offset}; $self->trace("i will sysread %u bytes.", $needtoread); } } } $self->trace("fake seek positioned at offset %u", $logfile->{offset}); } while (my $line = $logfile->{fh}->getline()) { my $filteredout = 0; $self->{linesread}++; if (! $logfile->{seekable}) { $logfile->{offset} += length($line) } if ($self->{options}->{encoding}) { # i am sure this is completely unreliable $line = Encode::encode("ascii", Encode::decode($self->{options}->{encoding}, $line)); # the input stream is somewhat binary, so chomp doesn't know # it neads to remove \r\n on windows. $line =~ s/$1/\n/g if $line =~ /(\r\n?|\n\r?)/; } chomp($line); # # If for example the prefilter option was set, check if the line # needs to be further examined. Only lines which match the needed filter # can pass. # if ($needfilter) { foreach my $filter (@{$self->{preliminaryfilter}->{NEED}}) { if ($line !~ /$filter/) { $self->trace(sprintf "no need for %s", $line); $filteredout = 1; last; } } } # # Skip lines with blacklist patterns # if ($skipfilter) { foreach my $filter (@{$self->{preliminaryfilter}->{SKIP}}) { if ($line =~ /$filter/) { $self->trace(sprintf "skip unwanted %s", $line); $self->trace(sprintf "because matching %s", $filter); $filteredout = 1; last; } } } next if $filteredout; $self->{linenumber}++; $self->update_context(0, $line); # store this line as before my $matches = {}; foreach my $nagioslevel qw(CRITICAL WARNING UNKNOWN) { my $level = $nagioslevel; # because it needs to be modified my $outplayed = 0; $matches->{$level} = []; foreach my $exception (@{$self->{exceptions}->{$level}}) { if ($line =~ /$exception/) { $self->trace("exception %s found. aborting.", $exception); $outplayed = 1; last; } } next if $outplayed; my $patcnt = -1; #foreach my $pattern (@{$self->{patterns}->{$level}}) { # $patcnt++; # printf STDERR "-->%s\n<<<%s\n", $line, $pattern; # if ($line =~ /$pattern/) { # push(@{$matches->{$level}}, $patcnt); # } #} foreach my $patternfunc (@{$self->{patternfuncs}->{$level}}) { $patcnt++; if (&${patternfunc}($line)) { push(@{$matches->{$level}}, $patcnt); } } } # now we have a structure with all the matches for this line # new option preferredlevel=critical if ($self->{options}->{preferredlevel}) { my $preferredlevel = uc $self->{options}->{preferredlevel}; if (scalar(@{$matches->{$preferredlevel}}) > 0) { # es gibt z.b. einen criticaltreffer und critical ist preferred # d.h. alle anderen level fliegen raus foreach my $level qw(CRITICAL WARNING UNKNOWN) { $matches->{$level} = [] unless $level eq $preferredlevel; } } } foreach my $nagioslevel qw(CRITICAL WARNING UNKNOWN) { my $level = $nagioslevel; # because it needs to be modified foreach my $patcnt (@{$matches->{$level}}) { my $pattern = @{$self->{patterns}->{$level}}[$patcnt]; $self->trace("MATCH %s %s with %s", $level, $pattern, $line); if ($self->{threshold}->{$level}) { if ($self->{thresholdcnt}->{$level} < $self->{threshold}->{$level}) { $self->trace("skip match and the next %d", $self->{threshold}->{$level} - $self->{thresholdcnt}->{$level}); $self->{thresholdcnt}->{$level}++; next; } else { $self->{thresholdcnt}->{$level} = 0; } } if ($self->{tivoli}->{object}) { $self->{tivoli}->{match} = $self->{tivoli}->{object}->match($line); $self->{privatestate}->{tivolimatch} = $self->{tivoli}->{match}; $level = (qw(OK WARNING CRITICAL UNKNOWN))[$self->{tivoli}->{match}->{exit_code}]; next if $self->{tivoli}->{match}->{format_name} eq 'NO MATCHING RULE'; $line = $self->{tivoli}->{match}->{subject}; } if ($self->{options}->{script}) { $self->{macros}->{CL_SERVICESTATE} = $level; $self->{macros}->{CL_SERVICESTATEID} = $ERRORS{$level}; $self->{macros}->{CL_SERVICEOUTPUT} = $line; $self->{macros}->{CL_PATTERN_NUMBER} = $patcnt; my ($actionsuccess, $actionrc, $actionoutput) = $self->action($self->{script}, $self->{scriptparams}, $self->{scriptstdin}, $self->{scriptdelay}, $self->{options}->{smartscript}, $self->{privatestate}); if (! $actionsuccess) { # note the script failure. multiple failures will generate # one single event in the end. $actionfailed = 1; $self->addevent($level, $line); } elsif ($self->{options}->{supersmartscript}) { # completely replace the matched line with the script output $self->addevent($actionrc, $actionoutput); } elsif ($self->{options}->{smartscript}) { # both matched line and script output are events $self->addevent($level, $line); $self->addevent($actionrc, $actionoutput); } else { # dumb scripts generate no events. only the matched line. $self->addevent($level, $line); } } else { $self->addevent($level, $line); } if ($self->{tivoli}->{object}) { delete $self->{privatestate}->{tivolimatch}; } #} } # count patterns which raise an alert only if they were not found. my $patcnt = -1; foreach my $pattern (@{$self->{negpatterns}->{$level}}) { $patcnt++; if ($line =~ /$pattern/) { $self->{negpatterncnt}->{$level}->[$patcnt]++; $self->trace("negative pattern %s found.", $pattern); } } } # maybe a okpattern wipes out the history foreach my $pattern (@{$self->{patterns}->{OK}}) { if ($line =~ /$pattern/) { $self->trace("remedy pattern %s wipes out previous errors", $pattern); $self->{options}->{sticky}++ if $self->{options}->{sticky}; # such a remedypattern neutralizes previous error $self->{matchlines}->{WARNING} = []; $self->{matchlines}->{CRITICAL} = []; $self->{matchlines}->{UNKNOWN} = []; last; } } } # # if there are more files to come, start searching at the beginning # of each file. # only the first (oldest) file will be positioned at an offset. # $self->{laststate}->{logoffset} = 0; $self->{newstate}->{logoffset} = $logfile->{seekable} ? $logfile->{fh}->tell() : $logfile->{offset}; $self->{newstate}->{logtime} = (stat $logfile->{fh})[9]; $self->{newstate}->{devino} = $self->getfilefingerprint($logfile->{fh}); $self->trace("stopped reading at position %u", $self->{newstate}->{logoffset}); } # # if patterns beginning with ! were not found, treat this as an alert. # if ($self->{hasinversepat}) { foreach my $level qw(CRITICAL WARNING) { my $patcnt = -1; foreach my $pattern (@{$self->{negpatterns}->{$level}}) { $patcnt++; if ($self->{negpatterncnt}->{$level}->[$patcnt] == 0) { if ($self->{options}->{script}) { $self->{macros}->{CL_SERVICESTATEID} = $ERRORS{$level}; $self->{macros}->{CL_SERVICEOUTPUT} = sprintf("MISSING: %s", $pattern); $self->{macros}->{CL_PATTERN_NUMBER} = $patcnt; my ($actionsuccess, $actionrc, $actionoutput) = $self->action($self->{script}, $self->{scriptparams}, $self->{scriptstdin}, $self->{scriptdelay}, $self->{options}->{smartscript}, $self->{privatestate}); if (! $actionsuccess) { $actionfailed = 1; $self->addevent($level, sprintf("MISSING: %s", $pattern)); } elsif ($self->{options}->{supersmartscript}) { $self->addevent($actionrc, $actionoutput); } elsif ($self->{options}->{smartscript}) { $self->addevent($level, sprintf("MISSING: %s", $pattern)); $self->addevent($actionrc, $actionoutput); } else { $self->addevent($level, sprintf("MISSING: %s", $pattern)); } } else { $self->addevent($level, sprintf("MISSING: %s", $pattern)); } } } } # # no files were examined, so no positioning took place. # keep the old status. # if (scalar @{$self->{relevantfiles}} == 0) { $self->{newstate}->{logoffset} = $self->{laststate}->{logoffset}; $self->{newstate}->{logtime} = $self->{laststate}->{logtime}; $self->{newstate}->{devino} = $self->{laststate}->{devino}; } } # # now the heavy work is done. logfiles were searched and matching lines # were found and noted. # close the open file handles and store the current position in a seekfile. # foreach my $logfile (@{$self->{relevantfiles}}) { $logfile->{fh}->close(); } if ((scalar @{$self->{relevantfiles}} > 0) && ($self->{logfile} ne @{$self->{relevantfiles}}[$#{$self->{relevantfiles}}]->{filename})) { # # only rotated files were examined and a new logfile was not created yet. # next time we hopefully will have a new logfile, so start at position 0. # set the lastlogtime to now, and don't care no longer for the past. # $self->trace("rotated logfiles examined but no current logfile found"); $self->{newstate}->{logoffset} = 0; $self->{newstate}->{logtime} = time; } if ($actionfailed) { $self->{options}->{count} = 1; push(@{$self->{matchlines}->{WARNING}}, sprintf "could not execute %s", $self->{script}); } } sub addfilter { my $self = shift; my $need = shift; my $pattern = shift; if ($need) { push(@{$self->{preliminaryfilter}->{NEED}}, $pattern); } else { push(@{$self->{preliminaryfilter}->{SKIP}}, $pattern); } } sub searchresult { my $self = shift; if (scalar @{$self->{matchlines}->{CRITICAL}}) { $self->{newstate}->{servicestateid} = 2; $self->{newstate}->{serviceoutput} = ${$self->{matchlines}->{CRITICAL}}[$#{$self->{matchlines}->{CRITICAL}}]; } elsif (scalar @{$self->{matchlines}->{WARNING}}) { $self->{newstate}->{servicestateid} = 1; $self->{newstate}->{serviceoutput} = ${$self->{matchlines}->{WARNING}}[$#{$self->{matchlines}->{WARNING}}]; } elsif (scalar @{$self->{matchlines}->{UNKNOWN}}) { $self->{newstate}->{servicestateid} = 3; $self->{newstate}->{serviceoutput} = ${$self->{matchlines}->{UNKNOWN}}[$#{$self->{matchlines}->{UNKNOWN}}]; } else { $self->{newstate}->{servicestateid} = 0; $self->{newstate}->{serviceoutput} = ""; } if ($self->{option}->{sticky} && $self->get_option('report') ne 'short') { # damit long/html output erhalten bleibt und nicht nur der letzte treffer $self->{newstate}->{matchlines} = $self->{matchlines}; } } sub reset { my $self = shift; $self->{matchlines} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] }; $self->{lastmsg} = { OK => "", WARNING => "", CRITICAL => "", UNKNOWN => "" }; $self->{negpatterncnt} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] }; $self->{thresholdcnt} = { OK => 0, WARNING => 0, CRITICAL => 0, UNKNOWN => 0 }; #$self->{preliminaryfilter} = { SKIP => [], NEED => [] }; $self->{perfdata} = ""; foreach my $level qw(CRITICAL WARNING UNKNOWN) { foreach my $pat (@{$self->{negpatterns}->{$level}}) { push(@{$self->{negpatterncnt}->{$level}}, 0); } } if (exists $self->{template} && exists $self->{dynamictag}) { $self->{macros}->{CL_TAG} = $self->{dynamictag}; $self->{macros}->{CL_TEMPLATE} = $self->{template}; } else { #$self->resolve_macros(\$self->{tag}); $self->{macros}->{CL_TAG} = $self->{tag}; } delete $self->{lastlogoffset}; delete $self->{lastlogtime}; delete $self->{lastlogoffset}; delete $self->{lastlogfile}; delete $self->{newlogoffset}; delete $self->{newlogtime}; delete $self->{newdevino}; delete $self->{newlogfile}; $self->{relevantfiles} = []; $self->{logrotated} = 0; $self->{logmodified} = 0; $self->{linesread} = 0; $self->{relevantfiles} = []; if (exists $self->{options}->{sticky}) { $self->{options}->{sticky} = 1 if ($self->{options}->{sticky} > 1); } return $self; } package Nagios::CheckLogfiles::Search::Simple; use strict; use Exporter; use File::Basename; use vars qw(@ISA); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; @ISA = qw(Nagios::CheckLogfiles::Search); sub new { my $self = bless {}, shift; return $self->init(shift); } sub analyze_situation { my $self = shift; $self->{logrotated} = 0; $self->{logmodified} = 0; if (! -e $self->{logfile}) { # # the logfile was deleted and no new events occurred since. # todo: no collection, but reset counters, incl. timestamp # with the modified flag we force a call to collectfiles where # [no]logfilenocry will be considered. $self->{logmodified} = 1; $self->trace(sprintf "there is no logfile %s at this moment", $self->{logfile}); $self->{laststate}->{logoffset} = 0; } elsif (! $self->getfileisreadable($self->{logfile})) { $self->{logmodified} = 1; $self->trace(sprintf "first noticed that logfile %s is unreadable", $self->{logfile}); } elsif ($self->{laststate}->{devino} ne $self->getfilefingerprint($self->{logfile})) { # the inode changed (! the old inode could have been reused) # or maybe this is the first time this logfile was seen $self->trace(sprintf "this is not the same logfile %s %s != %s", $self->{logfile}, $self->{laststate}->{devino}, $self->getfilefingerprint($self->{logfile})); $self->{logmodified} = 1; $self->{laststate}->{logoffset} = 0; $self->trace(sprintf "reset to offset 0"); } elsif ($self->getfilesize($self->{logfile}) > $self->{laststate}->{logoffset}) { # # the logfile grew. # this is the normal behaviour. in rare cases the logfile could have been # rotated/recreated and grown very fast. $self->trace(sprintf "the logfile grew to %d", $self->getfilesize($self->{logfile})); if ($self->{likeavirgin}) { # if the logfile grew because we initialized the plugin with an offset of 0, position # at the end of the file and skip this search. otherwise lots of outdated messages could # match and raise alerts. if ($self->{options}->{allyoucaneat}) { $self->trace("allyoucaneat the virgin"); $self->{laststate}->{logoffset} = 0; $self->{logmodified} = 1; # normally, virgins are not scanned. force it. } else { $self->{laststate}->{logoffset} = $self->getfilesize($self->{logfile}); } } else { $self->{logmodified} = 1; # this is only relevant if # 1.a new logfile is created using the same inode as the deleted logfile # 2.the new logfile grew beyond the size of the last logfile before check_logfile ran #if ($self->{firstline} ne $self->{laststate}->{firstline}) { # $self->trace(sprintf "a new logfile grew beyond the end of the last logfile"); # $self->{laststate}->{logoffset} = 0; #} } } elsif ($self->getfilesize($self->{logfile}) == 0) { # # the logfile was either truncated or deleted and touched. # nothing to do except reset the position $self->{logmodified} = 0; $self->{laststate}->{logoffset} = 0; $self->{laststate}->{logtime} = (stat $self->{logfile})[9]; $self->trace("logfile has been truncated"); } elsif ($self->getfilesize($self->{logfile}) < $self->{laststate}->{logoffset}) { # # logfile shrunk. either it was truncated or it was # rotated and a new logfile was created. $self->trace(sprintf "the logfile shrunk from %d to %d", $self->{laststate}->{logoffset}, $self->getfilesize($self->{logfile})); $self->{logmodified} = 1; $self->{laststate}->{logoffset} = 0; $self->trace(sprintf "reset to offset 0"); } elsif ($self->getfilesize($self->{logfile}) == $self->{laststate}->{logoffset}) { $self->trace(sprintf "the logfile did not change"); } else { $self->trace("I HAVE NO IDEA WHAT HAPPENED"); } return $self; } sub collectfiles { my $self = shift; my @rotatedfiles = (); if ($self->{logmodified}) { my $fh = new IO::File; # cygwin lets you open files even after chmodding them to 0000, so double check with -r if ($self->getfileisreadable($self->{logfile})) { $fh->open($self->{logfile}, "r"); $self->trace("opened logfile %s", $self->{logfile}); push(@rotatedfiles, { filename => $self->{logfile}, fh => $fh, seekable => 1 }); $self->trace("logfile %s (modified %s / accessed %s / inode %d / inode changed %s)", $self->{logfile}, scalar localtime((stat $self->{logfile})[9]), scalar localtime((stat $self->{logfile})[8]), (stat $self->{logfile})[1], scalar localtime((stat $self->{logfile})[10])); } else { if (-e $self->{logfile}) { # permission problem $self->trace("could not open logfile %s", $self->{logfile}); $self->addevent('CRITICAL', sprintf "could not open logfile %s", $self->{logfile}); } else { if ($self->{options}->{logfilenocry}) { # logfiles which are not rotated but deleted and re-created may be missing # maybe a rotation situation, a typo in the configfile,... $self->trace("could not find logfile %s", $self->{logfile}); $self->addevent('UNKNOWN', sprintf "could not find logfile %s", $self->{logfile}); } else { # dont care. $self->trace("could not find logfile %s, but that's ok", $self->{logfile}); } } } } $self->trace(sprintf "relevant files: %s", join(", ", map { basename $_->{filename} } @rotatedfiles)); $self->{relevantfiles} = \@rotatedfiles; } package Nagios::CheckLogfiles::Search::Rotating; use strict; use Exporter; use File::Basename; use vars qw(@ISA); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; @ISA = qw(Nagios::CheckLogfiles::Search); sub new { my $self = bless {}, shift; # sollte mal raus, da gibts kein sub dazu. # hier kommt eh keiner her, weil eins hoeher geblesst wird # $self->rotationpattern(); return $self->init(shift); } sub analyze_situation { my $self = shift; $self->{logrotated} = 0; $self->{logmodified} = 0; if (! $self->{NH_detection}) { if (! -e $self->{logfile}) { # # if no logfile exists, then probably it was rotated and no new logs # were written since. # find files which were modified after $lasttime. the most recent one # is probably the former logfile. position at $lastoffset. # if this configurations does not care for rotations, there is nothing # we can do here. # $self->{logrotated} = 1; $self->{logmodified} = 1; $self->trace(sprintf "there is no logfile %s at this moment", $self->{logfile}); } elsif ($self->{laststate}->{devino} ne $self->getfilefingerprint($self->{logfile})) { # the inode changed (! the old inode could be reused) $self->trace(sprintf "this is not the same logfile %s != %s", $self->{laststate}->{devino}, $self->getfilefingerprint($self->{logfile})); $self->{logrotated} = 1; $self->{logmodified} = 1; } elsif ($self->getfilesize($self->{logfile}) > $self->{laststate}->{logoffset}) { # # the logfile grew. # this is the normal behaviour. in rare cases the logfile could have been # rotated/recreated and grown very fast. $self->trace(sprintf "the logfile grew to %d", $self->getfilesize($self->{logfile})); if ($self->{likeavirgin}) { # if the logfile grew because we initialized the plugin with an offset of 0, position # at the end of the file and skip this search. otherwise lots of outdated messages could # match and raise alerts. $self->{laststate}->{logoffset} = $self->getfilesize($self->{logfile}); } else { $self->{logmodified} = 1; } } elsif ($self->getfilesize($self->{logfile}) == 0) { # # the logfile was either truncated or deleted and touched. # nothing to do except reset the position $self->{logrotated} = 1; $self->{laststate}->{logtime} = (stat $self->{logfile})[9]; } elsif ($self->getfilesize($self->{logfile}) < $self->{laststate}->{logoffset}) { # # logfile shrunk. either it was truncated or it was # rotated and a new logfile was created. $self->trace(sprintf "the logfile shrunk from %d to %d", $self->{laststate}->{logoffset}, $self->getfilesize($self->{logfile})); $self->{logmodified} = 1; $self->{logrotated} = 1; } elsif ($self->getfilesize($self->{logfile}) == $self->{laststate}->{logoffset}) { $self->trace(sprintf "the logfile did not change"); } else { $self->trace("I HAVE NO IDEA WHAT HAPPENED"); } return $self; } else { # Nigel Harnimans mtime-based algorithm my $filetime = (stat $self->{logfile})[9]; my $lastfiletime = $self->{laststate}->{logtime}; if (! -e $self->{logfile}) { # # if no logfile exists, then probably it was rotated and no new logs # were written since. # find files which were modified after $lasttime. the most recent one # is probably the former logfile. position at $lastoffset. # if this configurations does not care for rotations, there is nothing # we can do here. # $self->{logrotated} = 1; $self->{logmodified} = 1; $self->trace(sprintf "there is no logfile %s at this moment", $self->{logfile}); } elsif ($self->{laststate}->{devino} ne $self->getfilefingerprint($self->{logfile})) { # the inode changed (! the old inode could be reused) $self->trace(sprintf "this is not the same logfile %s != %s", $self->{laststate}->{devino}, $self->getfilefingerprint($self->{logfile})); $self->{logrotated} = 1; $self->{logmodified} = 1; # Ok, we need to make some changes here to handle a situation where the # inode is not changed on file rotation (since the writing app need # continuity) # 1) The last modified time is the same as that of the previously scanned # log file. Therefore it is the same file. No rotation or modification # 2) The last modified time is different, and the file is zero bytes: # - Modified = false # - Rotated = true # 3) The last modified time is different, and the file is not zero bytes # and is less than previous: # - Modified = true # - Rotated = true # 4) The last modified time is different, and the file is not zero bytes # and is more than previous: # - Modified = true # - Rotated = true (we can't actually tell, so need to play safe) } elsif ($filetime == $lastfiletime) { $self->trace(sprintf "Log file has the same modified time: %s ", scalar localtime($filetime)); $self->{laststate}->{logtime} = $filetime; } elsif ($filetime != $lastfiletime) { $self->trace(sprintf "Log file modified time: %s, last modified time: %s", scalar localtime($filetime), scalar localtime($lastfiletime)); if ($self->getfilesize($self->{logfile}) == 0) { $self->trace(sprintf "Log file is zero bytes"); $self->{logrotated} = 1; } else { $self->trace(sprintf "Log file is not zero bytes"); $self->{logrotated} = 1; $self->{logmodified} = 1; } } else { $self->trace("I HAVE NO IDEA WHAT HAPPENED"); } $self->trace(sprintf "Log offset: %i", $self->{laststate}->{logoffset}); return $self; } } sub collectfiles { my $self = shift; my @rotatedfiles = (); if ($self->{logrotated} && $self->{rotation}) { $self->trace("looking for rotated files in %s with pattern %s", $self->{archivedir}, $self->{filenamepattern}); if ($self->get_option('archivedirregexp')) { my $volume = undef; my @catdirs = (); my @dirs = split(/\//, $self->{archivedir}); foreach my $i (1..(scalar(@dirs) - $self->get_option('archivedirregexp'))) { push(@catdirs, shift @dirs); } my $searchdir = join('/', @catdirs); File::Find::find(sub { if (/^$self->{filenamepattern}/ && -f $_) { push(@rotatedfiles, $File::Find::name); } }, $searchdir); } else { opendir(DIR, $self->{archivedir}); @rotatedfiles = map { sprintf "%s/%s", $self->{archivedir}, $_; } grep /^$self->{filenamepattern}/, readdir(DIR); closedir(DIR); } #opendir(DIR, $self->{archivedir}); #@rotatedfiles = map { # sprintf "%s/%s", $self->{archivedir}, $_; #} grep /^$self->{filenamepattern}/, readdir(DIR); #closedir(DIR); # opendir(DIR, $self->{archivedir}); # read the filenames from DIR, match the filenamepattern, check the file age # open the file and return the handle # sort the handles by modification time #@rotatedfiles = sort { (stat $a->{fh})[9] <=> (stat $b->{fh})[9] } map { @rotatedfiles = sort { $a->{modtime} <=> $b->{modtime} } map { #if (/^$self->{filenamepattern}/) { #my $archive = sprintf "%s/%s", $self->{archivedir}, $_; my $archive = $_; $self->trace("archive %s matches (modified %s / accessed %s / inode %d / inode changed %s)", $archive, scalar localtime((stat $archive)[9]), scalar localtime((stat $archive)[8]), (stat $archive)[1], scalar localtime((stat $archive)[10])); if ((stat $archive)[9] >= $self->{laststate}->{logtime}) { $self->trace("archive %s was modified after %s", $archive, scalar localtime($self->{laststate}->{logtime})); my $fh = new IO::File; if (/.*\.gz\s*$/) { $self->trace("uncompressing %s with gzip -dc < %s|", $archive, $archive); if ($fh->open('gzip -dc < '.$archive.'|')) { ({ filename => $archive, fh => $fh, seekable => 0, modtime => (stat $archive)[9], fingerprint => $self->getfilefingerprint($archive).':'.$self->getfilesize($archive) }); } else { $self->trace("archive %s cannot be opened with gzip", $archive); (); } } elsif (/.*\.bz2\s*$/) { $self->trace("uncompressing %s with bzip2 -d < %s|", $archive, $archive); if ($fh->open('bzip2 -d < '.$archive.'|')) { ({ filename => $archive, fh => $fh, seekable => 0, modtime => (stat $archive)[9], fingerprint => $self->getfilefingerprint($archive).':'.$self->getfilesize($archive) }); } else { $self->trace("archive %s cannot be opened with bzip2", $archive); (); } } else { if ($fh->open($archive, "r")) { ({ filename => $archive, fh => $fh, seekable => 1, size => $self->getfilesize($fh), modtime => (stat $archive)[9], fingerprint => $self->getfilefingerprint($archive).':'.$self->getfilesize($archive) }); } else { $self->trace("archive %s cannot be opened", $archive); (); } } } else { (); } #} else { # (); #} } @rotatedfiles; # } readdir(DIR); # closedir(DIR); if (scalar(@rotatedfiles) == 0) { # # although a logfile rotation was detected, no archived files were found. # start seeking at position 0. # if (! $self->{NH_detection}) { $self->{laststate}->{logoffset} = 0; } else { # NH Commented this out, as we may find no rotated files, # in which case we need to use the current file offset again } $self->trace("although a logfile rotation was detected, no archived files were found"); } } if ($self->{logmodified}) { my $fh = new IO::File; # cygwin lets you open files even after chmodding them to 0000, so double check with -r if ($self->getfileisreadable($self->{logfile})) { $fh->open($self->{logfile}, "r"); $self->trace("opened logfile %s", $self->{logfile}); push(@rotatedfiles, { filename => $self->{logfile}, fh => $fh, seekable => 1, size => $self->getfilesize($self->{logfile}), fingerprint => $self->getfilefingerprint($self->{logfile}).':'.$self->getfilesize($self->{logfile}) }); $self->trace("logfile %s (modified %s / accessed %s / inode %d / inode changed %s)", $self->{logfile}, scalar localtime((stat $self->{logfile})[9]), scalar localtime((stat $self->{logfile})[8]), (stat $self->{logfile})[1], scalar localtime((stat $self->{logfile})[10])); } else { if (-e $self->{logfile}) { # permission problem $self->trace("could not open logfile %s", $self->{logfile}); $self->addevent('CRITICAL', sprintf "could not open logfile %s", $self->{logfile}); } else { if ($self->{options}->{logfilenocry}) { # logfiles which are not rotated but deleted and re-created may be missing # maybe a rotation situation, a typo in the configfile,... $self->trace("could not find logfile %s", $self->{logfile}); $self->addevent('UNKNOWN', sprintf "could not find logfile %s", $self->{logfile}); } else { # dont care. $self->trace("could not find logfile %s, but that's ok", $self->{logfile}); } } } } # now we have an array of structures each pointing to a file # which has been rotated since the last scan plus the current logfile. # the array members are sorted by modification time of the files. # now duplicate entries are removed. in one scenario the current logfile is # a symbolic link to a file which uses the same naming schema as the rotated # logfiles. $self->trace(sprintf "first relevant files: %s", join(", ", map { basename $_->{filename} } @rotatedfiles)); my %seen = (); @rotatedfiles = reverse map { $self->trace("%s has fingerprint %s", $_->{filename}, $_->{fingerprint}); # because of the windows dummy devino 0:0, we need to add the size if (exists $seen{$_->{fingerprint}}) { $self->trace("skipping %s (identical to %s)", $_->{filename}, $seen{$_->{fingerprint}}); (); } else { $seen{$_->{fingerprint}} = $_->{filename}; $_; } } reverse @rotatedfiles; if (0 && (scalar(@rotatedfiles) == 1) && ($rotatedfiles[0]->{filename} eq $self->{logfile}) && ! $self->get_option('randominode')) { # somehow rotated (devino has changed) but there are no rotated files # maybe logfile was rotated=deleted and recreated # a very special case which i found when i wrote 087randominode.t $self->{laststate}->{logoffset} = 0; } elsif (@rotatedfiles && (exists $rotatedfiles[0]->{size}) && ($rotatedfiles[0]->{size} < $self->{laststate}->{logoffset})) { $self->trace(sprintf "file %s is too short (%d < %d). this should not happen. reset", $rotatedfiles[0]->{filename}, $rotatedfiles[0]->{size}, $self->{laststate}->{logoffset}); if ($self->{NH_detection}) { # NH In this case, we have replaced the files, so set to beginning $self->{laststate}->{logoffset} = 0; } else { $self->{laststate}->{logoffset} = $rotatedfiles[0]->{size}; } } $self->trace(sprintf "relevant files: %s", join(", ", map { basename $_->{filename} } @rotatedfiles)); $self->{relevantfiles} = \@rotatedfiles; } sub prepare { my $self = shift; if ("LOGLOGDATE8GZ" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf '^%s[\.\-]{0,1}[0-9]{8}\.gz$', $self->{logbasename}; } elsif ("LOGLOGDATE8BZ2" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf '^%s[\.\-]{0,1}[0-9]{8}\.bz2$', $self->{logbasename}; } elsif ("LOGLOG0LOG1GZ" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf '^%s\.((0)|([1-9]+\.gz))$', $self->{logbasename}; } elsif ("LOGLOG0GZLOG1GZ" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf '^%s\.((0)|([1-9]+[0-9]*))\.gz$', $self->{logbasename}; } elsif ("LOGLOG0LOG1" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf '^%s\.((0)|([1-9]+[0-9]*))$', $self->{logbasename}; } elsif ("SUSE" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf "%s.*[0-9]*.gz", $self->{logbasename}; } elsif ("DEBIAN" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf "%s.0|%s.*[0-9]*.gz", $self->{logbasename}, $self->{logbasename}; } elsif ("QMAIL" eq uc($self->{rotation})) { $self->{filenamepattern} = "\@.*"; } elsif ("LOGROTATE" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf "%s.*[0-9]*.gz", $self->{logbasename}; } elsif ("SOLARIS" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf "%s.*\\.[0-9]+", $self->{logbasename}; } elsif ("HPUX" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf "OLD%s", $self->{logbasename}; } elsif ("BMWHPUX" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf 'OLD%s|%s\\.[A-Z][0-9]+_[0-9]+\\.gz$', $self->{logbasename}, $self->{logbasename}; } elsif ("MOD_LOG_ROTATE" eq uc($self->{rotation})) { $self->{filenamepattern} = sprintf 'access\.log\.\d{10}'; bless $self, "Nagios::CheckLogfiles::Search::Rotating::Uniform"; $self->prepare(); } else { $self->{filenamepattern} = $self->{rotation}; $self->resolve_macros_in_pattern(\$self->{filenamepattern}); } return $self; } package Nagios::CheckLogfiles::Search::Rotating::Uniform; use strict; use Exporter; use File::Basename; use File::Find; use vars qw(@ISA); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; @ISA = qw(Nagios::CheckLogfiles::Search::Rotating); sub new { my $self = bless {}, shift; return $self->init(shift); } sub prepare { my $self = shift; my $params = shift; my @matchingfiles = (); if (! $self->{filenamepattern}) { $self->{filenamepattern} = $self->{rotation}; $self->resolve_macros_in_pattern(\$self->{filenamepattern}); } # find newest rotatingpattern = logfile if ($self->get_option('archivedirregexp')) { my $volume = undef; my @catdirs = (); my @dirs = split(/\//, $self->{archivedir}); foreach my $i (1..(scalar(@dirs) - $self->get_option('archivedirregexp'))) { push(@catdirs, shift @dirs); } my $searchdir = join('/', @catdirs); File::Find::find(sub { if (/^$self->{filenamepattern}/ && -f $_) { push(@matchingfiles, $File::Find::name); } }, $searchdir); @matchingfiles = sort { $a->{modtime} <=> $b->{modtime} } map { my $archive = $_; ({ filename => $archive, modtime => (stat $archive)[9]}); } @matchingfiles; } else { opendir(DIR, $self->{archivedir}); @matchingfiles = sort { $a->{modtime} <=> $b->{modtime} } map { my $archive = $_; ({ filename => $archive, modtime => (stat $archive)[9]}); } map { sprintf "%s/%s", $self->{archivedir}, $_; } grep /^$self->{filenamepattern}/, readdir(DIR); closedir(DIR); } #opendir(DIR, $self->{archivedir}); #@matchingfiles = sort { $a->{modtime} <=> $b->{modtime} } map { # if (/^$self->{filenamepattern}/) { # my $archive = sprintf "%s/%s", $self->{archivedir}, $_; # ({ filename => $archive, modtime => (stat $archive)[9]}); # } else { # (); # } #} readdir(DIR); #closedir(DIR); if (@matchingfiles) { $self->{logfile} = $matchingfiles[-1]->{filename}; $self->{macros}->{CL_LOGFILE} = $self->{logfile}; $self->{privatestate}->{logfile} = $self->{logfile}; $self->trace("the newest uniform logfile i found is %s", $self->{logfile}); } else { $self->{logfile} = $self->{archivedir}.'/logfilenotfound'; $self->trace("i found no uniform logfiles in %s", $self->{archivedir}); } $self->construct_seekfile(); } sub construct_seekfile { my $self = shift; # modify seekfilename so it can be found even if the logfile has changed $self->{logbasename} = basename($self->{logfile}); if ($self->get_option('archivedirregexp')) { $self->{seekfilebase} = '/regexpuniformlogfile'; } else { $self->{seekfilebase} = dirname($self->{logfile}).'/uniformlogfile'; } $self->{seekfilebase} =~ s/\//_/g; $self->{seekfilebase} =~ s/\\/_/g; $self->{seekfilebase} =~ s/:/_/g; $self->{seekfilebase} =~ s/\s/_/g; $self->{seekfile} = sprintf "%s/%s.%s.%s", $self->{seekfilesdir}, $self->{cfgbase}, $self->{seekfilebase}, $self->{tag} eq "default" ? "seek" : $self->{tag}; $self->{pre3seekfile} = sprintf "/tmp/%s.%s.%s", $self->{cfgbase}, $self->{seekfilebase}, $self->{tag} eq "default" ? "seek" : $self->{tag}; $self->{pre2seekfile} = sprintf "%s/%s.%s.%s", $self->{seekfilesdir}, $self->{cfgbase}, $self->{logbasename}, $self->{tag} eq "default" ? "seek" : $self->{tag}; $self->trace("rewrote uniform seekfile to %s", $self->{seekfile}); return $self; } package Nagios::CheckLogfiles::Search::Virtual; use strict; use Exporter; use File::Basename; use vars qw(@ISA); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; @ISA = qw(Nagios::CheckLogfiles::Search); sub new { my $self = bless {}, shift; return $self->init(shift); } sub loadstate { my $self = shift; $self->{laststate}->{logoffset} = 0; } sub savestate { } sub analyze_situation { my $self = shift; $self->{logmodified} = 1; } sub collectfiles { my $self = shift; my @rotatedfiles = (); my $fh = new IO::File; if ($self->getfileisreadable($self->{logfile})) { $fh->open($self->{logfile}, "r"); $self->trace("opened logfile %s", $self->{logfile}); push(@rotatedfiles, { filename => $self->{logfile}, fh => $fh, seekable => 1 }); } else { if (-e $self->{logfile}) { # permission problem $self->trace("could not open logfile %s", $self->{logfile}); $self->addevent('CRITICAL', sprintf "could not open logfile %s", $self->{logfile}); } else { if ($self->{options}->{logfilenocry}) { $self->trace("could not find logfile %s", $self->{logfile}); $self->addevent('UNKNOWN', sprintf "could not find logfile %s", $self->{logfile}); } else { # dont care. $self->trace("could not find logfile %s, but that's ok", $self->{logfile}); } } } $self->{relevantfiles} = \@rotatedfiles; } package Nagios::CheckLogfiles::Search::Prescript; use strict; use Exporter; use File::Basename; use vars qw(@ISA); @ISA = qw(Nagios::CheckLogfiles::Search); sub new { my $self = bless {}, shift; return $self->init(shift); } sub init { my $self = shift; my $params = shift; $self->{tag} = "prescript"; $self->{scriptpath} = $params->{scriptpath}; $self->{macros} = $params->{macros}; $self->{tracefile} = $params->{tracefile}; $self->{cfgbase} = $params->{cfgbase}; $self->{logbasename} = "prescript"; $self->{script} = $params->{script}; $self->{scriptparams} = $params->{scriptparams}; $self->{scriptstdin} = $params->{scriptstdin}; $self->{scriptdelay} = $params->{scriptdelay}; $self->default_options({ script => 0, protocol => 0, count => 1, smartscript => 0, supersmartscript => 0, report => 'short', seekfileerror => 'critical', }); $self->{matchlines} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] }; $self->{lastmsg} = { OK => "", WARNING => "", CRITICAL => "", UNKNOWN => "" }; $self->{trace} = -e $self->{tracefile} ? 1 : 0; $self->refresh_options($params->{options}); $self->{exitcode} = 0; $self->{macros}->{CL_LOGFILE} = $params->{cfgbase}; $self->{macros}->{CL_TAG} = $self->{tag}; $self->{macros}->{CL_SERVICESTATEID} = $ERRORS{OK}; $self->{macros}->{CL_SERVICEOUTPUT} = "OK - starting up"; $self->{macros}->{CL_PATTERN_NUMBER} = 0; return $self; } sub run { my $self = shift; $self->trace("call (%s) prescript %s", $self->{options}->{smartscript} ? "smart" : "dumb", $self->{script}); my ($actionsuccess, $actionrc, $actionoutput) = $self->action($self->{script}, $self->{scriptparams}, $self->{scriptstdin}, $self->{scriptdelay}, $self->{options}->{smartscript}, $self->{privatestate}); if (! $actionsuccess) { $self->{options}->{count} = 1; $self->{options}->{protocol} = 1; $self->addevent('WARNING', sprintf "cannot execute %s", $self->{script}); } elsif ($self->{options}->{smartscript}) { if ($actionrc) { $actionoutput = "prescript" if ! $actionoutput; $self->addevent($actionrc, $actionoutput); } } $self->{exitcode} = $actionrc; } package Nagios::CheckLogfiles::Search::Postscript; use strict; use Exporter; use File::Basename; use vars qw(@ISA); @ISA = qw(Nagios::CheckLogfiles::Search); sub new { my $self = bless {}, shift; return $self->init(shift); } sub init { my $self = shift; my $params = shift; $self->{tag} = "postscript"; $self->{scriptpath} = $params->{scriptpath}; $self->{macros} = $params->{macros}; $self->{tracefile} = $params->{tracefile}; $self->{cfgbase} = $params->{cfgbase}; $self->{logbasename} = "postscript"; $self->{script} = $params->{script}; $self->{scriptparams} = $params->{scriptparams}; $self->{scriptstdin} = $params->{scriptstdin}; $self->{scriptdelay} = $params->{scriptdelay}; $self->{privatestate} = $params->{privatestate}; $self->default_options({ script => 0, protocol => 0, count => 1, smartscript => 0, supersmartscript => 0, report => 'short', seekfileerror => 'critical', }); $self->{matchlines} = { OK => [], WARNING => [], CRITICAL => [], UNKNOWN => [] }; $self->{lastmsg} = { OK => "", WARNING => "", CRITICAL => "", UNKNOWN => "" }; $self->{trace} = -e $self->{tracefile} ? 1 : 0; $self->refresh_options($params->{options}); $self->{exitcode} = 0; $self->{macros}->{CL_LOGFILE} = $params->{cfgbase}; $self->{macros}->{CL_TAG} = $self->{tag}; $self->{macros}->{CL_SERVICESTATEID} = 0; # will be set in SUPER::run() $self->{macros}->{CL_SERVICEOUTPUT} = ""; # will be set in SUPER::run() $self->{macros}->{CL_PATTERN_NUMBER} = 0; return $self; } sub run { my $self = shift; $self->trace("call postscript %s", $self->{script}); my ($actionsuccess, $actionrc, $actionoutput) = $self->action($self->{script}, $self->{scriptparams}, $self->{scriptstdin}, $self->{scriptdelay}, $self->{options}->{smartscript}, $self->{privatestate}); if (! $actionsuccess) { $self->{options}->{count} = 1; $self->{options}->{protocol} = 1; $self->addevent('WARNING', sprintf "cannot execute %s", $self->{script}); $actionrc = 2; } elsif ($self->{options}->{smartscript}) { if ($actionrc || $self->{options}->{supersmartscript}) { # strings containing 0 must be treated like a true value #$actionoutput = "postscript" if ! $actionoutput; $actionoutput = "postscript" unless $actionoutput || $actionoutput =~ /0[0\.]*/; $self->addevent($actionrc, $actionoutput); } } $self->{exitcode} = $actionrc; } package Nagios::CheckLogfiles::Search::Dummy; use strict; use Exporter; use File::Basename; use Time::Local; use IO::File; use vars qw(@ISA); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; @ISA = qw(Nagios::CheckLogfiles::Search); sub new { my $self = bless {}, shift; return $self->init(shift); } sub init { my $self = shift; my $params = shift; $self->{logfile} = sprintf "%s/dummy.%s", $self->{seekfilesdir}, $self->{tag}; $self->SUPER::init($params); } sub prepare { my $self = shift; $self->{options}->{nologfilenocry} = 1; } sub loadstate { my $self = shift; $self->SUPER::loadstate(); } sub savestate { my $self = shift; } sub analyze_situation { my $self = shift; } sub collectfiles { my $self = shift; } package Nagios::CheckLogfiles::Search::Errpt; use strict; use Exporter; use File::Basename; use Time::Local; use IO::File; use vars qw(@ISA); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; @ISA = qw(Nagios::CheckLogfiles::Search); sub new { my $self = bless {}, shift; return $self->init(shift); } sub init { my $self = shift; my $params = shift; $self->{logfile} = sprintf "%s/errpt.%s", $self->{seekfilesdir}, $self->{tag}; $self->SUPER::init($params); $self->{clo} = { path => $params->{errpt}->{path} ? $params->{errpt}->{path} : "errpt", errortype => $params->{errpt}->{errortype}, errorclass => $params->{errpt}->{errorclass}, errorlabel => $params->{errpt}->{errorlabel}, errorresource => $params->{errpt}->{errorresource}, }; $self->addfilter(0, 'IDENTIFIER TIMESTAMP'); } sub prepare { my $self = shift; $self->{options}->{nologfilenocry} = 1; # the last minute is the end time. in-progess minutes are not # interesting yet. my($sec, $min, $hour, $mday, $mon, $year) = #(localtime $self->{macros}->{CL_DATE_TIMESTAMP})[0, 1, 2, 3, 4, 5]; # macro is not suitable for testing because it is not updated (localtime time)[0, 1, 2, 3, 4, 5]; $self->{errpt}->{endtime} = timelocal(0, $min, $hour, $mday, $mon, $year) - 60; } sub loadstate { my $self = shift; $self->SUPER::loadstate(); # always scan the whole output. thst's what starttime is for. $self->{laststate}->{logoffset} = 0; # if this is the very first run, look back 5 mintes in the past. $self->{errpt}->{starttime} = $self->{laststate}->{logtime} ? $self->{laststate}->{logtime} + 60 : $self->{errpt}->{endtime} - 300; } sub savestate { my $self = shift; foreach (keys %{$self->{laststate}}) { $self->{newstate}->{$_} = $self->{laststate}->{$_}; } # remember the last minute scanned. $self->{newstate}->{logtime} = $self->{errpt}->{endtime}; $self->SUPER::savestate(); } sub analyze_situation { my $self = shift; if ($self->{errpt}->{starttime} <= $self->{errpt}->{endtime}) { $self->{logmodified} = 1; } else { # this happens if you call the plugin in too short intervals. $self->trace("%s not before %s", scalar localtime $self->{errpt}->{starttime}, scalar localtime $self->{errpt}->{endtime}); } } sub collectfiles { my $self = shift; my $fh = new IO::File; if ($self->{logmodified}) { my($sec, $min, $hour, $mday, $mon, $year) = (localtime $self->{errpt}->{starttime})[0, 1, 2, 3, 4, 5]; $self->{errpt}->{ibmstarttime} = sprintf "%02d%02d%02d%02d%02d", $mon + 1, $mday, $hour, $min, substr($year + 1900, 2, 2); ($sec, $min, $hour, $mday, $mon, $year) = (localtime $self->{errpt}->{endtime})[0, 1, 2, 3, 4, 5]; $self->{errpt}->{ibmendtime} = sprintf "%02d%02d%02d%02d%02d", $mon + 1, $mday, $hour, $min, substr($year + 1900, 2, 2); my $errpt = sprintf "%s -s %s -e %s %s %s %s %s|", $self->{clo}->{path}, $self->{errpt}->{ibmstarttime}, $self->{errpt}->{ibmendtime}, $self->{clo}->{errortype} ? '-T '.$self->{clo}->{errortype} : "", $self->{clo}->{errorclass} ? '-d '.$self->{clo}->{errorclass} : "", $self->{clo}->{errorlabel} ? '-J '.$self->{clo}->{errorlabel} : "", $self->{clo}->{errorresource} ? '-N '.$self->{clo}->{errorresource} : ""; $self->trace("calling %s", $errpt); $self->trace("calling errpt -s (%s) -e (%s)", scalar localtime $self->{errpt}->{starttime}, scalar localtime $self->{errpt}->{endtime}); if ($fh->open($errpt)) { push(@{$self->{relevantfiles}}, { filename => "errpt|", fh => $fh, seekable => 0, modtime => $self->{errpt}->{endtime}, fingerprint => "0:0" }); } else { $self->trace("cannot execute errpt"); $self->addevent('UNKNOWN', "cannot execute errpt"); } } } package Nagios::CheckLogfiles::Search::Ipmitool; # http://download.intel.com/design/servers/ipmi/IPMIv2_0rev1_0.pdf # # SEL Entries have a unique `Record ID' field. This field is used for # retrieving log entries from the SEL. SEL reading can be done in # a `random access' manner. That is, SEL Entries can be read in any # order assuming that the Record ID is known. # SEL Record IDs 0000h and FFFFh are reserved for functional use # and are not legal ID values. Record IDs are handles. They are not # required to be sequential or consecutive. Applications should not # assume that SEL Record IDs will follow any particular numeric ordering. # # Man beachte die letzten beiden Saetze. Sollte der dafuer Verantwortliche # diese Zeilen lesen: Ich finde dich, du Schwein! use strict; use Exporter; use File::Basename; use Time::Local; use IO::File; use vars qw(@ISA); require Digest::MD5; # qw(md5_base64); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; @ISA = qw(Nagios::CheckLogfiles::Search::Simple); sub new { my $self = bless { eventids => [], eventbuffer => [], }, shift; return $self->init(shift); } sub init { my $self = shift; my $params = shift; $self->{logfile} = sprintf "%s/ipmitool.%s", $self->{seekfilesdir}, $self->{tag}; $self->SUPER::init($params); $self->{clo} = { path => $params->{ipmitool}->{path} ? $params->{ipmitool}->{path} : "ipmitool", ## cache => exists $params->{ipmitool}->{cache} ? 1 : 0, ## using a local cache makes no sense here ## maybe checking remote sdr will be a feature in the future listcmd => exists $params->{ipmitool}->{elist} ? "elist" : "list" }; } sub prepare { my $self = shift; $self->{options}->{nologfilenocry} = 1; $self->{logfile} = sprintf "%s/ipmitool.sel.dump.%s", $self->system_tempdir(), $self->{tag}; $self->{sdrcache} = sprintf "%s/ipmitool.sdr.cache", $self->system_tempdir(); if ($self->{clo}->{cache} && (! -f $self->{sdrcache} || ((time - ($self->{sdrcache})[9]) > 86400))) { ## $self->trace("creating/refreshing sdr cache %s", $self->{sdrcache}); ## system($self->{clo}->{path}.' sdr dump '.$self->{sdrcache}.' >/dev/null 2>&1'); } unlink $self->{logfile}; my $ipmitool_sel_list = sprintf "%s %s sel %s 2>&1 |", $self->{clo}->{path}, $self->{clo}->{cache} ? "-S $self->{sdrcache}" : "", $self->{clo}->{listcmd}; my $ipmitool_fh = new IO::File; my $spool_fh = new IO::File; $self->trace("executing %s", $ipmitool_sel_list); # 8 | 08/10/2007 | 15:09:00 | Power Unit #0x01 | Power off/down # 9 | Pre-Init Time-stamp | Chassis #0xa9 | State Asserted if ($ipmitool_fh->open($ipmitool_sel_list)) { while (my $event = $ipmitool_fh->getline()) { chomp $event; next if $event =~ /SEL has no entries/; push(@{$self->{eventlog}->{eventbuffer}}, $event); } $ipmitool_fh->close(); } $self->trace("wrote spoolfile %s", $self->{logfile}); } sub loadstate { my $self = shift; $self->SUPER::loadstate(); $self->{eventlog}->{last_eventids} = $self->{laststate}->{eventids} || []; $self->{laststate}->{logoffset} = 0; } sub savestate { my $self = shift; foreach (keys %{$self->{laststate}}) { $self->{newstate}->{$_} = $self->{laststate}->{$_}; } $self->{newstate}->{eventids} = $self->{eventlog}->{eventids}; $self->SUPER::savestate(); } sub analyze_situation { my $self = shift; my $spool_fh = new IO::File; if ($spool_fh->open('>'.$self->{logfile})) { foreach my $event (@{$self->{eventlog}->{eventbuffer}}) { if ($event =~ /^\s*(\w+)\s*\|/) { my $eventid = $1; push(@{$self->{eventlog}->{eventids}}, $eventid); if (! grep { $eventid eq $_ } @{$self->{eventlog}->{last_eventids}}) { $self->trace("found new eventid %s", $eventid); $event =~ s/\|/;/g; $spool_fh->printf("%s\n", $event); $self->{logmodified} = 1; $self->{logrotated} = 1; } } else { $self->trace("no match eventid %s", $event); } } $spool_fh->close(); } } sub rewind { my $self = shift; $self->loadstate(); foreach (keys %{$self->{laststate}}) { $self->{newstate}->{$_} = $self->{laststate}->{$_}; } $self->addevent(0, "reset"); $self->{newstate}->{eventids} = []; $self->savestate(); return $self; } package Nagios::CheckLogfiles::Search::Oraclealertlog; use strict; use Exporter; use File::Basename; use Time::Local; use IO::File; use vars qw(@ISA); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; @ISA = qw(Nagios::CheckLogfiles::Search); sub new { my $self = bless {}, shift; return $self->init(shift); } sub init { my $self = shift; my $params = shift; $self->{oraalert}->{tns} = { connect => $params->{oraclealertlog}->{connect} || $params->{oraclealertlog}->{sid}, username => $params->{oraclealertlog}->{username}, password => $params->{oraclealertlog}->{password}, }; $self->{logfile} = sprintf "%s/alertlog.%s.%s", $self->{seekfilesdir}, $self->{tag}, $self->{oraalert}->{tns}->{connect}; $self->SUPER::init($params); $self->resolve_macros(\$self->{oraalert}->{tns}->{connect}); $self->resolve_macros(\$self->{oraalert}->{tns}->{username}); $self->resolve_macros(\$self->{oraalert}->{tns}->{password}); } sub prepare { my $self = shift; $self->{options}->{nologfilenocry} = 1; # the last second is the end time. in-progess seconds are not # interesting yet. $self->{oraalert}->{highestfound} = 0; } sub loadstate { my $self = shift; $self->SUPER::loadstate(); # always scan the whole output. thst's what starttime is for. $self->{laststate}->{logoffset} = 0; # if this is the very first run, look back 5 mintes in the past. # hopefully the clocks are synchronized $self->{laststate}->{logtime} = $self->{laststate}->{logtime} ? $self->{laststate}->{logtime} : time - 300; } sub savestate { my $self = shift; foreach (keys %{$self->{laststate}}) { $self->{newstate}->{$_} = $self->{laststate}->{$_}; } # remember the last second scanned. $self->{newstate}->{logtime} = $self->{oraalert}->{highestfound} ? $self->{oraalert}->{highestfound} : $self->{laststate}->{logtime}; $self->SUPER::savestate(); } sub analyze_situation { my $self = shift; $self->trace("last scanned until %s", scalar localtime $self->{laststate}->{logtime}); $self->{logmodified} = 1; } sub collectfiles { my $self = shift; my $fh = new IO::File; if ($self->{logmodified}) { # open database connection and select rows created # since $self->{laststate}->{logtime} and now (db now, not plugin now) my $linesread = 0; eval { require DBI; if (my $dbh = DBI->connect( sprintf("DBI:Oracle:%s", $self->{oraalert}->{tns}->{connect}), $self->{oraalert}->{tns}->{username}, $self->{oraalert}->{tns}->{password}, { RaiseError => 1, PrintError => 0 })) { $dbh->do(q{ ALTER SESSION SET NLS_NUMERIC_CHARACTERS=".," }); # suchen bis zur letzten abgeschlossenen sekunde (inklusive) my $sql = q{ SELECT alert_timestamp, alert_text FROM alert_log WHERE ROUND(alert_timestamp) > ? AND alert_date <= SYSDATE - 1/86400 ORDER BY alert_timestamp }; if (my $sth = $dbh->prepare($sql)) { $self->trace(sprintf "select events between %d and now (%s and sysdate())", $self->{laststate}->{logtime}, scalar localtime $self->{laststate}->{logtime}); $sth->execute($self->{laststate}->{logtime}); if (my $fh = new IO::File($self->{logfile}, "w")) { while(my($alert_timestamp, $alert_text) = $sth->fetchrow_array()) { next if ! $alert_text; # es gibt auch leere Zeilen # bei ora-perl-conversion gibts manchmal 1234567890.999999999 $alert_timestamp = int(0.5 + $alert_timestamp); $fh->printf("%s %s\n", scalar localtime $alert_timestamp, $alert_text); $self->{oraalert}->{highestfound} = $alert_timestamp; $linesread++; } $fh->close(); } $sth->finish(); } $dbh->disconnect(); } }; if ($@) { $self->trace(sprintf "database operation failed: %s", $@); $self->addevent('UNKNOWN', sprintf "database operation failed: %s", $@); } $self->trace(sprintf "read %d lines from database", $linesread); if ($linesread) { if (my $fh = new IO::File($self->{logfile}, "r")) { $self->trace(sprintf "reopen logfile"); push(@{$self->{relevantfiles}}, { filename => "eventlog|", fh => $fh, seekable => 0, modtime => time, # not relevant because already analyzed fingerprint => "0:0" }); } } } } package Nagios::CheckLogfiles::Search::Esxdiag; use strict; use Exporter; use File::Basename; use Time::Local; use IO::File; use vars qw(@ISA); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; @ISA = qw(Nagios::CheckLogfiles::Search); sub new { my $self = bless {}, shift; return $self->init(shift); } sub init { my $self = shift; my $params = shift; $self->{esxdiag}->{connect} = { server => $params->{esxdiag}->{server}, # host => $params->{esxdiag}->{server} ? # wenn server = datacenter $params->{esxdiag}->{host} : undef, username => $params->{esxdiag}->{username}, password => $params->{esxdiag}->{password}, log => $params->{esxdiag}->{log} || 'hostd', }; $self->{esxdiag}->{connect}->{url} = sprintf 'https://%s/sdk/webService', $self->{esxdiag}->{connect}->{server}; $self->{logfile} = sprintf "%s/esxdiag.%s_%s_%s", $self->{seekfilesdir}, $self->{esxdiag}->{connect}->{log}, $self->{esxdiag}->{connect}->{server}, $self->{esxdiag}->{connect}->{host} ? $self->{esxdiag}->{connect}->{host} : 'host', $self->{tag}; $self->{esxdiag}->{connect}->{token} = sprintf "%s/esxtok.%s_%s", $self->{seekfilesdir}, $self->{esxdiag}->{connect}->{server}, $self->{esxdiag}->{connect}->{host} ? $self->{esxdiag}->{connect}->{host} : 'host'; # virtualcenter + host # host managed by vc # virtualcenter. default logs = vc logs # host # esx server $self->SUPER::init($params); } sub prepare { my $self = shift; $self->{options}->{nologfilenocry} = 1; } sub loadstate { my $self = shift; $self->SUPER::loadstate(); # if this is the very first run, use an insane offet $self->{laststate}->{lineend} = $self->{laststate}->{lineend} ? ($self->{laststate}->{lineend} + 1) : 999999; } sub savestate { my $self = shift; foreach (keys %{$self->{laststate}}) { $self->{newstate}->{$_} = $self->{laststate}->{$_}; } $self->SUPER::savestate(); } sub analyze_situation { my $self = shift; $self->{logmodified} = 1; } sub collectfiles { my $self = shift; my $fh = new IO::File; if ($self->{logmodified}) { my $linesread = 0; eval { require VMware::VIRuntime; my %loginparams = ( service_url => $self->{esxdiag}->{connect}->{url}, user_name => $self->{esxdiag}->{connect}->{username}, password => $self->{esxdiag}->{connect}->{password}, ); my $vim = undef; eval { # das bringt's nicht. login ist genauso schnell #$vim = Vim::load_session( # service_url => $self->{esxdiag}->{connect}->{url}, # session_file => $self->{esxdiag}->{connect}->{token}); $vim = Vim::login(%loginparams) if ! $vim; #Vim::save_session( # session_file => $self->{esxdiag}->{connect}->{token}); }; if ($vim) { my $instance_type = Vim::get_service_content()->about->apiType; my $diagmgr = Vim::get_service_content()->diagnosticManager(); my $diagmgr_view = $diagmgr ? Vim::get_view(mo_ref => $diagmgr) : undef; if ($diagmgr_view) { my $host_view = undef; my $logdata = undef; if ($instance_type eq 'VirtualCenter') { my $host_views = Vim::find_entity_views( view_type => 'HostSystem', filter => {'name' => $self->{esxdiag}->{host}}, properties => ['name']); $host_view = $host_views->[0] if $host_views; } else { $host_view = Vim::find_entity_view( view_type => 'HostSystem', properties => ['name']); # increases the speed dramatically } if ($host_view) { my %browseparams = ( key => $self->{esxdiag}->{connect}->{log}, start => $self->{laststate}->{lineend}, ); $browseparams{host} = $self->{esxdiag}->{connect}->{host} if $self->{esxdiag}->{connect}->{host}; # VirtualCenter $self->trace(sprintf 'browsing view for host %s', $host_view->name); $self->trace(sprintf 'start reading at line %d', $self->{laststate}->{lineend}); $logdata = $diagmgr_view->BrowseDiagnosticLog(%browseparams); $self->trace(sprintf 'log interval is %d..%d', $logdata->lineStart, $logdata->lineEnd); if ($logdata->lineStart < $self->{laststate}->{lineend}) { # rotation, # z.b. "start reading at line 4133"-> "log interval is 0..43" $browseparams{start} = $logdata->lineStart; $logdata = $diagmgr_view->BrowseDiagnosticLog(%browseparams); $self->trace(sprintf 'rotation detected. new log interval %d..%d', $logdata->lineStart, $logdata->lineEnd); } $self->{laststate}->{lineend} = $logdata->lineEnd; if ($logdata->lineText) { if (my $fh = new IO::File($self->{logfile}, 'w')) { foreach my $line (@{$logdata->lineText}) { $fh->printf("%s\n", $line); $linesread++; } $fh->close(); } } else { $self->trace('nothing to do'); } } else { $self->trace('no host view'); } } else { $self->trace('no diag manager view'); } Vim::logout(); # auskommentieren, wenn sessions benutzt werden } else { chomp $@ if $@; $self->trace(sprintf 'unable to connect %s', $@); $self->addevent('UNKNOWN', sprintf 'unable to connect %s', $@); } }; if ($@) { $self->trace(sprintf "vi api operation failed: %s", $@); $self->addevent('UNKNOWN', sprintf "vi api operation failed: %s", $@); } $self->trace(sprintf "read %d lines from esx server", $linesread); if ($linesread) { if (my $fh = new IO::File($self->{logfile}, "r")) { $self->trace(sprintf "reopen logfile"); push(@{$self->{relevantfiles}}, { filename => "esxdiag", fh => $fh, seekable => 1, modtime => time, fingerprint => "0:0" }); } } } } package main; use strict; use utf8; use File::Basename; use File::Find; use Getopt::Long; #import Devel::TraceMethods qw( # Nagios::CheckLogfiles # Nagios::CheckLogfiles::Search # Nagios::CheckLogfiles::Search::Simple # Nagios::CheckLogfiles::Search::Rotating # Nagios::CheckLogfiles::Search::Rotating::Uniform # Nagios::CheckLogfiles::Search::Virtual # Nagios::CheckLogfiles::Search::Prescript # Nagios::CheckLogfiles::Search::Postscript # Nagios::Tivoli::Config::Logfile # Nagios::Tivoli::Config::Logfile::Format # Nagios::Tivoli::Config::Logfile::Hit #); #Devel::TraceMethods::callback ( # 'Nagios::CheckLogfiles' => \&logger, # 'Nagios::CheckLogfiles::Search' => \&logger, # 'Nagios::CheckLogfiles::Search::Simple' => \&logger, # 'Nagios::CheckLogfiles::Search::Rotating' => \&logger, # 'Nagios::CheckLogfiles::Search::Rotating::Uniform' => \&logger, # 'Nagios::CheckLogfiles::Search::Virtual' => \&logger, # 'Nagios::CheckLogfiles::Search::Prescript' => \&logger, # 'Nagios::CheckLogfiles::Search::Postscript' => \&logger, # 'Nagios::Tivoli::Config::Logfile' => \&logger, # 'Nagios::Tivoli::Config::Logfile::Format' => \&logger, # 'Nagios::Tivoli::Config::Logfile::Hit' => \&logger, #); use constant OK => 0; use constant WARNING => 1; use constant CRITICAL => 2; use constant UNKNOWN => 3; Getopt::Long::Configure qw(no_ignore_case); # compatibility with old perls use vars qw (%commandline); my @cfgfiles = (); my $needs_restart = 0; my $enough_info = 0; sub logger { my $method = shift; my @args = @_; printf STDERR "%s\n", $method; printf STDERR " %s\n", Data::Dumper::Dumper(\@args); } my $plugin_revision = '$Revision: 1.0 $ '; my $progname = basename($0); sub print_version { printf "%s v3.4.2\n", basename($0); } sub print_help { print < The configfile looks like this: \$seekfilesdir = '/opt/nagios/var/tmp'; # where the state information will be saved. \$protocolsdir = '/opt/nagios/var/tmp'; # where protocols with found patterns will be stored. \$scriptpath = '/opt/nagios/var/tmp'; # where scripts will be searched for. \$MACROS = \{ CL_DISK01 => "/dev/dsk/c0d1", CL_DISK02 => "/dev/dsk/c0d2" \}; \@searches = ( { tag => 'temperature', logfile => '/var/adm/syslog/syslog.log', rotation => 'bmwhpux', criticalpatterns => ['OVERTEMP_EMERG', 'Power supply failed'], warningpatterns => ['OVERTEMP_CRIT', 'Corrected ECC Error'], options => 'script,protocol,nocount', script => 'sendnsca_cmd' }, { tag => 'scsi', logfile => '/var/adm/messages', rotation => 'solaris', criticalpatterns => 'Sense Key: Not Ready', criticalexceptions => 'Sense Key: Not Ready /dev/testdisk', options => 'noprotocol' }, { tag => 'logins', logfile => '/var/adm/messages', rotation => 'solaris', criticalpatterns => ['illegal key', 'read error.*\$CL_DISK01\$'], criticalthreshold => 4 warningpatterns => ['read error.*\$CL_DISK02\$'], } ); EOTXT } sub print_usage { print < [--searches=tag1,tag2,...] check_logfiles [-t timeout] --logfile= --tag= --rotation= --criticalpattern= --warningpattern= EOTXT } %commandline = (); my @params = ( "timeout|t=i", "version|V", "help|h", "debug|d", "verbose|v", # # # "environment|e=s%", "daemon:i", "report=s", "reset", # # # "install", "deinstall", "service=s", "username=s", "password=s", # # which searches # "config|f=s", "configdir|F=s", "searches=s", "selectedsearches=s", # # globals # "seekfilesdir=s", "protocolsdir=s", "protocolsretention=i", "macro=s%", "seekfileerror=s", # # search # "template=s", "tag=s", "logfile=s", "rotation=s", "tivolipattern=s", "criticalpattern=s", "criticalexception=s", "warningpattern=s", "warningexception=s", "okpattern=s", "type=s", "archivedir=s", # # search options # "noprotocol", "nocase", "nologfilenocry", "maxlength=i", "syslogserver", "syslogclient=s", "sticky:s", "noperfdata", "winwarncrit", "lookback=s", "context=i", "criticalthreshold=i", "warningthreshold=i", "encoding=s", "preferredlevel=s", ); if (! GetOptions(\%commandline, @params)) { print_help(); exit $ERRORS{UNKNOWN}; } if (exists $commandline{version}) { print_version(); exit UNKNOWN; } if (exists $commandline{help}) { print_help(); exit UNKNOWN; } if (exists $commandline{config}) { $enough_info = 1; } elsif (exists $commandline{configdir}) { $enough_info = 1; } elsif (exists $commandline{logfile}) { $enough_info = 1; } elsif (exists $commandline{type} && $commandline{type} =~ /^eventlog/) { $enough_info = 1; } elsif (exists $commandline{deinstall}) { $commandline{type} = 'dummy'; $enough_info = 1; } if (! $enough_info) { print_usage(); exit UNKNOWN; } if (exists $commandline{daemon}) { my @newargv = (); foreach my $option (keys %commandline) { if (grep { /^$option/ && /=/ } @params) { push(@newargv, sprintf "--%s", $option); push(@newargv, sprintf "%s", $commandline{$option}); } else { push(@newargv, sprintf "--%s", $option); } } $0 = 'check_logfiles '.join(' ', @newargv); if (! $commandline{daemon}) { $commandline{daemon} = 300; } } if (exists $commandline{environment}) { # if the desired environment variable values are different from # the environment of this running script, then a restart is necessary. # because setting $ENV does _not_ change the environment of the running script. foreach (keys %{$commandline{environment}}) { if ((! $ENV{$_}) || ($ENV{$_} ne $commandline{environment}->{$_})) { $needs_restart = 1; $ENV{$_} = $commandline{environment}->{$_}; } } } if ($needs_restart) { my @newargv = (); foreach my $option (keys %commandline) { if (grep { /^$option/ && /=/ } @params) { if (ref ($commandline{$option}) eq "HASH") { foreach (keys %{$commandline{$option}}) { push(@newargv, sprintf "--%s", $option); push(@newargv, sprintf "%s=%s", $_, $commandline{$option}->{$_}); } } else { push(@newargv, sprintf "--%s", $option); push(@newargv, sprintf "%s", $commandline{$option}); } } else { push(@newargv, sprintf "--%s", $option); } } exec $0, @newargv; # this makes sure that even a SHLIB or LD_LIBRARY_PATH are set correctly # when the perl interpreter starts. Setting them during runtime does not # help loading e.g. libclntsh.so exit; } if (exists $commandline{configdir}) { sub eachFile { my $filename = $_; my $fullpath = $File::Find::name; #remember that File::Find changes your CWD, #so you can call open with just $_ if ((-f $filename) && ($filename =~ /\.(cfg|conf)$/)) { push(@cfgfiles, $fullpath); } } find (\&eachFile, $commandline{configdir}); @cfgfiles = sort { $a cmp $b } @cfgfiles; } if (exists $commandline{config}) { # -f is always first unshift(@cfgfiles, $commandline{config}); } if (scalar(@cfgfiles) == 1) { $commandline{config} = $cfgfiles[0]; } elsif (scalar(@cfgfiles) > 1) { $commandline{config} = \@cfgfiles; } if (exists $commandline{searches}) { $commandline{selectedsearches} = $commandline{searches}; } if (! exists $commandline{selectedsearches}) { $commandline{selectedsearches} = ""; } if (exists $commandline{type}) { my ($type, $details) = split(":", $commandline{type}); } if (exists $commandline{criticalpattern}) { $commandline{criticalpattern} = '.*' if $commandline{criticalpattern} eq 'match_them_all'; delete $commandline{criticalpattern} if $commandline{criticalpattern} eq 'match_never_ever'; } if (exists $commandline{warningpattern}) { $commandline{warningpattern} = '.*' if $commandline{warningpattern} eq 'match_them_all'; delete $commandline{warningpattern} if $commandline{warningpattern} eq 'match_never_ever'; } if (my $cl = Nagios::CheckLogfiles->new({ cfgfile => $commandline{config} ? $commandline{config} : undef, searches => [ map { if (exists $commandline{type}) { # "eventlog" or "eventlog:eventlog=application,include,source=cdrom,source=dvd,eventid=23,eventid=29,operation=or,exclude,eventid=4711,operation=and" my ($type, $details) = split(":", $commandline{type}); $_->{type} = $type; if ($details) { $_->{$type} = {}; my $toplevel = $_->{$type}; foreach my $detail (split(",", $details)) { my ($key, $value) = split("=", $detail); if ($value) { if (exists $toplevel->{$key}) { $toplevel->{$key} .= ','.$value; } else { $toplevel->{$key} = $value; } } else { $_->{$type}->{$key} = {}; $toplevel = $_->{$type}->{$key}; } } } } $_; } map { # ausputzen foreach my $key (keys %{$_}) { delete $_->{$key} unless $_->{$key}}; $_; } ({ tag => $commandline{tag} ? $commandline{tag} : undef, logfile => $commandline{logfile} ? $commandline{logfile} : undef, type => $commandline{type} ? $commandline{type} : undef, rotation => $commandline{rotation} ? $commandline{rotation} : undef, tivolipatterns => $commandline{tivolipattern} ? $commandline{tivolipattern} : undef, criticalpatterns => $commandline{criticalpattern} ? $commandline{criticalpattern} : undef, criticalexceptions => $commandline{criticalexception} ? $commandline{criticalexception} : undef, warningpatterns => $commandline{warningpattern} ? $commandline{warningpattern} : undef, warningexceptions => $commandline{warningexception} ? $commandline{warningexception} : undef, okpatterns => $commandline{okpattern} ? $commandline{okpattern} : undef, options => join(',', grep { $_ } $commandline{noprotocol} ? "noprotocol" : undef, $commandline{nocase} ? "nocase" : undef, $commandline{noperfdata} ? "noperfdata" : undef, $commandline{winwarncrit} ? "winwarncrit" : undef, $commandline{nologfilenocry} ? "nologfilenocry" : undef, $commandline{syslogserver} ? "syslogserver" : undef, $commandline{syslogclient} ? "syslogclient=".$commandline{syslogclient} : undef, $commandline{maxlength} ? "maxlength=".$commandline{maxlength} : undef, $commandline{lookback} ? "lookback=".$commandline{lookback} : undef, $commandline{context} ? "context=".$commandline{context} : undef, $commandline{allyoucaneat} ? "allyoucaneat" : undef, $commandline{criticalthreshold} ? "criticalthreshold=".$commandline{criticalthreshold} : undef, $commandline{warningthreshold} ? "warningthreshold=".$commandline{warningthreshold} : undef, $commandline{encoding} ? "encoding=".$commandline{encoding} : undef, defined $commandline{sticky} ? "sticky".($commandline{sticky} ? "=".$commandline{sticky} : "") : undef, $commandline{preferredlevel} ? "preferredlevel=".$commandline{preferredlevel} : undef, ), archivedir => $commandline{archivedir} ? $commandline{archivedir} : undef, })], options => join(',', grep { $_ } $commandline{report} ? "report=".$commandline{report} : undef, $commandline{seekfileerror} ? "seekfileerror=".(uc $commandline{seekfileerror}) : undef, ), selectedsearches => [split(/,/, $commandline{selectedsearches})], dynamictag => $commandline{tag} ? $commandline{tag} : undef, #report => $commandline{report} ? $commandline{report} : undef, cmdlinemacros => $commandline{macro}, seekfilesdir => $commandline{seekfilesdir} ? $commandline{seekfilesdir} : undef, protocolsdir => $commandline{protocolsdir} ? $commandline{protocolsdir} : undef, protocolsretention => $commandline{protocolsretention} ? $commandline{protocolsretention} : undef, reset => $commandline{reset} ? $commandline{reset} : undef, })) { $cl->{verbose} = $commandline{verbose} ? 1 : 0; $cl->{timeout} = $commandline{timeout} ? $commandline{timeout} : 60; if ($commandline{install}) { $cl->install_windows_service($commandline{service}, $commandline{config}, $commandline{username}, $commandline{password}); } elsif ($commandline{deinstall}) { $cl->deinstall_windows_service($commandline{service}); } elsif ($commandline{daemon}) { $cl->run_as_daemon($commandline{daemon}); } else { $cl->run(); } printf "%s%s\n%s", $cl->{exitmessage}, $cl->{perfdata} ? "|".$cl->{perfdata} : "", $cl->{long_exitmessage} ? $cl->{long_exitmessage}."\n" : ""; exit $cl->{exitcode}; } else { printf "%s\n", $Nagios::CheckLogfiles::ExitMsg; exit $Nagios::CheckLogfiles::ExitCode; }