Files
dsnap-sync/bin/dsnap-sync
2018-09-22 21:36:05 +02:00

2879 lines
99 KiB
Bash

#! /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.6.0"
# 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 --tmpdir=$XDG_RUNTIME_DIR/$progname -d)
PIPE=$TMPDIR_PIPE/$progname.out
test ! -d $XDG_RUNTIME_DIR/$progname && mkdir -p $XDG_RUNTIME_DIR/$progname
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
archive_type=full
batch=0
btrfs_quota=0
#btrfs_verbose_flag=--verbose
btrfs_verbose_flag=
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=''
ltfs_mountpoint="/media/tape"
target_count=0
target_match_count=0
tape_match=''
interactive=1
selected_configs=0
selected_subvol='none'
selected_target='none'
selected_uuid='none'
snapper_sync_id=0
snapper_snapshots=".snapshots" # hardcoded in snapper
snapper_subvolume_template="dsnap-sync"
snapper_backup_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 ssh >/dev/null 2>&1 || { printf "'ssh' is not installed." && exit 1; }
which scp >/dev/null 2>&1 || { printf "'scp' 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; }
# optional binaries:
which attr >/dev/null 2>&1 || { printf "'attr' is not installed." && exit 1; }
which ionice >/dev/null 2>&1 && { do_ionice_cmd=1; }
which notify-send >/dev/null 2>&1 && { donotify=1; }
which pv >/dev/null 2>&1 && { do_pv_cmd=1; }
which tape-admin >/dev/null 2>&1 && { tape_admin_cmd=1; }
if [ $(id -u) -ne 0 ] ; then printf "$progname: must be run as root\n" ; exit 1 ; fi
if [ -n "$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 selected_config=${1}
local batch=${2:-0}
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}check_snapper_failed_ids()...${NO_COLOR}\n"
fi
# 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=$(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=$(snapper --config "$selected_config" list --type single \
| awk -F '|' ' /'"$snap_description_running"'/' \
| awk ' NR==1 {print $1} ')
failed_id_last=$(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
#btrfs quota enable $snapper_source_snapshot
sync
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Snapper source snapshot ${GREEN}'%s'${MAGENTA} created${NO_COLOR}\n" "$snapper_source_id"
fi
else
printf "${MAGENTA}dryrun${NO_COLOR}: 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_archive_last_sync_id () {
local snapper_config=${1#snapper_config=}
local archive_type=${2##archive_type=}
local remote=${3##remote=}
local run_ssh=''
snapper_sync_id=0
if [ ${#remote} -ge 1 ]; then run_ssh=$ssh; fi
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}get_archive_last_sync_id()...${NO_COLOR}\n"
fi
if [ $verbose -ge 3 ]; then
if [ $remote ]; then
printf "${MAGENTA}Get sync-ID of ${GREEN}last '%s' btrfs-stream${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} on remote ${GREEN}'%s'${NO_COLOR}\n" \
$archive_type $snapper_config $remote
else
printf "${MAGENTA}Get sync-ID of ${GREEN}last '%s' btrfs-stream${MAGENTA} for snapper config ${GREEN}'%s'${NO_COLOR}\n" \
$archive_type $snapper_config
fi
fi
# target is LTFS tape
if [ ${#volume_name} -gt 1 ]; then
case ${archive_type} in
incremental)
cmd="find ${selected_target}/${backupdir}/${snapper_target_config} -name *_${archive_type}.btrfs \
| awk -F '.*/' '{ gsub(/_${archive_type}.btrfs\$/,"_"); print \$2}' "
;;
*)
cmd="find ${selected_target}/${backupdir}/${snapper_target_config} -name *_${archive_type}.btrfs \
| awk -F '.*/' '{ gsub(/_${archive_type}.btrfs\$/,"_"); print \$2}' "
;;
esac
fi
ret=$(eval $run_ssh "$cmd" 2>/dev/null)
if [ ${#ret} -ge 1 ]; then
snapper_sync_id=$ret
fi
}
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_media_infos () {
# select the target LTFS tape
if [ ${#mediapool_name} -gt 1 ] || [ ${#volume_name} -gt 1 ]; then
# read mounted LTFS structures
$ssh tape-admin --verbose=$verbose --mount ${mediapool_name} ${volume_name}
if [ $? -eq 0 ]; then
target_cmdline=$ltfs_mountpoint
if [ ${#volume_name} -eq 0 ]; then
# parse out volume-name (fuse - extended attribute)
volume_name=$($ssh attr -g ltfs.volumeName $ltfs_mountpoint)
volume_name=$(echo ${volume_name##*:} | sed -e 's/\r\n//g')
fi
get_tape_infos
else
printf "${RED}Error: ${NO_COLOR}Can't mount volume ${GREEN}'%s'${NO_COLOR} from MediaPool ${GREEN}'%s'${NO_COLOR}\n" \
"$volume_name" "$mediapool_name"
die "Can't mount valid tape."
fi
else
# read mounted BTRFS structures
get_disk_infos
fi
}
get_snapper_backup_type () {
local snapper_config=$1
local snapper_config_type='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_backup_type()...${NO_COLOR}\n"
fi
if [ ${#backuptype_cmdline} -gt 1 ]; then
snapper_backup_type=${backuptype_cmdline}
else
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Get snapper backup 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=$(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_backup_type=$(echo $value | sed -e 's/\"\(.*\)\"/\1/')
continue
;;
CHILD_CONFIG)
snapper_target_config=$(echo $value | sed -e 's/\"\(.*\)\"/\1/')
continue
;;
PARENT_CONFIG)
snapper_parent_config=$(echo $value | sed -e 's/\"\(.*\)\"/\1/')
continue
;;
*)
# value is not relevant
continue
;;
esac
done < $SNAPPER_CONFIG_DIR/$snapper_config
fi
case $snapper_backup_type in
archive|Archive)
# archive btrfs-snapshot to non btrfs-filesystem
snapper_backup_type=btrfs-archive
snapper_target_config=archive-$snapper_config
;;
child|Child)
# clone to btrfs-snapshot
snapper_backup_type=btrfs-clone
snapper_target_config=clone-$snapper_config
;;
parent|Parent)
# disk2disk btrfs-snapshot
snapper_backup_type=btrfs-snapshot
snapper_target_config=$snapper_config
;;
*)
# disk2disk btrfs-snapshot (default)
snapper_backup_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 backup type: '%s'\n" $snapper_backup_type
printf "Snapper target configuration: '%s'\n" $snapper_target_config
printf "Snapper parent configuration: '%s'\n" $snapper_parent_config
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_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
# target location is mounted as root subvolume
if [ -z $remote ]; then
# local root filesystem will be excluded as a valid target location
exclude_uuid=$(findmnt --noheadings --nofsroot --mountpoint / --output UUID)
disk_uuids=$(findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list \
| grep -v $exclude_uuid \
| awk '{print $1}')
disk_targets=$(findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list \
| grep -v $exclude_uuid \
| awk '{print $2}')
fs_options=$(findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list \
| grep -v $exclude_uuid \
| awk '{print $2}')
else
# remote root filesystem will be 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}'")
fi
else
# target location is not mounted as root subvolume
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: fs_type_$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
eval "fs_type_$i='btrfs'"
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'"
eval "fs_type_$i='btrfs'"
target_count=$(($target_count+1))
i=$((i+1))
done
i=0
for disk_target in $disk_targets; do
if [ "$disk_target" = "$target_cmdline" ]; then
target_match="$i"
target_match_count=$(($target_match_count+1))
fi
eval "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_tape_infos () {
local tape_id
local tape_target
local tape_fs_option
if [ $verbose -ge 3 ]; then
printf "${BLUE}get_tape_infos()...${NO_COLOR}\n"
fi
# get infos from mounted LTFS tapes
if [ -z $remote ]; then
# on localhost
if [ "$(findmnt --noheadings --nofsroot --types fuse --output SOURCE | awk -F ':' '{print $1}')" = "ltfs" ]; then
tape_ids=$(findmnt --noheadings --nofsroot --types fuse --output SOURCE --list \
| awk -F ':' '{print $2}')
tape_targets=$(findmnt --noheadings --nofsroot --types fuse --output TARGET --list \
| awk '{print $1}')
fs_options=$(findmnt --noheadings --nofsroot --types fuse --output SOURCE,OPTIONS --list \
| awk '{print $2}')
fi
else
# on remote host
if [ "$($ssh findmnt --noheadings --nofsroot --types fuse --output SOURCE --list | awk -F ':' '{print $1}')" = "ltfs" ]; then
tape_ids=$($ssh findmnt --noheadings --nofsroot --types fuse --output SOURCE --list \
| awk -F ':' '{print $2}')
tape_targets=$($ssh findmnt --noheadings --nofsroot --types fuse --output TARGET --list \
| awk '{print $1}')
fs_options=$($ssh findmnt --noheadings --nofsroot --types fuse --output SOURCE,OPTIONS --list \
| awk '{print $2}')
fi
fi
# we need at least one target disk
if [ ${#tape_targets} -eq 0 ]; then
printf "no suitable LTFS tape mounted yet\n"
fi
# Posix Shells do not support Array. Therefore using ...
# Pseudo-Arrays (assumption: equal number of members)
# Pseudo-Array: tape_id_$i
# Pseudo-Array: tape_target_$i
# Pseudo-Array: fs_options_$i
# Pseudo-Array: fs_type_$i
# Pseudo-Array: tape_selected_$i (reference to $i element)
# List: tape_id_match (reference to matching preselected uuids)
# List: tape_target_match (reference to matching preselected targets)
# initialize our structures
#i=0
y=$(($target_count+1))
i=$y
for tape_id in $tape_ids; do
if [ "$tape_id" = "$tapeid_cmdline" ]; then
if [ ${#tape_id_match} -gt 0 ]; then
tape_id_match="${tape_id_match} $i"
else
tape_id_match="$i"
fi
tape_id_match_count=$(($tape_id_match_count+1))
fi
eval "tape_id_$i='$tape_id'"
eval "fs_type_$i='ltfs'"
target_count=$(($target_count+1))
i=$((i+1))
done
i=$y
for tape_target in $tape_targets; do
if [ "$tape_target" = "$target_cmdline" ]; then
target_match="$i"
target_match_count=$(($target_match_count+1))
fi
eval "target_$i='$tape_target'"
i=$((i+1))
done
#i=0
i=$y
for fs_option in $fs_options; do
eval "fs_options_$i='$fs_option'"
i=$((i+1))
done
}
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
if [ ${#remote} -ge 1 ];then run_ssh=$ssh; fi
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 [ ${#snapper_subvolid} -ge 1 -a ${#snapper_uuid} -ge 1 ]; then
cmd="snapper --config $snapper_config list --type single \
| awk '/$snapper_description/' \
| awk '/subvolid=$snapper_subvolid, uuid=$snapper_uuid/' \
| awk 'END {print \$1}'"
else
cmd="snapper --config $snapper_config list --type single \
| awk '/$snapper_description/' \
| awk 'END {print \$1}'"
fi
snapper_sync_id=$(eval $run_ssh "$cmd")
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 grap latest successfull sync
if [ ${#snapper_subvolid} -ge 1 -a ${#snapper_uuid} -ge 1 ]; then
cmd="snapper --config $snapper_config list --type single \
| awk '/$snap_description_finished/' \
| awk '/subvolid=$selected_subvol, uuid=$selected_uuid/' \
| awk 'END {print \$1}'"
else
cmd="snapper --config $snapper_config list --type single \
| awk '/$snap_description_finished/' \
| awk 'END {print \$1}'"
fi
snapper_sync_id=$(eval $run_ssh "$cmd")
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 remote=${2##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 { gsub(/ /,_); print \$1} '"
ret=$(eval $run_ssh "$cmd")
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
i=0
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
;;
--backuptype)
backuptype_cmdline="$2"
shift 2
;;
-c|--config)
# Pseudo-Array: selected_config_$i
eval "selected_config_$i='${2}'"
i=$(($i+1))
selected_configs=$i
shift 2
;;
--color)
color=1
shift 1
;;
--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
;;
--mediapool)
mediapool_name="$2"
shift 2
;;
--nonotify)
donotify=0
shift 1
;;
--nopv)
do_pv_cmd=0
shift 1
;;
--noionice)
do_ionice_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
;;
--volumename)
volume_name="$2"
shift 2
;;
-u|--uuid|--UUID)
uuid_cmdline="$2"
shift 2
;;
--use-btrfs-quota)
btrfs_quota=1
shift 1
;;
-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
;;
--backuptype=*)
backuptype_cmdline=${1#*=}
shift
;;
--config=*)
# Pseudo-Array: selected_config_$i
eval "selected_config_$i='${1#*=}'"
i=$(($i+1))
selected_configs=$i
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
;;
--mediapool=*)
mediapool_name=${1#*=}
shift
;;
--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
;;
--volumename=*)
volume_name=${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
if [ $selected_configs -eq 0 ]; then
. $SNAPPER_CONFIG
i=0
for selected_config in $SNAPPER_CONFIGS; do
# Pseudo-Array: target_selected_$i (reference to $disk_uuid element)
eval "selected_config_$i='$selected_config'"
i=$(($i+1))
selected_configs=$i
done
fi
# 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 [ -n "$remote" ]; then
ssh="ssh $remote"
scp="scp"
if [ ! -z "$port" ]; then
ssh="$ssh -p $port"
scp="$scp -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"
i=0
while [ $i -lt $selected_configs ]; do
printf " selected config: '%s'\n" "$(eval echo \$selected_config_$i)"
i=$(($i+1))
done
printf "for backup-target:\n"
printf " disk UUID: '%s'\n" "$uuid_cmdline"
printf " disk SUBVOLID: '%s'\n" "$subvolid_cmdline"
printf " tape MediaPool: '%s'\n" "$mediapool_name"
printf " tape VolumeName: '%s'\n" "$volume_name"
printf " TARGET name: '%s'\n" "$target_cmdline"
printf " Backupdir: '%s'\n" "$backupdir_cmdline"
printf " Backup Type: '%s'\n" "$backuptype_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 [ $btrfs_quota -eq 1 ]; then snap_sync_options="${snap_sync_options} use-btrfs-quota=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
while [ $i -lt $selected_configs ]; do
# only process on existing configurations
selected_config=$(eval echo \$selected_config_$i)
verify_snapper_config $selected_config
# cleanup failed former runs
check_snapper_failed_ids $selected_config $batch
if [ $SNAP_SYNC_EXCLUDE = "yes" ]; then
continue
fi
# parse selected_config and set $snapper_target_config appropriately
get_snapper_backup_type $selected_config
# parse backupdir
get_backupdir $backupdir_cmdline
# get latest successfully finished snapshot
# verify source
# WIP: metadata from last snapshot!
case $snapper_backup_type in
btrfs-snapshot)
get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \
"snapper_uuid=" "snapper_subvolid=" "remote="
;;
btrfs-clone)
get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \
"snapper_uuid=" "snapper_subvolid=" "remote="
;;
btrfs-archive)
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_backup_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
case $snapper_backup_type in
btrfs-archive)
# set snapper_target_sync_id
get_archive_last_sync_id "snapper_config=${snapper_target_config}" \
"archive_type=full" \
"remote=${remote}"
snapper_target_sync_id=$snapper_sync_id
;;
*)
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
;;
esac
# check for corresponding source and target sync id's
snapper_common_sync_id=0
if [ $snapper_target_sync_id -ne $snapper_source_sync_id ]; then
# select commen sync id
get_snapper_sync_id "snapper_config=${selected_config}" "remote="
if [ $? -eq 0 ]; then
snapper_common_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot
snapper_common_sync_id=$snapper_sync_id
if [ $verbose -ge 2 ]; then
if [ $remote ]; then
printf "${MAGENTA}Common ${GREEN}synced snapshot${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} on '%s': ${GREEN}'%s'${NO_COLOR}\n" \
"$selected_config" "$remote" "$snapper_common_sync_id"
else
printf "${MAGENTA}Common ${GREEN}synced snapshot${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA}: ${GREEN}'%s'${NO_COLOR}\n" \
"$selected_config" "$snapper_common_sync_id"
fi
fi
else
#printf "no commen sync id found.\n"
snapper_target_sync_id=0
fi
fi
if [ $snapper_target_sync_id -eq 0 ]; then
if [ $verbose -ge 2 ]; then
if [ $remote ]; then
printf "${MAGENTA}No synced ${GREEN}target snapshot${MAGENTA} available for snapper config ${GREEN}'%s'${MAGENTA} on '%s'...${NO_COLOR}\n" \
"$selected_config" "$remote"
else
printf "${MAGENTA}No synced ${GREEN}target snapshot${MAGENTA} available for snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
"$selected_config"
fi
fi
backup_root=$selected_target/$backupdir/$snapper_target_config
else
case $selected_fstype in
btrfs)
# 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
;;
*)
# use snapper_target_snapshot
backup_root=$selected_target/$backupdir/$snapper_target_config
;;
esac
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_backup_type_$i='$snapper_backup_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 "snapper_common_sync_id_$i='$snapper_common_sync_id'"
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
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=0
selected_config=
while [ $i -lt $selected_configs ]; do
# only process on existing configurations
selected_config=$(eval echo \$selected_config_$i)
if [ $verbose -ge 1 ]; then
printf "${MAGENTA}Performing backup for config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
"${selected_config}"
fi
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=$(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_backup_type=$(eval echo \$snapper_backup_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)
snapper_common_sync_id=$(eval echo \$snapper_common_sync_id_$i)
backup_dir=$(eval echo \$backupdir_$i)
backup_root=$(eval echo \$backup_root_$i)
# create the needed snapper structure on the target
if [ $verbose -ge 3 ]; then
printf "Config_type '%s'\n" "$snapper_backup_type"
fi
case $snapper_backup_type in
btrfs-snapshot)
create_snapshot
# 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 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"
;;
btrfs-clone)
# 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
# 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 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"
;;
btrfs-archive)
# 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
# targets backup location will save the snapshots in the subdirectory (snapshot-id)
# the snapshot is either the base snapshot (flat-file), or an incremental snapshot (btrfs-send stream)
snapper_target_id=$snapper_source_id
snapper_target_snapshot=$target_cmdline/$backupdir/$snapper_target_config/$snapper_target_id
# 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"
;;
*)
if [ $verbose -ge 3 ]; then
printf "${RED}WIP:${NO_COLOR} what is needed for config_type '%s'\n" "$snapper_backup_type"
fi
;;
esac
if [ $dryrun -eq 0 ]; then
if [ "$interactive" -eq 1 ]; then
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Get size for given source snapshot (id=${GREEN}'%s'${MAGENTA}, path=${GREEN}'%s'${MAGENTA})${NO_COLOR} ...\n" \
"$snapper_source_id" "$snapper_source_snapshot"
fi
if [ $btrfs_quota -eq 1 ]; then
# qgroup for given path, exclude ancestrals
# qgroup identifiers conform to level/id where level 0 is reserved to the qgroups associated with subvolumes
snapper_source_snapshot_size=$(btrfs qgroup show -f --raw $snapper_source_snapshot 2>/dev/null \
| awk 'FNR>2 {print $2}')
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}BTRFS qgroup show result: ${GREEN}'%s'\b${NO_COLOR}'%s'\n" \
"$?" "$snapper_source_snapshot_size"
fi
#if [ $? -eq 1 ]; then
# subvolume is not configured for quota, (temporary?) enable that
#btrfs_quota_tmp=1
#btrfs quota enable $snapper_source_snapshot 2>/dev/null
#btrfs quota rescan -w $snapper_source_snapshot 2>/dev/null
#fi
# need to substitue btrfs 'x.yyGiB' suffix, since pv will need 'xG'
#if [ $snapper_source_snapshot_size -ge 1073741824 ]; then
if [ $snapper_source_snapshot_size -ge 1048576 ]; then
snapper_source_snapshot_size=$(btrfs qgroup show -f --gbytes $snapper_source_snapshot 2>/dev/null \
| awk 'FNR>2 { gsub(/.[0-9][0-9]GiB/,"G"); print $2}')
fi
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}BTRFS quota size for ${GREEN}source snapshot${MAGENTA}: id=${GREEN}'%s'${MAGENTA}, size=${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_source_id" "$snapper_source_snapshot_size"
fi
# should we disable quota usage again?
#if [ $btrfs_quota_tmp -eq 1 ]; then btrfs quota disable $snapper_source_snapshot; fi
else
snapper_source_snapshot_size=$(du --one-file-system --summarize $snapper_source_snapshot 2>/dev/null \
| awk -F ' ' '{print $1}')
if [ $snapper_source_snapshot_size -ge 1048576 ]; then
snapper_source_snapshot_size=$(($snapper_source_snapshot_size / 1024 / 1024))G
fi
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}BTRFS subvolume size for ${GREEN}source snapshot${MAGENTA}: id=${GREEN}'%s'${MAGENTA}, size=${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_source_id" "$snapper_source_snapshot_size"
fi
fi
fi
else
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}dryrun: Would calculate BTRFS subvolume size for ${GREEN}source snapshot${NO_COLOR} ...\n" \
"$snapper_source_id"
fi
fi
# setting process I/O scheduling options
if [ $do_ionice_cmd -eq 1 ]; then
# class: best-efford, priority: medium
#cmd_ionice="ionice --class 2 --classlevel 5"
# class: idle
cmd_ionice="ionice --class 3"
else
cmd_ionice=''
fi
# settings for interactive progress status
if [ $do_pv_cmd -eq 1 ]; then
pv_options="--delay-start 2 --interval 5 --format \"time elapsed [%t] | avg rate %a | rate %r | transmitted [%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
case $selected_fstype in
btrfs)
cmd="btrfs send $btrfs_verbose_flag $snapper_source_snapshot 2>$BTRFS_PIPE \
| $cmd_pv \
$cmd_ionice $ssh btrfs receive $btrfs_verbose_flag $snapper_target_snapshot 2>$BTRFS_PIPE"
;;
*)
# Can't use btrfs receive, since target filesystem can't support btrfs snapshot feature
snapper_target_stream=${snapper_target_id}_${archive_type}.btrfs
if [ ! -f $snapper_target_snapshot/$snapper_target_stream ]; then
if [ -z $remote ]; then
cmd="btrfs send $btrfs_verbose_flag $snapper_source_snapshot 2>$BTRFS_PIPE \
| $cmd_pv \
$cmd_ionice cat > $snapper_target_snapshot/$snapper_target_stream 2>$BTRFS_PIPE"
else
cmd="btrfs send $btrfs_verbose_flag $snapper_source_snapshot 2>$BTRFS_PIPE \
| $cmd_pv \
$cmd_ionice $ssh 'cat > $snapper_target_snapshot/$snapper_target_stream' 2>$BTRFS_PIPE"
fi
else
if [ $verbose -ge 2 ]; then
printf "${RED}BTRFS_Stream: %s${NO_COLOR} already saved.\n" \
"$snapper_target_snapshot/$snapper_target_stream"
fi
i=$(($i+1))
continue
fi
;;
esac
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" "$(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"
printf "Common synced snapshot id: ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_common_sync_id"
fi
# verify that we have a matching source and target snapshot-id
if [ $snapper_common_sync_id -eq 0 ]; then
if [ ${snapper_source_sync_id} != ${snapper_target_sync_id} ]; then
if [ $snapper_target_sync_id -lt $snapper_target_id ]; then
# try to find last target_sync_id in source_config
get_snapper_sync_id "snapper_config=${snapper_source_config}" "remote="
if [ $? -eq 0 ]; then
snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot
else
printf "no commen sync id.\n"
fi
fi
else
# use source_id as common sync-id
snapper_common_sync_id=$snapper_source_id
snapper_common_sync_snapshot=$snapper_source_sync_snapshot
fi
else
# we have a common sync_id
if [ ${snapper_source_sync_id} != ${snapper_target_sync_id} ]; then
# btrfs send: we need to use the common sync_id as a valid parent
snapper_source_sync_id=$snapper_common_sync_id
snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_source_sync_id/snapshot
fi
fi
cmd="$ssh stat --format %i $snapper_target_snapshot 2>/dev/null"
ret=$(eval $cmd)
if [ $? -eq 0 ]; then
case $selected_fstype in
btrfs)
# Sends the difference between the new snapshot and old synced snapshot.
# Using the flag -p (parent) will require the availibility of an identical readonly
# subvolume on the source and the receiving location (the parent-id).
# using "btrfs send -p" instead of "btrfs send -c", then no parent search would be
# needed (Andreij explained in: https://www.spinics.net/lists/linux-btrfs/msg69369.html)
cmd="btrfs send $btrfs_verbose_flag -p $snapper_common_sync_snapshot $snapper_source_snapshot 2>$BTRFS_PIPE \
| $cmd_pv \
$cmd_ionice $ssh btrfs receive $btrfs_verbose_flag $snapper_target_snapshot 2>$BTRFS_PIPE"
;;
*)
# Can't use btrfs receive, since target filesystem can't support btrfs snapshot feature
snapper_target_stream=${snapper_target_id}_incremental.btrfs
cmd="btrfs send $btrfs_verbose_flag -p $snapper_common_sync_snapshot $snapper_source_snapshot 2>$BTRFS_PIPE \
| $cmd_pv \
$cmd_ionice $ssh cat >$snapper_target_snapshot/$snapper_target_stream 2>$BTRFS_PIPE"
;;
esac
if [ $verbose -ge 2 ]; then
printf "${GREEN}btrfs send${NO_COLOR} is using \n parent snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR}) and\n snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n constructing incremental data-stream ...\n" \
"$snapper_common_sync_id" "$snapper_common_sync_snapshot" "$snapper_source_id" "$snapper_source_snapshot"
fi
if [ $verbose -ge 3 ]; then
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" "$(cat $BTRFS_PIPE)"
die "btrfs pipe error."
fi
else
# is this clause possible?
if [ $verbose -ge 3 ]; then
printf "${RED}Error: ${NO_COLOR}No commen sync snapshot ${GREEN}'%s'${NO_COLOR} on ${GREEN}source${NO_COLOR} to sync metadata ...\n" \
"$snapper_common_sync_snapshot"
die "btrfs send/recieve error."
fi
fi
fi
else
printf "${MAGENTA}dryrun${NO_COLOR}: Would run btrfs send / btrfs receive pipe\n"
#printf "dryrun: '%s'\n" "$cmd"
fi
# send the snapper info metadata
if [ $dryrun -eq 0 ]; then
if [ -z "$remote" ]; then
cp "$snapper_source_info" "$snapper_target_snapshot"
else
cmd="$scp $snapper_source_info root@$remote:$snapper_target_snapshot/"
$(eval $cmd) 1>/dev/null
fi
else
printf "${MAGENTA}dryrun${NO_COLOR}: Would copy info metadata '%s' to target.\n" \
"$snapper_source_info"
fi
# Save config specific values in pseudo arrays
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'"
# finalize backup tasks
run_finalize ${selected_config}
run_cleanup ${selected_config}
if [ $verbose -ge 1 ]; then
printf "${MAGENTA}Backup complete:${NOCOLOR} id=${GREEN}'%s'${NO_COLOR}, config=${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_source_id" "$selected_config"
fi
i=$(($i+1))
done
}
run_cleanup () {
local selected_config=${1}
local batch=${2:-$false}
if [ $verbose -ge 1 ]; then
printf "${MAGENTA}Performing cleanup for config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
${selected_config}
fi
if [ $dryrun -eq 0 ]; then
# cleanup failed runs
check_snapper_failed_ids "$selected_config" "$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
else
printf "${MAGENTA}dryrun${NO_COLOR}: Would cleanup TEMPDIR and failed snapshot IDs ...\n"
fi
}
run_finalize () {
local selected_config=${1}
if [ $verbose -ge 1 ]; then
printf "${MAGENTA}Finalize backups for config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \
${selected_config}
fi
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=$(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_backup_type=$(eval echo \$snapper_backup_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)
#tape_id=$(eval echo \$tape_id_$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=$(cat /etc/hostname)
src_uuid=$(findmnt --noheadings --output UUID --target $SUBVOLUME)
src_subvolid=$(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_description_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
snapperd_pid=$(eval $ssh pgrep snapperd)
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Kill runnint ${GREEN}snapperd${MAGENTA} on target id:${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapperd_pid"
fi
$(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_backup_type in
btrfs-archive)
# archive btrfs-snapshot to non btrfs-filesystem on target (e.g tape, ext4)
# save target-id
if [ $snapper_source_id -gt 0 ]; then
cmd="snapper --config $selected_config modify \
--userdata \"tapeid=$volume_name\" \
$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
# update tape attributes
if [ ${#mediapool_name} -gt 1 ] && [ ${#volume_name} -gt 1 ]; then
# read mounted LTFS structures
$ssh tape-admin --verbose=$verbose --update-lastwrite ${mediapool_name} ${volume_name}
$ssh tape-admin --verbose=$verbose --update-retensiondate ${mediapool_name} ${volume_name}
fi
fi
return 0
;;
btrfs-clone)
# no tagging needed
return 0
;;
btrfs-snapshot)
# create btrfs-snapshot on target
cmd="$ssh snapper --config \"$snapper_target_config\" list --type single \
| awk ' /'\"$snap_description_running\"'/ ' \
| awk -F '|' ' \$1 == $snapper_target_id {print \$1} ' "
;;
esac
while [ "$ii" -le "$ii_max" ]; do
if [ $verbose -ge 3 ]; then
printf "calling: '%s'\n" "$cmd"
fi
ret=$(eval "$cmd")
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" "$($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=$(eval 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, important=yes, 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)"
printf "calling: '%s'\n" "$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
# TODO: no method to remove userdata pair, use awk
cmd="snapper --config $selected_config modify \
--description \"$snap_description_finished\" \
--userdata \"important=no\" \
$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" "$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 "${MAGENTA}dryrun${NO_COLOR}: %s\n" "$cmd"
cmd="snapper --config $selected_config modify --description $snap_description_synced --userdata $userdata $snapper_sync_id"
printf "${MAGENTA}dryrun${NO_COLOR}: %s\n" "$cmd"
cmd="snapper --config $selected_config modify --description $snap_description_finished $snapper_source_sync_id"
printf "${MAGENTA}dryrun${NO_COLOR}: %s\n" "$cmd"
fi
}
select_target () {
local i=0
local target_id=0
local target_selected_count=0
local subvolid=''
local subvol=''
if [ $verbose -ge 1 ]; then
printf "${BLUE}Select backup target...${NO_COLOR}\n"
fi
# print selection table
if [ $verbose -ge 2 ]; then
if [ -z "$remote" ]; then
printf "Selecting a mounted device for backups on your local machine.\n"
else
printf "Selecting a mounted device for backups on %s.\n" "$remote"
fi
fi
while [ "$target_id" -eq 0 ] || [ "$target_id" -le $target_count ]; do
if [ "$disk_subvolid_match_count" -eq 1 ]; then
# matching SUBVOLID selection from commandline
if [ $verbose -ge 3 ]; then
printf "%s mount points were found with SUBVOLID '%s'.\n" \
"$disk_subvolid_match_count" "$subvolid_cmdline"
fi
# Pseudo-Array: target_selected_$i (reference to $disk_uuid element)
eval "target_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/')
target_selected=$disk_subvolid_match
break
fi
if [ "$target_match_count" -eq 1 ]; then
# matching TARGET selection from commandline
if [ $verbose -ge 3 ]; then
printf "%s mount points were found with TARGET '%s'.\n" \
"$target_match_count" "$target_cmdline"
fi
# Pseudo-Array: target_selected_$i (reference to $disk_uuid element)
eval "target_selected_$i='$disk_target_match'"
disk=$(eval echo \$disk_uuid_$target_match)
#target=$(eval echo \$disk_target_$disk_target_match)
target=$(eval echo \$target_$target_match)
fs_options=$(eval echo \$fs_options_$target_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
target_selected=$target_match
break
fi
if [ "$disk_uuid_match_count" -ge 1 ]; then
# matching UUID selection from commandline
target_count=$disk_uuid_match_count
if [ $verbose -ge 3 ]; 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 "target_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/')
if [ $disk_uuid_match -gt 1 ]; then
printf "%4s) %s (uuid=%s,%s)\n" "$i" "$target" "$disk" "$fs_options"
i=$((i+1))
else
target_selected=$disk_uuid_match
break
fi
done
else
while [ "$target_id" -lt $target_count ]; do
# present all mounted BTRFS filesystems
if [ $verbose -ge 3 ]; then
printf "Present selection for '%s' available targets\n" \
"$target_count"
fi
# Pseudo-Array: target_selected_$i (reference to $target_id element)
eval "target_selected_$i='$target_id'"
disk=$(eval echo \$disk_uuid_$target_id)
target=$(eval echo \$target_$target_id)
fs_options=$(eval echo \$fs_options_$target_id | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
fs_type=$(eval echo \$fs_type_$target_id)
case $fs_type in
btrfs)
printf "%4s) %s (type=%s,uuid=%s,%s)\n" "$i" "$target" "$fs_type" "$disk" "$fs_options"
;;
ltfs)
printf "%4s) %s (type=%s,%s)\n" "$i" "$target" "$fs_type" "$fs_options"
;;
esac
i=$((i+1))
target_id=$(($target_id+1))
done
fi
printf "%4s) Exit\n" "x"
read -r -p "Enter a number: " target_selected
case $target_selected in
x)
break
;;
[0-9][0-9]|[0-9])
if [ "$target_selected" -gt "$target_count" ]; then
target_id=0
i=0
else
break
fi
;;
*)
printf "\nNo disk selected. Select a disk to continue.\n"
target_id=0
i=0
;;
esac
done
if [ "$target_selected" = x ]; then
exit 0
fi
selected_target=$(eval echo \$target_$target_selected)
selected_fstype=$(eval echo \$fs_type_$target_selected)
selected_uuid=$(eval echo \$disk_uuid_$target_selected)
selected_subvol=$(eval echo \$fs_options_$target_selected | sed -e 's/.*subvolid=\([0-9]*\).*/\1/')
if [ $verbose -ge 2 ]; then
case $selected_fstype in
btrfs)
printf "${MAGENTA}You selected %s target with UUID ${GREEN}'%s'${MAGENTA} (subvolid=${GREEN}'%s'${MAGENTA})${NO_COLOR}.\n" \
"$selected_fstype" "$selected_uuid" "$selected_subvol"
;;
ltfs)
volume_name=$(attr -g ltfs.volumeName ${selected_target})
volume_name=$(echo ${volume_name##*:} | sed -e 's/\r\n//g')
printf "${MAGENTA}You selected %s target (VolumeName=${GREEN}'%s'${MAGENTA})${NO_COLOR}.\n" \
"$selected_fstype" "$volume_name"
;;
esac
if [ -z "$remote" ]; then
printf "${MAGENTA}Target 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
}
select_target_disk () {
local i=0
local disk_id=0
local disk_selected_count=0
local subvolid=''
local subvol=''
if [ $verbose -ge 1 ]; then
printf "${BLUE}Select backup target...${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
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
}
select_target_tape () {
local i=0
local tape_id=0
local tape_selected_count=0
local subvolid=''
local subvol=''
if [ $verbose -ge 1 ]; then
printf "${BLUE}Select target tape...${NO_COLOR}\n"
fi
# print selection table
if [ $verbose -ge 2 ]; then
if [ -z "$remote" ]; then
printf "Selecting a mounted LTFS tape for backups on your local machine.\n"
else
printf "Selecting a mounted LTFS tape for backups on %s.\n" "$remote"
fi
fi
while [ "$tape_id" -eq -1 ] || [ "$tape_id" -le $tape_count ]; do
if [ "$tape_match_count" -eq 1 ]; then
# matching LTFS selection from commandline
# Pseudo-Array: tape_selected_$i (reference to $tape_uuid element)
eval "tape_selected_$i='$tape_id_match'"
tape=$(eval echo \$tape_id_$tape_id_match)
#subvolid=$(eval echo \$tape_id_$tape_id_match)
#fs_options=$(eval echo \$tape_fs_options_$tape_id_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
tape_selected=$tape_id_match
break
fi
if [ "$tape_target_match_count" -eq 1 ]; then
# matching TARGET selection from commandline
# Pseudo-Array: tape_selected_$i (reference to $tape_id element)
eval "tape_selected_$i='$tape_target_match'"
tape=$(eval echo \$tape_id_$tape_target_match)
target=$(eval echo \$tape_target_$tape_target_match)
#fs_options=$(eval echo \$fs_options_$tape_target_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
tape_selected=$tape_target_match
break
fi
if [ "$tape_id_match_count" -gt 1 ]; then
# got LTFS ID selection from commandline
tape_count=$tape_id_match_count
if [ $verbose -ge 2 ]; then
printf "%s mount points were found with ID '%s'.\n" "$tape_id_match_count" "$uuid_cmdline"
fi
for tape_id in $tape_id_match; do
# Pseudo-Array: tape_selected_$i (reference to $tape_uuid element)
eval "tape_selected_$i='$tape_id'"
tape=$(eval echo \$tape_id_$tape_id)
tape_fs_options=$(eval echo \$fs_options_$tape_id | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
printf "%4s) %s (id=%s,%s)\n" "$i" "$target" "$tape" "$tape_fs_options"
i=$((i+1))
done
else
while [ "$tape_id" -le $tape_count ]; do
# present all mounted BTRFS filesystems
# Pseudo-Array: tape_selected_$i (reference to $tape_id element)
eval "tape_selected_$i='$tape_id'"
tape=$(eval echo \$tape_id_$tape_id)
target=$(eval echo \$tape_target_$tape_id)
#tape_fs_options=$(eval echo \$target_fs_options_$tape_id | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
printf "%4s) %s\n" "$i" "$target"
i=$((i+1))
tape_id=$(($tape_id+1))
done
fi
printf "%4s) Exit\n" "x"
read -r -p "Enter a number: " tape_selected
case $tape_selected in
x)
break
;;
[0-9][0-9]|[0-9])
if [ "$tape_selected" -gt "$tape_count" ]; then
tape_id=0
i=0
else
break
fi
;;
*)
printf "\nNo LTFS tape selected. Select a tape to continue.\n"
tape_id=0
i=0
;;
esac
done
if [ "$tape_selected" = x ]; then
exit 0
fi
selected_tape_id=$(eval echo \$tape_id_$tape_selected)
selected_tape_target=$(eval echo \$tape_target_$tape_selected)
#selected_subvol=$(eval echo \$fs_options_$tape_selected | sed -e 's/.*subvolid=\([0-9]*\).*/\1/')
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}You selected the LTFS tape with ID ${GREEN}'%s'${MAGENTA}${NO_COLOR}.\n" \
"$selected_tape_id"
if [ -z "$remote" ]; then
printf "${MAGENTA}Target tape is mounted at ${GREEN}'%s'${MAGENTA}.${NO_COLOR}\n" \
"$selected_tape_target"
else
printf "${MAGENTA}Target tape is mounted on host ${GREEN}'%s'${MAGENTA} at ${GREEN}'%s'${MAGENTA}.${NO_COLOR}\n" \
"$remote" "$selected_tape_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 [ "$tape_id_cmdline" != "none" ]; then
if [ -z "$remote" ]; then
notify_info "LTFS Target selected" "Using LTFS targets '$tape_id_cmdline'..."
else
notify_info "LTFS Target selected" "Using LTFS targets uuid '$tape_id_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 $selected_config
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
--backuptype <type> Specify backup type <clone | archive>
--batch no user interaction
-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. You can select multiple configurations
(e.g. -c "root" -c "home"; --config root --config home)
--config-postfix <name> Specify a postfix that will be appended to the destination snapper config name
--dry-run perform a trial run (no changes are written)
--mediapool Specify the name of the tape MediaPool
-n, --noconfirm Do not ask for confirmation for each configuration. Will still prompt for backup
--nonotify Disable graphical notification (via dbus)
--nopv Disable graphical progress output (disable pv)
--noionice Disable setting of I/O class and priority options on target
-p, --port <port> The remote port
-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
-s, --subvolid <subvlid> Specify the subvolume id of the mounted BTRFS subvolume to back up to. Defaults to 5
--use-btrfs-quota use btrfs-quota to calculate snapshot size
-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 selection
-t, --target <target> Specify the mountpoint of the backup device
--volumename Specify the name of the tape volume
-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()...${NO_COLOR}\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"
ret=$(eval $cmd)
if [ $? -eq 1 ]; then
# strip last dir from backup_root
#base_path=${backup_root%/*}; echo $base_path
if [ $dryrun -eq 0 ]; then
#$($ssh mkdir --mode=0700 --parents $base_path)
$($ssh mkdir --mode=0700 --parents $backup_root)
if [ $verbose -ge 3 ]; then
if [ -z $remote_host ]; then
printf "${MAGENTA}Create${NO_COLOR} new backup base-path ${GREEN}'%s'${NO_COLOR}...\n" \
"$backup_root"
else
printf "${MAGENTA}Create${NO_COLOR} new backup base-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 "${MAGENTA}dryrun${NO_COLOR}: Would create backup-path %s ...\n" \
"$base_path"
else
printf "${MAGENTA}dryrun${NO_COLOR}: Would create backup-path %s on remote host %s ...\n" \
"$remote_host" "$base_path"
fi
fi
fi
# archive type: full or incremental
# full snyc: save a btrfs full-stream
# incremental: save a btrfs incremental-stream. Stream will depend on parent
# restore process:
# 1) copy in last full to btrfs filesystem
# 2) loop though ordered incremental path: "cat <stream> | btrfs recieve"
# verify that target can take the new archive for given snapshot 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_id"
fi
cmd="$ssh stat --format %i $backup_root/$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_id"
fi
ret=$(eval $ssh mkdir --mode=0700 \
$backup_root/$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_id"
die "Can't create snapshot dir on target."
fi
fi
else
printf "${MAGENTA}dryrun${NO_COLOR}: Would check/create path %s to store target snapshot ...\n" \
"$backup_root/$snapper_snapshots/$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=$(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=$(eval 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%/*}
if [ ${#base_path} -ge 1 }; then
if [ $dryrun -eq 0 ]; then
$($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 "${MAGENTA}dryrun${NO_COLOR}: Would create backup-path %s ...\n" \
"$base_path"
else
printf "${MAGENTA}dryrun${NO_COLOR}: Would create backup-path %s on remote host %s ...\n" \
"$remote_host" "$base_path"
fi
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"
#$($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"
# $($cmd) || \
# die "Changing the directory mode for '$backup_root' on '$remote_host'."
else
printf "${MAGENTA}dryrun${NO_COLOR}: Would create new snapper configuration from template %s ...\n" "$snapper_subvolume_template"
printf "${MAGENTA}dryrun${NO_COLOR}: 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/.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 "${MAGENTA}dryrun${NO_COLOR}: 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
ret=$(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 $selected_config $batch
die "Can't backup to existing snapshot-id ($snapper_id)!"
fi
fi
else
printf "${MAGENTA}dryrun${NO_COLOR}: 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 $@
# parse infos for backup medium
get_media_infos
# select the backup target
select_target
# create and initialize structures for snapper configs
run_config_preparation
# run backups (Snapshot types: btrfs-snapshot, btrfs-clone, btrfs-archive)
run_backup
# cleanup
if [ -d $TMPDIR_PIPE ]; then
rm -rf $TMPDIR_PIPE || die "Failed to cleanup temporary directory '%s'\n" "$TMPDIR_PIPE"
fi
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