#!/usr/bin/perl
#
#   saneDate.pl  or  check.saneDate.hourly (in my cron.hourly file)
#
# Program to do a sanity check on the local clock's time and date.               
# Many if not most Unix based Web sites use ntp to synchronize their
# clock with that of a stratum 2 time server. ntp does a good job, 
# but if for some reason, your clock differs significantly (about
# one hour), ntp will stop functioning, and on my machine, it even
# exits, thus leaving you with an unsynchronized clock. This          
# typically happens during spring and fall daylight savings time
# changes. This program is meant to correct those situations and
# ensure that ntp is running and that your clock stays within   
# reasonable sync with the external servers.
        
#
# This is my first Perl program, so if you have any comments
# don't hesitate to let me know how I can become more adept at Perl.
#
#      Kern E. Sibbald, April 1999
#
#      Version 3, 7 June 1999
#
#  This program is in the public domain. 
#
# Assumptions:
#  1. Perl is in /usr/bin/perl (if not change the first line)
#  2. You are running NTP (tested with xntp3-5.93-4 and xntp3-5.93-12)
#  3. The program ntpq is available
#  4. The program ntpdate is available
#  5. This Perl program is run once an hour
#  6. This program always restarts NTP if it thinks it is not running.
#  7. This program leaves files in the /tmp directory when it detects
#     a problem. When all is OK, they are removed. If you automatically
#     remove files from /tmp, you may want to put these files elsewhere.
#     If they are removed, the worst that will happen is that you will
#     miss sync warning messages, and you may get more email than
#     you expected.

#
# Changes for Version 3 -- 7 June 99:
#  - added better error handling if network is down
#  - added separate variables for the directory for ntpdate and ntpq
#  - added check that config for ntpdate, ntpq, and ntp.conf are correct.
#
# Changes for Version 2 -- 12 May 99:
#  - added code to get clock sites from ntp.conf
#
# Definitions which should be carefully checked and 
# possibly changed for each site
#
#
#   if read_ntp_conf is set to one, this program will attempt to 
#   read the file in $ntp_conf and build a list of servers to be
#   used instead of the hardcoded $clocksites. If no servers are
#   found, then $clocksites is used.

    $read_ntp_conf = 1;

#   ntp_conf is the location of your ntp configuration file.
    $ntp_conf = "/etc/ntp.conf";

#   clocksites is the list of sites to be used for synchronization
#   it probably should be the same as the sites in your ntp.conf file.
#   Note, if read_ntp_conf is set, this program will attempt to build
#   a list from your ntp configuration file.
 
    $clocksites = "bernina.ethz.ch fartein.ifi.uio.no ntp0.strath.ac.uk";

#   ntpdatedir is the directory where ntpdate is located
#   It needs a trailing slash
    $ntpdatedir = "/usr/sbin/";

#   ntpqdir is the directory where ntpq is located
    $ntpqdir    = "/usr/sbin/";

#   ntpstop is the name of a command/script that will stop the ntp daemon

    $ntpstop = "/etc/rc.d/init.d/xntpd stop";

#   ntpstart is the name of a command/script that will start the ntp daemon

    $ntpstart = "/etc/rc.d/init.d/xntpd start";

#
# Definitions which might need changing for each site
#
#   Report_NTP_not_syncd is the number of syncs after which this program
#   will report that NTP is not synchronized. It appears to be normal that
#   NTP goes out of sync occassionally. Consequently, we only report
#   when successive executions of this program find NTP out of sync.

    $Report_NTP_not_syncd = "2";                # no. nonsyncs after which reported

#   if reset_clock_OK is nonzero, this program will attempt to reset the
#   clock if it is more than max_clock_offset seconds from the reference
#   site(s). In attempting to reset the clock, this program will
#   stop NTP then do the reset, then restart NTP. If reset_clock_OK
#   is zero, this program will not stop NTP, but if NTP is stopped,
#   is will attept to restart it.        

    $reset_clock_OK = "1";                      # set if you want clock reset

#   set max_clock_offset to the maximum offset in seconds that you 
#   tolerate between the local clock and the reference site(s).
#   This is a decimal number (e.g. 1.5 is OK too).

    $max_clock_offset = "5";                    # max clock offset in seconds  

