#!/usr/bin/perl
#
# $Id: pasvnag_lite 139 2013-10-18 09:32:15Z phil $
#
# pasvnag_lite: perform nagios checks and submit results via nrpe
# author, (c): Philippe Kueck <projects at unixadm dot org>
#
# requires: send_nsca
#

use strict;
use warnings;
use threads;
use IO::Handle;

my ($hosts, $services, $commands, $nagios, $debug) = (undef, undef, undef, {'sync' => 0}, 0);
&bailout("pipe error: $!") unless pipe nsca_read, nsca_write;
nsca_write->autoflush;

sub bailout {
	# syslog CRIT?
	printf STDERR "*** ERROR *** %s\n", $_[0];
	exit
}

sub compileCmd {
	my $ref = shift;
	my @args = split "!", $$ref;
	$$ref = $commands->{shift @args};
	bailout "command not defined" unless $$ref;
	my $cmd = sprintf "%s/%s", $nagios->{'plugin_dir'}, ($$ref =~ /^(\S+)/)[0];
	bailout "command $cmd not found" unless -x $cmd;
	while (@args) {$$ref =~ s/\$ARG\d+\$/shift @args/e or last}
	$$ref =~ s/ \$ARG\d+\$//g
}

sub parseCfg {
	my ($line, $blk, $hostgroups);
	open CFG, "< $_[0]" or die $@;
	while (<CFG>) {
		$line++;
		chomp; s/(^\s+|\s+$)//g; next if /^(?:#|$)/;
		unless (defined $blk) {
			($blk->{'def'}) = $_ =~ /^define\s+(nagios|host|service|command)\s*{/;
			next
		}
		if (/^}/) {
			if ($blk->{'def'} eq 'nagios') {
				# nothing to do here
			} elsif ($blk->{'def'} eq 'host') {
				bailout "$line: host definition: host_name is missing"
					unless $blk->{'host_name'};
				bailout "$line: duplicate host definition for $blk->{'host_name'}"
					if exists $hosts->{$blk->{'host_name'}};
				delete $blk->{'def'};
				@{$hosts->{$blk->{'host_name'}}}{keys %$blk} = values %$blk;
				delete $hosts->{$blk->{'host_name'}}->{'host_name'}
			} elsif ($blk->{'def'} eq 'service') {
				bailout "$line: service definition: service_description is missing"
					unless $blk->{'service_description'};
				bailout "$line: check_command is missing for $blk->{'service_description'}"
					unless $blk->{'check_command'};
				bailout "$line: duplicate service definition for $blk->{'service_description'}"
					if exists $services->{$blk->{'service_description'}};
				delete $blk->{'def'};
				$services->{$blk->{'service_description'}} = $blk->{'check_command'}
			} elsif ($blk->{'def'} eq 'command') {
				bailout "$line: command definition: command_name is missing"
					unless $blk->{'command_name'};
				bailout "$line: command_line is missing for $blk->{'command_name'}"
					unless $blk->{'command_line'};
				bailout "$line: duplicate command definition for $blk->{'command_name'}"
					if exists $commands->{$blk->{'command_name'}};
				delete $blk->{'def'};
				$commands->{$blk->{'command_name'}} = $blk->{'command_line'}
			}
			undef $blk; next
		}
		if ($blk->{'def'} eq "nagios") {
			if (/^(nsca_config_file|send_nsca|nsca_host|plugin_dir|sync)\s+(.+)/) {
				$nagios->{$1} = $2
			}
			next
		}
		if ($blk->{'def'} eq "host") {
			if (/^(host_name|hostgroups|address|check_command|dummy)\s+(.+)$/) {$blk->{$1} = $2}
			elsif (/^(parents)\s+(.+)$/) {@{$blk->{$1}} = split ",", $2}
			next
		}
		if ($blk->{'def'} eq "service") {
			if (/^(service_description|check_command)\s+(.+)$/) {
				$blk->{$1} = $2
			} elsif (/^hostgroup_name\s+(.+)$/) {
				foreach my $g (split ",", $1) {
					push @{$hostgroups->{$g}->{'services'}},
						$blk->{'service_description'}
				}
			} elsif (/^host_name\s+(.+)$/) {
				foreach my $h (split ",", $1) {
					push @{$hosts->{$h}->{'services'}},
						$blk->{'service_description'}
				}
			}
			next
		}
		if ($blk->{'def'} eq "command") {
			if (/^(command_(?:name|line))\s+(.+)$/) {$blk->{$1} = $2}
			next
		}
	}
	close CFG;

	# sanity check: nagios config
	bailout "nsca host not defined" unless $nagios->{'nsca_host'};
	bailout "send_nsca config file not defined" unless $nagios->{'nsca_config_file'};
	bailout "cannot open $nagios->{'nsca_config_file'}: $!" unless -r $nagios->{'nsca_config_file'};
	bailout "send_nsca binary not defined" unless $nagios->{'send_nsca'};
	bailout "cannot find $nagios->{'send_nsca'}: $!" unless -x $nagios->{'send_nsca'};
	bailout "plugin_dir not defined" unless $nagios->{'plugin_dir'};
	bailout "cannot find $nagios->{'plugin_dir'}: $!" unless -d $nagios->{'plugin_dir'};
	# sanity check: hosts
	my $need_ping = 0;
	foreach my $h (keys %$hosts) {
		bailout "host $h address missing" unless $hosts->{$h}->{'address'};
		# remove duplicate checks
		@{$hosts->{$h}->{'services'}} =
			keys %{{ map { $_ => 1 } @{$hosts->{$h}->{'services'}} }};
		# verify parents existence
		if (exists $hosts->{$h}->{'parents'}) {
			foreach my $p (@{$hosts->{$h}->{'parents'}}) {
				bailout "parent $p for host $h not defined"
					unless exists $hosts->{$p}
			}
		}
		# fill hostgroups
		if (exists $hosts->{$h}->{'hostgroups'}) {
			foreach my $g (split ",", $hosts->{$h}->{'hostgroups'}) {
				push @{$hostgroups->{$g}->{'members'}}, $h
			}
			delete $hosts->{$h}->{'hostgroups'};
		}
		# check_command
		next if $hosts->{$h}->{'dummy'};
		unless (exists $hosts->{$h}->{'check_command'}) {
			$need_ping++;
			$hosts->{$h}->{'check_command'} =
				'check_ping -H $HOSTADDRESS$ -w 3000.0,80% -c 5000.0,100% -p 5';
			next
		}
		compileCmd \$hosts->{$h}->{'check_command'}
	}
	if ($need_ping) {
		bailout "command $nagios->{'plugin_dir'}/check_ping not found"
			unless -x $nagios->{'plugin_dir'}."/check_ping"
	}
	# resolve hostgroups
	foreach my $g (keys %$hostgroups) {
		next unless exists $hostgroups->{$g}->{'members'};
		next unless exists $hostgroups->{$g}->{'services'};
		foreach my $h (@{$hostgroups->{$g}->{'members'}}) {
			push @{$hosts->{$h}->{'services'}},
				@{$hostgroups->{$g}->{'services'}}
		}
	}
	# sanity check & compile service check commands
	foreach my $s (keys %$services) {
		compileCmd \$services->{$s}
	}
}

