contrib/exec-nagios.px: Implement the "NRPEConfig" option.
[collectd.git] / contrib / exec-nagios.px
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5
6 =head1 NAME
7
8 exec-nagios.px
9
10 =head1 DESCRIPTION
11
12 This script allows you to use plugins that were written for Nagios with
13 collectd's C<exec-plugin>. If the plugin checks some kind of threshold, please
14 consider configuring the threshold using collectd's own facilities instead of
15 using this transition layer.
16
17 =cut
18
19 use Sys::Hostname ('hostname');
20 use File::Basename ('basename');
21 use Config::General ('ParseConfig');
22 use Regexp::Common ('number');
23
24 our $ConfigFile = '/etc/exec-nagios.conf';
25 our $TypeMap = {};
26 our $NRPEMap = {};
27 our $Scripts = [];
28 our $Interval = 300;
29
30 main ();
31 exit (0);
32
33 # Configuration
34 # {{{
35
36 =head1 CONFIGURATION
37
38 This script reads it's configuration from F</etc/exec-nagios.conf>. The
39 configuration is read using C<Config::General> which understands a Apache-like
40 config syntax, so it's very similar to the F<collectd.conf> syntax, too.
41
42 Here's a short sample config:
43
44   NRPEConfig "/etc/nrpe.cfg"
45   Interval 300
46   <Script /usr/lib/nagios/check_tcp>
47     Arguments -H alice -p 22
48     Type delay
49   </Script>
50   <Script /usr/lib/nagios/check_dns>
51     Arguments -H alice
52     Type delay
53   </Script>
54
55 The options have the following semantic (i.E<nbsp>e. meaning):
56
57 =over 4
58
59 =item B<NRPEConfig> I<File>
60
61 Read the NRPE config and add the command definitions to an alias table. After
62 reading the file you can use the NRPE command name rather than the script's
63 filename within B<Script> blocks (see below). If both, the NRPE config and the
64 B<Script> block, define arguments they will be merged by concatenating the
65 arguments together in the order "NRPE-args Script-args".
66
67 Please note that this option is rather dumb. It does not support "command
68 argument processing" (i.e. replacing C<$ARG1$> and friends), inclusion of other
69 NRPE config files, include directories etc.
70
71 =item B<Interval> I<Seconds>
72
73 Sets the interval in which the plugins are executed. This doesn't need to match
74 the interval setting of the collectd daemon. Usually, you want to execute the
75 Nagios plugins much less often, e.E<nbsp>g. every 300 seconds versus every 10
76 seconds.
77
78 =item E<lt>B<Script> I<File>E<gt>
79
80 Adds a script to the list of scripts to be executed once per I<Interval>
81 seconds. If the B<NRPEConfig> is given above the B<Script> block, you may use
82 the NRPE command name rather than the script's filename. You can use the
83 following optional arguments to specify the operation further:
84
85 =over 4
86
87 =item B<Arguments> I<Arguments>
88
89 Pass the arguments I<Arguments> to the script. This is often needed with Nagios
90 plugins, because much of the logic is implemented in the plugins, not in the
91 daemon. If you need to specify a warning and/or critical range here, please
92 consider using collectd's own threshold mechanism, which is by far the more
93 elegant solution than this transition layer.
94
95 =item B<Type> I<Type>
96
97 If the plugin provides "performance data" the performance data is dispatched to
98 collectd with this type. If no type is configured the data is ignored. Please
99 note that this is limited to types that take exactly one value, such as the
100 type C<delay> in the example above. If you need more complex performance data,
101 rewrite the plugin as a collectd plugin (or at least port it do run directly
102 with the C<exec-plugin>).
103
104 =back
105
106 =back
107
108 =cut
109
110 sub parse_nrpe_conf
111 {
112   my $file = shift;
113   my $fh;
114   my $status;
115
116   $status = open ($fh, '<', $file);
117   if (!$status)
118   {
119     print STDERR "Reading NRPE config from \"$file\" failed: $!\n";
120     return;
121   }
122
123   while (<$fh>)
124   {
125     my $line = $_;
126     chomp ($line);
127
128     if ($line =~ m/^\s*command\[([^\]]+)\]\s*=\s*(.+)$/)
129     {
130       my $alias = $1;
131       my $script;
132       my $arguments;
133
134       ($script, $arguments) = split (' ', $2, 2);
135
136       if ($NRPEMap->{$alias})
137       {
138         print STDERR "Warning: NRPE command \"$alias\" redefined.\n";
139       }
140
141       $NRPEMap->{$alias} = { script => $script };
142       if ($arguments)
143       {
144         $NRPEMap->{$alias}{'arguments'} = $arguments;
145       }
146     }
147   } # while (<$fh>)
148
149   close ($fh);
150 } # parse_nrpe_conf
151
152 sub handle_config_addtype
153 {
154   my $list = shift;
155
156   for (my $i = 0; $i < @$list; $i++)
157   {
158     my ($to, @from) = split (' ', $list->[$i]);
159     for (my $j = 0; $j < @from; $j++)
160     {
161       $TypeMap->{$from[$j]} = $to;
162     }
163   }
164 } # handle_config_addtype
165
166 # Update the script record. This function adds the name of the script /
167 # executable to the hash and merges the configured and NRPE arguments if
168 # required.
169 sub update_script_opts
170 {
171   my $opts = shift;
172   my $script = shift;
173   my $nrpe_args = shift;
174
175   $opts->{'script'} = $script;
176
177   if ($nrpe_args)
178   {
179     if ($opts->{'arguments'})
180     {
181       $opts->{'arguments'} = $nrpe_args . ' ' . $opts->{'arguments'};
182     }
183     else
184     {
185       $opts->{'arguments'} = $nrpe_args;
186     }
187   }
188 } # update_script_opts
189
190 sub handle_config_script
191 {
192   my $scripts = shift;
193
194   for (keys %$scripts)
195   {
196     my $script = $_;
197     my $opts = $scripts->{$script};
198
199     my $nrpe_args = '';
200
201     # Check if the script exists in the NRPE map. If so, replace the alias name
202     # with the actual script name.
203     if ($NRPEMap->{$script})
204     {
205       if ($NRPEMap->{$script}{'arguments'})
206       {
207         $nrpe_args = $NRPEMap->{$script}{'arguments'};
208       }
209       $script = $NRPEMap->{$script}{'script'};
210     }
211
212     # Check if the script exists and is executable.
213     if (!-e $script)
214     {
215       print STDERR "Script `$script' doesn't exist.\n";
216     }
217     elsif (!-x $script)
218     {
219       print STDERR "Script `$script' exists but is not executable.\n";
220     }
221     else
222     {
223       # Add the script to the global @$Script array.
224       if (ref ($opts) eq 'ARRAY')
225       {
226         for (@$opts)
227         {
228           my $opt = $_;
229           update_script_opts ($opt, $script, $nrpe_args);
230           push (@$Scripts, $opt);
231         }
232       }
233       else
234       {
235         update_script_opts ($opts, $script, $nrpe_args);
236         push (@$Scripts, $opts);
237       }
238     }
239   } # for (keys %$scripts)
240 } # handle_config_script
241
242 sub handle_config
243 {
244   my $config = shift;
245
246   if (defined ($config->{'nrpeconfig'}))
247   {
248     if (ref ($config->{'nrpeconfig'}) eq 'ARRAY')
249     {
250       for (@{$config->{'nrpeconfig'}})
251       {
252         parse_nrpe_conf ($_);
253       }
254     }
255     elsif (ref ($config->{'nrpeconfig'}) eq '')
256     {
257       parse_nrpe_conf ($config->{'nrpeconfig'});
258     }
259     else
260     {
261       print STDERR "Cannot handle ref type '"
262       . ref ($config->{'nrpeconfig'}) . "' for option 'NRPEConfig'.\n";
263     }
264   }
265
266   if (defined ($config->{'addtype'}))
267   {
268     if (ref ($config->{'addtype'}) eq 'ARRAY')
269     {
270       handle_config_addtype ($config->{'addtype'});
271     }
272     elsif (ref ($config->{'addtype'}) eq '')
273     {
274       handle_config_addtype ([$config->{'addtype'}]);
275     }
276     else
277     {
278       print STDERR "Cannot handle ref type '"
279       . ref ($config->{'addtype'}) . "' for option 'AddType'.\n";
280     }
281   }
282
283   if (defined ($config->{'script'}))
284   {
285     if (ref ($config->{'script'}) eq 'HASH')
286     {
287       handle_config_script ($config->{'script'});
288     }
289     else
290     {
291       print STDERR "Cannot handle ref type '"
292       . ref ($config->{'script'}) . "' for option 'Script'.\n";
293     }
294   }
295
296   if (defined ($config->{'interval'})
297     && (ref ($config->{'interval'}) eq ''))
298   {
299     my $num = int ($config->{'interval'});
300     if ($num > 0)
301     {
302       $Interval = $num;
303     }
304   }
305 } # handle_config }}}
306
307 sub scale_value
308 {
309   my $value = shift;
310   my $unit = shift;
311
312   if (!$unit)
313   {
314     return ($value);
315   }
316
317   if (($unit =~ m/^mb(yte)?$/i) || ($unit eq 'M'))
318   {
319     return ($value * 1000000);
320   }
321   elsif ($unit =~ m/^k(b(yte)?)?$/i)
322   {
323     return ($value * 1000);
324   }
325
326   return ($value);
327 }
328
329 sub sanitize_instance
330 {
331   my $inst = shift;
332
333   if ($inst eq '/')
334   {
335     return ('root');
336   }
337
338   $inst =~ s/[^A-Za-z_-]/_/g;
339   $inst =~ s/__+/_/g;
340   $inst =~ s/^_//;
341   $inst =~ s/_$//;
342
343   return ($inst);
344 }
345
346 sub handle_performance_data
347 {
348   my $host = shift;
349   my $plugin = shift;
350   my $pinst = shift;
351   my $type = shift;
352   my $time = shift;
353   my $line = shift;
354
355   my $tinst;
356   my $value;
357   my $unit;
358
359   if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/)
360   {
361     $tinst = sanitize_instance ($1);
362     $value = scale_value ($2, $3);
363   }
364   else
365   {
366     return;
367   }
368
369   print "PUTVAL $host/$plugin-$pinst/$type-$tinst interval=$Interval ${time}:$value\n";
370 }
371
372 sub execute_script
373 {
374   my $fh;
375   my $pinst;
376   my $time = time ();
377   my $script = shift;
378   my @args = ();
379   my $host = hostname () || 'localhost';
380
381   my $state = 0;
382   my $serviceoutput;
383   my @serviceperfdata;
384   my @longserviceoutput;
385
386   my $script_name = $script->{'script'};
387   
388   if ($script->{'arguments'})
389   {
390     @args = split (' ', $script->{'arguments'});
391   }
392
393   if (!open ($fh, '-|', $script_name, @args))
394   {
395     print STDERR "Cannot execute $script_name: $!";
396     return;
397   }
398
399   $pinst = sanitize_instance (basename ($script_name));
400
401   # Parse the output of the plugin. The format is seriously fucked up, because
402   # it got extended way beyond what it could handle.
403   while (my $line = <$fh>)
404   {
405     chomp ($line);
406
407     if ($state == 0)
408     {
409       my $perfdata;
410       ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
411       
412       if ($perfdata)
413       {
414         push (@serviceperfdata, split (' ', $perfdata));
415       }
416
417       $state = 1;
418     }
419     elsif ($state == 1)
420     {
421       my $longoutput;
422       my $perfdata;
423       ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
424
425       push (@longserviceoutput, $longoutput);
426
427       if ($perfdata)
428       {
429         push (@serviceperfdata, split (' ', $perfdata));
430         $state = 2;
431       }
432     }
433     else # ($state == 2)
434     {
435       push (@serviceperfdata, split (' ', $line));
436     }
437   }
438
439   close ($fh);
440   # Save the exit status of the check in $state
441   $state = $?;
442
443   if ($state == 0)
444   {
445     $state = 'okay';
446   }
447   elsif ($state == 1)
448   {
449     $state = 'warning';
450   }
451   else
452   {
453     $state = 'failure';
454   }
455
456   {
457     my $type = $script->{'type'} || 'nagios_check';
458
459     print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
460     . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
461   }
462
463   if ($script->{'type'})
464   {
465     for (@serviceperfdata)
466     {
467       handle_performance_data ($host, 'nagios', $pinst, $script->{'type'},
468         $time, $_);
469     }
470   }
471 } # execute_script
472
473 sub main
474 {
475   my $last_run;
476   my $next_run;
477
478   my %config = ParseConfig (-ConfigFile => $ConfigFile,
479     -AutoTrue => 1,
480     -LowerCaseNames => 1);
481   handle_config (\%config);
482
483   while (42)
484   {
485     $last_run = time ();
486     $next_run = $last_run + $Interval;
487
488     for (@$Scripts)
489     {
490       execute_script ($_);
491     }
492
493     while ((my $timeleft = ($next_run - time ())) > 0)
494     {
495       sleep ($timeleft);
496     }
497   }
498 } # main
499
500 =head1 REQUIREMENTS
501
502 This script requires the following Perl modules to be installed:
503
504 =over 4
505
506 =item C<Config::General>
507
508 =item C<Regexp::Common>
509
510 =back
511
512 =head1 SEE ALSO
513
514 L<http://www.nagios.org/>,
515 L<http://nagiosplugins.org/>,
516 L<http://collectd.org/>,
517 L<collectd-exec(5)>
518
519 =head1 AUTHOR
520
521 Florian octo Forster E<lt>octo at verplant.orgE<gt>
522
523 =cut
524
525 # vim: set sw=2 sts=2 ts=8 fdm=marker :