chiark / gitweb /
rsync-backup.{in,8}: Introduce `hook' concept.
[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##*/}
30
31VERSION=@VERSION@
32mntbkpdir=@mntbkpdir@
33logdir=@logdir@
34fshashdir=@fshashdir@
35conf=@sysconfdir@/rsync-backup.conf
36
37verbose=:
3f496b2b 38dryrun=nil
f6b4ffdc
MW
39
40###--------------------------------------------------------------------------
41### Utility functions.
42
43RSYNCOPTS="--verbose"
44
45do_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
60log () {
3f496b2b
MW
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
72maybe () {
73 ## Run CMD, if this isn't a dry run.
74
75 case $dryrun in
76 t) echo >&2 " +++ $*" ;;
77 nil) "$@" ;;
78 esac
f6b4ffdc
MW
79}
80
4f618c54
MW
81copy () {
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
f6b4ffdc
MW
90run () {
91 tag=$1 cmd=$2; shift 2
92 ## Run CMD, logging its output in a pleasing manner.
93
3f496b2b
MW
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; ) |
4f618c54
MW
106 copy "|" >&4; } 2>&1 |
107 copy "*" >&4; } 4>&1 |
3f496b2b
MW
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 ;;
f6b4ffdc
MW
115 esac
116 return $rc
117}
118
119localp () {
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
129hostrun () {
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"
fdd73e22 136 else run "@$host: $tag" ssh $userat$host "$cmd"
f6b4ffdc
MW
137 fi
138}
139
140_hostrun () {
141 h=$1 cmd=$2
f8d0b27d 142 ## Like hostrun, but without the complicated logging, and targetted at a
f6b4ffdc
MW
143 ## specific host.
144
145 if localp $h; then sh -c "$cmd"
146 else ssh $h "$cmd"
147 fi
148}
149
150hostpath () {
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
fdd73e22 156 else echo $userat$host:$path
f6b4ffdc
MW
157 fi
158}
159
9b1d71c6
MW
160defhook () {
161 hook=$1
162 ## Define a hook called HOOK.
163
164 eval hk_$hook=
165}
166
167addhook () {
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
175runhook () {
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
a8447303
MW
185###--------------------------------------------------------------------------
186### Database operations.
187
188INDEXDB=@pkglocalstatedir@/index.db
189
190insert_index () {
191 host=$1 fs=$2 date=$3 vol=$4
192
193 if [ -f "$INDEXDB" ]; then
194 sqlite3 "$INDEXDB" <<EOF
195INSERT INTO idx (host, fs, date, vol)
196 VALUES ('$host', '$fs', '$date', '$vol');
197EOF
198 fi
199}
200
201delete_index () {
202 host=$1 fs=$2 date=$3
203
204 if [ -f "$INDEXDB" ]; then
205 sqlite3 "$INDEXDB" <<EOF
206DELETE FROM idx WHERE
1da0cd47 207 host = '$host' AND fs = '$fs' AND date = '$date';
a8447303
MW
208EOF
209 fi
210}
211
f6b4ffdc
MW
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.
223snap_live () { hostpath "$2"; }
224unsnap_live () { :; }
225
226## Fake snapshot by remounting a live filesystem read-only. Useful if the
227## underlying storage isn't in LVM.
228
229snap_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
242unsnap_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
257SNAPSIZE="-l10%ORIGIN"
258SNAPDIR=@mntbkpdir@/snap
259
260snap_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
273unsnap_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
292do_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
fdd73e22 338 _hostrun >&2 3>&- $userat$lvhost \
f6b4ffdc
MW
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 *)
fdd73e22 349 _hostrun >&2 3>&- $userat$lvhost "lvremove -f $vg/$lv.bkp" || :
f6b4ffdc
MW
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.
fdd73e22 376 _hostrun >&2 $userat$lvhost "
f6b4ffdc
MW
377 mkdir -p $SNAPDIR/$lv
378 mount -oro /dev/$vg/$lv.bkp $SNAPDIR/$lv"
379}
380
381snap_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
390unsnap_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
400parsedate () {
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
411julian () {
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
447expire () {
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
530EOF
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
544STOREDIR=@mntbkpdir@/store
a8447303 545METADIR=@mntbkpdir@/meta
f6b4ffdc
MW
546MAXLOG=14
547HASH=sha256
a8447303 548unset VOLUME
f6b4ffdc
MW
549
550bkprc=0
551
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
f6b4ffdc 630
9b1d71c6
MW
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
f6b4ffdc
MW
639 ## Report the start of this attempt.
640 log "START BACKUP of $host:$fs"
641
5675acda
MW
642 ## Maybe we need to retry the backup.
643 while :; do
f6b4ffdc 644
5675acda 645 ## Create and mount the remote snapshot.
3f496b2b
MW
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
5675acda 655 $verbose " create snapshot"
f6b4ffdc 656
5675acda
MW
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
f6b4ffdc 663
5675acda 664 ## Copy files from the remote snapshot.
3f496b2b
MW
665 maybe mkdir -p new/
666 case $dryrun in
667 t) $verbose " running rsync" ;;
668 nil) $verbose -n " running rsync..." ;;
669 esac
5675acda
MW
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
3f496b2b 677 case $dryrun in nil) $verbose " done" ;; esac
f6b4ffdc 678
5675acda
MW
679 ## Collect a map of the snapshot for verification purposes.
680 set +e
3f496b2b
MW
681 case $dryrun in
682 t) $verbose " remote fshash" ;;
683 nil) $verbose -n " remote fshash..." ;;
684 esac
5675acda
MW
685 run "@$host: fshash $fs" remote_fshash
686 rc_fshash=$?
687 set -e
3f496b2b 688 case $dryrun in nil) $verbose " done" ;; esac
f6b4ffdc 689
5675acda 690 ## Remove the snapshot.
3f496b2b 691 maybe unsnap_$snap $snapargs $fs $fsarg
5675acda 692 $verbose " remove snapshot"
f6b4ffdc 693
5675acda
MW
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.
3f496b2b 702 maybe mkdir -m750 -p $STOREDIR/tmp/
5675acda 703 localmap=$STOREDIR/tmp/fshash.$host.$fs.$date
3f496b2b
MW
704 case $dryrun in
705 t) $verbose " local fshash" ;;
706 nil) $verbose -n " local fshash..." ;;
707 esac
5675acda 708 run "local fshash $host:$fs" local_fshash || return $?
3f496b2b 709 case $dryrun in nil) $verbose " done" ;; esac
5675acda
MW
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
f6b4ffdc 730
5675acda 731 ## Glorious success.
3f496b2b 732 maybe rm -f $localmap
f6b4ffdc
MW
733 $verbose " fshash match"
734
735 ## Commit this backup.
3f496b2b
MW
736 case $dryrun in
737 nil)
9b1d71c6 738 runhook precommit $host $fs $date
3f496b2b
MW
739 mv new $date
740 mv new.fshash $date.fshash
a8447303 741 insert_index $host $fs $date $VOLUME
9b1d71c6 742 runhook commit $host $fs $date
3f496b2b
MW
743 mkdir hack
744 ln -s $date hack/last
745 mv hack/last .
746 rmdir hack
747 ;;
748 esac
f6b4ffdc
MW
749 $verbose " commit"
750
751 ## Expire old backups.
f8d0b27d
MW
752 case "${expire_policy+t},${default_policy+t}" in
753 ,t) expire_policy=$default_policy ;;
754 esac
3f496b2b
MW
755 case "${expire_policy+t},$dryrun" in
756 t,nil) run "expiry for $host:$fs" expire_backups ;;
757 t,t) expire_backups ;;
f6b4ffdc 758 esac
f8d0b27d 759 clear_policy=t
f6b4ffdc
MW
760
761 ## Report success.
3f496b2b
MW
762 case $dryrun in
763 t) log "END BACKUP of $host:$fs" ;;
764 nil) log "SUCCESSFUL BACKUP of $host:$fs" ;;
765 esac
f6b4ffdc
MW
766}
767
3aa4fd30
MW
768run_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
f6b4ffdc
MW
823backup () {
824 ## backup FS[:ARG] ...
825 ##
826 ## Back up the filesystems on the currently selected host using the
827 ## currently selected snapshot type.
828
6037bdb3
MW
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
a8447303
MW
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
6037bdb3 843 ## Back up each requested file system in turn.
f6b4ffdc
MW
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
3f496b2b
MW
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
f6b4ffdc
MW
867 cd $host/$fs
868
869 ## Find out if we've already copied this filesystem today.
870 date=$(date +%Y-%m-%d)
3f496b2b 871 if [ $dryrun = nil ] && [ -d $date ]; then
f6b4ffdc
MW
872 $verbose " already dumped"
873 continue
874 fi
875
f6b4ffdc 876 ## Do the backup of this filesystem.
3aa4fd30 877 run_backup_cmd $fs $date do_backup $date $fs $fsarg
f6b4ffdc
MW
878 done
879}
880
881###--------------------------------------------------------------------------
882### Configuration functions.
883
9b1d71c6
MW
884defhook start
885defhook end
886
887done_first_host_p=nil
888
f8d0b27d
MW
889host () {
890 host=$1
fdd73e22 891 like= userat=
9b1d71c6
MW
892 case $done_first_host_p in
893 nil) runhook start; done_first_host_p=t ;;
894 esac
f8d0b27d
MW
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
5675acda 902snaptype () { snap=$1; shift; snapargs="$*"; retry=0; }
f6b4ffdc
MW
903rsyncargs () { rsyncargs="$*"; }
904like () { like="$*"; }
5675acda 905retry () { retry="$*"; }
fdd73e22 906user () { userat="$*@"; }
f6b4ffdc
MW
907
908retain () {
f8d0b27d 909 case $clear_policy in t) unset expire_policy; clear_policy=nil ;; esac
f6b4ffdc
MW
910 expire_policy="${expire_policy+$expire_policy
911}$*"
912}
913
914###--------------------------------------------------------------------------
915### Read the configuration and we're done.
916
917usage () {
5b4c55ab 918 echo "usage: $quis [-nv] [-c CONF]"
f6b4ffdc
MW
919}
920
921version () {
922 echo "$quis version $VERSION"
923}
924
925config () {
926 echo
927 cat <<EOF
928conf = $conf
929mntbkpdir = $mntbkpdir
930fshashdir = $fshashdir
931logdir = $logdir
932EOF
933}
934
935whine () { echo >&8 "$@"; }
936
3f496b2b 937while getopts "hVvc:n" opt; do
f6b4ffdc
MW
938 case "$opt" in
939 h) usage; exit 0 ;;
940 V) version; config; exit 0 ;;
941 v) verbose=whine ;;
942 c) conf=$OPTARG ;;
3f496b2b 943 n) dryrun=t ;;
f6b4ffdc
MW
944 *) exit 1 ;;
945 esac
946done
947shift $((OPTIND - 1))
948case $# in 0) ;; *) usage >&2; exit 1 ;; esac
949exec 8>&1
950
951. "$conf"
9b1d71c6
MW
952
953runhook end $bkprc
1da0cd47
MW
954case "$bkprc" in
955 0) $verbose "All backups successful" ;;
956 *) $verbose "Backups FAILED" ;;
957esac
f6b4ffdc
MW
958
959###----- That's all, folks --------------------------------------------------
960
961exit $bkprc