#!/usr/local/bin/perl
# $Id: daily_dhcp_summary.pl,v 1.9 2004/10/01 19:14:09 xxxxxxxx for-review $
# ===========================================================================

use strict;
use Carp;
use Getopt::Long;
use File::ReadBackwards;
use Getopt::Long;
use Time::Local;

my (%Snetworks, %Sbuilding, %Ssize);  # used globally
my $TopOfPage; # global
our($network, $building, $avg, $min, $max, $size); # for format

my(%Opts, $DBG);

my $INVOKEMAIL = '/usr/sbin/sendmail -oi';

sub dbg($) {
    print STDERR "$_[0]\n" if $DBG==2;
}

sub err($) {
    confess "ERR: $_[0] at input line no. $.:\n$_\n";
}

sub help(;$) {
    my $msg = shift;
    my $version = substr('$Id: daily_dhcp_summary.pl,v 1.9 2004/10/01 19:14:09 xxxxxxxx for-review $',0,79);
    my $delimit = "-" x length($version);
    $msg = $msg? "\n$delimit\n$msg": "";
    print STDERR <<HELP;

Runtime options:
$delimit
$version
$delimit
dbg  : turn on debug mode, copious output to STDERR (default: OFF)
       write debug output to STDERR. Default is 0 (off), 1 (some output),
       2 (a lot). All lines for dbg=1 start w/ 'scan:', and search/process
       the header lines ( =~ /^D/ ) in file.
help : shows this help
file : if present, specifies input file. Default:
       /usr/local/etc/dhcp-tools/report/netdat.all-3600/allinput
use-date: by default, just yesterday's stats. Can set a specific date, as
          yyyy/mm/dd.
email: can appear one or more time. Specify recipient email address.
$msg
HELP
    exit 1;
}

our(@show);
sub dbgscan(@) {
    return if !$DBG;
    local(@show) = @_;
    return if ! scalar @show;
    if ($show[0] eq "nnl")  { # nnl = NoNewLine
        shift @show;
    } else {
        print STDERR "\n";
    }
    s/\s*$// for(@show);
    write STDERR for(@show);
}


sub get_yr_yday($;$) {
    # inputs: unixtime stamp (eg: time());
    #         days back (0 if undefined). e.g: yesterday is -1.

    my ($t, $daysback) = @_;
    $daysback = 0 if !defined $daysback;
    die "illegal #daysback in get_yr_yday"
        if $daysback > 0 or $daysback < -360;

    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $t;
    $year += 1900;  # year is based on 1900
    $yday += 1;  # today's day no. (yday is zero-based)

    $yday += $daysback;  # note, can't go back more than one year
    if ($yday <= 0)  { # if last year if non-positive
        $year -= 1;
        $yday += (365 + ($year % 4? 0: 1));  # add one for leap year
    }
    return sprintf("%04d-%03d", $year, $yday);
}

sub get_search_area($) {
    my $setdate = shift;
    dbgscan("looking for: $setdate");
    #
    # eg: year (e.g: 2004), and yday (e.g: Jan 1 is 1, 09/29/04 is 273)
    #

    my $finame = $Opts{file};
    my $rio = File::ReadBackwards->new($finame);
    die "couldn't create obj" if !defined $rio;


    my $action = "initial";
    dbgscan("action=$action");
    my $reposition;  # initially undefined, will be reposition to start of search
    my($recdate, $lastrec, $lastdate, $lastpos);
    my $n=0;
    while($_ = $rio->readline) {
        next if !/^DATE (\d{4}-\d{3})-/;  # section start
        $recdate = $1;
        dbgscan("hdrrec: recdate=$recdate, line follows:", $_, "---");
        if ($action eq "initial" && $recdate le $setdate) {
            $lastrec = $_;
            $lastdate = $recdate;
            $lastpos = $rio->tell;
            $action = "scanstart";
            dbgscan("nnl", "initial reset to scanstart", "lastrec=$lastrec",
                "lastdate=$lastdate, lastpos=$lastpos")
        } elsif ($action eq "scanstart") {
            dbgscan("nnl", "action=scanstart, recdate=$recdate, lastdate=$lastdate");
            if ($recdate lt $lastdate) {
                $reposition = $lastpos;
                dbgscan("nnl", "action=scanstart, recdate < lastdate, " .
                    "reposition=$reposition; LAST");
                last;
            } else {
                $lastpos = $rio->tell;
                dbgscan("nnl", "action=scanstart, continues with lastpos=$lastpos");
            }
        }
    }
    dbgscan("done scanning backwards");
    if (!defined $reposition) {
        dbgscan("reposition undefined", "action: $action");
        if ($action eq "initial") {
            die "all recs > date";
        } else {
            dbgscan("reposition reset to start of file");
            $reposition = 0;
        }
    } else {
        dbgscan("reposition had been set to $reposition");
    }
    dbgscan("finished first pass");
    $rio->close;

    return $reposition, $lastrec;
}

