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