#!/usr/bin/perl # # collectd - snmp-probe-host.px # Copyright (C) 2008,2009 Florian octo Forster # Copyright (C) 2009 noris network AG # # 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 # use strict; use warnings; use SNMP; use Config::General ('ParseConfig'); use Getopt::Long ('GetOptions'); use Socket6; our %ExcludeOptions = ( 'IF-MIB64' => qr/^\.?1\.3\.6\.1\.2\.1\.31/, 'IF-MIB32' => qr/^\.?1\.3\.6\.1\.2\.1\.2/ ); 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 $excludes = @_ ? 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) { $cmd = 'GETNEXT'; if (defined ($conf->{'instance'})) { 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; } for (@$excludes) { if ($oid_orig =~ $_) { return; } } $vb = SNMP::Varbind->new ([$oid_orig]); if ($cmd eq 'GET') { $status = $sess->get ($vb); if ($sess->{'ErrorNum'} != 0) { return; } if (!defined ($status)) { return; } if ("$status" eq 'NOSUCHOBJECT') { 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 $excludes = @_ ? 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}, $excludes)) { 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) print < Address "$address" Version $version Community "$community" EOF for (sort (@valid_data)) { print " Collect \"$_\"\n"; } if (!@valid_data) { print < EOF } # probe_all sub exit_usage { print < [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. -x | --exclude Exclude a specific MIB. Call with "help" for more information. USAGE exit (1); } sub exit_usage_exclude { print "Available exclude MIBs:\n\n"; for (sort (keys %ExcludeOptions)) { print " $_\n"; } print "\n"; 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 script can be used to automatically generate SNMP configuration snippets for collectd's snmp plugin (see L). This script parses the collectd configuration and detects all "data" blocks that are defined for the SNMP plugin. It then queries the device specified on the command line for all OIDs and registers 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; my @excludes = (); =head1 OPTIONS The following command line options are accepted: =over 4 =item B<--host> I Hostname of the device. This B be a fully qualified domain name (FQDN), but anything the system can resolve to an IP address will word. B. =item B<--config> I 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), C statements cannot be parsed correctly. Defaults to F. =item B<--community> I SNMP community to use. Should be pretty straight forward. =item B<--exclude> I This option can be used to exclude specific data from being enabled in the generated config. Currently the following MIBs are understood: =over 4 =item B Exclude interface information, such as I and I. =back =back =cut GetOptions ('H|host|hostname=s' => \$host, 'C|conf|config=s' => \$file, 'c|community=s' => \$community, 'x|exclude=s' => \@excludes, 'h|help' => \&exit_usage) or die; if (!$host) { print STDERR "No hostname given. Please use `--host'.\n"; exit (1); } if (@excludes) { my $tmp = join (',', @excludes); my @tmp = split (/\s*,\s*/, $tmp); @excludes = (); for (@tmp) { my $mib = uc ($_); if ($mib eq 'HELP') { exit_usage_exclude (); } elsif (!exists ($ExcludeOptions{$mib})) { print STDERR "No such MIB: $mib\n"; exit_usage_exclude (); } push (@excludes, $ExcludeOptions{$mib}); } } $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 , , or block found.\n"; exit (1); } probe_all ($host, $community, $conf->{'plugin'}{'snmp'}{'data'}, \@excludes); exit (0); =head1 BUGS =over 4 =item C 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 EoctoEatEnoris.netE. Licensed under the terms of the GPLv2. Written for the norisEnetworkEAG L. =cut # vim: set sw=2 sts=2 ts=8 et :