mirror of
https://github.com/bashclub/bashclub-zfs-push-pull.git
synced 2024-11-07 21:01:58 +01:00
0850e82df3
Execution should not continue upon failure of certain zfs commands. In particular, cleanup/rotation of the oldest snapshots will no longer occur upon a failure to send/receive.
153 lines
3.6 KiB
Bash
Executable File
153 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"
|
|
|
|
###
|
|
### find newest snapshot matching the tag on dest
|
|
###
|
|
last="$(ZFS "$desthost" list -d 1 -t snapshot -H -S creation -o name $destfs/$srcbase 2>/dev/null \
|
|
| grep -F "@${tag}_" | 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
|