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 #
23 use strict;
24 use warnings;
25 use SNMP;
26 use Config::General ('ParseConfig');
27 use Getopt::Long ('GetOptions');
28 use Socket6;
30 sub get_config
31 {
32 my %conf;
33 my $file = shift;
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
49 sub probe_one
50 {
51 my $sess = shift;
52 my $conf = shift;
53 my @oids;
54 my $cmd = 'GET';
55 my $vl;
57 if (!$conf->{'table'} || !$conf->{'values'})
58 {
59 warn "No 'table' or 'values' setting";
60 return;
61 }
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 }
73 require Data::Dumper;
75 #print "probe_one: \@oids = (" . join (', ', @oids) . ");\n";
76 for (@oids)
77 {
78 my $oid_orig = $_;
79 my $vb;
80 my $status;
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 }
93 $vb = SNMP::Varbind->new ([$oid_orig]);
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;
115 $status = $sess->getnext ($vb);
116 if ($sess->{'ErrorNum'} != 0)
117 {
118 return;
119 }
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 }
133 #print "$oid_orig > $oid_copy ?\n";
134 if (substr ($oid_copy, 0, length ($oid_orig)) ne $oid_orig)
135 {
136 return;
137 }
138 }
140 #print STDOUT Data::Dumper->Dump ([$oid_orig, $status], [qw(oid_orig status)]);
141 } # for (@oids)
143 return (1);
144 } # probe_one
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;
156 {
157 my @status;
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;
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 }
186 while ($version > 0)
187 {
188 my $sess;
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 }
201 $begin = time ();
203 for (keys %$data)
204 {
205 my $name = $_;
206 if (probe_one ($sess, $data->{$name}))
207 {
208 push (@valid_data, $name);
209 }
211 if ((@valid_data == 0) && ((time () - $begin) > 10))
212 {
213 # break for loop
214 last;
215 }
216 }
218 if (@valid_data)
219 {
220 # break while loop
221 last;
222 }
224 $version--;
225 } # while ($version > 0)
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
251 sub exit_usage
252 {
253 print <<USAGE;
254 Usage: snmp-probe-host.px --host <host> [options]
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.
262 USAGE
263 exit (1);
264 }
266 =head1 NAME
268 snmp-probe-host.px - Find out what information an SNMP device provides.
270 =head1 SYNOPSIS
272 ./snmp-probe-host.px --host switch01.mycompany.com --community ei2Acoum
274 =head1 DESCRIPTION
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)>).
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.
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.
290 =cut
292 my $host;
293 my $file = '/etc/collectd/collectd.conf';
294 my $community = 'public';
295 my $conf;
296 my $working_data;
298 =head1 OPTIONS
300 The following command line options are accepted:
302 =over 4
304 =item B<--host> I<hostname>
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>.
310 =item B<--config> I<config file>
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>.
317 =item B<--community> I<community>
319 SNMP community to use. Should be pretty straight forward.
321 =back
323 =cut
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;
330 if (!$host)
331 {
332 print STDERR "No hostname given. Please use `--host'.\n";
333 exit (1);
334 }
336 $conf = get_config ($file) or die ("Cannot read config");
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 }
346 probe_all ($host, $community, $conf->{'plugin'}{'snmp'}{'data'});
348 exit (0);
350 =head1 BUGS
352 =over 4
354 =item
356 C<Include> statements in the config file are not handled correctly.
358 =item
360 SNMPv2 / SNMPv1 detection is a hack.
362 =back
364 =head1 AUTHOR
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/>.
370 =cut
372 # vim: set sw=2 sts=2 ts=8 et :