Merge remote branch 'powdahound/master'
[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 $Scripts = [];
27 our $Interval = 300;
28
29 main ();
30 exit (0);
31
32 # Configuration
33 # {{{
34
35 =head1 CONFIGURATION
36
37 This script reads it's configuration from F</etc/exec-nagios.conf>. The
38 configuration is read using C<Config::General> which understands a Apache-like
39 config syntax, so it's very similar to the F<collectd.conf> syntax, too.
40
41 Here's a short sample config:
42
43   Interval 300
44   <Script /usr/lib/nagios/check_tcp>
45     Arguments -H alice -p 22
46     Type delay
47   </Script>
48   <Script /usr/lib/nagios/check_dns>
49     Arguments -H alice
50     Type delay
51   </Script>
52
53 The options have the following semantic (i.E<nbsp>e. meaning):
54
55 =over 4
56
57 =item B<Interval> I<Seconds>
58
59 Sets the interval in which the plugins are executed. This doesn't need to match
60 the interval setting of the collectd daemon. Usually, you want to execute the
61 Nagios plugins much less often, e.E<nbsp>g. every 300 seconds versus every 10
62 seconds.
63
64 =item E<lt>B<Script> I<File>E<gt>
65
66 Adds a script to the list of scripts to be executed once per I<Interval>
67 seconds. You can use the following optional arguments to specify the operation
68 further:
69
70 =over 4
71
72 =item B<Arguments> I<Arguments>
73
74 Pass the arguments I<Arguments> to the script. This is often needed with Nagios
75 plugins, because much of the logic is implemented in the plugins, not in the
76 daemon. If you need to specify a warning and/or critical range here, please
77 consider using collectd's own threshold mechanism, which is by far the more
78 elegant solution than this transition layer.
79
80 =item B<Type> I<Type>
81
82 If the plugin provides "performance data" the performance data is dispatched to
83 collectd with this type. If no type is configured the data is ignored. Please
84 note that this is limited to types that take exactly one value, such as the
85 type C<delay> in the example above. If you need more complex performance data,
86 rewrite the plugin as a collectd plugin (or at least port it do run directly
87 with the C<exec-plugin>).
88
89 =back
90
91 =back
92
93 =cut
94
95 sub handle_config_addtype
96 {
97   my $list = shift;
98
99   for (my $i = 0; $i < @$list; $i++)
100   {
101     my ($to, @from) = split (' ', $list->[$i]);
102     for (my $j = 0; $j < @from; $j++)
103     {
104       $TypeMap->{$from[$j]} = $to;
105     }
106   }
107 } # handle_config_addtype
108
109 sub handle_config_script
110 {
111   my $scripts = shift;
112
113   for (keys %$scripts)
114   {
115     my $script = $_;
116     my $opts = $scripts->{$script};
117
118     if (!-e $script)
119     {
120       print STDERR "Script `$script' doesn't exist.\n";
121     }
122     elsif (!-x $script)
123     {
124       print STDERR "Script `$script' exists but is not executable.\n";
125     }
126     else
127     {
128       if (ref ($opts) eq 'ARRAY')
129         {
130           for (@$opts)
131             {
132               my $opt = $_;
133               $opt->{'script'} = $script;
134               push (@$Scripts, $opt);
135             }
136         }
137           else
138         {
139           $opts->{'script'} = $script;
140           push (@$Scripts, $opts);
141         }
142     }
143   } # for (keys %$scripts)
144 } # handle_config_script
145
146 sub handle_config
147 {
148   my $config = shift;
149
150   if (defined ($config->{'addtype'}))
151   {
152     if (ref ($config->{'addtype'}) eq 'ARRAY')
153     {
154       handle_config_addtype ($config->{'addtype'});
155     }
156     elsif (ref ($config->{'addtype'}) eq '')
157     {
158       handle_config_addtype ([$config->{'addtype'}]);
159     }
160     else
161     {
162       print STDERR "Cannot handle ref type '"
163       . ref ($config->{'addtype'}) . "' for option 'AddType'.\n";
164     }
165   }
166
167   if (defined ($config->{'script'}))
168   {
169     if (ref ($config->{'script'}) eq 'HASH')
170     {
171       handle_config_script ($config->{'script'});
172     }
173     else
174     {
175       print STDERR "Cannot handle ref type '"
176       . ref ($config->{'script'}) . "' for option 'Script'.\n";
177     }
178   }
179
180   if (defined ($config->{'interval'})
181     && (ref ($config->{'interval'}) eq ''))
182   {
183     my $num = int ($config->{'interval'});
184     if ($num > 0)
185     {
186       $Interval = $num;
187     }
188   }
189 } # handle_config }}}
190
191 sub scale_value
192 {
193   my $value = shift;
194   my $unit = shift;
195
196   if (!$unit)
197   {
198     return ($value);
199   }
200
201   if (($unit =~ m/^mb(yte)?$/i) || ($unit eq 'M'))
202   {
203     return ($value * 1000000);
204   }
205   elsif ($unit =~ m/^k(b(yte)?)?$/i)
206   {
207     return ($value * 1000);
208   }
209
210   return ($value);
211 }
212
213 sub sanitize_instance
214 {
215   my $inst = shift;
216
217   if ($inst eq '/')
218   {
219     return ('root');
220   }
221
222   $inst =~ s/[^A-Za-z_-]/_/g;
223   $inst =~ s/__+/_/g;
224   $inst =~ s/^_//;
225   $inst =~ s/_$//;
226
227   return ($inst);
228 }
229
230 sub handle_performance_data
231 {
232   my $host = shift;
233   my $plugin = shift;
234   my $pinst = shift;
235   my $type = shift;
236   my $time = shift;
237   my $line = shift;
238
239   my $tinst;
240   my $value;
241   my $unit;
242
243   if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/)
244   {
245     $tinst = sanitize_instance ($1);
246     $value = scale_value ($2, $3);
247   }
248   else
249   {
250     return;
251   }
252
253   print "PUTVAL $host/$plugin-$pinst/$type-$tinst interval=$Interval ${time}:$value\n";
254 }
255
256 sub execute_script
257 {
258   my $fh;
259   my $pinst;
260   my $time = time ();
261   my $script = shift;
262   my @args = ();
263   my $host = hostname () || 'localhost';
264
265   my $state = 0;
266   my $serviceoutput;
267   my @serviceperfdata;
268   my @longserviceoutput;
269
270   my $script_name = $script->{'script'};
271   
272   if ($script->{'arguments'})
273   {
274     @args = split (' ', $script->{'arguments'});
275   }
276
277   if (!open ($fh, '-|', $script_name, @args))
278   {
279     print STDERR "Cannot execute $script_name: $!";
280     return;
281   }
282
283   $pinst = sanitize_instance (basename ($script_name));
284
285   # Parse the output of the plugin. The format is seriously fucked up, because
286   # it got extended way beyond what it could handle.
287   while (my $line = <$fh>)
288   {
289     chomp ($line);
290
291     if ($state == 0)
292     {
293       my $perfdata;
294       ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
295       
296       if ($perfdata)
297       {
298         push (@serviceperfdata, split (' ', $perfdata));
299       }
300
301       $state = 1;
302     }
303     elsif ($state == 1)
304     {
305       my $longoutput;
306       my $perfdata;
307       ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
308
309       push (@longserviceoutput, $longoutput);
310
311       if ($perfdata)
312       {
313         push (@serviceperfdata, split (' ', $perfdata));
314         $state = 2;
315       }
316     }
317     else # ($state == 2)
318     {
319       push (@serviceperfdata, split (' ', $line));
320     }
321   }
322
323   close ($fh);
324   # Save the exit status of the check in $state
325   $state = $?;
326
327   if ($state == 0)
328   {
329     $state = 'okay';
330   }
331   elsif ($state == 1)
332   {
333     $state = 'warning';
334   }
335   else
336   {
337     $state = 'failure';
338   }
339
340   {
341     my $type = $script->{'type'} || 'nagios_check';
342
343     print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
344     . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
345   }
346
347   if ($script->{'type'})
348   {
349     for (@serviceperfdata)
350     {
351       handle_performance_data ($host, 'nagios', $pinst, $script->{'type'},
352         $time, $_);
353     }
354   }
355 } # execute_script
356
357 sub main
358 {
359   my $last_run;
360   my $next_run;
361
362   my %config = ParseConfig (-ConfigFile => $ConfigFile,
363     -AutoTrue => 1,
364     -LowerCaseNames => 1);
365   handle_config (\%config);
366
367   while (42)
368   {
369     $last_run = time ();
370     $next_run = $last_run + $Interval;
371
372     for (@$Scripts)
373     {
374       execute_script ($_);
375     }
376
377     while ((my $timeleft = ($next_run - time ())) > 0)
378     {
379       sleep ($timeleft);
380     }
381   }
382 } # main
383
384 =head1 REQUIREMENTS
385
386 This script requires the following Perl modules to be installed:
387
388 =over 4
389
390 =item C<Config::General>
391
392 =item C<Regexp::Common>
393
394 =back
395
396 =head1 SEE ALSO
397
398 L<http://www.nagios.org/>,
399 L<http://nagiosplugins.org/>,
400 L<http://collectd.org/>,
401 L<collectd-exec(5)>
402
403 =head1 AUTHOR
404
405 Florian octo Forster E<lt>octo at verplant.orgE<gt>
406
407 =cut
408
409 # vim: set sw=2 sts=2 ts=8 fdm=marker :