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