#!/usr/bin/perl -w
#
# Copyright (C) 2004 Jimmy Olsen
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 2 dated June,
# 1991.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
#
# $Log$
# Revision 1.8.2.1  2005/02/16 20:13:10  jimmyo
# Added man page for munin-node-configure-snmp.
#
# Revision 1.8  2004/11/16 20:00:42  jimmyo
# License cleanups.
#

use strict;
use Net::SNMP;
use Socket;
use Getopt::Long;

my $debug      = 0;
my $version    = "1.2.6";
my $config     = "/etc/munin/munin-node.conf";
my $servicedir = "/etc/munin/plugins";
my $libdir     = "/usr/local/libexec/munin/plugins";
my $bindir     = "/usr/local/sbin";

my $sysName      = "1.3.6.1.2.1.1.5.0";
my $name;

my $session;
my $error;
my $response;

my $community = "public";
my $snmpver   = "2c";
my $snmpport  = "161";

my $do_usage   = 0;
my $do_version = 0;
my $do_error   = 0;

my $newer      = undef;

my @plugins  = ();

my %plugconf = ();
my %hostconf = ();

$do_error = 1 unless GetOptions (
    "help"          => \$do_usage,
    "debug!"        => \$debug,
    "config=s"      => \$config,
    "servicedir=s"  => \$servicedir,
    "plugins=s"     => \@plugins,
    "libdir=s"      => \$libdir,
    "version!"      => \$do_version,
    "snmpversion=s" => \$snmpver,
    "community=s"   => \$community,
    "newer=s"      => \$newer
);

if (! @plugins)
{
	@plugins = &get_plugins ($libdir);
}

@plugins = split (/,/, join (',', @plugins));

print "# DEBUG: Checking plugins: ", join (',', @plugins), "\n" if $debug;

if ($do_error or $do_usage or !@ARGV)
{
	print "Usage: $0 [options] <host/cidr> [host/cidr] [...]

Options:
	--help              View this help page
	--version           Show version information
	--debug             View debug information (very verbose)
	--config <file>     Override configuration file
	                    [$config]
	--servicedir <dir>  Override plugin dir [$servicedir]
	--libdir <dir>      Override plugin lib [$libdir]


";

	exit (!$do_usage); # 1 if error, 0 if --help
}

if ($do_version)
{
	print "munin-node-configure-snmp (munin-node) version $version.\n";
	print "Written by Jimmy Olsen\n";
	print "\n";
	print "Copyright (C) 2004 Jimmy Olsen\n";
	print "This is free software released under the GNU Public License. There is NO\n";
	print "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n";
	exit 0;
}

foreach my $plugin (@plugins)
{
	&fetch_plugin_config ($plugin, \%plugconf);
}

while (my $addr = shift)
{
	my $num = 32;
	if ($addr =~ /([^\/]+)\/(\d+)/)
	{   
		$num  = $2;
		$addr = $1;
	}   
	$num = 32 - $num;
	$num = 2 ** $num;
	print "# Doing $addr / $num\n" if $debug;
	for (my $i = 0; $i < $num; $i++)
	{
		print "# Doing $addr -> $i...\n" if $debug;
		my $tmpaddr = $addr;
		if ($tmpaddr =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/)
		{
			my @tmpaddr = split (/\./, $tmpaddr);
			$tmpaddr[3] += $i;
			$tmpaddr = gethostbyaddr (inet_aton (join ('.', @tmpaddr)), AF_INET);
			$tmpaddr ||= join ('.', @tmpaddr);
		}
		print "# ($tmpaddr)\n" if $debug;
		&do_host ("$tmpaddr", $community, $snmpver, $snmpport);
	}
}


#interfaces ($name);

