140 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
			
		
		
	
	
			140 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
#!/bin/bash
 | 
						|
#
 | 
						|
# bashclub zfs replication script
 | 
						|
# Author: (C) 2023 Thorsten Spille <thorsten@spille-edv.de>
 | 
						|
 | 
						|
PATH="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin"
 | 
						|
 | 
						|
prog="$(basename $0)"
 | 
						|
zfs=$(which zfs)
 | 
						|
ssh=$(which ssh)
 | 
						|
debug=
 | 
						|
 | 
						|
#### default config file, can be changed with parameter -c
 | 
						|
conf=/etc/bashclub/zsync.conf
 | 
						|
 | 
						|
#### default values in config file
 | 
						|
# replication target on local machine 
 | 
						|
target=pool/dataset
 | 
						|
 | 
						|
# ssh address of remote machine
 | 
						|
source=user@host
 | 
						|
 | 
						|
# zfs user parameter to identify filesystems/volumes to replicate
 | 
						|
tag=bashclub:zsync
 | 
						|
 | 
						|
# if $tag=subvols, which source to filter: "inherited" or "inherited|received"
 | 
						|
subvol_source="inherited|received"
 | 
						|
 | 
						|
# 
 | 
						|
snapshot_filter="hourly|daily|weekly|monthly"
 | 
						|
 | 
						|
usage() {
 | 
						|
	cat >&2 <<-EOF
 | 
						|
	usage: $prog [-h] [-d] [-c CONFIG]
 | 
						|
	  creates a mirrored replication of configured zfs filesystems / volumes
 | 
						|
    -c CONFIG    Configuration file for this script
 | 
						|
    -d           Debug mode
 | 
						|
  ---------------------------------------------------------------------------
 | 
						|
    (C) 2023     by Spille IT Solutions for bashclub (github.com/bashclub)
 | 
						|
                 Author: Thorsten Spille <thorsten@spille-edv.de>
 | 
						|
  ---------------------------------------------------------------------------
 | 
						|
	EOF
 | 
						|
	exit $1
 | 
						|
}
 | 
						|
 | 
						|
while getopts "hdc:" opt; do
 | 
						|
  case $opt in
 | 
						|
    h) usage 0 ;;
 | 
						|
    c) conf=$OPTARG ;;
 | 
						|
    d) debug=-v ;;
 | 
						|
    *) usage 1 ;;
 | 
						|
  esac
 | 
						|
done
 | 
						|
shift $((OPTIND-1))
 | 
						|
 | 
						|
# load config file
 | 
						|
if [ -f $conf ]; then
 | 
						|
    echo "Reading configuration $conf"
 | 
						|
    source $conf
 | 
						|
else
 | 
						|
    mkdir -p $(dirname $conf)
 | 
						|
    cat << EOF > $conf
 | 
						|
target=$target
 | 
						|
source=$source
 | 
						|
tag=$tag
 | 
						|
subvol_source=$subvol_source
 | 
						|
snapshot_filter=$snapshot_filter
 | 
						|
EOF
 | 
						|
    echo "Initial config file created. Please adjust and restart script. Exiting..."
 | 
						|
    usage 0
 | 
						|
fi
 | 
						|
 | 
						|
if [[ $source == "" ]]; then
 | 
						|
    echo "source is empty, switching to local mode."
 | 
						|
    ssh=
 | 
						|
    echo -e "Configuration:\n\ttarget=$target\n\ttag=$tag\n\tsubvol_source=$subvol_source\n\tsnapshot_filter=$snapshot_filter\n"
 | 
						|
else
 | 
						|
    echo -e "Configuration:\n\ttarget=$target\n\tsource=$source\n\ttag=$tag\n\tsubvol_source=$subvol_source\n\tsnapshot_filter=$snapshot_filter\n"
 | 
						|
fi
 | 
						|
 | 
						|
# query source datasets/subvols to replicate
 | 
						|
IFS=$'\n'
 | 
						|
