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
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;
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
269 {
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);
283 }
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 }
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)
376 {
377 print STDERR "No hostname given. Please use `--host'.\n";
378 exit (1);
379 }
381 if (@excludes)
382 {
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 }
401 }
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'}))
408 {
409 print STDERR "Error: No <plugin>, <snmp>, or <data> block found.\n";
410 exit (1);
411 }
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 :