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   62122 use 5.006;
  1         3  
4 1     1   5 use strict;
  1         1  
  1         31  
5 1     1   5 use warnings;
  1         2  
  1         31  
6 1     1   486 use File::Slurp;
  1         28794  
  1         63  
7 1     1   486 use Sys::Hostname;
  1         897  
  1         1071  
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.1.0
16              
17             =cut
18              
19             our $VERSION = '0.1.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/JSON
98              
99             The data section of the return hash/JSON 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             - $hash{data}[checks}{$name} :: A hash with info on the checks ran.
125            
126             - $hash{data}[checks}{$name}{check} :: The command pre-variable substitution.
127            
128             - $hash{data}[checks}{$name}{ran} :: The command ran.
129            
130             - $hash{data}[checks}{$name}{output} :: The output of the check.
131            
132             - $hash{data}[checks}{$name}{exit} :: The exit code.
133            
134             - $hash{data}[checks}{$name}{error} :: Only present it died on a
135             signal or could not be executed. Provides a brief description.
136              
137             =head1 METHODS
138              
139             =head2 new
140              
141             Initiates the object.
142              
143             One argument is taken and that is a hash ref. If the key 'config'
144             is present, that will be the config file used. Otherwise
145             '/usr/local/etc/sneck.conf' is used. The key 'include' is a Perl
146             boolean for if the raw config should be included in the JSON.
147              
148             This function should always work. If there is an error with
149             parsing or the like, it will be reported in the expected format
150             when $sneck->run is called.
151              
152             This is meant to be rock solid and always work, meaning LibreNMS
153             style JSON is always returned(provided Perl and the other modules
154             are working).
155              
156              
157              
158             my $sneck=Monitoring::Sneck->new({config=>$file}, include=>0);
159              
160             =cut
161              
162             sub new {
163 0     0 1   my %args;
164 0 0         if ( defined( $_[1] ) ) {
165 0           %args = %{ $_[1] };
  0            
166             }
167              
168             # init the object
169 0           my $self = {
170             config => '/usr/local/etc/sneck.conf',
171             to_return => {
172             error => 0,
173             errorString => '',
174             data => {
175             hostname => hostname,
176             ok => 0,
177             warning => 0,
178             critical => 0,
179             unknown => 0,
180             errored => 0,
181             alert => 0,
182             alertString => '',
183             checks => {}
184             },
185             version => 1,
186             },
187             checks => {},
188             vars => {},
189             good => 1,
190             };
191 0           bless $self;
192              
193 0           my $config_raw;
194 0           eval { $config_raw = read_file( $self->{config} ); };
  0            
195 0 0         if ($@) {
196 0           $self->{good} = 0;
197 0           $self->{to_return}{error} = 1;
198 0           $self->{to_return}{errorString} = 'Failed to read in the config file "' . $self->{config} . '"... ' . $@;
199 0           $self->{checks} = {};
200 0           return $self;
201             }
202              
203             # include the config file if requested
204 0 0 0       if ( defined( $args{include} )
205             && $args{include} )
206             {
207 0           $self->{to_return}{data}{config} = $config_raw;
208             }
209              
210             # split the file and ignore any comments
211 0           my @config_split = grep( !/^[\t\ ]*#/, split( /\n/, $config_raw ) );
212 0           my $found_items = 0;
213 0           foreach my $line (@config_split) {
214 0           $line =~ s/^[\ \t]*//;
215 0 0         if ( $line =~ /^[A-Za-z0-9\_]+\=/ ) {
    0          
    0          
216              
217             # we found a variable
218              
219 0           my ( $name, $value ) = split( /\=/, $line, 2 );
220              
221             # make sure we have a value
222 0 0         if ( !defined($value) ) {
223 0           $self->{good} = 0;
224 0           $self->{to_return}{error} = 1;
225             $self->{to_return}{errorString}
226 0           = '"' . $line . '" seems to be a variable, but just a variable and no value';
227 0           return $self;
228             }
229              
230             # remove any white space from the end of the name
231 0           $name =~ s/[\t\ ]*$//;
232              
233             # check to make sure it is not already defined
234 0 0         if ( defined( $self->{vars}{$name} ) ) {
235 0           $self->{good} = 0;
236 0           $self->{to_return}{error} = 1;
237 0           $self->{to_return}{errorString} = 'variable "' . $name . '" is redefined on the line "' . $line . '"';
238 0           return $self;
239             }
240              
241 0           $self->{vars}{$name} = $value;
242             }
243             elsif ( $line =~ /^[A-Za-z0-9\_]+\|/ ) {
244              
245             # we found a check to add
246 0           my ( $name, $check ) = split( /\|/, $line, 2 );
247              
248             # make sure we have a check
249 0 0         if ( !defined($check) ) {
250 0           $self->{good} = 0;
251 0           $self->{to_return}{error} = 1;
252             $self->{to_return}{errorString}
253 0           = '"' . $line . '" seems to be a check, but just contains a check name and no check';
254 0           return $self;
255             }
256              
257             # remove any white space from the end of the name
258 0           $name =~ s/[\t\ ]*$//;
259              
260             # check to make sure it is not already defined
261 0 0         if ( defined( $self->{checks}{$name} ) ) {
262 0           $self->{good} = 0;
263 0           $self->{to_return}{error} = 1;
264 0           $self->{to_return}{errorString} = 'check "' . $name . '" is defined on the line "' . $line . '"';
265 0           return $self;
266             }
267              
268             # remove any white space from the start of the check
269 0           $check =~ s/^[\t\ ]*//;
270              
271 0           $self->{checks}{$name} = $check;
272              
273 0           $found_items++;
274             }
275             elsif ( $line =~ /^$/ ) {
276              
277             # just ignore empty lines so we don't error on them
278             }
279             else {
280             # we did not get a match for this line
281 0           $self->{good} = 0;
282 0           $self->{to_return}{error} = 1;
283 0           $self->{to_return}{errorString} = '"' . $line . '" is not a understood line';
284 0           return $self;
285             }
286             }
287              
288 0           $self;
289             }
290              
291             =head2 run
292              
293             This runs the checks and returns the return hash.
294              
295             my $return=$sneck->run;
296              
297             =cut
298              
299             sub run {
300 0     0 1   my $self = $_[0];
301              
302             # if something went wrong with new, just return
303 0 0         if ( !$self->{good} ) {
304 0           return $self->{to_return};
305             }
306              
307             # set the time it ran
308 0           $self->{to_return}{data}{time} = time;
309              
310 0           my @vars = keys( %{ $self->{vars} } );
  0            
311 0           my @checks = keys( %{ $self->{checks} } );
  0            
312 0           foreach my $name (@checks) {
313 0           my $check = $self->{checks}{$name};
314 0           $self->{to_return}{data}{checks}{$name} = { check => $check };
315              
316             # put the variables in place
317 0           foreach my $var_name (@vars) {
318 0           my $value = $self->{vars}{$var_name};
319 0           $check =~ s/%%%$var_name%%%/$value/g;
320             }
321 0           $self->{to_return}{data}{checks}{$name}{ran} = $check;
322              
323 0           $self->{to_return}{data}{checks}{$name}{output} = `$check`;
324 0           my $exit_code = $?;
325 0 0         if ( defined( $self->{to_return}{data}{checks}{$name}{output} ) ) {
326 0           chomp( $self->{to_return}{data}{checks}{$name}{output} );
327             }
328              
329             # handle the exit code
330 0 0         if ( $exit_code == -1 ) {
    0          
331 0           $self->{to_return}{data}{checks}{$name}{error} = 'failed to execute';
332             }
333             elsif ( $exit_code & 127 ) {
334 0 0         $self->{to_return}{data}{checks}{$name}{error} = sprintf(
335             "child died with signal %d, %s coredump\n",
336             ( $exit_code & 127 ),
337             ( $exit_code & 128 ) ? 'with' : 'without'
338             );
339             }
340             else {
341 0           $exit_code = $exit_code >> 8;
342             }
343 0           $self->{to_return}{data}{checks}{$name}{exit} = $exit_code;
344              
345             # anything other than 0, 1, 2, or 3 is a error
346 0 0         if ( $self->{to_return}{data}{checks}{$name}{exit} == 0 ) {
    0          
    0          
    0          
347 0           $self->{to_return}{data}{ok}++;
348             }
349             elsif ( $self->{to_return}{data}{checks}{$name}{exit} == 1 ) {
350 0           $self->{to_return}{data}{warning}++;
351 0           $self->{to_return}{data}{alert} = 1;
352             }
353             elsif ( $self->{to_return}{data}{checks}{$name}{exit} == 2 ) {
354 0           $self->{to_return}{data}{critical}++;
355 0           $self->{to_return}{data}{alert} = 1;
356             }
357             elsif ( $self->{to_return}{data}{checks}{$name}{exit} == 3 ) {
358 0           $self->{to_return}{data}{unknown}++;
359 0           $self->{to_return}{data}{alert} = 1;
360             }
361             else {
362 0           $self->{to_return}{data}{errored}++;
363 0           $self->{to_return}{data}{alert} = 1;
364             }
365              
366             # add it to the alert string if it is a warning
367 0 0 0       if ( $exit_code == 1 || $exit_code == 2 || $exit_code == 3 ) {
      0        
368             $self->{to_return}{data}{alertString}
369 0           = $self->{to_return}{data}{alertString} . $self->{to_return}{data}{checks}{$name}{output} . "\n";
370             }
371             }
372              
373 0           $self->{to_return}{data}{vars} = $self->{vars};
374              
375 0           return $self->{to_return};
376             }
377              
378             =head1 AUTHOR
379              
380             Zane C. Bowers-Hadley, C<< >>
381              
382             =head1 BUGS
383              
384             Please report any bugs or feature requests to C, or through
385             the web interface at L. I will be notified, and then you'll
386             automatically be notified of progress on your bug as I make changes.
387              
388              
389              
390              
391             =head1 SUPPORT
392              
393             You can find documentation for this module with the perldoc command.
394              
395             perldoc Monitoring::Sneck
396              
397              
398             You can also look for information at:
399              
400             =over 4
401              
402             =item * RT: CPAN's request tracker (report bugs here)
403              
404             L
405              
406             =item * CPAN Ratings
407              
408             L
409              
410             =item * Search CPAN
411              
412             L
413              
414             =item * Github
415              
416             l
417              
418             =back
419              
420              
421             =head1 ACKNOWLEDGEMENTS
422              
423              
424             =head1 LICENSE AND COPYRIGHT
425              
426             This software is Copyright (c) 2022 by Zane C. Bowers-Hadley.
427              
428             This is free software, licensed under:
429              
430             The Artistic License 2.0 (GPL Compatible)
431              
432              
433             =cut
434              
435             1; # End of Monitoring::Sneck