3 # Copyright (C) 2015 Network Time Foundation
6 # Original shell version:
7 # Copyright (C) 2014 Timothe Litt litt at acm dot org
9 # This script may be freely copied, used and modified providing that
10 # this notice and the copyright statement are included in all copies
11 # and derivative works. No warranty is offered, and use is entirely at
12 # your own risk. Bugfixes and improvements would be appreciated by the
17 use Digest::SHA qw(sha1_hex);
18 use File::Copy qw(move);
20 use Getopt::Long qw(:config auto_help no_ignore_case bundling);
25 # leap-seconds file manager/updater
27 # ########## Default configuration ##########
30 my $CRONJOB = $ENV{'CRONJOB'};
31 $CRONJOB = "" unless defined($CRONJOB);
36 # Where to get the file
37 my $LEAPSRC="ftp://time.nist.gov/pub/leap-seconds.list";
40 # How many times to try to download new file
44 # Where to find ntp config file
45 my $NTPCONF="/etc/ntp.conf";
47 # How long (in days) before expiration to get updated file
50 # How to restart NTP - older NTP: service ntpd? try-restart | condrestart
51 # Recent NTP checks for new file daily, so there's nothing to do
57 # Where to put temporary copy before it's validated
58 my $TMPFILE="/tmp/leap-seconds.$$.tmp";
63 # ###########################################
67 Usage: $0 [options] [leapfile]
69 Verifies and if necessary, updates leap-second definition file
71 All arguments are optional: Default (or current value) shown:
72 -s Specify the URL of the master copy to download
74 -d Specify the filename on the local system
76 -e Specify how long (in days) before expiration the file is to be
77 refreshed. Note that larger values imply more frequent refreshes.
79 -f Specify location of ntp.conf (used to make sure leapfile directive is
80 present and to default leapfile)
82 -F Force update even if current file is OK and not close to expiring.
83 -r Specify number of times to retry on get failure
85 -i Specify number of minutes between retries
87 -l Use syslog for output (Implied if CRONJOB is set)
88 -L Don't use syslog for output
89 -P Specify the syslog facility for logging
91 -t Name of temporary file used in validation
93 -q Only report errors to stdout
96 The following options are not (yet) implemented in the perl version:
99 -c Command to restart NTP after installing a new file
100 <none> - ntpd checks file daily
102 Prefer IPv4 or IPv6 (as specified) addresses, but use either
103 -z Specify path for utilities
105 -Z Only use system path
107 $0 will validate the file currently on the local system
109 Ordinarily, the file is found using the "leapfile" directive in $NTPCONF.
110 However, an alternate location can be specified on the command line.
112 If the file does not exist, is not valid, has expired, or is expiring soon,
113 a new copy will be downloaded. If the new copy validates, it is installed and
114 NTP is (optionally) restarted.
116 If the current file is acceptable, no download or restart occurs.
118 -c can also be used to invoke another script to perform administrative
119 functions, e.g. to copy the file to other local systems.
121 This can be run as a cron job. As the file is rarely updated, and leap
122 seconds are announced at least one month in advance (usually longer), it
123 need not be run more frequently than about once every three weeks.
125 For cron-friendly behavior, define CRONJOB=1 in the crontab.
130 # Default: Use syslog for logging if running under cron
132 my $SYSLOG = $CRONJOB;
154 $LOGFAC=$opt{P} if (defined($opt{P}));
155 $LEAPSRC=$opt{s} if (defined($opt{s}));
156 $PREFETCH=$opt{e} if (defined($opt{e}));
157 $NTPCONF=$opt{f} if (defined($opt{f}));
158 $FORCE="Y" if (defined($opt{F}));
159 $RESTART=$opt{c} if (defined($opt{c}));
160 $MAXTRIES=$opt{r} if (defined($opt{r}));
161 $INTERVAL=$opt{i} if (defined($opt{i}));
162 $TMPFILE=$opt{t} if (defined($opt{t}));
163 $SYSLOG="Y" if (defined($opt{l}));
164 $SYSLOG="" if (defined($opt{L}));
165 $QUIET="Y" if (defined($opt{q}));
166 $VERBOSE="Y" if (defined($opt{v}));
168 # export PATH="$PATHLIST$PATH"
172 openlog($0, 'pid', $LOGFAC);
175 my ($priority, $message) = @_;
177 # "priority" "message"
179 # Stdout unless syslog specified or logger isn't available
181 if ($SYSLOG eq "" or $LOGGER eq "") {
182 if ($QUIET ne "" and ( $priority eq "info" or $priority eq "notice" or $priority eq "debug" ) ) {
185 printf "%s: $message\n", uc $priority;
189 # Also log to stdout if cron job && notice or higher
190 if (($CRONJOB ne "" and ($priority ne "info" ) and ($priority ne "debug" )) || ($VERBOSE ne "")) {
191 # Log to stderr as well
192 print STDERR "$0: $priority: $message\n";
194 syslog($priority, $message);
198 # INTERVAL=$(( $INTERVAL *1 ))
200 # Validate a leap-seconds file checksum
202 # File format: (full description in files)
203 # # marks comments, except:
204 # #$ number : the NTP date of the last update
205 # #@ number : the NTP date that the file expires
206 # Date (seconds since 1900) leaps : leaps is the # of seconds to add for times >= Date
207 # Date lines have comments.
208 # #h hex hex hex hex hex is the SHA-1 checksum of the data & dates, excluding whitespace w/o leading zeroes
216 my ($file, $verbose) = @_;
222 # Remove comments, except those that are markers for last update,
225 unless (open(LF, $file)) {
226 warn "Can't open <$file>: $!\n";
227 print "Will try and create that file.\n";
241 $EXPIRES = $_ - 2208988800;
243 elsif (/^#h\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)/) {
246 $FSHA = sprintf("%08s%08s%08s%08s%08s", $1, $2, $3, $4, $5);
257 print "Unexpected line: <$_>\n";
262 # Remove all white space
265 # Compute the SHA hash of the data, removing the marker and filename
266 # Computed in binary mode, which shouldn't matter since whitespace has been removed
268 my $DSHA = sha1_hex($data);
270 # Extract the file's hash. Restore any leading zeroes in hash segments.
272 if ( ( "$FSHA" ne "" ) && ( $FSHA eq $DSHA ) ) {
273 if ( $verbose ne "" ) {
274 logger("info", "Checksum of $file validated");
277 logger("error", "Checksum of $file is invalid:");
278 $FSHA="(no checksum record found in file)"
280 logger("error", "EXPECTED: $FSHA");
281 logger("error", "COMPUTED: $DSHA");
285 # Check the expiration date, converting NTP epoch to Unix epoch used by date
287 if ( $EXPIRES < time() ) {
288 logger("notice", "File expired on " . gmtime($EXPIRES));
296 -r $NTPCONF || die "Missing ntp configuration: $NTPCONF\n";
298 # Parse ntp.conf for leapfile directive
300 open(LF, $NTPCONF) || die "Can't open <$NTPCONF>: $!\n";
303 if (/^ *leapfile\s+(\S+)/) {
309 -s $LEAPFILE || warn "$NTPCONF specifies $LEAPFILE as a leapfile, which is empty.\n";
311 # Allow placing the file someplace else - testing
313 if ( defined $ARGV[0] ) {
314 if ( $ARGV[0] ne $LEAPFILE ) {
315 logger("notice", "Requested install to $ARGV[0], but $NTPCONF specifies $LEAPFILE");
317 $LEAPFILE = $ARGV[0];
320 # Verify the current file
321 # If it is missing, doesn't validate or expired
322 # Or is expiring soon
325 if ( $FORCE ne "" || verifySHA($LEAPFILE, $VERBOSE) || ( $EXPIRES lt ( $PREFETCH * 86400 + time() ) )) {
327 my $ff = File::Fetch->new(uri => $LEAPSRC) || die "Fetch failed.\n";
330 logger("info", "Attempting download from $LEAPSRC, try $TRY..")
332 my $where = $ff->fetch( to => '/tmp' );
335 logger("info", "Download of $LEAPSRC succeeded");
337 if ( verifySHA($where, $VERBOSE )) {
338 # There is no point in retrying, as the file on the
339 # server is almost certainly corrupt.
341 logger("warning", "Downloaded file $where rejected -- saved for diagnosis");
345 # While the shell script version will set correct permissions
346 # on temporary file, for the perl version that's harder, so
347 # for now at least one should run this script as the
350 # REFFILE="$LEAPFILE"
351 # if [ ! -f $LEAPFILE ]; then
352 # logger "notice" "$LEAPFILE was missing, creating new copy - check permissions"
354 # # Can't copy permissions from old file, copy from NTPCONF instead
357 # chmod --reference $REFFILE $TMPFILE
358 # chown --reference $REFFILE $TMPFILE
359 # ( which selinuxenabled && selinuxenabled && which chcon ) >/dev/null 2>&1
360 # if [ $? == 0 ] ; then
361 # chcon --reference $REFFILE $TMPFILE
364 # Replace current file with validated new one
366 if ( move $where, $LEAPFILE ) {
367 logger("notice", "Installed new $LEAPFILE from $LEAPSRC");
369 logger("error", "Install $where => $LEAPFILE failed -- saved for diagnosis: $!");
373 # Restart NTP (or whatever else is specified)
375 if ( $RESTART ne "" ) {
376 if ( $VERBOSE ne "" ) {
377 logger("info", "Attempting restart action: $RESTART");
381 #R="$( 2>&1 $RESTART )"
382 #if [ $? -eq 0 ]; then
383 # logger "notice" "Restart action succeeded"
384 # if [ -n "$VERBOSE" -a -n "$R" ]; then
388 # logger "error" "Restart action failed"
389 # if [ -n "$R" ]; then
390 # logger "error" "$R"
398 # Failed to download. See about trying again
401 if ( $TRY ge $MAXTRIES ) {
404 if ( $VERBOSE ne "" ) {
405 logger("info", "Waiting $INTERVAL minutes before retrying...");
407 sleep $INTERVAL * 60 ;
410 # Failed and out of retries
412 logger("warning", "Download from $LEAPSRC failed after $TRY attempts");
416 print "FORCE is <$FORCE>\n";
417 print "verifySHA is " . verifySHA($LEAPFILE, "") . "\n";
418 print "EXPIRES <$EXPIRES> vs ". ( $PREFETCH * 86400 + time() ) . "\n";
420 logger("info", "Not time to replace $LEAPFILE");