2015-07-01 03:12:13 +00:00
|
|
|
#! @PATH_PERL@ -w
|
|
|
|
|
|
|
|
# Copyright (C) 2015 Network Time Foundation
|
|
|
|
# Author: Harlan Stenn
|
|
|
|
|
|
|
|
# Original shell version:
|
|
|
|
# Copyright (C) 2014 Timothe Litt litt at acm dot org
|
|
|
|
|
|
|
|
# This script may be freely copied, used and modified providing that
|
|
|
|
# this notice and the copyright statement are included in all copies
|
|
|
|
# and derivative works. No warranty is offered, and use is entirely at
|
|
|
|
# your own risk. Bugfixes and improvements would be appreciated by the
|
|
|
|
# author.
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
|
|
|
|
use Digest::SHA qw(sha1_hex);
|
|
|
|
use File::Copy qw(move);
|
|
|
|
use File::Fetch;
|
|
|
|
use Getopt::Long qw(:config auto_help no_ignore_case bundling);
|
|
|
|
use Sys::Syslog;
|
|
|
|
|
|
|
|
my $VERSION="1.003";
|
|
|
|
|
|
|
|
# leap-seconds file manager/updater
|
|
|
|
|
|
|
|
# ########## Default configuration ##########
|
|
|
|
#
|
|
|
|
|
|
|
|
my $CRONJOB = $ENV{'CRONJOB'};
|
|
|
|
$CRONJOB = "" unless defined($CRONJOB);
|
|
|
|
my $LOGGER;
|
|
|
|
my $QUIET = "";
|
|
|
|
my $VERBOSE = "";
|
|
|
|
|
|
|
|
# Where to get the file
|
2016-11-22 06:28:26 +00:00
|
|
|
# Choices:
|
|
|
|
# https://www.ietf.org/timezones/data/leap-seconds.list
|
|
|
|
# ftp://time.nist.gov/pub/leap-seconds.list
|
|
|
|
my $LEAPSRC="https://www.ietf.org/timezones/data/leap-seconds.list";
|
2015-07-01 03:12:13 +00:00
|
|
|
my $LEAPFILE;
|
|
|
|
|
|
|
|
# How many times to try to download new file
|
|
|
|
my $MAXTRIES=6;
|
|
|
|
my $INTERVAL=10;
|
|
|
|
|
|
|
|
# Where to find ntp config file
|
|
|
|
my $NTPCONF="/etc/ntp.conf";
|
|
|
|
|
|
|
|
# How long (in days) before expiration to get updated file
|
|
|
|
my $PREFETCH="60";
|
|
|
|
|
|
|
|
# How to restart NTP - older NTP: service ntpd? try-restart | condrestart
|
|
|
|
# Recent NTP checks for new file daily, so there's nothing to do
|
|
|
|
my $RESTART="";
|
|
|
|
|
|
|
|
my $EXPIRES;
|
|
|
|
my $FORCE = "";
|
|
|
|
|
|
|
|
# Where to put temporary copy before it's validated
|
|
|
|
my $TMPFILE="/tmp/leap-seconds.$$.tmp";
|
|
|
|
|
|
|
|
# Syslog facility
|
|
|
|
my $LOGFAC="daemon";
|
|
|
|
|
|
|
|
# ###########################################
|
|
|
|
|
|
|
|
=item update-leap
|
|
|
|
|
|
|
|
Usage: $0 [options] [leapfile]
|
|
|
|
|
|
|
|
Verifies and if necessary, updates leap-second definition file
|
|
|
|
|
|
|
|
All arguments are optional: Default (or current value) shown:
|
|
|
|
-s Specify the URL of the master copy to download
|
|
|
|
$LEAPSRC
|
|
|
|
-d Specify the filename on the local system
|
|
|
|
$LEAPFILE
|
|
|
|
-e Specify how long (in days) before expiration the file is to be
|
|
|
|
refreshed. Note that larger values imply more frequent refreshes.
|
|
|
|
"$PREFETCH"
|
|
|
|
-f Specify location of ntp.conf (used to make sure leapfile directive is
|
|
|
|
present and to default leapfile)
|
|
|
|
$NTPCONF
|
|
|
|
-F Force update even if current file is OK and not close to expiring.
|
|
|
|
-r Specify number of times to retry on get failure
|
|
|
|
$MAXTRIES
|
|
|
|
-i Specify number of minutes between retries
|
|
|
|
$INTERVAL
|
|
|
|
-l Use syslog for output (Implied if CRONJOB is set)
|
|
|
|
-L Don't use syslog for output
|
|
|
|
-P Specify the syslog facility for logging
|
|
|
|
$LOGFAC
|
|
|
|
-t Name of temporary file used in validation
|
|
|
|
$TMPFILE
|
|
|
|
-q Only report errors to stdout
|
|
|
|
-v Verbose output
|
|
|
|
|
|
|
|
The following options are not (yet) implemented in the perl version:
|
|
|
|
-4 Use only IPv4
|
|
|
|
-6 Use only IPv6
|
|
|
|
-c Command to restart NTP after installing a new file
|
|
|
|
<none> - ntpd checks file daily
|
|
|
|
-p 4|6
|
|
|
|
Prefer IPv4 or IPv6 (as specified) addresses, but use either
|
|
|
|
-z Specify path for utilities
|
|
|
|
$PATHLIST
|
|
|
|
-Z Only use system path
|
|
|
|
|
|
|
|
$0 will validate the file currently on the local system
|
|
|
|
|
|
|
|
Ordinarily, the file is found using the "leapfile" directive in $NTPCONF.
|
|
|
|
However, an alternate location can be specified on the command line.
|
|
|
|
|
|
|
|
If the file does not exist, is not valid, has expired, or is expiring soon,
|
|
|
|
a new copy will be downloaded. If the new copy validates, it is installed and
|
|
|
|
NTP is (optionally) restarted.
|
|
|
|
|
|
|
|
If the current file is acceptable, no download or restart occurs.
|
|
|
|
|
|
|
|
-c can also be used to invoke another script to perform administrative
|
|
|
|
functions, e.g. to copy the file to other local systems.
|
|
|
|
|
|
|
|
This can be run as a cron job. As the file is rarely updated, and leap
|
|
|
|
seconds are announced at least one month in advance (usually longer), it
|
|
|
|
need not be run more frequently than about once every three weeks.
|
|
|
|
|
|
|
|
For cron-friendly behavior, define CRONJOB=1 in the crontab.
|
|
|
|
|
|
|
|
Version $VERSION
|
|
|
|
=cut
|
|
|
|
|
|
|
|
# Default: Use syslog for logging if running under cron
|
|
|
|
|
|
|
|
my $SYSLOG = $CRONJOB;
|
|
|
|
|
|
|
|
# Parse options
|
|
|
|
|
|
|
|
our(%opt);
|
|
|
|
|
|
|
|
GetOptions(\%opt,
|
|
|
|
'c=s',
|
|
|
|
'e:60',
|
|
|
|
'F',
|
|
|
|
'f=s',
|
|
|
|
'i:10',
|
|
|
|
'L',
|
|
|
|
'l',
|
|
|
|
'P=s',
|
|
|
|
'q',
|
|
|
|
'r:6',
|
|
|
|
's=s',
|
|
|
|
't=s',
|
|
|
|
'v'
|
|
|
|
);
|
|
|
|
|
|
|
|
$LOGFAC=$opt{P} if (defined($opt{P}));
|
|
|
|
$LEAPSRC=$opt{s} if (defined($opt{s}));
|
|
|
|
$PREFETCH=$opt{e} if (defined($opt{e}));
|
|
|
|
$NTPCONF=$opt{f} if (defined($opt{f}));
|
|
|
|
$FORCE="Y" if (defined($opt{F}));
|
|
|
|
$RESTART=$opt{c} if (defined($opt{c}));
|
|
|
|
$MAXTRIES=$opt{r} if (defined($opt{r}));
|
|
|
|
$INTERVAL=$opt{i} if (defined($opt{i}));
|
|
|
|
$TMPFILE=$opt{t} if (defined($opt{t}));
|
|
|
|
$SYSLOG="Y" if (defined($opt{l}));
|
|
|
|
$SYSLOG="" if (defined($opt{L}));
|
|
|
|
$QUIET="Y" if (defined($opt{q}));
|
|
|
|
$VERBOSE="Y" if (defined($opt{v}));
|
|
|
|
|
|
|
|
# export PATH="$PATHLIST$PATH"
|
|
|
|
|
|
|
|
# Handle logging
|
|
|
|
|
|
|
|
openlog($0, 'pid', $LOGFAC);
|
|
|
|
|
|
|
|
sub logger {
|
|
|
|
my ($priority, $message) = @_;
|
|
|
|
|
|
|
|
# "priority" "message"
|
|
|
|
#
|
|
|
|
# Stdout unless syslog specified or logger isn't available
|
|
|
|
#
|
|
|
|
if ($SYSLOG eq "" or $LOGGER eq "") {
|
|
|
|
if ($QUIET ne "" and ( $priority eq "info" or $priority eq "notice" or $priority eq "debug" ) ) {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
printf "%s: $message\n", uc $priority;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Also log to stdout if cron job && notice or higher
|
|
|
|
if (($CRONJOB ne "" and ($priority ne "info" ) and ($priority ne "debug" )) || ($VERBOSE ne "")) {
|
|
|
|
# Log to stderr as well
|
|
|
|
print STDERR "$0: $priority: $message\n";
|
|
|
|
}
|
|
|
|
syslog($priority, $message);
|
|
|
|
}
|
|
|
|
|
|
|
|
# Verify interval
|
|
|
|
# INTERVAL=$(( $INTERVAL *1 ))
|
|
|
|
|
|
|
|
# Validate a leap-seconds file checksum
|
|
|
|
#
|
|
|
|
# File format: (full description in files)
|
|
|
|
# # marks comments, except:
|
|
|
|
# #$ number : the NTP date of the last update
|
|
|
|
# #@ number : the NTP date that the file expires
|
|
|
|
# Date (seconds since 1900) leaps : leaps is the # of seconds to add for times >= Date
|
|
|
|
# Date lines have comments.
|
|
|
|
# #h hex hex hex hex hex is the SHA-1 checksum of the data & dates, excluding whitespace w/o leading zeroes
|
|
|
|
#
|
|
|
|
# Returns:
|
|
|
|
# 0 File is valid
|
|
|
|
# 1 Invalid Checksum
|
|
|
|
# 2 Expired
|
|
|
|
|
|
|
|
sub verifySHA {
|
|
|
|
my ($file, $verbose) = @_;
|
|
|
|
|
|
|
|
my $raw = "";
|
|
|
|
my $data = "";
|
|
|
|
my $FSHA;
|
|
|
|
|
|
|
|
# Remove comments, except those that are markers for last update,
|
|
|
|
# expires and hash
|
|
|
|
|
|
|
|
unless (open(LF, $file)) {
|
|
|
|
warn "Can't open <$file>: $!\n";
|
|
|
|
print "Will try and create that file.\n";
|
|
|
|
return 1;
|
|
|
|
};
|
|
|
|
while (<LF>) {
|
|
|
|
if (/^#\$/) {
|
|
|
|
$raw .= $_;
|
|
|
|
s/^..//;
|
|
|
|
$data .= $_;
|
|
|
|
}
|
|
|
|
elsif (/^#\@/) {
|
|
|
|
$raw .= $_;
|
|
|
|
s/^..//;
|
|
|
|
$data .= $_;
|
|
|
|
s/\s+//g;
|
|
|
|
$EXPIRES = $_ - 2208988800;
|
|
|
|
}
|
|
|
|
elsif (/^#h\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)/) {
|
|
|
|
chomp;
|
|
|
|
$raw .= $_;
|
|
|
|
$FSHA = sprintf("%08s%08s%08s%08s%08s", $1, $2, $3, $4, $5);
|
|
|
|
}
|
|
|
|
elsif (/^#/) {
|
|
|
|
# ignore it
|
|
|
|
}
|
|
|
|
elsif (/^\d/) {
|
|
|
|
s/#.*$//;
|
|
|
|
$raw .= $_;
|
|
|
|
$data .= $_;
|
|
|
|
} else {
|
|
|
|
chomp;
|
|
|
|
print "Unexpected line: <$_>\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
close LF;
|
|
|
|
|
|
|
|
# Remove all white space
|
|
|
|
$data =~ s/\s//g;
|
|
|
|
|
|
|
|
# Compute the SHA hash of the data, removing the marker and filename
|
|
|
|
# Computed in binary mode, which shouldn't matter since whitespace has been removed
|
|
|
|
|
|
|
|
my $DSHA = sha1_hex($data);
|
|
|
|
|
|
|
|
# Extract the file's hash. Restore any leading zeroes in hash segments.
|
|
|
|
|
|
|
|
if ( ( "$FSHA" ne "" ) && ( $FSHA eq $DSHA ) ) {
|
|
|
|
if ( $verbose ne "" ) {
|
|
|
|
logger("info", "Checksum of $file validated");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logger("error", "Checksum of $file is invalid:");
|
|
|
|
$FSHA="(no checksum record found in file)"
|
|
|
|
if ( $FSHA eq "");
|
|
|
|
logger("error", "EXPECTED: $FSHA");
|
|
|
|
logger("error", "COMPUTED: $DSHA");
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check the expiration date, converting NTP epoch to Unix epoch used by date
|
|
|
|
|
|
|
|
if ( $EXPIRES < time() ) {
|
|
|
|
logger("notice", "File expired on " . gmtime($EXPIRES));
|
|
|
|
return 2;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Verify ntp.conf
|
|
|
|
|
|
|
|
-r $NTPCONF || die "Missing ntp configuration: $NTPCONF\n";
|
|
|
|
|
|
|
|
# Parse ntp.conf for leapfile directive
|
|
|
|
|
|
|
|
open(LF, $NTPCONF) || die "Can't open <$NTPCONF>: $!\n";
|
|
|
|
while (<LF>) {
|
|
|
|
chomp;
|
2016-11-22 06:28:26 +00:00
|
|
|
if (/^ *leapfile\s+"(\S+)"/) {
|
2015-07-01 03:12:13 +00:00
|
|
|
$LEAPFILE = $1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
close LF;
|
|
|
|
|
|
|
|
-s $LEAPFILE || warn "$NTPCONF specifies $LEAPFILE as a leapfile, which is empty.\n";
|
|
|
|
|
|
|
|
# Allow placing the file someplace else - testing
|
|
|
|
|
|
|
|
if ( defined $ARGV[0] ) {
|
|
|
|
if ( $ARGV[0] ne $LEAPFILE ) {
|
|
|
|
logger("notice", "Requested install to $ARGV[0], but $NTPCONF specifies $LEAPFILE");
|
|
|
|
}
|
|
|
|
$LEAPFILE = $ARGV[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
# Verify the current file
|
|
|
|
# If it is missing, doesn't validate or expired
|
|
|
|
# Or is expiring soon
|
|
|
|
# Download a new one
|
|
|
|
|
|
|
|
if ( $FORCE ne "" || verifySHA($LEAPFILE, $VERBOSE) || ( $EXPIRES lt ( $PREFETCH * 86400 + time() ) )) {
|
|
|
|
my $TRY = 0;
|
|
|
|
my $ff = File::Fetch->new(uri => $LEAPSRC) || die "Fetch failed.\n";
|
|
|
|
while (1) {
|
|
|
|
++$TRY;
|
|
|
|
logger("info", "Attempting download from $LEAPSRC, try $TRY..")
|
|
|
|
if ($VERBOSE ne "");
|
|
|
|
my $where = $ff->fetch( to => '/tmp' );
|
|
|
|
|
|
|
|
if ($where) {
|
|
|
|
logger("info", "Download of $LEAPSRC succeeded");
|
|
|
|
|
|
|
|
if ( verifySHA($where, $VERBOSE )) {
|
|
|
|
# There is no point in retrying, as the file on the
|
|
|
|
# server is almost certainly corrupt.
|
|
|
|
|
|
|
|
logger("warning", "Downloaded file $where rejected -- saved for diagnosis");
|
|
|
|
exit 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
# While the shell script version will set correct permissions
|
|
|
|
# on temporary file, for the perl version that's harder, so
|
|
|
|
# for now at least one should run this script as the
|
|
|
|
# appropriate user.
|
|
|
|
|
|
|
|
# REFFILE="$LEAPFILE"
|
|
|
|
# if [ ! -f $LEAPFILE ]; then
|
|
|
|
# logger "notice" "$LEAPFILE was missing, creating new copy - check permissions"
|
|
|
|
# touch $LEAPFILE
|
|
|
|
# # Can't copy permissions from old file, copy from NTPCONF instead
|
|
|
|
# REFFILE="$NTPCONF"
|
|
|
|
# fi
|
|
|
|
# chmod --reference $REFFILE $TMPFILE
|
|
|
|
# chown --reference $REFFILE $TMPFILE
|
|
|
|
# ( which selinuxenabled && selinuxenabled && which chcon ) >/dev/null 2>&1
|
|
|
|
# if [ $? == 0 ] ; then
|
|
|
|
# chcon --reference $REFFILE $TMPFILE
|
|
|
|
# fi
|
|
|
|
|
|
|
|
# Replace current file with validated new one
|
|
|
|
|
|
|
|
if ( move $where, $LEAPFILE ) {
|
|
|
|
logger("notice", "Installed new $LEAPFILE from $LEAPSRC");
|
|
|
|
} else {
|
|
|
|
logger("error", "Install $where => $LEAPFILE failed -- saved for diagnosis: $!");
|
|
|
|
exit 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Restart NTP (or whatever else is specified)
|
|
|
|
|
|
|
|
if ( $RESTART ne "" ) {
|
|
|
|
if ( $VERBOSE ne "" ) {
|
|
|
|
logger("info", "Attempting restart action: $RESTART");
|
|
|
|
}
|
|
|
|
|
|
|
|
# XXX
|
|
|
|
#R="$( 2>&1 $RESTART )"
|
|
|
|
#if [ $? -eq 0 ]; then
|
|
|
|
# logger "notice" "Restart action succeeded"
|
|
|
|
# if [ -n "$VERBOSE" -a -n "$R" ]; then
|
|
|
|
# logger "info" "$R"
|
|
|
|
# fi
|
|
|
|
#else
|
|
|
|
# logger "error" "Restart action failed"
|
|
|
|
# if [ -n "$R" ]; then
|
|
|
|
# logger "error" "$R"
|
|
|
|
# fi
|
|
|
|
# exit 2
|
|
|
|
#fi
|
|
|
|
}
|
|
|
|
exit 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Failed to download. See about trying again
|
|
|
|
|
|
|
|
# rm -f $TMPFILE
|
|
|
|
if ( $TRY ge $MAXTRIES ) {
|
|
|
|
last;
|
|
|
|
}
|
|
|
|
if ( $VERBOSE ne "" ) {
|
|
|
|
logger("info", "Waiting $INTERVAL minutes before retrying...");
|
|
|
|
}
|
|
|
|
sleep $INTERVAL * 60 ;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Failed and out of retries
|
|
|
|
|
|
|
|
logger("warning", "Download from $LEAPSRC failed after $TRY attempts");
|
|
|
|
exit 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
print "FORCE is <$FORCE>\n";
|
|
|
|
print "verifySHA is " . verifySHA($LEAPFILE, "") . "\n";
|
|
|
|
print "EXPIRES <$EXPIRES> vs ". ( $PREFETCH * 86400 + time() ) . "\n";
|
|
|
|
|
|
|
|
logger("info", "Not time to replace $LEAPFILE");
|
|
|
|
|
|
|
|
exit 0;
|
|
|
|
|
|
|
|
# EOF
|