#!/usr/bin/perl -w

# ntp-stats.pl -- John Ackermann   N8UR
# Version 0.99 -- 23 November 2007
# Licensed under General Public License Version 2

# Note that this program is optimized for low memory usage rather than
# for speed.  Consequently, it loops through the stats file several times
# rather than building what could be a very large array in memory.  Feel
# free to change that if you like.

use Getopt::Std;
use Socket;
use POSIX qw(tmpnam);
use IO::File;

###############################################################################
# local variables and paths
# Title and subtitle can be overridden on the command line
###############################################################################

my $title = "NTP Server Performance";
my $subtitle = "As seen by cesium.febo.com";
my $x_axis_label = "MJD";
my $y_axis_label = "Offset (Milliseconds)";
my $grace = "/usr/bin/gracebat";
my $work_dir = "/data/ntp/";
my $ntp_template = "/usr/local/bin/grace-cmd/ntp-stats-template.cmd";

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

# Refclocks -- used if monitoring clocks connected directly to this host
my %refclocks = (
	'127.127.1'  => 'local_clock',
	'127.127.2'  => 'gps_trak',
	'127.127.3'  => 'wwv_pst',
	'127.127.4'  => 'wwvb_spec',
	'127.127.5'  => 'truetime',
	'127.127.6'  => 'irig_audio',
	'127.127.7'  => 'chu',
	'127.127.8'  => 'parse',
	'127.127.9'  => 'gps_mx4200',
	'127.127.10' => 'gps_as2201',
	'127.127.11' => 'gps_arbiter',
	'127.127.12' => 'irig_tpro',
	'127.127.13' => 'atom_leitch',
	'127.127.14' => 'msf_ees',
	'127.127.15' => 'reserved',
	'127.127.16' => 'gps_bancomm',
	'127.127.17' => 'gps_datum',
	'127.127.18' => 'acts_modem',
	'127.127.19' => 'wwv_heath',
	'127.127.20' => 'nmea',
	'127.127.21' => 'gps_vme',
	'127.127.22' => 'pps',
	'127.127.23' => 'reserved',
	'127.127.24' => 'reserved',
	'127.127.25' => 'reserved',
	'127.127.26' => 'gps_hp',
	'127.127.27' => 'msf_arcron',
	'127.127.28' => 'pps_shm',
	'127.127.29' => 'gps_palisade',
	'127.127.30' => 'gps_oncore',
	'127.127.31' => 'gps_jupiter',
	'127.127.32' => 'chronolog',
	'127.127.33' => 'dumbclock',
	'127.127.34' => 'ulink',
	'127.127.35' => 'pcf',
	'127.127.36' => 'wwv',
	'127.127.37' => 'fg',
	'127.127.38' => 'hopf_s',
	'127.127.39' => 'hopf_p',
	'127.127.40' => 'jjy',
	'127.127.41' => 'truetime560_irig',
	'127.127.42' => 'zyfer_gps',
	'127.127.43' => 'ripe_ncc_palisac3',
	'127.127.44' => 'neoclock4x_dcf77'
	);

# Variables
my %hosts;
my $base = "";
my @ips;
my @peer_list;
my @host_list;
my @seen;
my @octet;
my $ipaddr;
my $count;
my $counter;
my $hostname;
my $ref_base;
my $datafile;
my $reading;
my $tmpfile;
my $outfile;
my $set;
my $set_color;
my $legend;
my $statsfile;

sub trim {
        $_ = shift;
        s/\n/ /sg;              # convert newlines to spaces
        s/\r/ /sg;              # convert carriage returns to spaces
        s/\000/ /sg;            # convert nulls to spaces
        s/^\s+//sg;             # trim leading spaces
        s/\s+$//sg;             # trim trailing spaces
        return $_;
}

#----------
# display usage
my $opt_string = 'hmqt:l:s:i:x:y:z:e:a:b:c:v:p:f:';

