1284990Scy#!/bin/bash 2284990Scy 3284990Scy# Copyright (C) 2014 Timothe Litt litt at acm dot org 4284990Scy 5284990Scy# This script may be freely copied, used and modified providing that 6284990Scy# this notice and the copyright statement are included in all copies 7284990Scy# and derivative works. No warranty is offered, and use is entirely at 8284990Scy# your own risk. Bugfixes and improvements would be appreciated by the 9284990Scy# author. 10284990Scy 11284990ScyVERSION="1.003" 12284990Scy 13284990Scy# leap-seconds file manager/updater 14284990Scy 15284990Scy# Depends on: 16284990Scy# wget sed, tr, shasum, logger 17284990Scy 18284990Scy# ########## Default configuration ########## 19284990Scy# 20284990Scy# Where to get the file 21284990ScyLEAPSRC="ftp://time.nist.gov/pub/leap-seconds.list" 22284990Scy 23284990Scy# How many times to try to download new file 24284990ScyMAXTRIES=6 25284990ScyINTERVAL=10 26284990Scy 27284990Scy# Where to find ntp config file 28284990ScyNTPCONF=/etc/ntp.conf 29284990Scy 30284990Scy# How long before expiration to get updated file 31284990ScyPREFETCH="60 days" 32284990Scy 33284990Scy# How to restart NTP - older NTP: service ntpd? try-restart | condrestart 34284990Scy# Recent NTP checks for new file daily, so there's nothing to do 35284990ScyRESTART= 36284990Scy 37284990Scy# Where to put temporary copy before it's validated 38284990ScyTMPFILE="/tmp/leap-seconds.$$.tmp" 39284990Scy 40284990Scy# Syslog facility 41284990ScyLOGFAC=daemon 42284990Scy# ########################################### 43284990Scy 44284990Scy# Places to look for commands. Allows for CRON having path to 45284990Scy# old utilities on embedded systems 46284990Scy 47284990ScyPATHLIST="/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:" 48284990Scy 49284990ScyREQUIREDCMDS=" wget logger tr sed shasum" 50284990Scy 51284990ScySELF="`basename $0`" 52284990Scy 53284990Scyfunction displayHelp { 54284990Scy cat <<EOF 55284990ScyUsage: $SELF [options] [leapfile] 56284990Scy 57284990ScyVerifies and if necessary, updates leap-second definition file 58284990Scy 59284990ScyAll arguments are optional: Default (or current value) shown: 60284990Scy -s Specify the URL of the master copy to download 61284990Scy $LEAPSRC 62284990Scy -4 Use only IPv4 63284990Scy -6 Use only IPv6 64284990Scy -p 4|6 65284990Scy Prefer IPv4 or IPv6 (as specified) addresses, but use either 66284990Scy -d Specify the filename on the local system 67284990Scy $LEAPFILE 68284990Scy -e Specify how long before expiration the file is to be refreshed 69284990Scy Units are required, e.g. "-e 60 days" Note that larger values 70284990Scy imply more frequent refreshes. 71284990Scy "$PREFETCH" 72284990Scy -f Specify location of ntp.conf (used to make sure leapfile directive is 73284990Scy present and to default leapfile) 74284990Scy $NTPCONF 75284990Scy -F Force update even if current file is OK and not close to expiring. 76284990Scy -c Command to restart NTP after installing a new file 77284990Scy <none> - ntpd checks file daily 78284990Scy -r Specify number of times to retry on get failure 79284990Scy $MAXTRIES 80284990Scy -i Specify number of minutes between retries 81284990Scy $INTERVAL 82284990Scy -l Use syslog for output (Implied if CRONJOB is set) 83284990Scy -L Don't use syslog for output 84284990Scy -P Specify the syslog facility for logging 85284990Scy $LOGFAC 86284990Scy -t Name of temporary file used in validation 87284990Scy $TMPFILE 88284990Scy -q Only report errors to stdout 89284990Scy -v Verbose output 90284990Scy -z Specify path for utilities 91284990Scy $PATHLIST 92284990Scy -Z Only use system path 93284990Scy 94284990Scy$SELF will validate the file currently on the local system 95284990Scy 96284990ScyOrdinarily, the file is found using the "leapfile" directive in $NTPCONF. 97284990ScyHowever, an alternate location can be specified on the command line. 98284990Scy 99284990ScyIf the file does not exist, is not valid, has expired, or is expiring soon, 100284990Scya new copy will be downloaded. If the new copy validates, it is installed and 101284990ScyNTP is (optionally) restarted. 102284990Scy 103284990ScyIf the current file is acceptable, no download or restart occurs. 104284990Scy 105284990Scy-c can also be used to invoke another script to perform administrative 106284990Scyfunctions, e.g. to copy the file to other local systems. 107284990Scy 108284990ScyThis can be run as a cron job. As the file is rarely updated, and leap 109284990Scyseconds are announced at least one month in advance (usually longer), it 110284990Scyneed not be run more frequently than about once every three weeks. 111284990Scy 112284990ScyFor cron-friendly behavior, define CRONJOB=1 in the crontab. 113284990Scy 114284990ScyThis script depends on$REQUIREDCMDS 115284990Scy 116284990ScyVersion $VERSION 117284990ScyEOF 118284990Scy return 0 119284990Scy} 120284990Scy 121284990Scy# Default: Use syslog for logging if running under cron 122284990Scy 123284990ScySYSLOG="$CRONJOB" 124284990Scy 125284990Scyif [ "$1" = "--help" ]; then 126284990Scy displayHelp 127284990Scy exit 0 128284990Scyfi 129284990Scy 130284990Scy# Parse options 131284990Scy 132284990Scywhile getopts 46p:P:s:e:f:Fc:r:i:lLt:hqvz:Z opt; do 133284990Scy case $opt in 134284990Scy 4) 135284990Scy PROTO="-4" 136284990Scy ;; 137284990Scy 6) 138284990Scy PROTO="-6" 139284990Scy ;; 140284990Scy p) 141284990Scy if [ "$OPTARG" = '4' -o "$OPTARG" = '6' ]; then 142284990Scy PREFER="--prefer-family=IPv$OPTARG" 143284990Scy else 144284990Scy echo "Invalid -p $OPTARG" >&2 145284990Scy exit 1; 146284990Scy fi 147284990Scy ;; 148284990Scy P) 149284990Scy LOGFAC="$OPTARG" 150284990Scy ;; 151284990Scy s) 152284990Scy LEAPSRC="$OPTARG" 153284990Scy ;; 154284990Scy e) 155284990Scy PREFETCH="$OPTARG" 156284990Scy ;; 157284990Scy f) 158284990Scy NTPCONF="$OPTARG" 159284990Scy ;; 160284990Scy F) 161284990Scy FORCE="Y" 162284990Scy ;; 163284990Scy c) 164284990Scy RESTART="$OPTARG" 165284990Scy ;; 166284990Scy r) 167284990Scy MAXTRIES="$OPTARG" 168284990Scy ;; 169284990Scy i) 170284990Scy INTERVAL="$OPTARG" 171284990Scy ;; 172284990Scy t) 173284990Scy TMPFILE="$OPTARG" 174284990Scy ;; 175284990Scy l) 176284990Scy SYSLOG="y" 177284990Scy ;; 178284990Scy L) 179284990Scy SYSLOG= 180284990Scy ;; 181284990Scy h) 182284990Scy displayHelp 183284990Scy exit 0 184284990Scy ;; 185284990Scy q) 186284990Scy QUIET="Y" 187284990Scy ;; 188284990Scy v) 189284990Scy VERBOSE="Y" 190284990Scy ;; 191284990Scy z) 192284990Scy PATHLIST="$OPTARG:" 193284990Scy ;; 194284990Scy Z) 195284990Scy PATHLIST= 196284990Scy ;; 197284990Scy *) 198284990Scy echo "$SELF -h for usage" >&2 199284990Scy exit 1 200284990Scy ;; 201284990Scy esac 202284990Scydone 203284990Scyshift $((OPTIND-1)) 204284990Scy 205284990Scyexport PATH="$PATHLIST$PATH" 206284990Scy 207284990Scy# Add to path to deal with embedded systems 208284990Scy# 209284990Scyfor P in $REQUIREDCMDS ; do 210284990Scy if >/dev/null 2>&1 which "$P" ; then 211284990Scy continue 212284990Scy fi 213284990Scy [ "$P" = "logger" ] && continue 214284990Scy echo "FATAL: missing $P command, please install" 215284990Scy exit 1 216284990Scydone 217284990Scy 218284990Scy# Handle logging 219284990Scy 220284990Scyif ! LOGGER="`2>/dev/null which logger`" ; then 221284990Scy LOGGER= 222284990Scyfi 223284990Scy 224284990Scyfunction log { 225284990Scy # "priority" "message" 226284990Scy # 227284990Scy # Stdout unless syslog specified or logger isn't available 228284990Scy # 229284990Scy if [ -z "$SYSLOG" -o -z "$LOGGER" ]; then 230284990Scy if [ -n "$QUIET" -a \( "$1" = "info" -o "$1" = "notice" -o "$1" = "debug" \) ]; then 231284990Scy return 0 232284990Scy fi 233284990Scy echo "`echo \"$1\" | tr a-z A-Z`: $2" 234284990Scy return 0 235284990Scy fi 236284990Scy 237284990Scy # Also log to stdout if cron job && notice or higher 238284990Scy local S 239284990Scy if [ -n "$CRONJOB" -a \( "$1" != "info" \) -a \( "$1" != "debug" \) ] || [ -n "$VERBOSE" ]; then 240284990Scy S="-s" 241284990Scy fi 242284990Scy $LOGGER $S -t "$SELF[$$]" -p "$LOGFAC.$1" "$2" 243284990Scy} 244284990Scy 245284990Scy# Verify interval 246284990ScyINTERVAL=$(( $INTERVAL *1 )) 247284990Scy 248284990Scy# Validate a leap-seconds file checksum 249284990Scy# 250284990Scy# File format: (full description in files) 251284990Scy# # marks comments, except: 252284990Scy# #$ number : the NTP date of the last update 253284990Scy# #@ number : the NTP date that the file expires 254284990Scy# Date (seconds since 1900) leaps : leaps is the # of seconds to add for times >= Date 255284990Scy# Date lines have comments. 256284990Scy# #h hex hex hex hex hex is the SHA-1 checksum of the data & dates, excluding whitespace w/o leading zeroes 257284990Scy 258284990Scyfunction verifySHA { 259284990Scy 260284990Scy if [ ! -f "$1" ]; then 261284990Scy return 1 262284990Scy fi 263284990Scy 264284990Scy # Remove comments, except those that are markers for last update, expires and hash 265284990Scy 266284990Scy local RAW="`sed $1 -e'/^\\([0-9]\\|#[\$@h]\)/!d' -e'/^#[\$@h]/!s/#.*\$//g'`" 267284990Scy 268284990Scy # Extract just the data, removing all whitespace 269284990Scy 270284990Scy local DATA="`echo \"$RAW\" | sed -e'/^#h/d' -e's/^#[\$@]//g' | tr -d '[:space:]'`" 271284990Scy 272284990Scy # Compute the SHA hash of the data, removing the marker and filename 273284990Scy # Computed in binary mode, which shouldn't matter since whitespace has been removed 274284990Scy # shasum comes in several flavors; a portable one is available in Perl (with Digest::SHA) 275284990Scy 276284990Scy local DSHA="`echo -n \"$DATA\" | shasum | sed -e's/[? *].*$//'`" 277284990Scy 278284990Scy # Extract the file's hash. Restore any leading zeroes in hash segments. 279284990Scy 280284990Scy # The sed [] includes a tab (\t) and space; #h is followed by a tab and space 281284990Scy local FSHA="`echo \"$RAW\" | sed -e'/^#h/!d' -e's/^#h//' -e's/[ ] */ 0x/g'`" 282284990Scy FSHA=`printf '%08x%08x%08x%08x%08x' $FSHA` 283284990Scy 284284990Scy if [ -n "$FSHA" -a \( "$FSHA" = "$DSHA" \) ]; then 285284990Scy if [ -n "$2" ]; then 286284990Scy log "info" "Checksum of $1 validated" 287284990Scy fi 288284990Scy else 289284990Scy log "error" "Checksum of $1 is invalid:" 290284990Scy [ -z "$FSHA" ] && FSHA="(no checksum record found in file)" 291284990Scy log "error" "EXPECTED: $FSHA" 292284990Scy log "error" "COMPUTED: $DSHA" 293284990Scy return 1 294284990Scy fi 295284990Scy 296284990Scy # Check the expiration date, converting NTP epoch to Unix epoch used by date 297284990Scy 298284990Scy EXPIRES="`echo \"$RAW\" | sed -e'/^#@/!d' -e's/^#@//' | tr -d '[:space:]'`" 299284990Scy EXPIRES="$(($EXPIRES - 2208988800 ))" 300284990Scy 301284990Scy if [ $EXPIRES -lt `date -u +%s` ]; then 302284990Scy log "notice" "File expired on `date -u -d \"Jan 1, 1970 00:00:00 +0000 + $EXPIRES seconds\"`" 303284990Scy return 2 304284990Scy fi 305284990Scy 306284990Scy} 307284990Scy 308284990Scy# Verify ntp.conf 309284990Scy 310284990Scyif ! [ -f "$NTPCONF" ]; then 311284990Scy log "critical" "Missing ntp configuration $NTPCONF" 312284990Scy exit 1 313284990Scyfi 314284990Scy 315284990Scy# Parse ntp.conf for leapfile directive 316284990Scy 317284990ScyLEAPFILE="`sed $NTPCONF -e'/^ *leapfile *.*$/!d' -e's/^ *leapfile *//'`" 318284990Scyif [ -z "$LEAPFILE" ]; then 319284990Scy log "error" "$NTPCONF does not specify a leapfile" 320284990Scyfi 321284990Scy 322284990Scy# Allow placing the file someplace else - testing 323284990Scy 324284990Scyif [ -n "$1" ]; then 325284990Scy if [ "$1" != "$LEAPFILE" ]; then 326284990Scy log "notice" "Requested install to $1, but $NTPCONF specifies $LEAPFILE" 327284990Scy fi 328284990Scy LEAPFILE="$1" 329284990Scyfi 330284990Scy 331284990Scy# Verify the current file 332284990Scy# If it is missing, doesn't validate or expired 333284990Scy# Or is expiring soon 334284990Scy# Download a new one 335284990Scy 336284990Scyif [ -n "$FORCE" ] || ! verifySHA $LEAPFILE "$VERBOSE" || [ $EXPIRES -lt `date -d "NOW + $PREFETCH" +%s` ] ; then 337284990Scy TRY=0 338284990Scy while true; do 339284990Scy TRY=$(( $TRY + 1 )) 340284990Scy if [ -n "$VERBOSE" ]; then 341284990Scy log "info" "Attempting download from $LEAPSRC, try $TRY.." 342284990Scy fi 343284990Scy if wget $PROTO $PREFER -o ${TMPFILE}.log $LEAPSRC -O $TMPFILE ; then 344284990Scy log "info" "Download of $LEAPSRC succeeded" 345284990Scy if [ -n "$VERBOSE" ]; then 346284990Scy cat ${TMPFILE}.log 347284990Scy fi 348284990Scy 349284990Scy if ! verifySHA $TMPFILE "$VERBOSE" ; then 350284990Scy # There is no point in retrying, as the file on the server is almost 351284990Scy # certainly corrupt. 352284990Scy 353284990Scy log "warning" "Downloaded file $TMPFILE rejected -- saved for diagnosis" 354284990Scy cat ${TMPFILE}.log 355284990Scy rm -f ${TMPFILE}.log 356284990Scy exit 1 357284990Scy fi 358284990Scy rm -f ${TMPFILE}.log 359284990Scy 360284990Scy # Set correct permissions on temporary file 361284990Scy 362284990Scy REFFILE="$LEAPFILE" 363284990Scy if [ ! -f $LEAPFILE ]; then 364284990Scy log "notice" "$LEAPFILE was missing, creating new copy - check permissions" 365284990Scy touch $LEAPFILE 366284990Scy # Can't copy permissions from old file, copy from NTPCONF instead 367284990Scy REFFILE="$NTPCONF" 368284990Scy fi 369284990Scy chmod --reference $REFFILE $TMPFILE 370284990Scy chown --reference $REFFILE $TMPFILE 371284990Scy ( which selinuxenabled && selinuxenabled && which chcon ) >/dev/null 2>&1 372284990Scy if [ $? == 0 ] ; then 373284990Scy chcon --reference $REFFILE $TMPFILE 374284990Scy fi 375284990Scy 376284990Scy # Replace current file with validated new one 377284990Scy 378284990Scy if mv -f $TMPFILE $LEAPFILE ; then 379284990Scy log "notice" "Installed new $LEAPFILE from $LEAPSRC" 380284990Scy else 381284990Scy log "error" "Install $TMPFILE => $LEAPFILE failed -- saved for diagnosis" 382284990Scy exit 1 383284990Scy fi 384284990Scy 385284990Scy # Restart NTP (or whatever else is specified) 386284990Scy 387284990Scy if [ -n "$RESTART" ]; then 388284990Scy if [ -n "$VERBOSE" ]; then 389284990Scy log "info" "Attempting restart action: $RESTART" 390284990Scy fi 391284990Scy R="$( 2>&1 $RESTART )" 392284990Scy if [ $? -eq 0 ]; then 393284990Scy log "notice" "Restart action succeeded" 394284990Scy if [ -n "$VERBOSE" -a -n "$R" ]; then 395284990Scy log "info" "$R" 396284990Scy fi 397284990Scy else 398284990Scy log "error" "Restart action failed" 399284990Scy if [ -n "$R" ]; then 400284990Scy log "error" "$R" 401284990Scy fi 402284990Scy exit 2 403284990Scy fi 404284990Scy fi 405284990Scy exit 0 406284990Scy fi 407284990Scy 408284990Scy # Failed to download. See about trying again 409284990Scy 410284990Scy rm -f $TMPFILE 411284990Scy if [ $TRY -ge $MAXTRIES ]; then 412284990Scy break; 413284990Scy fi 414284990Scy if [ -n "$VERBOSE" ]; then 415284990Scy cat ${TMPFILE}.log 416284990Scy log "info" "Waiting $INTERVAL minutes before retrying..." 417284990Scy fi 418284990Scy sleep $(( $INTERVAL * 60)) 419284990Scy done 420284990Scy 421284990Scy # Failed and out of retries 422284990Scy 423284990Scy log "warning" "Download from $LEAPSRC failed after $TRY attempts" 424284990Scy if [ -f ${TMPFILE}.log ]; then 425284990Scy cat ${TMPFILE}.log 426284990Scy rm -f ${TMPFILE}.log $TMPFILE 427284990Scy fi 428284990Scy exit 1 429284990Scyfi 430284990Scylog "info" "Not time to replace $LEAPFILE" 431284990Scy 432284990Scyexit 0 433284990Scy 434284990Scy# EOF