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