sub do_host
{
	my $host = shift;
	my $comm = shift;
	my $ver  = shift;
	my $port = shift;

	if ($host =~ /([^:]+):(\d+)/)
	{
		$host = $1;
		$port = $2;
	}

	($session, $error) = Net::SNMP->session(
			-hostname  => $host,
			-community => $comm,
			-port      => $port,
			-version   => $ver,
		);
	$session->translate (0);
	die $error if $error;

	if (!defined ($session))
	{
		print "# Dropping host \"$host\": $error" . "\n";
		return 0;
	}

	if (!defined ($response = $session->get_request($sysName)))
	{
		print "# Dropping host \"$host\": " . $session->error() . "\n";
		return 0;
	}
	$name = $response->{$sysName};

	foreach my $plugin (@plugins)
	{
		my $auto = snmp_autoconf_plugin ($plugin, \%plugconf, \%hostconf, $host);

		if (defined $auto)
		{
			if ($plugconf{$plugin}->{wild})
			{
				foreach my $id (@{$auto})
				{
					if (! -e "$servicedir/snmp_$host"."_$plugin"."_$id")
					{
						print "ln -s $libdir/snmp__$plugin", "_ $servicedir/snmp_$host", "_$plugin", "_$id\n";
					}
				}
			}
			else
			{
				if (! -e "$servicedir/snmp_$host"."_$plugin")
				{
					print "ln -s $libdir/snmp__$plugin", " $servicedir/snmp_$host", "_$plugin\n";
				}
			}
		}
	}
}

sub snmp_autoconf_plugin
{
	my $plugname = shift;
	my $plugconf = shift;
	my $hostconf = shift;
	my $host     = shift;

	print "# Running autoconf on $plugname for $host...\n" if $debug;

    # First round of requirements
	if (defined $plugconf->{$plugname}->{req})
	{
		print "# Checking requirements...\n" if $debug;
		foreach my $req (@{$plugconf->{$plugname}->{req}})
		{
			if ($req->[0] =~ /\.$/)
			{
				print "# Delaying testing of $req->[0], as we need the indexes first.\n" if $debug;
				next;
			}
			my $snmp_val = snmp_get_single ($session, $req->[0]);
			if (!defined $snmp_val or $snmp_val !~ /$req->[1]/)
			{
				print "# Nope. Duh.\n" if $debug;
				return undef;
			}
		}
	}

    # We need the number of "things" to autoconf

	my $num = 1;
	if (defined $plugconf->{$plugname}->{num})
	{
		$num = snmp_get_single ($session, $plugconf->{$plugname}->{num});
		return undef if !defined $num;
	}
	print "# Number of items to autoconf is $num...\n" if $debug;

    # Then the index base
	my $indexes;
	if (defined $plugconf->{$plugname}->{ind})
	{
		$indexes = snmp_get_index ($plugconf->{$plugname}->{ind}, $num);
		return undef if !defined $indexes;
	}
	else
	{
		$indexes->{0} = 1;
	}
	print "# Got indexes: ", join (',', keys (%{$indexes})), "\n" if $debug;

	return undef unless scalar keys %{$indexes};

    # Second round of requirements (now that we have the indexes)
	if (defined $plugconf->{$plugname}->{req})
	{
		print "# Checking requirements...\n" if $debug;
		foreach my $req (@{$plugconf->{$plugname}->{req}})
		{
			if ($req->[0] !~ /\.$/)
			{
				print "# Already tested of $req->[0], before we got hold of the indexes.\n" if $debug;
				next;
			}
			
			foreach my $key (keys %$indexes)
			{
				my $snmp_val = snmp_get_single ($session, $req->[0] . $key);
				if (!defined $snmp_val or $snmp_val !~ /$req->[1]/)
				{
					print "# Nope. Deleting $key from possible solutions.\n" if $debug;
					delete $indexes->{$key}; # Disable
				}
			}
		}
	}

	my @tmparr = sort keys %$indexes;
	return \@tmparr;
}

