chiark / gitweb /
rsync-backup.in: Retry unmounting snapshot filesystems a few times.
[rsync-backup] / rsync-backup.in
1 #! @BASH@
2 ###
3 ### Backup script
4 ###
5 ### (c) 2012 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 thishost=$(hostname -s)
29 quis=${0##*/}
30 . @pkgdatadir@/lib.sh
31
32 verbose=:
33 dryrun=nil
34
35 ###--------------------------------------------------------------------------
36 ### Utility functions.
37
38 RSYNCOPTS="--verbose"
39
40 do_rsync () {
41   ## Run rsync(1) in an appropriate manner.  Configuration should ovrride
42   ## this or set $RSYNCOPTS if it wants to do something weirder.  Arguments
43   ## to this function are passed on to rsync.
44
45   rsync \
46         --archive --hard-links --numeric-ids --del \
47         --sparse --compress \
48         --one-file-system \
49         --partial \
50         $RSYNCOPTS \
51         --filter="dir-merge .rsync-backup" \
52         "$@"
53 }
54
55 log () {
56   case $dryrun in
57     t)
58       echo >&2 "                *** $*"
59       ;;
60     nil)
61       now=$(date +"%Y-%m-%d %H:%M:%S %z")
62       echo >&9 "$now $*"
63       ;;
64   esac
65 }
66
67 maybe () {
68   ## Run CMD, if this isn't a dry run.
69
70   case $dryrun in
71     t) echo >&2 "               +++ $*" ;;
72     nil) "$@" ;;
73   esac
74 }
75
76 copy () {
77   prefix=$1
78   ## Copy lines from stdin to stdout, adding PREFIX.
79
80   while IFS= read -r line; do
81     printf "%s %s\n" "$prefix" "$line"
82   done
83 }
84
85 run () {
86   stdinp=nil
87   while :; do
88     case $1 in
89       -stdin) stdinp=t; shift ;;
90       --) shift; break ;;
91       *) break ;;
92     esac
93   done
94   tag=$1 cmd=$2; shift 2
95   ## Run CMD, logging its output in a pleasing manner.
96
97   case $dryrun in
98     t)
99       echo >&2 "                *** RUN $tag"
100       echo >&2 "                +++ $cmd $*"
101       rc=0
102       ;;
103     nil)
104       log "BEGIN $tag"
105       rc=$(
106         case $stdinp in nil) exec </dev/null ;; esac
107         { { { ( set +e
108                 "$cmd" "$@" 3>&- 4>&- 5>&- 9>&-
109                 echo $? >&5; ) |
110               copy "|" >&4; } 2>&1 |
111             copy "*" >&4; } 4>&1 |
112           cat >&9; } 5>&1
113       )
114       case $rc in
115         0) log "END $tag" ;;
116         *) log "FAIL $tag (rc = $rc)" ;;
117       esac
118       ;;
119   esac
120   return $rc
121 }
122
123 run_diff () {
124   out=$1 old=$2 new=$3
125   ## Write a unified diff from OLD to NEW, to OUT.
126
127   set +e; diff -u "$old" "$new" >"$out"; rc=$?; set -e
128   case $rc in 1) cat "$out" ;; esac
129   return $rc
130 }
131
132 localp () {
133   h=$1
134   ## Answer whether H is a local host.
135
136   case $h in
137     "$thishost") return 0 ;;
138     *) return 1 ;;
139   esac
140 }
141
142 hostrun () {
143   tag=$1 cmd=$2
144   ## Run CMD on the current host.  If the host seems local then run the
145   ## command through a local shell; otherwise run it through ssh(1).  Either
146   ## way it will be processed by a shell.
147
148   if localp $host; then run "@$host: $tag" sh -c "$cmd"
149   else run "@$host: $tag" ssh $userat$host "$cmd"
150   fi
151 }
152
153 _hostrun () {
154   h=$1 cmd=$2
155   ## Like hostrun, but without the complicated logging, and targetted at a
156   ## specific host.
157
158   if localp $h; then sh -c "$cmd"
159   else ssh $h "$cmd"
160   fi
161 }
162
163 hostpath () {
164   path=$1
165   ## Output (to stdout) either PATH or HOST:PATH, choosing the former if the
166   ## current host is local.
167
168   if localp $host; then echo $path
169   else echo $userat$host:$path
170   fi
171 }
172
173 defhook () {
174   hook=$1
175   ## Define a hook called HOOK.
176
177   eval hk_$hook=
178 }
179
180 addhook () {
181   hook=$1 cmd=$2
182   ## Add command CMD to the hook HOOK.
183
184   eval old=\$hk_$hook; new="$old $cmd"
185   eval hk_$hook=\$new
186 }
187
188 runhook () {
189   hook=$1; shift 1
190   ## Invoke HOOK, passing it the remaining arguments.
191
192   eval cmds=\$hk_$hook
193   for cmd in $cmds; do
194     if ! $cmd "$@"; then return $?; fi
195   done
196 }
197
198 remove_old_logfiles () {
199   base=$1
200   ## Remove old logfiles with names of the form BASE.DATE#N, so that there
201   ## are at most $MAXLOG of them.
202
203   ## Count up the logfiles.
204   nlog=0
205   for i in "$base".*; do
206     if [ ! -f "$i" ]; then continue; fi
207     nlog=$(( nlog + 1 ))
208   done
209
210   ## If there are too many, go through and delete some early ones.
211   if [ $dryrun = nil ] && [ $nlog -gt $MAXLOG ]; then
212     n=$(( nlog - MAXLOG ))
213     for i in "$base".*; do
214       if [ ! -f "$i" ]; then continue; fi
215       rm -f "$i"
216       n=$(( n - 1 ))
217       if [ $n -eq 0 ]; then break; fi
218     done
219   fi
220 }
221
222 ###--------------------------------------------------------------------------
223 ### Database operations.
224
225 insert_index () {
226   host=$1 fs=$2 date=$3 vol=$4
227
228   if [ -f "$INDEXDB" ]; then
229     sqlite3 "$INDEXDB" <<EOF
230 INSERT INTO idx (host, fs, date, vol)
231         VALUES ('$host', '$fs', '$date', '$vol');
232 EOF
233   fi
234 }
235
236 delete_index () {
237   host=$1 fs=$2 date=$3
238
239   if [ -f "$INDEXDB" ]; then
240     sqlite3 "$INDEXDB" <<EOF
241 DELETE FROM idx WHERE
242         host = '$host' AND fs = '$fs' AND date = '$date';
243 EOF
244   fi
245 }
246
247 ###--------------------------------------------------------------------------
248 ### Snapshot handling.
249
250 ## Snapshot protocol.  Each snapshot type has a pair of functions snap_TYPE
251 ## and unsnap_TYPE.  Each is given the current snapshot arguments and the
252 ## filesystem name to back up.  The snap_TYPE function should create and
253 ## mount the snapshot and output an rsync(1) path to where the filesystem can
254 ## be copied; the unsnap_TYPE function should unmount and tear down the
255 ## snapshot.
256
257 ## Fake snapshot by not doing anything.  Use only if you have no choice.
258 snap_live () { hostpath "$2"; }
259 unsnap_live () { :; }
260
261 ## Fake snapshot by remounting a live filesystem read-only.  Useful if the
262 ## underlying storage isn't in LVM.
263
264 snap_ro () {
265   fs=$1 mnt=$2
266
267   ## Place a marker in the filesystem so we know why it was made readonly.
268   ## (Also this serves to ensure that the filesystem was writable before.)
269   hostrun "snap-ro $mnt" "
270         echo rsync-backup >$mnt/.lock
271         mount -oremount,ro $mnt" || return $?
272
273   ## Done.
274   hostpath $mnt
275 }
276
277 unsnap_ro () {
278   fs=$1 mnt=$2
279
280   ## Check that the filesystem still has our lock marker.
281   hostrun "unsnap-ro $mnt" "
282         case \$(cat $mnt/.lock) in
283           rsync-backup) ;;
284           *) echo unlocked by someone else; exit 31 ;;
285         esac
286         mount -oremount,rw $mnt
287         rm $mnt/.lock" || return $?
288 }
289
290 ## Snapshot using LVM.
291
292 SNAPSIZE="-l10%ORIGIN"
293
294 snap_lvm () {
295   vg=$1 lv=$2
296
297   ## Make the snapshot.
298   hostrun "snap-lvm $vg/$lv" "
299         lvcreate --snapshot -n$lv.bkp $SNAPSIZE $vg/$lv
300         mkdir -p $SNAPDIR/$lv
301         mount -oro /dev/$vg/$lv.bkp $SNAPDIR/$lv" || return $?
302
303   ## Done.
304   hostpath $SNAPDIR/$lv
305 }
306
307 unsnap_lvm () {
308   vg=$1 lv=$2
309
310   ## Remove the snapshot.  Sometimes LVM doesn't notice that the snapshot is
311   ## no longer in open immdiately, so try several times.  Sometimes, more
312   ## mysteriously, something is keeping the filesystem from being unmounted,
313   ## so try that several times and report on things keeping the filesystem
314   ## open.
315   hostrun "unsnap-lvm $vg/$lv" "
316         for i in 1 2 3 4; do
317           echo \";;; BEGIN fuser -mv $SNAPDIR/$lv\"
318           fuser -mv $SNAPDIR/$lv | sed 's/^/;;; /'
319           echo \";;; END fuser -mv $SNAPDIR/$lv\"
320           echo \";;; BEGIN lsof $SNAPDIR/$lv\"
321           lsof $SNAPDIR/$lv | sed 's/^/;;; /'
322           echo \";;; END lsof $SNAPDIR/$lv\"
323           if umount $SNAPDIR/$lv; then break; fi
324           sleep 2
325         done
326         rc=1
327         for i in 1 2 3 4; do
328           if lvremove -f $vg/$lv.bkp; then rc=0; break; fi
329           sleep 2
330         done
331         exit $rc" || return $?
332 }
333
334 ## Complicated snapshot using LVM, where the volume group and filesystem are
335 ## owned by different machines, so they need to be synchronized during the
336 ## snapshot.
337
338 do_rfreezefs () {
339   lvhost=$1 vg=$2 lv=$3 fshost=$4 fsdir=$5
340
341   ## Engage in the rfreezefs protocol with the filesystem host.  This
342   ## involves some hairy plumbing.  We want to get exit statuses out of both
343   ## halves.
344   set +e
345   ssh $fshost rfreezefs $fsdir | {
346     set -e
347
348     ## Read the codebook from the remote end.
349     ready=nil
350     while read line; do
351       set -- $line
352       case "$1" in
353         PORT) port=$2 ;;
354         TOKEN) eval tok_$2=$3 ;;
355         READY) ready=t; break ;;
356         *)
357           echo >&2 "$quis: unexpected keyword $1 (rfreezefs to $rhost)"
358           exit 1
359           ;;
360       esac
361     done
362     case $ready in
363       nil)
364         echo >&2 "$quis: unexpected eof (rfreezefs to $rhost)"
365         exit 1
366         ;;
367     esac
368
369     ## Connect to the filesystem host's TCP port and get it to freeze its
370     ## filesystem.
371     exec 3<>/dev/tcp/$fshost/$port
372     echo $tok_FREEZE >&3
373     read tok <&3
374     case $tok in
375       "$tok_FROZEN") ;;
376       *)
377         echo >&2 "$quis: unexpected token $tok (rfreezefs $fsdir on $fshost)"
378         exit 1
379         ;;
380     esac
381
382     ## Get the volume host to create the snapshot.
383     set +e
384     _hostrun >&2 3>&- $userat$lvhost \
385       "lvcreate --snapshot -n$lv.bkp $SNAPSIZE $vg/$lv"
386     snaprc=$?
387     set -e
388
389     ## The filesystem can thaw now.
390     echo $tok_THAW >&3
391     read tok <&3
392     case $tok in
393       "$tok_THAWED") ;;
394       *)
395         _hostrun >&2 3>&- $userat$lvhost "lvremove -f $vg/$lv.bkp" || :
396         echo >&2 "$quis: unexpected token $tok (rfreezefs $fsdir on $fshost)"
397         exit 1
398         ;;
399     esac
400
401     ## Done.
402     exit $snaprc
403   }
404
405   ## Sift through the wreckage to find out what happened.
406   rc_rfreezefs=${PIPESTATUS[0]} rc_snapshot=${PIPESTATUS[1]}
407   set -e
408   case $rc_rfreezefs:$rc_snapshot in
409     0:0)
410       ;;
411     112:*)
412       echo >&2 "$quis: EMERGENCY failed to thaw $fsdir on $fshost!"
413       exit 112
414       ;;
415     *)
416       echo >&2 "$quis: failed to snapshot $vg/$lv ($fsdir on $fshost)"
417       exit 1
418       ;;
419   esac
420
421   ## Mount the snapshot on the volume host.
422   _hostrun >&2 $userat$lvhost "
423         mkdir -p $SNAPDIR/$lv
424         mount -oro /dev/$vg/$lv.bkp $SNAPDIR/$lv"
425 }
426
427 snap_rfreezefs () {
428   rhost=$1 vg=$2 lv=$3 rfs=$4
429
430   set -e
431   run "snap-rfreezefs $host:$vg/$lv $rhost:$rfs" \
432     do_rfreezefs $host $vg $lv $rhost $rfs || return $?
433   hostpath $SNAPDIR/$lv
434 }
435
436 unsnap_rfreezefs () {
437
438   ## Unshapping is the same as for plain LVM.
439   rhost=$1 vg=$2 lv=$3 rfs=$4
440   unsnap_lvm $vg $lv
441 }
442
443 ###--------------------------------------------------------------------------
444 ### Expiry computations.
445
446 expire () {
447   ## Read dates on stdin; write to stdout `EXPIRE date' for dates which
448   ## should be expired and `RETAIN date' for dates which should be retained.
449
450   ## Get the current date and convert it into useful forms.
451   now=$(date +%Y-%m-%d)
452   parsedate $now
453   now_jdn=$(julian $now) now_year=$year now_month=$month now_day=$day
454   kept=:
455
456   ## Work through each date in the input.
457   while read date; do
458     keep=nil
459
460     ## Convert the date into a useful form.
461     jdn=$(julian $date)
462     parsedate $date
463
464     ## Work through the policy list.
465     if [ $jdn -le $now_jdn ]; then
466       while read ival age; do
467
468         ## Decide whether the policy entry applies to this date.
469         apply=nil
470         case $age in
471           forever)
472             apply=t
473             ;;
474           year)
475             if [ $year -eq $now_year ] ||
476                ([ $year -eq $(( $now_year - 1 )) ] &&
477                 [ $month -ge $now_month ])
478             then apply=t; fi
479             ;;
480           month)
481             if ([ $month -eq $now_month ] && [ $year -eq $now_year ]) ||
482                ((([ $month -eq $(( $now_month - 1 )) ] &&
483                   [ $year -eq $now_year ]) ||
484                  ([ $month -eq 12 ] && [ $now_month -eq 1 ] &&
485                   [ $year -eq $(( $now_year - 1 )) ])) &&
486                 [ $day -ge $now_day ])
487             then apply=t; fi
488             ;;
489           week)
490             if [ $jdn -ge $(( $now_jdn - 7 )) ]; then apply=t; fi
491             ;;
492           *)
493             echo >&2 "$quis: unknown age symbol \`$age'"
494             exit 1
495             ;;
496         esac
497         case $apply in nil) continue ;; esac
498
499         ## Find the interval marker for this date.
500         case $ival in
501           daily)
502             marker=$date
503             ;;
504           weekly)
505             ydn=$(julian $year-01-01)
506             wk=$(( ($jdn - $ydn)/7 + 1 ))
507             marker=$year-w$wk
508             ;;
509           monthly)
510             marker=$year-$month
511             ;;
512           annually | yearly)
513             marker=$year
514             ;;
515           *)
516             echo >&2 "$quis: unknown interval symbol \`$ival'"
517             exit 1
518             ;;
519         esac
520
521         ## See if we've alredy retained something in this interval.
522         case $kept in
523           *:"$marker":*) ;;
524           *) keep=t kept=$kept$marker: ;;
525         esac
526
527       done <<EOF
528 $expire_policy
529 EOF
530     fi
531
532     case $keep in
533       t) echo RETAIN $date ;;
534       *) echo EXPIRE $date ;;
535     esac
536
537   done
538 }
539
540 ###--------------------------------------------------------------------------
541 ### Actually taking backups of filesystems.
542
543 MAXLOG=14
544 HASH=sha256
545 unset VOLUME
546
547 bkprc=0
548
549 hash_file () {
550   file=$1
551
552   case $HASH in
553     md5 | sha1 | sha224 | sha256 | sha384 | sha512)
554       set -- $(${HASH}sum <"$file")
555       echo "$1"
556       ;;
557     *)
558       set -- $(openssl dgst -$HASH <"$file")
559       echo "$2"
560       ;;
561   esac
562 }
563
564 remote_fshash () {
565   _hostrun $userat$host "
566         umask 077
567         mkdir -p $fshashdir
568         cd ${snapmnt#*:}
569         echo \"*** $host $fs $date\"; echo
570         rsync -rx --filter='dir-merge .rsync-backup' ./ |
571           fshash -c$fshashdir/$fs.bkp -a -H$HASH -frsync
572   " >new.fshash
573 }
574
575 local_fshash () {
576   { echo "*** $host $fs $date"; echo
577     fshash -c$STOREDIR/fshash.cache -H$HASH new/
578   } >$localmap
579 }
580
581 expire_backups () {
582   { seen=:
583     for i in *-*-*; do
584       i=${i%%.*}
585       case $i in *[!-0-9]*) continue ;; esac
586       case $seen in *:"$i":*) continue ;; esac
587       seen=$seen$i:
588       echo $i
589     done; } |
590   expire |
591   while read op date; do
592     case $op,$dryrun in
593       RETAIN,t)
594         echo >&2 "              --- keep   $date"
595         ;;
596       EXPIRE,t)
597         echo >&2 "              --- delete $date"
598         ;;
599       RETAIN,nil)
600         echo "keep   $date"
601         ;;
602       EXPIRE,nil)
603         echo "delete $date"
604         $verbose -n "   expire $date..."
605         rm -rf $date $date.*
606         delete_index $host $fs $date
607         $verbose " done"
608         ;;
609     esac
610   done
611 }
612
613 ## Backup hooks.
614 defhook setup
615 defhook precommit
616 defhook postcommit
617
618 backup_precommit_hook () {
619   host=$1 fs=$2 date=$3
620   ## Compatibility: You can override this hook in the configuration file for
621   ## special effects; but it's better to use `addhook precommit'.
622
623   :
624 }
625 addhook precommit backup_precommit_hook
626
627 backup_commit_hook () {
628   host=$1 fs=$2 date=$3
629   ## Compatibility: You can override this hook in the configuration file for
630   ## special effects; but it's better to use `addhook commit'.
631
632   :
633 }
634 addhook commit backup_commit_hook
635
636 do_backup () {
637   date=$1 fs=$2 fsarg=$3
638   ## Back up FS on the current host.
639
640   set -e
641   attempt=0
642   fshash_diff=nil
643
644   ## Run a hook beforehand.
645   set +e; runhook setup $host $fs $date; rc=$?; set -e
646   case $? in
647     0) ;;
648     99) log "BACKUP of $host:$fs SKIPPED by hook"; return 0 ;;
649     *) log "BACKUP of $host:$fs FAILED (hook returns $?)"; return $? ;;
650   esac
651
652   ## Report the start of this attempt.
653   log "START BACKUP of $host:$fs"
654
655   ## Maybe we need to retry the backup.
656   while :; do
657
658     ## Rig checksum variables to mismatch unless they're set later.
659     hrfs=REMOTE hlfs=LOCAL
660
661     ## Create and mount the remote snapshot.
662     case $dryrun in
663       t)
664         maybe snap_$snap $fs $fsarg
665         snapmnt="<snapshot>"
666         ;;
667       nil)
668         snapmnt=$(snap_$snap $snapargs $fs $fsarg) || return $?
669         ;;
670     esac
671     $verbose "  create snapshot"
672
673     ## If we had a fshash-mismatch, then clear out the potentially stale
674     ## entries, both locally and remotely.
675     case $fshash_diff in
676       nil) ;;
677       *)
678         $verbose "      prune cache"
679         run -stdin "local prune fshash" \
680           fshash -u -c$STOREDIR/fshash.cache -H$HASH new/ <$fshash_diff
681         run -stdin "@$host: prune fshash" \
682           _hostrun $userat$host <$fshash_diff \
683           "fshash -u -c$fshashdir/$fs.bkp -H$HASH ${snapmnt#*:}"
684         ;;
685     esac
686
687     ## Build the list of hardlink sources.
688     linkdests=""
689     for i in $host $like; do
690       d=$STOREDIR/$i/$fs/last/
691       if [ -d $d ]; then linkdests="$linkdests --link-dest=$d"; fi
692     done
693
694     ## Copy files from the remote snapshot.
695     maybe mkdir -p new/
696     case $dryrun in
697       t) $verbose "     running rsync" ;;
698       nil) $verbose -n "        running rsync..." ;;
699     esac
700     set +e
701     run "RSYNC of $host:$fs (snapshot on $snapmnt)" do_rsync \
702       $linkdests \
703       $rsyncargs \
704       $snapmnt/ new/
705     rc_rsync=$?
706     set -e
707     case $dryrun in nil) $verbose " done" ;; esac
708
709     ## Collect a map of the snapshot for verification purposes.
710     set +e
711     case $dryrun in
712       t) $verbose "     remote fshash" ;;
713       nil) $verbose -n "        remote fshash..." ;;
714     esac
715     run "@$host: fshash $fs" remote_fshash
716     rc_fshash=$?
717     set -e
718     case $dryrun in
719       nil)
720         hrfs=$(hash_file "new.fshash")
721         log "remote fshash $HASH checksum: $hrfs"
722         $verbose " done"
723         ;;
724       t)
725         hrfs=UNSET
726         ;;
727     esac
728
729     ## Remove the snapshot.
730     maybe unsnap_$snap $snapargs $fs $fsarg
731     $verbose "  remove snapshot"
732
733     ## If we failed to copy, then give up.
734     case $rc_rsync:$rc_fshash in
735       0:0) ;;
736       0:*) return $rc_fshash ;;
737       *) return $rc_rsync ;;
738     esac
739
740     ## Get a matching map of the files received.
741     maybe mkdir -m750 -p $STOREDIR/tmp/
742     localmap=$STOREDIR/tmp/fshash.$host.$fs.$date
743     case $dryrun in
744       t) $verbose "     local fshash" ;;
745       nil) $verbose -n "        local fshash..." ;;
746     esac
747     run "local fshash $host:$fs" local_fshash || return $?
748     case $dryrun in
749       nil)
750         hlfs=$(hash_file "$localmap")
751         log "local fshash $HASH checksum: $hlfs"
752         $verbose " done"
753         ;;
754       t)
755         hlfs=UNSET
756         ;;
757     esac
758
759     ## Compare the two maps.
760     set +e
761     fshash_diff=$STOREDIR/tmp/fshash-diff.$host.$fs.$date
762     run "compare fshash maps for $host:$fs" \
763       run_diff $fshash_diff new.fshash $localmap
764     rc_diff=$?
765     set -e
766     case $rc_diff in
767       0)
768         break
769         ;;
770       1)
771         if [ $attempt -ge $retry ]; then return $rc; fi
772         $verbose "      fshash mismatch; retrying"
773         attempt=$(( $attempt + 1 ))
774         ;;
775       *)
776         return $rc_diff
777         ;;
778     esac
779   done
780
781   ## Double-check the checksums.
782   if [ $hrfs != $hlfs ]; then
783     cat >&2 <<EOF
784 $0: INTERNAL ERROR: fshash $HASH checksum mismatch -- aborting
785         remote fshash checksum = $hrfs
786         local fshash checksum = $hlfs
787 EOF
788     exit 127
789   fi
790
791   ## Glorious success.
792   maybe rm -f $localmap
793   case $fshash_diff in nil) ;; *) maybe rm -f $fshash_diff ;; esac
794   $verbose "    fshash match"
795
796   ## Commit this backup.
797   case $dryrun in
798     nil)
799       runhook precommit $host $fs $date
800       mv new $date
801       mv new.fshash $date.fshash
802       insert_index $host $fs $date $VOLUME
803       runhook commit $host $fs $date
804       mkdir hack
805       ln -s $date hack/last
806       mv hack/last .
807       rmdir hack
808       ;;
809   esac
810   $verbose "    commit"
811
812   ## Expire old backups.
813   case "${expire_policy+t},${default_policy+t}" in
814     ,t) expire_policy=$default_policy ;;
815   esac
816   case "${expire_policy+t},$dryrun" in
817     t,nil) run "expiry for $host:$fs" expire_backups ;;
818     t,t) expire_backups ;;
819   esac
820   clear_policy=t
821
822   ## Report success.
823   case $dryrun in
824     t) log "END BACKUP of $host:$fs" ;;
825     nil) log "SUCCESSFUL BACKUP of $host:$fs" ;;
826   esac
827 }
828
829 run_backup_cmd () {
830   fs=$1 date=$2 cmd=$3; shift 3
831   ## try_backup FS DATE COMMAND ARGS ...
832   ##
833   ## Run COMMAND ARGS to back up filesystem FS on the current host,
834   ## maintaining a log, and checking whether it worked.  The caller has
835   ## usually worked out the DATE in order to set up the filesystem, and we
836   ## need it to name the log file properly.
837
838   ## Find a name for the log file.  In unusual circumstances, we may have
839   ## deleted old logs from today, so just checking for an unused sequence
840   ## number is insufficient.  Instead, check all of the logfiles for today,
841   ## and use a sequence number that's larger than any of them.
842   case $dryrun in
843     t)
844       log=/dev/null
845       ;;
846     nil)
847       seq=1
848       for i in "$logdir/$host/$fs.$date#"*; do
849         tail=${i##*#}
850         case "$tail" in [!1-9]* | *[!0-9]*) continue ;; esac
851         if [ -f "$i" -a $tail -ge $seq ]; then seq=$(( tail + 1 )); fi
852       done
853       log="$logdir/$host/$fs.$date#$seq"
854       ;;
855   esac
856
857   ## Run the backup command.
858   case $dryrun in nil) mkdir -p $logdir/$host ;; esac
859   if ! "$cmd" "$@" 9>$log 1>&9; then
860     echo >&2
861     echo >&2 "$quis: backup of $host:$fs FAILED!"
862     bkprc=1
863   fi
864
865   ## Clear away any old logfiles.
866   remove_old_logfiles "$logdir/$host/$fs"
867 }
868
869 backup () {
870   ## backup FS[:ARG] ...
871   ##
872   ## Back up the filesystems on the currently selected host using the
873   ## currently selected snapshot type.
874
875   ## Make sure that there's a store volume.  We must do this here rather than
876   ## in the main body of the script, since the configuration file needs a
877   ## chance to override STOREDIR.
878   if ! [ -r $STOREDIR/.rsync-backup-store ]; then
879     echo >&2 "$quis: no backup volume mounted"
880     exit 15
881   fi
882
883   ## Read the volume name if we don't have one already.  Again, this allows
884   ## the configuration file to provide a volume name.
885   case "${VOLUME+t}${VOLUME-nil}" in
886     nil) VOLUME=$(cat $METADIR/volume) ;;
887   esac
888
889   ## Back up each requested file system in turn.
890   for fs in "$@"; do
891
892     ## Parse the argument.
893     case $fs in
894       *:*) fsarg=${fs#*:} fs=${fs%%:*} ;;
895       *) fsarg="" ;;
896     esac
897     $verbose "  filesystem $fs"
898
899     ## Move to the store directory and set up somewhere to put this backup.
900     cd $STOREDIR
901     case $dryrun in
902       nil)
903         if [ ! -d $host ]; then
904           mkdir -m755 $host
905           chown root:root $host
906         fi
907         if [ ! -d $host/$fs ]; then
908           mkdir -m750 $host/$fs
909           chown root:backup $host/$fs
910         fi
911         ;;
912     esac
913     cd $host/$fs
914
915     ## Find out if we've already copied this filesystem today.
916     date=$(date +%Y-%m-%d)
917     if [ $dryrun = nil ] && [ -d $date ]; then
918       $verbose "        already dumped"
919       continue
920     fi
921
922     ## Do the backup of this filesystem.
923     run_backup_cmd $fs $date do_backup $date $fs $fsarg
924   done
925 }
926
927 ###--------------------------------------------------------------------------
928 ### Configuration functions.
929
930 defhook start
931 defhook end
932
933 done_first_host_p=nil
934
935 host () {
936   host=$1
937   like= userat=
938   case $done_first_host_p in
939     nil) runhook start; done_first_host_p=t ;;
940   esac
941   case "${expire_policy+t},${default_policy+t}" in
942     t,) default_policy=$expire_policy ;;
943   esac
944   unset expire_policy
945   $verbose "host $host"
946 }
947
948 snaptype () { snap=$1; shift; snapargs="$*"; retry=1; }
949 rsyncargs () { rsyncargs="$*"; }
950 like () { like="$*"; }
951 retry () { retry="$*"; }
952 user () { userat="$*@"; }
953
954 retain () {
955   case $clear_policy in t) unset expire_policy; clear_policy=nil ;; esac
956   expire_policy="${expire_policy+$expire_policy
957 }$*"
958 }
959
960 ###--------------------------------------------------------------------------
961 ### Read the configuration and we're done.
962
963 usage () {
964   echo "usage: $quis [-nv] [-c CONF]"
965 }
966
967 version () {
968   echo "$quis version $VERSION"
969 }
970
971 whine () { echo >&8 "$@"; }
972
973 while getopts "hVvc:n" opt; do
974   case "$opt" in
975     h) usage; exit 0 ;;
976     V) version; config; exit 0 ;;
977     v) verbose=whine ;;
978     c) conf=$OPTARG ;;
979     n) dryrun=t ;;
980     *) exit 1 ;;
981   esac
982 done
983 shift $((OPTIND - 1))
984 case $# in 0) ;; *) usage >&2; exit 1 ;; esac
985 exec 8>&1
986
987 . "$conf"
988
989 runhook end $bkprc
990 case "$bkprc" in
991   0) $verbose "All backups successful" ;;
992   *) $verbose "Backups FAILED" ;;
993 esac
994
995 ###----- That's all, folks --------------------------------------------------
996
997 exit $bkprc