bashclub-zfs-push-pull/backup-zfs
Kevin McCormick fcd5fb0e08 Implement catchup mode to fix interrupted transfer
When zfs send -R is interrupted, some child filesystems will be at a
more recent snapshot than their parent. There's then no way to do a
zfs send -R from the root/parent, as the children are inconsistent.

Catchup mode handles recursion on its own, instead of relying on zfs to
do it. Basically picking a single consistent snapshot across the board,
then bringing all to that level.
2016-10-10 10:17:33 -07:00

182 lines
4.7 KiB
Bash
Executable File

#!/bin/bash
# backup-zfs: use zfs send/recv to push/pull snapshots
prog="$(basename "$0")"
usage() {
cat >&2 <<-EOF
usage: $prog [-hvq] [-t tag] [-k keep] [-d dateopts] src dest
use zfs send/recv to push/pull snapshots
src the source fs, specified as [host:]pool/path/to/fs
dest the destination fs parent, specified as [host:]pool/path/to/fs
(the final path component of src will be appended to dest)
-h help
-v verbose mode
-q quiet mode
-t tag tag to use for naming snapshots (default: backup-zfs)
-k keep number of snapshots to keep on src (default: 5)
-d dateopts options for date(1) - used to name the snapshots (default: +%F_%T)
EOF
exit $1
}
# log to syslog; if verbose or on a tty, also to stdout
# usage: log msg
log() {
logger -t "$prog" -- "$@"
if ! $quiet && [[ -t 1 ]] || $verbose ; then
echo "$@" >&2
fi
}
# exit with a code & message
# usage: die $exitcode msg
die() {
code="$1"
shift
if [[ $code -ne 0 ]] ; then
verbose=true log "FATAL: $@"
else
log "$@"
fi
exit $code
}
# run zfs(1) command either locally or via ssh
# usage: ZFS "$host" command args...
ZFS() {
host="$1"
shift
if [[ -n $host ]] ; then
log "remote ($host): zfs $@"
ssh "$host" sudo zfs "$@"
else
log "local: zfs $@"
sudo zfs "$@"
fi
}
# If initial zfs send -R is interrupted, it can't be resumed.
# Rolling back all filesystems to the last consistent snapshot ought
# to do the trick, but that's annoying to throw that data away.
catchup() {
srcparent="${srcfs%/*}" # all but last child component of srcfs
root="${srcfs#*/}" # last child component of srcfs
target="$(ZFS "$srchost" list -d 1 -t snapshot -H -S creation -o name $srcfs | cut -f2 -d@ | grep "^$tag" | head -n1)"
for fs in $(ZFS "$desthost" list -Ho name -r "$destfs/$root" | tac) ; do
child="${fs#$destfs/}"
destsnap=$(ZFS "$desthost" list -Ho name -S creation -d1 -t snapshot $destfs/$child | head -n1 | cut -f2 -d@)
if [[ $destsnap != $target ]] ; then
ZFS "$srchost" send $send_opts -R -I @$destsnap $srcparent/$child@$target | ZFS "$desthost" receive $recv_opts -Fue "$destfs/${child%/*}"
fi
done
}
###
### defaults
###
tag="$prog"
dateopts="+%F_%T"
keep=5
verbose=false
quiet=false
catchup=false
###
### parse options
###
while getopts "chvqk:t:d:" opt ; do
case $opt in
c) catchup=true ;;
h) usage 0 ;;
v)
verbose=true
send_opts="-v"
recv_opts="-v"
;;
q) quiet=true ;;
k) keep=$OPTARG ;;
t) tag=$OPTARG ;;
d) dateopts=$OPTARG ;;
*) usage 1 ;;
esac
done
shift $((OPTIND-1))
date="$(date $dateopts)"
###
### parse src & dest host/fs info
###
# fail if there's ever >1 colon
if [[ $1 =~ :.*: || $2 =~ :.*: ]] ; then
die 1 "invalid fsspec: '$1' or '$2'"
fi
# fail if src or dest isn't specified
if [[ -z $1 || -z $2 ]] ; then
usage 1
fi
src="$1"
dest="$2"
# discard anything before a colon to get the fs
srcfs="${src#*:}"
destfs="${dest#*:}"
# iff there is a colon, discard everything after it to get the host
[[ $src =~ : ]] && srchost="${src%:*}"
[[ $dest =~ : ]] && desthost="${dest%:*}"
# get the last src component
srcbase="${srcfs##*/}"
# ensure the destination fs exists before proceeding
if [[ $(ZFS "$desthost" list -H -o name "$destfs" 2>/dev/null) != $destfs ]] ; then
die 1 "destination fs '$destfs' doesn't exist"
fi
###
### fix interrupted transfer
###
if $catchup ; then
catchup || die $? "catchup failed"
exit 0
fi
###
### create new snapshot on src
###
cur="$srcfs@${tag}_$date"
ZFS "$srchost" snapshot -r "$cur" || die $? "zfs snapshot failed"
###
### get newest snapshot on dest - it must exist on src
###
last="$(ZFS "$desthost" list -d 1 -t snapshot -H -S creation -o name $destfs/$srcbase | head -n1 | cut -f2 -d@)"
###
### send & receive
###
# 1st time: send full snapshot
if [[ -z $last ]] ; then
log "sending full recursive snapshot from $src to $dest"
ZFS "$srchost" send $send_opts -R "$cur" | ZFS "$desthost" receive $recv_opts -Fue "$destfs" || die $? "zfs full send failed"
# special case: tagged snapshots exist on dest, but src has rotated through all
elif ! ZFS "$srchost" list $srcfs@$last &>/dev/null ; then
die 1 "no incremental path from from $src to $dest"
# normal case: send incremental
else
log "sending incremental snapshot from $src to $dest (${last#${tag}_}..${cur#*@${tag}_})"
ZFS "$srchost" send $send_opts -R -I "$last" "$cur" | ZFS "$desthost" receive $recv_opts -Fue "$destfs" || die $? "zfs incremental send failed"
fi
###
### clean up old snapshots
###
for snap in $(ZFS "$srchost" list -d 1 -t snapshot -H -S creation -o name $srcfs \
| grep -F "@${tag}_" | cut -f2 -d@ | tail -n+$((keep+1)) ) ;
do
ZFS "$srchost" destroy -r $srcfs@$snap
done