1#!/usr/bin/perl 2# 3# Shows current leases. 4# 5# THIS SCRIPT IS PUBLIC DOMAIN, NO RIGHTS RESERVED! 6# 7# I've removed the email addresses of Christian and vom to avoid 8# putting them on spam lists. If either of you would like to have 9# your email in here please send mail to the DHCP bugs list at ISC. 10# 11# 2008-07-13, Christian Hammers 12# 13# 2009-06-?? - added loading progress counter, pulls hostname, adjusted formatting 14# vom 15# 16# 2013-04-22 - added option to choose lease file, made manufacture information 17# optional, sar 18# 19# 2016-01-19 - updated to better trim the manu string and output the hostnames, sar 20# 21# 2016-01-18 - Mainly cosmetics. Eliminated spurious output in "parsable" mode. 22# Provided for the various conventional lease file locations. (cbp) 23 24use strict; 25use warnings; 26use POSIX qw(strftime); 27 28my @LEASES = ('/var/db/dhcpd.leases', '/var/lib/dhcp/dhcpd.leases', '/var/lib/dhcp3/dhcpd.leases'); 29my @all_leases; 30my @leases; 31 32my @OUIS = ('/usr/share/misc/oui.txt', '/usr/local/etc/oui.txt'); 33my $OUI_URL = 'http://standards.ieee.org/regauth/oui/oui.txt'; 34my $oui; 35 36my %data; 37 38my $opt_format = 'human'; 39my $opt_keep = 'active'; 40 41our $total_leases = 0; 42 43## Return manufactorer name for specified MAC address (aa:bb:cc:dd:ee:ff). 44sub get_manufactorer_for_mac($) { 45 my $manu = "-NA-"; 46 47 if (defined $oui) { 48 $manu = join('-', ($_[0] =~ /^(..):(..):(..):/)); 49 $manu = `grep -i '$manu' $oui | cut -f3`; 50 $manu =~ s/^\s+|\s+$//g; 51 } 52 53 return $manu; 54} 55 56## Read oui.txt or print warning. 57sub check_oui_file() { 58 59 for my $oui_cand (@OUIS) { 60 if ( -r $oui_cand) { 61 $oui = $oui_cand; 62 last; 63 } 64 } 65 66 if (not defined $oui) { 67 print(STDERR "To get manufacturer names please download $OUI_URL "); 68 print(STDERR "to /usr/local/etc/oui.txt\n"); 69 } 70} 71 72## Read current leases file into array. 73sub read_dhcpd_leases() { 74 75 my $db; 76 for my $db_cand (@LEASES) { 77 if ( -r $db_cand) { 78 $db = $db_cand; 79 last; 80 } 81 } 82 die("Cannot find leases db") unless defined $db; 83 open(F, $db) or die("Cannot open $db: $!"); 84 print("Reading leases from $db\n") if $opt_format eq 'human'; 85 my $content = join('', <F>); 86 close(F); 87 @all_leases = split(/lease/, $content); 88 89 foreach my $lease (@all_leases) { 90 if ($lease =~ /^\s+([\.\d]+)\s+{.*starts \d+ ([\/\d\ \:]+);.*ends \d+ ([\/\d\ \:]+);.*ethernet ([a-f0-9:]+);/s) { 91 ++$total_leases; 92 } 93 } 94} 95 96## Add manufactor name and sort out obsolet assignements. 97sub process_leases() { 98 my $gm_now = strftime("%Y/%m/%d %H:%M:%S", gmtime()); 99 my %tmp_leases; # for sorting and filtering 100 101 my $counter = $opt_format eq 'human' ? 1 : 0; 102 103 # parse entries 104 foreach my $lease (@all_leases) { 105 # skip invalid lines 106 next if not ($lease =~ /^\s+([\.\d]+)\s+{.*starts \d+ ([\/\d\ \:]+);.*ends \d+ ([\/\d\ \:]+);.*ethernet ([a-f0-9:]+);(.*client-hostname \"(\S+)\";)*/s); 107 # skip outdated lines 108 next if ($opt_keep eq 'active' and $3 lt $gm_now); 109 110 if ($counter) { 111 my $percent = (($counter / $total_leases)*100); 112 printf "Processing: %2d%% complete\r", $percent; 113 ++$counter; 114 } 115 116 my $hostname = "-NA-"; 117 if ($6) { 118 $hostname = $6; 119 } 120 121 my $mac = $4; 122 my $date_end = $3; 123 my %entry = ( 124 'ip' => $1, 125 'date_begin' => $2, 126 'date_end' => $date_end, 127 'mac' => $mac, 128 'hostname' => $hostname, 129 'manu' => get_manufactorer_for_mac($mac), 130 ); 131 132 $entry{'date_begin'} =~ s#\/#-#g; # long live ISO 8601 133 $entry{'date_end'} =~ s#\/#-#g; 134 135 if ($opt_keep eq 'all') { 136 push(@leases, \%entry); 137 } elsif (not defined $tmp_leases{$mac} or $tmp_leases{$mac}{'date_end'} gt $date_end) { 138 $tmp_leases{$mac} = \%entry; 139 } 140 } 141 142 # In case we used the hash to filtered 143 if (%tmp_leases) { 144 foreach (sort keys %tmp_leases) { 145 my $h = $tmp_leases{$_}; 146 push(@leases, $h); 147 } 148 } 149 150 # print "\n"; 151 152} 153 154# Output all valid leases. 155sub output_leases() { 156 if ($opt_format eq 'human') { 157 printf "%-19s%-16s%-15s%-20s%-20s\n","MAC","IP","hostname","valid until","manufacturer"; 158 print("===============================================================================================\n"); 159 } 160 foreach (@leases) { 161 if ($opt_format eq 'human') { 162 printf("%-19s%-16s%-14.14s %-20s%-20s\n", 163 $_->{'mac'}, # MAC 164 $_->{'ip'}, # IP address 165 $_->{'hostname'}, # hostname 166 $_->{'date_end'}, # Date 167 $_->{'manu'}); # manufactor name 168 } else { 169 printf("MAC %s IP %s HOSTNAME %s BEGIN %s END %s MANUFACTURER %s\n", 170 $_->{'mac'}, 171 $_->{'ip'}, 172 $_->{'hostname'}, 173 $_->{'date_begin'}, 174 $_->{'date_end'}, 175 $_->{'manu'}); 176 } 177 } 178} 179 180# Commandline Processing. 181sub cli_processing() { 182 while (my $arg = shift(@ARGV)) { 183 if ($arg eq '--help') { 184 print( 185 "Prints active DHCP leases.\n\n". 186 "Usage: $0 [options]\n". 187 " --help shows this help\n". 188 " --parsable machine readable output with full dates\n". 189 " --last prints the last (even if end<now) entry for every MAC\n". 190 " --all prints all entries i.e. more than one per MAC\n". 191 " --lease uses the next argument as the name of the lease file\n". 192 " the default is to try /var/db/dhcpd.leases then\n". 193 " /var/lib/dhcp/dhcpd.leases then\n". 194 " /var/lib/dhcp3/dhcpd.leases\n". 195 "\n"); 196 exit(0); 197 } elsif ($arg eq '--parsable') { 198 $opt_format = 'parsable'; 199 } elsif ($arg eq '--last') { 200 $opt_keep = 'last'; 201 } elsif ($arg eq '--all') { 202 $opt_keep = 'all'; 203 } elsif ($arg eq '--lease') { 204 unshift @LEASES, shift(@ARGV); 205 } else { 206 die("Unknown option $arg"); 207 } 208 } 209} 210 211# 212# main() 213# 214cli_processing(); 215check_oui_file(); 216read_dhcpd_leases(); 217process_leases(); 218output_leases(); 219