thishost=$(hostname -s)
quis=${0##*/}
-
-VERSION=@VERSION@
-mntbkpdir=@mntbkpdir@
-logdir=@logdir@
-fshashdir=@fshashdir@
-conf=@sysconfdir@/rsync-backup.conf
+. @pkgdatadir@/lib.sh
verbose=:
dryrun=nil
}
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.
nil)
log "BEGIN $tag"
rc=$(
+ case $stdinp in nil) exec </dev/null ;; esac
{ { { ( set +e
"$cmd" "$@" 3>&- 4>&- 5>&- 9>&-
echo $? >&5; ) |
copy "|" >&4; } 2>&1 |
copy "*" >&4; } 4>&1 |
- cat >&9; } 5>&1 </dev/null
+ cat >&9; } 5>&1
)
case $rc in
0) log "END $tag" ;;
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.
## 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
}
## 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
## Snapshot using LVM.
SNAPSIZE="-l10%ORIGIN"
-SNAPDIR=@mntbkpdir@/snap
snap_lvm () {
vg=$1 lv=$2
## Get the volume host to create the snapshot.
set +e
- _hostrun >&2 3>&- $lvhost \
+ _hostrun >&2 3>&- $userat$lvhost \
"lvcreate --snapshot -n$lv.bkp $SNAPSIZE $vg/$lv"
snaprc=$?
set -e
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
;;
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"
}
###--------------------------------------------------------------------------
### 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.
###--------------------------------------------------------------------------
### 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#*:}
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
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"
## 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)
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
run "@$host: fshash $fs" remote_fshash
rc_fshash=$?
set -e
- case $dryrun in nil) $verbose " done" ;; esac
+ case $dryrun in
+ nil)
+ hrfs=$(hash_file "$fshashdir/$fs.bkp")
+ $log "remote fshash $HASH checksum: $hlfs"
+ $verbose " done"
+ ;;
+ t)
+ hrfs=UNSET
+ ;;
+ esac
## Remove the snapshot.
maybe unsnap_$snap $snapargs $fs $fsarg
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
esac
done
+ ## Double-check the checksums.
+ if [ $hrfs != $hlfs ]; then
+ cat >&2 <<EOF
+$0: INTERNAL ERROR: fshash $HASH checksum mismatch -- aborting
+ remote fshash checksum = $hrfs
+ local fshash checksum = $hlfs
+EOF
+ exit 127
+ fi
+
## Glorious success.
maybe rm -f $localmap
+ case $fshash_diff in nil) ;; *) maybe rm -f $fshash_diff ;; esac
$verbose " fshash match"
## Commit this backup.
case $dryrun in
nil)
- backup_precommit_hook $host $fs $date
+ runhook precommit $host $fs $date
mv new $date
mv new.fshash $date.fshash
insert_index $host $fs $date $VOLUME
- backup_commit_hook $host $fs $date
+ runhook commit $host $fs $date
mkdir hack
ln -s $date hack/last
mv hack/last .
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
+ ## Clear away any old logfiles.
+ remove_old_logfiles "$logdir/$host/$fs"
}
backup () {
###--------------------------------------------------------------------------
### 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
$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
### 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 <<EOF
-conf = $conf
-mntbkpdir = $mntbkpdir
-fshashdir = $fshashdir
-logdir = $logdir
-EOF
-}
-
whine () { echo >&8 "$@"; }
while getopts "hVvc:n" opt; do
exec 8>&1
. "$conf"
+
+runhook end $bkprc
case "$bkprc" in
0) $verbose "All backups successful" ;;
*) $verbose "Backups FAILED" ;;