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