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