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