b26981fb32c7687507ae1690a53ceefa29ca6984
[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   my $ident = "$host/$plugin-$pinst/$type-$tinst";
239
240   my $tinst;
241   my $value;
242   my $unit;
243
244   if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/)
245   {
246     $tinst = sanitize_instance ($1);
247     $value = scale_value ($2, $3);
248   }
249   else
250   {
251     return;
252   }
253
254   $ident =~ s/"/\\"/g;
255
256   print qq(PUTVAL "$ident" interval=$Interval ${time}:$value\n);
257 }
258
259 sub execute_script
260 {
261   my $fh;
262   my $pinst;
263   my $time = time ();
264   my $script = shift;
265   my @args = ();
266   my $host = hostname () || 'localhost';
267
268   my $state = 0;
269   my $serviceoutput;
270   my @serviceperfdata;
271   my @longserviceoutput;
272
273   my $script_name = $script->{'script'};
274   
275   if ($script->{'arguments'})
276   {
277     @args = split (' ', $script->{'arguments'});
278   }
279
280   if (!open ($fh, '-|', $script_name, @args))
281   {
282     print STDERR "Cannot execute $script_name: $!";
283     return;
284   }
285
286   $pinst = sanitize_instance (basename ($script_name));
287
288   # Parse the output of the plugin. The format is seriously fucked up, because
289   # it got extended way beyond what it could handle.
290   while (my $line = <$fh>)
291   {
292     chomp ($line);
293
294     if ($state == 0)
295     {
296       my $perfdata;
297       ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
298       
299       if ($perfdata)
300       {
301         push (@serviceperfdata, split (' ', $perfdata));
302       }
303
304       $state = 1;
305     }
306     elsif ($state == 1)
307     {
308       my $longoutput;
309       my $perfdata;
310       ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
311
312       push (@longserviceoutput, $longoutput);
313
314       if ($perfdata)
315       {
316         push (@serviceperfdata, split (' ', $perfdata));
317         $state = 2;
318       }
319     }
320     else # ($state == 2)
321     {
322       push (@serviceperfdata, split (' ', $line));
323     }
324   }
325
326   close ($fh);
327   # Save the exit status of the check in $state
328   $state = $?;
329
330   if ($state == 0)
331   {
332     $state = 'okay';
333   }
334   elsif ($state == 1)
335   {
336     $state = 'warning';
337   }
338   else
339   {
340     $state = 'failure';
341   }
342
343   {
344     my $type = $script->{'type'} || 'nagios_check';
345
346     print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
347     . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
348   }
349
350   if ($script->{'type'})
351   {
352     for (@serviceperfdata)
353     {
354       handle_performance_data ($host, 'nagios', $pinst, $script->{'type'},
355         $time, $_);
356     }
357   }
358 } # execute_script
359
360 sub main
361 {
362   my $last_run;
363   my $next_run;
364
365   my %config = ParseConfig (-ConfigFile => $ConfigFile,
366     -AutoTrue => 1,
367     -LowerCaseNames => 1);
368   handle_config (\%config);
369
370   while (42)
371   {
372     $last_run = time ();
373     $next_run = $last_run + $Interval;
374
375     for (@$Scripts)
376     {
377       execute_script ($_);
378     }
379
380     while ((my $timeleft = ($next_run - time ())) > 0)
381     {
382       sleep ($timeleft);
383     }
384   }
385 } # main
386
387 =head1 REQUIREMENTS
388
389 This script requires the following Perl modules to be installed:
390
391 =over 4
392
393 =item C<Config::General>
394
395 =item C<Regexp::Common>
396
397 =back
398
399 =head1 SEE ALSO
400
401 L<http://www.nagios.org/>,
402 L<http://nagiosplugins.org/>,
403 L<http://collectd.org/>,
404 L<collectd-exec(5)>
405
406 =head1 AUTHOR
407
408 Florian octo Forster E<lt>octo at verplant.orgE<gt>
409
410 =cut
411
412 # vim: set sw=2 sts=2 ts=8 fdm=marker :