File Coverage

blib/lib/Monitoring/Sneck.pm
Criterion Covered Total %
statement 14 107 13.0
branch 0 40 0.0
condition 0 9 0.0
subroutine 5 7 71.4
pod 2 2 100.0
total 21 165 12.7


line stmt bran cond sub pod time code
1             package Monitoring::Sneck;
2              
3 1     1   70380 use 5.006;
  1         3  
4 1     1   18 use strict;
  1         2  
  1         34  
5 1     1   6 use warnings;
  1         2  
  1         36  
6 1     1   620 use File::Slurp;
  1         35648  
  1         61  
7 1     1   509 use Sys::Hostname;
  1         1080  
  1         1415  
8              
9             =head1 NAME
10              
11             Monitoring::Sneck - a boopable LibreNMS JSON style SNMP extend for remotely running nagios style checks
12              
13             =head1 VERSION
14              
15             Version 0.2.0
16              
17             =cut
18              
19             our $VERSION = '0.2.0';
20              
21             =head1 SYNOPSIS
22              
23             use Monitoring::Sneck;
24              
25             my $file='/usr/local/etc/sneck.conf';
26              
27             my $sneck=Monitoring::Sneck->new({config=>$file});
28              
29             =head1 USAGE
30              
31             Not really meant to be used as a library. The library is more of
32             to support the script.
33              
34             =head1 CONFIG FORMAT
35              
36             White space is always cleared from the start of lines via /^[\t ]*/ for
37             each file line that is read in.
38              
39             Blank lines are ignored.
40              
41             Lines starting with /\#/ are comments lines.
42              
43             Lines matching /^[A-Za-z0-9\_]+\=/ are variables. Anything before the the
44             /\=/ is used as the name with everything after being the value.
45              
46             Lines matching /^[A-Za-z0-9\_]+\|/ are checks to run. Anything before the
47             /\|/ is the name with everything after command to run.
48              
49             Any other sort of lines are considered an error.
50              
51             Variables in the checks are in the form of %%%varaible_name%%%.
52              
53             Variable names and check names may not be redefined once defined in the config.
54              
55             =head2 EXAMPLE CONFIG
56              
57             PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
58             # this is a comment
59             geom_foo|/usr/bin/env PATH=%%%PATH%%% /usr/local/libexec/nagios/check_geom mirror foo
60             does_not_exist|/bin/this_will_error yup... that it will
61            
62             does_not_exist_2|/usr/bin/env /bin/this_will_also_error
63              
64             The first line creates a variable named path.
65              
66             The second is ignored as it is a comment.
67              
68             The third creates a check named geom_foo that calls env with and sets the PATH to the
69             the variable defined on line 1 and calls check_geom_mirror.
70              
71             The fourth is a example of an error that will show what will happen when you call to a
72             file that does not exit.
73              
74             The fifth line will be ignored as it is blank.
75              
76             The sixth is a example of another command erroring.
77              
78             When you run it, you will notice that errors for lines 4 and 5 are printed to STDERR.
79             For this reason you should use '2> /dev/null' when calling it from snmpd or
80             '2> /dev/null > /dev/null' when calling from cron.
81              
82             =head1 USAGE
83              
84             snmpd should be configured as below.
85              
86             extend sneck /usr/bin/env PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin /usr/local/bin/sneck -c
87              
88             Then just setup a entry in like cron such as below.
89              
90             */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
91              
92             Most likely want to run it once per polling interval.
93              
94             You can use it in a non-cached manner with out cron, but this will result in a
95             longer polling time for LibreNMS or the like when it queries it.
96              
97             =head1 RETURN HASH
98              
99             The data section of the return hash is as below.
100              
101             - $hash{data}{alert} :: 0/1 boolean for if there is a aloert or not.
102            
103             - $hash{data}{ok} :: Count of the number of ok checks.
104            
105             - $hash{data}{warning} :: Count of the number of warning checks.
106            
107             - $hash{data}{critical} :: Count of the number of critical checks.
108            
109             - $hash{data}{unknown} :: Count of the number of unkown checks.
110            
111             - $hash{data}{errored} :: Count of the number of errored checks.
112            
113             - $hash{data}{alertString} :: The cumulative outputs of anything
114             that returned a warning, critical, or unknown.
115            
116             - $hash{data}{vars} :: A hash with the variables to use.
117            
118             - $hash{data}{time} :: Time since epoch.
119            
120             - $hash{data}{time} :: The hostname the check was ran on.
121            
122             - $hash{data}{config} :: The raw config file if told to include it.
123              
124             For below '$name' is the name of the check in question.
125              
126             - $hash{data}{checks}{$name} :: A hash with info on the checks ran.
127            
128             - $hash{data}{checks}{$name}{check} :: The command pre-variable substitution.
129            
130             - $hash{data}{checks}{$name}{ran} :: The command ran.
131            
132             - $hash{data}{checks}{$name}{output} :: The output of the check.
133            
134             - $hash{data}{checks}{$name}{exit} :: The exit code.
135            
136             - $hash{data}{checks}{$name}{error} :: Only present it died on a
137             signal or could not be executed. Provides a brief description.
138              
139             =head1 METHODS
140              
141             =head2 new
142              
143             Initiates the object.
144              
145             One argument is taken and that is a hash ref. If the key 'config'
146             is present, that will be the config file used. Otherwise
147             '/usr/local/etc/sneck.conf' is used. The key 'include' is a Perl
148             boolean for if the raw config should be included in the JSON.
149              
150             This function should always work as long as it can read the config.
151             If there is an error with parsing or the like, it will be reported
152             in the expected format when $sneck->run is called.
153              
154             This is meant to be rock solid and always work, meaning LibreNMS
155             style JSON is always returned(provided Perl and the other modules
156             are working).
157              
158             my $sneck;
159             eval{
160             $sneck=Monitoring::Sneck->new({config=>$file}, include=>0);
161             };
162             if ($@){
163             die($@);
164             }
165              
166             =cut
167              
168             sub new {
169 0     0 1   my %args;
170 0 0         if ( defined( $_[1] ) ) {
171 0           %args = %{ $_[1] };
  0            
172             }
173              
174             # init the object
175 0           my $self = {
176             config => '/usr/local/etc/sneck.conf',
177             to_return => {
178             error => 0,
179             errorString => '',
180             data => {
181             hostname => hostname,
182             ok => 0,
183             warning => 0,
184             critical => 0,
185             unknown => 0,
186             errored => 0,
187             alert => 0,
188             alertString => '',
189             checks => {}
190             },
191             version => 1,
192             },
193             checks => {},
194             vars => {},
195             good => 1,
196             };
197 0           bless $self;
198              
199 0           my $config_raw;
200 0           eval { $config_raw = read_file( $self->{config} ); };
  0            
201 0 0         if ($@) {
202 0           $self->{good} = 0;
203 0           $self->{to_return}{error} = 1;
204 0           $self->{to_return}{errorString} = 'Failed to read in the config file "' . $self->{config} . '"... ' . $@;
205 0           $self->{checks} = {};
206 0           return $self;
207             }
208              
209             # include the config file if requested
210 0 0 0       if ( defined( $args{include} )
211             && $args{include} )
212             {
213 0           $self->{to_return}{data}{config} = $config_raw;
214             }
215              
216             # split the file and ignore any comments
217 0           my @config_split = grep( !/^[\t\ ]*#/, split( /\n/, $config_raw ) );
218 0           my $found_items = 0;
219 0           foreach my $line (@config_split) {
220 0           $line =~ s/^[\ \t]*//;
221 0 0         if ( $line =~ /^[A-Za-z0-9\_]+\=/ ) {
    0          
    0          
222              
223             # we found a variable
224              
225 0           my ( $name, $value ) = split( /\=/, $line, 2 );
226              
227             # make sure we have a value
228 0 0         if ( !defined($value) ) {
229 0           $self->{good} = 0;
230 0           $self->{to_return}{error} = 1;
231             $self->{to_return}{errorString}
232 0           = '"' . $line . '" seems to be a variable, but just a variable and no value';
233 0           return $self;
234             }
235              
236             # remove any white space from the end of the name
237 0           $name =~ s/[\t\ ]*$//;
238              
239             # check to make sure it is not already defined
240 0 0         if ( defined( $self->{vars}{$name} ) ) {
241 0           $self->{good} = 0;
242 0           $self->{to_return}{error} = 1;
243 0           $self->{to_return}{errorString} = 'variable "' . $name . '" is redefined on the line "' . $line . '"';
244 0           return $self;
245             }
246              
247 0           $self->{vars}{$name} = $value;
248             }
249             elsif ( $line =~ /^[A-Za-z0-9\_]+\|/ ) {
250              
251             # we found a check to add
252 0           my ( $name, $check ) = split( /\|/, $line, 2 );
253              
254             # make sure we have a check
255 0 0         if ( !defined($check) ) {
256 0           $self->{good} = 0;
257 0           $self->{to_return}{error} = 1;
258             $self->{to_return}{errorString}
259 0           = '"' . $line . '" seems to be a check, but just contains a check name and no check';
260 0           return $self;
261             }
262              
263             # remove any white space from the end of the name
264 0           $name =~ s/[\t\ ]*$//;
265              
266             # check to make sure it is not already defined
267 0 0         if ( defined( $self->{checks}{$name} ) ) {
268 0           $self->{good} = 0;
269 0           $self->{to_return}{error} = 1;
270 0           $self->{to_return}{errorString} = 'check "' . $name . '" is defined on the line "' . $line . '"';
271 0           return $self;
272             }
273              
274             # remove any white space from the start of the check
275 0           $check =~ s/^[\t\ ]*//;
276              
277 0           $self->{checks}{$name} = $check;
278              
279 0           $found_items++;
280             }
281             elsif ( $line =~ /^$/ ) {
282              
283             # just ignore empty lines so we don't error on them
284             }
285             else {
286             # we did not get a match for this line
287 0           $self->{good} = 0;
288 0           $self->{to_return}{error} = 1;
289 0           $self->{to_return}{errorString} = '"' . $line . '" is not a understood line';
290 0           return $self;
291             }
292             }
293              
294 0           $self;
295             }
296              
297             =head2 run
298              
299             This runs the checks and returns the return hash.
300              
301             my $return=$sneck->run;
302              
303             =cut
304              
305             sub run {
306 0     0 1   my $self = $_[0];
307              
308             # if something went wrong with new, just return
309 0 0         if ( !$self->{good} ) {
310 0           return $self->{to_return};
311             }
312              
313             # set the time it ran
314 0           $self->{to_return}{data}{time} = time;
315              
316 0           my @vars = keys( %{ $self->{vars} } );
  0            
317 0           my @checks = keys( %{ $self->{checks} } );
  0            
318 0           foreach my $name (@checks) {
319 0           my $check = $self->{checks}{$name};
320 0           $self->{to_return}{data}{checks}{$name} = { check => $check };
321              
322             # put the variables in place
323 0           foreach my $var_name (@vars) {
324 0           my $value = $self->{vars}{$var_name};
325 0           $check =~ s/%%%$var_name%%%/$value/g;
326             }
327 0           $self->{to_return}{data}{checks}{$name}{ran} = $check;
328              
329 0           $self->{to_return}{data}{checks}{$name}{output} = `$check`;
330 0           my $exit_code = $?;
331 0 0         if ( defined( $self->{to_return}{data}{checks}{$name}{output} ) ) {
332 0           chomp( $self->{to_return}{data}{checks}{$name}{output} );
333             }
334              
335             # handle the exit code
336 0 0         if ( $exit_code == -1 ) {
    0          
337 0           $self->{to_return}{data}{checks}{$name}{error} = 'failed to execute';
338             }
339             elsif ( $exit_code & 127 ) {
340 0 0         $self->{to_return}{data}{checks}{$name}{error} = sprintf(
341             "child died with signal %d, %s coredump\n",
342             ( $exit_code & 127 ),
343             ( $exit_code & 128 ) ? 'with' : 'without'
344             );
345             }
346             else {
347 0           $exit_code = $exit_code >> 8;
348             }
349 0           $self->{to_return}{data}{checks}{$name}{exit} = $exit_code;
350              
351             # anything other than 0, 1, 2, or 3 is a error
352 0 0         if ( $self->{to_return}{data}{checks}{$name}{exit} == 0 ) {
    0          
    0          
    0          
353 0           $self->{to_return}{data}{ok}++;
354             }
355             elsif ( $self->{to_return}{data}{checks}{$name}{exit} == 1 ) {
356 0           $self->{to_return}{data}{warning}++;
357 0           $self->{to_return}{data}{alert} = 1;
358             }
359             elsif ( $self->{to_return}{data}{checks}{$name}{exit} == 2 ) {
360 0           $self->{to_return}{data}{critical}++;
361 0           $self->{to_return}{data}{alert} = 1;
362             }
363             elsif ( $self->{to_return}{data}{checks}{$name}{exit} == 3 ) {
364 0           $self->{to_return}{data}{unknown}++;
365 0           $self->{to_return}{data}{alert} = 1;
366             }
367             else {
368 0           $self->{to_return}{data}{errored}++;
369 0           $self->{to_return}{data}{alert} = 1;
370             }
371              
372             # add it to the alert string if it is a warning
373 0 0 0       if ( $exit_code == 1 || $exit_code == 2 || $exit_code == 3 ) {
      0        
374             $self->{to_return}{data}{alertString}
375 0           = $self->{to_return}{data}{alertString} . $self->{to_return}{data}{checks}{$name}{output} . "\n";
376             }
377             }
378              
379 0           $self->{to_return}{data}{vars} = $self->{vars};
380              
381 0           return $self->{to_return};
382             }
383              
384             =head1 AUTHOR
385              
386             Zane C. Bowers-Hadley, C<< >>
387              
388             =head1 BUGS
389              
390             Please report any bugs or feature requests to C, or through
391             the web interface at L. I will be notified, and then you'll
392             automatically be notified of progress on your bug as I make changes.
393              
394              
395              
396              
397             =head1 SUPPORT
398              
399             You can find documentation for this module with the perldoc command.
400              
401             perldoc Monitoring::Sneck
402              
403              
404             You can also look for information at:
405              
406             =over 4
407              
408             =item * RT: CPAN's request tracker (report bugs here)
409              
410             L
411              
412             =item * Search CPAN
413              
414             L
415              
416             =item * Github
417              
418             l
419              
420             =back
421              
422              
423             =head1 ACKNOWLEDGEMENTS
424              
425              
426             =head1 LICENSE AND COPYRIGHT
427              
428             This software is Copyright (c) 2023 by Zane C. Bowers-Hadley.
429              
430             This is free software, licensed under:
431              
432             The Artistic License 2.0 (GPL Compatible)
433              
434              
435             =cut
436              
437             1; # End of Monitoring::Sneck