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