#!/usr/bin/perl -wT

# (c)2015 Rob Janssen
#
# Check NTP time server chrony. This plugin requires the chronyc utility to
# be installed on the system.
#
# $Id$
#

require 5.004;
use POSIX;
use strict;
use Getopt::Long;
use vars qw($opt_V $opt_h $opt_H $opt_w $opt_c $opt_r $verbose $PROGNAME);
use lib "/usr/lib/nagios/plugins"  ;
use utils qw($TIMEOUT %ERRORS &print_revision &support);

$PROGNAME="check_chrony.pl";

sub print_help ();
sub print_usage ();
sub fvalue ($$);

$ENV{'PATH'}='';
$ENV{'BASH_ENV'}='';
$ENV{'ENV'}='';

my $dflt_w = "20,4";
my $dflt_c = "40,9";
my $dflt_r = "1";

Getopt::Long::Configure('bundling');
GetOptions
  ("V"   => \$opt_V, "version"    => \$opt_V,
   "h"   => \$opt_h, "help"       => \$opt_h,
   "v" => \$verbose, "verbose"  => \$verbose,
   "w=s" => \$opt_w, "warning=s"  => \$opt_w,   # offset|adjust warning if above this number
   "c=s" => \$opt_c, "critical=s" => \$opt_c,   # offset|adjust critical if above this number
   "r=s" => \$opt_r, "rootdisp=s" => \$opt_c,   # rootdisp warning if above this number
   "H=s" => \$opt_H, "hostname=s" => \$opt_H);

if ($opt_V) {
  print_revision($PROGNAME,'$Revision: 1.7.PE1CHL $ ');
  exit $ERRORS{'OK'};
}

if ($opt_h) {
  print_help();
  exit $ERRORS{'OK'};
}

$opt_H = shift unless ($opt_H);
my $host = $1 if ($opt_H && $opt_H =~ m/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[a-zA-Z][-a-zA-Z0-9]+(\.[a-zA-Z][-a-zA-Z0-9]+)*)$/);
unless ($host) {
  print "No target host specified\n";
  print_usage();
  exit $ERRORS{'UNKNOWN'};
}

my $w_offset;
my $w_stratum;

($opt_w) || ($opt_w = shift) || ($opt_w = $dflt_w);
if ($opt_w =~ /([0-9]*\.?[0-9]*),([0-9]+)/) {
  $w_offset = $1;
  $w_stratum = $2;
} else {
  print "warning spec does not parse.\n";
  print_usage();
  exit $ERRORS{"UNKNOWN"};
}

my $c_offset;
my $c_stratum;

($opt_c) || ($opt_c = shift) || ($opt_c = $dflt_c);
if ($opt_c =~ /([0-9]*\.?[0-9]*),([0-9]+)/) {
  $c_offset = $1;
  $c_stratum = $2;
} else {
  print "critical spec does not parse.\n";
  print_usage();
  exit $ERRORS{"UNKNOWN"};
}

if ($c_offset < $w_offset) {
  print "Critical offset should be larger than warning offset\n";
  print_usage();
  exit $ERRORS{"UNKNOWN"};
}

if ($c_stratum < $w_stratum) {
  print "Critical stratum should be larger than warning stratum\n";
  print_usage();
  exit $ERRORS{"UNKNOWN"};
}

$opt_r = $dflt_r if (!defined $opt_r);

my $answer = undef;
my $refid = undef;
my $stratum = undef;
my $offset = undef;
my $frequency = undef;
my $rootdelay = undef;
my $rootdisp = undef;
my $leap = undef;

my $state = $ERRORS{'UNKNOWN'};
my $chronyc_error = $ERRORS{'UNKNOWN'};

# Just in case of problems, let's not hang Nagios
$SIG{'ALRM'} = sub {
  print ("ERROR: No response from ntp server (alarm)\n");
  exit $ERRORS{"UNKNOWN"};
};
alarm($TIMEOUT);

###
###
###

if (!open(CHRONYC, "/usr/local/bin/chronyc -h $host tracking 2>&1 |")) {
  print "Could not open chronyc: </usr/local/bin/chronyc -h $host tracking>\n";
  exit $ERRORS{"UNKNOWN"};
}

# Reference ID    : 50505300 (PPS)
# Stratum         : 1
# Ref time (UTC)  : Mon Oct 23 20:42:08 2017
# System time     : 0.000000769 seconds fast of NTP time
# Last offset     : -0.000000032 seconds
# RMS offset      : 0.000001465 seconds
# Frequency       : 5.850 ppm slow
# Residual freq   : +0.000 ppm
# Skew            : 0.028 ppm
# Root delay      : 0.000000001 seconds
# Root dispersion : 0.000024348 seconds
# Update interval : 16.0 seconds
# Leap status     : Normal

