#!/usr/bin/perl -I/usr/local/sauron
#
# update-dhcp-info  reads dhcpd syslog entries and updates host dhcp_date
#                   field in hosts table (for given server)
#
# Copyright (c) Timo Kokkonen <tjko@iki.fi>  2001-2005.
# $Id: update-dhcp-info,v 1.15 2005/12/06 05:45:56 tjko Exp $
#
require 5;
use Getopt::Long;
use Time::Local;
use Sauron::DB;
use Sauron::Util;
use Sauron::BackEnd;
use Sauron::Sauron;

load_config();

%months = (Jan=>0,Feb=>1,Mar=>2,Apr=>3,May=>4,Jun=>5,
	   Jul=>6,Aug=>7,Sep=>8,Oct=>9,Nov=>10,Dec=>11);

###################################################################

GetOptions("help|h","verbose|v","debug|d","commit","year=s");

if ($opt_help || @ARGV < 2) {
  print "syntax: $0 [--help] [--verbose] [--commit] [--year=<YYYY>]",
        " <servername> <logfile>\n\n";
  print "" if ($opt_help);
  exit(0);
}

$opt_verbose = ($opt_verbose ? 1 : 0);
$opt_debug = ($opt_debug ? 1 : 0);
$opt_commit = ($opt_commit ? 1 : 0);
$servername=$ARGV[0];
$file=$ARGV[1];
$timenow=time();
$year=(localtime($timenow))[5];

if ($opt_year) {
  fatal("invalid argument for option --year ($opt_year)")
    unless ($opt_year =~ /^\d\d\d\d$/);
  $year=$opt_year - 1900;
  fatal("invalid year specified: $opt_year") unless ($year > 0);
}

print "Using year (".($year+1900).") as the year for data in logfile.\n"
  if ($opt_verbose);


fatal("cannot read input file '$file'") unless (-r $file);
db_connect();

$serverid=get_server_id($servername);
fatal("cannot find server '$servername'") unless ($serverid > 0);
print "Updating server: $servername ($serverid)\n" if ($opt_verbose);

undef @q;
db_query("SELECT MAX(h.dhcp_date) FROM hosts h, zones z " .
	 "WHERE z.server=$serverid AND h.zone=z.id AND dhcp_date > 0;",\@q);
$latest=($q[0][0] > 0 ? $q[0][0] : 0);
$latest_str = localtime($latest);

print "Latest dhcp_date for this server: $latest_str ($latest)\n"
  if ($opt_verbose);


print "Reading logfile..." if ($opt_verbose);

if ($file =~ /\.gz$/) {
  open(FILE,"gzip -dc $file |") || fatal("cannot decompress file: $file");
} elsif ($file =~ /\.bz2$/) {
  open(FILE,"bzip2 -dc $file |") || fatal("cannot decompress file: $file");
} else {
  open(FILE,"$file") || fatal("cannot open file: $file");
}

