mirror of
https://github.com/bashclub/bashclub-zfs-push-pull.git
synced 2024-11-07 21:01:58 +01:00
720754a504
Ensure the backup connection to the SSH server can't do anything other than designed. Uses ssh's authorized_keys command= statement to launch backup-zfs-shell, which then supports only the handful of operations required to perform backups. $destpath is no longer used, but still included in the documentation until it can be entirely written out.
268 lines
7.2 KiB
Bash
Executable File
268 lines
7.2 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)
|
|
-s store mode - output snaps from local fs to ssh server
|
|
-r read mode - read snaps from ssh server to local fs
|
|
-g gpg-id gpg recipient key id (store mode only)
|
|
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
|
|
tossh=false
|
|
fromssh=false
|
|
|
|
###
|
|
### parse options
|
|
###
|
|
while getopts "hvqk:t:d:srg:" opt ; do
|
|
case $opt in
|
|
h) usage 0 ;;
|
|
v)
|
|
verbose=true
|
|
send_opts="-v"
|
|
recv_opts="-v"
|
|
;;
|
|
q) quiet=true ;;
|
|
k) keep=$OPTARG ;;
|
|
t) tag=$OPTARG ;;
|
|
d) dateopts=$OPTARG ;;
|
|
s) tossh=true ;;
|
|
r) fromssh=true ;;
|
|
g) gpgid="$OPTARG" ;;
|
|
*) usage 1 ;;
|
|
esac
|
|
done
|
|
shift $((OPTIND-1))
|
|
date="$(date $dateopts)"
|
|
$tossh && $fromssh && die 1 "-s and -r are mutually exclusive"
|
|
if ! $tossh && [[ -n $gpgid ]] ; then
|
|
die 1 "-g can only be used with -s"
|
|
fi
|
|
|
|
###
|
|
### 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"
|
|
|
|
###
|
|
### 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
|
|
###
|
|
snap="${tag}_$date"
|
|
cur="$src@$snap"
|
|
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" zfslast)"
|
|
|
|
###
|
|
### 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 $([[ -n $gpgid ]] && echo "encrypted ")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"
|
|
if [[ -n $gpgid ]] ; then
|
|
ZFS "$srchost" send $send_opts -R -I "$last" "$cur" \
|
|
| gpg --trust-model always --encrypt --recipient "$gpgid" \
|
|
| ssh "$desthost" zfswrite "${tag}_$date.zfssnap.gpg" \
|
|
|| die $? "zfs incremental send failed"
|
|
ssh "$desthost" zfslast "$snap"
|
|
else
|
|
ZFS "$srchost" send $send_opts -R -I "$last" "$cur" \
|
|
| ssh "$desthost" zfswrite "${tag}_$date.zfssnap" \
|
|
|| die $? "zfs incremental send failed"
|
|
ssh "$desthost" zfslast "$snap"
|
|
fi
|
|
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" zfsfind "$srcpath" | sort) ; do
|
|
if [[ $file =~ \.gpg$ ]] ; then
|
|
ssh "$srchost" zfsget "$file" | gpg | ZFS "$desthost" receive $recv_opts -Fue "$dest" \
|
|
&& ssh "$srchost" rm "$file"
|
|
else
|
|
ssh "$srchost" zfsget "$file" | ZFS "$desthost" receive $recv_opts -Fue "$dest" \
|
|
&& ssh "$srchost" rm "$file"
|
|
fi
|
|
done
|
|
|
|
exit
|
|
fi
|
|
|
|
# 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 $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
|