Added library link check and addressed review comments
[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 #
23
24 use strict;
25 use warnings;
26 use SNMP;
27 use Config::General ('ParseConfig');
28 use Getopt::Long ('GetOptions');
29 use Socket6;
30
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 );
36
37 sub get_config
38 {
39   my %conf;
40   my $file = shift;
41
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
55
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;
64
65   if (!$conf->{'table'} || !$conf->{'values'})
66   {
67     warn "No 'table' or 'values' setting";
68     return;
69   }
70
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   }
80
81   require Data::Dumper;
82
83   #print "probe_one: \@oids = (" . join (', ', @oids) . ");\n";
84   for (@oids)
85   {
86     my $oid_orig = $_;
87     my $vb;
88     my $status;
89
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     }
100
101     for (@$excludes)
102     {
103       if ($oid_orig =~ $_)
104       {
105         return;
106       }
107     }
108
109     $vb = SNMP::Varbind->new ([$oid_orig]);
110
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;
130
131       $status = $sess->getnext ($vb);
132       if ($sess->{'ErrorNum'} != 0)
133       {
134         return;
135       }
136
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       }
148
149       #print "$oid_orig > $oid_copy ?\n";
150       if (substr ($oid_copy, 0, length ($oid_orig)) ne $oid_orig)
151       {
152         return;
153       }
154     }
155
156     #print STDOUT Data::Dumper->Dump ([$oid_orig, $status], [qw(oid_orig status)]);
157   } # for (@oids)
158
159   return (1);
160 } # probe_one
161
162 sub probe_all
163 {
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;
172
173   {
174     my @status;
175
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;
186
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   }
202
203   while ($version > 0)
204   {
205     my $sess;
206
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     }
217
218     $begin = time ();
219
220     for (keys %$data)
221     {
222       my $name = $_;
223       if (probe_one ($sess, $data->{$name}, $excludes))
224       {
225         push (@valid_data, $name);
226       }
227
228       if ((@valid_data == 0) && ((time () - $begin) > 10))
229       {
230         # break for loop
231         last;
232       }
233     }
234
235     if (@valid_data)
236     {
237       # break while loop
238       last;
239     }
240
241     $version--;
242   } # while ($version > 0)
243
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
267
268 sub exit_usage
269 {
270   print <<USAGE;
271 Usage: snmp-probe-host.px --host <host> [options]
272
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.
280
281 USAGE
282   exit (1);
283 }
284
285 sub exit_usage_exclude
286 {
287   print "Available exclude MIBs:\n\n";
288   for (sort (keys %ExcludeOptions))
289   {
290     print "  $_\n";
291   }
292   print "\n";
293   exit (1);
294 }
295
296 =head1 NAME
297
298 snmp-probe-host.px - Find out what information an SNMP device provides.
299
300 =head1 SYNOPSIS
301
302   ./snmp-probe-host.px --host switch01.mycompany.com --community ei2Acoum
303
304 =head1 DESCRIPTION
305
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)>).
308
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.
315
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.
319
320 =cut
321
322 my $host;
323 my $file = '/etc/collectd/collectd.conf';
324 my $community = 'public';
325 my $conf;
326 my $working_data;
327 my @excludes = ();
328
329 =head1 OPTIONS
330
331 The following command line options are accepted:
332
333 =over 4
334
335 =item B<--host> I<hostname>
336
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>.
340
341 =item B<--config> I<config file>
342
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>.
347
348 =item B<--community> I<community>
349
350 SNMP community to use. Should be pretty straight forward.
351
352 =item B<--exclude> I<MIB>
353
354 This option can be used to exclude specific data from being enabled in the
355 generated config. Currently the following MIBs are understood:
356
357 =over 4
358
359 =item B<IF-MIB>
360
361 Exclude interface information, such as I<ifOctets> and I<ifPackets>.
362
363 =back
364
365 =back
366
367 =cut
368
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;
374
375 if (!$host)
376 {
377   print STDERR "No hostname given. Please use `--host'.\n";
378   exit (1);
379 }
380
381 if (@excludes)
382 {
383   my $tmp = join (',', @excludes);
384   my @tmp = split (/\s*,\s*/, $tmp);
385
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   }
401 }
402
403 $conf = get_config ($file) or die ("Cannot read config");
404
405 if (!defined ($conf->{'plugin'})
406   || !defined ($conf->{'plugin'}{'snmp'})
407   || !defined ($conf->{'plugin'}{'snmp'}{'data'}))
408 {
409   print STDERR "Error: No <plugin>, <snmp>, or <data> block found.\n";
410   exit (1);
411 }
412
413 probe_all ($host, $community, $conf->{'plugin'}{'snmp'}{'data'}, \@excludes);
414
415 exit (0);
416
417 =head1 BUGS
418
419 =over 4
420
421 =item
422
423 C<Include> statements in the config file are not handled correctly.
424
425 =item
426
427 SNMPv2 / SNMPv1 detection is a hack.
428
429 =back
430
431 =head1 AUTHOR
432
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/>.
436
437 =cut
438
439 # vim: set sw=2 sts=2 ts=8 et :