1#!/bin/bash 2 3# Copyright (C) 2014 Timothe Litt litt at acm dot org 4 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 9# author. 10 11VERSION="1.003" 12 13# leap-seconds file manager/updater 14 15# Depends on: 16# wget sed, tr, shasum, logger 17 18# ########## Default configuration ########## 19# 20# Where to get the file 21LEAPSRC="ftp://time.nist.gov/pub/leap-seconds.list" 22 23# How many times to try to download new file 24MAXTRIES=6 25INTERVAL=10 26 27# Where to find ntp config file 28NTPCONF=/etc/ntp.conf 29 30# How long before expiration to get updated file 31PREFETCH="60 days" 32 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 35RESTART= 36 37# Where to put temporary copy before it's validated 38TMPFILE="/tmp/leap-seconds.$$.tmp" 39 40# Syslog facility 41LOGFAC=daemon 42# ########################################### 43 44# Places to look for commands. Allows for CRON having path to 45# old utilities on embedded systems 46 47PATHLIST="/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:" 48 49REQUIREDCMDS=" wget logger tr sed shasum" 50 51SELF="`basename $0`" 52 53function displayHelp { 54 cat <<EOF 55Usage: $SELF [options] [leapfile] 56 57Verifies and if necessary, updates leap-second definition file 58 59All arguments are optional: Default (or current value) shown: 60 -s Specify the URL of the master copy to download 61 $LEAPSRC 62 -4 Use only IPv4 63 -6 Use only IPv6 64 -p 4|6 65 Prefer IPv4 or IPv6 (as specified) addresses, but use either 66 -d Specify the filename on the local system 67 $LEAPFILE 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. 71 "$PREFETCH" 72 -f Specify location of ntp.conf (used to make sure leapfile directive is 73 present and to default leapfile) 74 $NTPCONF 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 79 $MAXTRIES 80 -i Specify number of minutes between retries 81 $INTERVAL 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 85 $LOGFAC 86 -t Name of temporary file used in validation 87 $TMPFILE 88 -q Only report errors to stdout 89 -v Verbose output 90 -z Specify path for utilities 91 $PATHLIST 92 -Z Only use system path 93 94$SELF will validate the file currently on the local system 95 96Ordinarily, the file is found using the "leapfile" directive in $NTPCONF. 97However, an alternate location can be specified on the command line. 98 99If the file does not exist, is not valid, has expired, or is expiring soon, 100a new copy will be downloaded. If the new copy validates, it is installed and 101NTP is (optionally) restarted. 102 103If the current file is acceptable, no download or restart occurs. 104 105-c can also be used to invoke another script to perform administrative 106functions, e.g. to copy the file to other local systems. 107 108This can be run as a cron job. As the file is rarely updated, and leap 109seconds are announced at least one month in advance (usually longer), it 110need not be run more frequently than about once every three weeks. 111 112For cron-friendly behavior, define CRONJOB=1 in the crontab. 113 114This script depends on$REQUIREDCMDS 115 116Version $VERSION 117EOF 118 return 0 119} 120 121# Default: Use syslog for logging if running under cron 122 123SYSLOG="$CRONJOB" 124 125if [ "$1" = "--help" ]; then 126 displayHelp 127 exit 0 128fi 129 130# Parse options 131 132while getopts 46p:P:s:e:f:Fc:r:i:lLt:hqvz:Z opt; do 133 case $opt in 134 4) 135 PROTO="-4" 136 ;; 137 6) 138 PROTO="-6" 139 ;; 140 p) 141 if [ "$OPTARG" = '4' -o "$OPTARG" = '6' ]; then 142 PREFER="--prefer-family=IPv$OPTARG" 143 else 144 echo "Invalid -p $OPTARG" >&2 145 exit 1; 146 fi 147 ;; 148 P) 149 LOGFAC="$OPTARG" 150 ;; 151 s) 152 LEAPSRC="$OPTARG" 153 ;; 154 e) 155 PREFETCH="$OPTARG" 156 ;; 157 f) 158 NTPCONF="$OPTARG" 159 ;; 160 F) 161 FORCE="Y" 162 ;; 163 c) 164 RESTART="$OPTARG" 165 ;; 166 r) 167 MAXTRIES="$OPTARG" 168 ;; 169 i) 170 INTERVAL="$OPTARG" 171 ;; 172 t) 173 TMPFILE="$OPTARG" 174 ;; 175 l) 176 SYSLOG="y" 177 ;; 178 L) 179 SYSLOG= 180 ;; 181 h) 182 displayHelp 183 exit 0 184 ;; 185 q) 186 QUIET="Y" 187 ;; 188 v) 189 VERBOSE="Y" 190 ;; 191 z) 192 PATHLIST="$OPTARG:" 193 ;; 194 Z) 195 PATHLIST= 196 ;; 197 *) 198 echo "$SELF -h for usage" >&2 199 exit 1 200 ;; 201 esac 202done 203shift $((OPTIND-1)) 204 205export PATH="$PATHLIST$PATH" 206 207# Add to path to deal with embedded systems 208# 209for P in $REQUIREDCMDS ; do 210 if >/dev/null 2>&1 which "$P" ; then 211 continue 212 fi 213 [ "$P" = "logger" ] && continue 214 echo "FATAL: missing $P command, please install" 215 exit 1 216done 217 218# Handle logging 219 220if ! LOGGER="`2>/dev/null which logger`" ; then 221 LOGGER= 222fi 223 224function log { 225 # "priority" "message" 226 # 227 # Stdout unless syslog specified or logger isn't available 228 # 229 if [ -z "$SYSLOG" -o -z "$LOGGER" ]; then 230 if [ -n "$QUIET" -a \( "$1" = "info" -o "$1" = "notice" -o "$1" = "debug" \) ]; then 231 return 0 232 fi 233 echo "`echo \"$1\" | tr a-z A-Z`: $2" 234 return 0 235 fi 236 237 # Also log to stdout if cron job && notice or higher 238 local S 239 if [ -n "$CRONJOB" -a \( "$1" != "info" \) -a \( "$1" != "debug" \) ] || [ -n "$VERBOSE" ]; then 240 S="-s" 241 fi 242 $LOGGER $S -t "$SELF[$$]" -p "$LOGFAC.$1" "$2" 243} 244 245# Verify interval 246INTERVAL=$(( $INTERVAL *1 )) 247 248# Validate a leap-seconds file checksum 249# 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 257 258function verifySHA { 259 260 if [ ! -f "$1" ]; then 261 return 1 262 fi 263 264 # Remove comments, except those that are markers for last update, expires and hash 265 266 local RAW="`sed $1 -e'/^\\([0-9]\\|#[\$@h]\)/!d' -e'/^#[\$@h]/!s/#.*\$//g'`" 267 268 # Extract just the data, removing all whitespace 269 270 local DATA="`echo \"$RAW\" | sed -e'/^#h/d' -e's/^#[\$@]//g' | tr -d '[:space:]'`" 271 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) 275 276 local DSHA="`echo -n \"$DATA\" | shasum | sed -e's/[? *].*$//'`" 277 278 # Extract the file's hash. Restore any leading zeroes in hash segments. 279 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` 283 284 if [ -n "$FSHA" -a \( "$FSHA" = "$DSHA" \) ]; then 285 if [ -n "$2" ]; then 286 log "info" "Checksum of $1 validated" 287 fi 288 else 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" 293 return 1 294 fi 295 296 # Check the expiration date, converting NTP epoch to Unix epoch used by date 297 298 EXPIRES="`echo \"$RAW\" | sed -e'/^#@/!d' -e's/^#@//' | tr -d '[:space:]'`" 299 EXPIRES="$(($EXPIRES - 2208988800 ))" 300 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\"`" 303 return 2 304 fi 305 306} 307 308# Verify ntp.conf 309 310if ! [ -f "$NTPCONF" ]; then 311 log "critical" "Missing ntp configuration $NTPCONF" 312 exit 1 313fi 314 315# Parse ntp.conf for leapfile directive 316 317LEAPFILE="`sed $NTPCONF -e'/^ *leapfile *.*$/!d' -e's/^ *leapfile *//'`" 318if [ -z "$LEAPFILE" ]; then 319 log "error" "$NTPCONF does not specify a leapfile" 320fi 321 322# Allow placing the file someplace else - testing 323 324if [ -n "$1" ]; then 325 if [ "$1" != "$LEAPFILE" ]; then 326 log "notice" "Requested install to $1, but $NTPCONF specifies $LEAPFILE" 327 fi 328 LEAPFILE="$1" 329fi 330 331# Verify the current file 332# If it is missing, doesn't validate or expired 333# Or is expiring soon 334# Download a new one 335 336if [ -n "$FORCE" ] || ! verifySHA $LEAPFILE "$VERBOSE" || [ $EXPIRES -lt `date -d "NOW + $PREFETCH" +%s` ] ; then 337 TRY=0 338 while true; do 339 TRY=$(( $TRY + 1 )) 340 if [ -n "$VERBOSE" ]; then 341 log "info" "Attempting download from $LEAPSRC, try $TRY.." 342 fi 343 if wget $PROTO $PREFER -o ${TMPFILE}.log $LEAPSRC -O $TMPFILE ; then 344 log "info" "Download of $LEAPSRC succeeded" 345 if [ -n "$VERBOSE" ]; then 346 cat ${TMPFILE}.log 347 fi 348 349 if ! verifySHA $TMPFILE "$VERBOSE" ; then 350 # There is no point in retrying, as the file on the server is almost 351 # certainly corrupt. 352 353 log "warning" "Downloaded file $TMPFILE rejected -- saved for diagnosis" 354 cat ${TMPFILE}.log 355 rm -f ${TMPFILE}.log 356 exit 1 357 fi 358 rm -f ${TMPFILE}.log 359 360 # Set correct permissions on temporary file 361 362 REFFILE="$LEAPFILE" 363 if [ ! -f $LEAPFILE ]; then 364 log "notice" "$LEAPFILE was missing, creating new copy - check permissions" 365 touch $LEAPFILE 366 # Can't copy permissions from old file, copy from NTPCONF instead 367 REFFILE="$NTPCONF" 368 fi 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 374 fi 375 376 # Replace current file with validated new one 377 378 if mv -f $TMPFILE $LEAPFILE ; then 379 log "notice" "Installed new $LEAPFILE from $LEAPSRC" 380 else 381 log "error" "Install $TMPFILE => $LEAPFILE failed -- saved for diagnosis" 382 exit 1 383 fi 384 385 # Restart NTP (or whatever else is specified) 386 387 if [ -n "$RESTART" ]; then 388 if [ -n "$VERBOSE" ]; then 389 log "info" "Attempting restart action: $RESTART" 390 fi 391 R="$( 2>&1 $RESTART )" 392 if [ $? -eq 0 ]; then 393 log "notice" "Restart action succeeded" 394 if [ -n "$VERBOSE" -a -n "$R" ]; then 395 log "info" "$R" 396 fi 397 else 398 log "error" "Restart action failed" 399 if [ -n "$R" ]; then 400 log "error" "$R" 401 fi 402 exit 2 403 fi 404 fi 405 exit 0 406 fi 407 408 # Failed to download. See about trying again 409 410 rm -f $TMPFILE 411 if [ $TRY -ge $MAXTRIES ]; then 412 break; 413 fi 414 if [ -n "$VERBOSE" ]; then 415 cat ${TMPFILE}.log 416 log "info" "Waiting $INTERVAL minutes before retrying..." 417 fi 418 sleep $(( $INTERVAL * 60)) 419 done 420 421 # Failed and out of retries 422 423 log "warning" "Download from $LEAPSRC failed after $TRY attempts" 424 if [ -f ${TMPFILE}.log ]; then 425 cat ${TMPFILE}.log 426 rm -f ${TMPFILE}.log $TMPFILE 427 fi 428 exit 1 429fi 430log "info" "Not time to replace $LEAPFILE" 431 432exit 0 433 434# EOF