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