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