#!/usr/bin/perl ################################################################ # ISC DHCP Server reporting tool ################################################################ my $path_to_leasefile = "/var/lib/dhcpd/dhcpd.leases"; my $path_to_conf = "/etc/dhcp/dhcpd.conf"; my $date_format = "%m-%d-%Y %I:%M%p"; my $ver = "0.7.0"; use strict; use warnings; use Getopt::Long; use JSON::PP; use Sys::Hostname; use Time::Local; # For timegm() #Populate all the command line variables my ($debug,$showexpired,$help,$color,$ping_client,$extended_mac,$mrtg,$xml,$summary,$pool); my $json = 0; my $search = ''; GetOptions( 'expired|x' => \$showexpired, 'help|h' => \$help, 'color|c' => \$color, 'ping|p' => \$ping_client, 'n' => \$extended_mac, 'mrtg' => \$mrtg, 'xml' => \$xml, 'search=s' => \$search, 'summary' => \$summary, 'debug' => \$debug, 'json' => \$json, 'pool=s' => \$pool, ); # for backwards compatibility convert 4/123 to 4-123 #$search =~ s/\//-/g; #Display the usage if they pass in --help or -h if ($help) { die(usage()); } my $out = {}; my $output = ''; my %hash = get_lease_data($path_to_leasefile); if ($summary) { show_summary(\%hash); } my @list = sort(keys %hash); my $total = scalar(@list); my $count = 0; foreach my $ip (@list) { my $hostname = $hash{$ip}->{'hostname'} || "*blank*"; my $remoteid = $hash{$ip}->{'remoteid'} || "*none*"; my $mac = $hash{$ip}->{'mac'} || ""; my $opt82 = $hash{$ip}->{'remoteid'} || ''; my $lease_end = $hash{$ip}->{'lease_end'}; if (length($opt82) > 32) { $opt82 = substr($opt82, 0, 32) . "..."; } $ip = long2ip($ip); my $line = sprintf("%15s %19s %35s %s\n", $ip, $mac, $opt82, $hostname); if ($search && $line !~ /$search/) { next; } $output .= $line; push(@{$out->{'item'}},{ 'ip' => trim($ip), 'mac' => trim($mac), 'option82' => trim($opt82), 'lease_end' => trim($lease_end), 'hostname' => trim($hostname), }); $count++; } if ($search) { $output .= "\n$count matching leases found"; } $output .= "\n$total total actives leases found\n"; if ($json) { print encode_json($out); exit; } if ($mrtg) { $output = "$total\n"; $output .= "$total\n"; $output .= "Running since: Unknown\n"; $output .= "DHCP Server Leases\n"; exit(0); } my $percent; if (!$total == 0) { $percent = sprintf("%2.f%%", ($count/$total) * 100); } else { $percent = "100%"; } if ($ping_client) { print "$count active leases ($percent)\n"; } print $output; ################################################################################################# ################################################################################################# ################################################################################################# sub get_option_82 { my $data = shift; if (!$data) { return "N/A"; } my $ret; # If it's an interface (Calix/Occam) if ($data =~ /(N[\d\-].*)/i) { $ret = $1; # Remove anything after ADSL/ONT (extra calix junk) $ret =~ s/-(Adsl|Ont).*//g; # Remove anything after _HSI_ETH3 (Adtran specific stuff) $ret =~ s/_HSI.*//g; # Occam ports are N99-1-2-DSL3 so we remove the -DSL part and leave the number $ret =~ s/-DSL/-/g; } elsif ($data =~ /(CXNK\w+)\b/) { #$ret = $1; $ret = $data; } return $ret; } sub leasegm_to_epoch { my $str = $_[0]; my ($sec,$min,$hours,$mday,$mon,$year); if (my @list = $str =~ /(\d{4})\/(\d{1,2})\/(\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})/) { $sec = $list[5]; $min = $list[4]; $hours = $list[3]; $mday = $list[2]; $mon = $list[1] - 1; $year = $list[0] - 1900; } elsif ($str =~ /ends never/) { $sec = 1; $min = 1; $hours = 1; $mday = 1; $mon = 1; $year = 132; } else { die("Whoa that aint good! '$_[0]'\n"); } #print "$sec,$min,$hours,$mday,$mon,$year\n"; my $time_string = timegm($sec,$min,$hours,$mday,$mon,$year); return $time_string; } sub usage { my $output .= "dhcpreport $ver\n$0 [options] -x --expired show lease expiration times -m --mac show lease MAC address -n show extended MAC information -a --atm show lease ATM (Option 82) information -i --IP=1.2.3.4 filter for ip 1.2.3.4 (regexp) -c --color show output in color for readability -p --ping ping each host to see if it's alive also --mrtg output capable of graphing with MRTG -h --help show help --summary show shared network pool summary "; } sub ip2long { my $ip = shift; my @ip = split(/\./,$ip); #Make sure it's a valid ip if ($ip !~ /\d{1,3}\.\d{1,3}\.\d{1,3}/) { return 0; } if (scalar(@ip) != 4) { return 0; } #Perform the bit shifting to align each octet in the long correctly my $i = ($ip[0] << 24) + ($ip[1] << 16) + ($ip[2] << 8) + $ip[3]; return $i; } sub long2ip { my $long = shift(); my (@i,$i); $i[0] = ($long & 0xff000000) >> 24; $i[1] = ($long & 0x00ff0000) >> 16; $i[2] = ($long & 0x0000ff00) >> 8; $i[3] = ($long & 0x000000ff); $i = "$i[0].$i[1].$i[2].$i[3]"; return $i; } sub show_summary { my $info = shift(); my $pools = get_pools($path_to_conf); my $res; foreach my $ip (keys %$info) { my $pool = what_pool($ip, $pools); my $ip_text = long2ip($ip); if (!$pool) { $pool = 'UNKNOWN POOL'; next; } $res->{$pool}->{'used'}++; #print "$ip_text is in $pool\n"; } my @sort_pools = sort(keys %$res); #k(keys(%$pools)); #k(@sort_pools); foreach my $pool(@sort_pools) { $count = $res->{$pool}->{'used'}; $total = $pools->{$pool}->{'count'}; my $percent = sprintf("%.2f",($count / $total) * 100); $res->{$pool}->{total} = $total; $res->{$pool}->{percent_used} = $percent + 0; if (!$json) { print "Pool '$pool' has $count of $total active leases ($percent% full)\n"; } } my $ret; $ret->{pools} = $res; $ret->{system_uptime} = get_uptime(); $ret->{hostname} = hostname; if ($json) { print encode_json($ret); } exit; } sub get_pools { my $file = shift(); if (!-r $file) { return undef; } my ($network,$ret); my $conf = get_conf($path_to_conf); foreach my $line (@$conf) { if ($line =~ /shared-network\s+([-\w]+)?\s+/) { $network = $1; #print "Network: $network\n"; } elsif ($line =~ /^\s+[^#]range ([\d\.]+)\s+([\d\.]+)/) { my $start = ip2long($1); my $end = ip2long($2); my $count = ($end - $start) + 1; if ($debug) { print "$network = $1 ($start) -> $2 ($end) ($count)\n"; } my $temp = [$start,$end]; push(@{$ret->{$network}->{'pools'}},$temp); $ret->{$network}->{'count'} += $count; } } return $ret; } # This gets the config, and also handles the include statements sub get_conf { my $file = shift(); my $lines = file_get_contents($file, 1); my @ret; foreach my $line (@$lines) { $line =~ s/\n+$//; if ($line =~ /include "(.+?)";/) { my $file = $1; #push(@ret, "# INCLUDED FROM $file\n"); my $x = get_conf($file); @ret = (@ret, @$x); } else { push(@ret, $line); } } return \@ret; } sub what_pool { my ($ip, $pools) = @_; foreach my $pool_name(keys %$pools) { foreach my $pool(@{$pools->{$pool_name}->{'pools'}}) { my $start = $pool->[0]; my $end = $pool->[1]; #print "$start $ip $end\n"; if ($ip >= $start && $ip <= $end) { return $pool_name; } } } return ""; } sub trim { my ($s) = (@_, $_); # Passed in var, or default to $_ if (!defined($s) || length($s) == 0) { return ""; } $s =~ s/^\s*//; $s =~ s/\s*$//; return $s; } sub get_uptime { open(PROC, "<", "/proc/uptime"); my $line = trim(); my @p = split(/\s+/,$line); my $ret = $p[0] + 0; close PROC; return $ret; } sub file_get_contents { open (my $fh, "<", $_[0]) or return undef; my $ret_array_ref = $_[1]; my $ret; if ($ret_array_ref) { @$ret = readline($fh); } else { local $/ = undef; # Input rec separator (slurp) $ret = readline($fh); } return $ret; } sub get_lease_data { my $leases_file = shift(); my $ok = open(my $fh, "<", $leases_file); my $ret = {}; my $ip = ''; my $obj = {}; while (my $line = readline($fh)) { if ($line =~ /^lease (.+?) \{/) { $ip = ip2long($1); } elsif ($line =~ /^\s+binding state (\w+)/) { $obj->{state} = $1; } elsif ($line =~ /hardware ethernet (.+?);/) { $obj->{mac} = $1; } elsif ($line =~ /ends \d (.+?);/) { my $epoch = leasegm_to_epoch($1); $obj->{lease_end} = $epoch; } elsif ($line =~ /option agent\.remote-id "(.+?)"/) { $obj->{remoteid} = $1; } elsif ($line =~ /client-hostname \"(.*)\"/) { $obj->{hostname} = $1; } elsif ($line =~ /^}/) { if ($obj->{state} eq "active") { $ret->{$ip} = $obj; } $ip = ''; $obj = {}; } } return %$ret; } sub ip_sort { my $c = long2ip($a); my $d = long2ip($b); return $c <=> $d; } sub max_length { my $max = 0; foreach my $item (@_) { my $len = length($item); if ($len > $max) { $max = $len; } } return $max; } # Debug print variable using either Data::Dump::Color (preferred) or Data::Dumper # Creates methods k() and kd() to print, and print & die respectively BEGIN { if (eval { require Data::Dump::Color }) { *k = sub { Data::Dump::Color::dd(@_) }; } else { require Data::Dumper; *k = sub { print Data::Dumper::Dumper(\@_) }; } sub kd { k(@_); printf("Died at %2\$s line #%3\$s\n",caller()); exit(15); } }