while (<FILE>) {
  next unless /^(\S+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+((\S+)\s+dhcpd:(\s+\[.+?\])?|\[dhcpd\])\s(.*)$/;

  $m=$months{$1}; $d=$2; $hour=$3; $min=$4; $sec=$5; $host=$7; $rest=$9;
  $t=timelocal($sec,$min,$hour,$d,$m,$year);
  $first_date = $t unless (defined $first_date);
  # hostname info not currently used anywhere...
  $first_host = $host unless (defined $first_host);
  fatal("Log contains new year or time shifts backwards!")
    if (($t+86400) < $first_date);
  $mac=''; $cmd=''; $ip=''; $via='';
  #print "$m,$d,$hour,$min,$sec,$host,$rest\n";

  if ($rest =~ /^(DHCPDISCOVER|BOOTREQUEST)\s+from\s+(\S+)\s+via\s+(.*)$/) {
    #print "$1 $2 $3\n";
    $cmd=$1;
    $mac=$2;
    $via=$3;
  }
  elsif ($rest =~ /^(DHCPREQUEST)\s+for\s+(\S+)(\s+\((\S+)\))?\s+from\s+(\S+)\s+via\s+(.*)$/) {
    $cmd=$1;
    $mac=$5;
    $via=$6;
    #print "$1 $2 ($4) $5 '$6' : $mac\n";
  }
  elsif ($rest =~ /^(DHCPACK|DHCPNAK)\s+on\s+(\S+)\s+to\s+(\S+)\s+via\s+(.*)$/) {
    $cmd=$1;
    $ip=$2;
    $mac=$3;
    $via=$4;
    #print "$1 $2 $3 '$4'\n";
  }
  elsif ($rest =~ /^(BOOTREPLY)\s+for\s+(\S+)\s+to\s+(\S+)\s+\((\S+)\)\s+via\s+(.*)$/) {
    $cmd=$1;
    $ip=$2;
    $mac=$4;
    $via=$5;
    #print "$1 $2 $3 $4 '$5'\n";
  }


  if ($mac) {
    my $origmac=$mac;
    $mac = uc($mac);
    $mac =~ s/[^A-F0-9]//g;
    next if (length($mac) == 32); # ignore those damn MS RAS requests...
    unless (length($mac)==12) {
      print "$file($.): invalid mac addres found ($origmac)\n";
      next;
    }
    #print "$t : $mac\n";
    $hash{$mac}={} unless (defined $hash{$mac});
    $hash{$mac}->{$cmd}=$t;
    $hash{$mac}->{"$cmd-via"}=$via;
    $hash{$mac}->{"$cmd-ip"}=$ip;
    $iphash{$ip}=$mac if ($ip);
  }

}

close(FILE);

$hashcount = (keys %hash);
print "done. (found $hashcount different MACs)\n" if ($opt_verbose);

print "Fetching host info from database..." if ($opt_verbose);
undef @q;
db_query("SELECT h.id,h.ether,h.dhcp_date,h.domain,a.ip " .
	 "FROM hosts h JOIN zones z ON h.zone=z.id " .
	 " LEFT JOIN a_entries a ON a.host=h.id " .
	 "WHERE z.server=$serverid AND h.type=1",\@q);
$c=@q;
print "done. ($c suitable host entries found in database)\n" if ($opt_verbose);
$matchbyip=0;
$notfounds=0;
$notupdated=0;

for $i (0..$#q) {
  $mac=uc($q[$i][1]);
  $ip=$q[$i][4];
  $matchtype='MAC';
  unless (defined $hash{$mac}) {
    # try to lookup host record by IP when MAC lookup fails...
    if ($iphash{$ip}) {
      $mac=$iphash{$ip};
      unless (defined $hash{$mac}) {
	$notfounds++;
	next;
      }
      $matchtype='IP';
      $matchbyip++;
    } else {
      $notfounds++;
      next;
    }
  }
  $id=$q[$i][0];
  $date=($q[$i][2] > 0 ? $q[$i][2] : 0);
  $domain=$q[$i][3];
  $t = $hash{$mac}->{DHCPACK} > 0  ?  $hash{$mac}->{DHCPACK}  :  0;
  $via = $hash{$mac}->{'DHCPACK-via'};
  $ip = $hash{$mac}->{'DHCPACK-ip'};
  if ($hash{$mac}->{BOOTREPLY} > $t) {
    $t = $hash{$mac}->{BOOTREPLY};
    $via = $hash{$mac}->{'BOOTREPLY-via'};
    $ip = $hash{$mac}->{'BOOTREPLY-ip'};
  }
  
  next unless ($t > 0); # skip unless time from DHCPACK/BOOTREPLY

  if ( ($t <= $date) &&
       ($date < ($timenow + 30*86400)) # handle bogus host dhcp_dates
     ) {
    print "notupdating: $domain (".dhcpether($mac).") : $t : $date\n"
      if ($opt_debug);
    $notupdated++;
    next;
  }
  
  print "match by IP: $domain $id $date : $t ($via,$ip)\n" 
    if ($opt_debug && $matchtype eq 'IP');
  push @match, [$id,$t,"$ip for " . dhcpether($mac) . " via $via"];
  $matchhash{$id}=$matchtype;
}

print "$matchbyip host(s) matched using IP\n",
      "$notupdated host(s) info not updated\n" if ($opt_debug);

$mcount=@match;
unless ($mcount > 0) {
  print "Nothing to do (no records to be updated)\n" if ($opt_verbose);
  exit;
}

print "Updating dhcp_date for $mcount host records..." if ($opt_verbose);
unless ($opt_commit) {
  print "\nNot updating records (missing --commit)\n";
  exit(1);
}

db_begin();
for $i (0..$#match) {
  $id=$match[$i][0];
  $t=$match[$i][1];
  $info=db_encode_str($match[$i][2]);
  $res=db_exec("UPDATE hosts SET dhcp_date=$t,dhcp_info=$info WHERE id=$id;");
  fatal("cannot update record id=$id") if ($res < 0);
}
fatal("cannot commit changes to database") if (db_commit() < 0);

print "done.\n" if ($opt_verbose);

exit 0;

# eof

