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