+#!/bin/bash
+
+# Author: Carl Baldwin & Alan Pippin
+# Description: This script replicates a given zfs filesystem to a given zfs pool.
+# This script will keep all snapshots in sync, removing the ones
+# that have been deleted since the last replicate was performed.
+# This script will only send the new, or missing, snapshots since
+# the last replicate was performed.
+
+# In test mode (test=1) commands are echoed, not executed
+test=0
+
+[ $test == 0 ] && exec >> /var/log/zfs/zfs-replicate.log 2>&1
+
+# Usage: zfs-backup [filesystem] [destination_pool]
+# This script has a limitation with children under a given filesystem.
+# You must initially backup the parent filesystems first using this script
+# before backing up any of the children filesystems.
+
+fs=$1
+fs=${fs%/}
+fsname=${1#*/}
+srcpool=${1%%/*}
+srcfs="${srcpool}/$fsname"
+srcfs=${srcfs%/}
+dstpool=$2
+dstfs="${dstpool}/$srcfs"
+dstfs=${dstfs%/}
+nodstsnaps=0
+common=""
+
+if [ $test == 1 ]; then
+ echo "fs: $fs"
+ echo "fsname: $fsname"
+ echo "srcpool: $srcpool"
+ echo "srcfs: $srcfs"
+ echo "dstpool: $dstpool"
+ echo "dstfs: $dstfs"
+fi
+
+if ! zpool list -H "$srcpool" >/dev/null 2>&1; then
+ echo >&2 "-E- The source pool, '$srcpool' doesn't seem to exist."
+ exit 1
+fi
+
+if ! zpool list -H "$dstpool" >/dev/null 2>&1; then
+ echo >&2 "-E- The destination pool, '$dstpool' doesn't seem to exist."
+ exit 1
+fi
+
+if ! zfs list -rH -t snapshot "$dstfs" 2>&1 | grep "$dstfs@" > /dev/null 2>&1; then
+ echo >&2 "-W- No snapshots detected on the destination drive for this filesystem: $dstfs"
+ if zfs list -t filesystem | grep "$dstfs"; then
+ echo >&2 "-I- Found zfs filesystem $dstfs on the destination pool $dstpool without any snapshots"
+ echo >&2 "-I- Removing the zfs filesystem: $dstfs"
+ zfs destroy "$dstfs"
+ fi
+ nodstsnaps=1
+fi
+
+if [ $nodstsnaps == 0 ]; then
+ zfs list -rH -t snapshot $srcfs | grep "$srcfs@" | awk '{print $1}' > /tmp/source-list
+ zfs list -rH -t snapshot $dstfs | grep "$dstfs@" | sed "s,$dstpool/,," | awk '{print $1}' > /tmp/destination-list
+ diff -u /tmp/source-list /tmp/destination-list | grep -v '^+++' | awk '/^\+/ {print}' | sed "s,^\+,$dstpool/," > /tmp/obsolete-snapshots
+ rm -f /tmp/source-list /tmp/destination-list
+
+ echo >&2 "Removing obsolete backups from the destination pool"
+ for snapshot in $(cat /tmp/obsolete-snapshots); do
+ echo >&2 "Removing '$snapshot' from destination."
+ [ $test == 0 ] && zfs destroy "$snapshot"
+ done
+
+ echo >&2 "Rolling back to the most recent snapshot on the destination."
+ [ $test == 0 ] && zfs rollback $(zfs list -rH -t snapshot $dstfs | grep "$dstfs@" | awk '{snap=$1} END {print snap}')
+
+ echo >&2 "Calculating the most recent common snapshot between the two filesystems."
+ if zfs list -H "$dstfs" > /dev/null 2>&1; then
+ for snap in $(zfs list -rH -t snapshot "$dstfs" | grep "$dstfs@" |
+ sed 's,.*@,,' | awk '{print$1}'); do
+ if zfs list -rH -t snapshot "$fs" | grep "$fs@" | sed 's,.*@,,' | awk '{print$1}' | grep "^${snap}$" >/dev/null 2>&1; then
+ common=$snap
+ fi
+ done
+ fi
+fi
+
+base=$common
+foundcommon=false
+if [ -z "$common" ]; then
+ foundcommon=true
+fi
+
+for snap in $(zfs list -rH -t snapshot "$fs" | grep "$fs@" |
+ sed 's,.*@,,' | awk '{print$1}'); do
+ if [ "$snap" = "$common" ]; then
+ foundcommon=true
+ continue
+ fi
+
+ if $foundcommon; then
+ if [ -z "$base" ]; then
+ echo >&2 "Sending '$fs@$snap'"
+ [ $test == 0 ] && zfs set readonly=on "$dstpool"
+ [ $test == 0 ] && zfs set atime=off "$dstpool"
+ [ $test == 0 ] && zfs set sharenfs=off "$dstpool"
+ [ $test == 0 ] && zfs set mountpoint=legacy "$dstpool"
+ [ $test == 0 ] && zfs send "$fs@$snap" | zfs recv -v "$dstfs"
+ else
+ echo >&2 "Sending '$fs@$base' -> '$fs@$snap'"
+ [ $test == 0 ] && zfs send -i "$fs@$base" "$fs@$snap" | zfs recv -v "$dstfs"
+ fi
+ base=$snap
+ fi
+done
+
+true