1 #!/usr/bin/perl
3 use strict;
4 use warnings;
6 =head1 NAME
8 exec-nagios.px
10 =head1 DESCRIPTION
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.
17 =cut
19 use Sys::Hostname ('hostname');
20 use File::Basename ('basename');
21 use Config::General ('ParseConfig');
22 use Regexp::Common ('number');
24 our $ConfigFile = '/etc/exec-nagios.conf';
25 our $TypeMap = {};
26 our $NRPEMap = {};
27 our $Scripts = [];
28 our $Interval = 300;
30 main ();
31 exit (0);
33 # Configuration
34 # {{{
36 =head1 CONFIGURATION
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.
42 Here's a short sample config:
44 NRPEConfig "/etc/nrpe.cfg"
45 Interval 300
46 <Script /usr/lib/nagios/check_tcp>
47 Arguments -H alice -p 22
48 Type delay
49 </Script>
50 <Script /usr/lib/nagios/check_dns>
51 Arguments -H alice
52 Type delay
53 </Script>
55 The options have the following semantic (i.E<nbsp>e. meaning):
57 =over 4
59 =item B<NRPEConfig> I<File>
61 Read the NRPE config and add the command definitions to an alias table. After
62 reading the file you can use the NRPE command name rather than the script's
63 filename within B<Script> blocks (see below). If both, the NRPE config and the
64 B<Script> block, define arguments they will be merged by concatenating the
65 arguments together in the order "NRPE-args Script-args".
67 Please note that this option is rather dumb. It does not support "command
68 argument processing" (i.e. replacing C<$ARG1$> and friends), inclusion of other
69 NRPE config files, include directories etc.
71 =item B<Interval> I<Seconds>
73 Sets the interval in which the plugins are executed. This doesn't need to match
74 the interval setting of the collectd daemon. Usually, you want to execute the
75 Nagios plugins much less often, e.E<nbsp>g. every 300 seconds versus every 10
76 seconds.
78 =item E<lt>B<Script> I<File>E<gt>
80 Adds a script to the list of scripts to be executed once per I<Interval>
81 seconds. If the B<NRPEConfig> is given above the B<Script> block, you may use
82 the NRPE command name rather than the script's filename. You can use the
83 following optional arguments to specify the operation further:
85 =over 4
87 =item B<Arguments> I<Arguments>
89 Pass the arguments I<Arguments> to the script. This is often needed with Nagios
90 plugins, because much of the logic is implemented in the plugins, not in the
91 daemon. If you need to specify a warning and/or critical range here, please
92 consider using collectd's own threshold mechanism, which is by far the more
93 elegant solution than this transition layer.
95 =item B<Type> I<Type>
97 If the plugin provides "performance data" the performance data is dispatched to
98 collectd with this type. If no type is configured the data is ignored. Please
99 note that this is limited to types that take exactly one value, such as the
100 type C<delay> in the example above. If you need more complex performance data,
101 rewrite the plugin as a collectd plugin (or at least port it do run directly
102 with the C<exec-plugin>).
104 =back
106 =back
108 =cut
110 sub parse_nrpe_conf
111 {
112 my $file = shift;
113 my $fh;
114 my $status;
116 $status = open ($fh, '<', $file);
117 if (!$status)
118 {
119 print STDERR "Reading NRPE config from \"$file\" failed: $!\n";
120 return;
121 }
123 while (<$fh>)
124 {
125 my $line = $_;
126 chomp ($line);
128 if ($line =~ m/^\s*command\[([^\]]+)\]\s*=\s*(.+)$/)
129 {
130 my $alias = $1;
131 my $script;
132 my $arguments;
134 ($script, $arguments) = split (' ', $2, 2);
136 if ($NRPEMap->{$alias})
137 {
138 print STDERR "Warning: NRPE command \"$alias\" redefined.\n";
139 }
141 $NRPEMap->{$alias} = { script => $script };
142 if ($arguments)
143 {
144 $NRPEMap->{$alias}{'arguments'} = $arguments;
145 }
146 }
147 } # while (<$fh>)
149 close ($fh);
150 } # parse_nrpe_conf
152 sub handle_config_addtype
153 {
154 my $list = shift;
156 for (my $i = 0; $i < @$list; $i++)
157 {
158 my ($to, @from) = split (' ', $list->[$i]);
159 for (my $j = 0; $j < @from; $j++)
160 {
161 $TypeMap->{$from[$j]} = $to;
162 }
163 }
164 } # handle_config_addtype
166 # Update the script record. This function adds the name of the script /
167 # executable to the hash and merges the configured and NRPE arguments if
168 # required.
169 sub update_script_opts
170 {
171 my $opts = shift;
172 my $script = shift;
173 my $nrpe_args = shift;
175 $opts->{'script'} = $script;
177 if ($nrpe_args)
178 {
179 if ($opts->{'arguments'})
180 {
181 $opts->{'arguments'} = $nrpe_args . ' ' . $opts->{'arguments'};
182 }
183 else
184 {
185 $opts->{'arguments'} = $nrpe_args;
186 }
187 }
188 } # update_script_opts
190 sub handle_config_script
191 {
192 my $scripts = shift;
194 for (keys %$scripts)
195 {
196 my $script = $_;
197 my $opts = $scripts->{$script};
199 my $nrpe_args = '';
201 # Check if the script exists in the NRPE map. If so, replace the alias name
202 # with the actual script name.
203 if ($NRPEMap->{$script})
204 {
205 if ($NRPEMap->{$script}{'arguments'})
206 {
207 $nrpe_args = $NRPEMap->{$script}{'arguments'};
208 }
209 $script = $NRPEMap->{$script}{'script'};
210 }
212 # Check if the script exists and is executable.
213 if (!-e $script)
214 {
215 print STDERR "Script `$script' doesn't exist.\n";
216 }
217 elsif (!-x $script)
218 {
219 print STDERR "Script `$script' exists but is not executable.\n";
220 }
221 else
222 {
223 # Add the script to the global @$Script array.
224 if (ref ($opts) eq 'ARRAY')
225 {
226 for (@$opts)
227 {
228 my $opt = $_;
229 update_script_opts ($opt, $script, $nrpe_args);
230 push (@$Scripts, $opt);
231 }
232 }
233 else
234 {
235 update_script_opts ($opts, $script, $nrpe_args);
236 push (@$Scripts, $opts);
237 }
238 }
239 } # for (keys %$scripts)
240 } # handle_config_script
242 sub handle_config
243 {
244 my $config = shift;
246 if (defined ($config->{'nrpeconfig'}))
247 {
248 if (ref ($config->{'nrpeconfig'}) eq 'ARRAY')
249 {
250 for (@{$config->{'nrpeconfig'}})
251 {
252 parse_nrpe_conf ($_);
253 }
254 }
255 elsif (ref ($config->{'nrpeconfig'}) eq '')
256 {
257 parse_nrpe_conf ($config->{'nrpeconfig'});
258 }
259 else
260 {
261 print STDERR "Cannot handle ref type '"
262 . ref ($config->{'nrpeconfig'}) . "' for option 'NRPEConfig'.\n";
263 }
264 }
266 if (defined ($config->{'addtype'}))
267 {
268 if (ref ($config->{'addtype'}) eq 'ARRAY')
269 {
270 handle_config_addtype ($config->{'addtype'});
271 }
272 elsif (ref ($config->{'addtype'}) eq '')
273 {
274 handle_config_addtype ([$config->{'addtype'}]);
275 }
276 else
277 {
278 print STDERR "Cannot handle ref type '"
279 . ref ($config->{'addtype'}) . "' for option 'AddType'.\n";
280 }
281 }
283 if (defined ($config->{'script'}))
284 {
285 if (ref ($config->{'script'}) eq 'HASH')
286 {
287 handle_config_script ($config->{'script'});
288 }
289 else
290 {
291 print STDERR "Cannot handle ref type '"
292 . ref ($config->{'script'}) . "' for option 'Script'.\n";
293 }
294 }
296 if (defined ($config->{'interval'})
297 && (ref ($config->{'interval'}) eq ''))
298 {
299 my $num = int ($config->{'interval'});
300 if ($num > 0)
301 {
302 $Interval = $num;
303 }
304 }
305 } # handle_config }}}
307 sub scale_value
308 {
309 my $value = shift;
310 my $unit = shift;
312 if (!$unit)
313 {
314 return ($value);
315 }
317 if (($unit =~ m/^mb(yte)?$/i) || ($unit eq 'M'))
318 {
319 return ($value * 1000000);
320 }
321 elsif ($unit =~ m/^k(b(yte)?)?$/i)
322 {
323 return ($value * 1000);
324 }
326 return ($value);
327 }
329 sub sanitize_instance
330 {
331 my $inst = shift;
333 if ($inst eq '/')
334 {
335 return ('root');
336 }
338 $inst =~ s/[^A-Za-z_-]/_/g;
339 $inst =~ s/__+/_/g;
340 $inst =~ s/^_//;
341 $inst =~ s/_$//;
343 return ($inst);
344 }
346 sub handle_performance_data
347 {
348 my $host = shift;
349 my $plugin = shift;
350 my $pinst = shift;
351 my $type = shift;
352 my $time = shift;
353 my $line = shift;
355 my $tinst;
356 my $value;
357 my $unit;
359 if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/)
360 {
361 $tinst = sanitize_instance ($1);
362 $value = scale_value ($2, $3);
363 }
364 else
365 {
366 return;
367 }
369 print "PUTVAL $host/$plugin-$pinst/$type-$tinst interval=$Interval ${time}:$value\n";
370 }
372 sub execute_script
373 {
374 my $fh;
375 my $pinst;
376 my $time = time ();
377 my $script = shift;
378 my @args = ();
379 my $host = hostname () || 'localhost';
381 my $state = 0;
382 my $serviceoutput;
383 my @serviceperfdata;
384 my @longserviceoutput;
386 my $script_name = $script->{'script'};
388 if ($script->{'arguments'})
389 {
390 @args = split (' ', $script->{'arguments'});
391 }
393 if (!open ($fh, '-|', $script_name, @args))
394 {
395 print STDERR "Cannot execute $script_name: $!";
396 return;
397 }
399 $pinst = sanitize_instance (basename ($script_name));
401 # Parse the output of the plugin. The format is seriously fucked up, because
402 # it got extended way beyond what it could handle.
403 while (my $line = <$fh>)
404 {
405 chomp ($line);
407 if ($state == 0)
408 {
409 my $perfdata;
410 ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
412 if ($perfdata)
413 {
414 push (@serviceperfdata, split (' ', $perfdata));
415 }
417 $state = 1;
418 }
419 elsif ($state == 1)
420 {
421 my $longoutput;
422 my $perfdata;
423 ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
425 push (@longserviceoutput, $longoutput);
427 if ($perfdata)
428 {
429 push (@serviceperfdata, split (' ', $perfdata));
430 $state = 2;
431 }
432 }
433 else # ($state == 2)
434 {
435 push (@serviceperfdata, split (' ', $line));
436 }
437 }
439 close ($fh);
440 # Save the exit status of the check in $state
441 $state = $?;
443 if ($state == 0)
444 {
445 $state = 'okay';
446 }
447 elsif ($state == 1)
448 {
449 $state = 'warning';
450 }
451 else
452 {
453 $state = 'failure';
454 }
456 {
457 my $type = $script->{'type'} || 'nagios_check';
459 print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
460 . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
461 }
463 if ($script->{'type'})
464 {
465 for (@serviceperfdata)
466 {
467 handle_performance_data ($host, 'nagios', $pinst, $script->{'type'},
468 $time, $_);
469 }
470 }
471 } # execute_script
473 sub main
474 {
475 my $last_run;
476 my $next_run;
478 my %config = ParseConfig (-ConfigFile => $ConfigFile,
479 -AutoTrue => 1,
480 -LowerCaseNames => 1);
481 handle_config (\%config);
483 while (42)
484 {
485 $last_run = time ();
486 $next_run = $last_run + $Interval;
488 for (@$Scripts)
489 {
490 execute_script ($_);
491 }
493 while ((my $timeleft = ($next_run - time ())) > 0)
494 {
495 sleep ($timeleft);
496 }
497 }
498 } # main
500 =head1 REQUIREMENTS
502 This script requires the following Perl modules to be installed:
504 =over 4
506 =item C<Config::General>
508 =item C<Regexp::Common>
510 =back
512 =head1 SEE ALSO
514 L<http://www.nagios.org/>,
515 L<http://nagiosplugins.org/>,
516 L<http://collectd.org/>,
517 L<collectd-exec(5)>
519 =head1 AUTHOR
521 Florian octo Forster E<lt>octo at verplant.orgE<gt>
523 =cut
525 # vim: set sw=2 sts=2 ts=8 fdm=marker :