#! /bin/sh # change this to the name of your ZFS pool. Or set ZPOOL envvar at runtime zpool=${ZPOOL:-"rpool"} # name of backup to take bkname=${1:-"default"} # make a backup collection having 1 full and N-1 incremental backups. # set to 1 to avoid incrementals and always take full dumps. max_incremental=${2:-"4"} # names of the DATASETs to exclude (datasets, not mountpoints!) # can override this list at runtime with EXCLUDES envvar. # can extend this list at runtime with EXTRA_EXCLUDES envvar. excludes=${EXCLUDES:-"rpool rpool/ROOT rpool/data"} UPLOAD_PATH="b2://arclightbackup/zfssnap" usage () { echo "Usage:" echo "zfsbk.sh [numincr]" echo "Takes & sends backup with given name. If snapshots with" echo "this name exist, backup incrementally. Limit incremental" echo "backup sequences to 'numincr' steps (default 1 = alw. full)." echo echo "Optional envvars:" echo "UPLOAD_PATH upload generated backup to this path. scp:// or rsync://" echo "CLEAR_BKFILE remove local backup file at the end of the process." } failmsg () { echo $* echo usage exit 1 } verify_zpool () { zpool status $zpool >/dev/null 2>&1 || failmsg "ZFS pool '$zpool' not found. Fix your \$ZPOOL envvar." } verify_zpool echo $bkname | grep -qiE '^[a-z0-9]+$' || failmsg "Given bkname is invalid. Want alphanumeric." echo "$max_incremental" | grep -qE '^[1-9][0-9]*$' || failmsg "Invalid number of backups sequence steps, want positive integer." export PATH=$PATH:/usr/local/bin # list snapshots by decreasing time. Arguments: [name] restrict listing to this tag # list_named_snaps () { # local bkname=$1 # snapdir="/.zfs/snapshot" # for i in $snapdir/zbk-$bkname-* # do # test -e "$i" && (echo $i |sed "s@^$snapdir/@@") # done | sort -r # } # list all snapshots for the tag by decreasing time. # Arguments: # [name] restrict listing to this tag # [set] restrict listing to this set all_snaps () { local bkname=$1 zfs list -t snapshot | grep $bkname | cut -f1 -d' ' | sort -r } # list snapshots for the set by decreasing time. # Arguments: # [name] restrict listing to this tag # [set] restrict listing to this set snaps_for_set () { local bkname=$1 local set=$2 zfs list -t snapshot | grep $bkname | grep $set | cut -f1 -d' ' | sort -r } # list datasets with snapshots. Arguments: [name] restrict listing to this tag list_snap_sets () { local bkname=$1 zfs list -t snapshot | grep $bkname | cut -f1 -d' ' | cut -f1 -d'@' | uniq } # list the datasets we want to backup. # this is snap sets minus excludes list_backup_snap_sets () { local bkname=$1 list_snap_sets $bkname | while read set do matched=0 for excl in $excludes do if [ "x$set" = "x$excl" ] then matched=1 break fi done if [ $matched -ne 1 ] then echo -n "$set " fi done } # remove all datasets for a given tag. Arguments: $1 -> bkname reset_sequence () { local bkname=$1 all_snaps $bkname | while read snapname do zfs destroy $snapname done } # take snapshot echo ./zfssnap.sh $bkname $max_incremental if ! ./zfssnap.sh $bkname $max_incremental then echo "Error creating snapshot! Code $?" exit 1 fi # iterate all the sets we want to back up # and write dump files num_snaps=0 # save for later for set in `list_backup_snap_sets $bkname` ; do snaps=`snaps_for_set $bkname $set` num_snaps=`echo $snaps | awk 'BEGIN {RS=" "} END {print NR}'` label_latest=`echo $snaps | awk 'BEGIN {RS=" "} NR==1'` #echo "$set: $num_snaps snaps, latest: $label_latest" # decide what to do based on how many snapshots were found if [ $num_snaps -eq 0 ] then # no snapshots. Something's wrong echo "Error, no snapshots found for $set $bkname!" echo $snaps exit 1 elif [ $num_snaps -eq 1 ] then # first snapshot. Send full echo "first snapshot. Send full" file_latest=`echo $label_latest | sed 's@/@_@g'` bkfile="/backups/${file_latest}.dump" echo "zfs send -R $label_latest > $bkfile" zfs send -R $label_latest 2>/dev/null > $bkfile else # n-th snapshot. Send incrementally echo "n-th snapshot. Send incrementally" file_latest=`echo $label_latest | sed 's@/@_@g'` label_2ndlatest=`echo $snaps | awk 'BEGIN {RS=" "} NR==2'` file_2ndlatest=`echo $label_2ndlatest | sed 's@/@_@g'` bkfile="/backups/${file_latest}--${file_2ndlatest}.dump" echo "zfs send -i $label_2ndlatest -R $label_latest > $bkfile" zfs send -i $label_2ndlatest -R $label_latest 2>/dev/null > $bkfile fi done if [ $num_snaps -eq $max_incremental ] then # finished the incremental sequence. Take full backup next time echo "Sequence of $max_incremental increm steps complete. Resetting." reset_sequence $bkname fi # upload backup to remote location if [ "x$UPLOAD_PATH" != x ] then upload_excode=1 # assume failure, override with actual outcome #echo "Archiving $bkfile remotely..." if echo "$UPLOAD_PATH" | grep -qE '^rsync://' then rsync -qz $bkfile ${UPLOAD_PATH#rsync://} upload_excode=$? elif echo "$UPLOAD_PATH" | grep -qE '^scp://' then SCP_PATH=${UPLOAD_PATH#scp://} if [ "$num_snaps" -eq 1 -a "x$DELETE_OUTDATED" != x ] then echo "Clearing old zbk-${bkname}* sequence remotely..." echo "rm zbk-${bkname}*" | sftp -q -b- $SCP_PATH fi scp -BCq $bkfile "$SCP_PATH" upload_excode=$? elif echo "$UPLOAD_PATH" | grep -qE '^b2://' then #B2_PATH=${UPLOAD_PATH#b2://} echo "Uploading to b2: $bkfile" #echo "b2 upload-file --noProgress $bkfile zfssnap/$B2_PATH" echo "b2 sync --delete --noProgress /backups $UPLOAD_PATH" upload_excode=$? else echo "UPLOAD_PATH not understood! '$UPLOAD_PATH'" echo "Expecting rsync://.. or scp://.." exit 1 fi # remove local backup if requested & upload was successful if [ $upload_excode -eq 0 -a "x$CLEAR_BKFILE" != x ] then # remove backup file if requested rm -f $bkfile fi fi