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