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