sub fetch_plugin_config
{
	my $plugname = shift;
	my $plugconf = shift;
	my $plugin   = "snmp__" . $plugname;

	if (-x "$libdir/$plugin" . "_")
	{
		$plugin .= "_";
		$plugconf->{$plugname}->{wild} = 1;
	}
	elsif (-x "$libdir/$plugin")
	{
		$plugconf->{$plugname}->{wild} = 0;
	}
	else
	{
		print "# Skipping $plugname: Couldn't find plugin \"$libdir/$plugin\".\n" if $debug;
		return 0;
	}

	print "# SNMPconfing plugin \"$plugname\" ( $libdir/$plugin )\n" if $debug;

	my $fork = open (PLUG, "-|");

	if ($fork == -1)
	{
		die "# ERROR: Unable to fork: $!";
	}
	elsif ($fork == 0) # Child
	{
		close (STDERR);
		open (STDERR, ">&STDOUT");
		exec ("$bindir/munin-run", "--config", $config, "--servicedir", $libdir, $plugin, "snmpconf");
	}
	else
	{
		while (<PLUG>)
		{
			chomp;
			s/^\s+//;
			s/\s+$//;
			my ($a, $b) = split (/\s+/, $_, 2);
			next unless defined $a;

			if ($a =~ /^require$/i and defined $b)
			{
				my ($oid, $val) = split (/\s+/, $b);
				if (! defined $val)
				{
					$val = ".*";
				}
				push (@{$plugconf->{$plugname}->{req}}, [$oid, $val]);
				print "# Registered $plugname  requirement: $oid =~ /$val/\n" if $debug;
			}
			elsif ($a =~ /^index$/i and defined $b)
			{
				$plugconf->{$plugname}->{ind} = $b;
				print "# Registered $plugname  index      : $b\n" if $debug;
			}
			elsif ($a =~ /^number$/i and defined $b)
			{
				$plugconf->{$plugname}->{num} = $b;
				print "# Registered $plugname  number     : $b\n" if $debug;
			}
			elsif ($a =~ /^env\.(\S+)$/)
			{
				$plugconf->{$plugname}->{env}->{$1} = $b;
				print "# Registered $plugname  env        : $b\n" if $debug;
			}
			else
			{
				print "# Couldn't parse line line $_\n";
			}
		}
	}
	return 0;
}

sub snmp_get_single
{
	my $session = shift;
	my $oid     = shift;

	if ((!defined ($response = $session->get_request($oid))) or
			$session->error_status)
	{
		return undef;
	}
	print "# Fetched value \"$response->{$oid}\"\n" if $debug; 
	return $response->{$oid};
}

sub snmp_get_index
{
	my $oid   = shift;
	my $num   = shift;
	my $ret   = $oid . "0";
	my $rhash = {};

	$num++; # Avaya switch b0rkenness...

	for (my $i = 0; $i < $num; $i++)
	{
		if ($i == 0)
		{
			print "# Checking for $ret\n" if $debug;
			$response = $session->get_request($ret);
		}
		if ($i or !defined $response or $session->error_status)
		{
			print "# Checking for sibling of $ret\n" if $debug;
			$response = $session->get_next_request($ret);
		}
		if (!$response or $session->error_status)
		{
			return undef;
		}
		my @keys = keys %$response;
		$ret = $keys[0];
		last unless ($ret =~ /^$oid\d+$/);
		print "# Index $i: ", join ('|', @keys), "\n" if $debug;
		$rhash->{$response->{$ret}} = 1;
	}
	return $rhash;
}

