#!/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

=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
    {
      if (ref ($opts) eq 'ARRAY')
        {
          for (@$opts)
            {
              my $opt = $_;
              $opt->{'script'} = $script;
              push (@$Scripts, $opt);
            }
        }
          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 :
