contrib/exec-nagios.px: Added a Perl script which handles Nagios plugins.
[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 =cut
92
93 sub handle_config_addtype
94 {
95   my $list = shift;
96
97   for (my $i = 0; $i < @$list; $i++)
98   {
99     my ($to, @from) = split (' ', $list->[$i]);
100     for (my $j = 0; $j < @from; $j++)
101     {
102       $TypeMap->{$from[$j]} = $to;
103     }
104   }
105 } # handle_config_addtype
106
107 sub handle_config_script
108 {
109   my $scripts = shift;
110
111   for (keys %$scripts)
112   {
113     my $script = $_;
114     my $opts = $scripts->{$script};
115
116     if (!-e $script)
117     {
118       print STDERR "Script `$script' doesn't exist.\n";
119     }
120     elsif (!-x $script)
121     {
122       print STDERR "Script `$script' exists but is not executable.\n";
123     }
124     else
125     {
126       $opts->{'script'} = $script;
127       push (@$Scripts, $opts);
128     }
129   } # for (keys %$scripts)
130 } # handle_config_script
131
132 sub handle_config
133 {
134   my $config = shift;
135
136   if (defined ($config->{'addtype'}))
137   {
138     if (ref ($config->{'addtype'}) eq 'ARRAY')
139     {
140       handle_config_addtype ($config->{'addtype'});
141     }
142     elsif (ref ($config->{'addtype'}) eq '')
143     {
144       handle_config_addtype ([$config->{'addtype'}]);
145     }
146     else
147     {
148       print STDERR "Cannot handle ref type '"
149       . ref ($config->{'addtype'}) . "' for option 'AddType'.\n";
150     }
151   }
152
153   if (defined ($config->{'script'}))
154   {
155     if (ref ($config->{'script'}) eq 'HASH')
156     {
157       handle_config_script ($config->{'script'});
158     }
159     else
160     {
161       print STDERR "Cannot handle ref type '"
162       . ref ($config->{'script'}) . "' for option 'Script'.\n";
163     }
164   }
165
166   if (defined ($config->{'interval'})
167     && (ref ($config->{'interval'}) eq ''))
168   {
169     my $num = int ($config->{'interval'});
170     if ($num > 0)
171     {
172       $Interval = $num;
173     }
174   }
175 } # handle_config }}}
176
177 sub scale_value
178 {
179   my $value = shift;
180   my $unit = shift;
181
182   if (!$unit)
183   {
184     return ($value);
185   }
186
187   if (($unit =~ m/^mb(yte)?$/i) || ($unit eq 'M'))
188   {
189     return ($value * 1000000);
190   }
191   elsif ($unit =~ m/^k(b(yte)?)?$/i)
192   {
193     return ($value * 1000);
194   }
195
196   return ($value);
197 }
198
199 sub sanitize_instance
200 {
201   my $inst = shift;
202
203   if ($inst eq '/')
204   {
205     return ('root');
206   }
207
208   $inst =~ s/[^A-Za-z_-]/_/g;
209   $inst =~ s/__+/_/g;
210   $inst =~ s/^_//;
211   $inst =~ s/_$//;
212
213   return ($inst);
214 }
215
216 sub handle_performance_data
217 {
218   my $host = shift;
219   my $plugin = shift;
220   my $pinst = shift;
221   my $type = shift;
222   my $time = shift;
223   my $line = shift;
224
225   my $tinst;
226   my $value;
227   my $unit;
228
229   if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/)
230   {
231     $tinst = sanitize_instance ($1);
232     $value = scale_value ($2, $3);
233   }
234   else
235   {
236     return;
237   }
238
239   print "PUTVAL $host/$plugin-$pinst/$type-$tinst interval=$Interval ${time}:$value\n";
240 }
241
242 sub execute_script
243 {
244   my $fh;
245   my $pinst;
246   my $time = time ();
247   my $script = shift;
248   my @args = ();
249   my $host = hostname () || 'localhost';
250
251   my $state = 0;
252   my $serviceoutput;
253   my @serviceperfdata;
254   my @longserviceoutput;
255
256   my $script_name = $script->{'script'};
257   
258   if ($script->{'arguments'})
259   {
260     @args = split (' ', $script->{'arguments'});
261   }
262
263   if (!open ($fh, '-|', $script_name, @args))
264   {
265     print STDERR "Cannot execute $script_name: $!";
266     return;
267   }
268
269   $pinst = sanitize_instance (basename ($script_name));
270
271   # Parse the output of the plugin. The format is seriously fucked up, because
272   # it got extended way beyond what it could handle.
273   while (my $line = <$fh>)
274   {
275     chomp ($line);
276
277     if ($state == 0)
278     {
279       my $perfdata;
280       ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
281       
282       if ($perfdata)
283       {
284         push (@serviceperfdata, split (' ', $perfdata));
285       }
286
287       $state = 1;
288     }
289     elsif ($state == 1)
290     {
291       my $longoutput;
292       my $perfdata;
293       ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
294
295       push (@longserviceoutput, $longoutput);
296
297       if ($perfdata)
298       {
299         push (@serviceperfdata, split (' ', $perfdata));
300         $state = 2;
301       }
302     }
303     else # ($state == 2)
304     {
305       push (@serviceperfdata, split (' ', $line));
306     }
307   }
308
309   close ($fh);
310   # Save the exit status of the check in $state
311   $state = $?;
312
313   if ($state == 0)
314   {
315     $state = 'okay';
316   }
317   elsif ($state == 1)
318   {
319     $state = 'warning';
320   }
321   else
322   {
323     $state = 'failure';
324   }
325
326   {
327     my $type = $script->{'type'} || 'nagios_check';
328
329     print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
330     . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
331   }
332
333   if ($script->{'type'})
334   {
335     for (@serviceperfdata)
336     {
337       handle_performance_data ($host, 'nagios', $pinst, $script->{'type'},
338         $time, $_);
339     }
340   }
341 } # execute_script
342
343 sub main
344 {
345   my $last_run;
346   my $next_run;
347
348   my %config = ParseConfig (-ConfigFile => $ConfigFile,
349     -AutoTrue => 1,
350     -LowerCaseNames => 1);
351   handle_config (\%config);
352
353   while (42)
354   {
355     $last_run = time ();
356     $next_run = $last_run + $Interval;
357
358     for (@$Scripts)
359     {
360       execute_script ($_);
361     }
362
363     while ((my $timeleft = ($next_run - time ())) > 0)
364     {
365       sleep ($timeleft);
366     }
367   }
368 } # main
369
370 =head1 REQUIREMENTS
371
372 This script requires the following Perl modules to be installed:
373
374 =over 4
375
376 =item C<Config::General>
377
378 =item C<Regexp::Common>
379
380 =back
381
382 =head1 SEE ALSO
383
384 L<http://www.nagios.org/>,
385 L<http://nagiosplugins.org/>,
386 L<http://collectd.org/>,
387 L<collectd-exec(5)>
388
389 =head1 AUTHOR
390
391 Florian octo Forster E<lt>octo at verplant.orgE<gt>
392
393 =cut
394
395 # vim: set sw=2 sts=2 ts=8 fdm=marker :