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
18use strict;
19use warnings;
20use POSIX qw(strftime);
21
22my $LEASES = '/var/db/dhcpd.leases';
23my @all_leases;
24my @leases;
25
26my @OUIS = ('/usr/share/misc/oui.txt', '/usr/local/etc/oui.txt');
27my $OUI_URL = 'http://standards.ieee.org/regauth/oui/oui.txt';
28my $oui;
29
30my %data;
31
32my $opt_format = 'human';
33my $opt_keep = 'active';
34
35our $total_leases = 0;
36
37## Return manufactorer name for specified MAC address (aa:bb:cc:dd:ee:ff).
38sub get_manufactorer_for_mac($) {
39    my $manu = "-NA-";
40
41    if (defined $oui) {
42	$manu = join('-', ($_[0] =~ /^(..):(..):(..):/));
43	$manu = `grep -i '$manu' $oui | cut -f3`;
44	chomp($manu);
45    }
46
47    return $manu;
48}
49
50## Read oui.txt or print warning.
51sub check_oui_file() {
52
53    for my $oui_cand (@OUIS) {
54        if ( -r $oui_cand) {
55        $oui = $oui_cand;
56        last;
57        }
58    }
59
60    if (not defined $oui) {
61	print(STDERR "To get manufacturer names please download $OUI_URL ");
62	print(STDERR "to /usr/local/etc/oui.txt\n");
63    }
64}
65
66## Read current leases file into array.
67sub read_dhcpd_leases() {
68
69    open(F, $LEASES) or die("Cannot open $LEASES: $!");
70    my $content = join('', <F>);
71    close(F);
72    @all_leases = split(/lease/, $content);
73
74    foreach my $lease (@all_leases) {
75    if ($lease =~ /^\s+([\.\d]+)\s+{.*starts \d+ ([\/\d\ \:]+);.*ends \d+ ([\/\d\ \:]+);.*ethernet ([a-f0-9:]+);/s) {
76       ++$total_leases;
77       }
78    }
79}
80
81## Add manufactor name and sort out obsolet assignements.
82sub process_leases() {
83    my $gm_now = strftime("%Y/%m/%d %H:%M:%S", gmtime());
84    my %tmp_leases; # for sorting and filtering
85
86    my $counter = 1;
87
88    # parse entries
89    foreach my $lease (@all_leases) {
90	# skip invalid lines
91	next if not ($lease =~ /^\s+([\.\d]+)\s+{.*starts \d+ ([\/\d\ \:]+);.*ends \d+ ([\/\d\ \:]+);.*ethernet ([a-f0-9:]+);(.*client-hostname \"(\S+)\";)*/s);
92	# skip outdated lines
93	next if ($opt_keep eq 'active'  and  $3 lt $gm_now);
94
95	my $percent = (($counter / $total_leases)*100);
96	printf "Processing: %2d%% complete\r", $percent;
97	++$counter;
98
99	 my $hostname = "-NA-";
100	 if ($6) {
101	     $hostname = $6;
102	 }
103
104	my $mac = $4;
105	my $date_end = $3;
106	my %entry = (
107	    'ip' => $1,
108	    'date_begin' => $2,
109	    'date_end' => $date_end,
110	    'mac' => $mac,
111	    'hostname' => $hostname,
112	    'manu' => get_manufactorer_for_mac($mac),
113	    );
114
115	$entry{'date_begin'} =~ s#\/#-#g; # long live ISO 8601
116	$entry{'date_end'}   =~ s#\/#-#g;
117
118	if ($opt_keep eq 'all') {
119	    push(@leases, \%entry);
120	} elsif (not defined $tmp_leases{$mac}  or  $tmp_leases{$mac}{'date_end'} gt $date_end) {
121	    $tmp_leases{$mac} = \%entry;
122	}
123    }
124
125    # In case we used the hash to filtered
126    if (%tmp_leases) {
127	foreach (sort keys %tmp_leases) {
128	    my $h = $tmp_leases{$_};
129	    push(@leases, $h);
130	}
131    }
132
133    # print "\n";
134
135}
136
137# Output all valid leases.
138sub output_leases() {
139    if ($opt_format eq 'human') {
140	printf "%-19s%-16s%-15s%-20s%-20s\n","MAC","IP","hostname","valid until","manufacturer";
141	print("===============================================================================================\n");
142    }
143    foreach (@leases) {
144       if ($opt_format eq 'human') {
145	   printf("%-19s%-16s%-15s%-20s%-20s\n",
146		  $_->{'mac'},       # MAC
147		  $_->{'ip'},        # IP address
148		  $_->{'hostname'},  # hostname
149		  $_->{'date_end'},  # Date
150		  $_->{'manu'});     # manufactor name
151       } else {
152	   printf("MAC %s IP %s HOSTNAME %s BEGIN %s END %s MANUFACTURER %s\n",
153		  $_->{'mac'},
154		  $_->{'ip'},
155		  $_->{'hostname'},
156		  $_->{'date_begin'},
157		  $_->{'date_end'},
158		  $_->{'manu'});
159       }
160    }
161}
162
163# Commandline Processing.
164sub cli_processing() {
165    while (my $arg = shift(@ARGV)) {
166	if ($arg eq '--help') {
167	    print(
168		"Prints active DHCP leases.\n\n".
169		"Usage: $0 [options]\n".
170		" --help      shows this help\n".
171		" --parsable  machine readable output with full dates\n".
172		" --last      prints the last (even if end<now) entry for every MAC\n".
173		" --all       prints all entries i.e. more than one per MAC\n".
174		" --lease     uses the next argument as the name of the lease file\n".
175		"             the default is /var/db/dhcpd.leases\n".
176		"\n");
177	    exit(0);
178	} elsif ($arg eq '--parsable') {
179	    $opt_format = 'parsable';
180	} elsif ($arg eq '--last') {
181	    $opt_keep = 'last';
182	} elsif ($arg eq '--all') {
183	    $opt_keep = 'all';
184	} elsif ($arg eq '--lease') {
185	    $LEASES = shift(@ARGV);
186	} else {
187	    die("Unknown option $arg");
188	}
189    }
190}
191
192#
193# main()
194#
195cli_processing();
196check_oui_file();
197read_dhcpd_leases();
198process_leases();
199output_leases();
200