--- /dev/null
+#!/usr/bin/perl
+#
+# collectd - snmp-probe-host.px
+# Copyright (C) 2008 Florian octo Forster
+#
+# 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; only version 2 of the License is applicable.
+#
+# 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.,
+# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Author:
+# Florian octo Forster <octo at noris.net>
+#
+
+use strict;
+use warnings;
+use SNMP;
+use Config::General ('ParseConfig');
+use Getopt::Long ('GetOptions');
+use Socket6;
+
+sub get_config
+{
+ my %conf;
+ my $file = shift;
+
+ %conf = ParseConfig (-ConfigFile => $file,
+ -LowerCaseNames => 1,
+ -UseApacheInclude => 1,
+ -IncludeDirectories => 1,
+ ($Config::General::VERSION >= 2.38) ? (-IncludeAgain => 0) : (),
+ -MergeDuplicateBlocks => 1,
+ -CComments => 0);
+ if (!%conf)
+ {
+ return;
+ }
+ return (\%conf);
+} # get_config
+
+sub probe_one
+{
+ my $sess = shift;
+ my $conf = shift;
+ my @oids;
+ my $cmd = 'GET';
+ my $vl;
+
+ if (!$conf->{'table'} || !$conf->{'values'})
+ {
+ warn "No 'table' or 'values' setting";
+ return;
+ }
+
+ @oids = split (/"\s*"/, $conf->{'values'});
+ if (($conf->{'table'} =~ m/^(true|yes|on)$/i) && ($conf->{'instance'}))
+ {
+ $cmd = 'GETNEXT';
+ push (@oids, $conf->{'instance'});
+ }
+
+ require Data::Dumper;
+
+ #print "probe_one: \@oids = (" . join (', ', @oids) . ");\n";
+ for (@oids)
+ {
+ my $oid_orig = $_;
+ my $vb;
+ my $status;
+
+ if ($oid_orig =~ m/[^0-9\.]/)
+ {
+ my $tmp = SNMP::translateObj ($oid_orig);
+ if (!defined ($tmp))
+ {
+ warn ("Cannot translate OID $oid_orig");
+ return;
+ }
+ $oid_orig = $tmp;
+ }
+
+ $vb = SNMP::Varbind->new ([$oid_orig]);
+
+ if ($cmd eq 'GET')
+ {
+ $status = $sess->get ($vb);
+ if ($sess->{'ErrorNum'} != 0)
+ {
+ return;
+ }
+ }
+ else
+ {
+ my $oid_copy;
+
+ $status = $sess->getnext ($vb);
+ if ($sess->{'ErrorNum'} != 0)
+ {
+ return;
+ }
+
+ $oid_copy = $vb->[0];
+ if ($oid_copy =~ m/[^0-9\.]/)
+ {
+ my $tmp = SNMP::translateObj ($oid_copy);
+ if (!defined ($tmp))
+ {
+ warn ("Cannot translate OID $oid_copy");
+ return;
+ }
+ $oid_copy = $tmp;
+ }
+
+ #print "$oid_orig > $oid_copy ?\n";
+ if (substr ($oid_copy, 0, length ($oid_orig)) ne $oid_orig)
+ {
+ return;
+ }
+ }
+
+ #print STDOUT Data::Dumper->Dump ([$oid_orig, $status], [qw(oid_orig status)]);
+ } # for (@oids)
+
+ return (1);
+} # probe_one
+
+sub probe_all
+{
+ my $host = shift;
+ my $community = shift;
+ my $data = shift;
+ my $version = 2;
+ my @valid_data = ();
+ my $begin;
+ my $address;
+
+ {
+ my @status;
+
+ @status = getaddrinfo ($host, 'snmp');
+ while (@status >= 5)
+ {
+ my $family = shift (@status);
+ my $socktype = shift (@status);
+ my $proto = shift (@status);
+ my $saddr = shift (@status);
+ my $canonname = shift (@status);
+ my $host;
+ my $port;
+
+ ($host, $port) = getnameinfo ($saddr, NI_NUMERICHOST);
+ if (defined ($port))
+ {
+ $address = $host;
+ }
+ else
+ {
+ warn ("getnameinfo failed: $host");
+ }
+ }
+ }
+ if (!$address)
+ {
+ return;
+ }
+
+ while ($version > 0)
+ {
+ my $sess;
+
+ $sess = new SNMP::Session (DestHost => $host,
+ Community => $community,
+ Version => $version,
+ Timeout => 1000000,
+ UseNumeric => 1);
+ if (!$sess)
+ {
+ $version--;
+ next;
+ }
+
+ $begin = time ();
+
+ for (keys %$data)
+ {
+ my $name = $_;
+ if (probe_one ($sess, $data->{$name}))
+ {
+ push (@valid_data, $name);
+ }
+
+ if ((@valid_data == 0) && ((time () - $begin) > 10))
+ {
+ # break for loop
+ last;
+ }
+ }
+
+ if (@valid_data)
+ {
+ # break while loop
+ last;
+ }
+
+ $version--;
+ } # while ($version > 0)
+
+ if (!@valid_data)
+ {
+ return;
+ }
+
+ print <<EOF;
+ <Host "$host">
+ Address "$address"
+ Version $version
+ Community "$community"
+EOF
+ for (sort (@valid_data))
+ {
+ print " Collect \"$_\"\n";
+ }
+ print <<EOF;
+ Interval 60
+ </Host>
+EOF
+} # probe_all
+
+sub exit_usage
+{
+ print <<USAGE;
+Usage: snmp-probe-host.px --host <host> [options]
+
+Options are:
+ -H | --host Hostname of the device to probe.
+ -C | --config Path to config file holding the SNMP data blocks.
+ -c | --community SNMP community to use. Default: `public'.
+ -h | --help Print this information and exit.
+
+USAGE
+ exit (1);
+}
+
+=head1 NAME
+
+snmp-probe-host.px - Find out what information an SNMP device provides.
+
+=head1 SYNOPSIS
+
+ ./snmp-probe-host.px --host switch01.mycompany.com --community ei2Acoum
+
+=head1 DESCRIPTION
+
+The C<snmp-probe-host.px> script can be used to automatically generate SNMP
+configuration snippets for collectd's snmp plugin (see L<collectd-snmp(5)>).
+
+This script parses the collectd configuration and detecs all "data" blocks that
+are defined for the SNMP plugin. It then queries the device specified on the
+command line for all OIDs and registeres which OIDs could be answered correctly
+and which resulted in an error. With that information the script figures out
+which "data" blocks can be used with this hosts and prints an appropriate
+"host" block to standard output.
+
+The script first tries to contact the device via SNMPv2. If after ten seconds
+no working "data" block has been found, it will try to downgrade to SNMPv1.
+This is a bit a hack, but works for now.
+
+=cut
+
+my $host;
+my $file = '/etc/collectd/collectd.conf';
+my $community = 'public';
+my $conf;
+my $working_data;
+
+=head1 OPTIONS
+
+The following command line options are accepted:
+
+=over 4
+
+=item B<--host> I<hostname>
+
+Hostname of the device. This B<should> be a fully qualified domain name (FQDN),
+but anything the system can resolve to an IP address will word. B<Required
+argument>.
+
+=item B<--config> I<config file>
+
+Sets the name of the collectd config file which defined the SNMP "data" blocks.
+Due to limitations of the config parser used in this script
+(C<Config::General>), C<Include> statements cannot be parsed correctly.
+Defaults to F</etc/collectd/collectd.conf>.
+
+=item B<--community> I<community>
+
+SNMP community to use. Should be pretty straight forward.
+
+=back
+
+=cut
+
+GetOptions ('H|host|hostname=s' => \$host,
+ 'C|conf|config=s' => \$file,
+ 'c|community=s' => \$community,
+ 'h|help' => \&exit_usage) or die;
+
+if (!$host)
+{
+ print STDERR "No hostname given. Please use `--host'.\n";
+ exit (1);
+}
+
+$conf = get_config ($file) or die ("Cannot read config");
+
+if (!defined ($conf->{'plugin'})
+ || !defined ($conf->{'plugin'}{'snmp'})
+ || !defined ($conf->{'plugin'}{'snmp'}{'data'}))
+{
+ print STDERR "Error: No <plugin>, <snmp>, or <data> block found.\n";
+ exit (1);
+}
+
+probe_all ($host, $community, $conf->{'plugin'}{'snmp'}{'data'});
+
+exit (0);
+
+=head1 BUGS
+
+=over 4
+
+=item
+
+C<Include> statements in the config file are not handled correctly.
+
+=item
+
+SNMPv2 / SNMPv1 detection is a hack.
+
+=back
+
+=head1 AUTHOR
+
+Copyright (c) 2008 by Florian octo Forster
+E<lt>octoE<nbsp>atE<nbsp>noris.netE<gt>. Licensed under the terms of the GPLv2.
+Written for the norisE<nbsp>networkE<nbsp>AG L<http://noris.net/>.
+
+=cut
+
+# vim: set sw=2 sts=2 ts=8 et :