Code

Merge remote-tracking branch 'github/pr/387'
[collectd.git] / contrib / snmp-probe-host.px
1 #!/usr/bin/perl
2 #
3 # collectd - snmp-probe-host.px
4 # Copyright (C) 2008,2009  Florian octo Forster
5 # Copyright (C) 2009       noris network AG
6 #
7 # This program is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by the
9 # Free Software Foundation; only version 2 of the License is applicable.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
19 #
20 # Author:
21 #   Florian octo Forster <octo at noris.net>
22 #
24 use strict;
25 use warnings;
26 use SNMP;
27 use Config::General ('ParseConfig');
28 use Getopt::Long ('GetOptions');
29 use Socket6;
31 our %ExcludeOptions =
32 (
33   'IF-MIB64' => qr/^\.?1\.3\.6\.1\.2\.1\.31/,
34   'IF-MIB32' => qr/^\.?1\.3\.6\.1\.2\.1\.2/
35 );
37 sub get_config
38 {
39   my %conf;
40   my $file = shift;
42   %conf = ParseConfig (-ConfigFile => $file,
43     -LowerCaseNames => 1,
44     -UseApacheInclude => 1,
45     -IncludeDirectories => 1,
46     ($Config::General::VERSION >= 2.38) ? (-IncludeAgain => 0) : (),
47     -MergeDuplicateBlocks => 1,
48     -CComments => 0);
49   if (!%conf)
50   {
51     return;
52   }
53   return (\%conf);
54 } # get_config
56 sub probe_one
57 {
58   my $sess = shift;
59   my $conf = shift;
60   my $excludes = @_ ? shift : [];
61   my @oids;
62   my $cmd = 'GET';
63   my $vl;
65   if (!$conf->{'table'} || !$conf->{'values'})
66   {
67     warn "No 'table' or 'values' setting";
68     return;
69   }
71   @oids = split (/"\s*"/, $conf->{'values'});
72   if ($conf->{'table'} =~ m/^(true|yes|on)$/i)
73   {
74     $cmd = 'GETNEXT';
75     if (defined ($conf->{'instance'}))
76     {
77       push (@oids, $conf->{'instance'});
78     }
79   }
81   require Data::Dumper;
83   #print "probe_one: \@oids = (" . join (', ', @oids) . ");\n";
84   for (@oids)
85   {
86     my $oid_orig = $_;
87     my $vb;
88     my $status;
90     if ($oid_orig =~ m/[^0-9\.]/)
91     {
92       my $tmp = SNMP::translateObj ($oid_orig);
93       if (!defined ($tmp))
94       {
95         warn ("Cannot translate OID $oid_orig");
96         return;
97       }
98       $oid_orig = $tmp;
99     }
101     for (@$excludes)
102     {
103       if ($oid_orig =~ $_)
104       {
105         return;
106       }
107     }
109     $vb = SNMP::Varbind->new ([$oid_orig]);
111     if ($cmd eq 'GET')
112     {
113       $status = $sess->get ($vb);
114       if ($sess->{'ErrorNum'} != 0)
115       {
116         return;
117       }
118       if (!defined ($status))
119       {
120         return;
121       }
122       if ("$status" eq 'NOSUCHOBJECT')
123       {
124         return;
125       }
126     }
127     else
128     {
129       my $oid_copy;
131       $status = $sess->getnext ($vb);
132       if ($sess->{'ErrorNum'} != 0)
133       {
134         return;
135       }
137       $oid_copy = $vb->[0];
138       if ($oid_copy =~ m/[^0-9\.]/)
139       {
140         my $tmp = SNMP::translateObj ($oid_copy);
141         if (!defined ($tmp))
142         {
143           warn ("Cannot translate OID $oid_copy");
144           return;
145         }
146         $oid_copy = $tmp;
147       }
149       #print "$oid_orig > $oid_copy ?\n";
150       if (substr ($oid_copy, 0, length ($oid_orig)) ne $oid_orig)
151       {
152         return;
153       }
154     }
156     #print STDOUT Data::Dumper->Dump ([$oid_orig, $status], [qw(oid_orig status)]);
157   } # for (@oids)
159   return (1);
160 } # probe_one
162 sub probe_all
164   my $host = shift;
165   my $community = shift;
166   my $data = shift;
167   my $excludes = @_ ? shift : [];
168   my $version = 2;
169   my @valid_data = ();
170   my $begin;
171   my $address;
173   {
174     my @status;
176     @status = getaddrinfo ($host, 'snmp');
177     while (@status >= 5)
178     {
179       my $family    = shift (@status);
180       my $socktype  = shift (@status);
181       my $proto     = shift (@status);
182       my $saddr     = shift (@status);
183       my $canonname = shift (@status);
184       my $host;
185       my $port;
187       ($host, $port) = getnameinfo ($saddr, NI_NUMERICHOST);
188       if (defined ($port))
189       {
190         $address = $host;
191       }
192       else
193       {
194         warn ("getnameinfo failed: $host");
195       }
196     }
197   }
198   if (!$address)
199   {
200     return;
201   }
203   while ($version > 0)
204   {
205     my $sess;
207     $sess = new SNMP::Session (DestHost => $host,
208       Community => $community,
209       Version => $version,
210       Timeout => 1000000,
211       UseNumeric => 1);
212     if (!$sess)
213     {
214       $version--;
215       next;
216     }
218     $begin = time ();
220     for (keys %$data)
221     {
222       my $name = $_;
223       if (probe_one ($sess, $data->{$name}, $excludes))
224       {
225         push (@valid_data, $name);
226       }
228       if ((@valid_data == 0) && ((time () - $begin) > 10))
229       {
230         # break for loop
231         last;
232       }
233     }
235     if (@valid_data)
236     {
237       # break while loop
238       last;
239     }
241     $version--;
242   } # while ($version > 0)
244   print <<EOF;
245   <Host "$host">
246     Address "$address"
247     Version $version
248     Community "$community"
249 EOF
250   for (sort (@valid_data))
251   {
252     print "    Collect \"$_\"\n";
253   }
254   if (!@valid_data)
255   {
256     print <<EOF;
257 # WARNING: Autoconfiguration failed.
258 # TODO: Add one or more `Collect' statements here:
259 #   Collect "foo"
260 EOF
261   }
262   print <<EOF;
263     Interval 60
264   </Host>
265 EOF
266 } # probe_all
268 sub exit_usage
270   print <<USAGE;
271 Usage: snmp-probe-host.px --host <host> [options]
273 Options are:
274   -H | --host          Hostname of the device to probe.
275   -C | --config        Path to config file holding the SNMP data blocks.
276   -c | --community     SNMP community to use. Default: `public'.
277   -h | --help          Print this information and exit.
278   -x | --exclude       Exclude a specific MIB. Call with "help" for more
279                        information.
281 USAGE
282   exit (1);
285 sub exit_usage_exclude
287   print "Available exclude MIBs:\n\n";
288   for (sort (keys %ExcludeOptions))
289   {
290     print "  $_\n";
291   }
292   print "\n";
293   exit (1);
296 =head1 NAME
298 snmp-probe-host.px - Find out what information an SNMP device provides.
300 =head1 SYNOPSIS
302   ./snmp-probe-host.px --host switch01.mycompany.com --community ei2Acoum
304 =head1 DESCRIPTION
306 The C<snmp-probe-host.px> script can be used to automatically generate SNMP
307 configuration snippets for collectd's snmp plugin (see L<collectd-snmp(5)>).
309 This script parses the collectd configuration and detecs all "data" blocks that
310 are defined for the SNMP plugin. It then queries the device specified on the
311 command line for all OIDs and registeres which OIDs could be answered correctly
312 and which resulted in an error. With that information the script figures out
313 which "data" blocks can be used with this hosts and prints an appropriate
314 "host" block to standard output.
316 The script first tries to contact the device via SNMPv2. If after ten seconds
317 no working "data" block has been found, it will try to downgrade to SNMPv1.
318 This is a bit a hack, but works for now.
320 =cut
322 my $host;
323 my $file = '/etc/collectd/collectd.conf';
324 my $community = 'public';
325 my $conf;
326 my $working_data;
327 my @excludes = ();
329 =head1 OPTIONS
331 The following command line options are accepted:
333 =over 4
335 =item B<--host> I<hostname>
337 Hostname of the device. This B<should> be a fully qualified domain name (FQDN),
338 but anything the system can resolve to an IP address will word. B<Required
339 argument>.
341 =item B<--config> I<config file>
343 Sets the name of the collectd config file which defined the SNMP "data" blocks.
344 Due to limitations of the config parser used in this script
345 (C<Config::General>), C<Include> statements cannot be parsed correctly.
346 Defaults to F</etc/collectd/collectd.conf>.
348 =item B<--community> I<community>
350 SNMP community to use. Should be pretty straight forward.
352 =item B<--exclude> I<MIB>
354 This option can be used to exclude specific data from being enabled in the
355 generated config. Currently the following MIBs are understood:
357 =over 4
359 =item B<IF-MIB>
361 Exclude interface information, such as I<ifOctets> and I<ifPackets>.
363 =back
365 =back
367 =cut
369 GetOptions ('H|host|hostname=s' => \$host,
370   'C|conf|config=s' => \$file,
371   'c|community=s' => \$community,
372   'x|exclude=s' => \@excludes,
373   'h|help' => \&exit_usage) or die;
375 if (!$host)
377   print STDERR "No hostname given. Please use `--host'.\n";
378   exit (1);
381 if (@excludes)
383   my $tmp = join (',', @excludes);
384   my @tmp = split (/\s*,\s*/, $tmp);
386   @excludes = ();
387   for (@tmp)
388   {
389     my $mib = uc ($_);
390     if ($mib eq 'HELP')
391     {
392       exit_usage_exclude ();
393     }
394     elsif (!exists ($ExcludeOptions{$mib}))
395     {
396       print STDERR "No such MIB: $mib\n";
397       exit_usage_exclude ();
398     }
399     push (@excludes, $ExcludeOptions{$mib});
400   }
403 $conf = get_config ($file) or die ("Cannot read config");
405 if (!defined ($conf->{'plugin'})
406   || !defined ($conf->{'plugin'}{'snmp'})
407   || !defined ($conf->{'plugin'}{'snmp'}{'data'}))
409   print STDERR "Error: No <plugin>, <snmp>, or <data> block found.\n";
410   exit (1);
413 probe_all ($host, $community, $conf->{'plugin'}{'snmp'}{'data'}, \@excludes);
415 exit (0);
417 =head1 BUGS
419 =over 4
421 =item
423 C<Include> statements in the config file are not handled correctly.
425 =item
427 SNMPv2 / SNMPv1 detection is a hack.
429 =back
431 =head1 AUTHOR
433 Copyright (c) 2008 by Florian octo Forster
434 E<lt>octoE<nbsp>atE<nbsp>noris.netE<gt>. Licensed under the terms of the GPLv2.
435 Written for the norisE<nbsp>networkE<nbsp>AG L<http://noris.net/>.
437 =cut
439 # vim: set sw=2 sts=2 ts=8 et :