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) && ($conf->{'instance'}))
65 {
66 $cmd = 'GETNEXT';
67 push (@oids, $conf->{'instance'});
68 }
70 require Data::Dumper;
72 #print "probe_one: \@oids = (" . join (', ', @oids) . ");\n";
73 for (@oids)
74 {
75 my $oid_orig = $_;
76 my $vb;
77 my $status;
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 }
90 $vb = SNMP::Varbind->new ([$oid_orig]);
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;
104 $status = $sess->getnext ($vb);
105 if ($sess->{'ErrorNum'} != 0)
106 {
107 return;
108 }
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 }
122 #print "$oid_orig > $oid_copy ?\n";
123 if (substr ($oid_copy, 0, length ($oid_orig)) ne $oid_orig)
124 {
125 return;
126 }
127 }
129 #print STDOUT Data::Dumper->Dump ([$oid_orig, $status], [qw(oid_orig status)]);
130 } # for (@oids)
132 return (1);
133 } # probe_one
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;
145 {
146 my @status;
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;
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 }
175 while ($version > 0)
176 {
177 my $sess;
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 }
190 $begin = time ();
192 for (keys %$data)
193 {
194 my $name = $_;
195 if (probe_one ($sess, $data->{$name}))
196 {
197 push (@valid_data, $name);
198 }
200 if ((@valid_data == 0) && ((time () - $begin) > 10))
201 {
202 # break for loop
203 last;
204 }
205 }
207 if (@valid_data)
208 {
209 # break while loop
210 last;
211 }
213 $version--;
214 } # while ($version > 0)
216 if (!@valid_data)
217 {
218 return;
219 }
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
237 sub exit_usage
238 {
239 print <<USAGE;
240 Usage: snmp-probe-host.px --host <host> [options]
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.
248 USAGE
249 exit (1);
250 }
252 =head1 NAME
254 snmp-probe-host.px - Find out what information an SNMP device provides.
256 =head1 SYNOPSIS
258 ./snmp-probe-host.px --host switch01.mycompany.com --community ei2Acoum
260 =head1 DESCRIPTION
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)>).
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.
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.
276 =cut
278 my $host;
279 my $file = '/etc/collectd/collectd.conf';
280 my $community = 'public';
281 my $conf;
282 my $working_data;
284 =head1 OPTIONS
286 The following command line options are accepted:
288 =over 4
290 =item B<--host> I<hostname>
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>.
296 =item B<--config> I<config file>
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>.
303 =item B<--community> I<community>
305 SNMP community to use. Should be pretty straight forward.
307 =back
309 =cut
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;
316 if (!$host)
317 {
318 print STDERR "No hostname given. Please use `--host'.\n";
319 exit (1);
320 }
322 $conf = get_config ($file) or die ("Cannot read config");
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 }
332 probe_all ($host, $community, $conf->{'plugin'}{'snmp'}{'data'});
334 exit (0);
336 =head1 BUGS
338 =over 4
340 =item
342 C<Include> statements in the config file are not handled correctly.
344 =item
346 SNMPv2 / SNMPv1 detection is a hack.
348 =back
350 =head1 AUTHOR
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/>.
356 =cut
358 # vim: set sw=2 sts=2 ts=8 et :