sub main() {
    local(*F);

    # read options

    Getopt::Long::Configure(qw(ignore_case  nopass_through  auto_abbrev));
    die "\nGetOptions is unhappy" if
        !GetOptions(\%Opts, qw(dbg:i
                               help|h|?
                               file:s
                               use-date:s
                               email|msg|mail=s@));
    help() if $Opts{help};

    $DBG = $Opts{dbg} || 0;
    die "illegal dbg value, must be either unspecifed or 0, 1 or 2"
        if $DBG !~ /^[0-2]$/;

    # use default if not specified
    $Opts{file} = '/usr/local/etc/dhcp-tools/report/netdat.all-3600/allinput'
        if !$Opts{file};

    # simple sanity checks on input file
    
    my $file = $Opts{file};
    die "file $file doesn't exist" if !-f $file;
    die "file $file not text" if !-T $file;
    die "file $file empty" if -z $file;
    open(F, $file) or die "can't open file $file: $!";
    close(F) or die "can't close file $file: $!";

    # need at least one email address (no syntax checks)
    
    my(@mailto) = @{$Opts{email}} if defined $Opts{email};
    die "no email addresses have been specified" if ! @mailto;
    my $mailto = join(", ", @mailto); 

    my $setdate = $Opts{'use-date'};
    if ($setdate) { # date sanity check
        my($setyr, $setmo, $setday);

        die "invalid date fmt (not yyyy/mm/dd)"
            if $setdate !~ m#^(\d{4})/(\d\d)/(\d\d)$#;
        ($setyr, $setmo, $setday) = ($1, $2, $3);

        die "invalid date fmt (not yyyy/mm/dd); bad year"
            if $setyr < 2004 or $setyr > 2200;
        die "invalid date fmt (not yyyy/mm/dd); bad month"
            if $setmo < 1 or $setmo > 12;
        die "invalid date fmt (not yyyy/mm/dd); bad day"
            if $setmo < 1 or $setmo > 31;
        my $t = timelocal(0,0,0,$setday,$setmo-1,$setyr-1900);
        $setdate = get_yr_yday($t);        # get specified time's year/day
    } else {
        $setdate = get_yr_yday(time, -1);  # get yesterday's year/day
    }

    # scan backwards thru the file looking for the starting header
    # containing the (earliest) specified yyyy/ddd   (year & yday);
    # returns seek address to reposition file for reading, and the
    # last header record to include in the report.

    my($reposition, $lastrec) = get_search_area($setdate);

    open(MAIL, "| $INVOKEMAIL $mailto | tee ")
            or die "can't open sendmail: $!\n";

    select(MAIL);
    $= = 9999;  # set a very large no. lines per FORMAT'd page so it
                # only does one section line per report

    print MAIL "Subject: DHCP Daily Summary\n";
    print MAIL "From: DHCP-Summary-Robot\n";
    print MAIL "\n";

    open(F, $file) or die "couldn't open file $file: $!";
    seek(F, $reposition, 0);  # position to data range start
    my $stop_next_hdr = 0;

    my $state = 0;
    my ($year, $yday, $timestamp, $txtday, $time);
    %Snetworks = %Sbuilding = %Ssize = ();
    my $daterecno = 0;
    my $relno = -1;
    while(<F>) {
        if (/^D/) {
            $relno += 1;
            my $endpos = tell(F);
            dbgscan("FOR REPORT, relative line no. $relno, endpos: $endpos",
                    $_,"---");
            if ($stop_next_hdr) {
                dbgscan("nnl",
                        "FOR REPORT, PAST END OF RANGE, END PROCESSING");
                last;
            }
        }
        if ($_ eq $lastrec) {     # ok, read last valid header in range;
                                  # bail at next header
            $stop_next_hdr = 1;
            dbgscan("nnl", "FOR REPORT, THIS IS THE LAST VALID HEADER");
        }

        chomp;
        if (/^DATE \s (\d{4})-(\d{3})-.*? \s => \s (\d+) \s at \s
                (.+?) \s (\d\d:\S+)/x) {
            summarize($year, $txtday) if defined($yday) and $2 ne $yday;
            ($year, $yday, $timestamp, $txtday, $time) = ($1, $2, $3, $4);
            $state = 1;
            $txtday =~ s/ +/ /g;
            $daterecno = $.;
            #print "year=($year), yday=($yday), txtday=($txtday), time=($time)\n";
        } elsif ($state == 1) {
            if ($_ ne "  Used %Used Aband %Aban   Size Network             Building") {
                if ($. - $daterecno > 10) {
                    err("state==1 but line $./$daterecno not 'Used ...'");
                } else {
                    next; # keep looking
                }
            }
            $state = 2;
        } elsif ($state == 2) {
            err("state==2 but line not '---- ...'") if $_ ne " ----- ----- ----- -----  ----- ------------------- ----------------------------";
            $state = 3;
        } elsif ($state == 3) {
            if (/^ -----/) {
                $state = 0;
                next;
            }
            my ($used, $perc, $size, $network, $building) =
                    (split(' ', $_, 7))[0,1,4,5,6];
            push(@{$Snetworks{$network}}, [$timestamp, $used, $perc, $size]);
            $Sbuilding{$network} = $building;
            $Ssize{$network} = $size;
        }
    }
    summarize($year, $txtday);
    close(F);
}

