5 ### (c) 2012 Mark Wooding
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of the `rsync-backup' program.
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.
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.
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.
28 thishost=$(hostname -s)
35 ###--------------------------------------------------------------------------
36 ### Utility functions.
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.
46 --archive --hard-links --numeric-ids --del \
51 --filter="dir-merge .rsync-backup" \
61 now=$(date +"%Y-%m-%d %H:%M:%S %z")
68 ## Run CMD, if this isn't a dry run.
71 t) echo >&2 " +++ $*" ;;
78 ## Copy lines from stdin to stdout, adding PREFIX.
80 while IFS= read -r line; do
81 printf "%s %s\n" "$prefix" "$line"
89 -stdin) stdinp=t; shift ;;
94 tag=$1 cmd=$2; shift 2
95 ## Run CMD, logging its output in a pleasing manner.
99 echo >&2 " *** RUN $tag"
100 echo >&2 " +++ $cmd $*"
106 case $stdinp in nil) exec </dev/null ;; esac
108 "$cmd" "$@" 3>&- 4>&- 5>&- 9>&-
110 copy "|" >&4; } 2>&1 |
111 copy "*" >&4; } 4>&1 |
116 *) log "FAIL $tag (rc = $rc)" ;;
125 ## Write a unified diff from OLD to NEW, to OUT.
127 set +e; diff -u "$old" "$new" >"$out"; rc=$?; set -e
128 case $rc in 1) cat "$out" ;; esac
134 ## Answer whether H is a local host.
137 "$thishost") return 0 ;;
144 ## Run CMD on the current host. If the host seems local then run the
145 ## command through a local shell; otherwise run it through ssh(1). Either
146 ## way it will be processed by a shell.
148 if localp $host; then run "@$host: $tag" sh -c "$cmd"
149 else run "@$host: $tag" ssh $userat$host "$cmd"
155 ## Like hostrun, but without the complicated logging, and targetted at a
158 if localp $h; then sh -c "$cmd"
165 ## Output (to stdout) either PATH or HOST:PATH, choosing the former if the
166 ## current host is local.
168 if localp $host; then echo $path
169 else echo $userat$host:$path
175 ## Define a hook called HOOK.
182 ## Add command CMD to the hook HOOK.
184 eval old=\$hk_$hook; new="$old $cmd"
190 ## Invoke HOOK, passing it the remaining arguments.
194 if ! $cmd "$@"; then return $?; fi
198 remove_old_logfiles () {
200 ## Remove old logfiles with names of the form BASE.DATE#N, so that there
201 ## are at most $MAXLOG of them.
203 ## Count up the logfiles.
205 for i in "$base".*; do
206 if [ ! -f "$i" ]; then continue; fi
210 ## If there are too many, go through and delete some early ones.
211 if [ $dryrun = nil ] && [ $nlog -gt $MAXLOG ]; then
212 n=$(( nlog - MAXLOG ))
213 for i in "$base".*; do
214 if [ ! -f "$i" ]; then continue; fi
217 if [ $n -eq 0 ]; then break; fi
222 ###--------------------------------------------------------------------------
223 ### Database operations.
226 host=$1 fs=$2 date=$3 vol=$4
228 if [ -f "$INDEXDB" ]; then
229 sqlite3 "$INDEXDB" <<EOF
230 INSERT INTO idx (host, fs, date, vol)
231 VALUES ('$host', '$fs', '$date', '$vol');
237 host=$1 fs=$2 date=$3
239 if [ -f "$INDEXDB" ]; then
240 sqlite3 "$INDEXDB" <<EOF
241 DELETE FROM idx WHERE
242 host = '$host' AND fs = '$fs' AND date = '$date';
247 ###--------------------------------------------------------------------------
248 ### Snapshot handling.
250 ## Snapshot protocol. Each snapshot type has a pair of functions snap_TYPE
251 ## and unsnap_TYPE. Each is given the current snapshot arguments and the
252 ## filesystem name to back up. The snap_TYPE function should create and
253 ## mount the snapshot and output an rsync(1) path to where the filesystem can
254 ## be copied; the unsnap_TYPE function should unmount and tear down the
257 ## Fake snapshot by not doing anything. Use only if you have no choice.
258 snap_live () { hostpath "$2"; }
259 unsnap_live () { :; }
261 ## Fake snapshot by remounting a live filesystem read-only. Useful if the
262 ## underlying storage isn't in LVM.
267 ## Place a marker in the filesystem so we know why it was made readonly.
268 ## (Also this serves to ensure that the filesystem was writable before.)
269 hostrun "snap-ro $mnt" "
270 echo rsync-backup >$mnt/.lock
271 mount -oremount,ro $mnt" || return $?
280 ## Check that the filesystem still has our lock marker.
281 hostrun "unsnap-ro $mnt" "
282 case \$(cat $mnt/.lock) in
284 *) echo unlocked by someone else; exit 31 ;;
286 mount -oremount,rw $mnt
287 rm $mnt/.lock" || return $?
290 ## Snapshot using LVM.
292 SNAPSIZE="-l10%ORIGIN"
297 ## Make the snapshot.
298 hostrun "snap-lvm $vg/$lv" "
299 lvcreate --snapshot -n$lv.bkp $SNAPSIZE $vg/$lv
300 mkdir -p $SNAPDIR/$lv
301 mount -oro /dev/$vg/$lv.bkp $SNAPDIR/$lv" || return $?
304 hostpath $SNAPDIR/$lv
310 ## Remove the snapshot. Sometimes LVM doesn't notice that the snapshot is
311 ## no longer in open immdiately, so try several times.
312 hostrun "unsnap-lvm $vg/$lv" "
316 if lvremove -f $vg/$lv.bkp; then rc=0; break; fi
319 exit $rc" || return $?
322 ## Complicated snapshot using LVM, where the volume group and filesystem are
323 ## owned by different machines, so they need to be synchronized during the
327 lvhost=$1 vg=$2 lv=$3 fshost=$4 fsdir=$5
329 ## Engage in the rfreezefs protocol with the filesystem host. This
330 ## involves some hairy plumbing. We want to get exit statuses out of both
333 ssh $fshost rfreezefs $fsdir | {
336 ## Read the codebook from the remote end.
342 TOKEN) eval tok_$2=$3 ;;
343 READY) ready=t; break ;;
345 echo >&2 "$quis: unexpected keyword $1 (rfreezefs to $rhost)"
352 echo >&2 "$quis: unexpected eof (rfreezefs to $rhost)"
357 ## Connect to the filesystem host's TCP port and get it to freeze its
359 exec 3<>/dev/tcp/$fshost/$port
365 echo >&2 "$quis: unexpected token $tok (rfreezefs $fsdir on $fshost)"
370 ## Get the volume host to create the snapshot.
372 _hostrun >&2 3>&- $userat$lvhost \
373 "lvcreate --snapshot -n$lv.bkp $SNAPSIZE $vg/$lv"
377 ## The filesystem can thaw now.
383 _hostrun >&2 3>&- $userat$lvhost "lvremove -f $vg/$lv.bkp" || :
384 echo >&2 "$quis: unexpected token $tok (rfreezefs $fsdir on $fshost)"
393 ## Sift through the wreckage to find out what happened.
394 rc_rfreezefs=${PIPESTATUS[0]} rc_snapshot=${PIPESTATUS[1]}
396 case $rc_rfreezefs:$rc_snapshot in
400 echo >&2 "$quis: EMERGENCY failed to thaw $fsdir on $fshost!"
404 echo >&2 "$quis: failed to snapshot $vg/$lv ($fsdir on $fshost)"
409 ## Mount the snapshot on the volume host.
410 _hostrun >&2 $userat$lvhost "
411 mkdir -p $SNAPDIR/$lv
412 mount -oro /dev/$vg/$lv.bkp $SNAPDIR/$lv"
416 rhost=$1 vg=$2 lv=$3 rfs=$4
419 run "snap-rfreezefs $host:$vg/$lv $rhost:$rfs" \
420 do_rfreezefs $host $vg $lv $rhost $rfs || return $?
421 hostpath $SNAPDIR/$lv
424 unsnap_rfreezefs () {
426 ## Unshapping is the same as for plain LVM.
427 rhost=$1 vg=$2 lv=$3 rfs=$4
431 ###--------------------------------------------------------------------------
432 ### Expiry computations.
435 ## Read dates on stdin; write to stdout `EXPIRE date' for dates which
436 ## should be expired and `RETAIN date' for dates which should be retained.
438 ## Get the current date and convert it into useful forms.
439 now=$(date +%Y-%m-%d)
441 now_jdn=$(julian $now) now_year=$year now_month=$month now_day=$day
444 ## Work through each date in the input.
448 ## Convert the date into a useful form.
452 ## Work through the policy list.
453 if [ $jdn -le $now_jdn ]; then
454 while read ival age; do
456 ## Decide whether the policy entry applies to this date.
463 if [ $year -eq $now_year ] ||
464 ([ $year -eq $(( $now_year - 1 )) ] &&
465 [ $month -ge $now_month ])
469 if ([ $month -eq $now_month ] && [ $year -eq $now_year ]) ||
470 ((([ $month -eq $(( $now_month - 1 )) ] &&
471 [ $year -eq $now_year ]) ||
472 ([ $month -eq 12 ] && [ $now_month -eq 1 ] &&
473 [ $year -eq $(( $now_year - 1 )) ])) &&
474 [ $day -ge $now_day ])
478 if [ $jdn -ge $(( $now_jdn - 7 )) ]; then apply=t; fi
481 echo >&2 "$quis: unknown age symbol \`$age'"
485 case $apply in nil) continue ;; esac
487 ## Find the interval marker for this date.
493 ydn=$(julian $year-01-01)
494 wk=$(( ($jdn - $ydn)/7 + 1 ))
504 echo >&2 "$quis: unknown interval symbol \`$ival'"
509 ## See if we've alredy retained something in this interval.
512 *) keep=t kept=$kept$marker: ;;
521 t) echo RETAIN $date ;;
522 *) echo EXPIRE $date ;;
528 ###--------------------------------------------------------------------------
529 ### Actually taking backups of filesystems.
538 _hostrun $userat$host "
542 echo \"*** $host $fs $date\"; echo
543 rsync -rx --filter='dir-merge .rsync-backup' ./ |
544 fshash -c$fshashdir/$fs.bkp -a -H$HASH -frsync
549 { echo "*** $host $fs $date"; echo
550 fshash -c$STOREDIR/fshash.cache -H$HASH new/
558 case $i in *[!-0-9]*) continue ;; esac
559 case $seen in *:"$i":*) continue ;; esac
564 while read op date; do
567 echo >&2 " --- keep $date"
570 echo >&2 " --- delete $date"
577 $verbose -n " expire $date..."
579 delete_index $host $fs $date
591 backup_precommit_hook () {
592 host=$1 fs=$2 date=$3
593 ## Compatibility: You can override this hook in the configuration file for
594 ## special effects; but it's better to use `addhook precommit'.
598 addhook precommit backup_precommit_hook
600 backup_commit_hook () {
601 host=$1 fs=$2 date=$3
602 ## Compatibility: You can override this hook in the configuration file for
603 ## special effects; but it's better to use `addhook commit'.
607 addhook commit backup_commit_hook
610 date=$1 fs=$2 fsarg=$3
611 ## Back up FS on the current host.
617 ## Run a hook beforehand.
618 set +e; runhook setup $host $fs $date; rc=$?; set -e
621 99) log "BACKUP of $host:$fs SKIPPED by hook"; return 0 ;;
622 *) log "BACKUP of $host:$fs FAILED (hook returns $?)"; return $? ;;
625 ## Report the start of this attempt.
626 log "START BACKUP of $host:$fs"
628 ## Maybe we need to retry the backup.
631 ## Create and mount the remote snapshot.
634 maybe snap_$snap $fs $fsarg
638 snapmnt=$(snap_$snap $snapargs $fs $fsarg) || return $?
641 $verbose " create snapshot"
643 ## If we had a fshash-mismatch, then clear out the potentially stale
644 ## entries, both locally and remotely.
648 $verbose " prune cache"
649 run -stdin "local prune fshash" \
650 fshash -u -c$STOREDIR/fshash.cache -H$HASH new/ <$fshash_diff
651 run -stdin "@$host: prune fshash" \
652 _hostrun $userat$host <$fshash_diff \
653 "fshash -u -c$fshashdir/$fs.bkp -H$HASH ${snapmnt#*:}"
657 ## Build the list of hardlink sources.
659 for i in $host $like; do
660 d=$STOREDIR/$i/$fs/last/
661 if [ -d $d ]; then linkdests="$linkdests --link-dest=$d"; fi
664 ## Copy files from the remote snapshot.
667 t) $verbose " running rsync" ;;
668 nil) $verbose -n " running rsync..." ;;
671 run "RSYNC of $host:$fs (snapshot on $snapmnt)" do_rsync \
677 case $dryrun in nil) $verbose " done" ;; esac
679 ## Collect a map of the snapshot for verification purposes.
682 t) $verbose " remote fshash" ;;
683 nil) $verbose -n " remote fshash..." ;;
685 run "@$host: fshash $fs" remote_fshash
688 case $dryrun in nil) $verbose " done" ;; esac
690 ## Remove the snapshot.
691 maybe unsnap_$snap $snapargs $fs $fsarg
692 $verbose " remove snapshot"
694 ## If we failed to copy, then give up.
695 case $rc_rsync:$rc_fshash in
697 0:*) return $rc_fshash ;;
698 *) return $rc_rsync ;;
701 ## Get a matching map of the files received.
702 maybe mkdir -m750 -p $STOREDIR/tmp/
703 localmap=$STOREDIR/tmp/fshash.$host.$fs.$date
705 t) $verbose " local fshash" ;;
706 nil) $verbose -n " local fshash..." ;;
708 run "local fshash $host:$fs" local_fshash || return $?
709 case $dryrun in nil) $verbose " done" ;; esac
711 ## Compare the two maps.
713 fshash_diff=$STOREDIR/tmp/fshash-diff.$host.$fs.$date
714 run "compare fshash maps for $host:$fs" \
715 run_diff $fshash_diff new.fshash $localmap
723 if [ $attempt -ge $retry ]; then return $rc; fi
724 $verbose " fshash mismatch; retrying"
725 attempt=$(( $attempt + 1 ))
734 maybe rm -f $localmap
735 case $fshash_diff in nil) ;; *) maybe rm -f $fshash_diff ;; esac
736 $verbose " fshash match"
738 ## Commit this backup.
741 runhook precommit $host $fs $date
743 mv new.fshash $date.fshash
744 insert_index $host $fs $date $VOLUME
745 runhook commit $host $fs $date
747 ln -s $date hack/last
754 ## Expire old backups.
755 case "${expire_policy+t},${default_policy+t}" in
756 ,t) expire_policy=$default_policy ;;
758 case "${expire_policy+t},$dryrun" in
759 t,nil) run "expiry for $host:$fs" expire_backups ;;
760 t,t) expire_backups ;;
766 t) log "END BACKUP of $host:$fs" ;;
767 nil) log "SUCCESSFUL BACKUP of $host:$fs" ;;
772 fs=$1 date=$2 cmd=$3; shift 3
773 ## try_backup FS DATE COMMAND ARGS ...
775 ## Run COMMAND ARGS to back up filesystem FS on the current host,
776 ## maintaining a log, and checking whether it worked. The caller has
777 ## usually worked out the DATE in order to set up the filesystem, and we
778 ## need it to name the log file properly.
780 ## Find a name for the log file. In unusual circumstances, we may have
781 ## deleted old logs from today, so just checking for an unused sequence
782 ## number is insufficient. Instead, check all of the logfiles for today,
783 ## and use a sequence number that's larger than any of them.
790 for i in "$logdir/$host/$fs.$date#"*; do
792 case "$tail" in [!1-9]* | *[!0-9]*) continue ;; esac
793 if [ -f "$i" -a $tail -ge $seq ]; then seq=$(( tail + 1 )); fi
795 log="$logdir/$host/$fs.$date#$seq"
799 ## Run the backup command.
800 case $dryrun in nil) mkdir -p $logdir/$host ;; esac
801 if ! "$cmd" "$@" 9>$log 1>&9; then
803 echo >&2 "$quis: backup of $host:$fs FAILED!"
807 ## Clear away any old logfiles.
808 remove_old_logfiles "$logdir/$host/$fs"
812 ## backup FS[:ARG] ...
814 ## Back up the filesystems on the currently selected host using the
815 ## currently selected snapshot type.
817 ## Make sure that there's a store volume. We must do this here rather than
818 ## in the main body of the script, since the configuration file needs a
819 ## chance to override STOREDIR.
820 if ! [ -r $STOREDIR/.rsync-backup-store ]; then
821 echo >&2 "$quis: no backup volume mounted"
825 ## Read the volume name if we don't have one already. Again, this allows
826 ## the configuration file to provide a volume name.
827 case "${VOLUME+t}${VOLUME-nil}" in
828 nil) VOLUME=$(cat $METADIR/volume) ;;
831 ## Back up each requested file system in turn.
834 ## Parse the argument.
836 *:*) fsarg=${fs#*:} fs=${fs%%:*} ;;
839 $verbose " filesystem $fs"
841 ## Move to the store directory and set up somewhere to put this backup.
845 if [ ! -d $host ]; then
847 chown root:root $host
849 if [ ! -d $host/$fs ]; then
850 mkdir -m750 $host/$fs
851 chown root:backup $host/$fs
857 ## Find out if we've already copied this filesystem today.
858 date=$(date +%Y-%m-%d)
859 if [ $dryrun = nil ] && [ -d $date ]; then
860 $verbose " already dumped"
864 ## Do the backup of this filesystem.
865 run_backup_cmd $fs $date do_backup $date $fs $fsarg
869 ###--------------------------------------------------------------------------
870 ### Configuration functions.
875 done_first_host_p=nil
880 case $done_first_host_p in
881 nil) runhook start; done_first_host_p=t ;;
883 case "${expire_policy+t},${default_policy+t}" in
884 t,) default_policy=$expire_policy ;;
887 $verbose "host $host"
890 snaptype () { snap=$1; shift; snapargs="$*"; retry=1; }
891 rsyncargs () { rsyncargs="$*"; }
892 like () { like="$*"; }
893 retry () { retry="$*"; }
894 user () { userat="$*@"; }
897 case $clear_policy in t) unset expire_policy; clear_policy=nil ;; esac
898 expire_policy="${expire_policy+$expire_policy
902 ###--------------------------------------------------------------------------
903 ### Read the configuration and we're done.
906 echo "usage: $quis [-nv] [-c CONF]"
910 echo "$quis version $VERSION"
913 whine () { echo >&8 "$@"; }
915 while getopts "hVvc:n" opt; do
918 V) version; config; exit 0 ;;
925 shift $((OPTIND - 1))
926 case $# in 0) ;; *) usage >&2; exit 1 ;; esac
933 0) $verbose "All backups successful" ;;
934 *) $verbose "Backups FAILED" ;;
937 ###----- That's all, folks --------------------------------------------------