sub send_nsca_listener {
	$|++;
	my @queue;
	while (<nsca_read>) {
		chomp;
		last if $_ eq 'EOF';
		if ($nagios->{'sync'}) {
			if ($debug) {printf "send_nsca [%s]\n", $_; next}
			open NSCA, "| $nagios->{'send_nsca'} -H $nagios->{'nsca_host'} -c $nagios->{'nsca_config_file'} >& /dev/null"
				or bailout "send_nsca error: $@";
			printf NSCA "%s\n", $_;
			close NSCA;
			next
		}
		push @queue, $_
	}
	return if $nagios->{'sync'};
	if ($debug) {
		printf "send_nsca:\n\t%s\n", join "\n\t", @queue;
		return
	}
	open NSCA, "| $nagios->{'send_nsca'} -H $nagios->{'nsca_host'} -c $nagios->{'nsca_config_file'} >& /dev/null"
		or bailout "send_nsca error: $@";
	printf NSCA "%s\n", $_ foreach @queue;
	close NSCA
}

sub check_service {
	my $check_command;
	if ($services->{$_[1]} =~ /^\//) {$check_command = $services->{$_[1]}}
	else {$check_command = $nagios->{'plugin_dir'}."/".$services->{$_[1]}}
	$check_command =~ s/\$HOSTADDRESS\$/$hosts->{$_[0]}->{'address'}/g;

	my $output = `$check_command 2>&1`; chomp $output;
	printf nsca_write "%s\t%s\t%d\t%s\n", $_[0], $_[1], $? >> 8, $output
}

sub check_host {
	return if exists $hosts->{$_[0]}->{'reachable'};
	if (exists $hosts->{$_[0]}->{'parents'}) {
		my $parent_reachable = 0;
		foreach my $p (@{$hosts->{$_[0]}->{'parents'}}) {
			&check_host($p) unless exists $hosts->{$p}->{'reachable'};
			bailout "check_host dependency resolver: something went wrong"
				unless exists $hosts->{$p}->{'reachable'};
			$parent_reachable |= $hosts->{$p}->{'reachable'}
		}
		unless ($parent_reachable) {
			printf nsca_write "%s\t2\tCRITICAL - Host Unreachable (%s)\n", $_[0], $_[0];
			$hosts->{$_[0]}->{'reachable'} = 0;
			return
		}
		
	}

	unless ($hosts->{$_[0]}->{'dummy'}) {
		my $check_command;
		if ($hosts->{$_[0]}->{'check_command'} =~ /^\//) {
			$check_command = $hosts->{$_[0]}->{'check_command'}
		} else {
			$check_command = $nagios->{'plugin_dir'}."/".
				$hosts->{$_[0]}->{'check_command'}
		}
		$check_command =~ s/\$HOSTADDRESS\$/$hosts->{$_[0]}->{'address'}/g;

		my $output = `$check_command`; chomp $output;
		my $ret = $? >> 8;
		printf nsca_write "%s\t%d\t\%s\n", $_[0], $ret, $output;
		$hosts->{$_[0]}->{'reachable'} = ($ret < 2);
		return unless $hosts->{$_[0]}->{'reachable'}
	} else {
		$hosts->{$_[0]}->{'reachable'} = 1
	}

	foreach my $s (@{$hosts->{$_[0]}->{'services'}}) {
		new threads(sub {check_service(@_)}, $_[0], $s);
	}
}


bailout "usage: $0 </path/to/config/file>" unless $ARGV[0];
foreach (@ARGV) {
	if ($_ eq '-d') {$debug++; next}
	bailout "cannot read config file $_: $!" unless -r $_;
	parseCfg $_; last
}

my $controller = new threads(sub{send_nsca_listener});
foreach my $h (keys %$hosts) {&check_host($h)}
foreach (threads->list) {
	next if $_->tid eq $controller->tid;
	$_->join
}
print nsca_write "EOF\n"; close nsca_write;
$controller->join;

__END__

=head1 NAME

pasvnag_lite - a (passive) nagios checker

=head1 VERSION

$Revision: 139 $

=head1 SYNOPSIS

pasvnag_lite [options] <configfile>

=head1 OPTIONS

=over 8

=item B<-d>

debug mode, does not send anything to nrpe, just print what would be sent

=back

=head1 DESCRIPTION

This script runs nagios (or icinga, ..) checks on defined objects and submits the results to a specific NSCA host.

=head1 DEPENDENCIES

you'll need send_nrpe.

=head1 INSTALLATION

Move the script anywhere you like and create a (mostly nagios compatible) config file.

=head1 CONFIGURATION

pasvnag_lite is trying to be compatible with nagios, so the configuration file is quite similar.

=head2 NSCA DEFINITION

 define nagios {
     send_nsca /path/to/send_nsca
     nsca_config_file /path/to/send_nsca.cfg
     sync 0
     nsca_host host_where_nsca_is_running
     plugin_dir /path/to/nagios/plugins
 }

=over 8

=item C<send_nsca>

Where to find the send_nsca binary.

=item C<nsca_config_file>

Where to find the send_nsca config file.

=item C<sync>

Whether or not send_nsca should be invoked every time a check result is available (C<sync 1>) or once every tcheck has finished (C<sync 0>, default).

=item C<nsca_host>

The NSCA host the check results are sent to.

=item C<plugin_dir>

The nagios/icinga/.. plugin dir (e.g. libdir/nagios/plugins).

=back

=head2 COMMAND DEFINITION

 define command {
     command_name check_command_name
     command_line check_command_line -H $HOSTADDRESS$ $ARG1$ .. $ARGn$
 }

=over 8

=item C<command_name>

The check command name.

=item C<command_line>

The check command line. $HOSTADDRESS$ is substituted with the host's C<address> (see below), $ARG1$ to $ARGn$ with the given arguments.

=back

=head2 HOST DEFINITION

 define host {
     host_name a.host.name
     address fqdn_or_ip_address
     hostgroups optional_hostgroup
     check_command host-alive-check-command
     dummy
 }

=over 8

=item C<host_name>

The host name.

=item C<check_command>

The host alive check command (defaults to check_ping).

=item C<dummy>

Assume the host to be alive, don't perform host alive checks and don't send status or perfdata to NSCA.

=item C<address>

The hosts's address (FQDN or IP).

=item C<hostgroups>

An optional comma separated list of groups the host is member of.

=back

=head2 SERVICE DEFINITION

 define service {
     service_description service_check_name
     check_command check_command_name!ARG1!..!ARGn
     host_name a.host.name
     hostgroup_name a.hostgroup.name
 }

=over 8

=item C<service_description>

The service check name / description.

=item C<check_command>

The check command (see L</COMMAND DEFINITION>). Arguments to the command are separated by exclamation marks.

=item C<host_name>

A comma separated list of hosts (see L</HOST DEFINITION>) the service check should be run against.

=item C<hostgroup_name>

A comma separated list of hostgroups (see above) the service check should be run against.

=back

Either host_name or hostgroup_name is mandatory, obviously.

=head1 AUTHOR

Philippe Kueck <projects at unixadm dot org>

=cut