sub usage() {
print STDERR << "EOF";

usage: $0 [-h] [-t title] [-s subtitle] [-ixyz domain] [-eabc domain]
	[-l local server] [-m] [-v] -p png filename -f input filename

-h	: this (help) message
-t	: graph title; overrides default
-s	: graph subtitle; overrides default

-i	: include domain; regex
-x	: second include domain; regex
-y	: third include domain; regex
-z	: fourth include domain; regex

-e	: exclude domain; regex
-a	: second exclude domain; regex
-b	: third exclude domain; regex
-c	: fourth exclude domain; regex

-l	: local server to append after refclock ID ; implies -q
-q	: include local refclock
-v	: average data input values by value
-m	: format X axis in whole numbers for MJD; default is hms
-f	: input file using full pathname;

EOF
}

# main loop
#----------

getopts( "$opt_string", \my %opt ) or usage() and exit;

# print usage
usage() and exit if $opt{h};
usage() and exit if !$opt{f};
usage() and exit if !$opt{p};

# set variables to command line params
if ($opt{t}) {
	$title = trim($opt{t});
	}

if ($opt{s}) {
	$subtitle = trim($opt{s});
	}

my @include = ();
if ($opt{i}) {
	$include[0] = trim($opt{i});
	}

if ($opt{x}) {
	$include[1] = trim($opt{x});
	}

if ($opt{y}) {
	$include[2] = trim($opt{y});
	}

if ($opt{z}) {
	$include[3] = trim($opt{z});
	}

my @exclude = ();
if ($opt{e}) {
	$exclude[0] = trim($opt{e});
	}

if ($opt{a}) {
	$exclude[1] = trim($opt{a});
	}

if ($opt{b}) {
	$exclude[2] = trim($opt{b});
	}

if ($opt{c}) {
	$exclude[3] = trim($opt{c});
	}

my $include_refclocks = 0;
if ($opt{q}) {
	$include_refclocks = 1;
	}

my $average = 0;
if ($opt{v}) {
	$average = $opt{v};
	}

my $local_server = "";
if ($opt{l}) {
	$local_server = trim($opt{l});
	$include_refclocks = 1;
	}

my $x_axis_format = "hms";
my $x_axis_precision = "1";
$x_axis_label = "UTC";

if ($opt{m}) {
	$x_axis_format = "decimal";
	$x_axis_precision = "0";
	$x_axis_label = "MJD";
	}

my $pngfile;
$pngfile = trim($opt{p});

my $infile;
$infile = trim($opt{f});

# set up input file
open (INFILE, "$infile") || die "Can't open input file $infile!\n";

#----------


# Read the input file grabbing the server address
while ($reading=<INFILE>) {
	($day,$sec,$address,$status,$offset,$delay,$dispersion,$jitter) =
			split(/\s+/,$reading);
	push (@ips,$address);
	}
close INFILE;

# Create a list of the unique server addresses
%seen = ();
foreach $ipaddr (@ips) {
    push(@peer_list, $ipaddr) unless $seen{$ipaddr}++;

}

# Process the input file once for each peer adress found
foreach $ipaddr (@peer_list) {
	# Get the hostname
	$hostname = lc gethostbyaddr(inet_aton($ipaddr),AF_INET);
	
	# If no reverse match
	if ($hostname eq "") {
		# Check to see if it's a refclock
		@octet = split(/\./,$ipaddr);
		$refbase = $octet[0].".".$octet[1].".".$octet[2];
		if (exists $refclocks{$refbase}) {
			if (!$include_refclocks) {
				$ipaddr = "";
				$hostname = "";
				}
			else {
				$hostname = $refclocks{$refbase};
				if ($local_server) {
					$hostname .= "\." . $local_server;
					}
				}

			}

		}
	if ($hostname eq "") {
		$hostname = $ipaddr;
		}

### LOCAL HACK -- this addr has multiple cnames; force the one we want
if ($ipaddr eq "24.123.66.139") { $hostname = "meow.febo.com"; }
###

	$hostname = trim($hostname);

	# If there's an include list, and this host isn't on it, zero it out

	# if no @include, then include everything
	if (@include) { $found = 0 } else { $found = 1; }

	# if it matches, set the flag
	foreach $include(@include) {
		if ( $hostname =~ m/\Q$include\E/i ) { $found = 1; } }

	# if flag is not set, zero it out
	if (!$found) {
		$ipaddr = "";
		$hostname = "";
	}


	# Now test for excludes

	$found = 0;

	# if it matches, set the flag
	foreach $exclude(@exclude) {
		if ( $hostname =~ m/\Q$exclude\E/i ) { $found = 1; } }

	# if flag is set, zero it out
	if ($found) {
		$ipaddr = "";
		$hostname = "";
	}

	# Loop through the input file, dumping
	# all records for this server to a file called $hostname.dat
	if ($ipaddr) {
		push (@host_list,$hostname);
		$datafile = $work_dir . $hostname . ".dat";
		open (LOG, ">$datafile") || die "Can't open $datafile!\n";
		open (INFILE, "$infile") || die "Can't open $infile!\n";

		$count = $average;
		while ($reading=<INFILE>) {
			($day,$sec,$address,$status,$offset,$delay,
				$dispersion,$jitter) = split(/\s+/,$reading);
			if ( ($count == $average) && ($address eq $ipaddr) ) {
				# $day is MJD and $sec is seconds since UTC
				# midnight, so we need to subtract 1/2 day
				$mjd = $day + ($sec/86400) - 0.5;
				print LOG $mjd," ",$offset, $delay," ",
					" ",$dispersion," ",$jitter,"\n";
				if ($average) {$count = 1;}
				}
			elsif (($address eq $ipaddr) && ($average)) {$count++;}
			}
		close INFILE;
		close LOG;
		}
	}