while (<CHRONYC>) {
  # print $_ if ($verbose);
  chomp;
  my @val = split / *: */;

  if (0) {
  } elsif ($val[0] eq "Reference ID") {
    $refid = $val[1];
    $refid =~ s/.*\((.*)\)/$1/;
    print "refid <$refid>\n" if ($verbose);
  } elsif ($val[0] eq "Stratum") {
    $stratum = $val[1];
    print "stratum <$stratum>\n" if ($verbose);
  } elsif ($val[0] eq "Last offset") {
    $offset = $val[1];
    $offset =~ s/ seconds.*//;
    print "offset <$offset>\n" if ($verbose);
  } elsif ($val[0] eq "Frequency") {
    $frequency = $val[1];
    $frequency = '-'.$frequency if ($frequency =~ /slow/);
    $frequency =~ s/ ppm.*//;
    print "frequency <$frequency>\n" if ($verbose);
  } elsif ($val[0] eq "Root delay") {
    $rootdelay = $val[1];
    $rootdelay =~ s/ seconds.*//;
    print "rootdelay <$rootdelay>\n" if ($verbose);
  } elsif ($val[0] eq "Root dispersion") {
    $rootdisp = $val[1];
    $rootdisp =~ s/ seconds.*//;
    print "rootdisp <$rootdisp>\n" if ($verbose);
  } elsif ($val[0] eq "Leap status") {
    $leap = $val[1];
    print "leap <$leap>\n" if ($verbose);
  }
}

close (CHRONYC);

# declare an error if we also get a non-zero return code from chronyc
# unless already set to critical

if ( $? ) {
  print "stderr = $? : $! \n" if $verbose;
  $chronyc_error = $ERRORS{'CRITICAL'};
} else {
  $chronyc_error = $ERRORS{'OK'};
}

if (($chronyc_error != $ERRORS{'OK'})) {
  $state = $chronyc_error;
  $answer = "Server for ntp probably down\n";
} else { # no error from chronyc
  $state = $ERRORS{'OK'};
  $answer = "";

  if (defined $refid) {
    $answer .= $refid." ";
  }

  if (defined $offset) {
    if (abs($offset) < $w_offset) {
      $answer .= sprintf("Offset %g,",$offset);
    } elsif (abs($offset) < $c_offset) {
      $state = $ERRORS{'WARNING'};
      $answer .= "Time difference $offset gt $w_offset,";
    } else {
      $state = $ERRORS{'CRITICAL'};
      $answer .= "Time difference $offset gt $c_offset,";
    }
  } else { # can't happen?
    $state = $ERRORS{'UNKNOWN'};
    $answer .= "No offset found from chronyc,";
  }

  if (defined $stratum) {
    if ($stratum < $w_stratum) {
      $answer .= "Stratum $stratum";
    } elsif ($stratum < $c_stratum) {
      $state = $ERRORS{'WARNING'} unless ($state == $ERRORS{'CRITICAL'});
      $answer .= "Stratum $stratum ge $w_stratum";
    } else {
      $state = $ERRORS{'CRITICAL'};
      $answer .= "Stratum $stratum ge $c_stratum";
    }
  } else { # can't happen?
    $state = $ERRORS{'UNKNOWN'};
    $answer .= "No stratum found from chronyc";
  }

  if (defined $rootdisp) {
    if ($rootdisp >= $opt_r) {
      $state = $ERRORS{'WARNING'} unless ($state == $ERRORS{'CRITICAL'});
      $answer .= ",Root dispersion $rootdisp ge $opt_r";
    }
  } else { # can't happen?
    $state = $ERRORS{'UNKNOWN'};
    $answer .= "No rootdisp found from chronyc";
  }

  if (defined $leap) {
    if ($leap ne "Normal") {
      $answer .= ",Leap $leap";
    }
  } else { # can't happen?
    $state = $ERRORS{'UNKNOWN'};
    $answer .= "No leap found from chronyc";
  }

  $answer .= " | ";
  $answer .= fvalue("offset=%s;",$offset) if (defined $offset);
  $answer .= sprintf("stratum=%s;",$stratum) if (defined $stratum);
  $answer .= fvalue("frequency=%s;",$frequency) if (defined $frequency);
  $answer .= fvalue("rootdelay=%s;",$rootdelay) if (defined $rootdelay);
  $answer .= fvalue("rootdisp=%s;",$rootdisp) if (defined $rootdisp);
  $answer .= "\n";
}

foreach my $key (keys %ERRORS) {
	if ($state==$ERRORS{$key}) {
		print ("$key: $answer");
		last;
	}
}
exit $state;

sub print_usage () {
  print "Usage: $PROGNAME -H <host> [-w <warn>] [-c <crit>] [-r <w_rootdisp>]\n";
  print " where:\n";
  print "\t-w defaults to ".$dflt_w."\n";
  print "\t-c defaults to ".$dflt_c."\n";
  print " and the values correspond to offset, stratum\n";
  print "\t-r defaults to ".$dflt_r."\n";
}

sub print_help () {
  print_revision($PROGNAME,'$Revision$');
  print "Copyright (c) 2015 Rob Janssen\n";
  print "\n";
  print_usage();
  print "\n";
  print "<warn> = Clock offset in secs at which a warning message will be generated.\n	Defaults to 60.\n";
  print "<crit> = Clock offset in secs at which a critical message will be generated.\n	Defaults to 120.\n\n";
  print "<w_rootdisp> = Root dispersion in secs at which a warning message will be generated.\n	Defaults to 1.\n\n";
  support();
}

sub fvalue ($$) {
  my $fmt = shift;
  my $val = shift;
  my $rv = sprintf("%.10f",$val);
  $rv =~ s/0*$//;
  $rv = 0 if ($rv eq "" or $rv eq "0.");
  $rv = sprintf($fmt,$rv);
  return $rv;
}
