From 3bb83e802973ff942ccf5f2e5a5e215d73d0f626 Mon Sep 17 00:00:00 2001 From: Kevin McCormick Date: Tue, 7 Mar 2017 09:43:08 -0800 Subject: [PATCH] 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. --- backup-zfs | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/backup-zfs b/backup-zfs index fdab0e9..61de398 100755 --- a/backup-zfs +++ b/backup-zfs @@ -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#*:}"