3 # Copyright (C) 2014 Timothe Litt litt at acm dot org
5 # This script may be freely copied, used and modified providing that
6 # this notice and the copyright statement are included in all copies
7 # and derivative works. No warranty is offered, and use is entirely at
8 # your own risk. Bugfixes and improvements would be appreciated by the
13 # leap-seconds file manager/updater
16 # wget sed, tr, shasum, logger
18 # ########## Default configuration ##########
20 # Where to get the file
21 LEAPSRC="ftp://time.nist.gov/pub/leap-seconds.list"
23 # How many times to try to download new file
27 # Where to find ntp config file
30 # How long before expiration to get updated file
33 # How to restart NTP - older NTP: service ntpd? try-restart | condrestart
34 # Recent NTP checks for new file daily, so there's nothing to do
37 # Where to put temporary copy before it's validated
38 TMPFILE="/tmp/leap-seconds.$$.tmp"
42 # ###########################################
44 # Places to look for commands. Allows for CRON having path to
45 # old utilities on embedded systems
47 PATHLIST="/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:"
49 REQUIREDCMDS=" wget logger tr sed shasum"
53 function displayHelp {
55 Usage: $SELF [options] [leapfile]
57 Verifies and if necessary, updates leap-second definition file
59 All arguments are optional: Default (or current value) shown:
60 -s Specify the URL of the master copy to download
65 Prefer IPv4 or IPv6 (as specified) addresses, but use either
66 -d Specify the filename on the local system
68 -e Specify how long before expiration the file is to be refreshed
69 Units are required, e.g. "-e 60 days" Note that larger values
70 imply more frequent refreshes.
72 -f Specify location of ntp.conf (used to make sure leapfile directive is
73 present and to default leapfile)
75 -F Force update even if current file is OK and not close to expiring.
76 -c Command to restart NTP after installing a new file
77 <none> - ntpd checks file daily
78 -r Specify number of times to retry on get failure
80 -i Specify number of minutes between retries
82 -l Use syslog for output (Implied if CRONJOB is set)
83 -L Don't use syslog for output
84 -P Specify the syslog facility for logging
86 -t Name of temporary file used in validation
88 -q Only report errors to stdout
90 -z Specify path for utilities
92 -Z Only use system path
94 $SELF will validate the file currently on the local system
96 Ordinarily, the file is found using the "leapfile" directive in $NTPCONF.
97 However, an alternate location can be specified on the command line.
99 If the file does not exist, is not valid, has expired, or is expiring soon,
100 a new copy will be downloaded. If the new copy validates, it is installed and
101 NTP is (optionally) restarted.
103 If the current file is acceptable, no download or restart occurs.
105 -c can also be used to invoke another script to perform administrative
106 functions, e.g. to copy the file to other local systems.
108 This can be run as a cron job. As the file is rarely updated, and leap
109 seconds are announced at least one month in advance (usually longer), it
110 need not be run more frequently than about once every three weeks.
112 For cron-friendly behavior, define CRONJOB=1 in the crontab.
114 This script depends on$REQUIREDCMDS
121 # Default: Use syslog for logging if running under cron
125 if [ "$1" = "--help" ]; then
132 while getopts 46p:P:s:e:f:Fc:r:i:lLt:hqvz:Z opt; do
141 if [ "$OPTARG" = '4' -o "$OPTARG" = '6' ]; then
142 PREFER="--prefer-family=IPv$OPTARG"
144 echo "Invalid -p $OPTARG" >&2
198 echo "$SELF -h for usage" >&2
205 export PATH="$PATHLIST$PATH"
207 # Add to path to deal with embedded systems
209 for P in $REQUIREDCMDS ; do
210 if >/dev/null 2>&1 which "$P" ; then
213 [ "$P" = "logger" ] && continue
214 echo "FATAL: missing $P command, please install"
220 if ! LOGGER="`2>/dev/null which logger`" ; then
225 # "priority" "message"
227 # Stdout unless syslog specified or logger isn't available
229 if [ -z "$SYSLOG" -o -z "$LOGGER" ]; then
230 if [ -n "$QUIET" -a \( "$1" = "info" -o "$1" = "notice" -o "$1" = "debug" \) ]; then
233 echo "`echo \"$1\" | tr a-z A-Z`: $2"
237 # Also log to stdout if cron job && notice or higher
239 if [ -n "$CRONJOB" -a \( "$1" != "info" \) -a \( "$1" != "debug" \) ] || [ -n "$VERBOSE" ]; then
242 $LOGGER $S -t "$SELF[$$]" -p "$LOGFAC.$1" "$2"
246 INTERVAL=$(( $INTERVAL *1 ))
248 # Validate a leap-seconds file checksum
250 # File format: (full description in files)
251 # # marks comments, except:
252 # #$ number : the NTP date of the last update
253 # #@ number : the NTP date that the file expires
254 # Date (seconds since 1900) leaps : leaps is the # of seconds to add for times >= Date
255 # Date lines have comments.
256 # #h hex hex hex hex hex is the SHA-1 checksum of the data & dates, excluding whitespace w/o leading zeroes
260 if [ ! -f "$1" ]; then
264 # Remove comments, except those that are markers for last update, expires and hash
266 local RAW="`sed $1 -e'/^\\([0-9]\\|#[\$@h]\)/!d' -e'/^#[\$@h]/!s/#.*\$//g'`"
268 # Extract just the data, removing all whitespace
270 local DATA="`echo \"$RAW\" | sed -e'/^#h/d' -e's/^#[\$@]//g' | tr -d '[:space:]'`"
272 # Compute the SHA hash of the data, removing the marker and filename
273 # Computed in binary mode, which shouldn't matter since whitespace has been removed
274 # shasum comes in several flavors; a portable one is available in Perl (with Digest::SHA)
276 local DSHA="`echo -n \"$DATA\" | shasum | sed -e's/[? *].*$//'`"
278 # Extract the file's hash. Restore any leading zeroes in hash segments.
280 # The sed [] includes a tab (\t) and space; #h is followed by a tab and space
281 local FSHA="`echo \"$RAW\" | sed -e'/^#h/!d' -e's/^#h//' -e's/[ ] */ 0x/g'`"
282 FSHA=`printf '%08x%08x%08x%08x%08x' $FSHA`
284 if [ -n "$FSHA" -a \( "$FSHA" = "$DSHA" \) ]; then
286 log "info" "Checksum of $1 validated"
289 log "error" "Checksum of $1 is invalid:"
290 [ -z "$FSHA" ] && FSHA="(no checksum record found in file)"
291 log "error" "EXPECTED: $FSHA"
292 log "error" "COMPUTED: $DSHA"
296 # Check the expiration date, converting NTP epoch to Unix epoch used by date
298 EXPIRES="`echo \"$RAW\" | sed -e'/^#@/!d' -e's/^#@//' | tr -d '[:space:]'`"
299 EXPIRES="$(($EXPIRES - 2208988800 ))"
301 if [ $EXPIRES -lt `date -u +%s` ]; then
302 log "notice" "File expired on `date -u -d \"Jan 1, 1970 00:00:00 +0000 + $EXPIRES seconds\"`"
310 if ! [ -f "$NTPCONF" ]; then
311 log "critical" "Missing ntp configuration $NTPCONF"
315 # Parse ntp.conf for leapfile directive
317 LEAPFILE="`sed $NTPCONF -e'/^ *leapfile *.*$/!d' -e's/^ *leapfile *//'`"
318 if [ -z "$LEAPFILE" ]; then
319 log "error" "$NTPCONF does not specify a leapfile"
322 # Allow placing the file someplace else - testing
325 if [ "$1" != "$LEAPFILE" ]; then
326 log "notice" "Requested install to $1, but $NTPCONF specifies $LEAPFILE"
331 # Verify the current file
332 # If it is missing, doesn't validate or expired
333 # Or is expiring soon
336 if [ -n "$FORCE" ] || ! verifySHA $LEAPFILE "$VERBOSE" || [ $EXPIRES -lt `date -d "NOW + $PREFETCH" +%s` ] ; then
340 if [ -n "$VERBOSE" ]; then
341 log "info" "Attempting download from $LEAPSRC, try $TRY.."
343 if wget $PROTO $PREFER -o ${TMPFILE}.log $LEAPSRC -O $TMPFILE ; then
344 log "info" "Download of $LEAPSRC succeeded"
345 if [ -n "$VERBOSE" ]; then
349 if ! verifySHA $TMPFILE "$VERBOSE" ; then
350 # There is no point in retrying, as the file on the server is almost
353 log "warning" "Downloaded file $TMPFILE rejected -- saved for diagnosis"
360 # Set correct permissions on temporary file
363 if [ ! -f $LEAPFILE ]; then
364 log "notice" "$LEAPFILE was missing, creating new copy - check permissions"
366 # Can't copy permissions from old file, copy from NTPCONF instead
369 chmod --reference $REFFILE $TMPFILE
370 chown --reference $REFFILE $TMPFILE
371 ( which selinuxenabled && selinuxenabled && which chcon ) >/dev/null 2>&1
372 if [ $? == 0 ] ; then
373 chcon --reference $REFFILE $TMPFILE
376 # Replace current file with validated new one
378 if mv -f $TMPFILE $LEAPFILE ; then
379 log "notice" "Installed new $LEAPFILE from $LEAPSRC"
381 log "error" "Install $TMPFILE => $LEAPFILE failed -- saved for diagnosis"
385 # Restart NTP (or whatever else is specified)
387 if [ -n "$RESTART" ]; then
388 if [ -n "$VERBOSE" ]; then
389 log "info" "Attempting restart action: $RESTART"
391 R="$( 2>&1 $RESTART )"
392 if [ $? -eq 0 ]; then
393 log "notice" "Restart action succeeded"
394 if [ -n "$VERBOSE" -a -n "$R" ]; then
398 log "error" "Restart action failed"
408 # Failed to download. See about trying again
411 if [ $TRY -ge $MAXTRIES ]; then
414 if [ -n "$VERBOSE" ]; then
416 log "info" "Waiting $INTERVAL minutes before retrying..."
418 sleep $(( $INTERVAL * 60))
421 # Failed and out of retries
423 log "warning" "Download from $LEAPSRC failed after $TRY attempts"
424 if [ -f ${TMPFILE}.log ]; then
426 rm -f ${TMPFILE}.log $TMPFILE
430 log "info" "Not time to replace $LEAPFILE"