Merge pull request #2961 from octo/ff/utils
[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 = defined ($ENV{'COLLECTD_INTERVAL'}) ? (0 + $ENV{'COLLECTD_INTERVAL'}) : 300;
29 our $Hostname = defined ($ENV{'COLLECTD_HOSTNAME'}) ? $ENV{'COLLECTD_HOSTNAME'} : '';
30
31 main ();
32 exit (0);
33
34 # Configuration
35 # {{{
36
37 =head1 CONFIGURATION
38
39 This script reads its 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.
42
43 Here's a short sample config:
44
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>
55
56 The options have the following semantic (i.E<nbsp>e. meaning):
57
58 =over 4
59
60 =item B<NRPEConfig> I<File>
61
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".
67
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.
71
72 =item B<Interval> I<Seconds>
73
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.
78
79 =item E<lt>B<Script> I<File>E<gt>
80
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:
85
86 =over 4
87
88 =item B<Arguments> I<Arguments>
89
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.
95
96 =item B<Type> I<Type>
97
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>).
104
105 =back
106
107 =back
108
109 =cut
110
111 sub parse_nrpe_conf
112 {
113   my $file = shift;
114   my $fh;
115   my $status;
116
117   $status = open ($fh, '<', $file);
118   if (!$status)
119   {
120     print STDERR "Reading NRPE config from \"$file\" failed: $!\n";
121     return;
122   }
123
124   while (<$fh>)
125   {
126     my $line = $_;
127     chomp ($line);
128
129     if ($line =~ m/^\s*command\[([^\]]+)\]\s*=\s*(.+)$/)
130     {
131       my $alias = $1;
132       my $script;
133       my $arguments;
134
135       ($script, $arguments) = split (' ', $2, 2);
136
137       if ($NRPEMap->{$alias})
138       {
139         print STDERR "Warning: NRPE command \"$alias\" redefined.\n";
140       }
141
142       $NRPEMap->{$alias} = { script => $script };
143       if ($arguments)
144       {
145         $NRPEMap->{$alias}{'arguments'} = $arguments;
146       }
147     }
148   } # while (<$fh>)
149
150   close ($fh);
151 } # parse_nrpe_conf
152
153 sub handle_config_addtype
154 {
155   my $list = shift;
156
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
166
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
171 {
172   my $opts = shift;
173   my $script = shift;
174   my $nrpe_args = shift;
175
176   $opts->{'script'} = $script;
177
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
190
191 sub handle_config_script
192 {
193   my $scripts = shift;
194
195   for (keys %$scripts)
196   {
197     my $script = $_;
198     my $opts = $scripts->{$script};
199
200     my $nrpe_args = '';
201
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     }
212
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
242
243 sub handle_config
244 {
245   my $config = shift;
246
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   }
266
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   }
283
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   }
296
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 }}}
307
308 sub scale_value
309 {
310   my $value = shift;
311   my $unit = shift;
312
313   if (!$unit)
314   {
315     return ($value);
316   }
317
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   }
326
327   return ($value);
328 }
329
330 sub sanitize_instance
331 {
332   my $inst = shift;
333
334   if ($inst eq '/')
335   {
336     return ('root');
337   }
338
339   $inst =~ s/[^A-Za-z_-]/_/g;
340   $inst =~ s/__+/_/g;
341   $inst =~ s/^_//;
342   $inst =~ s/_$//;
343
344   return ($inst);
345 }
346
347 sub handle_performance_data
348 {
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";
356
357   my $tinst;
358   my $value;
359   my $unit;
360
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   }
370
371   $ident =~ s/"/\\"/g;
372
373   print qq(PUTVAL "$ident" interval=$Interval ${time}:$value\n);
374 }
375
376 sub execute_script
377 {
378   my $fh;
379   my $pinst;
380   my $time = time ();
381   my $script = shift;
382   my @args = ();
383   my $host = $Hostname || hostname () || 'localhost';
384
385   my $state = 0;
386   my $serviceoutput;
387   my @serviceperfdata;
388   my @longserviceoutput;
389
390   my $script_name = $script->{'script'};
391   
392   if ($script->{'arguments'})
393   {
394     @args = split (' ', $script->{'arguments'});
395   }
396
397   if (!open ($fh, '-|', $script_name, @args))
398   {
399     print STDERR "Cannot execute $script_name: $!";
400     return;
401   }
402
403   $pinst = sanitize_instance (basename ($script_name));
404
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);
410
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       }
420
421       $state = 1;
422     }
423     elsif ($state == 1)
424     {
425       my $longoutput;
426       my $perfdata;
427       ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
428
429       push (@longserviceoutput, $longoutput);
430
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   }
442
443   close ($fh);
444   # Save the exit status of the check in $state
445   $state = $? >> 8;
446
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   }
459
460   {
461     my $type = $script->{'type'} || 'nagios_check';
462
463     print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
464     . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
465   }
466
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
476
477 sub main
478 {
479   my $last_run;
480   my $next_run;
481
482   my %config = ParseConfig (-ConfigFile => $ConfigFile,
483     -AutoTrue => 1,
484     -LowerCaseNames => 1);
485   handle_config (\%config);
486
487   while (42)
488   {
489     $last_run = time ();
490     $next_run = $last_run + $Interval;
491
492     for (@$Scripts)
493     {
494       execute_script ($_);
495     }
496
497     while ((my $timeleft = ($next_run - time ())) > 0)
498     {
499       sleep ($timeleft);
500     }
501   }
502 } # main
503
504 =head1 REQUIREMENTS
505
506 This script requires the following Perl modules to be installed:
507
508 =over 4
509
510 =item C<Config::General>
511
512 =item C<Regexp::Common>
513
514 =back
515
516 =head1 SEE ALSO
517
518 L<http://www.nagios.org/>,
519 L<http://nagiosplugins.org/>,
520 L<http://collectd.org/>,
521 L<collectd-exec(5)>
522
523 =head1 AUTHOR
524
525 Florian octo Forster E<lt>octo at verplant.orgE<gt>
526
527 =cut
528
529 # vim: set sw=2 sts=2 ts=8 fdm=marker :