contrib/exec-nagios.px: Added a Perl script which handles Nagios plugins.
authorFlorian Forster <octo@leeloo.lan.home.verplant.org>
Sun, 10 Feb 2008 15:40:58 +0000 (16:40 +0100)
committerFlorian Forster <octo@leeloo.lan.home.verplant.org>
Sun, 10 Feb 2008 15:40:58 +0000 (16:40 +0100)
It handles most of the Nagios plugin API, including multi-line output (a
Nagios 3 feature) and performance data. It's basic on purpose so that easy
stuff is possible but complex stuff is about as hard as rewriting the plugin ;)

contrib/exec-nagios.conf [new file with mode: 0644]
contrib/exec-nagios.px [new file with mode: 0755]

diff --git a/contrib/exec-nagios.conf b/contrib/exec-nagios.conf
new file mode 100644 (file)
index 0000000..26dd621
--- /dev/null
@@ -0,0 +1,13 @@
+# Run `perldoc exec-nagios.px' for details on this config file.
+
+Interval 300
+
+<Script /usr/lib/nagios/check_tcp>
+       Arguments -H alice -p 22
+       Type delay
+</Script>
+
+<Script /usr/lib/nagios/check_dns>
+       Arguments -H alice
+       Type delay
+</Script>
diff --git a/contrib/exec-nagios.px b/contrib/exec-nagios.px
new file mode 100755 (executable)
index 0000000..3a84724
--- /dev/null
@@ -0,0 +1,395 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+exec-nagios.px
+
+=head1 DESCRIPTION
+
+This script allows you to use plugins that were written for Nagios with
+collectd's C<exec-plugin>. If the plugin checks some kind of threshold, please
+consider configuring the threshold using collectd's own facilities instead of
+using this transition layer.
+
+=cut
+
+use Sys::Hostname ('hostname');
+use File::Basename ('basename');
+use Config::General ('ParseConfig');
+use Regexp::Common ('number');
+
+our $ConfigFile = '/etc/exec-nagios.conf';
+our $TypeMap = {};
+our $Scripts = [];
+our $Interval = 300;
+
+main ();
+exit (0);
+
+# Configuration
+# {{{
+
+=head1 CONFIGURATION
+
+This script reads it's configuration from F</etc/exec-nagios.conf>. The
+configuration is read using C<Config::General> which understands a Apache-like
+config syntax, so it's very similar to the F<collectd.conf> syntax, too.
+
+Here's a short sample config:
+
+  Interval 300
+  <Script /usr/lib/nagios/check_tcp>
+    Arguments -H alice -p 22
+    Type delay
+  </Script>
+  <Script /usr/lib/nagios/check_dns>
+    Arguments -H alice
+    Type delay
+  </Script>
+
+The options have the following semantic (i.E<nbsp>e. meaning):
+
+=over 4
+
+=item B<Interval> I<Seconds>
+
+Sets the interval in which the plugins are executed. This doesn't need to match
+the interval setting of the collectd daemon. Usually, you want to execute the
+Nagios plugins much less often, e.E<nbsp>g. every 300 seconds versus every 10
+seconds.
+
+=item E<lt>B<Script> I<File>E<gt>
+
+Adds a script to the list of scripts to be executed once per I<Interval>
+seconds. You can use the following optional arguments to specify the operation
+further:
+
+=over 4
+
+=item B<Arguments> I<Arguments>
+
+Pass the arguments I<Arguments> to the script. This is often needed with Nagios
+plugins, because much of the logic is implemented in the plugins, not in the
+daemon. If you need to specify a warning and/or critical range here, please
+consider using collectd's own threshold mechanism, which is by far the more
+elegant solution than this transition layer.
+
+=item B<Type> I<Type>
+
+If the plugin provides "performance data" the performance data is dispatched to
+collectd with this type. If no type is configured the data is ignored. Please
+note that this is limited to types that take exactly one value, such as the
+type C<delay> in the example above. If you need more complex performance data,
+rewrite the plugin as a collectd plugin (or at least port it do run directly
+with the C<exec-plugin>).
+
+=back
+
+=cut
+
+sub handle_config_addtype
+{
+  my $list = shift;
+
+  for (my $i = 0; $i < @$list; $i++)
+  {
+    my ($to, @from) = split (' ', $list->[$i]);
+    for (my $j = 0; $j < @from; $j++)
+    {
+      $TypeMap->{$from[$j]} = $to;
+    }
+  }
+} # handle_config_addtype
+
+sub handle_config_script
+{
+  my $scripts = shift;
+
+  for (keys %$scripts)
+  {
+    my $script = $_;
+    my $opts = $scripts->{$script};
+
+    if (!-e $script)
+    {
+      print STDERR "Script `$script' doesn't exist.\n";
+    }
+    elsif (!-x $script)
+    {
+      print STDERR "Script `$script' exists but is not executable.\n";
+    }
+    else
+    {
+      $opts->{'script'} = $script;
+      push (@$Scripts, $opts);
+    }
+  } # for (keys %$scripts)
+} # handle_config_script
+
+sub handle_config
+{
+  my $config = shift;
+
+  if (defined ($config->{'addtype'}))
+  {
+    if (ref ($config->{'addtype'}) eq 'ARRAY')
+    {
+      handle_config_addtype ($config->{'addtype'});
+    }
+    elsif (ref ($config->{'addtype'}) eq '')
+    {
+      handle_config_addtype ([$config->{'addtype'}]);
+    }
+    else
+    {
+      print STDERR "Cannot handle ref type '"
+      . ref ($config->{'addtype'}) . "' for option 'AddType'.\n";
+    }
+  }
+
+  if (defined ($config->{'script'}))
+  {
+    if (ref ($config->{'script'}) eq 'HASH')
+    {
+      handle_config_script ($config->{'script'});
+    }
+    else
+    {
+      print STDERR "Cannot handle ref type '"
+      . ref ($config->{'script'}) . "' for option 'Script'.\n";
+    }
+  }
+
+  if (defined ($config->{'interval'})
+    && (ref ($config->{'interval'}) eq ''))
+  {
+    my $num = int ($config->{'interval'});
+    if ($num > 0)
+    {
+      $Interval = $num;
+    }
+  }
+} # handle_config }}}
+
+sub scale_value
+{
+  my $value = shift;
+  my $unit = shift;
+
+  if (!$unit)
+  {
+    return ($value);
+  }
+
+  if (($unit =~ m/^mb(yte)?$/i) || ($unit eq 'M'))
+  {
+    return ($value * 1000000);
+  }
+  elsif ($unit =~ m/^k(b(yte)?)?$/i)
+  {
+    return ($value * 1000);
+  }
+
+  return ($value);
+}
+
+sub sanitize_instance
+{
+  my $inst = shift;
+
+  if ($inst eq '/')
+  {
+    return ('root');
+  }
+
+  $inst =~ s/[^A-Za-z_-]/_/g;
+  $inst =~ s/__+/_/g;
+  $inst =~ s/^_//;
+  $inst =~ s/_$//;
+
+  return ($inst);
+}
+
+sub handle_performance_data
+{
+  my $host = shift;
+  my $plugin = shift;
+  my $pinst = shift;
+  my $type = shift;
+  my $time = shift;
+  my $line = shift;
+
+  my $tinst;
+  my $value;
+  my $unit;
+
+  if ($line =~ m/^([^=]+)=($RE{num}{real})([^;]*)/)
+  {
+    $tinst = sanitize_instance ($1);
+    $value = scale_value ($2, $3);
+  }
+  else
+  {
+    return;
+  }
+
+  print "PUTVAL $host/$plugin-$pinst/$type-$tinst interval=$Interval ${time}:$value\n";
+}
+
+sub execute_script
+{
+  my $fh;
+  my $pinst;
+  my $time = time ();
+  my $script = shift;
+  my @args = ();
+  my $host = hostname () || 'localhost';
+
+  my $state = 0;
+  my $serviceoutput;
+  my @serviceperfdata;
+  my @longserviceoutput;
+
+  my $script_name = $script->{'script'};
+  
+  if ($script->{'arguments'})
+  {
+    @args = split (' ', $script->{'arguments'});
+  }
+
+  if (!open ($fh, '-|', $script_name, @args))
+  {
+    print STDERR "Cannot execute $script_name: $!";
+    return;
+  }
+
+  $pinst = sanitize_instance (basename ($script_name));
+
+  # Parse the output of the plugin. The format is seriously fucked up, because
+  # it got extended way beyond what it could handle.
+  while (my $line = <$fh>)
+  {
+    chomp ($line);
+
+    if ($state == 0)
+    {
+      my $perfdata;
+      ($serviceoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
+      
+      if ($perfdata)
+      {
+       push (@serviceperfdata, split (' ', $perfdata));
+      }
+
+      $state = 1;
+    }
+    elsif ($state == 1)
+    {
+      my $longoutput;
+      my $perfdata;
+      ($longoutput, $perfdata) = split (m/\s*\|\s*/, $line, 2);
+
+      push (@longserviceoutput, $longoutput);
+
+      if ($perfdata)
+      {
+       push (@serviceperfdata, split (' ', $perfdata));
+       $state = 2;
+      }
+    }
+    else # ($state == 2)
+    {
+      push (@serviceperfdata, split (' ', $line));
+    }
+  }
+
+  close ($fh);
+  # Save the exit status of the check in $state
+  $state = $?;
+
+  if ($state == 0)
+  {
+    $state = 'okay';
+  }
+  elsif ($state == 1)
+  {
+    $state = 'warning';
+  }
+  else
+  {
+    $state = 'failure';
+  }
+
+  {
+    my $type = $script->{'type'} || 'nagios_check';
+
+    print "PUTNOTIF time=$time severity=$state host=$host plugin=nagios "
+    . "plugin_instance=$pinst type=$type message=$serviceoutput\n";
+  }
+
+  if ($script->{'type'})
+  {
+    for (@serviceperfdata)
+    {
+      handle_performance_data ($host, 'nagios', $pinst, $script->{'type'},
+       $time, $_);
+    }
+  }
+} # execute_script
+
+sub main
+{
+  my $last_run;
+  my $next_run;
+
+  my %config = ParseConfig (-ConfigFile => $ConfigFile,
+    -AutoTrue => 1,
+    -LowerCaseNames => 1);
+  handle_config (\%config);
+
+  while (42)
+  {
+    $last_run = time ();
+    $next_run = $last_run + $Interval;
+
+    for (@$Scripts)
+    {
+      execute_script ($_);
+    }
+
+    while ((my $timeleft = ($next_run - time ())) > 0)
+    {
+      sleep ($timeleft);
+    }
+  }
+} # main
+
+=head1 REQUIREMENTS
+
+This script requires the following Perl modules to be installed:
+
+=over 4
+
+=item C<Config::General>
+
+=item C<Regexp::Common>
+
+=back
+
+=head1 SEE ALSO
+
+L<http://www.nagios.org/>,
+L<http://nagiosplugins.org/>,
+L<http://collectd.org/>,
+L<collectd-exec(5)>
+
+=head1 AUTHOR
+
+Florian octo Forster E<lt>octo at verplant.orgE<gt>
+
+=cut
+
+# vim: set sw=2 sts=2 ts=8 fdm=marker :