sub summarize {
    my($year, $txtday) = @_;
    my %Summary = ();
    return if !scalar keys %Snetworks;

    my @netkeys = sort {$Sbuilding{$a} cmp $Sbuilding{$b} or $a cmp $b}
            keys %Snetworks;
    dbg("1 ** $year, $txtday");
    eval(<<TOP_OF_PAGE);
format MAIL_TOP =

             * DHCP NETWORK SUMMARY FOR $txtday, $year *

Network              Building                      avg%  min%  max%  #Nodes
-------------------  ----------------------------  ----  ----  ----  ------
.
TOP_OF_PAGE

    confess "couldn't eval TopOfPage\n===\n$TopOfPage\n===\n: $@" if $@ ne "";
    $- = 0; # force formfeed
    for my $network (@netkeys) {
        my @info = @{$Snetworks{$network}};
        my ($min, $max, $average);
        my ($sumperc, $count) = (0, 0);
        for (@info) {
            my ($timestamp, $used, $perc, $size) = @$_;
            dbg("2 ** network=($network), timestamp=($timestamp), used=($used), perc=($perc), size=($size)");

            next if !$size;  # should never happen, but would
                             # give division by zero if it did
            # ecompute perc (get more accuracy)
            $perc = 100 * $used / $size;
            $min = $perc if ! defined $min or $perc < $min;
            $max = $perc if ! defined $max or $perc > $max;
            $sumperc += $perc;
            ++$count;
        }
        if (!$count) {
            $min = $max = $average = "----";
        } else {
            $average = $sumperc / $count;
            for ($min, $max, $average) {
                $_ = sprintf("%3d%%", int(0.5 + $_));
            }
        }
        dbg("3 ** network=($network): count=($count), sumperc=($sumperc), min=($min), max=($max), average=($average)");
        $Summary{$network} = [$min, $max, $average];
    }
    $- = 0;
    for(@netkeys) {
        local($network, $building, $avg, $min, $max, $size);
        $network = $_;
        $building = $Sbuilding{$_};
        ($min, $max, $avg) = @{$Summary{$_}};
        $size = $Ssize{$_};
        write MAIL;
    }
    close(MAIL) or die "can't close external mail cmd: $!\n";
}


main();

format MAIL =
^<<<<<<<<<<<<<<<<<<  ^<<<<<<<<<<<<<<<<<<<<<<<<<<<  ^>>>  ^>>>  ^>>>  ^>>>>>
$network,            $building,                    $avg, $min, $max, $size
.


format STDERR =
scan: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$_
scan:     ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~
$_
.