for zvol in $($ssh $source "zfs get -H -o name,value,source -t filesystem,volume $tag"); do
 | 
						|
    name=$(echo $zvol | cut -f1)
 | 
						|
    if [[ "$(echo $zvol | cut -f2)" == "subvols" ]] && [[ $(echo $zvol | grep -E $subvol_source) ]]; then
 | 
						|
        echo "Including $name"
 | 
						|
        syncvols=("${syncvols[@]}" "$name")
 | 
						|
    elif [[ "$(echo $zvol | cut -f2)" == "all" ]];then
 | 
						|
        echo "Including $name"
 | 
						|
        syncvols=("${syncvols[@]}" "$name")
 | 
						|
    else
 | 
						|
        echo "Excluding $name"
 | 
						|
    fi
 | 
						|
done
 | 
						|
 | 
						|
 | 
						|
for name in "${syncvols[@]}"; do
 | 
						|
    IFS=$' '
 | 
						|
    if [ $($zfs list -H -s creation -t snapshot $target/$name > /dev/null 2>&1 ; echo $?) -gt 0 ]; then
 | 
						|
        # create parent datasets
 | 
						|
        prefix=""
 | 
						|
        for part in $(echo $target/$(echo $name | cut -d'/' -f1) | sed "s/\// /g"); do
 | 
						|
            if [ $($zfs list $prefix$part > /dev/null 2>&1 ; echo $?) -gt 0 ]; then
 | 
						|
                echo "Creating $prefix$part"
 | 
						|
                $zfs create -p $prefix$part
 | 
						|
            fi
 | 
						|
            prefix="$prefix$part/"
 | 
						|
        done
 | 
						|
 | 
						|
        # start initial replication
 | 
						|
        IFS=$'\n'
 | 
						|
        for snap in $($ssh $source "zfs list -H -t snapshot -o name -S creation $name | grep -E "$snapshot_filter" | tail -1"); do
 | 
						|
            echo "Start initial replication: $snap => $target/$(echo $name | cut -d'/' -f1)"
 | 
						|
            $ssh $source "zfs send -p $debug $snap" | $zfs receive -x mountpoint -x canmount -x $tag -x com.sun:auto-snapshot $debug -dF $target/$(echo $name | cut -d'/' -f1)
 | 
						|
        done
 | 
						|
    fi
 | 
						|
 | 
						|
    # replicate incremental
 | 
						|
    guid=$($zfs list -H -o guid -s creation -t snapshot $target/$name | tail -1)
 | 
						|
    last=$($ssh $source "zfs list -H -o name,guid -t snapshot $name | grep $guid | tail -1 | cut -f1")
 | 
						|
    IFS=$'\n'
 | 
						|
    for snap in $($ssh $source "zfs list -H -o name,guid -s creation -t snapshot $name | grep -E "$snapshot_filter" | grep --after-context=200 $guid | grep -v $guid | cut -f1"); do
 | 
						|
        echo "Replicating delta of $last <=> $snap to $target/$name"
 | 
						|
        $ssh $source "zfs send $debug -i $last $snap" | zfs receive -x mountpoint -x canmount -x $tag -x com.sun:auto-snapshot -F $debug $target/$name
 | 
						|
        last=$snap
 | 
						|
    done
 | 
						|
 | 
						|
    # cleanup old snapshots
 | 
						|
    filter=$(echo -e $snapshot_filter | sed "s/|/ /g")
 | 
						|
    IFS=$' '
 | 
						|
    for interval in $filter ; do
 | 
						|
        guid=$($ssh $source "zfs list -H -o guid,name -S creation -t snapshot $name | grep $interval | cut -f1 | tail -1")
 | 
						|
        if [[ "$(echo -e "$guid" | sed 's/\n//g')" != "" ]]; then
 | 
						|
            for snap in $($zfs list -H -o name,guid -S creation -t snapshot $target/$name | grep $interval | grep --after-context=200 $guid | grep -v $guid | cut -f1); do
 | 
						|
                echo "Deleting $snap"
 | 
						|
                $zfs destroy $debug $snap
 | 
						|
            done
 | 
						|
        fi
 | 
						|
    done
 | 
						|
done |