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