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