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 = defined ($ENV{'COLLECTD_INTERVAL'}) ? (0 + $ENV{'COLLECTD_INTERVAL'}) : 300;
29 our $Hostname = defined ($ENV{'COLLECTD_HOSTNAME'}) ? $ENV{'COLLECTD_HOSTNAME'} : '';
31 main ();
32 exit (0);
34 # Configuration
35 # {{{
37 =head1 CONFIGURATION
39 This script reads it's configuration from F</etc/exec-nagios.conf>. The
40 configuration is read using C<Config::General> which understands a Apache-like
41 config syntax, so it's very similar to the F<collectd.conf> syntax, too.
43 Here's a short sample config:
45 NRPEConfig "/etc/nrpe.cfg"
46 Interval 300
47 <Script /usr/lib/nagios/check_tcp>
48 Arguments -H alice -p 22
49 Type delay
50 </Script>
51 <Script /usr/lib/nagios/check_dns>
52 Arguments -H alice
53 Type delay
54 </Script>
56 The options have the following semantic (i.E<nbsp>e. meaning):
58 =over 4
60 =item B<NRPEConfig> I<File>
62 Read the NRPE config and add the command definitions to an alias table. After
63 reading the file you can use the NRPE command name rather than the script's
64 filename within B<Script> blocks (see below). If both, the NRPE config and the
65 B<Script> block, define arguments they will be merged by concatenating the
66 arguments together in the order "NRPE-args Script-args".
68 Please note that this option is rather dumb. It does not support "command
69 argument processing" (i.e. replacing C<$ARG1$> and friends), inclusion of other
70 NRPE config files, include directories etc.
72 =item B<Interval> I<Seconds>
74 Sets the interval in which the plugins are executed. This doesn't need to match
75 the interval setting of the collectd daemon. Usually, you want to execute the
76 Nagios plugins much less often, e.E<nbsp>g. every 300 seconds versus every 10
77 seconds.
79 =item E<lt>B<Script> I<File>E<gt>
81 Adds a script to the list of scripts to be executed once per I<Interval>
82 seconds. If the B<NRPEConfig> is given above the B<Script> block, you may use
83 the NRPE command name rather than the script's filename. You can use the
84 following optional arguments to specify the operation further:
86 =over 4
88 =item B<Arguments> I<Arguments>
90 Pass the arguments I<Arguments> to the script. This is often needed with Nagios
91 plugins, because much of the logic is implemented in the plugins, not in the
92 daemon. If you need to specify a warning and/or critical range here, please
93 consider using collectd's own threshold mechanism, which is by far the more
94 elegant solution than this transition layer.
96 =item B<Type> I<Type>
98 If the plugin provides "performance data" the performance data is dispatched to
99 collectd with this type. If no type is configured the data is ignored. Please
100 note that this is limited to types that take exactly one value, such as the
101 type C<delay> in the example above. If you need more complex performance data,
102 rewrite the plugin as a collectd plugin (or at least port it do run directly
103 with the C<exec-plugin>).
105 =back
107 =back
109 =cut
111 sub parse_nrpe_conf
112 {
113 my $file = shift;
114 my $fh;
115 my $status;
117 $status = open ($fh, '<', $file);
118 if (!$status)
119 {
120 print STDERR "Reading NRPE config from \"$file\" failed: $!\n";
121 return;
122 }
124 while (<$fh>)
125 {
126 my $line = $_;
127 chomp ($line);
129 if ($line =~ m/^\s*command\[([^\]]+)\]\s*=\s*(.+)$/)
130 {
131 my $alias = $1;
132 my $script;
133 my $arguments;
135 ($script, $arguments) = split (' ', $2, 2);
137 if ($NRPEMap->{$alias})
138 {
139 print STDERR "Warning: NRPE command \"$alias\" redefined.\n";
140 }
142 $NRPEMap->{$alias} = { script => $script };
143 if ($arguments)
144 {
145 $NRPEMap->{$alias}{'arguments'} = $arguments;
146 }
147 }
148 } # while (<$fh>)
150 close ($fh);
151 } # parse_nrpe_conf
153 sub handle_config_addtype
154 {
155 my $list = shift;
157 for (my $i = 0; $i < @$list; $i++)
158 {
159 my ($to, @from) = split (' ', $list->[$i]);
160 for (my $j = 0; $j < @from; $j++)
161 {
162 $TypeMap->{$from[$j]} = $to;
163 }
164 }
165 } # handle_config_addtype
167 # Update the script record. This function adds the name of the script /
168 # executable to the hash and merges the configured and NRPE arguments if
169 # required.
170 sub update_script_opts
171 {
172 my $opts = shift;
173 my $script = shift;
174 my $nrpe_args = shift;
176 $opts->{'script'} = $script;
178 if ($nrpe_args)
179 {
180 if ($opts->{'arguments'})
181 {
182 $opts->{'arguments'} = $nrpe_args . ' ' . $opts->{'arguments'};
183 }
184 else
185 {
186 $opts->{'arguments'} = $nrpe_args;
187 }
188 }
189 } # update_script_opts
191 sub handle_config_script
192 {
193 my $scripts = shift;
195 for (keys %$scripts)
196 {
197 my $script = $_;
198 my $opts = $scripts->{$script};
200 my $nrpe_args = '';
202 # Check if the script exists in the NRPE map. If so, replace the alias name
203 # with the actual script name.
204 if ($NRPEMap->{$script})
205 {
206 if ($NRPEMap->{$script}{'arguments'})
207 {
208 $nrpe_args = $NRPEMap->{$script}{'arguments'};
209 }
210 $script = $NRPEMap->{$script}{'script'};
211 }
213 # Check if the script exists and is executable.
214 if (!-e $script)
215 {
216 print STDERR "Script `$script' doesn't exist.\n";
217 }
218 elsif (!-x $script)
219 {
220 print STDERR "Script `$script' exists but is not executable.\n";
221 }
222 else
223 {
224 # Add the script to the global @$Script array.
225 if (ref ($opts) eq 'ARRAY')
226 {
227 for (@$opts)
228 {
229 my $opt = $_;
230 update_script_opts ($opt, $script, $nrpe_args);
231 push (@$Scripts, $opt);
232 }
233 }
234 else
235 {
236 update_script_opts ($opts, $script, $nrpe_args);
237 push (@$Scripts, $opts);
238 }
239 }
240 } # for (keys %$scripts)
241 } # handle_config_script
243 sub handle_config
244 {
245 my $config = shift;
247 if (defined ($config->{'nrpeconfig'}))
248 {
249 if (ref ($config->{'nrpeconfig'}) eq 'ARRAY')
250 {
251 for (@{$config->{'nrpeconfig'}})
252 {
253 parse_nrpe_conf ($_);
254 }
255 }
256 elsif (ref ($config->{'nrpeconfig'}) eq '')
257 {
258 parse_nrpe_conf ($config->{'nrpeconfig'});
259 }
260 else
261 {
262 print STDERR "Cannot handle ref type '"
263 . ref ($config->{'nrpeconfig'}) . "' for option 'NRPEConfig'.\n";
264 }
265 }
267 if (defined ($config->{'addtype'}))
268 {
269 if (ref ($config->{'addtype'}) eq 'ARRAY')
270 {
271 handle_config_addtype ($config->{'addtype'});
272 }
273 elsif (ref ($config->{'addtype'}) eq '')
274 {
275 handle_config_addtype ([$config->{'addtype'}]);
276 }
277 else
278 {
279 print STDERR "Cannot handle ref type '"
280 . ref ($config->{'addtype'}) . "' for option 'AddType'.\n";
281 }
282 }
284 if (defined ($config->{'script'}))
285 {
286 if (ref ($config->{'script'}) eq 'HASH')
287 {
288 handle_config_script ($config->{'script'});
289 }
290 else
291 {
292 print STDERR "Cannot handle ref type '"
293 . ref ($config->{'script'}) . "' for option 'Script'.\n";
294 }
295 }
297 if (defined ($config->{'interval'})
298 && (ref ($config->{'interval'}) eq ''))
299 {
300 my $num = int ($config->{'interval'});
301 if ($num > 0)
302 {
303 $Interval = $num;
304 }
305 }
306 } # handle_config }}}
308 sub scale_value
309 {
310 my $value = shift;
311 my $unit = shift;
313 if (!$unit)
314 {
315 return ($value);
316 }
318 if (($unit =~ m/^mb(yte)?$/i) || ($unit eq 'M'))
319 {
320 return ($value * 1000000);
321 }
322 elsif ($unit =~ m/^k(b(yte)?)?$/i)
323 {
324 return ($value * 1000);
325 }
327 return ($value);
328 }
330 sub sanitize_instance
331 {
332 my $inst = shift;
334 if ($inst eq '/')
335 {
336 return ('root');
337 }
339 $inst =~ s/[^A-Za-z_-]/_/g;
340 $inst =~ s/__+/_/g;
341 $inst =~ s/^_//;
342 $inst =~ s/_$//;
344 return ($inst);
345 }
347 sub handle_performance_data
348 {
349 my $host = shift;
350 my $plugin = shift;
351 my $pinst = shift;
352 my $type = shift;
353 my $time = shift;
354 my $line = shift;
355 my $ident = "$host/$plugin-$pinst/$type-$tinst";
357 my $tinst;
358 my $value;
359 my $unit;
361 if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/)
362 {
363 $tinst = sanitize_instance ($1);
364 $value = scale_value ($2, $3);
365 }
366 else
367 {
368 return;
369 }
371 $ident =~ s/"/\\"/g;
373 print qq(PUTVAL "$ident" interval=$Interval ${time}:$value\n);
374 }
376 sub execute_script
377 {
378 my $fh;
379 my $pinst;
380 my $time = time ();
381 my $script = shift;
382 my @args = ();
383 my $host = $Hostname || hostname () || 'localhost';
385 my $state = 0;
386 my $serviceoutput;
387 my @serviceperfdata;
388 my @longserviceoutput;
390 my $script_name = $script->{'script'};
392 if ($script->{'arguments'})
393 {
394 @args = split (' ', $script->{'arguments'});
395 }
397 if (!open ($fh, '-|', $script_name, @args))
398 {
399 print STDERR "Cannot execute $script_name: $!";
400 return;
401 }
403 $pinst = sanitize_instance (basename ($script_name));
405 # Parse the output of the plugin. The format is seriously fucked up, because
406 # it got extended way beyond what it could handle.
407 while (my $line = <$fh>)
408 {
409 chomp ($line);
411 if ($state == 0)
412 {
413 my $perfdata;
414 ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
416 if ($perfdata)
417 {
418 push (@serviceperfdata, split (' ', $perfdata));
419 }
421 $state = 1;
422 }
423 elsif ($state == 1)
424 {
425 my $longoutput;
426 my $perfdata;
427 ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
429 push (@longserviceoutput, $longoutput);
431 if ($perfdata)
432 {
433 push (@serviceperfdata, split (' ', $perfdata));
434 $state = 2;
435 }
436 }
437 else # ($state == 2)
438 {
439 push (@serviceperfdata, split (' ', $line));
440 }
441 }
443 close ($fh);
444 # Save the exit status of the check in $state
445 $state = $?;
447 if ($state == 0)
448 {
449 $state = 'okay';
450 }
451 elsif ($state == 1)
452 {
453 $state = 'warning';
454 }
455 else
456 {
457 $state = 'failure';
458 }
460 {
461 my $type = $script->{'type'} || 'nagios_check';
463 print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
464 . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
465 }
467 if ($script->{'type'})
468 {
469 for (@serviceperfdata)
470 {
471 handle_performance_data ($host, 'nagios', $pinst, $script->{'type'},
472 $time, $_);
473 }
474 }
475 } # execute_script
477 sub main
478 {
479 my $last_run;
480 my $next_run;
482 my %config = ParseConfig (-ConfigFile => $ConfigFile,
483 -AutoTrue => 1,
484 -LowerCaseNames => 1);
485 handle_config (\%config);
487 while (42)
488 {
489 $last_run = time ();
490 $next_run = $last_run + $Interval;
492 for (@$Scripts)
493 {
494 execute_script ($_);
495 }
497 while ((my $timeleft = ($next_run - time ())) > 0)
498 {
499 sleep ($timeleft);
500 }
501 }
502 } # main
504 =head1 REQUIREMENTS
506 This script requires the following Perl modules to be installed:
508 =over 4
510 =item C<Config::General>
512 =item C<Regexp::Common>
514 =back
516 =head1 SEE ALSO
518 L<http://www.nagios.org/>,
519 L<http://nagiosplugins.org/>,
520 L<http://collectd.org/>,
521 L<collectd-exec(5)>
523 =head1 AUTHOR
525 Florian octo Forster E<lt>octo at verplant.orgE<gt>
527 =cut
529 # vim: set sw=2 sts=2 ts=8 fdm=marker :