ssh mode: stage snapshots on ssh server

Turn backup-zfs into a two-step process:
1. Send snapshots to a server over SSH
2. Receive those snapshots over SSH

Supports a disconnected backup environment where the backup storage host
and the system being backed up don't have to be online at the same time.

Use case: Send snapshots from my home server to my work server while I'm
asleep, then read them over the LAN from my work server to my external
drive while I'm at work.
This commit is contained in:
Kevin McCormick 2017-03-07 09:43:08 -08:00
parent 6d906ad640
commit 3bb83e8029

View File

@ -17,6 +17,8 @@ usage() {
-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)
-s store mode - output snaps from local fs to ssh server
-r read mode - read snaps from ssh server to local fs
EOF
exit $1
}
@ -65,11 +67,13 @@ dateopts="+%F_%T"
keep=5
verbose=false
quiet=false
tossh=false
fromssh=false
###
### parse options
###
while getopts "hvqk:t:d:" opt ; do
while getopts "hvqk:t:d:sr" opt ; do
case $opt in
h) usage 0 ;;
v)
@ -81,11 +85,14 @@ while getopts "hvqk:t:d:" opt ; do
k) keep=$OPTARG ;;
t) tag=$OPTARG ;;
d) dateopts=$OPTARG ;;
s) tossh=true ;;
r) fromssh=true ;;
*) usage 1 ;;
esac
done
shift $((OPTIND-1))
date="$(date $dateopts)"
$tossh && $fromssh && die 1 "-s and -r are mutually exclusive"
###
### parse src & dest host/fs info
@ -102,6 +109,89 @@ fi
src="$1"
dest="$2"
###
### ssh mode - output snaps from local fs to ssh or read snaps from ssh to local fs
if $tossh ; then
log "sending from local zfs filesystem to SSH server"
# make sure src exists
if [[ $src =~ : ]] ; then
die 1 "$src must be a local zfs filesystem"
elif [[ $(ZFS "" list -H -o name "$src" 2>/dev/null) != $src ]] ; then
die 1 "$src must be a local zfs filesystem"
fi
# split dest to components
if [[ $dest =~ : ]] ; then
desthost="${dest%:*}"
destpath="${dest#*:}"
else
die 1 "$dest must be ssh host:path"
fi
# get the last src component
srcbase="${src##*/}"
###
### create new snapshot on src
###
cur="$src@${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@)"
last="$(ssh "$desthost" cat "$destpath/.last")"
###
### send
###
# refuse to send without a valid .last maker
if [[ -z $last ]] ; then
die 1 "ssh path contains no .last file"
# special case: tagged snapshots exist on dest, but src has rotated through all
elif ! ZFS "$srchost" list $src@$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"
ZFS "$srchost" send $send_opts -R -I "$last" "$cur" | ssh "$desthost" "cat > \"$destpath/${tag}_$date.zfssnap\"" || die $? "zfs incremental send failed"
fi
exit
elif $fromssh ; then
log "receving from SSH server to local zfs filesystem"
# make sure dest exists
if [[ $dest =~ : ]] ; then
die 1 "$dest must be a local zfs filesystem"
elif [[ $(ZFS "" list -H -o name "$dest" 2>/dev/null) != $dest ]] ; then
die 1 "$dest must be a local zfs filesystem"
fi
# split src into components
if [[ $src =~ : ]] ; then
srchost="${src%:*}"
srcpath="${src#*:}"
else
die 1 "$src must be ssh host:path"
fi
###
### receive
###
log "receiving incremental snapshot from $src to $dest"
#ZFS "$srchost" send $send_opts -R -I "$last" "$cur" | ZFS "$desthost" receive $recv_opts -Fue "$destfs" || die $? "zfs incremental send failed"
for file in $(ssh "$srchost" "find \"$srcpath\" -name \"*.zfssnap\"") ; do
ssh "$srchost" "cat \"$file\"" | ZFS "$desthost" receive $recv_opts -Fue "$dest" && ssh "$srchost" "rm \"$file\""
done
exit
fi
die 1 "neither -s nor -r was specified"
# discard anything before a colon to get the fs
srcfs="${src#*:}"
destfs="${dest#*:}"