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