diff --git a/.gitignore b/.gitignore index ecf66f8..46533fa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.o *.pm.tdy *.bs +bin/.exists # Devel::Cover cover_db/ diff --git a/Makefile.PL b/Makefile.PL index 34b791e..6c55d49 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -10,6 +10,7 @@ my %WriteMakefileArgs = ( ABSTRACT_FROM => 'lib/Monitoring/Sneck.pm', LICENSE => 'artistic_2', MIN_PERL_VERSION => '5.006', + INST_SCRIPT => 'bin', CONFIGURE_REQUIRES => { 'ExtUtils::MakeMaker' => '0', }, @@ -17,8 +18,9 @@ my %WriteMakefileArgs = ( 'Test::More' => '0', }, PREREQ_PM => { - 'JSON' => '0', - 'File::Slurp' => '0', + 'JSON' => '0', + 'File::Slurp' => '0', + 'Sys::Hostname' => '0', }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, clean => { FILES => 'Monitoring-Sneck-*' }, diff --git a/bin/sneck b/bin/sneck old mode 100644 new mode 100755 index e69de29..1b93211 --- a/bin/sneck +++ b/bin/sneck @@ -0,0 +1,160 @@ +#!/usr/bin/env perl + +=head1 NAME + +sneck - a boopable LibreNMS JSON style SNMP extend for remotely running nagios style checks + +=head1 SYNOPSIS + +sneck -u [B<-C> ] [B<-p>] [B<-i>] + +sneck -c [B<-C> ] [B<-p>] [B<-i>] + +=head1 DESCRIPTION + +For a description of the config file format and output, +please see L. + +=head1 FLAGS + +=head2 -f + +The config file to use. + +Defaults to '/usr/local/etc/sneck.conf'. + +=head2 -p + +Pretty it in a nicely formatted format. + +=head2 -C + +The cache file to use. + +Defaults to '/var/cache/sneck.cache'. + +=head2 -u + +Update the cache file. Will also print the was written to it. + +=head2 -c + +Print the cache file. Please note that B<-p> or B<-i> won't affect +this as this flag only reads/prints the cache file. + +=head2 -i + +Includes the config file used. + +=cut + +use strict; +use warnings; +use Getopt::Long; +use File::Slurp; +use JSON; +use Monitoring::Sneck; + +sub version { + print "sneck v. 0.0.1\n"; +} + +sub help { + &version; + + print ' + +-f Config file to use. + Default: /usr/local/etc/sneck.conf + +-c Print the cache and exit. Requires -u being used previously. + +-C Cache file. + Default: /var/cache/sneck.cache + +-u Run and write to cache. + +-p Pretty print. Does not affect -c. + +-i Include the raw config in the JSON. + +-h Print help info. +--help Print help info. +-v Print version info. +--version Print version info. + +'; +} + +my $cache_file = '/var/cache/sneck.cache'; +my $config_file = '/usr/local/etc/sneck.conf'; +my $update; +my $print_cache; +my $fallback; +my $help; +my $version; +my $pretty; +my $include; +Getopt::Long::Configure('no_ignore_case'); +Getopt::Long::Configure('bundling'); +GetOptions( + 'version' => \$version, + 'v' => \$version, + 'help' => \$help, + 'h' => \$help, + 'c' => \$print_cache, + 'f=s' => \$config_file, + 'C=s' => \$cache_file, + 'p' => \$pretty, + 'u' => \$update, + 'i' => \$include, +); + +# print version or help if requested +if ($help) { + &help; + exit 42; +} +if ($version) { + &version; + exit 42; +} + +# prints the cache and exit if requested +if ($print_cache) { + if ( !-f $cache_file || !-r $cache_file ) { + my $error = 'Cache file does not exist or is not readable "' . $cache_file . '"'; + my $possible_error + = { error => 1, version => 1, errorString => $error, data => { alert => 1, alertString => $error } }; + print encode_json($possible_error) . "\n"; + exit 3; + } + my $cache = read_file($cache_file); + print $cache; + exit; +} + +my $sneck = Monitoring::Sneck->new( { config => $config_file, include=>$include } ); +my $returned = $sneck->run; + +# encode it and print it +my $json = JSON->new->utf8->canonical(1); +if ($pretty) { + $json->pretty; +} +my $raw_json = $json->encode($returned); + +# non-pretty does not include a new line, so add it +if ( !$pretty ) { + $raw_json = $raw_json . "\n"; +} +print $raw_json; + +if ($update) { + my $fh; + open( $fh, '>', $cache_file ); + print $fh $raw_json; + close($fh); +} diff --git a/lib/Monitoring/Sneck.pm b/lib/Monitoring/Sneck.pm index 8ffb532..b61e9f9 100644 --- a/lib/Monitoring/Sneck.pm +++ b/lib/Monitoring/Sneck.pm @@ -4,6 +4,7 @@ use 5.006; use strict; use warnings; use File::Slurp; +use Sys::Hostname; =head1 NAME @@ -11,11 +12,11 @@ Monitoring::Sneck - a boopable LibreNMS JSON style SNMP extend for remotely runn =head1 VERSION -Version 0.0.0 +Version 0.0.1 =cut -our $VERSION = '0.0.0'; +our $VERSION = '0.0.1'; =head1 SYNOPSIS @@ -25,6 +26,114 @@ our $VERSION = '0.0.0'; my $sneck=Monitoring::Sneck->new({config=>$file}); +=head1 USAGE + +Not really meant to be used as a library. The library is more of +to support the script. + +=head1 CONFIG FORMAT + +White space is always cleared from the start of lines via /^[\t ]*/ for +each file line that is read in. + +Blank lines are ignored. + +Lines starting with /\#/ are comments lines. + +Lines matching /^[A-Za-z0-9\_]+\=/ are variables. Anything before the the +/\=/ is used as the name with everything after being the value. + +Lines matching /^[A-Za-z0-9\_]+\|/ are checks to run. Anything before the +/\|/ is the name with everything after command to run. + +Any other sort of lines are considered an error. + +Variables in the checks are in the form of %%%varaible_name%%%. + +Variable names and check names may not be redefined once defined in the config. + +=head2 EXAMPLE CONFIG + + PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin + # this is a comment + geom_foo|/usr/bin/env PATH=%%%PATH%%% /usr/local/libexec/nagios/check_geom mirror foo + does_not_exist|/bin/this_will_error yup... that it will + + does_not_exist_2|/usr/bin/env /bin/this_will_also_error + +The first line creates a variable named path. + +The second is ignored as it is a comment. + +The third creates a check named geom_foo that calls env with and sets the PATH to the +the variable defined on line 1 and calls check_geom_mirror. + +The fourth is a example of an error that will show what will happen when you call to a +file that does not exit. + +The fifth line will be ignored as it is blank. + +The sixth is a example of another command erroring. + +When you run it, you will notice that errors for lines 4 and 5 are printed to STDERR. +For this reason you should use '2> /dev/null' when calling it from snmpd or +'2> /dev/null > /dev/null' when calling from cron. + +=head1 USAGE + +snmpd should be configured as below. + + extend sneck /usr/bin/env PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin /usr/local/bin/sneck -c + +Then just setup a entry in like cron such as below. + + */5 * * * * /usr/bin/env PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin /usr/local/bin/sneck -u 2> /dev/null > /dev/null + +Most likely want to run it once per polling interval. + +You can use it in a non-cached manner with out cron, but this will result in a +longer polling time for LibreNMS or the like when it queries it. + +=head1 RETURN HASH/JSON + +The data section of the return hash/JSON is as below. + + - $hash{data}{alert} :: 0/1 boolean for if there is a aloert or not. + + - $hash{data}{ok} :: Count of the number of ok checks. + + - $hash{data}{warning} :: Count of the number of warning checks. + + - $hash{data}{critical} :: Count of the number of critical checks. + + - $hash{data}{unknown} :: Count of the number of unkown checks. + + - $hash{data}{errored} :: Count of the number of errored checks. + + - $hash{data}{alertString} :: The cumulative outputs of anything + that returned a warning, critical, or unknown. + + - $hash{data}{vars} :: A hash with the variables to use. + + - $hash{data}{time} :: Time since epoch. + + - $hash{data}{time} :: The hostname the check was ran on. + + - $hash{data}{config} :: The raw config file if told to include it. + + - $hash{data}[checks}{$name} :: A hash with info on the checks ran. + + - $hash{data}[checks}{$name}{check} :: The command pre-variable substitution. + + - $hash{data}[checks}{$name}{ran} :: The command ran. + + - $hash{data}[checks}{$name}{output} :: The output of the check. + + - $hash{data}[checks}{$name}{exit} :: The exit code. + + - $hash{data}[checks}{$name}{error} :: Only present it died on a + signal or could not be executed. Provides a brief description. + =head1 METHODS =head2 new @@ -33,9 +142,20 @@ Initiates the object. One argument is taken and that is a hash ref. If the key 'config' is present, that will be the config file used. Otherwise -'/usr/local/etc/sneck.conf' is used. +'/usr/local/etc/sneck.conf' is used. The key 'include' is a Perl +boolean for if the raw config should be included in the JSON. - my $sneck=Monitoring::Sneck->new({config=>$file}); +This function should always work. If there is an error with +parsing or the like, it will be reported in the expected format +when $sneck->run is called. + +This is meant to be rock solid and always work, meaning LibreNMS +style JSON is always returned(provided Perl and the other modules +are working). + + + + my $sneck=Monitoring::Sneck->new({config=>$file}, include=>0); =cut @@ -51,7 +171,17 @@ sub new { to_return => { error => 0, errorString => '', - data => { ok => 0, warning => 0, critical => 0, unknown => 0, erroed=>0, alert => 0, alertString => '', checks => {} }, + data => { + hostname => hostname, + ok => 0, + warning => 0, + critical => 0, + unknown => 0, + errored => 0, + alert => 0, + alertString => '', + checks => {} + }, version => 1, }, checks => {}, @@ -61,25 +191,32 @@ sub new { bless $self; my $config_raw; - eval { my $config_raw = read_file( $self->{config} ); }; + eval { $config_raw = read_file( $self->{config} ); }; if ($@) { $self->{good} = 0; $self->{to_return}{error} = 1; - $self->{to_return}{errorString} = 'Failed to read in the config file "' . $self->{conffig} . '"... ' . $@; + $self->{to_return}{errorString} = 'Failed to read in the config file "' . $self->{config} . '"... ' . $@; $self->{checks} = {}; return $self; } + # include the config file if requested + if ( defined( $args{include} ) + && $args{include} ) + { + $self->{to_return}{data}{config} = $config_raw; + } + # split the file and ignore any comments my @config_split = grep( !/^[\t\ ]*#/, split( /\n/, $config_raw ) ); my $found_items = 0; foreach my $line (@config_split) { $line =~ s/^[\ \t]*//; - if ( $line =~ /^[A-Za-z0-9\_]+]\=/ ) { + if ( $line =~ /^[A-Za-z0-9\_]+\=/ ) { # we found a variable - my ( $name, $value ) = split( /\|=/, $line, 2 ); + my ( $name, $value ) = split( /\=/, $line, 2 ); # make sure we have a value if ( !defined($value) ) { @@ -94,7 +231,7 @@ sub new { $name =~ s/[\t\ ]*$//; # check to make sure it is not already defined - if ( defiend( $self->{vars}{$name} ) ) { + if ( defined( $self->{vars}{$name} ) ) { $self->{good} = 0; $self->{to_return}{error} = 1; $self->{to_return}{errorString} = 'variable "' . $name . '" is redefined on the line "' . $line . '"'; @@ -103,9 +240,9 @@ sub new { $self->{vars}{$name} = $value; } - elsif ( $line =~ /^[A-Za-z0-9\_]+]\|/ ) { + elsif ( $line =~ /^[A-Za-z0-9\_]+\|/ ) { - # we found a item to add + # we found a check to add my ( $name, $check ) = split( /\|/, $line, 2 ); # make sure we have a check @@ -121,7 +258,7 @@ sub new { $name =~ s/[\t\ ]*$//; # check to make sure it is not already defined - if ( defiend( $self->{checks}{$name} ) ) { + if ( defined( $self->{checks}{$name} ) ) { $self->{good} = 0; $self->{to_return}{error} = 1; $self->{to_return}{errorString} = 'check "' . $name . '" is defined on the line "' . $line . '"'; @@ -167,33 +304,41 @@ sub run { return $self->{to_return}; } + # set the time it ran + $self->{to_return}{data}{time} = time; + my @vars = keys( %{ $self->{vars} } ); my @checks = keys( %{ $self->{checks} } ); foreach my $name (@checks) { my $check = $self->{checks}{$name}; - $self->{to_return}{checks}{$name} = { check => $check }; + $self->{to_return}{data}{checks}{$name} = { check => $check }; # put the variables in place foreach my $var_name (@vars) { my $value = $self->{vars}{$var_name}; $check =~ s/%%%$var_name%%%/$value/g; } - $self->{to_return}{checks}{$name}{ran} = $check; + $self->{to_return}{data}{checks}{$name}{ran} = $check; - $self->{to_return}{data}{checks}{$name}{output} = system($check); + $self->{to_return}{data}{checks}{$name}{output} = `$check`; my $exit_code = $?; - chomp( $self->{to_return}{data}{checks}{$name}{output} ); + if ( defined( $self->{to_return}{data}{checks}{$name}{output} ) ) { + chomp( $self->{to_return}{data}{checks}{$name}{output} ); + } # handle the exit code - if ( $? == -1 ) { + if ( $exit_code == -1 ) { $self->{to_return}{data}{checks}{$name}{error} = 'failed to execute'; } - elsif ( $? & 127 ) { - $self->{to_return}{data}{checks}{$name}{error} = sprintf( "child died with signal %d, %s coredump\n", - ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without' ); + elsif ( $exit_code & 127 ) { + $self->{to_return}{data}{checks}{$name}{error} = sprintf( + "child died with signal %d, %s coredump\n", + ( $exit_code & 127 ), + ( $exit_code & 128 ) ? 'with' : 'without' + ); } else { - $exit_code = $? >> 8; + $exit_code = $exit_code >> 8; } $self->{to_return}{data}{checks}{$name}{exit} = $exit_code; @@ -225,45 +370,11 @@ sub run { } } - $self->{to_return}{data}{vars}=$self->{vars}; + $self->{to_return}{data}{vars} = $self->{vars}; return $self->{to_return}; } -=head1 RETURN HASH - -The data section of the return hash is as below. - - - $hash{data}{alert} :: 0/1 boolean for if there is a aloert or not. - - - $hash{data}{ok} :: Count of the number of ok checks. - - - $hash{data}{warning} :: Count of the number of warning checks. - - - $hash{data}{critical} :: Count of the number of critical checks. - - - $hash{data}{unknown} :: Count of the number of unkown checks. - - - $hash{data}{errored} :: Count of the number of errored checks. - - - $hash{data}{alertString} :: The cumulative outputs of anything - that returned a warning, critical, or unknown. - - - $hash{data}{vars} :: A hash with the variables to use. - - - $hash{data}[checks}{$name} :: A hash with info on the checks ran. - - - $hash{data}[checks}{$name}{check} :: The command pre-variable substitution. - - - $hash{data}[checks}{$name}{ran} :: The command ran. - - - $hash{data}[checks}{$name}{output} :: The output of the check. - - - $hash{data}[checks}{$name}{exit} :: The exit code. - - - $hash{data}[checks}{$name}{error} :: Only present it died on a - signal or could not be executed. Provides a brief description. - =head1 AUTHOR Zane C. Bowers-Hadley, C<< >>