Files
dsnap-sync/bin/dsnap-sync

1613 lines
57 KiB
Bash
Executable File

#! /bin/dash
# dsnap-sync
# https://github.com/rzerres/dsnap-sync
# Copyright (C) 2016, 2017 James W. Barnett
# Copyright (C) 2017, 2018 Ralf Zerres
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# -------------------------------------------------------------------------
# Takes snapshots of each snapper configuration. It then sends the snapshot to
# a location on an external drive. After the initial transfer, it does
# incremental snapshots on later calls. It's important not to delete the
# snapshot created on your system since that will be used to determine the
# difference for the next incremental snapshot.
progname="${0##*/}"
version="0.5.5"
# The following lines are modified by the Makefile or
# find_snapper_config script
#SNAPPER_CONFIG=/etc/conf.d/snapper
SNAPPER_CONFIG=/etc/default/snapper
SNAPPER_TEMPLATE_DIR=/etc/snapper/config-templates
SNAPPER_CONFIG_DIR=/etc/snapper/configs
# define fifo pipes
TMPDIR_PIPE=$(mktemp -d)
PIPE=$TMPDIR_PIPE/$progname.out
mkfifo $PIPE
systemd-cat --identifier="$progname" < $PIPE &
BTRFS_PIPE=$TMPDIR_PIPE/btrfs.out
#mkfifo $BTRFS_PIPE
#systemd-cat --identifier="btrfs-pipe" < $BTRFS_PIPE &
# redirect descriptors to given pipes
exec 3>$PIPE 4>$BTRFS_PIPE
# global variables
args=
donotify=0
answer=no
disk_count=-1
disk_uuid_match_count=0
disk_target_match_count=0
disk_subvolid_match_count=0
disk_uuid_match=''
selected_uuid='none'
selected_target='none'
selected_subvol='none'
snapper_snapshots=".snapshots" # hardcoded in snapper
snapper_subvolume_template="dsnap-sync"
snapper_config_type='none'
#snapper_config_postfix="."`hostname`
snapper_config_postfix=
snap_cleanup_algorithm="timeline"
verbose=0
# ascii color
BLUE=
GREEN=
MAGENTA=
RED=
YELLOW=
NO_COLOR=
###
# functions
###
check_prerequisites () {
# requested binaries:
which awk >/dev/null 2>&1 || { printf "'awk' is not installed." && exit 1; }
which sed >/dev/null 2>&1 || { printf "'sed' is not installed." && exit 1; }
which tee >/dev/null 2>&1 || { printf "'tee' is not installed." && exit 1; }
which btrfs >/dev/null 2>&1 || { printf "'btrfs' is not installed." && exit 1; }
which findmnt >/dev/null 2>&1 || { printf "'findmnt' is not installed." && exit 1; }
which systemd-cat >/dev/null 2>&1 || { printf "'systemd-cat' is not installed." && exit 1; }
which snapper >/dev/null 2>&1 || { printf "'snapper' is not installed." && exit 1; }
which wc >/dev/null 2>&1 || { printf "'wc' is not installed." && exit 1; }
# optional binaries:
which notify-send >/dev/null 2>&1 && { donotify=1; }
which pv >/dev/null 2>&1 && { do_pv_cmd=true; }
if [ $(id -u) -ne 0 ] ; then printf "$progname: must be run as root\n" ; exit 1 ; fi
if [ -z "$remote" ]; then
$ssh which sh >/dev/null 2>&1 || \
{ printf "'remote shell' is not working!\n \
Please correct your public authentication and try again.\n" && exit 1; }
fi
if [ ! -r "$SNAPPER_CONFIG" ]; then
die "$progname: $SNAPPER_CONFIG does not exist."
fi
}
check_snapper_failed_ids () {
local batch=${1:-$false}
# active, non finished snapshot backups are marked with a reasonable string
# default: $(snap_description_running -> "$progname backup in progress" (userdata: host=$source)
snapper_failed_ids=$(eval snapper --config $selected_config list --type single \
| awk '/'"$snap_description_running"'/ {cnt++} END {print cnt}')
#snapper_failed_ids="snapper --config $selected_config list --type single \
# | awk '/'"$snap_description_running"'/' \
# | awk ' /'host='"$remote"'/ {cnt++} END {print cnt}'"
if [ ${#snapper_failed_ids} -gt 0 ]; then
if [ "$batch" ]; then
answer="yes"
else
#printf "\nNOTE: Found %s previous failed sync runs for '%s'.\n" "${snapper_failed_ids}" "$selected_config" | tee $PIPE
printf "${MAGENTA}Found %s previous failed sync runs for '%s'${NO_COLOR}\n" "${snapper_failed_ids}" "$selected_config"
answer=no
get_answer_yes_no "Delete failed backup snapshots [y/N]? " "$answer"
fi
if [ "$answer" = "yes" ]; then
cmd2="snapper --config \"$selected_config\" list | awk '/'\"$snap_description_running\"'/ {print \$3}'"
#cmd2="snapper --config \"$selected_config\" list \
# | awk '/'\"$snap_description_running\"'/' \
# | awk '/'host='\"$remote\"'/ {print \$3}'"
cmd="snapper --config \"$selected_config\" delete "
$(eval $cmd $(eval $cmd2))
fi
fi
}
if [ $verbose -ge 1 ]; then
printf "Verify snapper configuration type for %s...\n" $snapper_config
fi
# WIP -> exit now
# Patch snapper to parse for new pairs: CONFIG_TYPE="child | root"; CONFIG_PARENT="<string>"
##config_type=$(eval snapper --config $1 get-config | awk '/'"CONFIG_PARENT=$snap_description_running"'/ {cnt++} END {print cnt}')
# for now, we cut an parse ourself
IFS="="
while read -r key value
do
if [ "$key" = "CONFIG_TYPE" ]; then
snapper_config_type=$(eval echo $value | sed -e 's/\"\(.*\)\"/\1/')
break
fi
done < $SNAPPER_CONFIG_DIR/$snapper_config
case $snapper_config_type in
child|Child)
# d2d2d backup
snapper_config_type="child2"
snapper_target_config=d2d-$snapper_config
;;
parent|Parent)
# d2d backup
snapper_config_type="parent2disk"
snapper_target_config=$snapper_config
;;
*)
# d2s backup (default)
snapper_config_type="parent2disk"
snapper_target_config=$snapper_config
;;
esac
if [ -n $snapper_config_postfix ]; then
snapper_target_config=${snapper_target_config}${snapper_config_postfix}
fi
if [ $verbose -ge 1 ]; then
printf "Snappper configuration type: '%s'\n" $snapper_config_type
printf "Snappper target configuration: '%s'\n" $snapper_target_config
fi
}
die () {
error "$@"
exit 1
}
error () {
printf "\n==> ERROR: %s\n" "$@"
notify_error 'Error' 'Check journal for more information.'
} >&2
get_answer_yes_no () {
local message="${1:-'Do you want to proceed [y/N]? '}"
local i="none"
# hack: answer is a global variable, using it for preselection
while [ "$i" = "none" ]; do
read -r -p "$message" i
case $i in
y|Y|yes|Yes)
answer="yes"
break
;;
n|N|no|No)
answer="no"
break
;;
*)
if [ -n "$answer" ]; then
i="$answer"
else
i="none"
printf "Select 'y' or 'n'.\n"
fi
;;
esac
done
}
get_config () {
local config=${1:-"$TEMPLATE_DIR/$snapper_snapsync_template"}
#local config_key=${2:-SUBVOLUME}
local config_key=${2:-CONFIG_TYPE}
IFS="="
while read -r name value
do
if [ "$name" = "$config_key" ]; then
value="$value"
key="$value"
return $true
break
fi
done < $config
return $false
}
get_disk_infos () {
local disk_uuid
local disk_target
local fs_option
# get mounted BTRFS infos
if [ "$($ssh findmnt --noheadings --nofsroot --mountpoint / --output FSTYPE)" = "btrfs" ]; then
# root filesystem is never seen as valid target location
exclude_uuid=$($ssh findmnt --noheadings --nofsroot --types btrfs --mountpoint / --output UUID)
disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list | grep -v $exclude_uuid | awk '{print $1}')
disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list | grep -v $exclude_uuid | awk '{print $2}')
fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | grep -v $exclude_uuid | awk '{print $2}')
else
disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID --list)
disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output TARGET --list)
fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | awk '{print $2}')
fi
# we need at least one target disk
if [ ${#disk_targets} -eq 0 ]; then die "no suitable target disk found"; fi
# Posix Shells do not support Array. Therefore using ...
# Pseudo-Arrays (assumption: equal number of members)
# Pseudo-Array: disk_uuid_$i
# Pseudo-Array: disk_target_$i
# Pseudo-Array: fs_options_$i
# Pseudo-Array: disk_selected_$i (reference to $i element)
# List: disk_uuid_match (reference to matching preselected uuids)
# List: disk_target_match (reference to matching preselected targets)
# initialize our structures
i=0
for disk_uuid in $disk_uuids; do
if [ "$disk_uuid" = "$uuid_cmdline" ]; then
if [ ${#disk_uuid_match} -gt 0 ]; then
disk_uuid_match="${disk_uuid_match} $i"
else
disk_uuid_match="$i"
fi
disk_uuid_match_count=$(($disk_uuid_match_count+1))
fi
eval "disk_uuid_$i='$disk_uuid'"
disk_count=$(($disk_count+1))
i=$((i+1))
done
i=0
for disk_target in $disk_targets; do
if [ "$disk_target" = "$target_cmdline" ]; then
disk_target_match="$i"
disk_target_match_count=$(($disk_target_match_count+1))
fi
eval "disk_target_$i='$disk_target'"
i=$((i+1))
done
i=0
for fs_option in $fs_options; do
subvolid=$(eval echo \$fs_option | sed -e 's/.*subvolid=\([0-9]*\).*/\1/')
if [ "$subvolid" = "$subvolid_cmdline" ]; then
disk_subvolid_match="$i"
disk_subvolid_match_count=$(($disk_subvolid_match_count+1))
fi
eval "fs_options_$i='$fs_option'"
i=$((i+1))
done
}
notify () {
# estimation: batch calls should just log
if [ $donotify -gt 0 ]; then
for u in $(users | sed 's/ /\n/' | sort -u); do
sudo -u $u DISPLAY=:0 \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(sudo -u $u id -u)/bus \
notify-send -a $progname "$progname: $1" "$2" --icon="dialog-$3"
done
else
printf "%s\n" "$2"
fi
}
notify_error () {
notify "$1" "$2" "error"
}
notify_info () {
notify "$1" "$2" "information"
}
parse_params () {
#printf "\n${BLUE}Parse arguments...${NO_COLOR}\n"
# Evaluate given call parameters
while [ $# -gt 0 ]; do
key="$1"
case $key in
-h | --help | \-\? | --usage)
# Call usage() function.
usage
;;
-b|--backupdir)
backupdir_cmdline="$2"
shift 2
;;
-c|--config)
if [ ${#selected_config} -gt 0 ]; then
selected_configs="${selected_configs} ${2}"
else
selected_configs="$2"
fi
shift 2
;;
--config-postfix)
snapper_config_postfix="$2"
shift 2
;;
--description-finished)
shift
snap_description_finished="${*}"
snap_description_finished="${snap_description_finished%% -*}"
params=$*
set -- $snap_description_finished
count=$#
set -- $params
shift $count
;;
--description-running)
shift
snap_description_running=${*}
snap_description_running="${snap_description_running%% -*}"
params=$*
set -- $snap_description_running
count=$#
set -- $params
shift $count
;;
-d|--description|--description-synced)
shift
snap_description_synced="${*}"
snap_description_synced="${snap_description_synced%% -*}"
params=$*
set -- $snap_description_synced
count=$#
set -- $params
shift $count
;;
--dry-run|--dryrun)
dryrun=1
shift 1
;;
-i|--interactive)
interactive=1
donotify=1
shift
;;
-n|--noconfirm|--batch)
batch=1
do_pv_cmd=false
donotify=0
shift
;;
--color)
color=1
shift 1
;;
--nonotify)
donotify=0
shift 1
;;
--nopv)
do_pv_cmd=false
shift 1
;;
-p|--port)
port=$2
shift 2
;;
--remote)
remote=$2
shift 2
;;
-s|--subvolid|--SUBVOLID)
subvolid_cmdline="$2"
shift 2
;;
-t|--target|--TARGET)
target_cmdline="$2"
shift 2
;;
-u|--uuid|--UUID)
uuid_cmdline="$2"
shift 2
;;
-v|--verbose)
verbose=$(($verbose + 1))
shift 1
;;
--version)
printf "%s v%s\n" "$progname" "$version"
exit 0
;;
--backupdir=*)
backupdir_cmdline=${1#*=}
shift
;;
--config=*)
if [ ${#selected_config} -gt 0 ]; then
selected_config="${selected_config} ${1#*=}"
else
selected_config="${1#*=}"
fi
shift
;;
--config-postfix=*)
snapper_config_postfix="${1#*=}"
shift
;;
--color=*)
case ${1#*=} in
yes | Yes | True | true)
color=1;
;;
*)
;;
esac
shift
;;
--description-finished=*)
snap_description_finished="${*#*=}"
snap_description_finished="${snap_description_finished%% -*}"
params_new=${*#*=}
params_new=${params_new##${snap_description_finished}}
if [ ${#params_new} -gt 0 ]; then
set -- $snap_description_finished
count=$#
set -- $params_new
fi
;;
--description-running=*)
snap_description_running="${*#*=}"
snap_description_running="${snap_description_running%% -*}"
params_new=${*#*=}
params_new=${params_new##${snap_description_running}}
params=$#
if [ ${#params_new} -gt 0 ]; then
set -- $snap_description_running
count=$#
set -- $params_new
fi
;;
-d=*|--description=*|--description-synced=*)
snap_description_synced="${*#*=}"
snap_description_synced="${snap_description_synced%% -*}"
params_new=${*#*=}
params_new=${params_new##${snap_description_synced}}
if [ ${#params_new} -gt 0 ]; then
set -- $snap_description_synced
count=$#
set -- $params_new
else
break
fi
;;
--port=*)
port=${1#*=}
shift
;;
--remote=*)
remote=${1#*=}
shift
;;
--subvolid=*|--SUBVOLID=*)
subvolid_cmdline=${1#*=}
shift
;;
--target=*|--TARGET=*)
target_cmdline=${1#*=}
shift
;;
--uuid=*|--UUID=*)
uuid_cmdline=${1#*=}
shift
;;
--) # End of all options
shift
break
;;
-*)
printf "WARN: Unknown option (ignored): $1" >&2
die "Unknown option"
;;
*)
printf "Unknown option: %s\nRun '%s -h' for valid options.\n" $key $progname
die "Unknown option"
;;
esac
done
# Set reasonable defaults
. $SNAPPER_CONFIG
selected_configs=${selected_configs:-$SNAPPER_CONFIGS}
# message-text used for snapper fields
snap_description_finished=${snap_description_finished:-"dsnap-sync backup"}
snap_description_running=${snap_description_running:-"dsnap-sync in progress"}
snap_description_synced=${snap_description_synced:-"dsnap-sync last incremental"}
uuid_cmdline=${uuid_cmdline:-"none"}
target_cmdline=${target_cmdline:-"none"}
subvolid_cmdline=${subvolid_cmdline:-"none"}
backupdir_cmdline=${backupdir_cmdline:-"none"}
if [ -z "$remote" ]; then
ssh=""
else
ssh="ssh $remote"
if [ ! -z "$port" ]; then
ssh="$ssh -p $port"
fi
fi
if [ "$color" ]; then
# ascii color
BLUE='\033[0;34m'
GREEN='\033[0;32m'
MAGENTA='\033[0;35m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NO_COLOR='\033[0m'
fi
if [ $verbose -ge 1 ]; then
printf "${BLUE}$progname (runtime arguments)...${NO_COLOR}\n"
printf "for backup-source:\n"
printf " selected configs: '%s'\n" "$selected_configs"
printf "for backup-target:\n"
printf " disk UUID: '%s'\n" "$uuid_cmdline"
printf " disk TARGET: '%s'\n" "$target_cmdline"
printf " disk SUBVOLID: '%s'\n" "$subvolid_cmdline"
printf " disk Backupdir: '%s'\n" "$backupdir_cmdline"
printf " config postfix: '%s'\n" "$snapper_config_postfix"
printf " remote host: '%s'\n" "$remote"
printf " ssh options: '%s'\n" "$ssh"
printf "Snapper Descriptions\n"
printf " backup finished: '%s'\n" "$snap_description_finished"
printf " backup running: '%s'\n" "$snap_description_running"
printf " backup synced: '%s'\n" "$snap_description_synced"
if [ $verbose -ge 1 ]; then snap_sync_options="verbose_level=$verbose"; fi
if [ "$dryrun" ]; then snap_sync_options="${snap_sync_options} dry-run=true"; fi
if [ "$nonotify" ]; then snap_sync_options="${snap_sync_options} donotify=0"; fi
if [ "$color" ]; then snap_sync_options="${snap_sync_options} color=true"; fi
if [ "$batch" ]; then
snap_sync_options="${snap_sync_options} batch=true do_pv_cmd=false"
else
snap_sync_options="${snap_sync_options} do_pv_cmd=$do_pv_cmd"
fi
if [ "$interactive" ]; then snap_sync_options="${snap_sync_options} interactive=true batch=false"; fi
printf "Options: '%s'\n\n" "${snap_sync_options}"
fi
}
run_config () {
printf "${BLUE}Verify configuration...${NO_COLOR}\n"
SNAP_SYNC_EXCLUDE=no
# loop though selected snapper configurations
# Pseudo Arrays $i -> store associated elements of selected_config
i=0
for selected_config in $selected_configs; do
# only process existing dsnap-sync configurations
if [ ! -f "/etc/snapper/configs/$selected_config" ]; then
if [ $verbose -ge 1 ]; then
printf "Did you forget to create the snapper configuration for config '%s' on source?\n" \
"$selected_config"
printf "You can create it with following command:\n"
printf "${MAGENTA}snapper --config ${GREEN}%s${MAGENTA} create-config --template ${GREEN}%s${MAGENTA} <btrfs-subvolume-path>${NO_COLOR}\n" \
"$selected_config" "$snapper_subvolume_template"
fi
die "Can't backup snapper configuration '$selected_config' that does not exist."
else
. /etc/snapper/configs/$selected_config
if [ "$SUBVOLUME" = "/" ]; then
SUBVOLUME=''
fi
count=$(eval snapper --config $selected_config list --type single | \
awk '/'"$snap_description_synced"'/' | \
awk '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {cnt++} END {print cnt}')
if [ -n "$count" ] && [ "$count" -gt 1 ]; then
printf "%s entries are marked as '%s' for snapper config '%s'\n" \
"$count" "$snap_description_synced" "$selected_config"
printf "Pointing to target with UUID '%s' and SUBVOLID '%s'. Skipping configuration '%s'.\n" \
"$selected_uuid" "$selected_subvol" "$selected_config"
printf "Please cleanup for further processing.\n"
error "Skipping configuration $selected_config."
selected_configs=$(echo $selected_configs | sed -e "s/\($selected_config\)//")
continue
fi
fi
# cleanup failed former runs
check_snapper_failed_ids $batch
if [ $SNAP_SYNC_EXCLUDE = "yes" ]; then
continue
fi
# get latest successfully finished snapshot
# (tagged with userdata key/value pairs)
snapper_sync_id=$(eval snapper --config "$selected_config" list --type single | \
awk '/'"$snap_description_synced"'/' | \
awk '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {print $1}')
if [ ${#snapper_sync_id} -gt 0 ]; then
snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot
fi
eval "snapper_sync_id_$i='$snapper_sync_id'"
eval "snapper_sync_snapshot_$i='$snapper_sync_snapshot'"
# verify backupdir
if [ -z "$snapper_sync_id" ]; then
if [ $verbose -ge 1 ]; then
printf "No backups have been performed for snapper config '%s' on target disk.\n" \
"$selected_config"
fi
if [ "$backupdir_cmdline" != "none" ]; then
backupdir=$backupdir_cmdline
backup_root="$selected_target/$backupdir"
else
if [ ! $batch ]; then
read -r -p "Enter name of directory to store backups, relative to $selected_target (to be created if not existing): " backupdir
if [ -z "$backupdir" ]; then
backup_root="$selected_target"
else
backup_root="$selected_target/$backupdir"
fi
else
# use sane default
if [ -z "$backup_root" ]; then
backup_root="$selected_target"
fi
fi
fi
else
if [ $verbose -ge 1 ]; then
printf "Last syncronized Snapshot-ID for '%s': %s\n" "$selected_config" "$snapper_sync_id"
printf "Last syncronized Snapshot-Path for '%s': %s\n" "$selected_config" "$snapper_sync_snapshot"
fi
backupdir=$(snapper --config "$selected_config" list --type single \
| awk -F "|" '/'"$snap_description_synced"'/' \
| awk -F "|" '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {print $5}' \
| awk -F "," '/backupdir/ {print $1}' \
| awk -F"=" '{print $2}')
if [ "$interactive" ]; then
if [ -z "$backupdir"]; then
answer=yes
get_answer_yes_no "Keep empty backupdir [Y/n]? " "$answer"
else
get_answer_yes_no "Keep backupdir '$backupdir' [Y/n]? " "$answer"
fi
if [ "$answer" = "no" ]; then
read -r -p "Enter name of directory to store backups, relative to $selected_target (to be created if not existing): " backupdir
if [ -z "$backupdir" ]; then
backup_root="$selected_target"
else
backup_root="$selected_target/$backupdir"
fi
fi
fi
if [ -z "$backupdir" ]; then
backup_root="$selected_target"
else
backup_root="$selected_target/$backupdir"
fi
fi
eval "backup_root_$i=$backup_root"
eval "backup_dir_$i=$backupdir"
if [ $verbose -ge 1 ]; then
if [ -n "$remote" ];then
printf "Backup-Path on remote %s: %s\n" "$remote" "$backup_root"
else
printf "Backup-Path: %s\n" "$backup_root"
fi
fi
run_snapshot
i=$(($i+1))
done
}
run_snapshot () {
printf "${BLUE}Prepare snapshot...${NO_COLOR}\n"
# acting on source system
if [ ! $dryrun ]; then
#printf "Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE
if [ $verbose -ge 1 ]; then
printf "Creating new snapshot with snapper config '%s' ...\n" "$selected_config"
fi
snapper_new_id=$(snapper --config "$selected_config" create --print-number --description "$snap_description_running" --userdata "host=$remote")
snapper_new_snapshot=$SUBVOLUME/.snapshots/$snapper_new_id/snapshot
snapper_new_info=$SUBVOLUME/.snapshots/$snapper_new_id/info.xml
sync
if [ $verbose -ge 1 ]; then
printf "Snapper snapshot %s created\n" "$snapper_new_id"
answer=yes
get_answer_yes_no "Continue [Y/n]? " "$answer"
if [ "$answer" = "no" ]; then
die "Exit on user request."
fi
fi
else
#printf "dryrun: Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE
printf "dryrun: Creating new snapshot with snapper config '%s' ...\n" "$selected_config"
snapper_new_id="<new_snapper_id>"
fi
# Snapshot types: d2d and cloning
# d2d: 1st stage snapshots: CHILD_CONFIG="false" or missing
# clone: 2nd stage snapshots: CHILD_CONFIG="true" PARENT_CONFIG="<snapper config name>"
# parse selected_config and return with $snapper_target_config set appropriately
set_snapper_target_config $selected_config
# if we want to use snapper on the target to supervise the synced snapshots
# the backup_location needs to be in a subvol ".snapshots" inside $selected_config (hardcoded in snapper)
snapper_target_snapshot=$backup_root/$snapper_target_config/.snapshots/$snapper_new_id
if [ $verbose -ge 1 ]; then
if [ -z "$remote" ]; then
printf "Will backup '%s' to '%s'\n" "$snapper_new_snapshot" "$snapper_target_snapshot/snapshot"
else
printf "Will backup '%s' to '%s'\n" "$snapper_new_snapshot" "$remote":"$snapper_target_snapshot/snapshot"
fi
fi
# save in config specific infos in pseudo Arrays
eval "snapper_new_id_$i='$snapper_new_id'"
eval "snapper_new_snapshot_$i='$snapper_new_snapshot'"
eval "snapper_new_info_$i='$snapper_new_info'"
eval "snapper_sync_snapshot_$i='$snapper_sync_snapshot'"
eval "snapper_config_$i='$selected_config'"
eval "snapper_target_config_$i='$snapper_target_config'"
eval "snapper_target_snapshot_$i='$snapper_target_snapshot'"
cont_backup="K"
eval "snapper_activate_$i=yes"
if [ $batch ]; then
cont_backup="yes"
else
answer=yes
get_answer_yes_no "Continue with backup [Y/n]? " "$answer"
if [ "$answer" = "no" ]; then
eval "snapper_activate_$i=no"
printf "Aborting backup for this configuration.\n"
snapper --config $selected_config delete $snapper_new_id
fi
fi
}
run_cleanup () {
batch="1"
# cleanup failed runs
check_snapper_failed_ids "$batch"
# cleanup target
#$ssh btrfs subvolume delete $backup_root/$snapper_target_config/$snapper_snapshots/$snapper_new_id/snapshot
#$ssh rm -rf $backup_root/$snapper_target_config/$snapper_snapshots/$snapper_new_id
# cleanup TEMPDIR
if [ -d $TMPDIR_PIPE ]; then
rm -rf $TMPDIR_PIPE || die "Failed to cleanup temporary directory '%s'\n" "$TMPDIR_PIPE"
fi
}
run_backup () {
printf "${BLUE}Performing backups...${NO_COLOR}\n"
i=-1
for selected_config in $selected_configs; do
i=$(($i+1))
SNAP_SYNC_EXCLUDE=no
if [ -f "/etc/snapper/configs/$selected_config" ]; then
. /etc/snapper/configs/$selected_config
else
die "Selected snapper configuration '$selected_config' does not exist."
fi
cont_backup=$(eval echo \$snapper_activate_$i)
if [ "$cont_backup" = "no" ] || [ "$SNAP_SYNC_EXCLUDE" = "yes" ]; then
if [ $donotify -gt 0 ]; then
notify_info "Backup in progress" "NOTE: Skipping '$selected_config' configuration."
fi
continue
fi
notify_info "Backup in progress" "Backing up data for configuration '$selected_config'."
# retrieve config specific infos from pseudo Arrays
snapper_config=$(eval echo \$snapper_config_$i)
backup_root=$(eval echo \$backup_root_$i)
backup_dir=$(eval echo \$backup_dir_$i)
snapper_sync_id=$(eval echo \$snapper_sync_id_$i)
snapper_new_id=$(eval echo \$snapper_new_id_$i)
snapper_sync_snapshot=$(eval echo \$snapper_sync_snapshot_$i)
snapper_new_snapshot=$(eval echo \$snapper_new_snapshot_$i)
snapper_new_info=$(eval echo \$snapper_new_info_$i)
snapper_target_config=$(eval echo \$snapper_target_config_$i)
snapper_target_snapshot=$(eval echo \$snapper_target_snapshot_$i)
# create the needed snapper structure on the target
verify_snapper_structure "backup_root=$backup_root" "snapper_target_config=$snapper_target_config" "snapper_new_id=$snapper_new_id" "remote=$remote"
# TODO: to report correct values btrfs-quota must be active for the source subvol!
snapper_target_snapshot_size=$(du --sum $snapper_new_snapshot 2>/dev/null | awk -F ' ' '{print $1}')
# settings for interactive progress status
if [ "$do_pv_cmd" = "true" ]; then
pv_options="--delay-start 2 --interval 5 --timer --rate --bytes --fineta --no-splice --buffer-percent --progress"
cmd_pv="pv $pv_options --size $snapper_target_snapshot_size |"
#cmd_pv="pv $pv_options --size $snapper_target_snapshot_size | dialog --gauge \"$progname: Progress for config '$selected_config'\" 6 85 |"
else
cmd_pv=''
fi
if [ -z "$snapper_sync_id" ]; then
# target never received any snapshot before
cmd="btrfs send $verbose_flag $snapper_new_snapshot 2>$BTRFS_PIPE | $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>$BTRFS_PIPE"
if [ $verbose -ge 1 ]; then
printf "${MAGENTA}Sending first snapshot${NO_COLOR} for snapper config ${MAGENTA}'%s' (size=%s)${NO_COLOR} ...\n" "$selected_config" "$snapper_target_snapshot_size"
fi
if [ ! "$dryrun" ]; then
# the actual data sync to the target
# this may take a while, depending on datasize and line-speed
if [ $verbose -ge 2 ]; then
printf "cmd: '%s'\n" "$cmd"
fi
$(eval $cmd) 1>/dev/null
if [ "$?" -gt 0 ]; then
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}" "$(eval cat $BTRFS_PIPE)"
die "btrfs pipe error."
fi
else
printf "dryrun: %s\n" "$cmd"
fi
else
# target holds synced snapshots
# checking if parent snapshot-id (as saved on source) is also available on target
if [ $verbose -ge 1 ]; then
printf "${MAGENTA}Sending incremental snapshot${NO_COLOR} for snapper config ${MAGENTA}'%s'${NO_COLOR} ...\n" "$selected_config"
fi
if [ $verbose -ge 2 ]; then
printf "Old synced snapshot: '%s' (id: %s)\n" "$snapper_sync_snapshot" "$snapper_sync_id"
printf "New source snapshot: '%s' (id: %s)\n" "$snapper_new_snapshot" "$snapper_new_id"
printf "New target snapshot: '%s' (id: %s)\n" "$snapper_target_snapshot/snapshot" "$snapper_new_id"
fi
cmd="$ssh stat --format %i $backup_root/$snapper_target_config/$snapper_snapshots/$snapper_source_sync_id 2>/dev/null"
ret=$(eval $cmd)
if [ $? -eq 0 ]; then
# Sends the difference between the new snapshot and old synced snapshot to the
# backup location. Using the -c (clone-source) flag instead of -p (parent) tells it
# that there is an identical subvolume to the synced snapshot at the receiving
# location where it can get its data. This helps speed up the transfer.
cmd="btrfs send $verbose_flag -c $snapper_sync_snapshot $snapper_new_snapshot 2>$BTRFS_PIPE | $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>$BTRFS_PIPE"
if [ ! "$dryrun" ]; then
if [ $verbose -ge 2 ]; then
printf "${GREEN}btrfs-send${NO_COLOR} will use snapshot ${GREEN}'%s' on target${NO_COLOR} to sync metadata for %s ...\n" "$snapper_sync_snapshot" "$snapper_new_snapshot"
printf "cmd: '%s'\n" "$cmd"
fi
eval $cmd 1>/dev/null
if [ "$?" -gt 0 ]; then
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}" "$(eval cat $BTRFS_PIPE)"
die "btrfs pipe error."
fi
else
printf "dryrun: '%s'" "cmd"
#printf "dryrun: btrfs send %s -c %s %s | %s btrfs receive %s %s\n" \
# "$verbose_flag" "$snapper_sync_snapshot" "$snapper_new_snapshot" \
# "$remote" "$verbose_flag" "$snapper_target_snapshot"
printf "dryrun: snapper --config %s delete %s\n" "$selected_config" "$snapper_sync_id"
fi
else
# need to use source snapshot to provide metadata for target
cmd="btrfs send $verbose_flag -p $snapper_sync_snapshot snapper_new_snapshot 2>$BTRFS_PIPE | $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>BTRFS_PIPE"
if [ ! "$dryrun" ]; then
if [ $verbose -ge 1 ]; then
printf "${GREEN}btrfs-send${NO_COLOR} is using snapshot ${GREEN}'%s' from source${NO_COLOR} to read metadata ...\n" "$snapper_sync_snapshot"
printf "cmd: '%s'\n" "$cmd"
fi
$(eval $cmd)
if [ "$?" -gt 0 ]; then
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}" "$(eval cat $BTRFS_PIPE)"
die "btrfs pipe error."
fi
#printf "btrfs returns: '%i'\n" "$ret"
else
printf "dryrun: Would run btrfs-send / btrfs-recieve\n"
#printf "dryrun: btrfs send %s -p %s %s | %s %s btrfs receive %s %s\n" \
# "$verbose_flag" "$snapper_sync_snapshot" "$snapper_new_snapshot" \
# "$cmd_pv" "$ssh" \
# "$remote" "$verbose_flag" "$snapper_target_snapshot"
fi
fi
fi
# finally: send the snapper info metadata
if [ -z "$remote" ]; then
if [ ! "$dryrun" ]; then
cp "$snapper_new_info" "$snapper_target_snapshot"
else
cmd="cp $snapper_new_info $snapper_target_snapshot"
printf "dryrun: %s\n" "$cmd"
fi
else
if [ ! "$dryrun" ]; then
if [ -n "$port" ]; then
rsync -avzq -e "ssh -p $port" "$snapper_new_info" "$remote:$snapper_target_snapshot"
else
rsync -avzq "$snapper_new_info" "$remote":"$snapper_target_snapshot"
fi
else
if [ -n "$port" ]; then
cmd="rsync -avzq -e \"ssh -p $port\" $snapper_new_info $remote:$snapper_target_snapshot"
else
cmd="rsync -avzq $snapper_new_info $remote:$snapper_target_snapshot"
fi
printf "dryrun: %s\n" "$cmd"
fi
fi
done
}
run_finalize () {
# Actual backing up
printf "${BLUE}Finalize backups...${NO_COLOR}\n"
i=-1
for selected_config in $selected_configs; do
i=$(($i+1))
SNAP_SYNC_EXCLUDE=no
if [ -f "/etc/snapper/configs/$selected_config" ]; then
. /etc/snapper/configs/$selected_config
else
die "Selected snapper configuration '$selected_config' does not exist."
fi
cont_backup=$(eval echo \$snapper_activate_$i)
if [ "$cont_backup" = "no" ] || [ "$SNAP_SYNC_EXCLUDE" = "yes" ]; then
if [ $donotify -gt 0 ]; then
notify_info "Finalize backup" "NOTE: Skipping '$selected_config' configuration."
fi
continue
fi
if [ $donotify -gt 0 ]; then
notify_info "Finalize backup" "Cleanup tasks for configuration '$selected_config'."
fi
# retrieve config specific infos from pseudo Arrays
snapper_config=$(eval echo \$snapper_config_$i)
backup_root=$(eval echo \$backup_root_$i)
backup_dir=$(eval echo \$backup_dir_$i)
snapper_sync_id=$(eval echo \$snapper_sync_id_$i)
snapper_new_id=$(eval echo \$snapper_new_id_$i)
snapper_sync_snapshot=$(eval echo \$snapper_sync_snapshot_$i)
snapper_new_snapshot=$(eval echo \$snapper_new_snapshot_$i)
snapper_new_info=$(eval echo \$snapper_new_info_$i)
snapper_target_config=$(eval echo \$snapper_target_config_$i)
snapper_target_snapshot=$(eval echo \$snapper_target_snapshot_$i)
# It's important not to change the values of the snapper key/value pairs ($userdata)
# which is stored in snappers info.xml file of the source snapshot.
# This is how we find the parent.
src_host=$(eval cat /etc/hostname)
src_uuid=$(eval findmnt --noheadings --output UUID --target $SUBVOLUME)
src_subvolid=$(eval findmnt --noheadings --output OPTIONS $SUBVOLUME | sed -e 's/.*subvolid=\([0-9]*\).*/\1/')
# Tag new snapshots key/value parameter
if [ $verbose -ge 1 ]; then
printf "${MAGENTA}Tagging target ...${NO_COLOR}\n"
fi
if [ ! "$dryrun" ]; then
# target snapshot
# 1) wait for target snapshot to show up in snapper list
# find "$snapper_new_id" -> marked as "$snap_descrition_running"
# 2) toggle metadata -> mark as "$snap_description_finished", reference "$target_userdata" (subvolid, uuid, host)
# !!! ugly hack !!!: wait for snapper to list target snapshot in database.
# Problem: how to trigger that database is synced? -> a feature request is send to snapper upstream source
# Solution: right now, it is no-deterministic, when the entry in the listing will show up
# -> wait ii_max * ii_sleep seconds ( 20*15 = 300 -> 5 Min)
local ii=1
local ii_max=20
local ii_sleep=15
# Solution2: kill running snapperd
# -> will restart and sync
$(eval $ssh killall -SIGTERM snapperd)
#printf "Killall '%s'\n" "$?"
if [ $verbose -ge 2 ]; then
printf "${YELLOW}Identify snapper id ${GREEN}'%s'${YELLOW} on target for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_new_id" "$snapper_target_config"
fi
# construct snapper match command
cmd="$ssh snapper --verbose --config \"$snapper_target_config\" list --type single \
| awk ' /'\"$snap_description_running\"'/ ' \
| awk -F '|' ' \$1 == $snapper_new_id {print \$1} ' "
while [ "$ii" -le "$ii_max" ]; do
if [ $verbose -ge 2 ]; then
printf "calling: '%s'\n" "$cmd"
fi
ret=$(eval $cmd)
#ret=$ssh snapper --verbose --config \"$snapper_target_config\" list --type single \
# | awk ' /'\"$snap_description_running\"'/ ' \
# | awk -F '|' ' $1 == "$snapper_new_id" {print $1} '
#printf "return: '%s'\n" "$?"
if [ $? -eq 0 ]; then
#printf "return: snapper_new_id '%s'\n" "$ret"
if [ $ret -eq $snapper_new_id ]; then
# got snapshot as $snapper_new_id
if [ $verbose -ge 2 ]; then
printf "${YELLOW}Found${NO_COLOR} snapper id ${GREEN}'%s'${NO_COLOR} on target for configuration ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_new_id" "$snapper_target_config"
fi
if [ $verbose -ge 2 ]; then
printf "${YELLOW}Tagging metadata${NO_COLOR} for snapper id ${GREEN}'%s'${NO_COLOR} on target for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_new_id" "$snapper_target_config"
#printf "calling: '%s'\n" "$cmd"
fi
# call command (respect needed quotes)
if [ $remote ]; then
ret=$(eval $ssh snapper --config \\\'$snapper_target_config\\\' modify \
--description \\\'$snap_description_finished\\\' \
--userdata \\\'host=$src_host, subvolid=$src_subvolid, uuid=$src_uuid\\\' \
--cleanup-algorithm \'timeline\' \
\'$snapper_new_id\')
else
ret=$(snapper --config $snapper_target_config modify \
--description \"$snap_description_finished\" \
--userdata \'"subvolid=$src_subvolid, uuid=$src_uuid, host=$src_host"\' \
--cleanup-algorithm \"timeline\" \
$snapper_new_id)
fi
if [ $verbose -ge 2 ]; then
printf "return: '%s'\n" "$ret"
fi
break
fi
fi
if [ $verbose -ge 2 ]; then
printf "%s/%s: ${RED}Waiting another '%s' seconds${NO_COLOR} for snappers database update on target ...\n" \
"$ii" "$ii_max" "$ii_sleep"
fi
sleep $ii_sleep
ii=$(($ii + 1))
done
# source snapshot
if [ $verbose -ge 1 ]; then
printf "${MAGENTA}Tagging source ...${NO_COLOR}\n"
fi
cmd="snapper --config $selected_config modify \
--description \"$snap_description_synced\" \
--userdata \"backupdir=$backup_dir, subvolid=$selected_subvol, uuid=$selected_uuid, host=$remote\" \
--cleanup-algorithm \"timeline\" \
$snapper_new_id"
if [ $verbose -ge 2 ]; then
printf "${YELLOW}Tagging snapper metadata${NO_COLOR} for snapper id ${GREEN}'%s'${NO_COLOR} on source for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_new_id" "$selected_config"
printf "calling: '%s'\n" "$cmd"
fi
ret=$(eval "$cmd")
# !!working!!
#snapper --config $snapper_config modify \
# --description \"$snap_description_synced\" \
# --cleanup-algorithm \"timeline\" \
# --userdata "\'backupdir=$backup_dir, subvolid=$selected_subvol, uuid=$selected_uuid, host=$remote\'" \
# $snapper_new_id
if [ $verbose -ge 2 ]; then
printf "return: '%s'\n" "$?"
fi
sync
if [ ${#snapper_sync_id} -gt 0 ]; then
cmd="snapper --config $selected_config modify \
--description \"$snap_description_finished\" \
$snapper_sync_id"
if [ $verbose -ge 2 ]; then
printf "${YELLOW}Tagging snapper metadata${NO_COLOR} for snapper sync id ${GREEN}'%s'${NO_COLOR} on source for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_sync_id" "$selected_config"
printf "calling: '%s'\n" "$cmd"
fi
ret=$(eval "$cmd")
snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot
fi
else
# dry-run output
cmd="$ssh snapper --verbose --config $snapper_target_config modify -description $snap_description_finished --userdata $target_userdata --cleanup-algorithm $snap_cleanup_algorithm $snapper_new_id"
printf "dryrun: %s\n" "$cmd"
cmd="snapper --config $selected_config modify --description $snap_description_synced --userdata $userdata $snapper_new_id"
printf "dryrun: %s\n" "$cmd"
cmd="snapper --config $selected_config modify --description $snap_description_finished $snapper_sync_id"
printf "dryrun: %s\n" "$cmd"
fi
printf "Backup complete for snapper configuration '%s'.\n" "$selected_config" > $PIPE
done
}
select_target_disk () {
local i=0
local disk_id=0
#local disk_selected_ids=''
local disk_selected_count=0
local subvolid=''
local subvol=''
printf "${BLUE}Select target disk${NO_COLOR} on target %s...\n" \
"$remote"
# print selection table
if [ $verbose -ge 1 ]; then
if [ -z "$remote" ]; then
printf "Selecting a mounted BTRFS device for backups on your local machine.\n"
else
printf "Selecting a mounted BTRFS device for backups on %s.\n" "$remote"
fi
fi
while [ "$disk_id" -eq -1 ] || [ "$disk_id" -le $disk_count ]; do
if [ "$disk_subvolid_match_count" -eq 1 ]; then
# matching SUBVOLID selection from commandline
# Pseudo-Array: disk_selected_$i (reference to $disk_uuid element)
eval "disk_selected_$i='$disk_subvolid_match'"
disk=$(eval echo \$disk_uuid_$disk_subvolid_match)
subvolid=$(eval echo \$disk_subvolid_$disk_subvolid_match)
fs_options=$(eval echo \$fs_options_$disk_subvolid_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
disk_selected=$disk_subvolid_match
break
fi
if [ "$disk_target_match_count" -eq 1 ]; then
# matching TARGET selection from commandline
# Pseudo-Array: disk_selected_$i (reference to $disk_uuid element)
eval "disk_selected_$i='$disk_target_match'"
disk=$(eval echo \$disk_uuid_$disk_target_match)
target=$(eval echo \$disk_target_$disk_target_match)
fs_options=$(eval echo \$fs_options_$disk_target_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
disk_selected=$disk_target_match
break
fi
if [ "$disk_uuid_match_count" -gt 1 ]; then
# got UUID selection from commandline
disk_count=$disk_uuid_match_count
if [ $verbose -ge 1 ]; then
printf "%s mount points were found with UUID '%s'.\n" "$disk_uuid_match_count" "$uuid_cmdline"
fi
for disk_uuid in $disk_uuid_match; do
# Pseudo-Array: disk_selected_$i (reference to $disk_uuid element)
eval "disk_selected_$i='$disk_uuid'"
disk=$(eval echo \$disk_uuid_$disk_uuid)
fs_options=$(eval echo \$fs_options_$disk_uuid | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
printf "%4s) %s (uuid=%s,%s)\n" "$i" "$target" "$disk" "$fs_options"
i=$((i+1))
done
else
while [ "$disk_id" -le $disk_count ]; do
# present all mounted BTRFS filesystems
# Pseudo-Array: disk_selected_$i (reference to $disk_id element)
eval disk_selected_$i="$disk_id"
disk=$(eval echo \$disk_uuid_$disk_id)
target=$(eval echo \$disk_target_$disk_id)
fs_options=$(eval echo \$fs_options_$disk_id | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
printf "%4s) %s (uuid=%s,%s)\n" "$i" "$target" "$disk" "$fs_options"
i=$((i+1))
disk_id=$(($disk_id+1))
done
fi
printf "%4s) Exit\n" "x"
read -r -p "Enter a number: " disk_selected
case $disk_selected in
x)
break
;;
[0-9][0-9]|[0-9])
if [ "$disk_selected" -gt "$disk_count" ]; then
disk_id=0
i=0
else
break
fi
;;
*)
printf "\nNo disk selected. Select a disk to continue.\n"
disk_id=0
i=0
;;
esac
done
if [ "$disk_selected" = x ]; then
exit 0
fi
selected_uuid=$(eval echo \$disk_uuid_$disk_selected)
selected_target=$(eval echo \$disk_target_$disk_selected)
selected_subvol=$(eval echo \$fs_options_$disk_selected | sed -e 's/.*subvolid=\([0-9]*\).*/\1/')
if [ "$dryrun" ]; then
printf "Selected Subvol-ID=%s: %s on %s\n" "$selected_subvol" "$selected_target" "$selected_uuid"
fi
if [ $verbose -ge 1 ]; then
printf "\nYou selected the disk with UUID %s (subvolid=%s).\n" "$selected_uuid" "$selected_subvol"
if [ -z "$remote" ]; then
printf "The disk is mounted at %s.\n" "$selected_target"
else
printf "The disk is mounted at %s:%s.\n" "$remote" "$selected_target"
fi
fi
}
set_config(){
local config=${1:-/etc/snapper/config-templates/"$snapper_subvolume_template"}
local config_key=${2:-SUBVOLUME}
local config_value=${3:-/var/lib/dsnap-sync}
if [ -n "$remote" ]; then
$ssh sed -i \'"s#^\($config_key\s*=\s*\).*\$#\1\"$config_value\"#"\' $config
else
sed -i "s#^\($config_key\s*=\s*\).*\$#\1\"$config_value\"#" $config
fi
}
traperror () {
printf "Exited due to error on line %s.\n" $1
printf "exit status: %s\n" "$2"
#printf "command: %s\n" "$3"
#printf "bash line: %s\n" "$4"
#printf "function name: %s\n" "$5"
exit 1
}
trapkill () {
printf "Exited due to user intervention.\n"
run_cleanup 1
exit 0
}
usage () {
cat <<EOF
$progname $version
Usage: $progname [options]
Options:
-b, --backupdir <prefix> backupdir is a relative path that will be appended to target backup-root
-d, --description <desc> Change the snapper description. Default: "latest incremental backup"
--label-finished <desc> snapper description tagging successful jobs. Default: "dsnap-sync backup"
--label-running <desc> snapper description tagging active jobs. Default: "dsnap-sync in progress"
--label-synced <desc> snapper description tagging last synced jobs.
Default: "dsnap-sync last incremental"
--color Enable colored output messages
-c, --config <config> Specify the snapper configuration to use. Otherwise will perform for each snapper
configuration. Can list multiple configurations within quotes, space-separated
(e.g. -c "root home").
--config-postfix <name> Specify a postfix that will be appended to the destination snapper config name.
-n, --noconfirm Do not ask for confirmation for each configuration. Will still prompt for backup
--batch directory name on first backup"
--nonotify Disable graphical notification (via dbus)
--nopv Disable graphical progress output (disable pv)
-r, --remote <address> Send the snapshot backup to a remote machine. The snapshot will be sent via ssh.
You should specify the remote machine's hostname or ip address. The 'root' user
must be permitted to login on the remote machine.
-p, --port <port> The remote port.
-s, --subvolid <subvlid> Specify the subvolume id of the mounted BTRFS subvolume to back up to. Defaults to 5.
-u, --uuid <UUID> Specify the UUID of the mounted BTRFS subvolume to back up to. Otherwise will prompt."
If multiple mount points are found with the same UUID, will prompt user."
-t, --target <target> Specify the mountpoint of the BTRFS subvolume to back up to.
--remote <address> Send the snapshot backup to a remote machine. The snapshot will be sent via ssh. You
should specify the remote machine's hostname or ip address. The 'root' user must be
permitted to login on the remote machine.
--dry-run perform a trial run where no changes are made.
-v, --verbose Be more verbose on what's going on.
--version show program version
EOF
exit 1
}
verify_snapper_structure () {
local backup_root=${1##backup_root=}
local snapper_config=${2##snapper_target_config=}
local snapper_id=${3##snapper_new_id=}
local remote_host=${4##remote=}
if [ $verbose -ge 1 ]; then
printf "${MAGENTA}Verify snapper filesystem structure${NO_COLOR} on target %s...\n" \
"$remote"
fi
# if not accessible, create backup-path
cmd="$ssh stat --format %i $backup_root 2>/dev/null"
if [ -z $(eval $cmd) ]; then
if [ ! "$dryrun" ]; then
if [ $verbose -ge 1 ]; then
if [ -z $remote_host ]; then
printf "Create backup-path %s ...\n" \
"$backup_root"
else
printf "Create backup-path %s:%s ...\n" \
"$remote_host" "$backup_root"
fi
fi
if [ ! $dryrun ]; then
$(eval $ssh mkdir --mode=0700 --parents $backup_root)
else
if [ -z $remote_host ]; then
printf "dryrun: Would create backup-path %s ...\n" \
"$backup_root"
else
printf "dryrun: Would create backup-path %s on remote host %s ...\n" \
"$remote_host" "$backup_root"
fi
fi
fi
fi
# verify that we have a snapper compatible structure for selected config on target (a btrfs subvolume)
cmd="$ssh stat --format %i $backup_root/$snapper_config 2>/dev/null"
if [ -z $(eval $cmd) ]; then
if [ $verbose -ge 1 ]; then
if [ -z "$remote" ]; then
printf "${MAGENTA}Create${NO_COLOR} new snapper capable BTRFS subvolume ${MAGENTA}'%s'${NO_COLOR} ...\n" \
"$backup_root/$snapper_config"
else
printf "${MAGENTA}Create${NO_COLOR} new snapper capable BTRFS subvolume '%s' on ${MAGENTA}remote host '%s'${NO_COLOR} ...\n" \
"$remote_host" "$backup_root/$snapper_config"
fi
fi
if [ ! "$dryrun" ]; then
# verify that we can use the correct snapper template
cmd="$ssh stat --format %i $SNAPPER_TEMPLATE_DIR/$snapper_subvolume_template 2>/dev/null"
if [ -z $(eval $cmd) ]; then
printf "${RED}Missing a snapper template %s${NO_COLOR} to configure the snapper subvolume %s in %s on %s.\n" \
"$snapper_subvolume_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host"
printf "Did you miss to install the dsnap-sync's default snapper template on %s?\n" \
"$remote"
die "snapper template %s to configure the snapper subvolume %s is missing in %s on %s.\n" \
"$snapper_subvolume_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host"
fi
# create the non existing remote BTRFS subvolume for given config
cmd="$ssh btrfs subvolume create $backup_root/$snapper_config 1>/dev/null"
$(eval $cmd) || die "Creation of BTRFS subvolume %s:%s failed.\n" \
"$remote_host" "$backup_root/$snapper_config"
cmd="$ssh chmod 0700 $backup_root/$snapper_config"
$(eval $cmd) || die "Changing the directory mode for %s on %s failed.\n" \
"$backup_root/$snapper_config" "$remote_host"
# create the non existing remote BTRFS subvolume for given snapshot
cmd="$ssh stat --format %i $backup_root/$snapper_config/$snapper_snapshot 2>/dev/null"
if [ -z $(eval $cmd) ]; then
cmd="$ssh btrfs subvolume create $backup_root/$snapper_config/$snapper_snapshot 1>/dev/null"
$(eval $cmd) || \
die "Creation of BTRFS subvolume $remote_host: $backup_root/$snapper_config failed."
cmd="$ssh chmod 0700 $backup_root/$snapper_config 1>/dev/null"
$(eval $cmd) || \
die "Changing the directory mode for '$backup_root/$snapper_config' on '$remote_host'."
fi
else
printf "dryrun: Would create new snapper configuration from template %s ...\n" "$snapper_subvolume_template"
printf "dryrun: Would create new snapper subvolume '%s' ...\n" "$backup_root/$snapper_config/$snapper_snapshot"
fi
else
cmd="$ssh stat --format %i $backup_root/$snapper_config 2>/dev/null"
if [ $(eval $cmd) -ne 256 ]; then
die "%s needs to be a BTRFS subvolume. But given %s is just a directory.\n" "$snapper_config" "$backup_root/$snapper_config"
fi
if [ $verbose -ge 1 ]; then
printf "${RED}TODO:${NO_COLOR} check and adapt SUBVOLUME in given config '%s', since mount path might have changed meanwhile\n" "$snapper_config"
fi
#$ssh $(. /etc/snapper/configs/$snapper_config)
#get_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME"
#if $ssh [ "$SUBVOLUME" != \"$backup_root/$snapper_config\" ]; then
# SUBVOLUME="$backup_root/$snapper_config"
# set_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" "$SUBVOLUME"
#fi
fi
# verify that we have a valid snapper config
if [ ! "$dryrun" ]; then
cmd="$ssh stat --format %i $SNAPPER_CONFIG_DIR/$snapper_config 2>/dev/null"
if [ -z $(eval $cmd) ]; then
# snapper will create new structure at $backup_root/$snapper_config/.snapshots
cmd="$ssh snapper --config $snapper_config create-config --template $snapper_subvolume_template --fstype btrfs $backup_root/$snapper_config"
if [ $verbose -ge 1 ]; then
printf "create new snapper_config '%s' using template '%s'\n" $snapper_config $snapper_subvolume_template
fi
$(eval $cmd) || die "Creation of snapper capable config %s on %s failed.\n" \
"$backup_root/$snapper_config" "$remote_host"
else
# verify if SUBVOLUME needs to be updated for given snapper config
cmd="$ssh snapper list-configs | awk -F '|' '/'\"^$snapper_config\"'/ {print \$1}'"
#cmd="$ssh snapper list-configs | awk '/'\"^$snapper_config\"'/' | awk -F '|' ' /'\$1 == "$snapper_config"'/ {print \$1}'"
#cmd="$ssh snapper list-configs | awk '/'\"$snapper_config\"'/'"
#ret=$(eval $cmd)
#if [ -z $ret ]; then
if [ -n $(eval $cmd) ]; then
# if changed, adapt targets SUBVOLUME path
if [ $verbose -ge 1 ]; then
printf "${RED}TODO:${NO_COLOR} Check if value for key 'SUBVOLUME' needs an update in snapper config %s\n" \
"$snapper_config"
fi
#get_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME"
#if $ssh [ "$SUBVOLUME" != \"$backup_root/$snapper_config\" ]; then
# SUBVOLUME="$backup_root/$snapper_config"
# set_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" "$SUBVOLUME"
# cmd="btrfs subvolume create $backup_root/$snapper_config/$snapper_snapshots"
# $ssh $cmd || die "Can't create subvolume %s in %s to hold target snapshots.\n" "$snapper_snapshots" "$backup_root/$snapper_config"
#fi
fi
# verify existence of SUBVOLUME $backup_root/$snapper_config/.snapshots
cmd="$ssh stat --format %i $backup_root/$snapper_config/$snapper_snapshots 2>/dev/null"
ret=$(eval $cmd)
if [ -z $ret ]; then
if [ $verbose -ge 1 ]; then
printf "create new BTRFS subvolume '%s'\n" $backup_root/$snapper_config/$snapper_snapshots
fi
cmd="$ssh btrfs subvolume create $backup_root/$snapper_config/$snapper_snapshots"
$(eval $cmd) || die "Creation of snapper subvolume %s failed.\n" \
"$backup_root/$snapper_config/$snapper_snapshots"
else
if [ $ret -ne 256 ]; then
die "%s needs to be a BTRFS subvolume. But given %s is just a directory.\n" \
"$snapper_config" "$backup_root/$snapper_config"
fi
fi
fi
else
printf "dryrun: Would check/create for valid snapper config %s ...\n" \
"$snapper_config"
fi
# verify that target snapshot can take the new snapshot data id
if [ ! "$dryrun" ]; then
if [ $verbose -ge 1 ]; then
printf "${MAGENTA}Verify existence of path '%s'.${NO_COLOR}\n" \
"$backup_root/$snapper_config/$snapper_snapshots/$snapper_id"
fi
cmd="$ssh stat --format %i $backup_root/$snapper_config/$snapper_snapshots/$snapper_id 2>/dev/null"
if [ -z "$(eval $cmd)" ]; then
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Create path %s${NO_COLOR} to store target snapshot.\n" \
"$backup_root/$snapper_config/$snapper_snapshots/$snapper_id"
fi
$(eval $ssh mkdir --mode=0700 \
$backup_root/$snapper_config/$snapper_snapshots/$snapper_id)
if [ $? -ne 0 ]; then
printf "${RED}Cancel path snapshot creation${NO_COLOR}: Can't create path '%s' to store target snapshot.\n" \
"$backup_root/$snapper_config/$snapper_snapshots/$snapper_id"
die "Can't create snapshot dir on target."
fi
else
if [ -z "$remote" ]; then
printf "${RED}Cancel snapshot creation${NO_COLOR}: Former snapshot with id ${GREEN}'%s'${NO_COLOR} already exist in ${BLUE}'%s'${NO_COLOR}\n" \
"$snapper_id" "$backup_root/$snapper_config/$snapper_snapshots"
else
printf "${RED}Cancel snapshot creation${NO_COLOR}: Former snapshot with id ${GREEN}'%s'${NO_COLOR} already exist on ${BLUE}'%s' in '%s'${NO_COLOR}\n" \
"$snapper_id" "$remote" "$backup_root/$snapper_config/$snapper_snapshots"
fi
# cleanup generated snapper entry
check_snapper_failed_ids $batch
die "Can't create new snapshot with given snapshot-id!"
fi
else
printf "dryrun: Would check/create path %s to store target snapshot ...\n" \
"$backup_root/$snapper_config/$snapper_snapshots/$snapper_id"
fi
}
###
# Main
###
cwd=`pwd`
ssh=""
# can't be ported to dash (ERR is not supported)
#trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCNAME[@]}"' ERR
trap trapkill TERM INT
check_prerequisites
# validate commandline options, set resonable defaults
parse_params $@
# read mounted BTRFS structures
get_disk_infos
if [ $donotify -gt 0 ]; then
if [ "$target_cmdline" != "none" ]; then
if [ -z "$remote" ]; then
notify_info "Backup started" "Starting backups to '$target_cmdline' ..."
else
notify_info "Backup started" "Starting backups to '$target_cmdline' at $remote ..."
fi
elif [ "$uuid_cmdline" != "none" ]; then
if [ -z "$remote" ]; then
notify_info "Backup started" "Starting backups to $uuid_cmdline..."
else
notify_info "Backup started" "Starting backups to $uuid_cmdline at $remote..."
fi
else
if [ -z "$remote" ]; then
notify_info "Backup started" "Starting backups. Use command line menu to select disk."
else
notify_info "Backup started" "Starting backups. Use command line menu to select disk on $remote."
fi
fi
fi
# select the target BTRFS subvol
select_target_disk
# create and initialize structures for snapper configs
run_config
# run backups using btrfs-send -> btrfs-receive
run_backup
# finalize backup tasks
run_finalize
printf "${BLUE}Done!${NO_COLOR}\n"
exec 3>&-
# cleanup
run_cleanup
if [ $donotify -gt 0 ]; then
if [ "$uuid_cmdline" != "none" ]; then
notify_info "Finished" "Backups to $uuid_cmdline complete!"
else
notify_info "Finished" "Backups complete!"
fi
fi