bashclub-zfs-push-pull/backup-zfs
Kevin McCormick cf353fafd4 Allow matching any snapshot between src & dest
Previously we looked for the specific tagged snapshot, but that doesn't
actually work properly. Now simply find the most recent snapshot on dest
and compare to src. It must exist on src.
2016-09-16 16:10:33 -07:00

152 lines
3.6 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
}
###
### defaults
###
tag="$prog"
dateopts="+%F_%T"
keep=5
verbose=false
quiet=false
###
### parse options
###
while getopts "hvqk:t:d:" opt ; do
case $opt in
h) usage 0 ;;
v) verbose=true ;;
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
###
### 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 -R "$cur" | ZFS "$desthost" receive -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 -R -I "$last" "$cur" | ZFS "$desthost" receive -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