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