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