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