Files
dsnap-sync/bin/dsnap-sync
2018-09-21 15:53:58 +02:00

2147 lines
75 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.7"
# 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=
answer=no
batch=0
color=0
donotify=0
dryrun=0
disk_count=-1
disk_uuid_match_count=0
disk_target_match_count=0
disk_subvolid_match_count=0
disk_uuid_match=''
interactive=1
selected_uuid='none'
selected_target='none'
selected_subvol='none'
snapper_sync_id=0
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=1; }
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:-0}
# 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 "${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
failed_id_first=$(eval snapper --config \"$selected_config\" list --type single \
| awk -F '|' ' /'"$snap_description_running"'/' \
| awk ' NR==1 {print $1} ')
failed_id_last=$(eval snapper --config \"$selected_config\" list --type single \
| awk -F '|' ' /'"$snap_description_running"'/' \
| awk ' END {print $1} ')
cmd="snapper --config \"$selected_config\" delete"
if [ $failed_id_first -lt $failed_id_last ]; then
$(eval $cmd $failed_id_first-$failed_id_last)
else
$(eval $cmd $failed_id_first)
fi
fi
fi
}
create_snapshot () {
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}create_snapshot()...${NO_COLOR}\n" $snapper_config
fi
# acting on source system
if [ $dryrun -eq 0 ]; then
#printf "Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Create new snapshot using snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
"$selected_config"
fi
if [ -z $remote ]; then
snapper_source_id=$(snapper --config "$selected_config" create \
--print-number \
--description "$snap_description_running" \
--userdata "host=$HOSTNAME")
else
snapper_source_id=$(snapper --config "$selected_config" create \
--print-number \
--description "$snap_description_running" \
--userdata "host=$remote")
fi
if [ $SUBVOLUME = "/" ]; then
SUBVOLUME=""
fi
snapper_source_snapshot=$SUBVOLUME/.snapshots/$snapper_source_id/snapshot
snapper_source_info=$SUBVOLUME/.snapshots/$snapper_source_id/info.xml
sync
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Snapper source snapshot ${GREEN}'%s'${MAGENTA} created${NO_COLOR}\n" "$snapper_source_id"
fi
else
printf "dryrun: Would create source snapshot with snapper config '%s' ...\n" "$selected_config"
snapper_source_sync_id="<snapper_source_id>"
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_backupdir () {
local backup_dir=$1
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}get_backupdir()...${NO_COLOR}\n" $snapper_config
fi
backupdir=$backup_dir
if [ ! $batch ]; then
answer=yes
if [ -z "$backupdir" ]; then
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
fi
fi
if [ $verbose -ge 2 ] && [ -n "$backupdir" ]; then
printf "${MAGENTA}Backup-Dir is ${GREEN}'%s'${NO_COLOR}\n" \
"$backupdir"
fi
}
get_snapper_config_value () {
local remote=${1##remote=}
local config_file=${2##snapper_config=}
local config_key=${3##config_key=}
local run_ssh=''
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}get_snapper_config_value()...${NO_COLOR}\n"
fi
[ ${#remote} -gt 0 ] && run_ssh=$ssh
value=$(eval $run_ssh cat $config_file \
| awk '/'"$config_key"'/' \
| awk -F "=" '{ gsub("\"",""); print $2}')
if [ $verbose -ge 3 ]; then
printf "Snapper ${GREEN}config file${NO_COLOR}: '%s'\n" \
"$config_file"
printf "Snapper key ${GREEN}'%s'${NO_COLOR}: '%s'\n" \
"$config_key" "$value"
fi
}
get_snapper_target_backupdir () {
local backupdir_cmdline=$1
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}get_snapper_target_backupdir()...${NO_COLOR}\n" $snapper_config
fi
if [ $snapper_target_sync_id -gt 0 ]; then
# get metadata for backupdir from sync snapshot
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 not found, get metadata for backupdir from finished snapshot
if [ -z "$backupdir" ]; then
backupdir=$(snapper --config "$selected_config" list --type single \
| awk -F "|" '/'"$snap_description_finished"'/' \
| awk -F "|" '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {print $5}' \
| awk -F "," '/backupdir/ {print $1}' \
| awk -F"=" 'END {print $2}')
fi
if [ "$backupdir_cmdline" != 'none' ] && [ "$backupdir_cmdline" != "$backupdir" ] \
&& [ "${#backupdir}" -gt 0 ] ; then
if [ $verbose -ge 2 ]; then
# WIP: we need to adapt existing target config SUBVOLUME to reflect the new backupdir
printf "${RED}TODO: ${MAGENTA}Reset backupdir ${GREEN}'%s'${MAGENTA} as requested per commandline.${NO_COLOR}\n" \
"$backupdir"
printf "${RED}TODO: ${NO_COLOR}Need to adapt ${GREEN}SUBVOLUME${NO_COLOR} for existing ${GREEN}target-config${NO_COLOR}, to reflect the new backupdir.\n"
fi
die "Changing the backupdir for an already existing target-config is not supported yet.\n"
elif [ "$backupdir_cmdline" != 'none' ] && [ -z "$backupdir" ] ; then
backupdir=$backupdir_cmdline
fi
fi
}
get_disk_infos () {
local disk_uuid
local disk_target
local fs_option
if [ $verbose -ge 3 ]; then
printf "${BLUE}Get target_disk_infos()...${NO_COLOR}\n"
fi
# wakeup automounter units
if [ ${#automount_path} -gt 0 ]; then
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Mount automounter unit ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
"$automount_path"
fi
if [ $remote ]; then
ret=$(systemctl --host=$remote status *.automount \
| awk -F ': ' '$0 ~ pattern { print $2 }' \
pattern="Where: $automount_path")
else
ret=$(systemctl status *.automount \
| awk -F ': ' '$0 ~ pattern { print $2 }' \
pattern="Where: $automount_path")
fi
if [ ${#ret} -gt 0 ]; then
ret=$($ssh stat --file-system --terse $ret)
fi
fi
# get mounted BTRFS infos
if [ "$($ssh findmnt --noheadings --nofsroot --mountpoint / --output FSTYPE)" = "btrfs" ]; then
# root filesystem is excluded as a valid target location
exclude_uuid=$($ssh findmnt --noheadings --nofsroot --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
# target location is not mounted as root
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
}
get_snapper_config_type () {
local snapper_config=$1
local snapper_config_tpye='none'
local key
local value
# Snapshot types: btrfs-snapshot, btrfs-clone, btrfs-archive
# snapshot: 1st stage: CHILD_CONFIG="false" or missing
# clone: 2nd stage: CHILD_CONFIG="true" PARENT_CONFIG="<snapper config name>"
# archive: 3nd stage: CHILD_CONFIG="true" PARENT_CONFIG="<child snapper config name>"
# parse selected_config and return with $snapper_target_config set appropriately
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}get_snapper_config_type()...${NO_COLOR}\n"
fi
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Get snapper configuration type for config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" $snapper_config
fi
# WIP -> create a PR for snapper
# 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
case $key in
CONFIG_TYPE)
snapper_config_type=$(eval echo $value | sed -e 's/\"\(.*\)\"/\1/')
continue
;;
CHILD_CONFIG)
snapper_target_config=$(eval echo $value | sed -e 's/\"\(.*\)\"/\1/')
continue
;;
PARENT_CONFIG)
snapper_parent_config=$(eval echo $value | sed -e 's/\"\(.*\)\"/\1/')
continue
;;
*)
# value is not relevant
continue
;;
esac
done < $SNAPPER_CONFIG_DIR/$snapper_config
case $snapper_config_type in
archive|Archive)
# archive btrfs-snapshot to non btrfs-filesystem
snapper_config_type=btrfs-archive
snapper_target_config=archive-$snapper_config
;;
child|Child)
# clone to btrfs-snapshot
snapper_config_type=btrfs-clone
snapper_target_config=clone-$snapper_config
;;
parent|Parent)
# disk2disk btrfs-snapshot
snapper_config_type=btrfs-snapshot
snapper_target_config=$snapper_config
;;
*)
# disk2disk btrfs-snapshot (default)
snapper_config_type=btrfs-snapshot
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 2 ]; then
printf "Snapper configuration type: '%s'\n" $snapper_config_type
printf "Snapper target configuration: '%s'\n" $snapper_target_config
printf "Snapper parent configuration: '%s'\n" $snapper_parent_config
fi
}
get_snapper_last_sync_id () {
local snapper_config=${1#snapper_config=}
local snapper_description=${2##snapper_description=}
local snapper_uuid=${3##snapper_uuid=}
local snapper_subvolid=${4##snapper_subvolid=}
local remote=${5##remote=}
local run_ssh=''
snapper_sync_id=0
[ ${#remote} -gt 0 ] && run_ssh=$ssh
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}get_snapper_last_sync_id()...${NO_COLOR}\n"
fi
# only process, if config does exist
cmd="$run_ssh stat --format %i $SNAPPER_CONFIG_DIR/$snapper_config 2>/dev/null"
if [ -z $(eval $cmd) ]; then
if [ $verbose -ge 3 ]; then
if [ $remote ]; then
printf "${MAGENTA}snapper config ${GREEN}'%s'${MAGENTA} on remote ${GREEN}'%s'${MAGENTA} does not exist yet.${NO_COLOR}\n" \
$snapper_config $remote
else
printf "${MAGENTA}snapper config ${GREEN}'%s'${MAGENTA} does not exist yes.${NO_COLOR}\n" \
$snapper_config
fi
fi
return 1
fi
if [ $verbose -ge 3 ]; then
if [ $remote ]; then
printf "${MAGENTA}Get last sync-ID for snapper config ${GREEN}'%s'${MAGENTA} on remote ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
$snapper_config $remote
else
printf "${MAGENTA}Get last sync-ID for snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
$snapper_config
fi
fi
if [ -z $snapper_uuid ] && [ -z $snapper_subvolid ]; then
snapper_sync_id=$(eval $run_ssh snapper --config "$snapper_config" list --type single | \
awk '/'"$snapper_description"'/' | \
awk 'END {print $1}' )
else
snapper_sync_id=$(eval $run_ssh snapper --config "$snapper_config" list --type single | \
awk '/'"$snapper_description"'/' | \
awk '/subvolid='"$snapper_subvolid"'/, /uuid='"$snapper_uuid"'/' | \
awk 'END {print $1}')
fi
if [ ${#snapper_sync_id} -ge 1 ]; then
# ok, matching snapshot found
snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot
else
# no snapshot found, try for latest finished sync
if [ -z $snapper_uuid ] && [ -z $snapper_subvolid ]; then
snapper_sync_id=$(eval $run_ssh snapper --config "$snapper_config" list --type single | \
awk '/'"$snap_description_finished"'/' | \
awk 'END {print $1} ')
else
snapper_sync_id=$(eval $run_ssh snapper --config "$snapper_config" list --type single | \
awk '/'"$snap_description_finished"'/' | \
awk '/subvolid='"$snapper_subvolid"'/, /uuid='"$snapper_uuid"'/' | \
awk 'END {print $1}')
fi
if [ ${#snapper_sync_id} -ge 1 ]; then
# ok, matching snapshot found
snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot
else
# no snapshot available
snapper_sync_id=0
fi
fi
}
get_snapper_sync_id () {
local snapper_config=${1#snapper_config=}
local snapper_sync_id=${2##snapper_sync_id=}
local remote=${3##remote=}
local run_ssh=''
local ret=
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}get_snapper_sync_id()...${NO_COLOR}\n"
fi
if [ $verbose -ge 3 ]; then
if [ $remote ]; then
printf "${MAGENTA}Get sync-ID ${GREEN}'%s'${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} on remote ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
$snapper_sync_id $snapper_config $remote
else
printf "${MAGENTA}Get sync-ID ${GREEN}'%s'${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
$snapper_sync_id $snapper_config
fi
fi
[ ${#remote} -gt 0 ] && run_ssh=$ssh
cmd="snapper --config "$snapper_config" list --type single \
| awk -F '|' '\$1 == "$snapper_sync_id" {print \$1}' "
ret=$(eval $run_ssh snapper --config "$snapper_config" list --type single \
| awk -F '|' '$1 == '"$snapper_sync_id"' {gsub(" ",""); print $1}')
printf "ret: '%s'\n" $ret
if [ ${#ret} -ge 1 ]; then
# ok, matching snapshot found
snapper_sync_id=$ret
if [ $verbose -ge 3 ]; then
printf "Got source snapshot: ${GREEN}'%s'${NO_COLOR} (id: ${GREEN}'%s'${NO_COLOR})\n" \
$snapper_config $snapper_sync_id
fi
return 0
else
# no snapshot found
return 1
fi
}
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
;;
-a|--automount)
automount_path="$2"
shift 2
;;
-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
interactive=0
do_pv_cmd=0
donotify=0
shift
;;
--color)
color=1
shift 1
;;
--nonotify)
donotify=0
shift 1
;;
--nopv)
do_pv_cmd=0
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
;;
--automount=*)
automount_path=${1#*=}
shift
;;
--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
;;
--v=* | --verbose=*)
verbose=${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 2 ]; 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 2 ]; then snap_sync_options="verbose_level=$verbose"; fi
if [ $dryrun -eq 1 ]; then snap_sync_options="${snap_sync_options} dry-run=true"; fi
if [ $donotify -eq 1 ]; then snap_sync_options="${snap_sync_options} donotify=true"; fi
if [ $color -eq 1 ]; then snap_sync_options="${snap_sync_options} color=true"; fi
if [ $batch -eq 1 ]; then
snap_sync_options="${snap_sync_options} batch=true do_pv_cmd=$do_pv_cmd"
else
snap_sync_options="${snap_sync_options} interactive=true do_pv_cmd=$do_pv_cmd"
fi
#if [ "$interactive" -eq 1 ]; then snap_sync_options="${snap_sync_options} interactive=true"; fi
printf "Options: '%s'\n\n" "${snap_sync_options}"
fi
}
quote_args () {
# quote command in ssh call to prevent remote side from expanding any arguments
# using dash's buildin printf
args=
if [ $# -gt 0 ]; then
# no need to make COMMAND an array - ssh will merge it anyway
COMMAND=
while [ $# -gt 0 ]; do
arg=$(printf "%s" "$1")
COMMAND="${COMMAND}${arg} "
shift
done
args="${args}${COMMAND}"
fi
}
run_config_preparation () {
if [ $verbose -ge 1 ]; then
printf "${BLUE}Prepare configuration structures...${NO_COLOR}\n"
fi
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 on existing configurations
verify_snapper_config $selected_config
# cleanup failed former runs
check_snapper_failed_ids $batch
if [ $SNAP_SYNC_EXCLUDE = "yes" ]; then
continue
fi
# parse selected_config and set $snapper_target_config appropriately
get_snapper_config_type $selected_config
# parse backupdir
get_backupdir $backupdir_cmdline
# get latest successfully finished snapshot
# verify source
# WIP: metadata from last snapshot!
case $snapper_config_type in
btrfs-snapshot)
get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \
"snapper_uuid=${selected_uuid}" "snapper_subvolid=${selected_subvol}" "remote="
;;
btrfs-clone)
if [ $verbose -ge 3 ]; then
printf "${RED}TODO:${NO_COLOR} config_type '%s'?\n" "$snapper_config_type"
fi
get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \
"snapper_uuid=" "snapper_subvolid=" "remote="
;;
*)
if [ $verbose -ge 3 ]; then
printf "${RED}TODO:${NO_COLOR} what is needed for config_type '%s'?\n" "$snapper_config_type"
fi
;;
esac
snapper_source_sync_id=$snapper_sync_id
if [ $snapper_sync_id -eq 0 ]; then
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}No previous snapshot available for snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
"$selected_config"
fi
snapper_target_sync_id=0
snapper_source_sync_snapshot='none'
snapper_target_sync_snapshot='none'
backup_root=$selected_target/$backupdir/$snapper_target_config
else
# Set snapshot-path for source
get_snapper_config_value "remote=" \
"snapper_config=$SNAPPER_CONFIG_DIR/$selected_config" \
"config_key=SUBVOLUME"
if [ $? -eq 0 ]; then
snapper_source_sync_snapshot=$value/.snapshots/$snapper_source_sync_id/snapshot
fi
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Last synced ${GREEN}source snapshot${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} is ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \
"$selected_config" "$snapper_sync_id"
fi
# verfiy target
get_snapper_last_sync_id "snapper_config=${snapper_target_config}" \
"snapper_description=${snap_description_synced}" \
"snapper_uuid=" "snapper_subvolid=" "remote=${remote}"
snapper_target_sync_id=$snapper_sync_id
if [ $snapper_target_sync_id -eq 0 ]; then
if [ $verbose -ge 2 ]; then
if [ $remote ]; then
printf "${MAGENTA}No target shapshot available for snapper config ${GREEN}'%s'${MAGENTA} on '%s'...${NO_COLOR}\n" \
"$selected_config" "$remote"
else
printf "${MAGENTA}No target shapshot available for snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
"$selected_config"
fi
fi
backup_root=$selected_target/$backupdir/$snapper_target_config
#snapper_target_sync_id=$snapper_source_sync_id
else
# get backupdir from snapper target
get_snapper_target_backupdir $backupdir
# set target sync_snapshot path
get_snapper_config_value "remote=$remote" \
"snapper_config=$SNAPPER_CONFIG_DIR/$snapper_target_config" \
"config_key=SUBVOLUME"
if [ $? -eq 0 ]; then
snapper_target_sync_snapshot=$value/.snapshots/$snapper_target_sync_id/snapshot
backup_root=$value
fi
if [ $verbose -ge 2 ]; then
if [ $remote ]; then
printf "${MAGENTA}Last synced ${GREEN}target snapshot${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} on remote '%s' is ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_target_config" "$remote" "$snapper_target_sync_id"
else
printf "${MAGENTA}Last synced ${GREEN}target snapshot${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} is ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_target_config" "$snapper_target_sync_id"
fi
fi
fi
fi
# save values in config specific pseudo arrays
eval "snapper_source_config_$i='$selected_config'"
eval "snapper_target_config_$i='$snapper_target_config'"
eval "snapper_config_type_$i='$snapper_config_type'"
eval "snapper_source_sync_id_$i='$snapper_source_sync_id'"
eval "snapper_source_sync_snapshot_$i='$snapper_source_sync_snapshot'"
eval "snapper_target_sync_id_$i='$snapper_target_sync_id'"
eval "snapper_target_sync_snapshot_$i='$snapper_target_sync_snapshot'"
eval "backupdir_$i='$backupdir'"
eval "backup_root_$i='$backup_root'"
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_sync_id
fi
fi
i=$(($i+1))
done
}
run_backup () {
if [ $verbose -ge 1 ]; then
printf "${BLUE}Performing backups...${NO_COLOR}\n"
fi
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
if [ $donotify -gt 0 ]; then
notify_info "Backup in progress" "Backing up data for configuration '$selected_config'."
fi
# retrieve config specific infos from pseudo Arrays
snapper_source_config=$(eval echo \$snapper_source_config_$i)
snapper_target_config=$(eval echo \$snapper_target_config_$i)
snapper_config_type=$(eval echo \$snapper_config_type_$i)
snapper_source_sync_id=$(eval echo \$snapper_source_sync_id_$i)
snapper_source_sync_snapshot=$(eval echo \$snapper_source_sync_snapshot_$i)
snapper_target_sync_id=$(eval echo \$snapper_target_sync_id_$i)
backup_dir=$(eval echo \$backupdir_$i)
backup_root=$(eval echo \$backup_root_$i)
case $snapper_config_type in
btrfs-snapshot)
create_snapshot
;;
btrfs-clone)
if [ $verbose -ge 3 ]; then
printf "${RED}WIP:${NO_COLOR} config_type '%s'\n" "$snapper_config_type"
fi
# check for last common snapshot
snapper_source_id=$snapper_source_sync_id
snapper_source_snapshot=$SUBVOLUME/.snapshots/$snapper_source_sync_id/snapshot
snapper_source_info=$SUBVOLUME/.snapshots/$snapper_source_sync_id/info.xml
;;
*)
if [ $verbose -ge 3 ]; then
printf "${RED}WIP:${NO_COLOR} what is needed for config_type '%s'\n" "$snapper_config_type"
fi
;;
esac
# 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_id=$snapper_source_id
snapper_target_snapshot=$backup_root/.snapshots/$snapper_target_id
# create the needed snapper structure on the target
case $snapper_config_type in
btrfs-archive)
# archive btrfs-snapshot to non btrfs-filesystem on target (e.g tape, ext4)
verify_archive_structure "backup_root=$backup_root" "snapper_target_config=$snapper_target_config" "snapper_target_sync_id=$snapper_target_id" "remote=$remote"
;;
btrfs-clone|btrfs-snapshot)
# create btrfs-snapshot on target
verify_snapper_structure "backup_root=$backup_root" "snapper_target_config=$snapper_target_config" "snapper_target_id=$snapper_target_id" "remote=$remote"
;;
esac
# TODO: to report correct values btrfs-quota must be activated for the source subvol!
snapper_source_snapshot_size=$(du --sum $snapper_source_snapshot 2>/dev/null | awk -F ' ' '{print $1}')
snapper_source_snapshot_size=$(($snapper_source_snapshot_size / 1024 / 1024))G
# settings for interactive progress status
if [ $do_pv_cmd -eq 1 ]; then
pv_options="--delay-start 2 --interval 5 --format \"time elapsed [%t] | rate %r | total size [%b] | %p | time remaining [%e]\" "
cmd_pv="pv --size $snapper_source_snapshot_size $pv_options |"
#cmd_pv="pv $pv_options --size ${snapper_source_snapshot_size} | dialog --gauge \"$progname: Progress for config '$selected_config'\" 6 85 |"
else
cmd_pv=''
fi
cmd="btrfs send $verbose_flag $snapper_source_snapshot 2>$BTRFS_PIPE | $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>$BTRFS_PIPE"
if [ "$dryrun" -eq 0 ]; then
if [ "$snapper_source_sync_id" -eq 0 ] || [ "$snapper_target_sync_id" -eq 0 ]; then
# target never received any snapshot before
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Sending ${GREEN}first snapshot${NO_COLOR} for snapper config ${GREEN}'%s' ${MAGENTA}(id='%s', size='%s')${NO_COLOR} ...\n" \
"$selected_config" "$snapper_source_id" "$snapper_source_snapshot_size"
fi
# the actual data sync to the target
# this may take a while, depending on datasize and line-speed
if [ $verbose -ge 3 ]; then
printf "cmd: '%s'\n" "$cmd"
fi
$(eval $cmd) 1>/dev/null
if [ "$?" -gt 0 ]; then
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}\n" "$(eval cat $BTRFS_PIPE)"
die "btrfs pipe error."
fi
else
# source holds synced snapshots
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Sending ${GREEN}incremental snapshot${NO_COLOR} (id: ${GREEN}'%s'${NO_COLOR}) for snapper config ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_target_id" "$selected_config"
fi
if [ $verbose -ge 3 ]; then
printf "Last synced ${GREEN}source${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \
"$snapper_source_sync_id" "$snapper_source_sync_snapshot"
printf "New ${GREEN}source${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \
"$snapper_source_id" "$snapper_source_snapshot"
printf "Last synced ${GREEN}target${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \
"$snapper_target_sync_id" "$snapper_target_sync_snapshot"
printf "New ${GREEN}target${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \
"$snapper_target_id" "$snapper_target_snapshot/snapshot"
fi
# verify that source snapshot-id corresponds to a matching target snapshot-id
if [ $snapper_target_sync_id -ne $snapper_source_sync_id ]; then
#if [ $snapper_target_sync_id -lt $snapper_source_sync_id ]; then
if [ $snapper_target_sync_id -lt $snapper_target_id ]; then
# select commen sync id
ret=$(eval get_snapper_sync_id "snapper_config=${snapper_source_config}" "snapper_sync_id=$snapper_target_sync_id" "remote=")
if [ $ret ]; then
snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot
else
printf "no commen sync id.\n"
fi
fi
fi
cmd="$ssh stat --format %i $backup_root/$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_source_sync_snapshot $snapper_source_snapshot 2>$BTRFS_PIPE \
| $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>$BTRFS_PIPE"
if [ $verbose -ge 3 ]; then
printf "${GREEN}btrfs send${NO_COLOR} is using snapshot ${GREEN}'%s'${NO_COLOR} from ${GREEN}target${NO_COLOR} to sync metadata for new snapshot ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_source_sync_snapshot" "$snapper_source_snapshot"
printf "${GREEN}btrfs command:${NO_COLOR} '%s'\n" "$cmd"
fi
eval $cmd 1>/dev/null
if [ "$?" -gt 0 ]; then
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}\n" "$(eval cat $BTRFS_PIPE)"
die "btrfs pipe error."
fi
else
# need to use source snapshot to provide metadata for target
cmd="btrfs send $verbose_flag -p $snapper_source_sync_snapshot $snapper_source_snapshot 2>$BTRFS_PIPE \
| $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>BTRFS_PIPE"
if [ $verbose -ge 3 ]; then
printf "${GREEN}btrfs send${NO_COLOR} is using snapshot ${GREEN}'%s'${NO_COLOR} from ${GREEN}source${NO_COLOR} to sync metadata ...\n" \
"$snapper_source_sync_snapshot"
printf "${GREEN}btrfs command:${NO_COLOR} '%s'\n" "$cmd"
fi
eval $cmd 1>/dev/null
if [ "$?" -gt 0 ]; then
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}\n" "$(eval cat $BTRFS_PIPE)"
die "btrfs pipe error."
fi
fi
fi
else
printf "dryrun: Would run btrfs send / btrfs receive pipe\n"
#printf "dryrun: '%s'\n" "$cmd"
fi
# finally: send the snapper info metadata
if [ $dryrun -eq 0 ]; then
if [ -z "$remote" ]; then
cmd="cp $snapper_source_info $snapper_target_snapshot"
cp "$snapper_source_info" "$snapper_target_snapshot"
else
if [ -n "$port" ]; then
rsync -avzq -e "ssh -p $port" "$snapper_source_info" "$remote:$snapper_target_snapshot"
else
rsync -avzq "$snapper_source_info" "$remote":"$snapper_target_snapshot"
fi
fi
else
printf "dryrun: Would copy info metadate '%s' to target.\n" \
"$snapper_source_info"
fi
# Save config specific values in pseudo Array
eval "snapper_source_id_$i='$snapper_source_id'"
eval "snapper_source_snapshot_$i='$snapper_source_snapshot'"
eval "snapper_source_info_$i='$snapper_source_info'"
eval "snapper_target_id_$i='$snapper_source_id'"
eval "snapper_target_snapshot_$i='$snapper_target_snapshot'"
done
}
run_cleanup () {
local batch=${1:-$false}
if [ $verbose -ge 1 ]; then
printf "${BLUE}Performing cleanup ...${NO_COLOR}\n"
fi
if [ $dryrun -eq 0 ]; then
# cleanup failed runs
check_snapper_failed_ids "$batch"
# cleanup target
#$ssh btrfs subvolume delete $backup_root/$snapper_snapshots/$snapper_target_sync_id/snapshot
#$ssh rm -rf $backup_root/$snapper_snapshots/$snapper_target_sync_id
# cleanup TEMPDIR
if [ -d $TMPDIR_PIPE ]; then
rm -rf $TMPDIR_PIPE || die "Failed to cleanup temporary directory '%s'\n" "$TMPDIR_PIPE"
fi
else
printf "dryrun: Would cleanup TEMPDIR and failed snashot IDs ...\n"
fi
}
run_finalize () {
# Actual backing up
if [ $verbose -ge 1 ]; then
printf "${BLUE}Finalize backups...${NO_COLOR}\n"
fi
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_source_config=$(eval echo \$snapper_source_config_$i)
backupdir=$(eval echo \$backupdir_$i)
backup_root=$(eval echo \$backup_root_$i)
snapper_config_type=$(eval echo \$snapper_config_type_$i)
snapper_source_sync_id=$(eval echo \$snapper_source_sync_id_$i)
snapper_target_sync_id=$(eval echo \$snapper_target_sync_id_$i)
snapper_source_sync_snapshot=$(eval echo \$snapper_source_sync_snapshot_$i)
snapper_source_id=$(eval echo \$snapper_source_id_$i)
snapper_source_snapshot=$(eval echo \$snapper_source_snapshot_$i)
snapper_source_info=$(eval echo \$snapper_source_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 --target $SUBVOLUME | sed -e 's/.*subvolid=\([0-9]*\).*/\1/')
# Tag new snapshots key/value parameter
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Tagging target ...${NO_COLOR}\n"
fi
if [ "$dryrun" -eq 0 ]; then
# target snapshot
# 1) wait for target snapshot to show up in snapper list
# find "$snapper_target_sync_id" -> marked as "$snap_descrition_running"
# 2) toggle metadata -> mark as "$snap_description_finished", reference "$target_userdata" (host, uuid, subvolid)
# snapper orders userdata pairs lexical ascending
# !!! ugly hack !!!:
# Problem: how to trigger that database is synced? -> a feature request is send to snapper upstream source
# Solution1: 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)
# for snapper to list target snapshot in database.
local ii=1
local ii_max=20
local ii_sleep=15
# Solution2: kill running snapperd
# -> will restart and sync; any unseen interdependencies?
$(eval $ssh killall -SIGTERM snapperd)
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Identify snapper id ${GREEN}'%s'${MAGENTA} on target for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_target_id" "$snapper_target_config"
fi
# construct snapper match command
case $snapper_config_type in
btrfs-archive)
# archive btrfs-snapshot to non btrfs-filesystem on target (e.g tape, ext4)
return 0
;;
btrfs-clone)
# no tagging needed
return 0
;;
btrfs-snapshot)
# create btrfs-snapshot on target
cmd="$ssh snapper --verbose --config \"$snapper_target_config\" list --type single \
| awk ' /'\"$snap_description_running\"'/ ' \
| awk -F '|' ' \$1 == $snapper_target_sync_id {print \$1} ' "
;;
esac
while [ "$ii" -le "$ii_max" ]; do
if [ $verbose -ge 3 ]; then
printf "calling: '%s'\n" "$(eval $cmd)"
fi
ret=$(eval $cmd)
#ret=$ssh snapper --verbose --config \"$snapper_target_config\" list --type single \
# | awk ' /'\"$snap_description_running\"'/ ' \
# | awk -F '|' ' $1 == "$snapper_target_sync_id" {print $1} '
#printf "return: '%s'\n" "$?"
if [ $? -eq 0 ]; then
if [ $ret -eq $snapper_target_id ]; then
# got snapshot as $snapper_target_id
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Found${NO_COLOR} snapper id ${GREEN}'%s'${NO_COLOR} on target for configuration ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_target_id" "$snapper_target_config"
fi
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Tagging metadata${NO_COLOR} for snapper id ${GREEN}'%s'${NO_COLOR} on target for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_target_id" "$snapper_target_config"
#printf "calling: '%s'\n" "$(eval $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_target_id\')
else
ret=$(snapper --config $snapper_target_config modify \
--description "$snap_description_finished" \
--userdata "host=$src_host, subvolid=$src_subvolid, uuid=$src_uuid" \
--cleanup-algorithm "timeline" \
$snapper_target_id)
fi
if [ $verbose -ge 3 ]; then
printf "return: '%s'\n" "$ret"
fi
break
fi
fi
if [ $verbose -ge 3 ]; 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 2 ]; then
printf "${MAGENTA}Tagging source ...${NO_COLOR}\n"
fi
if [ $snapper_source_id -gt 0 ]; then
cmd="snapper --config $selected_config modify \
--description \"$snap_description_synced\" \
--userdata \"backupdir=$backupdir, host=$remote, subvolid=$selected_subvol, uuid=$selected_uuid\" \
--cleanup-algorithm \"timeline\" \
$snapper_source_id"
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Tagging snapper metadata${NO_COLOR} for snapper id ${GREEN}'%s'${NO_COLOR} on source for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_source_id" "$selected_config"
printf "calling: '%s'\n" "$(eval $cmd)"
fi
ret=$(eval "$cmd")
if [ $verbose -ge 3 ]; then
printf "return: '%s'\n" "$?"
fi
sync
fi
if [ ${#snapper_source_sync_id} -gt 0 ]; then
cmd="snapper --config $selected_config modify \
--description \"$snap_description_finished\" \
$snapper_source_sync_id"
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Tagging snapper metadata${NO_COLOR} for snapper sync id ${GREEN}'%s'${NO_COLOR} on source for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_source_sync_id" "$selected_config"
printf "calling: '%s'\n" "$(eval $cmd)"
fi
ret=$(eval "$cmd")
snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_source_sync_id/snapshot
else
snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_source_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_sync_id"
printf "dryrun: %s\n" "$cmd"
cmd="snapper --config $selected_config modify --description $snap_description_synced --userdata $userdata $snapper_sync_id"
printf "dryrun: %s\n" "$cmd"
cmd="snapper --config $selected_config modify --description $snap_description_finished $snapper_source_sync_id"
printf "dryrun: %s\n" "$cmd"
fi
if [ $verbose -ge 1 ]; then
printf "Backup complete for snapper configuration '%s'.\n" "$selected_config" > $PIPE
fi
done
}
select_target_disk () {
local i=0
local disk_id=0
#local disk_selected_ids=''
local disk_selected_count=0
local subvolid=''
local subvol=''
if [ $verbose -ge 1 ]; then
printf "${BLUE}Select target disk...${NO_COLOR}\n"
fi
# print selection table
if [ $verbose -ge 2 ]; 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 2 ]; 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 [ $verbose -ge 2 ]; then
printf "${MAGENTA}You selected the disk with UUID ${GREEN}'%s'${MAGENTA} (subvolid=${GREEN}'%s'${MAGENTA})${NO_COLOR}.\n" \
"$selected_uuid" "$selected_subvol"
if [ -z "$remote" ]; then
printf "${MAGENTA}Target disk is mounted at ${GREEN}'%s'${MAGENTA}.${NO_COLOR}\n" \
"$selected_target"
else
printf "${MAGENTA}Target disk is mounted on host ${GREEN}'%s'${MAGENTA} at ${GREEN}'%s'${MAGENTA}.${NO_COLOR}\n" \
"$remote" "$selected_target"
fi
fi
if [ $donotify -gt 0 ]; then
if [ "$target_cmdline" != "none" ]; then
if [ -z "$remote" ]; then
notify_info "Target selected" "Using target '$target_cmdline'..."
else
notify_info "Target selected" "Using target '$target_cmdline' at '$remote'..."
fi
elif [ "$uuid_cmdline" != "none" ]; then
if [ -z "$remote" ]; then
notify_info "Target selected" "Using targets uuid '$uuid_cmdline'..."
else
notify_info "Target selected" "Using targets uuid '$uuid_cmdline' at '$remote'..."
fi
else
if [ -z "$remote" ]; then
notify_info "Target selected" "Use command line menu to select target disk..."
else
notify_info "Target selected" "Use command line menu to select target disk on $remote..."
fi
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
exit 0
}
usage () {
cat <<EOF
$progname $version
Usage: $progname [options]
Options:
-a, --automount <path> start automount for given path to get a valid target mountpoint.
-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 verbose on what's going on (min: --verbose=1, max: --verbose=3)
--version show program version
EOF
exit 0
}
verify_archive_structure () {
local backup_root=${1##backup_root=}
local snapper_config=${2##snapper_target_config=}
local snapper_id=${3##snapper_target_sync_id=}
local remote_host=${4##remote=}
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}verify-archive_structure()...\n"
fi
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Verify archive 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" -eq 0 ]; then
if [ $verbose -ge 2 ]; 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 -eq 0 ]; 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
# WIP: this is the idea right now ...
# archive type: full or incremental
# full snyc: regular btrfs send
# incremental: save a btrfs stream. Stream will depend on parent
# restore process:
# 1) copy in last full to btrfs filesystem
# 2) loop though ordered incremental path: btrfs recieve the stream
# verify that target can take the new archive for given snapshot id
if [ "$dryrun" -eq 0 ]; then
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Verify existence of path ${GREEN}'%s'${NO_COLOR}\n" \
"$backup_root/$snapper_config/$snapper_id"
fi
cmd="$ssh stat --format %i $backup_root/$snapper_config/$snapper_id 2>/dev/null"
if [ -z "$(eval $cmd)" ]; then
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Create path %s${NO_COLOR} to store target snapshot.\n" \
"$backup_root/$snapper_config/$snapper_id"
fi
$(eval $ssh mkdir --mode=0700 \
$backup_root/$snapper_config/$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_id"
die "Can't create snapshot dir on target."
fi
else
if [ -z "$remote" ]; then
printf "${RED}Cancel archive creation${NO_COLOR}: Former archive with id ${GREEN}'%s'${NO_COLOR} already exist in ${BLUE}'%s'${NO_COLOR}\n" \
"$snapper_id" "$backup_root/$snapper_config"
else
printf "${RED}Cancel archive creation${NO_COLOR}: Former archive with id ${GREEN}'%s'${NO_COLOR} already exist on ${BLUE}'%s' in '%s'${NO_COLOR}\n" \
"$snapper_id" "$remote" "$backup_root/$snapper_config"
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 archive on target ...\n" \
"$backup_root/$snapper_config/$snapper_id"
fi
}
verify_backupdir () {
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}verify_backupdir()...${NO_COLOR}\n"
fi
# verify backupdir
if [ "$snapper_target_sync_id" -eq 0 ]; then
# first backup run
snapper_target_sync_id=0
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
fi
}
verify_snapper_config () {
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}verify_snapper_config()...${NO_COLOR}\n"
fi
if [ ! -f "/etc/snapper/configs/$selected_config" ]; then
if [ $verbose -ge 2 ]; 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 "${RED}Error: ${GREEN}%s${NO_COLOR} entries are ${RED}marked as ${GREEN}'%s'${NO_COLOR} for snapper config ${RED}'%s'${NO_COLOR}\n" \
"$count" "$snap_description_synced" "$selected_config"
printf "Pointing to target with UUID ${GREEN}'%s'${NO_COLOR} and SUBVOLID ${GREEN}'%s'${NO_COLOR}. Skipping configuration ${GREEN}'%s'${NO_COLOR}.\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
}
verify_snapper_structure () {
local backup_root=${1##backup_root=}
local snapper_config=${2##snapper_target_config=}
local snapper_id=${3##snapper_target_id=}
local remote_host=${4##remote=}
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}verify-snapper_structure()...${NO_COLOR}\n"
fi
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Verify snapper filesystem structure${NO_COLOR} on target ${GREEN}'%s'${NO_COLOR}...\n" \
"$remote"
fi
# if not accessible, create backup-path
cmd="$ssh stat --format %i $backup_root 2>/dev/null"
ret=$(eval $cmd)
if [ $? -eq 1 ]; then
if [ "$dryrun" -eq 0 ]; then
if [ $verbose -ge 3 ]; then
if [ -z $remote_host ]; then
printf "${MAGENTA}Create${NO_COLOR} new backup-path ${GREEN}'%s'${NO_COLOR}...\n" \
"$backup_root"
else
printf "${MAGENTA}Create${NO_COLOR} new backup-path ${GREEN}'%s'${NO_COLOR} on ${MAGENTA}remote host ${GREEN}'%s'${NO_COLOR=} ...\n" \
"$backup_root" "$remote_host"
fi
fi
# strip last dir from backup_root
base_path=${backup_root%/*}; echo $base_path
if [ $dryrun -eq 0 ]; then
$(eval $ssh mkdir --mode=0700 --parents $base_path)
if [ $verbose -ge 3 ]; then
if [ -z $remote_host ]; then
printf "${MAGENTA}Create${NO_COLOR} new backup-path ${GREEN}'%s'${NO_COLOR}...\n" \
"$backup_root"
else
printf "${MAGENTA}Create${NO_COLOR} new backup-path ${GREEN}'%s'${NO_COLOR} on ${MAGENTA}remote host ${GREEN}'%s'${NO_COLOR=} ...\n" \
"$backup_root" "$remote_host"
fi
fi
else
if [ -z $remote_host ]; then
printf "dryrun: Would create backup-path %s ...\n" \
"$base_path"
else
printf "dryrun: Would create backup-path %s on remote host %s ...\n" \
"$remote_host" "$base_path"
fi
fi
fi
fi
# verify that we have a snapper compatible structure for target config (a btrfs subvolume)
cmd="$ssh stat --format %i $backup_root 2>/dev/null"
ret=$(eval $cmd)
if [ $? -eq 1 ]; then
# no inode for given backup_root
if [ $dryrun -eq 0 ]; then
if [ $verbose -ge 2 ]; then
if [ -z "$remote" ]; then
printf "${MAGENTA}Create${NO_COLOR} new snapper capable BTRFS ${MAGENTA}subvolume ${GREEN}'%s'${NO_COLOR} ...\n" \
"$backup_root"
else
printf "${MAGENTA}Create${NO_COLOR} new snapper capable BTRFS ${MAGENTA}subvolume ${GREEN}'%s'${NO_COLOR} on ${MAGENTA}remote host ${GREEN}'%s'${NO_COLOR} ...\n" \
"$backup_root" "$remote_host"
fi
fi
# 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 1>/dev/null"
$(eval $cmd) || die "Creation of BTRFS subvolume (backup-root) %s:%s failed.\n" \
"$remote_host" "$backup_root"
cmd="$ssh chmod 0700 $backup_root"
$(eval $cmd) || die "Changing the directory mode for %s on %s failed.\n" \
"$backup_root" "$remote_host"
# create the non existing remote BTRFS subvolume for given snapshot
#cmd="$ssh btrfs subvolume create $backup_root/$snapper_snapshot 1>/dev/null"
#$(eval $cmd) || \
# die "Creation of BTRFS subvolume (snapshot): %s:%s failed.\n" \
# "$remote_host" "$backup_root" "$remote_host"
# cmd="$ssh chmod 0700 $backup_root 1>/dev/null"
# $(eval $cmd) || \
# die "Changing the directory mode for '$backup_root' on '$remote_host'."
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_snapshot"
fi
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"
fi
if [ $verbose -ge 3 ]; 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 $(. $SNAPPER_CONFIG_DIR/$snapper_config)
#get_snapper_config "$SNAPPER_CONFIG_DIR/$snapper_config" "SUBVOLUME"
#if $ssh [ "$SUBVOLUME" != \"$backup_root\" ]; then
# SUBVOLUME="$backup_root"
# set_config "$SNAPPER_CONFIG_DIR/$snapper_config" "SUBVOLUME" "$SUBVOLUME"
#fi
fi
# verify that we have a valid snapper config
if [ $dryrun -eq 0 ]; then
cmd="$ssh stat --format %i $SNAPPER_CONFIG_DIR/$snapper_config 2>/dev/null"
ret=$(eval $cmd)
if [ $? -eq 1 ]; then
# path does not exist, let snapper create the structure
# and path $backup_root/.snapshots
cmd="$ssh snapper --config $snapper_config create-config \
--template $snapper_subvolume_template \
--fstype btrfs $backup_root"
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Create new BTRFS subvolume ${GREEN}'%s'${NO_COLOR} using template ${GREEN}'%s'${NO_COLOR}\n" \
$snapper_config $snapper_subvolume_template
fi
$(eval $cmd)
if [ $? -ne 0 ]; then
if [ $remote ]; then
die "Creation of snapper capable config %s on %s failed.\n" \
"$backup_root" "$remote_host"
else
die "Creation of snapper capable config %s failed.\n" \
"$backup_root"
fi
fi
else
# WIP:
# snapper_config exist, now verify if SUBVOLUME needs to be updated
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}'"
if [ -n $(eval $cmd) ]; then
# if changed, adapt targets SUBVOLUME path
if [ $verbose -ge 3 ]; then
printf "${RED}TODO:${NO_COLOR} Check if value for key 'SUBVOLUME' needs an update in snapper config %s\n" \
"$snapper_config"
fi
# WIP: Update SUBVOLUME, if backupdir has changed
#get_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME"
#if $ssh [ "$SUBVOLUME" != \"$backup_root\" ]; then
# SUBVOLUME="$backup_root"
# set_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" "$SUBVOLUME"
# cmd="btrfs subvolume create $backup_root/$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_snapshots 2>/dev/null"
ret=$(eval $cmd)
if [ -z $ret ]; then
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Create new BTRFS subvolume ${GREEN}'%s'${NO_COLOR}\n" \
$backup_root/$snapper_snapshots
fi
cmd="$ssh btrfs subvolume create $backup_root/$snapper_snapshots 2>/dev/null"
ret=$(eval $cmd)
if [ $? -ne 0 ] && [ $ret -ne 256 ]; then
die "Creation of snapper subvolume %s failed.\n" \
"$backup_root/$snapper_snapshots"
fi
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"
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 -eq 0 ]; then
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Verify existence of path ${GREEN}'%s'${NO_COLOR}...\n" \
"$backup_root/$snapper_snapshots/$snapper_id"
fi
cmd="$ssh stat --format %i $backup_root/$snapper_snapshots/$snapper_id 2>/dev/null"
ret=$(eval $cmd)
if [ $? -eq 1 ]; then
# Path does not exist
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Create${NO_COLOR} path ${GREEN}'%s'${NO_COLOR} to store target snapshot.\n" \
"$backup_root/$snapper_snapshots/$snapper_id"
fi
$(eval $ssh mkdir --mode=0700 \
$backup_root/$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_snapshots/$snapper_id"
die "Can't create snapshot dir on target."
fi
else
cmd="$ssh stat --format %i $backup_root/$snapper_snapshots/$snapper_id/snapshot 2>/dev/null"
ret=$(eval $cmd)
if [ $? -eq 0 ] && [ $ret -eq 256 ]; then
# a former btrfs snapshot already exists
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_snapshots"
else
printf "${RED}Cancel snapshot creation${NO_COLOR}: Former snapshot with id ${GREEN}'%s'${NO_COLOR} already exists on ${BLUE}'%s'${NO_COLOR} in ${BLUE}'%s'${NO_COLOR}\n" \
"$snapper_id" "$remote" "$backup_root/$snapper_snapshots"
fi
# cleanup generated snapper entry
check_snapper_failed_ids $batch
die "Can't backup to existing snapshot-id ($snapper_id)!"
fi
fi
else
printf "dryrun: Would check/create path %s to store target snapshot ...\n" \
"$backup_root/$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
# select the target BTRFS subvol
select_target_disk
# create and initialize structures for snapper configs
run_config_preparation
# run backups (Snapshot types: btrfs-snapshot, btrfs-clone, btrfs-archive)
run_backup
# finalize backup tasks
run_finalize
# cleanup
run_cleanup
printf "${BLUE}Backups done!${NO_COLOR}\n"
exec 3>&-
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