File Coverage

blib/lib/Monitoring/Sneck.pm
Criterion Covered Total %
statement 26 187 13.9
branch 0 88 0.0
condition 0 9 0.0
subroutine 9 11 81.8
pod 2 2 100.0
total 37 297 12.4


line stmt bran cond sub pod time code
1             package Monitoring::Sneck;
2              
3 1     1   121887 use 5.006;
  1         4  
4 1     1   6 use strict;
  1         3  
  1         49  
5 1     1   90 use warnings;
  1         3  
  1         105  
6 1     1   849 use File::Slurp qw(read_file);
  1         52147  
  1         94  
7 1     1   598 use Sys::Hostname qw(hostname);
  1         1617  
  1         75  
8 1     1   597 use IPC::Open3 qw(open3);
  1         4112  
  1         79  
9 1     1   9 use Symbol qw(gensym);
  1         2  
  1         46  
10 1     1   533 use IO::Select;
  1         2110  
  1         89  
11 1     1   8 use Time::HiRes qw(time);
  1         2  
  1         11  
12              
13             =head1 NAME
14              
15             Monitoring::Sneck - a boopable LibreNMS JSON style SNMP extend for remotely running nagios style checks
16              
17             =head1 VERSION
18              
19             Version 1.3.0
20              
21             =cut
22              
23             our $VERSION = '1.3.0';
24              
25             =head1 SYNOPSIS
26              
27             use Monitoring::Sneck;
28              
29             my $file='/usr/local/etc/sneck.conf';
30              
31             my $sneck=Monitoring::Sneck->new({config=>$file});
32              
33             =head1 USAGE
34              
35             Not really meant to be used as a library. The library is more of
36             to support the script.
37              
38             =head1 CONFIG FORMAT
39              
40             White space is always cleared from the start of lines via /^[\t ]*/ for
41             each file line that is read in.
42              
43             Blank lines are ignored.
44              
45             Lines starting with /\#/ are comments lines.
46              
47             Lines matching /^[Ee][Nn][Vv]\ [A-Za-z0-9\_]+\=/ are variables. Anything before the the
48             /\=/ is used as the name with everything after being the value.
49              
50             Lines matching /^[A-Za-z0-9\_]+\=/ are variables. Anything before the the
51             /\=/ is used as the name with everything after being the value.
52              
53             Lines matching /^[A-Za-z0-9\_]+\|/ are checks to run. Anything before the
54             /\|/ is the name with everything after command to run.
55              
56             Lines matching /^\%[A-Za-z0-9\_]+\|/ are debug check to run. Anything before the
57             /\|/ is the name with everything after command to run. These will not count towards
58             the any of the counts. This exists purely for debugging purposes.
59              
60             Any other sort of lines are considered an error.
61              
62             Variables in the checks are in the form of /%+varaible_name%+/.
63              
64             Variable names and check names may not be redefined once defined in the config.
65              
66             =head2 EXAMPLE CONFIG
67              
68             env PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
69             # this is a comment
70             GEOM_DEV=foo
71             geom_foo|/usr/local/libexec/nagios/check_geom mirror %GEOM_DEV%
72             does_not_exist|/bin/this_will_error yup... that it will
73              
74             does_not_exist_2|/usr/bin/env /bin/this_will_also_error
75              
76             The first line sets the %ENV variable PATH.
77              
78             The second is ignored as it is a comment.
79              
80             The third sets the variable GEOM_DEV to 'foo'
81              
82             The fourth creates a check named geom_foo that calls check_geom_mirror
83             with the variable supplied to it being the value specified by the variable
84             GEOM_DEV.
85              
86             The fith is a example of an error that will show what will happen when
87             you call to a file that does not exit.
88              
89             The sixth line will be ignored as it is blank.
90              
91             The seventh is a example of another command erroring.
92              
93             When you run it, you will notice that errors for lines 4 and 5 are printed to STDERR.
94             For this reason you should use '2> /dev/null' when calling it from snmpd or
95             '2> /dev/null > /dev/null' when calling from cron.
96              
97             =head1 USAGE
98              
99             snmpd should be configured as below.
100              
101             extend sneck /usr/bin/env PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin /usr/local/bin/sneck -c
102              
103             Then just setup a entry in like cron such as below.
104              
105             */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
106              
107             Most likely want to run it once per polling interval.
108              
109             You can use it in a non-cached manner with out cron, but this will result in a
110             longer polling time for LibreNMS or the like when it queries it.
111              
112             =head1 RETURN HASH
113              
114             The data section of the return hash is as below.
115              
116             - $hash{data}{alert} :: 0/1 boolean for if there is a aloert or not.
117              
118             - $hash{data}{ok} :: Count of the number of ok checks.
119              
120             - $hash{data}{warning} :: Count of the number of warning checks.
121              
122             - $hash{data}{critical} :: Count of the number of critical checks.
123              
124             - $hash{data}{unknown} :: Count of the number of unkown checks.
125              
126             - $hash{data}{errored} :: Count of the number of errored checks.
127              
128             - $hash{data}{alertString} :: The cumulative outputs of anything
129             that returned a warning, critical, or unknown.
130              
131             - $hash{data}{vars} :: A hash with the variables to use.
132              
133             - $hash{data}{time} :: Time since epoch.
134              
135             - $hash{data}{hostname} :: The hostname the check was ran on.
136              
137             - $hash{data}{config} :: The raw config file if told to include it.
138              
139             - $hash{data}{run_time} :: How long it took to run all checks.
140              
141             For below '$name' is the name of the check in question.
142              
143             - $hash{data}{checks}{$name} :: A hash with info on the checks ran.
144              
145             - $hash{data}{checks}{$name}{check} :: The command pre-variable substitution.
146              
147             - $hash{data}{checks}{$name}{ran} :: The command ran.
148              
149             - $hash{data}{checks}{$name}{output} :: The output of the check.
150              
151             - $hash{data}{checks}{$name}{exit} :: The exit code.
152              
153             - $hash{data}{checks}{$name}{error} :: Only present it died on a
154             signal or could not be executed. Provides a brief description.
155              
156             - $hash{data}{checks}{$name}{run_time} :: How long it took to run the checks.
157              
158             For below '$name' is the name of the debug checks in question.
159              
160             - $hash{data}{debugs}{$name} :: A hash with info on the checks ran.
161              
162             - $hash{data}{debugs}{$name}{check} :: The command pre-variable substitution.
163              
164             - $hash{data}{debugs}{$name}{ran} :: The command ran.
165              
166             - $hash{data}{debugs}{$name}{output} :: The output of the check.
167              
168             - $hash{data}{debugs}{$name}{exit} :: The exit code.
169              
170             - $hash{data}{debugs}{$name}{error} :: Only present it died on a
171             signal or could not be executed. Provides a brief description.
172              
173             - $hash{data}{checks}{$name}{run_time} :: How long it took to run the debug.
174              
175             =head1 METHODS
176              
177             =head2 new
178              
179             Initiates the object.
180              
181             One argument is taken and that is a hash ref. If the key 'config'
182             is present, that will be the config file used. Otherwise
183             '/usr/local/etc/sneck.conf' is used. The key 'include' is a Perl
184             boolean for if the raw config should be included in the JSON.
185              
186             This function should always work as long as it can read the config.
187             If there is an error with parsing or the like, it will be reported
188             in the expected format when $sneck->run is called.
189              
190             This is meant to be rock solid and always work, meaning LibreNMS
191             style JSON is always returned(provided Perl and the other modules
192             are working).
193              
194             If 'debug' is true, when run is called, debugging info will be
195             printed.
196              
197             my $sneck;
198             eval{
199             $sneck=Monitoring::Sneck->new({config=>$file, include=>0, debug=>0});
200             };
201             if ($@){
202             die($@);
203             }
204              
205             =cut
206              
207             sub new {
208 0     0 1   my %args;
209 0 0         if ( defined( $_[1] ) ) {
210 0           %args = %{ $_[1] };
  0            
211             }
212              
213             # init the object
214              
215 0           my $self = {
216              
217             config => '/usr/local/etc/sneck.conf',
218             to_return => {
219             error => 0,
220             errorString => '',
221             data => {
222             hostname => hostname,
223             ok => 0,
224             warning => 0,
225             critical => 0,
226             unknown => 0,
227             errored => 0,
228             alert => 0,
229             alertString => '',
230             checks => {},
231             debugs => {},
232             },
233             version => 1,
234             },
235             checks => {},
236             vars => {},
237             good => 1,
238             debug => 0,
239             };
240 0           bless $self;
241              
242 0           my $config_raw;
243 0           eval { $config_raw = read_file( $self->{config} ); };
  0            
244 0 0         if ($@) {
245 0           $self->{good} = 0;
246 0           $self->{to_return}{error} = 1;
247 0           $self->{to_return}{errorString} = 'Failed to read in the config file "' . $self->{config} . '"... ' . $@;
248 0           $self->{checks} = {};
249 0           return $self;
250             }
251              
252             # include the config file if requested
253 0 0 0       if ( defined( $args{include} )
254             && $args{include} )
255             {
256 0           $self->{to_return}{data}{config} = $config_raw;
257             }
258              
259 0 0         if ( defined( $args{debug} ) ) {
260 0           $self->{debug} = $args{debug};
261             }
262              
263             # split the file and ignore any comments
264 0           my @config_split = grep( !/^[\t\ ]*#/, split( /\n/, $config_raw ) );
265 0           my $found_items = 0;
266 0           foreach my $line (@config_split) {
267 0           $line =~ s/^[\ \t]*//;
268 0 0         if ( $line =~ /^[Ee][Nn][Vv]\ [A-Za-z0-9\_]+\=/ ) {
    0          
    0          
    0          
269 0           my ( $name, $value ) = split( /\=/, $line, 2 );
270              
271             # make sure we have a value
272 0 0         if ( !defined($value) ) {
273 0           $value = '';
274             }
275              
276             # remove the starting bit
277 0           $name =~ s/^[Ee][Nn][Vv]\ //;
278 0           $ENV{$name} = $value;
279             } elsif ( $line =~ /^[A-Za-z0-9\_]+\=/ ) {
280              
281             # we found a variable
282 0           my ( $name, $value ) = split( /\=/, $line, 2 );
283              
284             # make sure we have a value
285 0 0         if ( !defined($value) ) {
286 0           $self->{good} = 0;
287 0           $self->{to_return}{error} = 1;
288             $self->{to_return}{errorString}
289 0           = '"' . $line . '" seems to be a variable, but just a variable and no value';
290 0           return $self;
291             }
292              
293             # remove any white space from the end of the name
294 0           $name =~ s/[\t\ ]*$//;
295              
296             # check to make sure it is not already defined
297 0 0         if ( defined( $self->{vars}{$name} ) ) {
298 0           $self->{good} = 0;
299 0           $self->{to_return}{error} = 1;
300 0           $self->{to_return}{errorString} = 'variable "' . $name . '" is redefined on the line "' . $line . '"';
301 0           return $self;
302             }
303              
304 0           $self->{vars}{$name} = $value;
305             } elsif ( $line =~ /^\%*[A-Za-z0-9\_]+\|/ ) {
306              
307             # we found a check to add
308 0           my ( $name, $check ) = split( /\|/, $line, 2 );
309              
310             # make sure we have a check
311 0 0         if ( !defined($check) ) {
312 0           $self->{good} = 0;
313 0           $self->{to_return}{error} = 1;
314             $self->{to_return}{errorString}
315 0           = '"' . $line . '" seems to be a check, but just contains a check name and no check';
316 0           return $self;
317             }
318              
319             # remove any white space from the end of the name
320 0           $name =~ s/[\t\ ]*$//;
321              
322             # check to make sure it is not already defined
323 0 0         if ( defined( $self->{checks}{$name} ) ) {
324 0           $self->{good} = 0;
325 0           $self->{to_return}{error} = 1;
326 0           $self->{to_return}{errorString} = 'check "' . $name . '" is defined on the line "' . $line . '"';
327 0           return $self;
328             }
329              
330             # remove any white space from the start of the check
331 0           $check =~ s/^[\t\ ]*//;
332              
333 0           $self->{checks}{$name} = $check;
334              
335 0           $found_items++;
336             } elsif ( $line =~ /^$/ ) {
337              
338             # just ignore empty lines so we don't error on them
339             } else {
340             # we did not get a match for this line
341 0           $self->{good} = 0;
342 0           $self->{to_return}{error} = 1;
343 0           $self->{to_return}{errorString} = '"' . $line . '" is not a understood line';
344 0           return $self;
345             }
346             } ## end foreach my $line (@config_split)
347              
348 0           $self;
349             } ## end sub new
350              
351             =head2 run
352              
353             This runs the checks and returns the return hash.
354              
355             my $return=$sneck->run;
356              
357             =cut
358              
359             sub run {
360 0     0 1   my $self = $_[0];
361              
362 0           my $run_start_time = Time::HiRes::time;
363 0 0         if ( $self->{debug} ) {
364 0           warn( 'run started at ' . $run_start_time );
365             }
366              
367             # if something went wrong with new, just return
368 0 0         if ( !$self->{good} ) {
369 0 0         if ( $self->{debug} ) {
370 0           warn('$self->{good} false... returning $self->{to_return}');
371             }
372 0           return $self->{to_return};
373             }
374              
375             # set the time it ran
376 0           $self->{to_return}{data}{time} = time;
377             #make sure it is a int
378 0           $self->{to_return}{data}{time}=~s/\..*$//;
379              
380 0           my @vars = keys( %{ $self->{vars} } );
  0            
381 0           my @checks = sort( keys( %{ $self->{checks} } ) );
  0            
382 0           foreach my $name (@checks) {
383 0           my $check_start_time = Time::HiRes::time;
384 0 0         if ( $self->{debug} ) {
385 0           warn( $name . ' processing started at ' . $check_start_time );
386             }
387              
388 0           my $type = 'checks';
389 0 0         if ( $name =~ /^\%/ ) {
390 0           $type = 'debugs';
391             }
392 0 0         if ( $self->{debug} ) {
393 0           warn( $name . ' is of type ' . $type );
394             }
395              
396 0           my $check = $self->{checks}{$name};
397 0           $name =~ s/^\%//;
398 0           $self->{to_return}{data}{$type}{$name} = { check => $check };
399              
400 0 0         if ( $self->{debug} ) {
401 0           warn( $name . ' check string: "' . $check . '"' );
402             }
403              
404             # put the variables in place
405 0           foreach my $var_name (@vars) {
406 0           my $value = $self->{vars}{$var_name};
407 0           $check =~ s/%+$var_name%+/$value/g;
408             }
409 0           $self->{to_return}{data}{$type}{$name}{ran} = $check;
410 0 0         if ( $self->{debug} ) {
411 0           warn( $name . ' check string post variable replacement: "' . $check . '"' );
412             }
413              
414 0           my $check_pid = open3( my $std_in, my $std_out, my $std_err = gensym, $check );
415 0 0         if ( $self->{debug} ) {
416 0           warn( $name . ' open3 called' );
417             }
418              
419 0           my $s = IO::Select->new();
420 0           $s->add($std_out);
421 0           $s->add($std_err);
422 0           my $output = '';
423 0           while ( my @ready = $s->can_read ) {
424 0           foreach my $handle (@ready) {
425 0 0         if ( sysread( $handle, my $buf, 4096 ) ) {
426 0           $output = $output . $buf;
427             } else {
428 0           $s->remove($handle);
429             }
430             }
431             }
432              
433 0 0         if ( $self->{debug} ) {
434 0           warn( $name . ' IO::Select for open3 done... output is... "' . $output . '"' );
435             }
436              
437             # call wait pid so we can get the exit code
438 0           waitpid( $check_pid, 0 );
439 0           my $exit_code = $?;
440 0           $self->{to_return}{data}{$type}{$name}{output} = $output;
441 0 0         if ( defined( $self->{to_return}{data}{$type}{$name}{output} ) ) {
442 0           chomp( $self->{to_return}{data}{$type}{$name}{output} );
443             }
444              
445             # handle the exit code
446 0 0         if ( $exit_code == -1 ) {
    0          
447 0           $self->{to_return}{data}{$type}{$name}{error} = 'failed to execute';
448             } elsif ( $exit_code & 127 ) {
449 0 0         $self->{to_return}{data}{$type}{$name}{error} = sprintf(
450             "child died with signal %d, %s coredump\n",
451             ( $exit_code & 127 ),
452             ( $exit_code & 128 ) ? 'with' : 'without'
453             );
454             } else {
455 0           $exit_code = $exit_code >> 8;
456             }
457 0           $self->{to_return}{data}{$type}{$name}{exit} = $exit_code;
458              
459 0 0         if ( $self->{debug} ) {
460 0           warn( $name . ' exit code is ' . $exit_code );
461             }
462              
463             # anything other than 0, 1, 2, or 3 is a error
464 0 0         if ( $type eq 'checks' ) {
465 0 0         if ( $self->{to_return}{data}{checks}{$name}{exit} == 0 ) {
    0          
    0          
    0          
466 0           $self->{to_return}{data}{ok}++;
467 0 0         if ( $self->{debug} ) {
468 0           warn( $name . ' is ok' );
469             }
470             } elsif ( $self->{to_return}{data}{checks}{$name}{exit} == 1 ) {
471 0           $self->{to_return}{data}{warning}++;
472 0           $self->{to_return}{data}{alert} = 1;
473 0 0         if ( $self->{debug} ) {
474 0           warn( $name . ' is warning' );
475             }
476             } elsif ( $self->{to_return}{data}{checks}{$name}{exit} == 2 ) {
477 0           $self->{to_return}{data}{critical}++;
478 0           $self->{to_return}{data}{alert} = 1;
479 0 0         if ( $self->{debug} ) {
480 0           warn( $name . ' is critical' );
481             }
482             } elsif ( $self->{to_return}{data}{checks}{$name}{exit} == 3 ) {
483 0           $self->{to_return}{data}{unknown}++;
484 0           $self->{to_return}{data}{alert} = 1;
485 0 0         if ( $self->{debug} ) {
486 0           warn( $name . ' is unknown' );
487             }
488             } else {
489 0           $self->{to_return}{data}{errored}++;
490 0           $self->{to_return}{data}{alert} = 1;
491 0 0         if ( $self->{debug} ) {
492 0           warn( $name . ' is errored' );
493             }
494             }
495              
496             # add it to the alert string if it is a warning
497 0 0 0       if ( $exit_code == 1 || $exit_code == 2 || $exit_code == 3 ) {
      0        
498             $self->{to_return}{data}{alertString}
499 0           = $self->{to_return}{data}{alertString} . $self->{to_return}{data}{checks}{$name}{output} . "\n";
500             }
501             } ## end if ( $type eq 'checks' )
502              
503             # figure out how long the run took
504 0           my $check_stop_time = Time::HiRes::time;
505 0 0         if ( $self->{debug} ) {
506 0           warn( $name . ' finished at ' . $check_stop_time );
507             }
508 0           my $check_time = $check_stop_time - $check_start_time;
509             # round to the 9th place to avoid scientific notation
510 0           $self->{to_return}{data}{$type}{$name}{run_time} = sprintf( '%.9f', $check_time );
511             } ## end foreach my $name (@checks)
512              
513 0           $self->{to_return}{data}{vars} = $self->{vars};
514              
515             # figure out how long the run took
516 0           my $run_stop_time = Time::HiRes::time;
517 0 0         if ( $self->{debug} ) {
518 0           warn( 'run finished at ' . $run_stop_time );
519             }
520              
521 0           my $run_time = $run_stop_time - $run_start_time;
522 0 0         if ( $self->{debug} ) {
523 0           warn( 'run time was ' . $run_time );
524             }
525              
526             # round to the 9th place to avoid scientific notation
527 0           $self->{to_return}{data}{run_time} = sprintf( '%.9f', $run_time );
528              
529 0 0         if ( $self->{debug} ) {
530 0           warn('run is returning now');
531             }
532 0           return $self->{to_return};
533             } ## end sub run
534              
535             =head1 AUTHOR
536              
537             Zane C. Bowers-Hadley, C<< >>
538              
539             =head1 BUGS
540              
541             Please report any bugs or feature requests to C, or through
542             the web interface at L. I will be notified, and then you'll
543             automatically be notified of progress on your bug as I make changes.
544              
545              
546              
547              
548             =head1 SUPPORT
549              
550             You can find documentation for this module with the perldoc command.
551              
552             perldoc Monitoring::Sneck
553              
554              
555             You can also look for information at:
556              
557             =over 4
558              
559             =item * RT: CPAN's request tracker (report bugs here)
560              
561             L
562              
563             =item * Search CPAN
564              
565             L
566              
567             =item * Github
568              
569             l
570              
571             =back
572              
573              
574             =head1 ACKNOWLEDGEMENTS
575              
576              
577             =head1 LICENSE AND COPYRIGHT
578              
579             This software is Copyright (c) 2023 by Zane C. Bowers-Hadley.
580              
581             This is free software, licensed under:
582              
583             The Artistic License 2.0 (GPL Compatible)
584              
585              
586             =cut
587              
588             1; # End of Monitoring::Sneck