1#!/usr/bin/perl
2
3use strict;
4use warnings;
5
6=head1 NAME
7
8exec-nagios.px
9
10=head1 DESCRIPTION
11
12This script allows you to use plugins that were written for Nagios with
13collectd's C<exec-plugin>. If the plugin checks some kind of threshold, please
14consider configuring the threshold using collectd's own facilities instead of
15using this transition layer.
16
17=cut
18
19use Sys::Hostname ('hostname');
20use File::Basename ('basename');
21use Config::General ('ParseConfig');
22use Regexp::Common ('number');
23
24our $ConfigFile = '/etc/exec-nagios.conf';
25our $TypeMap = {};
26our $NRPEMap = {};
27our $Scripts = [];
28our $Interval = defined ($ENV{'COLLECTD_INTERVAL'}) ? (0 + $ENV{'COLLECTD_INTERVAL'}) : 300;
29our $Hostname = defined ($ENV{'COLLECTD_HOSTNAME'}) ? $ENV{'COLLECTD_HOSTNAME'} : '';
30
31main ();
32exit (0);
33
34# Configuration
35# {{{
36
37=head1 CONFIGURATION
38
39This script reads its configuration from F</etc/exec-nagios.conf>. The
40configuration is read using C<Config::General> which understands a Apache-like
41config syntax, so it's very similar to the F<collectd.conf> syntax, too.
42
43Here's a short sample config:
44
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>
55
56The options have the following semantic (i.E<nbsp>e. meaning):
57
58=over 4
59
60=item B<NRPEConfig> I<File>
61
62Read the NRPE config and add the command definitions to an alias table. After
63reading the file you can use the NRPE command name rather than the script's
64filename within B<Script> blocks (see below). If both, the NRPE config and the
65B<Script> block, define arguments they will be merged by concatenating the
66arguments together in the order "NRPE-args Script-args".
67
68Please note that this option is rather dumb. It does not support "command
69argument processing" (i.e. replacing C<$ARG1$> and friends), inclusion of other
70NRPE config files, include directories etc.
71
72=item B<Interval> I<Seconds>
73
74Sets the interval in which the plugins are executed. This doesn't need to match
75the interval setting of the collectd daemon. Usually, you want to execute the
76Nagios plugins much less often, e.E<nbsp>g. every 300 seconds versus every 10
77seconds.
78
79=item E<lt>B<Script> I<File>E<gt>
80
81Adds a script to the list of scripts to be executed once per I<Interval>
82seconds. If the B<NRPEConfig> is given above the B<Script> block, you may use
83the NRPE command name rather than the script's filename. You can use the
84following optional arguments to specify the operation further:
85
86=over 4
87
88=item B<Arguments> I<Arguments>
89
90Pass the arguments I<Arguments> to the script. This is often needed with Nagios
91plugins, because much of the logic is implemented in the plugins, not in the
92daemon. If you need to specify a warning and/or critical range here, please
93consider using collectd's own threshold mechanism, which is by far the more
94elegant solution than this transition layer.
95
96=item B<Type> I<Type>
97
98If the plugin provides "performance data" the performance data is dispatched to
99collectd with this type. If no type is configured the data is ignored. Please
100note that this is limited to types that take exactly one value, such as the
101type C<delay> in the example above. If you need more complex performance data,
102rewrite the plugin as a collectd plugin (or at least port it do run directly
103with the C<exec-plugin>).
104
105=back
106
107=back
108
109=cut
110
111sub parse_nrpe_conf
112{
113  my $file = shift;
114  my $fh;
115  my $status;
116
117  $status = open ($fh, '<', $file);
118  if (!$status)
119  {
120    print STDERR "Reading NRPE config from \"$file\" failed: $!\n";
121    return;
122  }
123
124  while (<$fh>)
125  {
126    my $line = $_;
127    chomp ($line);
128
129    if ($line =~ m/^\s*command\[([^\]]+)\]\s*=\s*(.+)$/)
130    {
131      my $alias = $1;
132      my $script;
133      my $arguments;
134
135      ($script, $arguments) = split (' ', $2, 2);
136
137      if ($NRPEMap->{$alias})
138      {
139        print STDERR "Warning: NRPE command \"$alias\" redefined.\n";
140      }
141
142      $NRPEMap->{$alias} = { script => $script };
143      if ($arguments)
144      {
145        $NRPEMap->{$alias}{'arguments'} = $arguments;
146      }
147    }
148  } # while (<$fh>)
149
150  close ($fh);
151} # parse_nrpe_conf
152
153sub handle_config_addtype
154{
155  my $list = shift;
156
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
166
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.
170sub update_script_opts
171{
172  my $opts = shift;
173  my $script = shift;
174  my $nrpe_args = shift;
175
176  $opts->{'script'} = $script;
177
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
190
191sub handle_config_script
192{
193  my $scripts = shift;
194
195  for (keys %$scripts)
196  {
197    my $script = $_;
198    my $opts = $scripts->{$script};
199
200    my $nrpe_args = '';
201
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    }
212
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
242
243sub handle_config
244{
245  my $config = shift;
246
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  }
266
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  }
283
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  }
296
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 }}}
307
308sub scale_value
309{
310  my $value = shift;
311  my $unit = shift;
312
313  if (!$unit)
314  {
315    return ($value);
316  }
317
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  }
326
327  return ($value);
328}
329
330sub sanitize_instance
331{
332  my $inst = shift;
333
334  if ($inst eq '/')
335  {
336    return ('root');
337  }
338
339  $inst =~ s/[^A-Za-z_-]/_/g;
340  $inst =~ s/__+/_/g;
341  $inst =~ s/^_//;
342  $inst =~ s/_$//;
343
344  return ($inst);
345}
346
347sub 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";
356
357  my $tinst;
358  my $value;
359  my $unit;
360
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  }
370
371  $ident =~ s/"/\\"/g;
372
373  print qq(PUTVAL "$ident" interval=$Interval ${time}:$value\n);
374}
375
376sub 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';
384
385  my $state = 0;
386  my $serviceoutput;
387  my @serviceperfdata;
388  my @longserviceoutput;
389
390  my $script_name = $script->{'script'};
391
392  if ($script->{'arguments'})
393  {
394    @args = split (' ', $script->{'arguments'});
395  }
396
397  if (!open ($fh, '-|', $script_name, @args))
398  {
399    print STDERR "Cannot execute $script_name: $!";
400    return;
401  }
402
403  $pinst = sanitize_instance (basename ($script_name));
404
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);
410
411    if ($state == 0)
412    {
413      my $perfdata;
414      ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
415
416      if ($perfdata)
417      {
418        push (@serviceperfdata, split (' ', $perfdata));
419      }
420
421      $state = 1;
422    }
423    elsif ($state == 1)
424    {
425      my $longoutput;
426      my $perfdata;
427      ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
428
429      push (@longserviceoutput, $longoutput);
430
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  }
442
443  close ($fh);
444  # Save the exit status of the check in $state
445  $state = $? >> 8;
446
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  }
459
460  {
461    my $type = $script->{'type'} || 'nagios_check';
462
463    print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
464    . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
465  }
466
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
476
477sub main
478{
479  my $last_run;
480  my $next_run;
481
482  my %config = ParseConfig (-ConfigFile => $ConfigFile,
483    -AutoTrue => 1,
484    -LowerCaseNames => 1);
485  handle_config (\%config);
486
487  while (42)
488  {
489    $last_run = time ();
490    $next_run = $last_run + $Interval;
491
492    for (@$Scripts)
493    {
494      execute_script ($_);
495    }
496
497    while ((my $timeleft = ($next_run - time ())) > 0)
498    {
499      sleep ($timeleft);
500    }
501  }
502} # main
503
504=head1 REQUIREMENTS
505
506This script requires the following Perl modules to be installed:
507
508=over 4
509
510=item C<Config::General>
511
512=item C<Regexp::Common>
513
514=back
515
516=head1 SEE ALSO
517
518L<http://www.nagios.org/>,
519L<http://nagiosplugins.org/>,
520L<http://collectd.org/>,
521L<collectd-exec(5)>
522
523=head1 AUTHOR
524
525Florian octo Forster E<lt>octo at verplant.orgE<gt>
526
527=cut
528
529# vim: set sw=2 sts=2 ts=8 fdm=marker :
530