diff --git a/backup-zfs b/backup-zfs deleted file mode 100755 index b97b1d0..0000000 --- a/backup-zfs +++ /dev/null @@ -1,270 +0,0 @@ -#!/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) - -p ssh port - -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 -C -p $port "$host" zfs "$@" - else - log "local: zfs $@" - zfs "$@" - fi -} - -### -### defaults -### -tag="$prog" -dateopts="+%F_%T" -keep=5 -verbose=false -quiet=false -tossh=false -fromssh=false -port=22 - -### -### parse options -### -while getopts "hvqk:p: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 ;; - p) port=$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 | sort) ; do - log "receiving $file from $srchost" - 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