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