sub interfaces
{
	my $name = shift;
	my %interfaces = ();
	my $num;
	my $ifNumber     = "1.3.6.1.2.1.2.1.0";
	my $ifEntryIndex = "1.3.6.1.2.1.2.2.1.1"; # dot something
	my $ifEntryType  = "1.3.6.1.2.1.2.2.1.3"; # dot something
	my $ifEntrySpeed = "1.3.6.1.2.1.2.2.1.5"; # dot something

	print "# System name: ", $name, "\n" if $debug;

	if (!defined ($response = $session->get_request($ifNumber)) or 
			$session->error_status)
	{
		die "Croaking: " . $session->error();
	}

	$num = $response->{$ifNumber} +1; # Add one because of bogus switch entries
	print "# Number of interfaces: ", $num, "\n" if $debug;

	my $ret = $ifEntryIndex . ".0";

	for (my $i = 0; $i < $num;)
	{
		if ($i == 0)
		{
			$response = $session->get_request($ret);
		}
		if ($i or !defined $response or $session->error_status)
		{
			$response = $session->get_next_request($ret);
		}
		if (!$response or $session->error_status)
		{
			die "Croaking: ", $session->error();
		}
		my @keys = keys %$response;
		$ret = $keys[0];
		last unless ($ret =~ /^$ifEntryIndex\.\d+$/);
		print "# Index $i: ", join ('|', @keys), "\n" if $debug;
		$interfaces{$response->{$ret}} = 1;
		$i++;
	}

	foreach my $key (keys %interfaces)
	{
		$response = $session->get_request($ifEntrySpeed . "." . $key);
		if (!$response or $session->error_status)
		{
			die "Croaking: ", $session->error();
		}
		my @keys = keys %$response;
		print "# Speed $key: ", join ('|', @keys), ": ", $response->{$keys[0]}, "\n" if $debug;
		if ($response->{$keys[0]} == 0)
		{
			delete $interfaces{$key};
		}
	}

	foreach my $key (keys %interfaces)
	{
		$response = $session->get_request($ifEntryType . "." . $key);
		if (!$response or $session->error_status)
		{
			die "Croaking: ", $session->error();
		}
		my @keys = keys %$response;
		print "# Type  $key: ", join ('|', @keys), ": ", $response->{$keys[0]}, "\n" if $debug;
		if ($response->{$keys[0]} != 6)
		{
			delete $interfaces{$key};
		}
	}

	foreach my $key (sort keys %interfaces)
	{
		print "snmp_${name}_if_$key\n";
	}
}

sub get_plugins
{
	my $dir   = shift;
	my @plugs = ();
	my @plugins = ();

	print "DEBUG: Opening \"$dir\" for reading...\n" if $debug;
	opendir (DIR, $dir) or die "Could not open \"$dir\" for reading: $!";
	@plugs = readdir (DIR);
	closedir (DIR);

	foreach my $plug (@plugs)
	{
		my $p = undef;
		my $path = "$dir/$plug";
		$path = readlink($path) and $path = $path =~ /^\// ? $path : "$dir/$path" while -l $path;
		next unless -f $path;
		next unless -x _;

		next if $plug =~ /^\./;

		$p->{'family'} = "contrib"; # Set default family...

			print "DEBUG: Checking plugin: $plug..." if $debug;
		if (! open (FILE, "$dir/$plug"))
		{
			warn "WARNING: Could not open file \"$dir/$plug\" for reading ($!). Skipping.";
			next;
		}
		while (<FILE>)
		{
			chomp;
			if (/#%#\s+family\s*=\s*(\S+)\s*$/)
			{
				$p->{'family'} = $1;
				print "$1..." if $debug;
			}
			elsif (/#%#\s+capabilities\s*=\s*(.+)$/)
			{
				foreach my $cap (split (/\s+/, $1))
				{
					$p->{'capability'}->{$cap} = 1;
					print "$cap..." if $debug;
				}
			}
		}
		close (FILE);
		print "\n" if $debug;

		if (defined $p->{'capability'}->{'snmpconf'})
		{
			$plug =~ s/^snmp__//;
			$plug =~ s/_$//;
			push (@plugins, $plug);
		}
	}
	return @plugins;
}

1;

=head1 NAME

munin-node-configure-snmp - A sub-program used by munin-node-configure to
do the actual SNMP probing.

=head1 SYNOPSIS

munin-node-configure-snmp [options] <host/cidr> [host/cidr] [...]

=head1 DESCRIPTION

Munin's node is a daemon that Munin connects to fetch data. This data is
stored in .rrd-files, and later graphed and htmlified. It's designed to
let it be very easy to graph new datasources.

Munin-node-configure-snmp is a program that is used by another program in
the Munin package, munin-node-configure, to do SNMP probing of hosts or
networks.

This program is only meant to be run by other programs in the Munin
package, not by hand.

=head1 VERSION

This is munin-node v1.2.6

=head1 AUTHORS

Jimmy Olsen.

=head1 BUGS

munin-node-configure-snmp does not have any known bugs.

Please report other bugs in the bug tracker at L<http://munin.sf.net/>.

=head1 COPYRIGHT

Copyright © 2004 Jimmy Olsen.

This is free software; see the source for copying conditions. There is
NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE.

This program is released under the GNU General Public License

=cut