# Sort the array of hosts
@host_list = sort (@host_list);

# Create a temporary file for the grace cmd file
do { $tmpfile = tmpnam() }
	until $outfile = IO::File->new($tmpfile, O_RDWR|O_CREAT|O_EXCL);
$infile = new IO::File $ntp_template, "r";

if (!$infile) {die "Couldn't open temporary file!\n";}

# Write the top part of the grace cmd file
while ($reading = <$infile>) {
	$reading =~ s/##TITLE##/$title/;
	$reading =~ s/##SUBTITLE##/$subtitle/;
	$reading =~ s/##XAXIS LABEL##/$x_axis_label/;
	$reading =~ s/##XAXIS FORMAT##/$x_axis_format/;
	$reading =~ s/##XAXIS PRECISION##/$x_axis_precision/;
	$reading =~ s/##YAXIS LABEL##/$y_axis_label/;
	print $outfile $reading;
	}
close $infile;
		
# Write the set data, one stanza for each server
$counter = 0;

foreach $ipaddr (@host_list) {
	$statsfile = $work_dir . $ipaddr . ".dat";
	$set = "s". $counter;
	$set_color = $counter+1;
	$legend = $ipaddr;

### LOCAL HACK -- append refclock source to my local stratum 1 servers
if ($legend =~ m/tick.febo.com/i) {
	$legend = "tick.febo.com (LORAN-C)";
}
if ($legend =~ m/tock.febo.com/i) {
	$legend = "tock.febo.com (Z3801A GPS)";
}
if ($legend =~ m/toe.febo.com/i) {
	$legend = "toe.febo.com (WWVB)";
}
if ($legend =~ m/pps.cesium.febo.com/i) {
	$legend = "cesium.febo.com (HP 5061A)";
}
###
	# Write the set information, one stanza per set
	print $outfile "    $set hidden false\n";
	print $outfile "    $set type xy\n";
	print $outfile "    $set line type 1\n";
	print $outfile "    $set line linestyle 1\n";
	print $outfile "    $set line linewidth 1.0\n";
	print $outfile "    $set line color $set_color\n";
	print $outfile "    $set line pattern 1\n";
	print $outfile "    $set legend  \"$legend\"\n";
	print $outfile "read  block pipe \"cat $statsfile\"\n";
	print $outfile "    target $set\n";
	print $outfile "    block xy \"1:2\"\n";
	print $outfile "    y=y*1000\n";
	print $outfile "    autoscale\n";

	$counter++;
	}

# Generate the graph
my @args = ($grace,"-nosafe","-b",$tmpfile,"-hdevice","PNG",
	"-hardcopy","-printfile",$pngfile);
system(@args) == 0 or die "system @args failed: $?";

unlink($tmpfile) or die "Couldn't unlink $tmpfile!";
