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