chiark / gitweb /
check-bkp-status.in: Force `sqlite3' output options to be correct.
[rsync-backup] / check-bkp-status.in
1 #! @BASH@
2 ###
3 ### Check that dumps have been made as expected.
4 ###
5 ### (c) 2013 Mark Wooding
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of the `rsync-backup' program.
11 ###
12 ### rsync-backup is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
16 ###
17 ### rsync-backup is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 ### GNU General Public License for more details.
21 ###
22 ### You should have received a copy of the GNU General Public License
23 ### along with rsync-backup; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26 set -e
27
28 quis=${0##*/}
29 . @pkgdatadir@/lib.sh
30
31 ###--------------------------------------------------------------------------
32 ### Stubs for the configuration file.
33
34 defhook () { :; }
35 addhook () { :; }
36
37 retain () { :; }
38 snaptype () { :; }
39 like () { :; }
40 retry () { :; }
41 user () { :; }
42
43 ###--------------------------------------------------------------------------
44 ### Output format switch.
45
46 formats=:
47 defformat () { formats=$formats$1:; }
48 fmtstate=begin
49
50 fmt () {
51   state=$1; shift
52
53   while :; do
54     ostate=$fmtstate
55     case $fmtstate in $state) break ;; esac
56     case "$fmtstate,$state" in
57       begin,end)
58         fmtstate=end
59         ;;
60       begin,*)
61         ${fmt}_header "$@"
62         fmtstate=hosttbl_begin
63         ;;
64       hosttbl_begin,hosttbl_*)
65         ${fmt}_hosttbl_begin "$@"
66         fmtstate=hosttbl_host
67         ;;
68       hosttbl_host,hosttbl_fs)
69         fmtstate=hosttbl_fs
70         ;;
71       hosttbl_fs,hosttbl_host)
72         ${fmt}_hosttbl_sep "$@"
73         fmtstate=hosttbl_host
74         ;;
75       hosttbl_begin,* | hosttbl_end,*)
76         fmtstate=logdump_begin
77         ;;
78       hosttbl_*,*)
79         ${fmt}_hosttbl_end "$@"
80         fmtstate=hosttbl_end
81         ;;
82       logdump_begin,logdump_*)
83         fmtstate=logdump_out
84         ;;
85       logdump_out,logdump_info | logdump_out,logdump_file)
86         fmtstate=$state
87         ;;
88       logdump_*,logdump_begin)
89         ${fmt}_logdump_end "$@"
90         fmtstate=logdump_begin
91         ;;
92       logdump_end,*)
93         fmtstate=footer
94         ;;
95       logdump_*,*)
96         fmtstate=logdump_end
97         ;;
98       footer,end)
99         ${fmt}_footer "$@"
100         fmtstate=end
101         ;;
102     esac
103     case $fmtstate in
104       "$ostate")
105         echo >&2 "$quis: FATAL!  NO PROGRESS IN FMT STATE MACHINE"
106         exit 9
107         ;;
108     esac
109   done
110
111   ${fmt}_$fmtstate "$@"
112 }
113
114 ###--------------------------------------------------------------------------
115 ### Plain text output.
116
117 defformat txt
118
119 txt_header () { :; }
120 txt_hosttbl_begin () { :; }
121 txt_hosttbl_host () { echo "HOST $1"; }
122 txt_hosttbl_fs () { printf "        %-23s %s\n" "$2" "$4"; }
123 txt_hosttbl_sep () { echo; }
124 txt_hosttbl_end () { :; }
125
126 txt_logdump_begin () {
127   cat <<EOF
128
129 -----------------------------------------------------------------------------
130 Log tail for $1:$2
131
132 EOF
133 }
134
135 txt_logdump_info () { echo "!!! $3"; }
136 txt_logdump_file () { cat "$3"; }
137 txt_logdump_end () { :; }
138
139 txt_footer () { :; }
140 txt_end () { :; }
141
142 fmt=txt
143
144 ###--------------------------------------------------------------------------
145 ### HTML output.
146
147 defformat html
148
149 html_header () {
150   cat <<EOF
151 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
152           "http://www.w3c.org/TR/html4/strict.dtd">
153 <html>
154 <head>
155   <title>$now rsync-backup report</title>
156   <meta name=viewport content="width=device-width initial-scale=1.0">
157   <style type='text/css'><!--
158         body {
159           background: white;
160           color: black;
161           margin: 1ex 2em;
162         }
163
164         h1, h2 { font-family: sans-serif; font-weight: bold; }
165
166         h1 {
167           padding-bottom: 1ex;
168           border-bottom: solid medium black;
169           margin-bottom: 3ex;
170           font-size: 200%;
171         }
172
173         h2 {
174           border-top: solid thin black;
175           padding-top: 1ex;
176           margin-top: 3ex;
177           font-size: x-large;
178         }
179
180         h1 + h2 {
181           border-top: none;
182           padding-top: 0;
183         }
184
185         div.footer {
186           border-top: solid medium black;
187           margin-top: 3ex;
188           padding-top: 1ex;
189           font-size: small;
190           clear: both;
191           text-align: right;
192           font-style: italic;
193         }
194
195         table.hosttbl {
196           border-collapse: collapse;
197           display: inline-table;
198           margin: 1ex 1em;
199         }
200         table.hosttbl tr.fs td {
201           border: solid thin black;
202         }
203         table.hosttbl td { padding: 0.5ex 0.5em; }
204         table.hosttbl tr.host td {
205           padding-top: 2ex;
206           text-align: center;
207           font-weight: bold;
208           font-family: monospace;
209         }
210         table.hosttbl td.fs {
211           font-family: monospace;
212         }
213
214         table.hosttbl td.winning { background: #2f4; }
215         table.hosttbl td.failed { background: #f80; }
216         table.hosttbl td.broken { background: #f11; }
217         table.hosttbl td.never { background: #66f; }
218
219         pre {
220           clear: both;
221           margin: 3ex 2em;
222           padding: 1ex;
223           background: #eee;
224           border: solid thin black;
225           overflow: auto;
226         }
227
228         pre.logdump { max-height: 120ex; }
229
230         .hide { display: none; }
231         a.expand-button {
232           float: right;
233           font-size: medium;
234           font-weight: initial;
235           padding-top: 1ex;
236         }
237
238         div.logdump-info {
239           margin: 3ex 2em;
240           padding: 1ex;
241           background: #f11;
242           border: solid thin black;
243         }
244   --></style>
245   <script type='text/javascript'><!--
246         var LAST_EXPAND = null;
247         function elt(id) { return document.getElementById(id); }
248         function elt_class_p(elt, cls) {
249           return elt.className.match('\\\\b' + cls + '\\\\b');
250         }
251         function add_elt_class(elt, cls) {
252           if (!elt_class_p(elt, cls)) elt.className += ' ' + cls;
253         }
254         function rm_elt_class(elt, cls) {
255           elt.className = elt.className.replace(
256             new RegExp('\\\\s*\\\\b' + cls + '\\\\b\\\\s*'), ' ');
257         }
258         function toggle_expand(ev, tag) {
259           var d = elt('logdump-' + tag);
260           var b = elt('expand-' + tag);
261           if (elt_class_p(d, 'hide')) do_show(d, b);
262           else do_hide(d, b);
263           ev.preventDefault();
264         }
265         function do_show(d, b) {
266           rm_elt_class(d, 'hide');
267           b.textContent = '[hide]';
268         }
269         function do_hide(d, b) {
270           add_elt_class(d, 'hide');
271           b.textContent = '[show]';
272           if (LAST_EXPAND !== null && d === LAST_EXPAND[0])
273             LAST_EXPAND = null;
274         }
275         function expand_log(tag) {
276           if (LAST_EXPAND !== null) do_hide(LAST_EXPAND[0], LAST_EXPAND[1]);
277           var d = elt('logdump-' + tag);
278           var b = elt('expand-' + tag);
279           if (elt_class_p(d, 'hide')) {
280             LAST_EXPAND = [d, b];
281             do_show(d, b);
282           }
283         }
284         function make_toggle_button(tag) {
285           document.write(
286             "<a class=expand-button id='expand-" + tag + "' href='#' " +
287                "onclick='toggle_expand(event, \"" + tag + "\")'>" +
288             "[show]" +
289             "</a>");
290         }
291         function hide_logdump(tag) {
292           add_elt_class(elt('logdump-' + tag), 'hide');
293         }
294   --></script>
295 </head>
296 <body>
297
298 <h1><tt>rsync-backup</tt> report: $now_dow $now</tt></h1>
299 EOF
300 }
301
302 html_hosttbl_begin () {
303   cat <<EOF
304
305 <h2>Overview</h2>
306 EOF
307 }
308
309 html_hosttbl_host () {
310   cat <<EOF
311 <table class=hosttbl>
312   <tr class=host><td colspan=2>$1</tc>
313 EOF
314 }
315
316 html_hosttbl_fs () {
317   case $3 in
318     winning) link="" knil="" ;;
319     *)
320       link="<a href='#log-$host:$fs' onclick='expand_log(\"$host:$fs\")'>"
321       knil="</a>"
322       ;;
323   esac
324   cat <<EOF
325   <tr class=fs>
326     <td class=fs>$2</td>
327     <td class=$3>$link$4$knil</td>
328 EOF
329 }
330
331 html_hosttbl_sep () {
332   cat <<EOF
333 </table>
334 EOF
335 }
336
337 html_hosttbl_end () {
338   cat <<EOF
339 </table>
340 EOF
341 }
342
343 html_logdump_begin () {
344   cat <<EOF
345
346 <h2 id='log-$1:$2'><a name='log-$1:$2'>Log tail for <tt>$1:$2</tt></a>
347 <script type='text/javascript'><!--
348         make_toggle_button('$1:$2');
349 --></script></h2>
350 EOF
351 }
352
353 html_logdump_info () {
354   cat <<EOF
355 <div id='logdump-$1:$2' class=logdump-info><script type='text/javascript'><!--
356         hide_logdump('$1:$2');
357 --></script>$3</div>
358 EOF
359 }
360
361 html_logdump_file () {
362   cat <<EOF
363 <pre id='logdump-$1:$2' class=logdump>
364 <script type='text/javascript'><!--
365         hide_logdump('$1:$2');
366 --></script>$3</div>
367 EOF
368   cat "$3"
369   cat <<EOF
370 </pre>
371 EOF
372 }
373
374 html_logdump_end () { :; }
375
376 html_footer () {
377   cat <<EOF
378
379 <div class=footer>
380   Checked at $now $now_time<br>
381   <tt>rsync-backup</tt> $VERSION; &copy; 2014 Mark Wooding
382 </div>
383
384 </body>
385 </html>
386 EOF
387 }
388
389 html_end () { :; }
390
391 ###--------------------------------------------------------------------------
392 ### Main checking.
393
394 INDEXDB=@pkglocalstatedir@/index.db
395
396 host () { host=$1; }
397
398 patterns=
399 showhost=
400 failures=
401
402 backup () {
403   for fs in "$@"; do
404     case $fs in *:*) fs=${fs%%:*} ;; esac
405     matchp=nil
406     for p in "${patterns[@]}"; do
407       case $host:$fs in $p) matchp=t; break ;; esac
408     done
409     case $matchp in nil) return ;; esac
410     when=$(sqlite3 -batch -list -separator \| -noheader $INDEXDB \
411       "SELECT MAX(date) FROM idx WHERE host = '$host' AND fs = '$fs';")
412     case $when in
413       "")
414         class=never
415         info="NEVER"
416         show=t
417         win=nil
418         ;;
419       "$now")
420         class=winning
421         info="today"
422         show=$verbose
423         win=t
424         ;;
425       *)
426         jdn=$(julian "$when")
427         ago=$(( $now_jdn - $jdn ))
428         case $ago in 1 | -1) days=day ;; *) days=days ;; esac
429         case $ago in
430           -*) class=future; info="${ago#-} $days in the FUTURE" ;;
431           1 | 2) class=failed; info="$ago $days ago" ;;
432           *) class=broken; info="$ago $days ago" ;;
433         esac
434         show=t
435         win=nil
436         ;;
437     esac
438     case $show in
439       t)
440         case $showhost in
441           "$host") ;;
442           *)
443             fmt hosttbl_host $host
444             showhost=$host
445             ;;
446         esac
447         fmt hosttbl_fs $host $fs $class "$info"
448         case $win in
449           nil) failures="$failures $host:$fs" ;;
450         esac
451         ;;
452     esac
453   done
454 }
455
456 failure_logs () {
457   for fail in $failures; do
458     host=${fail%:*} fs=${fail##*:}
459     any=nil
460     for i in "$logdir/$host/$fs.$now#"*; do
461       if [ -f "$i" ]; then log=$i; any=t; fi
462     done
463     fmt logdump_begin $host $fs
464     case $any in
465       t) fmt logdump_file $host $fs "$log" ;;
466       nil) fmt logdump_info $host $fs "No log!  No backup attempted." ;;
467     esac
468   done
469 }
470
471 ###--------------------------------------------------------------------------
472 ### Read the configuration and we're done.
473
474 usage () {
475   echo "usage: $quis [-v] [-c CONF] [-f FORMAT] [PATTERNS...]"
476 }
477
478 version () {
479   echo "$quis, rsync-backup version $VERSION"
480 }
481
482 verbose=nil
483
484 while getopts "hVc:f:v" opt; do
485   case "$opt" in
486     h) usage; exit 0 ;;
487     V) version; config; exit 0 ;;
488     c) conf=$OPTARG ;;
489     f)
490       case $formats in
491         *":$OPTARG:"*) ;;
492         *) echo >&2 "$0: unknown format \`$OPTARG'"; exit 1 ;;
493       esac
494       fmt=$OPTARG
495       ;;
496     v) verbose=t ;;
497     *) exit 1 ;;
498   esac
499 done
500 shift $((OPTIND - 1))
501 case $# in
502   0) declare -a patterns=("*") ;;
503   *) declare -a patterns=("$@") ;;
504 esac
505
506 now=$(date +"%Y-%m-%d")
507 now_time=$(date +"%H:%M:%S %z")
508 now_dow=$(date +"%A")
509 now_jdn=$(julian $now)
510 . "$conf"
511 failure_logs
512 fmt end
513
514 ###----- That's all, folks --------------------------------------------------