1#! @PATH_PERL@ -w 2 3# Copyright (C) 2015 Network Time Foundation 4# Author: Harlan Stenn 5 6# Original shell version: 7# Copyright (C) 2014 Timothe Litt litt at acm dot org 8 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 13# author. 14 15use strict; 16 17use Digest::SHA qw(sha1_hex); 18use File::Copy qw(move); 19use File::Fetch; 20use Getopt::Long qw(:config auto_help no_ignore_case bundling); 21use Sys::Syslog; 22 23my $VERSION="1.003"; 24 25# leap-seconds file manager/updater 26 27# ########## Default configuration ########## 28# 29 30my $CRONJOB = $ENV{'CRONJOB'}; 31$CRONJOB = "" unless defined($CRONJOB); 32my $LOGGER; 33my $QUIET = ""; 34my $VERBOSE = ""; 35 36# Where to get the file 37my $LEAPSRC="ftp://time.nist.gov/pub/leap-seconds.list"; 38my $LEAPFILE; 39 40# How many times to try to download new file 41my $MAXTRIES=6; 42my $INTERVAL=10; 43 44# Where to find ntp config file 45my $NTPCONF="/etc/ntp.conf"; 46 47# How long (in days) before expiration to get updated file 48my $PREFETCH="60"; 49 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 52my $RESTART=""; 53 54my $EXPIRES; 55my $FORCE = ""; 56 57# Where to put temporary copy before it's validated 58my $TMPFILE="/tmp/leap-seconds.$$.tmp"; 59 60# Syslog facility 61my $LOGFAC="daemon"; 62 63# ########################################### 64 65=item update-leap 66 67Usage: $0 [options] [leapfile] 68 69Verifies and if necessary, updates leap-second definition file 70 71All arguments are optional: Default (or current value) shown: 72 -s Specify the URL of the master copy to download 73 $LEAPSRC 74 -d Specify the filename on the local system 75 $LEAPFILE 76 -e Specify how long (in days) before expiration the file is to be 77 refreshed. Note that larger values imply more frequent refreshes. 78 "$PREFETCH" 79 -f Specify location of ntp.conf (used to make sure leapfile directive is 80 present and to default leapfile) 81 $NTPCONF 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 84 $MAXTRIES 85 -i Specify number of minutes between retries 86 $INTERVAL 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 90 $LOGFAC 91 -t Name of temporary file used in validation 92 $TMPFILE 93 -q Only report errors to stdout 94 -v Verbose output 95 96The following options are not (yet) implemented in the perl version: 97 -4 Use only IPv4 98 -6 Use only IPv6 99 -c Command to restart NTP after installing a new file 100 <none> - ntpd checks file daily 101 -p 4|6 102 Prefer IPv4 or IPv6 (as specified) addresses, but use either 103 -z Specify path for utilities 104 $PATHLIST 105 -Z Only use system path 106 107$0 will validate the file currently on the local system 108 109Ordinarily, the file is found using the "leapfile" directive in $NTPCONF. 110However, an alternate location can be specified on the command line. 111 112If the file does not exist, is not valid, has expired, or is expiring soon, 113a new copy will be downloaded. If the new copy validates, it is installed and 114NTP is (optionally) restarted. 115 116If the current file is acceptable, no download or restart occurs. 117 118-c can also be used to invoke another script to perform administrative 119functions, e.g. to copy the file to other local systems. 120 121This can be run as a cron job. As the file is rarely updated, and leap 122seconds are announced at least one month in advance (usually longer), it 123need not be run more frequently than about once every three weeks. 124 125For cron-friendly behavior, define CRONJOB=1 in the crontab. 126 127Version $VERSION 128=cut 129 130# Default: Use syslog for logging if running under cron 131 132my $SYSLOG = $CRONJOB; 133 134# Parse options 135 136our(%opt); 137 138GetOptions(\%opt, 139 'c=s', 140 'e:60', 141 'F', 142 'f=s', 143 'i:10', 144 'L', 145 'l', 146 'P=s', 147 'q', 148 'r:6', 149 's=s', 150 't=s', 151 'v' 152 ); 153 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})); 167 168# export PATH="$PATHLIST$PATH" 169 170# Handle logging 171 172openlog($0, 'pid', $LOGFAC); 173 174sub logger { 175 my ($priority, $message) = @_; 176 177 # "priority" "message" 178 # 179 # Stdout unless syslog specified or logger isn't available 180 # 181 if ($SYSLOG eq "" or $LOGGER eq "") { 182 if ($QUIET ne "" and ( $priority eq "info" or $priority eq "notice" or $priority eq "debug" ) ) { 183 return 0 184 } 185 printf "%s: $message\n", uc $priority; 186 return 0; 187 } 188 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"; 193 } 194 syslog($priority, $message); 195} 196 197# Verify interval 198# INTERVAL=$(( $INTERVAL *1 )) 199 200# Validate a leap-seconds file checksum 201# 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 209# 210# Returns: 211# 0 File is valid 212# 1 Invalid Checksum 213# 2 Expired 214 215sub verifySHA { 216 my ($file, $verbose) = @_; 217 218 my $raw = ""; 219 my $data = ""; 220 my $FSHA; 221 222 # Remove comments, except those that are markers for last update, 223 # expires and hash 224 225 unless (open(LF, $file)) { 226 warn "Can't open <$file>: $!\n"; 227 print "Will try and create that file.\n"; 228 return 1; 229 }; 230 while (<LF>) { 231 if (/^#\$/) { 232 $raw .= $_; 233 s/^..//; 234 $data .= $_; 235 } 236 elsif (/^#\@/) { 237 $raw .= $_; 238 s/^..//; 239 $data .= $_; 240 s/\s+//g; 241 $EXPIRES = $_ - 2208988800; 242 } 243 elsif (/^#h\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)/) { 244 chomp; 245 $raw .= $_; 246 $FSHA = sprintf("%08s%08s%08s%08s%08s", $1, $2, $3, $4, $5); 247 } 248 elsif (/^#/) { 249 # ignore it 250 } 251 elsif (/^\d/) { 252 s/#.*$//; 253 $raw .= $_; 254 $data .= $_; 255 } else { 256 chomp; 257 print "Unexpected line: <$_>\n"; 258 } 259 } 260 close LF; 261 262 # Remove all white space 263 $data =~ s/\s//g; 264 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 267 268 my $DSHA = sha1_hex($data); 269 270 # Extract the file's hash. Restore any leading zeroes in hash segments. 271 272 if ( ( "$FSHA" ne "" ) && ( $FSHA eq $DSHA ) ) { 273 if ( $verbose ne "" ) { 274 logger("info", "Checksum of $file validated"); 275 } 276 } else { 277 logger("error", "Checksum of $file is invalid:"); 278 $FSHA="(no checksum record found in file)" 279 if ( $FSHA eq ""); 280 logger("error", "EXPECTED: $FSHA"); 281 logger("error", "COMPUTED: $DSHA"); 282 return 1; 283 } 284 285 # Check the expiration date, converting NTP epoch to Unix epoch used by date 286 287 if ( $EXPIRES < time() ) { 288 logger("notice", "File expired on " . gmtime($EXPIRES)); 289 return 2; 290 } 291 return 0; 292} 293 294# Verify ntp.conf 295 296-r $NTPCONF || die "Missing ntp configuration: $NTPCONF\n"; 297 298# Parse ntp.conf for leapfile directive 299 300open(LF, $NTPCONF) || die "Can't open <$NTPCONF>: $!\n"; 301while (<LF>) { 302 chomp; 303 if (/^ *leapfile\s+(\S+)/) { 304 $LEAPFILE = $1; 305 } 306} 307close LF; 308 309-s $LEAPFILE || warn "$NTPCONF specifies $LEAPFILE as a leapfile, which is empty.\n"; 310 311# Allow placing the file someplace else - testing 312 313if ( defined $ARGV[0] ) { 314 if ( $ARGV[0] ne $LEAPFILE ) { 315 logger("notice", "Requested install to $ARGV[0], but $NTPCONF specifies $LEAPFILE"); 316 } 317 $LEAPFILE = $ARGV[0]; 318} 319 320# Verify the current file 321# If it is missing, doesn't validate or expired 322# Or is expiring soon 323# Download a new one 324 325if ( $FORCE ne "" || verifySHA($LEAPFILE, $VERBOSE) || ( $EXPIRES lt ( $PREFETCH * 86400 + time() ) )) { 326 my $TRY = 0; 327 my $ff = File::Fetch->new(uri => $LEAPSRC) || die "Fetch failed.\n"; 328 while (1) { 329 ++$TRY; 330 logger("info", "Attempting download from $LEAPSRC, try $TRY..") 331 if ($VERBOSE ne ""); 332 my $where = $ff->fetch( to => '/tmp' ); 333 334 if ($where) { 335 logger("info", "Download of $LEAPSRC succeeded"); 336 337 if ( verifySHA($where, $VERBOSE )) { 338 # There is no point in retrying, as the file on the 339 # server is almost certainly corrupt. 340 341 logger("warning", "Downloaded file $where rejected -- saved for diagnosis"); 342 exit 1; 343 } 344 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 348 # appropriate user. 349 350 # REFFILE="$LEAPFILE" 351 # if [ ! -f $LEAPFILE ]; then 352 # logger "notice" "$LEAPFILE was missing, creating new copy - check permissions" 353 # touch $LEAPFILE 354 # # Can't copy permissions from old file, copy from NTPCONF instead 355 # REFFILE="$NTPCONF" 356 # fi 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 362 # fi 363 364 # Replace current file with validated new one 365 366 if ( move $where, $LEAPFILE ) { 367 logger("notice", "Installed new $LEAPFILE from $LEAPSRC"); 368 } else { 369 logger("error", "Install $where => $LEAPFILE failed -- saved for diagnosis: $!"); 370 exit 1; 371 } 372 373 # Restart NTP (or whatever else is specified) 374 375 if ( $RESTART ne "" ) { 376 if ( $VERBOSE ne "" ) { 377 logger("info", "Attempting restart action: $RESTART"); 378 } 379 380# XXX 381 #R="$( 2>&1 $RESTART )" 382 #if [ $? -eq 0 ]; then 383 # logger "notice" "Restart action succeeded" 384 # if [ -n "$VERBOSE" -a -n "$R" ]; then 385 # logger "info" "$R" 386 # fi 387 #else 388 # logger "error" "Restart action failed" 389 # if [ -n "$R" ]; then 390 # logger "error" "$R" 391 # fi 392 # exit 2 393 #fi 394 } 395 exit 0; 396 } 397 398 # Failed to download. See about trying again 399 400 # rm -f $TMPFILE 401 if ( $TRY ge $MAXTRIES ) { 402 last; 403 } 404 if ( $VERBOSE ne "" ) { 405 logger("info", "Waiting $INTERVAL minutes before retrying..."); 406 } 407 sleep $INTERVAL * 60 ; 408 } 409 410 # Failed and out of retries 411 412 logger("warning", "Download from $LEAPSRC failed after $TRY attempts"); 413 exit 1; 414} 415 416print "FORCE is <$FORCE>\n"; 417print "verifySHA is " . verifySHA($LEAPFILE, "") . "\n"; 418print "EXPIRES <$EXPIRES> vs ". ( $PREFETCH * 86400 + time() ) . "\n"; 419 420logger("info", "Not time to replace $LEAPFILE"); 421 422exit 0; 423 424# EOF 425