1 | #! /usr/bin/perl
|
---|
2 | # --------------------------------------------------------------------
|
---|
3 | # Copyright (C) 2006 Oliver Hitz <oliver@net-track.ch>
|
---|
4 | #
|
---|
5 | # $Id: dhcpd-snmp.in,v 1.2 2006/01/25 19:26:00 oli Exp $
|
---|
6 | #
|
---|
7 | # This program is free software; you can redistribute it and/or modify
|
---|
8 | # it under the terms of the GNU General Public License as published by
|
---|
9 | # the Free Software Foundation; either version 2 of the License, or
|
---|
10 | # (at your option) any later version.
|
---|
11 | #
|
---|
12 | # This program is distributed in the hope that it will be useful, but
|
---|
13 | # WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
---|
15 | # General Public License for more details.
|
---|
16 | #
|
---|
17 | # You should have received a copy of the GNU General Public License
|
---|
18 | # along with this program; if not, write to the Free Software
|
---|
19 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston,
|
---|
20 | # MA 02111-1307, USA.
|
---|
21 | # --------------------------------------------------------------------
|
---|
22 | # dhcpd-snmp
|
---|
23 | #
|
---|
24 | # An extension for polling the active and available lease counts of a
|
---|
25 | # running dhcpd.
|
---|
26 | #
|
---|
27 | # Please read the man page dhcpd-snmp(8) for instructions.
|
---|
28 | # --------------------------------------------------------------------
|
---|
29 |
|
---|
30 | use Time::Local;
|
---|
31 | use strict;
|
---|
32 |
|
---|
33 | # The base OID of this extension. Has to match the OID in snmpd.conf:
|
---|
34 | my $baseoid = ".1.3.6.1.4.1.21695.1.2";
|
---|
35 |
|
---|
36 | # Results are cached for some seconds so that an SNMP walk doesn't
|
---|
37 | # result in dhcpd.leases being parsed multiple times.
|
---|
38 | my $cache_secs = 60;
|
---|
39 |
|
---|
40 | # --------------------------------------------------------------------
|
---|
41 |
|
---|
42 | my $mib;
|
---|
43 | my $mibtime;
|
---|
44 |
|
---|
45 | # Load configuration file
|
---|
46 | my $conf = read_configuration($ARGV[0]);
|
---|
47 |
|
---|
48 | # Switch on autoflush
|
---|
49 | $| = 1;
|
---|
50 |
|
---|
51 | # Main loop
|
---|
52 | while (my $cmd = <STDIN>) {
|
---|
53 | chomp $cmd;
|
---|
54 |
|
---|
55 | if ($cmd eq "PING") {
|
---|
56 | print "PONG\n";
|
---|
57 | } elsif ($cmd eq "get") {
|
---|
58 | my $oid_in = <STDIN>;
|
---|
59 |
|
---|
60 | my $oid = get_oid($oid_in);
|
---|
61 | my $mib = create_dhcp_mib();
|
---|
62 |
|
---|
63 | if ($oid != 0 && defined($mib->{$oid})) {
|
---|
64 | print "$baseoid.$oid\n";
|
---|
65 | print $mib->{$oid}[0]."\n";
|
---|
66 | print $mib->{$oid}[1]."\n";
|
---|
67 | } else {
|
---|
68 | print "NONE\n";
|
---|
69 | }
|
---|
70 | } elsif ($cmd eq "getnext") {
|
---|
71 | my $oid_in = <STDIN>;
|
---|
72 |
|
---|
73 | my $oid = get_oid($oid_in);
|
---|
74 | my $found = 0;
|
---|
75 |
|
---|
76 | my $mib = create_dhcp_mib();
|
---|
77 | my @s = sort { oidcmp($a, $b) } keys %{ $mib };
|
---|
78 | for (my $i = 0; $i < @s; $i++) {
|
---|
79 | if (oidcmp($oid, $s[$i]) == -1) {
|
---|
80 | print "$baseoid.".$s[$i]."\n";
|
---|
81 | print $mib->{$s[$i]}[0]."\n";
|
---|
82 | print $mib->{$s[$i]}[1]."\n";
|
---|
83 | $found = 1;
|
---|
84 | last;
|
---|
85 | }
|
---|
86 | }
|
---|
87 | if (!$found) {
|
---|
88 | print "NONE\n";
|
---|
89 | }
|
---|
90 | } else {
|
---|
91 | # Unknown command
|
---|
92 | }
|
---|
93 | }
|
---|
94 |
|
---|
95 | exit 0;
|
---|
96 |
|
---|
97 | sub get_oid
|
---|
98 | {
|
---|
99 |
|
---|
100 | my ($oid) = @_;
|
---|
101 | chomp $oid;
|
---|
102 |
|
---|
103 | my $base = $baseoid;
|
---|
104 | $base =~ s/\./\\./g;
|
---|
105 |
|
---|
106 | if ($oid !~ /^$base(\.|$)/) {
|
---|
107 | # Requested oid doesn't match base oid
|
---|
108 | return 0;
|
---|
109 | }
|
---|
110 |
|
---|
111 | $oid =~ s/^$base\.?//;
|
---|
112 | return $oid;
|
---|
113 | }
|
---|
114 |
|
---|
115 | sub oidcmp {
|
---|
116 | my ($x, $y) = @_;
|
---|
117 |
|
---|
118 | my @a = split /\./, $x;
|
---|
119 | my @b = split /\./, $y;
|
---|
120 |
|
---|
121 | my $i = 0;
|
---|
122 |
|
---|
123 | while (1) {
|
---|
124 |
|
---|
125 | if ($i > $#a) {
|
---|
126 | if ($i > $#b) {
|
---|
127 | return 0;
|
---|
128 | } else {
|
---|
129 | return -1;
|
---|
130 | }
|
---|
131 | } elsif ($i > $#b) {
|
---|
132 | return 1;
|
---|
133 | }
|
---|
134 |
|
---|
135 | if ($a[$i] < $b[$i]) {
|
---|
136 | return -1;
|
---|
137 | } elsif ($a[$i] > $b[$i]) {
|
---|
138 | return 1;
|
---|
139 | }
|
---|
140 |
|
---|
141 | $i++;
|
---|
142 | }
|
---|
143 | }
|
---|
144 |
|
---|
145 | sub create_dhcp_mib
|
---|
146 | {
|
---|
147 | # We cache the results for $cache_secs seconds
|
---|
148 | if (time - $mibtime < $cache_secs) {
|
---|
149 | return $mib;
|
---|
150 | }
|
---|
151 |
|
---|
152 | # Read in all leases
|
---|
153 | read_leases();
|
---|
154 |
|
---|
155 | my %dhcp = (
|
---|
156 | "1" => [ "integer", 0 ], # Number of pools
|
---|
157 | );
|
---|
158 |
|
---|
159 | foreach my $i (keys %{ $conf->{"pools"} }) {
|
---|
160 | $dhcp{"1"}[1]++;
|
---|
161 |
|
---|
162 | my $pool = $conf->{"pools"}->{$i};
|
---|
163 |
|
---|
164 | $dhcp{"2.1.".$i} = [ "integer", $i ];
|
---|
165 | $dhcp{"2.2.".$i} = [ "string", $pool->{"name"} ];
|
---|
166 | $dhcp{"2.3.".$i} = [ "integer", $pool->{"total"} ];
|
---|
167 | $dhcp{"2.4.".$i} = [ "integer", $pool->{"active"} ];
|
---|
168 | $dhcp{"2.5.".$i} = [ "integer", $pool->{"expired"} ];
|
---|
169 | $dhcp{"2.6.".$i} = [ "integer", $pool->{"total"} - $pool->{"active"} ];
|
---|
170 | }
|
---|
171 |
|
---|
172 | $mib = \%dhcp;
|
---|
173 | $mibtime = time;
|
---|
174 | return $mib;
|
---|
175 | }
|
---|
176 |
|
---|
177 | sub ip2int {
|
---|
178 | my ($ip) = @_;
|
---|
179 |
|
---|
180 | if ($ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) {
|
---|
181 | return 256*(256*(256*$1+$2)+$3)+$4;
|
---|
182 | } else {
|
---|
183 | return -1;
|
---|
184 | }
|
---|
185 | }
|
---|
186 |
|
---|
187 | sub read_leases
|
---|
188 | {
|
---|
189 | # Clear leases
|
---|
190 | foreach my $i (keys %{ $conf->{"pools"} }) {
|
---|
191 | $conf->{"pools"}->{$i}->{"leases"} = ();
|
---|
192 | $conf->{"pools"}->{$i}->{"active"} = 0;
|
---|
193 | $conf->{"pools"}->{$i}->{"expired"} = 0;
|
---|
194 | }
|
---|
195 |
|
---|
196 | # Read leases
|
---|
197 | if (!open(LEASES, $conf->{"leases"})) {
|
---|
198 | printf STDERR "Unable to open leases file '%s'!\n", $conf->{leases};
|
---|
199 | return;
|
---|
200 | }
|
---|
201 |
|
---|
202 | my %l = undef;
|
---|
203 |
|
---|
204 | while (my $line = <LEASES>) {
|
---|
205 | if ($line =~ /^lease (\d+\.\d+\.\d+\.\d+) \{$/) {
|
---|
206 | my $ip = ip2int($1);
|
---|
207 | undef %l;
|
---|
208 |
|
---|
209 | foreach my $i (keys %{ $conf->{"pools"} }) {
|
---|
210 | my $pool = $conf->{"pools"}->{$i};
|
---|
211 | my $found = 0;
|
---|
212 |
|
---|
213 | foreach my $r (@{ $pool->{"ranges"} }) {
|
---|
214 | if (($ip >= $r->{"from"}) && ($ip <= $r->{"to"})) {
|
---|
215 | %l = ( "pool" => $i, "ip" => $ip );
|
---|
216 | $found = 1;
|
---|
217 | last;
|
---|
218 | }
|
---|
219 | }
|
---|
220 | if ($found) {
|
---|
221 | last;
|
---|
222 | }
|
---|
223 | }
|
---|
224 | } elsif (defined %l && $line =~ /^\s+ends \d (\d+)\/(\d+)\/(\d+) (\d+):(\d+):(\d+);$/) {
|
---|
225 | $l{"ends"} = timegm($6, $5, $4, $3, $2-1, $1);
|
---|
226 | } elsif (defined %l && $line =~ /^\s+ends never;$/) {
|
---|
227 | $l{"ends"} = -1;
|
---|
228 | } elsif (defined %l && $line =~ /^\}$/) {
|
---|
229 | $conf->{"pools"}->{$l{"pool"}}->{"leases"}->{$l{"ip"}} = $l{"ends"};
|
---|
230 | }
|
---|
231 | }
|
---|
232 |
|
---|
233 | close(LEASES);
|
---|
234 |
|
---|
235 | # Count active and expired leases
|
---|
236 | my $now = time();
|
---|
237 |
|
---|
238 | foreach my $i (keys %{ $conf->{"pools"} }) {
|
---|
239 | my $pool = $conf->{"pools"}->{$i};
|
---|
240 |
|
---|
241 | foreach my $ip (keys %{ $pool->{"leases"} }) {
|
---|
242 | my $end = $pool->{"leases"}->{$ip};
|
---|
243 | if (($end == -1) || ($end >= $now)) {
|
---|
244 | $pool->{"active"}++;
|
---|
245 | } else {
|
---|
246 | $pool->{"expired"}++;
|
---|
247 | }
|
---|
248 | }
|
---|
249 | }
|
---|
250 | }
|
---|
251 |
|
---|
252 | sub read_configuration
|
---|
253 | {
|
---|
254 | my ($f) = @_;
|
---|
255 |
|
---|
256 | my %conf = ( "leases" => undef,
|
---|
257 | "pools" => { } );
|
---|
258 |
|
---|
259 | open C, "$f";
|
---|
260 | while (my $l = <C>) {
|
---|
261 | $l =~ s/#.*//;
|
---|
262 | $l =~ s/^\s*//;
|
---|
263 | $l =~ s/\s*$//;
|
---|
264 |
|
---|
265 | if ($l eq "") {
|
---|
266 | next;
|
---|
267 | }
|
---|
268 |
|
---|
269 | if ($l =~ /^leases:\s*(\S+)$/) {
|
---|
270 |
|
---|
271 | $conf{"leases"} = $1;
|
---|
272 |
|
---|
273 | # Check if file is readable
|
---|
274 | if (open(LEASES, $conf{"leases"})) {
|
---|
275 | close(LEASES);
|
---|
276 | } else {
|
---|
277 | printf STDERR "Unable to open leases file '%s'!\n", $conf{"leases"};
|
---|
278 | }
|
---|
279 |
|
---|
280 | } elsif ($l =~ /^pool:\s*(\d+)\s*,\s*("[^"]*"|[^"][^,]*)\s*,\s*(.*)$/) {
|
---|
281 |
|
---|
282 | # Read the pool definition
|
---|
283 | my %p = ( "index" => $1,
|
---|
284 | "name" => $2,
|
---|
285 | "ranges" => [ ],
|
---|
286 | "total" => 0,
|
---|
287 | "leases" => { } );
|
---|
288 |
|
---|
289 | my @ranges = split /\s*,\s*/, $3;
|
---|
290 |
|
---|
291 | $p{"name"} =~ s/^\"//;
|
---|
292 | $p{"name"} =~ s/\"$//;
|
---|
293 |
|
---|
294 | foreach my $r (@ranges) {
|
---|
295 | if ($r !~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})-(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/) {
|
---|
296 | printf STDERR "Invalid range definition '%s'.\n", $r;
|
---|
297 | next;
|
---|
298 | }
|
---|
299 |
|
---|
300 | my ($from, $to) = ($1, $2);
|
---|
301 |
|
---|
302 | my $fromip = ip2int($from);
|
---|
303 | my $toip = ip2int($to);
|
---|
304 |
|
---|
305 | if ($toip < $fromip) {
|
---|
306 | my $t = $toip;
|
---|
307 | $toip = $fromip;
|
---|
308 | $fromip = $t;
|
---|
309 | }
|
---|
310 |
|
---|
311 | $p{"total"} += $toip-$fromip+1;
|
---|
312 |
|
---|
313 | my %range = ( "from" => $fromip,
|
---|
314 | "to" => $toip );
|
---|
315 |
|
---|
316 | push @{ $p{"ranges"} }, \%range;
|
---|
317 | }
|
---|
318 |
|
---|
319 | $conf{"pools"}{$p{"index"}} = \%p;
|
---|
320 | } else {
|
---|
321 |
|
---|
322 | printf STDERR "Invalid line '%s'.\n", $l;
|
---|
323 |
|
---|
324 | }
|
---|
325 | }
|
---|
326 |
|
---|
327 | return \%conf;
|
---|
328 | }
|
---|
329 |
|
---|
330 | __END__
|
---|
331 |
|
---|
332 | =head1 NAME
|
---|
333 |
|
---|
334 | dhcpd-snmp
|
---|
335 |
|
---|
336 | =head1 SYNOPSIS
|
---|
337 |
|
---|
338 | dhcpd-snmp dhcpd-snmp.conf
|
---|
339 |
|
---|
340 | =head1 DESCRIPTION
|
---|
341 |
|
---|
342 | B<dhcpd-snmp> is an extension for the Net-SNMP agent and the ISC DHCP
|
---|
343 | server. It allows you to monitor and track the address usage of your
|
---|
344 | dynamic IP address pools through SNMP.
|
---|
345 |
|
---|
346 | =head1 CONFIGURATION FILE
|
---|
347 |
|
---|
348 | The configuration file defines the location of the F<dhcpd.leases>
|
---|
349 | file as well as the pools of which you want to access the lease
|
---|
350 | counts.
|
---|
351 |
|
---|
352 | The file is in B<key: value> format and allows only two keys:
|
---|
353 |
|
---|
354 | =over 8
|
---|
355 |
|
---|
356 | =item B<leases>: C</var/lib/dhcp3/dhcpd.leases>
|
---|
357 |
|
---|
358 | Location of the F<dhcpd.leases> file. This file needs to be accessible
|
---|
359 | by the script.
|
---|
360 |
|
---|
361 | =item B<pool>: C<index>, C<description>, C<ip1-ip2, ip3-ip4...>
|
---|
362 |
|
---|
363 | Defines a pool to monitor. C<index> is a unique numeric index,
|
---|
364 | C<description> a textual description of this pool, and C<ip1-ip2,
|
---|
365 | ip3-ip4, ...> defines the ranges of IP addresses belonging to this
|
---|
366 | pool.
|
---|
367 |
|
---|
368 | =back
|
---|
369 |
|
---|
370 | Since this extension is a persistent script, changes to the
|
---|
371 | configuration file require a restart of snmpd.
|
---|
372 |
|
---|
373 | =head1 INSTALLATION
|
---|
374 |
|
---|
375 | After installing the B<dhcpd-snmp> script and adapting the
|
---|
376 | configuration file, it is best to test it manually. This can be done
|
---|
377 | with the following dialog:
|
---|
378 |
|
---|
379 | PING
|
---|
380 |
|
---|
381 | The script should return "PONG".
|
---|
382 |
|
---|
383 | get
|
---|
384 | .1.3.6.1.4.1.21695.1.2.1
|
---|
385 |
|
---|
386 | The script should return three lines: the OID, "integer", and the
|
---|
387 | number of configured pools.
|
---|
388 |
|
---|
389 | get
|
---|
390 | .1.3.6.1.4.1.21695.1.2.2.2.1
|
---|
391 |
|
---|
392 | OID, "string", and the name of your first address pool.
|
---|
393 |
|
---|
394 | get
|
---|
395 | .1.3.6.1.4.1.21695.1.2.2.4.1
|
---|
396 |
|
---|
397 | OID, "integer", and the number of active leases.
|
---|
398 |
|
---|
399 | Quit the dialog using CTRL-D.
|
---|
400 |
|
---|
401 | If everything works, insert the following line into your Net-SNMP's
|
---|
402 | B<snmpd.conf> configuration file:
|
---|
403 |
|
---|
404 | pass_persist .1.3.6.1.4.1.21695.1.2 path/to/dhcpd-snmp path/to/dhcpd-snmp.conf
|
---|
405 |
|
---|
406 | Net-SNMP will need to be restarted after this change.
|
---|
407 |
|
---|
408 | You should now be able to get the statistics using F<snmpwalk>, for example:
|
---|
409 |
|
---|
410 | $ snmpwalk host community .1.3.6.1.4.1.21695.1.2
|
---|
411 |
|
---|
412 | This should give you a list of the statistics of your DHCP server.
|
---|
413 |
|
---|
414 | =head1 MIB
|
---|
415 |
|
---|
416 | The script returns the following variables:
|
---|
417 |
|
---|
418 | .1.3.6.1.4.1.21695.1.2.1: number of configured pools
|
---|
419 | .1.3.6.1.4.1.21695.1.2.2.<pool>: pool description
|
---|
420 | .1.3.6.1.4.1.21695.1.2.3.<pool>: size of the pool (number of addresses)
|
---|
421 | .1.3.6.1.4.1.21695.1.2.4.<pool>: active leases
|
---|
422 | .1.3.6.1.4.1.21695.1.2.5.<pool>: expired leases
|
---|
423 | .1.3.6.1.4.1.21695.1.2.6.<pool>: available addresses (size - active leases)
|
---|
424 |
|
---|
425 | For a complete MIB file see the C<mibs> directory in the source archive.
|
---|
426 |
|
---|
427 | =head1 SECURITY
|
---|
428 |
|
---|
429 | It is assumed that users of this script know how to properly secure
|
---|
430 | their snmpd. Please read the corresponding man pages on more
|
---|
431 | information about this.
|
---|
432 |
|
---|
433 | =head1 COPYRIGHT AND LICENSE
|
---|
434 |
|
---|
435 | Copyright (C) 2006 Oliver Hitz
|
---|
436 |
|
---|
437 | This program is free software; you can redistribute it and/or modify
|
---|
438 | it under the terms of the GNU General Public License as published by
|
---|
439 | the Free Software Foundation; either version 2 of the License, or (at
|
---|
440 | your option) any later version.
|
---|
441 |
|
---|
442 | This program is distributed in the hope that it will be useful, but
|
---|
443 | WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
444 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
---|
445 | General Public License for more details.
|
---|
446 |
|
---|
447 | You should have received a copy of the GNU General Public License
|
---|
448 | along with this program; if not, write to the Free Software
|
---|
449 | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
|
---|
450 | USA.
|
---|
451 |
|
---|
452 | =cut
|
---|