Code

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