dhcpd-pool 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061
  1. #!/usr/bin/perl
  2. #
  3. # This program is free software: you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation, either version 3 of the License, or
  6. # (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. use strict;
  16. use Getopt::Long;
  17. use Socket qw(inet_aton);
  18. use Time::Local qw(timegm);
  19. use DB_File;
  20. use Pod::Usage;
  21. #---------------------------------------------------------------------
  22. # Global variables
  23. #---------------------------------------------------------------------
  24. # Version information
  25. my $VERSION = '0.2';
  26. # Two hashes were all info found in the dhcpd.conf and dhcpd.leases
  27. # files are stored
  28. my %subnet = ();
  29. my %lease = ();
  30. # Options with default values
  31. my %opt = ( 'config' => '/etc/dhcpd.conf',
  32. 'leases' => '/var/db/dhcpd.leases',
  33. 'munin' => 0,
  34. 'pool' => q{},
  35. 'append' => q{},
  36. 'nagios' => 0,
  37. 'verbose' => 0,
  38. 'help' => 0,
  39. 'version' => 0,
  40. 'cache-period' => 5,
  41. );
  42. # IP address regexp (not really precise, but close enough)
  43. my $ip_regexp = qr{
  44. \d{1,3} [.]
  45. \d{1,3} [.]
  46. \d{1,3} [.]
  47. \d{1,3}
  48. }xms;
  49. # Get options from command line
  50. GetOptions ( "c|config=s" => \$opt{'config'},
  51. "leases=s" => \$opt{'leases'},
  52. "m|munin" => \$opt{'munin'},
  53. "pool=s" => \$opt{'pool'},
  54. "append=s" => \$opt{'append'},
  55. "nagios" => \$opt{'nagios'},
  56. "snmp" => \$opt{'snmp'},
  57. "v|verbose" => \$opt{'verbose'},
  58. "help" => \$opt{'help'},
  59. "man" => \$opt{'man'},
  60. "version" => \$opt{'version'},
  61. "cache-period=i" => \$opt{'cache_period'}
  62. ) or exit 1;
  63. #my $cache_dir = '/tmp/dhcpd-pool'; # handy for debugging
  64. my $cache_dir = '/var/cache/dhcpd-pool';
  65. my $cache_file = $cache_dir . '/cache.db';
  66. #=====================================================================
  67. # Main program
  68. #=====================================================================
  69. # If user requested help
  70. if ($opt{'help'}) {
  71. pod2usage(0);
  72. }
  73. # If user requested man page
  74. if ($opt{'man'}) {
  75. pod2usage(-exitstatus => 0, -verbose => 2);
  76. }
  77. # If user requested version info
  78. if ($opt{'version'}) {
  79. print "dhcpd-pool version $VERSION\n";
  80. exit 0;
  81. }
  82. # If munin option is specified, set options the Munin way
  83. if ($opt{'munin'}) {
  84. $opt{'config'} = $ENV{'configfile'} ? $ENV{'configfile'} : $opt{'config'};
  85. $opt{'leases'} = $ENV{'leasefile'} ? $ENV{'leasefile'} : $opt{'leases'};
  86. }
  87. # Check and possibly create the cache dir
  88. if (! -d $cache_dir) {
  89. mkdir $cache_dir, 0700
  90. or die "Couldn't create cache directory $cache_dir: $!\n";
  91. }
  92. # Stat the cache file, and if mtime is less than the cache period in
  93. # the past, read the cache instead of the config and leases files
  94. my @cstat = stat($cache_file);
  95. if ( (time() - $cstat[9]) < ($opt{'cache_period'} * 60) ) {
  96. read_cache(); # read cache
  97. }
  98. else {
  99. read_config($opt{'config'}); # read config file
  100. read_leases(); # read leases file
  101. write_cache(); # write cache
  102. }
  103. # Behaviour depending on options
  104. if ($opt{'nagios'}) {
  105. # Act as Nagios plugin
  106. my $retval = nagios_plugin();
  107. exit $retval;
  108. }
  109. elsif ($opt{'munin'}) {
  110. # Act as a Munin plugin
  111. my $retval = munin_plugin();
  112. exit $retval;
  113. }
  114. else {
  115. # Default behaviour
  116. print_status();
  117. }
  118. #---------------------------------------------------------------------
  119. # Functions
  120. #---------------------------------------------------------------------
  121. # Writes the cache file. Uses a Berkeley DB via DB_File
  122. sub write_cache {
  123. # Delete cache file
  124. unlink $cache_file;
  125. # The cache hash
  126. my %cache = ();
  127. # Open the cache file
  128. my $db = tie %cache, 'DB_File', $cache_file, O_CREAT|O_RDWR, 0600, $DB_HASH
  129. or die "Cannot open file '$cache_file': $!\n";
  130. # Write config to cache
  131. foreach my $net (keys %subnet) {
  132. my $mask = $subnet{$net}{'mask'};
  133. foreach my $pool (keys %{ $subnet{$net}{'pool'} }) {
  134. foreach my $key (qw(name warning critical monitor)) {
  135. my $ckey = join('__CaChEiD__', '0', join('__SuBnEt__', $net, $mask, $pool, $key));
  136. $cache{$ckey} = $subnet{$net}{'pool'}{$pool}->{$key};
  137. }
  138. }
  139. }
  140. # Write lease info to cache
  141. foreach my $ip (keys %lease) {
  142. foreach my $key (qw(pool state)) {
  143. my $ckey = join('__CaChEiD__', '1', join('__LeAsE__', $ip, $key));
  144. $cache{$ckey} = $lease{$ip}{$key};
  145. }
  146. }
  147. # Cleanup
  148. undef $db;
  149. untie %cache;
  150. }
  151. # Reads the cache file. Uses a Berkeley DB via DB_File
  152. sub read_cache {
  153. # The cache hash
  154. my %cache = ();
  155. # Open the cache file
  156. my $db = tie %cache, 'DB_File', $cache_file, O_RDONLY, 0600, $DB_HASH
  157. or die "Cannot tie '$cache_file': $!\n";
  158. # Read config and leases from cache
  159. foreach my $key (keys %cache) {
  160. my ($id, $rest) = split(/__CaChEiD__/, $key);
  161. if ($id == 0) {
  162. my ($net, $mask, $pool, $attr) = split(/__SuBnEt__/, $rest);
  163. $subnet{$net}{'mask'} = $mask;
  164. $subnet{$net}{'pool'}{$pool}{$attr} = $cache{$key};
  165. }
  166. elsif ($id == 1) {
  167. my ($ip, $attr) = split(/__LeAsE__/, $rest);
  168. $lease{$ip}{$attr} = $cache{$key};
  169. }
  170. }
  171. # Cleanup
  172. undef $db;
  173. untie %cache;
  174. }
  175. # Convert octal subnet mask to its decimal form
  176. # E.g. 255.255.255.0 = 24
  177. sub convert_netmask {
  178. my $subnetmask = shift;
  179. my $mask = 0;
  180. foreach my $oct ( split('\.', $subnetmask) ) {
  181. for (my $i = 0; $i < 8; ++$i) {
  182. ++$mask if ($oct & 2**$i) == (2**$i);
  183. }
  184. }
  185. return $mask;
  186. }
  187. #
  188. # Read the DHCP configuration file. Whenever we find a pool
  189. # declaration, monitoring information (i.e. warning and critical limits),
  190. # the IP range and subnet information is stored.
  191. #
  192. # This function is recursive, to take into account "include"
  193. # statements in the configuration file.
  194. #
  195. sub read_config {
  196. my $cf = shift;
  197. # Limit declaration regexp (semi-evil and obscure)
  198. # Example: # monitor: 80% 90% Y My subnet
  199. my $limit_regexp = qr{
  200. \# \s*? # Comment sign
  201. monitor: \s+? # monitor:
  202. (-{0,1}) # Optional minus sign
  203. (\d+) # WARNING limit
  204. (%{0,1}) \s+? # Optional percent sign
  205. (-{0,1}) # Optional minus sign
  206. (\d+) # CRITICAL limit
  207. (%{0,1}) \s+? # Optional percent sign
  208. ([YN]) \s+? # Y or N
  209. ([^\n]*) # Name of pool (optional)
  210. }ixms;
  211. # Subnet declaration regexp
  212. # Example: subnet 129.240.202.0 netmask 255.255.254.0 {
  213. my $subnet_regexp = qr{
  214. \A \s*
  215. subnet \s+
  216. ($ip_regexp) \s+
  217. netmask \s+
  218. ($ip_regexp)
  219. }xms;
  220. # Range declaration regexp
  221. # Examples: range 129.240.203.200 129.240.203.246;
  222. # range 129.240.203.187;
  223. my $range_regexp = qr{
  224. \A \s*
  225. range \s+
  226. ($ip_regexp) \s*
  227. (($ip_regexp){0,1}) \s*
  228. ;
  229. }xms;
  230. recursive_read_config($cf);
  231. # The recursive part
  232. sub recursive_read_config {
  233. $cf = shift;
  234. my $count = 0;
  235. my $scope = q{};
  236. my $net = q{};
  237. my $mask = q{};
  238. my $name = q{};
  239. my %warning = ('pool' => q{}, 'subnet' => q{});
  240. my %critical = ('pool' => q{}, 'subnet' => q{});
  241. my %monitor = ('pool' => q{}, 'subnet' => q{});
  242. # Open and read the configuration file
  243. open my $CONF, '<', $cf
  244. or die "Couldn't open config file ($cf): $!\n";
  245. while (<$CONF>) {
  246. # Found an include statement. Call ourself recursively
  247. if (m/\A\s* include \s+ ['"](.*?)['"];/xms) {
  248. my $newcf = $1;
  249. #$newcf =~ s{/etc/dhcpd.conf.d/}{}; # handy for debugging
  250. recursive_read_config($newcf);
  251. }
  252. # Found a subnet declaration
  253. elsif (m{$subnet_regexp}xms) {
  254. $net = $1;
  255. $mask = convert_netmask($2);
  256. # We're inside a subnet scope
  257. $scope = 'subnet';
  258. # store subnet info
  259. $subnet{$net}{'mask'} = $mask;
  260. # reset pool count
  261. $count = 0;
  262. }
  263. # Found a pool declaration
  264. elsif (m/\A \s* pool \s* \{/xms) {
  265. # We're inside a pool scope
  266. $scope = 'pool';
  267. # increase the pool count
  268. ++$count;
  269. }
  270. # Found a limit statement
  271. elsif (m{$limit_regexp}xms) {
  272. $warning{$scope} = $1 . $2 . $3;
  273. $critical{$scope} = $4 . $5 . $6;
  274. $monitor{$scope} = $7;
  275. $name = $8;
  276. chomp $name;
  277. }
  278. # Found a range declaration
  279. elsif (m{$range_regexp}xms) {
  280. # store pool info
  281. if ($scope eq 'pool' and $monitor{'pool'} ne q{}) {
  282. $subnet{$net}{'pool'}{$count}{'warning'} = $warning{'pool'};
  283. $subnet{$net}{'pool'}{$count}{'critical'} = $critical{'pool'};
  284. $subnet{$net}{'pool'}{$count}{'monitor'} = $monitor{'pool'};
  285. }
  286. else {
  287. $subnet{$net}{'pool'}{$count}{'warning'} = $warning{'subnet'};
  288. $subnet{$net}{'pool'}{$count}{'critical'} = $critical{'subnet'};
  289. $subnet{$net}{'pool'}{$count}{'monitor'} = $monitor{'subnet'};
  290. }
  291. $name = 'Anonymous' if $scope eq 'subnet';
  292. $name = 'N/A' if $name eq q{};
  293. $subnet{$net}{'pool'}{$count}{'name'} = $name;
  294. if ($2 eq q{}) {
  295. $lease{$1}->{'pool'} = "$net/$mask/$count";
  296. }
  297. else {
  298. foreach my $ip ( @{ explode_range($1, $2) } ) {
  299. $lease{$ip}->{'pool'} = "$net/$mask/$count";
  300. }
  301. }
  302. }
  303. # End of pool
  304. elsif ($scope eq 'pool' and m@\}@) {
  305. $scope = 'subnet';
  306. #reset variables
  307. $name = q{};
  308. $warning{'pool'} = q{};
  309. $critical{'pool'} = q{};
  310. $monitor{'pool'} = q{};
  311. }
  312. # End of subnet
  313. elsif ($scope eq 'subnet' and m@\}@) {
  314. $scope = q{};
  315. # reset variables
  316. $net = q{};
  317. $mask = q{};
  318. $name = q{};
  319. $warning{'subnet'} = q{};
  320. $critical{'subnet'} = q{};
  321. $monitor{'subnet'} = q{};
  322. }
  323. }
  324. close $CONF;
  325. }
  326. }
  327. #
  328. # Explode the range of IP addresses declared in the range
  329. # declaration. Arguments are the "to" and "from" in the range
  330. # declaration. Returns pointer to a list with all IP addresses in the
  331. # range.
  332. #
  333. sub explode_range {
  334. my $ipaddress1 = shift;
  335. my $ipaddress2 = shift;
  336. my @range = ();
  337. my @ip1 = split('\.', $ipaddress1);
  338. my @ip2 = split('\.', $ipaddress2);
  339. my @i = @ip1;
  340. while (@i[3] != @ip2[3] or @i[2] != @ip2[2]
  341. or @i[1] != @ip2[1] or @i[0] != @ip2[0]) {
  342. push @range, join('.', @i);
  343. if ($i[3] < 255) {
  344. $i[3]++;
  345. }
  346. elsif ($i[3] == 255 and $i[2] < 255) {
  347. $i[3] = 0;
  348. $i[2]++;
  349. }
  350. elsif ($i[2] == 255 and $i[1] < 255) {
  351. $i[3] = 0;
  352. $i[2] = 0;
  353. $i[1]++;
  354. }
  355. elsif ($i[1] == 255 and $i[0] < 255) {
  356. $i[3] = 0;
  357. $i[2] = 0;
  358. $i[1] = 0;
  359. $i[0]++;
  360. }
  361. else {
  362. die "Range error: IP out of range\n";
  363. }
  364. }
  365. push @range, join('.', @ip2);
  366. return \@range;
  367. }
  368. #
  369. # Function that reads the dhcpd.leases file. End time for leases are
  370. # calculated and the lease is flagged as either free, expired or
  371. # active (i.e. in use).
  372. #
  373. sub read_leases {
  374. # Initialize leases
  375. foreach my $l (keys %lease) {
  376. $lease{$l}->{'state'} = '-';
  377. }
  378. my $valid = 0; # flag: if a lease is found in a range
  379. my $pid = q{}; # pool ID
  380. my $now = time(); # current time
  381. my $ends = q{}; # lease end time
  382. my $ip = q{}; # lease IP number
  383. # ends regexp
  384. # Example: ends 5 2008/04/04 10:40:45;
  385. my $ends_regexp = qr{
  386. \A \s+
  387. ends \s
  388. \d \s
  389. (\d+)/(\d+)/(\d+) \s
  390. (\d+):(\d+):(\d+) ;
  391. }xms;
  392. # Open and read the dhcpd.leases file. Store the lease and
  393. # relevant information in the %lease hash.
  394. open my $LEASES, '<', $opt{'leases'}
  395. or die "Couldn't open leases file ($opt{leases}): $!\n";
  396. while (<$LEASES>) {
  397. if (m/^lease ($ip_regexp) \{$/) {
  398. $ip = $1;
  399. POOL:
  400. foreach my $l (keys %lease) {
  401. if ($l eq $ip) {
  402. $valid = 1; # this is a valid lease
  403. $pid = $lease{$l}->{'pool'};
  404. last POOL;
  405. }
  406. }
  407. }
  408. elsif ($valid and m{$ends_regexp}xms) {
  409. $ends = timegm($6, $5, $4, $3, $2-1, $1);
  410. }
  411. elsif ($valid and /^\s+ends never;$/) {
  412. $ends = -1;
  413. }
  414. elsif ($valid and /^\}$/) {
  415. if ($ends == -1 or $ends >= $now) {
  416. $lease{$ip}->{'state'} = 'active';
  417. }
  418. else {
  419. # A lease can exist several places in the leases
  420. # file. If one of the entries is active, the others
  421. # should be ignored
  422. if ($lease{$ip}->{'state'} ne 'active') {
  423. $lease{$ip}->{'state'} = 'expired';
  424. }
  425. }
  426. $valid = 0;
  427. $ends = q{};
  428. }
  429. }
  430. close $LEASES;
  431. }
  432. #
  433. # Function that does the Nagios stuff
  434. #
  435. sub nagios_plugin {
  436. my %limit = ();
  437. my $lease_total = 0;
  438. my $lease_active = 0;
  439. SUBNET:
  440. foreach my $net (sort by_ip keys %subnet) {
  441. my $mask = $subnet{$net}{'mask'};
  442. POOL:
  443. foreach my $pool (sort keys %{ $subnet{$net}{'pool'} }) {
  444. next POOL if $subnet{$net}{'pool'}{$pool}->{'monitor'} ne 'Y';
  445. # Some helper variables
  446. my $monitor = $subnet{$net}{'pool'}{$pool}->{'monitor'};
  447. my $warning = $subnet{$net}{'pool'}{$pool}->{'warning'};
  448. my $critical = $subnet{$net}{'pool'}{$pool}->{'critical'};
  449. my $name = $subnet{$net}{'pool'}{$pool}->{'name'};
  450. # Summarize active/total leases in pool
  451. my $active = 0;
  452. my $range = 0;
  453. foreach my $l (keys %lease) {
  454. if ($lease{$l}->{'pool'} eq "$net/$mask/$pool") {
  455. ++$range;
  456. if ($lease{$l}->{'state'} eq 'active') {
  457. ++$active;
  458. }
  459. }
  460. }
  461. # Count leases and adresses
  462. $lease_total += $range;
  463. $lease_active += $active;
  464. # Handle the critical and warning limits
  465. foreach ( qw(critical warning) ) {
  466. my $treshold = $subnet{$net}{'pool'}{$pool}->{$_};
  467. # If limit is given in percent
  468. if ($treshold =~ m/^(\d+)%$/) {
  469. my $lim = $1;
  470. my $percent = $active * 100 / $range;
  471. if ($percent > $lim) {
  472. my $line = sprintf("Pool \"%s\" in subnet %s/%s is %.1f%% full",
  473. $name, $net, $mask, $percent);
  474. push @{ $limit{$_} }, $line;
  475. next POOL;
  476. }
  477. }
  478. # If limit is given in number of free leases
  479. elsif ($treshold =~ m/^-(\d+)$/) {
  480. my $lim = $1;
  481. my $free = $range - $active;
  482. if ($free < $lim) {
  483. my $line = sprintf("Pool \"%s\" in subnet %s/%s has only %d free leases",
  484. $name, $net, $mask, $free);
  485. push @{ $limit{$_} }, $line;
  486. next POOL;
  487. }
  488. }
  489. # If limit is given in number of leases in use
  490. elsif ($treshold =~ m/^(\d+)$/) {
  491. my $lim = $1;
  492. if ($active > $lim) {
  493. my $line = sprintf("Pool \"%s\" in subnet %s/%s has %d active leases (of total %d)",
  494. $name, $net, $mask, $active, $range);
  495. push @{ $limit{$_} }, $line;
  496. next POOL;
  497. }
  498. }
  499. }
  500. }
  501. }
  502. # Print the criticals, if any
  503. foreach (@{ $limit{'critical'} }) {
  504. print "$_\n";
  505. }
  506. # Print the warnings, if any
  507. foreach (@{ $limit{'warning'} }) {
  508. print "$_\n";
  509. }
  510. # Determine proper return value
  511. # (critical = 2, warning = 1, normal = 0)
  512. my $retval = 0;
  513. if (scalar(@{ $limit{'critical'} }) > 0) {
  514. $retval = 2;
  515. }
  516. elsif (scalar(@{ $limit{'warning'} }) > 0) {
  517. $retval = 1;
  518. }
  519. if ($retval == 0) {
  520. printf ("All fine: %d / %d (%.1f%%) leases in use in all pools.\n",
  521. $lease_active, $lease_total, $lease_active * 100 / $lease_total);
  522. }
  523. return $retval;
  524. }
  525. #
  526. # Function that implements the Munin feature
  527. #
  528. sub munin_plugin {
  529. # Suggest option
  530. if ($opt{'append'} eq 'suggest') {
  531. foreach my $net (sort by_ip keys %subnet) {
  532. POOL:
  533. foreach my $pool (sort keys %{ $subnet{$net}{'pool'} }) {
  534. next POOL if $subnet{$net}{'pool'}{$pool}->{'monitor'} ne 'Y';
  535. print join('_', $net, $subnet{$net}{'mask'}, $pool) . "\n";
  536. }
  537. }
  538. return 0;
  539. }
  540. # Count number of active leases in each pool
  541. foreach my $net (keys %subnet) {
  542. foreach my $pool (keys %{ $subnet{$net}{'pool'} }) {
  543. # Initialize
  544. $subnet{$net}{'pool'}{$pool}{'active'} = 0;
  545. $subnet{$net}{'pool'}{$pool}{'range'} = 0;
  546. # Summarize
  547. foreach my $l (keys %lease) {
  548. if ($lease{$l}{'pool'} eq join('/', $net, $subnet{$net}{'mask'}, $pool)) {
  549. $subnet{$net}{'pool'}{$pool}{'range'}++;
  550. if ($lease{$l}->{'state'} eq 'active') {
  551. $subnet{$net}{'pool'}{$pool}{'active'}++;
  552. }
  553. }
  554. }
  555. }
  556. }
  557. # Graph with all pools
  558. if ($opt{'pool'} eq 'total') {
  559. # If config is requested
  560. if ($opt{'append'} eq 'config') {
  561. my %label = ();
  562. print "graph_title All DHCP pools\n";
  563. print "graph_args --base 1000\n";
  564. print "graph_vlabel % full\n";
  565. print "graph_category DHCP\n";
  566. print "graph_order";
  567. foreach my $net (sort by_ip keys %subnet) {
  568. POOL:
  569. foreach my $pool (sort keys %{ $subnet{$net}{'pool'} }) {
  570. next POOL if $subnet{$net}{'pool'}{$pool}->{'monitor'} ne 'Y';
  571. my $lab = $net;
  572. $lab =~ s/\./_/g;
  573. print " " . join('_', $lab, $subnet{$net}{'mask'}, $pool);
  574. $label{join('_', $lab, $subnet{$net}{'mask'}, $pool)}
  575. = $subnet{$net}{'pool'}{$pool}->{'name'};
  576. }
  577. }
  578. print "\n";
  579. foreach my $l (keys %label) {
  580. my $lab = $l;
  581. $lab =~ s/\./_/g;
  582. print "$lab.label $label{$l}\n";
  583. print "$lab.min 0\n";
  584. print "$lab.max 100\n";
  585. }
  586. return 0;
  587. }
  588. # If values are requested
  589. else {
  590. foreach my $net (sort by_ip keys %subnet) {
  591. POOL:
  592. foreach my $pool (sort keys %{ $subnet{$net}{'pool'} }) {
  593. next POOL if $subnet{$net}{'pool'}{$pool}->{'monitor'} ne 'Y';
  594. my $lab = $net;
  595. $lab =~ s/\./_/g;
  596. print join('_', $lab, $subnet{$net}{'mask'}, $pool)
  597. . '.value '
  598. . $subnet{$net}{'pool'}{$pool}{'active'} * 100 / $subnet{$net}{'pool'}{$pool}{'range'}
  599. . "\n";
  600. }
  601. }
  602. return 0;
  603. }
  604. }
  605. # Identify which pool the user wants to graph
  606. my ($net, $pool);
  607. SUBNET:
  608. foreach my $n (keys %subnet) {
  609. foreach my $p (keys %{ $subnet{$n}{'pool'} }) {
  610. if ($opt{'pool'} eq join('_', $n, $subnet{$n}{'mask'}, $p)) {
  611. $net = $n;
  612. $pool = $p;
  613. last SUBNET;
  614. }
  615. }
  616. }
  617. # If pool is monitored, get warning/critical values
  618. my %val = ('warning' => 0, 'critical' => 0);
  619. if ($subnet{$net}{'pool'}{$pool}{'monitor'} eq 'Y') {
  620. foreach (qw(warning critical)) {
  621. my $lim = $subnet{$net}{'pool'}{$pool}->{$_};
  622. if ($lim =~ m/^(\d+)%$/) {
  623. $val{$_} = ($1 / 100) * $subnet{$net}{'pool'}{$pool}{'range'};
  624. }
  625. elsif ($lim =~ m/^-(\d+)$/) {
  626. $val{$_} = $subnet{$net}{'pool'}{$pool}{'range'} - $1;
  627. }
  628. elsif ($lim =~ m/^(\d+)$/) {
  629. $val{$_} = $1;
  630. }
  631. }
  632. }
  633. # If config is requested
  634. if ($opt{'append'} eq 'config') {
  635. print "graph_title DHCP leases in \"" . $subnet{$net}{'pool'}{$pool}{'name'} . "\"\n";
  636. print "graph_args --base 1000 -v leases -l 0\n";
  637. print "graph_category DHCP\n";
  638. print "active.info Number of active leases\n";
  639. print "active.draw AREA\n";
  640. print "active.min 0\n";
  641. print "active.max $subnet{$net}{pool}{$pool}{range}\n";
  642. print "active.label Active leases\n";
  643. # If pool is monitored, include warning/critical tresholds
  644. if ($subnet{$net}{pool}{$pool}{'monitor'} eq 'Y') {
  645. print "warning.label Warning at $subnet{$net}{pool}{$pool}{warning}\n";
  646. print "warning.min 0\n";
  647. print "warning.max $subnet{$net}{pool}{$pool}{range}\n";
  648. print "warning.info Warning treshold\n";
  649. print "critical.label Critical at $subnet{$net}{pool}{$pool}{critical}\n";
  650. print "critical.min 0\n";
  651. print "critical.max $subnet{$net}{pool}{$pool}{range}\n";
  652. print "critical.info Critical treshold\n";
  653. }
  654. print "max.label Total leases\n";
  655. print "max.min 0\n";
  656. print "max.max $subnet{$net}{pool}{$pool}{range}\n";
  657. print "max.info Total number of leases in range\n";
  658. # If pool is monitored, include warning/critical tresholds
  659. if ($subnet{$net}{'pool'}{$pool}{'monitor'} eq 'Y') {
  660. printf ("active.warning %.1f\n", $val{'warning'});
  661. printf ("active.critical %.1f\n", $val{'critical'});
  662. }
  663. }
  664. # If values are requested
  665. else {
  666. print "active.value $subnet{$net}{pool}{$pool}{active}\n";
  667. # If pool is monitored, include warning/critical tresholds
  668. if ($subnet{$net}{'pool'}{$pool}{'monitor'} eq 'Y') {
  669. printf ("warning.value %.1f\n", $val{'warning'});
  670. printf ("critical.value %.1f\n", $val{'critical'});
  671. }
  672. print "max.value $subnet{$net}{pool}{$pool}{range}\n";
  673. }
  674. return 0;
  675. }
  676. # Sort by IP address
  677. sub by_ip {
  678. (inet_aton($a) || 0) cmp (inet_aton($b) || 0);
  679. }
  680. #
  681. # This function prints various status information
  682. #
  683. sub print_status {
  684. my $pools_monitored = 0;
  685. my $pools_unmonitored = 0;
  686. my $pools_total = 0;
  687. my $lease_total = 0;
  688. my $lease_active = 0;
  689. foreach my $net (sort by_ip keys %subnet) {
  690. my $mask = $subnet{$net}{'mask'};
  691. if (defined $subnet{$net}{'pool'}) {
  692. print "\n";
  693. print "Subnet $net/$mask\n";
  694. print '-' x 50, "\n\n";
  695. }
  696. foreach my $pool (sort keys %{ $subnet{$net}{'pool'} }) {
  697. # Some helper variables
  698. my $monitor = $subnet{$net}{'pool'}{$pool}->{'monitor'};
  699. my $warning = $subnet{$net}{'pool'}{$pool}->{'warning'};
  700. my $critical = $subnet{$net}{'pool'}{$pool}->{'critical'};
  701. my $name = $subnet{$net}{'pool'}{$pool}->{'name'};
  702. # Count values
  703. ++$pools_total;
  704. $monitor eq 'Y'
  705. ? ++$pools_monitored
  706. : ++$pools_unmonitored;
  707. # Sum active/free/total leases
  708. my $active = 0;
  709. my $range = 0;
  710. foreach my $l (keys %lease) {
  711. if ($lease{$l}->{'pool'} eq "$net/$mask/$pool") {
  712. ++$range;
  713. ++$lease_total;
  714. if ($lease{$l}->{'state'} eq 'active') {
  715. ++$active;
  716. ++$lease_active;
  717. }
  718. }
  719. }
  720. # Print information about pool
  721. if ($name eq 'Anonymous') {
  722. print " Anonymous pool:\n";
  723. }
  724. else {
  725. print " $pool. Pool \"$name\":\n";
  726. }
  727. print "\n";
  728. print " Monitoring: " . ($monitor eq 'Y' ? "ON" : "OFF") . "\n";
  729. if ($monitor eq 'Y') {
  730. print " Warning limit: " . ($warning =~ m/^-/ ? q{} : q{ }) . "$warning\n";
  731. print " Critical limit: " . ($critical =~ m/^-/ ? q{} : q{ }) . "$critical\n";
  732. }
  733. printf (" Active leases: %d/%d (%.1f\%)\n",
  734. $active, $range, ($active * 100 / $range) );
  735. # Print IP range if verbose
  736. if ($opt{'verbose'}) {
  737. print " IP range ($range addresses):\n";
  738. foreach my $l (sort by_ip keys %lease) {
  739. if ($lease{$l}->{'pool'} eq "$net/$mask/$pool") {
  740. print " $l\t" . $lease{$l}->{'state'} . "\n";
  741. }
  742. }
  743. }
  744. print "\n";
  745. }
  746. }
  747. # Print a short summary at the end
  748. print "\nSUMMARY\n";
  749. print '=' x 50 . "\n\n";
  750. print " Total pools: $pools_total\n";
  751. print " Total pools monitored: $pools_monitored\n";
  752. print " Total pools un-monitored: $pools_unmonitored\n";
  753. print "\n";
  754. print " Total leases: $lease_total\n";
  755. printf (" Total active leases: %d (%.1f%%)\n",
  756. $lease_active, ($lease_active * 100 / $lease_total) );
  757. print "\n";
  758. }
  759. __END__
  760. =head1 NAME
  761. dhcpd-pool - Monitor and report ISC dhcpd pool usage
  762. =head1 SYNOPSIS
  763. dhcpd-pool [-c|--config <configfile>] [-l|--leases <leasefile>]
  764. [-m|--munin [-p|--pool <poolID>] [-a|--append <string>]]
  765. [-n|--nagios] [-v|--verbose] [-h|--help]
  766. =head1 DESCRIPTION
  767. This script will report pool usage on a ISC dhcpd server. Does also
  768. work on failover pairs, since each node will have identical config
  769. (when it comes to subnets and pools) and a complete leases file.
  770. Configuration is done in the DHCP config file, but the script will
  771. report usage on pools without configuration. Details below.
  772. The script can operate as a Nagios plugin, reporting pool usage above
  773. the treshold configured by the user. It can also act as a Munin
  774. plugin, creating one graph per pool and/or one graph with all pools.
  775. B<dhcpd-pool> uses a cache file (via Berkeley DB) to speed up runtime
  776. and decrease load impact on the DHCP server. The cache is updated if
  777. it is more than 5 minutes old.
  778. =head1 OPTIONS
  779. =over 4
  780. =item B<-c>, B<--config>
  781. The ISC dhcpd config file. Normally F</etc/dhcpd.conf>, which is the
  782. default.
  783. =item B<-l>, B<--leases>
  784. The ISC dhcpd leases file. Default is F</var/db/dhcpd.leases>
  785. =item B<-n>, B<--nagios>
  786. Act as a Nagios plugin. Notification limits for each pool, as well as
  787. which pools will be monitored, is configured in the DHCP config
  788. file. Details below.
  789. =item B<-m>, B<--munin>
  790. Act as a Munin plugin. One can create one graph per pool, or create
  791. one graph with all pools. In case of the latter percentage usage is
  792. graphed instead of absolute usage, since many different pools in one
  793. graph usually don't make sense and is pretty useless.
  794. =item B<-p>, B<--pool>
  795. Pool ID. This option is only used when acting as a Munin plugin. The
  796. pool ID has the form F<subnet_mask_X> where F<X> is the pool number as
  797. found in the DHCP config file. Pools declared with "pool" starts on 1,
  798. while anonymous pools (without pool declaration) has number 0.
  799. Example: 129.240.202.0_23_1
  800. =item B<-a>, B<--append>
  801. This is the regular Munin option, i.e. "config". Other possibilities
  802. are "suggest" and "autoconf". This option does only make sense when
  803. the script is used as a Munin plugin.
  804. =item B<-v>, B<--verbose>
  805. Be more verbose. This option has only effect when the script is used
  806. in its default form, i.e. not Nagios or Munin. When this option is
  807. given, the IP range for each pool is printed out.
  808. =item B<--cache-period>
  809. The default timeout (or TTL) for the cache, given in minutes. The
  810. default cache period is 5 minutes. You may want to increase this if
  811. you're polling less frequently than the default, in which case the
  812. cache has no effect.
  813. =item B<-h>, B<--help>
  814. Short help text.
  815. =item B<--man>
  816. Display the man page.
  817. =item B<--version>
  818. Display version information
  819. =back
  820. =head1 CONFIGURATION
  821. Configuration is done in the DHCP config file. For each pool you want
  822. to monitor, include a statement like this in the beginning of each
  823. pool scope:
  824. # monitor: <warning> <critical> <Y|N> [name]
  825. Note the comment sign. The limits configuration is a comment in the
  826. DHCP config, and is only recognized by this script.
  827. The B<Y> or B<N> is simply a yes or no to monitoring. If monitoring is
  828. set to B<N>, the Nagios plugin will ignore the pool.
  829. The name is optional, but it's encouraged to set a proper name for
  830. each pool.
  831. The warning and critical limits can each be given in three different
  832. forms. In the examples, if the pool holds 200 leases total, the limits
  833. are effectively identical.
  834. =over 4
  835. =item B<Percentage>
  836. When a percent sign (%) follows the number, the limit is given in
  837. percent. E.g. if the limit is 80% and the pool range contains 200
  838. leases, a notification will occur if the number of active leases is
  839. more than or equal to 160.
  840. Example: # monitor: 80% 90% Y My pool
  841. =item B<Absolute>
  842. When the limit is given as a positive integer, a notification will
  843. occur when the number of active leases is greater than or equal to the
  844. limit.
  845. Example: # monitor: 160 180 Y My pool
  846. =item B<Leases left>
  847. If a minus sign (-) precedes the number, a notification will occur if
  848. the number of free (not active) leases is less than or equal to the
  849. limit.
  850. Example: # monitor: -40 -20 Y My pool
  851. =back
  852. The limit configuration can be set in both the subnet scope and the
  853. pool scope. If set in the pool scope, that takes precedence for that
  854. particular pool. Setting the limits configuration per pool is
  855. recommended.
  856. =head1 FILES
  857. Cache file: F</var/cache/dhcpd-pool/cache.db>
  858. DHCP config: F</etc/dhcpd.conf>
  859. DHCP leases: F</var/db/dhcpd.leases>
  860. =head1 SEE ALSO
  861. Complete documentation: L<http://folk.uio.no/trondham/software/dhcpd-pool.html>
  862. =head1 AUTHOR
  863. Trond H. Amundsen <t.h.amundsen@usit.uio.no>
  864. =head1 BUGS
  865. Probably.
  866. =cut