X-Git-Url: https://git.distorted.org.uk/~mdw/rsync-backup/blobdiff_plain/a8447303fe9883920c783acae85c76caeaa78c32..HEAD:/rsync-backup.in diff --git a/rsync-backup.in b/rsync-backup.in index ac9cfdc..a48d58a 100644 --- a/rsync-backup.in +++ b/rsync-backup.in @@ -27,12 +27,7 @@ set -e thishost=$(hostname -s) quis=${0##*/} - -VERSION=@VERSION@ -mntbkpdir=@mntbkpdir@ -logdir=@logdir@ -fshashdir=@fshashdir@ -conf=@sysconfdir@/rsync-backup.conf +. @pkgdatadir@/lib.sh verbose=: dryrun=nil @@ -78,7 +73,24 @@ maybe () { esac } +copy () { + prefix=$1 + ## Copy lines from stdin to stdout, adding PREFIX. + + while IFS= read -r line; do + printf "%s %s\n" "$prefix" "$line" + done +} + run () { + stdinp=nil + while :; do + case $1 in + -stdin) stdinp=t; shift ;; + --) shift; break ;; + *) break ;; + esac + done tag=$1 cmd=$2; shift 2 ## Run CMD, logging its output in a pleasing manner. @@ -91,12 +103,13 @@ run () { nil) log "BEGIN $tag" rc=$( + case $stdinp in nil) exec &- 4>&- 5>&- 9>&- echo $? >&5; ) | - while IFS= read line; do echo "| $line"; done >&4; } 2>&1 | - while IFS= read line; do echo "* $line"; done >&4; } 4>&1 | - cat >&9; } 5>&1 &4; } 2>&1 | + copy "*" >&4; } 4>&1 | + cat >&9; } 5>&1 ) case $rc in 0) log "END $tag" ;; @@ -107,6 +120,15 @@ run () { return $rc } +run_diff () { + out=$1 old=$2 new=$3 + ## Write a unified diff from OLD to NEW, to OUT. + + set +e; diff -u "$old" "$new" >"$out"; rc=$?; set -e + case $rc in 1) cat "$out" ;; esac + return $rc +} + localp () { h=$1 ## Answer whether H is a local host. @@ -124,7 +146,7 @@ hostrun () { ## way it will be processed by a shell. if localp $host; then run "@$host: $tag" sh -c "$cmd" - else run "@$host: $tag" ssh $host "$cmd" + else run "@$host: $tag" ssh $userat$host "$cmd" fi } @@ -144,15 +166,62 @@ hostpath () { ## current host is local. if localp $host; then echo $path - else echo $host:$path + else echo $userat$host:$path + fi +} + +defhook () { + hook=$1 + ## Define a hook called HOOK. + + eval hk_$hook= +} + +addhook () { + hook=$1 cmd=$2 + ## Add command CMD to the hook HOOK. + + eval old=\$hk_$hook; new="$old $cmd" + eval hk_$hook=\$new +} + +runhook () { + hook=$1; shift 1 + ## Invoke HOOK, passing it the remaining arguments. + + eval cmds=\$hk_$hook + for cmd in $cmds; do + if ! $cmd "$@"; then return $?; fi + done +} + +remove_old_logfiles () { + base=$1 + ## Remove old logfiles with names of the form BASE.DATE#N, so that there + ## are at most $MAXLOG of them. + + ## Count up the logfiles. + nlog=0 + for i in "$base".*; do + if [ ! -f "$i" ]; then continue; fi + nlog=$(( nlog + 1 )) + done + + ## If there are too many, go through and delete some early ones. + if [ $dryrun = nil ] && [ $nlog -gt $MAXLOG ]; then + n=$(( nlog - MAXLOG )) + for i in "$base".*; do + if [ ! -f "$i" ]; then continue; fi + rm -f "$i" + n=$(( n - 1 )) + if [ $n -eq 0 ]; then break; fi + done fi } ###-------------------------------------------------------------------------- ### Database operations. -INDEXDB=@pkglocalstatedir@/index.db - insert_index () { host=$1 fs=$2 date=$3 vol=$4 @@ -170,7 +239,7 @@ delete_index () { if [ -f "$INDEXDB" ]; then sqlite3 "$INDEXDB" <&2 3>&- $lvhost \ + _hostrun >&2 3>&- $userat$lvhost \ "lvcreate --snapshot -n$lv.bkp $SNAPSIZE $vg/$lv" snaprc=$? set -e @@ -312,7 +392,7 @@ do_rfreezefs () { case $tok in "$tok_THAWED") ;; *) - _hostrun >&2 3>&- $lvhost "lvremove -f $vg/$lv.bkp" || : + _hostrun >&2 3>&- $userat$lvhost "lvremove -f $vg/$lv.bkp" || : echo >&2 "$quis: unexpected token $tok (rfreezefs $fsdir on $fshost)" exit 1 ;; @@ -339,7 +419,7 @@ do_rfreezefs () { esac ## Mount the snapshot on the volume host. - _hostrun >&2 $lvhost " + _hostrun >&2 $userat$lvhost " mkdir -p $SNAPDIR/$lv mount -oro /dev/$vg/$lv.bkp $SNAPDIR/$lv" } @@ -363,53 +443,6 @@ unsnap_rfreezefs () { ###-------------------------------------------------------------------------- ### Expiry computations. -parsedate () { - date=$1 - ## Parse an ISO8601 DATE, and set YEAR, MONTH, DAY appropriately (and - ## without leading zeros). - - ## Extract the components of the date and trim leading zeros (which will - ## cause things to be interpreted as octal and fail). - year=${date%%-*} rest=${date#*-}; month=${rest%%-*} day=${rest#*-} - year=${year#0} month=${month#0} day=${day#0} -} - -julian () { - date=$1 - ## Convert an ISO8601 DATE to a Julian Day Number. - - parsedate $date - - ## The actual calculation: convert a (proleptic) Gregorian calendar date - ## into a Julian day number. This is taken from Wikipedia's page - ## http://en.wikipedia.org/wiki/Julian_day#Calculation but the commentary - ## is mine. The epoch is 4713BC-01-01 (proleptic) Julian, or 4714BC-11-24 - ## proleptic Gregorian. - - ## If the MONTH is January or February then set a = 1, otherwise set a = 0. - a=$(( (14 - $month)/12 )) - - ## Compute a year offset relative to 4799BC-03-01. This puts the leap day - ## as the very last day in a year, which is very convenient. The offset - ## here is sufficient to make all y values positive (within the range of - ## the JDN calendar), and is a multiple of 400, which is the Gregorian - ## cycle length. - y=$(( $year + 4800 - $a )) - - ## Compute the offset month number in that year. These months count from - ## zero, not one. - m=$(( $month + 12*$a - 3 )) - - ## Now for the main event. The (153 m + 2)/5 term is a surprising but - ## correct trick for obtaining the number of days in the first m months of - ## the (shifted) year). The magic offset 32045 is what you get when you - ## plug the proper JDN epoch (year = -4713, month = 11, day = 24) into the - ## above machinery. - jdn=$(( $day + (153*$m + 2)/5 + 365*$y + $y/4 - $y/100 + $y/400 - 32045 )) - - echo $jdn -} - expire () { ## Read dates on stdin; write to stdout `EXPIRE date' for dates which ## should be expired and `RETAIN date' for dates which should be retained. @@ -507,16 +540,29 @@ EOF ###-------------------------------------------------------------------------- ### Actually taking backups of filesystems. -STOREDIR=@mntbkpdir@/store -METADIR=@mntbkpdir@/meta MAXLOG=14 HASH=sha256 unset VOLUME bkprc=0 +hash_file () { + file=$1 + + case $HASH in + md5 | sha1 | sha224 | sha256 | sha384 | sha512) + set -- $(${HASH}sum <"$file") + echo "$1" + ;; + *) + set -- $(openssl dgst -$HASH <"$file") + echo "$2" + ;; + esac +} + remote_fshash () { - _hostrun $host " + _hostrun $userat$host " umask 077 mkdir -p $fshashdir cd ${snapmnt#*:} @@ -564,19 +610,28 @@ expire_backups () { done } +## Backup hooks. +defhook setup +defhook precommit +defhook postcommit + backup_precommit_hook () { host=$1 fs=$2 date=$3 - ## Override this hook in the configuration file for special effects. + ## Compatibility: You can override this hook in the configuration file for + ## special effects; but it's better to use `addhook precommit'. : } +addhook precommit backup_precommit_hook backup_commit_hook () { host=$1 fs=$2 date=$3 - ## Override this hook in the configuration file for special effects. + ## Compatibility: You can override this hook in the configuration file for + ## special effects; but it's better to use `addhook commit'. : } +addhook commit backup_commit_hook do_backup () { date=$1 fs=$2 fsarg=$3 @@ -584,6 +639,15 @@ do_backup () { set -e attempt=0 + fshash_diff=nil + + ## Run a hook beforehand. + set +e; runhook setup $host $fs $date; rc=$?; set -e + case $? in + 0) ;; + 99) log "BACKUP of $host:$fs SKIPPED by hook"; return 0 ;; + *) log "BACKUP of $host:$fs FAILED (hook returns $?)"; return $? ;; + esac ## Report the start of this attempt. log "START BACKUP of $host:$fs" @@ -591,6 +655,9 @@ do_backup () { ## Maybe we need to retry the backup. while :; do + ## Rig checksum variables to mismatch unless they're set later. + hrfs=REMOTE hlfs=LOCAL + ## Create and mount the remote snapshot. case $dryrun in t) @@ -603,6 +670,20 @@ do_backup () { esac $verbose " create snapshot" + ## If we had a fshash-mismatch, then clear out the potentially stale + ## entries, both locally and remotely. + case $fshash_diff in + nil) ;; + *) + $verbose " prune cache" + run -stdin "local prune fshash" \ + fshash -u -c$STOREDIR/fshash.cache -H$HASH new/ <$fshash_diff + run -stdin "@$host: prune fshash" \ + _hostrun $userat$host <$fshash_diff \ + "fshash -u -c$fshashdir/$fs.bkp -H$HASH ${snapmnt#*:}" + ;; + esac + ## Build the list of hardlink sources. linkdests="" for i in $host $like; do @@ -634,7 +715,16 @@ do_backup () { run "@$host: fshash $fs" remote_fshash rc_fshash=$? set -e - case $dryrun in nil) $verbose " done" ;; esac + case $dryrun in + nil) + hrfs=$(hash_file "new.fshash") + log "remote fshash $HASH checksum: $hrfs" + $verbose " done" + ;; + t) + hrfs=UNSET + ;; + esac ## Remove the snapshot. maybe unsnap_$snap $snapargs $fs $fsarg @@ -655,11 +745,22 @@ do_backup () { nil) $verbose -n " local fshash..." ;; esac run "local fshash $host:$fs" local_fshash || return $? - case $dryrun in nil) $verbose " done" ;; esac + case $dryrun in + nil) + hlfs=$(hash_file "$localmap") + log "local fshash $HASH checksum: $hlfs" + $verbose " done" + ;; + t) + hlfs=UNSET + ;; + esac ## Compare the two maps. set +e - run "compare fshash maps for $host:$fs" diff -u new.fshash $localmap + fshash_diff=$STOREDIR/tmp/fshash-diff.$host.$fs.$date + run "compare fshash maps for $host:$fs" \ + run_diff $fshash_diff new.fshash $localmap rc_diff=$? set -e case $rc_diff in @@ -677,18 +778,29 @@ do_backup () { esac done + ## Double-check the checksums. + if [ $hrfs != $hlfs ]; then + cat >&2 <$log 1>&9; then + echo >&2 + echo >&2 "$quis: backup of $host:$fs FAILED!" + bkprc=1 + fi + + ## Clear away any old logfiles. + remove_old_logfiles "$logdir/$host/$fs" +} + backup () { ## backup FS[:ARG] ... ## @@ -767,59 +919,25 @@ backup () { continue fi - ## Find a name for the log file. In unusual circumstances, we may have - ## deleted old logs from today, so just checking for an unused sequence - ## number is insufficient. Instead, check all of the logfiles for today, - ## and use a sequence number that's larger than any of them. - case $dryrun in - t) - log=/dev/null - ;; - nil) - seq=1 - for i in "$logdir/$host/$fs.$date#"*; do - tail=${i##*#} - case "$tail" in [!1-9]* | *[!0-9]*) continue ;; esac - if [ -f "$i" -a $tail -ge $seq ]; then seq=$(( tail + 1 )); fi - done - log="$logdir/$host/$fs.$date#$seq" - ;; - esac - ## Do the backup of this filesystem. - case $dryrun in nil) mkdir -p $logdir/$host ;; esac - if ! do_backup $date $fs $fsarg 9>$log 1>&9; then - echo >&2 - echo >&2 "$quis: backup of $host:$fs FAILED!" - bkprc=1 - fi - - ## Count up the logfiles. - nlog=0 - for i in "$logdir/$host/$fs".*; do - if [ ! -f "$i" ]; then continue; fi - nlog=$(( nlog + 1 )) - done - - ## If there are too many, go through and delete some early ones. - if [ $dryrun = nil ] && [ $nlog -gt $MAXLOG ]; then - n=$(( nlog - MAXLOG )) - for i in "$logdir/$host/$fs".*; do - if [ ! -f "$i" ]; then continue; fi - rm -f "$i" - n=$(( n - 1 )) - if [ $n -eq 0 ]; then break; fi - done - fi + run_backup_cmd $fs $date do_backup $date $fs $fsarg done } ###-------------------------------------------------------------------------- ### Configuration functions. +defhook start +defhook end + +done_first_host_p=nil + host () { host=$1 - like= + like= userat= + case $done_first_host_p in + nil) runhook start; done_first_host_p=t ;; + esac case "${expire_policy+t},${default_policy+t}" in t,) default_policy=$expire_policy ;; esac @@ -827,10 +945,11 @@ host () { $verbose "host $host" } -snaptype () { snap=$1; shift; snapargs="$*"; retry=0; } +snaptype () { snap=$1; shift; snapargs="$*"; retry=1; } rsyncargs () { rsyncargs="$*"; } like () { like="$*"; } retry () { retry="$*"; } +user () { userat="$*@"; } retain () { case $clear_policy in t) unset expire_policy; clear_policy=nil ;; esac @@ -842,23 +961,13 @@ retain () { ### Read the configuration and we're done. usage () { - echo "usage: $quis [-v] [-c CONF]" + echo "usage: $quis [-nv] [-c CONF]" } version () { echo "$quis version $VERSION" } -config () { - echo - cat <&8 "$@"; } while getopts "hVvc:n" opt; do @@ -877,6 +986,12 @@ exec 8>&1 . "$conf" +runhook end $bkprc +case "$bkprc" in + 0) $verbose "All backups successful" ;; + *) $verbose "Backups FAILED" ;; +esac + ###----- That's all, folks -------------------------------------------------- exit $bkprc