#   To avoid the possibility of being innundated by error messages,
#   set maxmail to the maximum number of mail messages that
#   you want to receive during a problem period. Once NTP is running
#   and the clock is synchronized, new mail will be sent on any
#   subsequent problem.

    $maxmail = 10;                              # max mail messages

#   set mailto to the address where you want the mail messages sent

    $mailto = "root";                           # where to mail reports

#   The following two files need to remain on the system during 
#   an error with NTP. They will be deleted after the error is 
#   resolved.
    $mailcountf = "/tmp/saneDate.mailcount";    # mail sent count
    $nosynccountf = "/tmp/saneDate.nosynccount"; # no synch count file


# Utility definitions -- normally should not need to be changed

    $mailprog = "/bin/mail";                    # location of mail
    $dateoutf = "/tmp/saneDate.date";           # temp program output 
    $outf      = "/tmp/saneDate.output";        # temp mail output file
    $datefailmsg = "ntpdate command failed\: saneDate cannot do date-time sanity check\n";
    $ntpqcmd = $ntpqdir . "ntpq -p";            # ntp query
    $ntpdatequerycmd = $ntpdatedir . "ntpdate -q";  # remote date query
    $ntpdatesetcmd = $ntpdatedir . "ntpdate ";      # reset date


# return difference in seconds between local clock and remote clock(s).
sub clockdiff {
     $notdebugging = 1;
     if ($notdebugging) {
         # issue ntpdate -q date/time query command
         if (system("$ntpdatequerycmd $clocksites 2>$dateoutf >$dateoutf")) {
             return 1;                          # error
         }
     }
    # read output from ntpdate -q command
    open(CONS, "cat $dateoutf |");
    @cons = ;
    close(CONS);
    if ($#cons < 1) {                           # must have at least two lines
        return 1;                               # nope error
    }
    $rdateo = $cons[$#cons-1];
    chop($rdateo);
    @ntpdate = split ' ', $rdateo;
    if ($ntpdate[0] ne "server") {              # second to last line starts with server
        return 1;                               # nope error
    }
    $dateo = $cons[$#cons];
    chop($dateo);
    @ntpdate = split ' ', $dateo;
    if ($ntpdate[8] ne "offset") {
        return 1;                               # error
    }
    # offset is of form (-)111.2222 in seconds
    $offset = $ntpdate[9];                      # get time difference in secs
    return 0;                                   # OK

}

    (-e ($ntpdatedir . "/ntpdate")) or die "Configuration problem: ntpdate program not found\n";
    (-e ($ntpqdir . "/ntpq")) or die "Configuration problem: ntpq program not found\n";
    (-e $ntp_conf) or die "Configuration problem: ntp.conf not found\n";

    # get count of mail already sent
    if (-e $mailcountf) {
       $mailcount = `cat $mailcountf`;
    } else {
       $mailcount = 0;
    }
    # get count of times NTP not synced
    if (-e $nosynccountf) {
       $nosynccount = `cat $nosynccountf`;
    } else {
       $nosynccount = 0;
    }

    $me = `hostname`;                           # Host running this program
    chop($me);
    #take only machine name: machine.domain.top
    $me=~ s/^(.*?)\..*$/$1/;

    $clksites = "";
   # if so configured, read the ntp.conf file and pickup the list
   # of clocksites.
    if ($read_ntp_conf) { # read sites from ntp.conf file
        open(CONF, $ntp_conf);
        @conf = ;
        close(CONF);
        for ($i = 0; $i <= $#conf; $i++) {
            if (substr($conf[$i], 0, 6) eq "server") {
                @words = split ' ', $conf[$i];
                $clksites = $clksites . $words[1] . " ";
            }
        }
        if ($clksites ne "") {
            $clocksites = $clksites;
        }
    }

    # get offset between local clock and reference site(s)
    if (clockdiff()) {
        # some error occurred
        if ($mailcount < $maxmail) {
            ++$mailcount;
            `echo $mailcount >$mailcountf`;
            if ($mailcount == $maxmail) {
                $subject = "\"" . $me . " ntpdate failure - Last warning\"";
            } else {
                $subject = "\"" . $me . " ntpdate failure\"";
            }
            `echo $datefailmsg >$outf`;
            `echo $ntpdatequerycmd $clocksites >>$outf`;
            `cat $dateoutf >>$outf`;
            `cat $outf | $mailprog -s $subject $mailto`;
        }
        `rm -f $outf`;
        `rm -f $dateoutf`;
        exit 0;
    }

    $ntp_running = 0;
    $notsyncd = 1;
    $clock_reset = 0;
    $ntp_stopped = 0;
    $oldoffset = $offset;

    # check if local NTP is running and is synchronized
    `rm -f $dateoutf`;
    `$ntpqcmd 2>$outf >$dateoutf`;
     open(CONS, "cat $dateoutf |");
     @cons = ;
     close(CONS);
     # if we get no lines (-1), NTP is probably not running
     if ($#cons != -1) {
        $ntp_running = 1;
        for ($i = 0; $i <= $#cons; $i++) {
            if (substr($cons[$i], 0, 1) eq "*") {
                $notsyncd = 0;                  # we've got synchronization
                last;
            }
        }
        if ($notsyncd) {
            # NTP is running but is not synchronized
            ++$nosynccount;
            `echo $nosynccount >$nosynccountf`;
            if ($nosynccount >= $Report_NTP_not_syncd) {
                if ($mailcount < $maxmail) {
                    ++$mailcount;
                    `echo $mailcount >$mailcountf`;
                    if ($mailcount == $maxmail) {
                        $subject = "\"" . $me . " NTP not synchronized - Last warning\"";
                    } else {
                        $subject = "\"" . $me . " NTP not synchronized\"";
                    }
                    `echo "NTP is not syncronized\n" >$outf`;
                    $msg = "\"Clock offset is: " . $offset . " seconds\n\"";
                    `echo $msg >>$outf`;
                    `cat $dateoutf >>$outf`;
                    `cat $outf | $mailprog -s $subject $mailto`;
                    `rm -f $outf`;
                }
            }
        } else {
            # NTP is synchronized, remove nosync file
            `rm -f $nosynccountf`;
        }
     }
     if (abs($offset) > abs($max_clock_offset)) { # we want to do something
         if ($reset_clock_OK) {
             # note, eventhough we may believe the NTP is not running
             # we stop it anyway, if configured to do so
             `$ntpstop >$outf`;                 # stop NTP
             $ntp_stopped = 1;
         }
         if ($reset_clock_OK) {
             # reset clock
             `$ntpdatesetcmd $clocksites >$dateoutf`;
              $clock_reset = 1;
         }
     }
     if (!$ntp_running or $ntp_stopped) {
         `$ntpstart >$outf`;                    # restart NTP
     }
     if (($clock_reset or !$ntp_running) and ($mailcount < $maxmail)) {
         $msg = "\"NTP problem on " . $me . "\n\"";
         `echo $msg >$outf`;
         if (!$ntp_running) {
             `echo "NTP was not running." >>$outf`;
         }
        $msg = "\"Clock offset for " . $me . " is: " . $offset . " seconds\n\"";
         `echo $msg >>$outf`;
         if ($ntp_stopped) {
             `echo "NTP stopped.\n" >>$outf`;
         }
         if ($clock_reset) {
             `echo "Clock reset.\n" >>$outf`;
             `cat $dateoutf >>$outf`;
             `echo "\n" >>$outf`;
         }
         if (!$ntp_running or $ntp_stopped) {
             `echo "NTP restarted.\n" >>$outf`;
         }
         if ($clock_reset and !clockdiff()) {
            $msg = "\"New clock offset for " . $me . " is: " . $offset . " seconds\n\"";
            `echo $msg >>$outf`;
         }     
         ++$mailcount;
         `echo $mailcount >$mailcountf`;
         if ($mailcount == $maxmail) {
             $subject = "\"" . $me . " NTP problem - Last warning\"";
         } else {
             $subject = "\"" . $me . " NTP problem\"";
         }
         `cat $outf | $mailprog -s $subject $mailto`;
     }
     # cleanup
     `rm -f $outf`;
     `rm -f $dateoutf`;
     if (!$notsyncd and $ntp_running and !$clock_reset and (abs($oldoffset) <= abs($max_clock_offset))) {
         # all is OK, so blowaway any mail count file
         `rm -f $mailcountf`;
     }