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