Files
dsnap-sync/bin/dsnap-sync
Ralf Zerres 5c9dc9c45b dsnap-sync: correction for tranfer_size calculation
* adapt btrfs output to be compatible with pv requested format
  convert MiB or GiB to M or G

Signed-off-by: Ralf Zerres <ralf.zerres@networkx.de>
2025-08-04 10:26:15 +02:00

3217 lines
110 KiB
Bash
Executable File

#!/bin/sh
# dsnap-sync
# https://github.com/rzerres/dsnap-sync
# Copyright (C) 2016, 2017 James W. Barnett
# Copyright (C) 2017 - 2019 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.5.1"
# 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
# Create TEMPDIR
test ! -d $XDG_RUNTIME_DIR/$progname && mkdir -p $XDG_RUNTIME_DIR/$progname
TMPDIR=$(mktemp --tmpdir=$XDG_RUNTIME_DIR/$progname -d)
# define fifo pipes
BTRFS_PIPE=$TMPDIR/btrfs.fifo
test -p $BTRFS_PIPE && mkfifo $BTRFS_PIPE
# global variables
args=
answer=no
archive_type=full
batch=0
btrfs_quota=0
btrfs_quota_tmp=1
#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=''
error_count=0
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_snapshot_name="snapshot" # hardcoded in snapper
snapper_subvolume_template="dsnap-sync"
snapper_backup_type='none'
#snapper_config_postfix="."`hostname`
snapper_config_postfix=
snap_cleanup_algorithm="timeline"
transfer_size=0
verbose=0
volume_name=
# 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; }
which nc >/dev/null 2>&1 && { nc_cmd=1; }
if [ $(id -u) -ne 0 ] ; then printf "$progname: must be run as root\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
}
check_transfer_size () {
local source_snapshot=${1##source_snapshot=}
local clone_snapshot=${2##clone_snapshot=}
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}check_transfer_size()...${NO_COLOR}\n"
fi
if [ $dryrun -eq 0 ]; then
transfer_size=0
if [ "$interactive" -eq 1 ]; then
if [ $verbose -ge 2 ]; then
if [ ${#clone_snapshot} -gt 0 ]; then
printf "${MAGENTA}Calculate transfer size for incremental snapshot (clone=${GREEN}'%s'${MAGENTA}, source=${GREEN}'%s'${MAGENTA})${NO_COLOR} ...\n" \
"$clone_snapshot" "$source_snapshot"
else
printf "${MAGENTA}Calculate transfer size for snapshot (source=${GREEN}'%s'${MAGENTA})${NO_COLOR} ...\n" \
"$ssource_snapshot"
fi
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
transfer_size=$(btrfs qgroup show -f --raw $source_snapshot 2>/dev/null \
| awk 'FNR>2 {print $2}')
if [ $? -eq 1 ]; then
# subvolume is not configured for quota, (temporary?, expensive?) enable that
if [ $btrfs_quota_tmp -eq 1 ]; then
btrfs quota enable $source_snapshot 2>/dev/null
btrfs quota rescan -w $source_snapshot 2>/dev/null
transfer_size=$(btrfs qgroup show -f --raw $source_snapshot 2>/dev/null \
| awk 'FNR>2 {print $2}')
fi
fi
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}BTRFS qgroup show result: ${GREEN}'%s'\b${NO_COLOR}'%s'\n" \
"$?" "$transfer_size"
fi
# need to substitue btrfs 'x.yyGiB' suffix, since pv will need 'xG'
if [ $transfer_size -ge 1048576 ]; then
transfer_size=$(btrfs qgroup show -f --gbytes $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}: size=${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_source_id" "$transfer_size"
fi
# should we disable quota usage again?
if [ $btrfs_quota_tmp -eq 1 ]; then btrfs quota disable $source_snapshot; fi
else
if [ ${#clone_snapshot} -gt 0 ]; then
# WIP: dry run with btrfs send
# need to substitue btrfs 'x.yyGiB' suffix, since pv will need 'xG'
transfer_size=$(btrfs send -v -p $clone_snapshot $source_snapshot 2>$BTRFS_PIPE \
| pv -f 2>&1 >/dev/null \
| awk -F ' ' '{ gsub(/.[0-9][0-9]MiB/,"M"); gsub(/.[0-9][0-9]GiB/,"G"); print $1 }' )
else
# filesystem size
transfer_size=$(du --one-file-system --summarize $snapper_source_snapshot 2>/dev/null \
| awk -F ' ' '{print $1}')
if [ $transfer_size -ge 1048576 ]; then
transfer_size=$(($transfer_size / 1024 / 1024))G
fi
fi
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}BTRFS transfer size for ${GREEN}source snapshot${MAGENTA}: size=${GREEN}'%s'${NO_COLOR} ...\n" \
"$transfer_size"
fi
fi
fi
else
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}dryrun: Would calculate transfer size for BTRFS ${GREEN}source snapshot${NO_COLOR} ...\n" \
"$snapper_source_id"
fi
fi
}
create_pv_cmd () {
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}create_pv_cmd()...${NO_COLOR}\n"
fi
# prepare cmdline output 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 $transfer_size $pv_options | "
#cmd_pv="pv $pv_options --size ${transfer_size} | dialog --gauge \"$progname: Progress for config '$selected_config'\" 6 85 |"
else
cmd_pv=''
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
ret=$(snapper --config "$selected_config" create \
--print-number \
--description "$snap_description_running" \
--userdata "host=$HOSTNAME")
else
ret=$(snapper --config "$selected_config" create \
--print-number \
--description "$snap_description_running" \
--userdata "host=$remote")
fi
if [ "$ret" ]; then
snapper_source_id=$ret
else
# if snapper call fails, return value will do be a snapshot-id
printf "${RED}Creation of snapper source snapshot failed${NO_COLOR}.\n" "$snapper_source_id"
snapper_source_id=-1
return 1
fi
if [ $SUBVOLUME = "/" ]; then
SUBVOLUME=""
fi
snapper_source_snapshot=$SUBVOLUME/.snapshots/$snapper_source_id/$snapper_snapshot_name
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}' \
# | awk ' \$1 == $snapper_source_sync_id {print \$1} ' "
# ;;
*)
cmd="find ${selected_target}/${backupdir}/${snapper_target_config} -name *_${archive_type}.btrfs \
| awk -F '.*/' '{ gsub(/_${archive_type}.btrfs\$/,"_"); cnt++} END {print cnt}'"
ret=$(eval $run_ssh "$cmd" 2>/dev/null)
if [ ${#ret} -ge 1 ]; then
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Found ${GREEN}'%s'${MAGENTA} previous synced archives for config ${GREEN}'%s'${NO_COLOR}\n" \
"${ret}" "$selected_config"
fi
# get last sync-id
cmd="find ${selected_target}/${backupdir}/${snapper_target_config} -name *_${archive_type}.btrfs \
| awk -F '.*/' '{ gsub(/_${archive_type}.btrfs\$/,"_") } END {print \$2}'"
else
return 1
fi
;;
esac
fi
ret=$(eval $run_ssh "$cmd" 2>/dev/null)
if [ ${#ret} -ge 1 ]; then
# ok, matching snapshot found
snapper_sync_id=$ret
if [ $verbose -ge 3 ]; then
printf "Got archive snapshot: ${GREEN}'%s'${NO_COLOR} (id: ${GREEN}'%s'${NO_COLOR})\n" \
$snapper_target_config $snapper_sync_id
fi
return 0
else
# no snapshot found
return 1
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
if [ -z $remote ]; then
tape-admin --verbose=$verbose --mount ${mediapool_name} ${volume_name}
else
$ssh tape-admin --version
$ssh tape-admin --verbose=$verbose --mount ${mediapool_name} ${volume_name}
fi
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
if [ $? -ne 0 ]; then
printf "${RED}Error: ${NO_COLOR}Can't use valid volume from MediaPool ${GREEN}'%s'${NO_COLOR}\n" \
"$mediapool_name"
die "Can't use valid tape."
fi
else
if [ ${#volume_name} -gt 0 ]; then
printf "${RED}Error: ${NO_COLOR}Can't use volume ${GREEN}'%s'${NO_COLOR} from MediaPool ${GREEN}'%s'${NO_COLOR}\n" \
"$volume_name" "$mediapool_name"
else
printf "${RED}Error: ${NO_COLOR}Can't use valid volume from MediaPool ${GREEN}'%s'${NO_COLOR}\n" \
"$mediapool_name"
fi
die "Can't use 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
backupdir=$backupdir_cmdline
#printf "${RED}Error: ${MAGENTA}Changing the backupdir for an already existing target-config is not supported yet${NO_COLOR}\n"
#return 1
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
sleep 0.2
exclude_uuid=$($ssh findmnt --noheadings --nofsroot --mountpoint / --output UUID)
sleep 0.2
disk_uuids=$($ssh "findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list \
| grep -v \"$exclude_uuid\" \
| awk '{print \$1}'")
sleep 0.2
disk_targets=$($ssh "findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list \
| grep -v \"$exclude_uuid\" \
| awk '{print \$2}'")
sleep 0.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
sleep 0.2
disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID --list)
sleep 0.2
disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output TARGET --list)
sleep 0.2
fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | awk '{print $2}')
sleep 0.2
fi
# we need at least one target disk
if [ ${#disk_targets} -eq 0 ]; then
printf "${RED}Error: ${MAGENTA}No suitable target disk found${NO_COLOR}"
return 1
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 "${RED}Error: ${MAGENTA}No suitable LTFS tape mounted yet${NO_COLOR}\n"
return 1
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 snapper_tapeid=${5##snapper_tapeid=}
local snapper_backupdir=${6##snapper_backupdir=}
local remote=${7##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"
#printf "${MAGENTA}snapper_description: ${GREEN}'%s'${NO_COLOR}\n" \
# "${snapper_description}"
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 yet.${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}'"
elif [ ${#snapper_tapeid} -ge 1 ]; then
# | awk '/"$snapper_description"' '/"$snap_description_finished/"' \
cmd="snapper --config $snapper_config list --type single \
| awk '/$snapper_description/' \
| awk '/tapeid="$snapper_tapeid"/' \
| awk 'END {print \$1}'"
elif [ ${#backup_dir} -ge 1 ]; then
cmd="snapper --config $snapper_config list --type single \
| awk '/$snapper_description/' \
| awk '/backupdir="$backup_dir"/' \
| 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/$snapper_snapshot_name
else
# no snapshot found, 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}'"
elif [ ${#snapper_tapeid} -ge 1 ]; then
cmd="snapper --config $snapper_config list --type single \
| awk '/$snap_description_finished/' \
| awk '/tapeid="$snapper_tapeid"/' \
| 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/$snapper_snapshot_name
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/g' | 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
;;
--mode)
backup_mode="$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
;;
--mode=*)
backup_mode=${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"
else
port=22
fi
if [ $nc_cmd ]; then
nc -w 3 -z $remote $port > /dev/null || \
die "Can't connect to remote host."
else
$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
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 " Backup Mode: '%s'\n" "$backup_mode"
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=" "snapper_tapeid=" \
"snapper_backupdir=${backupdir}" "remote="
;;
btrfs-clone)
get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \
"snapper_uuid=" "snapper_subvolid=" "snapper_tapeid=" "snapper_backupdir=" "remote="
;;
btrfs-archive)
get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \
"snapper_uuid=" "snapper_subvolid=" "snapper_tapeid=" "snapper_backupdir=" "remote="
#"snapper_uuid=" "snapper_subvolid=" "snapper_tapeid=${volume_name}" "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/$snapper_snapshot_name
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}"
;;
*)
get_snapper_last_sync_id "snapper_config=${snapper_target_config}" \
"snapper_description=${snap_description_synced}" \
"snapper_uuid=" "snapper_subvolid=" "snapper_tapeid=" \
"snapper_backupdir=" \
"remote=${remote}"
;;
esac
if [ $? -eq 0 ]; then
snapper_target_sync_id=$snapper_sync_id
else
snapper_target_sync_id=0
fi
# 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/$snapper_snapshot_name
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
# no commen sync id found
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
if [ $verbose -ge 3 ]; then
if [ $remote ]; then
printf "${MAGENTA}backupdir on remote '%s': ${GREEN}'%s'${NO_COLOR}\n" \
"$remote" "$backupdir"
else
printf "${MAGENTA}backupdir: ${GREEN}'%s'${NO_COLOR}\n" \
"$backupdir"
fi
fi
# 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
backup_root=$value
snapper_target_sync_snapshot=$value/.snapshots/$snapper_target_sync_id/$snapper_snapshot_name
fi
# commandline settings for backupdir on selected_target will have priority
if [ "$backup_root" != "$selected_target/$backupdir" ]; then
backup_root=$selected_target/$backupdir/$snapper_target_config
snapper_target_sync_snapshot=$backup_root/.snapshots/$snapper_target_sync_id/$snapper_snapshot_name
fi
if [ $verbose -ge 3 ]; then
if [ $remote ]; then
printf "${MAGENTA}backup_root on remote '%s': ${GREEN}'%s'${NO_COLOR}\n" \
"$remote" "$backup_root"
else
printf "${MAGENTA}backup_root: ${GREEN}'%s'${NO_COLOR}\n" \
"$backup_root"
fi
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
printf "${RED}Error: ${MAGENTA}Selected snapper configuration ${GREEN}'$selected_config'${MAGENTA} does not exist${NO_COLOR}\n"
# go for next configuration
i=$(($i+1))
continue
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
# go for next configuration
i=$(($i+1))
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)
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}snapper_source_config: ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_source_config"
printf "${MAGENTA}snapper_target_config: ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_target_config"
printf "${MAGENTA}snapper_backup_type: ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_backup_type"
printf "${MAGENTA}snapper_source_sync_id: ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_source_sync_id"
printf "${MAGENTA}snapper_common_sync_id: ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_common_sync_id"
printf "${MAGENTA}backup_dir: ${GREEN}'%s'${NO_COLOR}\n" "$backup_dir"
printf "${MAGENTA}backup_root: ${GREEN}'%s'${NO_COLOR}\n" "$backup_root"
fi
# create the needed snapper structure on the target
case $snapper_backup_type in
btrfs-snapshot)
create_snapshot
if [ $snapper_source_id -lt 0 ]; then
return 1
fi
# 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/$snapper_snapshot_name
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)
if [ $snapper_source_sync_id -eq 0 ]; then
create_snapshot
if [ $snapper_source_id -lt 0 ]; then
return 1
fi
else
# check for last common snapshot
snapper_source_id=$snapper_source_sync_id
snapper_source_snapshot=$SUBVOLUME/.snapshots/$snapper_source_sync_id/$snapper_snapshot_name
snapper_source_info=$SUBVOLUME/.snapshots/$snapper_source_sync_id/info.xml
fi
# 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 [ $? -gt 0 ]; then
error_count=$(($error_count+1))
# go for next configuration
i=$(($i+1))
continue
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
# prepare pipe command
if [ "$dryrun" -eq 0 ]; then
if [ "$snapper_source_sync_id" -eq 0 ] \
|| [ "$snapper_target_sync_id" -eq 0 ] \
|| [ "$backup_mode" = "full" ] ; then
# get size of stream that needs to be transfered
check_transfer_size "source_snapshot=$snapper_source_snapshot"
# prepare send pipe command
create_pv_cmd
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/ 1>$BTRFS_PIPE 2>&1"
;;
*)
# 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>/dev/null \
| $cmd_pv \
$cmd_ionice cat > $snapper_target_snapshot/$snapper_target_stream"
else
cmd="btrfs send $btrfs_verbose_flag $snapper_source_snapshot 2>/dev/null \
| $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
# go for next configuration
i=$(($i+1))
continue
fi
;;
esac
# send full snapshot to target
if [ $verbose -ge 2 ]; then
if [ ${#transfer_size} -gt 0 ]; then
printf "${MAGENTA}Sending ${GREEN}snapshot${NO_COLOR} for snapper config ${GREEN}'%s' ${MAGENTA}(id='%s', size='%s')${NO_COLOR} ...\n" \
"$selected_config" "$snapper_source_id" "$transfer_size"
else
printf "${MAGENTA}Sending ${GREEN}snapshot${NO_COLOR} for snapper config ${GREEN}'%s' ${MAGENTA}(id='%s')${NO_COLOR} ...\n" \
"$selected_config"
fi
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)
if [ "$?" -gt 0 ]; then
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}\n" "$(cat <$BTRFS_PIPE)"
error_count=$(($error_count+1))
# go for next configuration
i=$(($i+1))
continue
fi
else
# source holds synced snapshots
if [ $verbose -ge 3 ]; then
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 "New ${GREEN}target${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \
"$snapper_target_id" "$snapper_target_snapshot/$snapper_snapshot_name"
printf "Common synced snapshot id: ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_common_sync_id"
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 "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"
fi
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Prepare ${GREEN}incremental snapshot${NO_COLOR} (id: ${GREEN}'%s'${NO_COLOR}) for snapper config ${GREEN}'%s'${NO_COLOR} ...\n" \
"$snapper_target_id" "$selected_config"
fi
# verify that we have a matching source and target snapshot-id
if [ $snapper_common_sync_id -eq 0 ]; then
if [ ${snapper_source_id} -eq ${snapper_target_sync_id} ]; then
# nothing to do, snapshot already in sync
if [ $verbose -ge 3 ]; then
printf "${MAGENTA}Nothing to do! Source and target snapshot (id: ${GREEN}'%s'${MAGENTA}) are in sync.${NO_COLOR}\n" \
"$snapper_target_sync_id"
fi
# go for next configuration
i=$(($i+1))
continue
elif [ ${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_id=$snapper_sync_id
snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/$snapper_snapshot_name
else
printf "${RED}Error: ${MAGENTA}No common sync id found. Aborting backup for config ${GREEN}'%s'${NO_COLOR}\n"
error_count=$(($error_count+1))
# go for next configuration
i=$(($i+1))
continue
fi
fi
elif [ ${snapper_source_id} != ${snapper_source_sync_id} ]; then
snapper_common_sync_id=$snapper_source_sync_id
snapper_common_sync_snapshot=$snapper_source_sync_snapshot
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: use 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/$snapper_snapshot_name
fi
fi
cmd="$ssh stat --format %i $snapper_target_snapshot 2>/dev/null"
ret=$(eval $cmd)
if [ $? -eq 0 ]; then
# get size of stream that needs to be transfered
check_transfer_size "source_snapshot=$snapper_source_snapshot" "clone_snapshot=$snapper_common_sync_snapshot"
create_pv_cmd
case $selected_fstype in
btrfs)
# Sends the difference between the new snapshot and old synced snapshot.
# Using the flag -c (clone-src) 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/ 1>$BTRFS_PIPE 2>&1"
;;
*)
# Can't use btrfs receive, since target filesystem can't support btrfs snapshot feature
snapper_target_stream=${snapper_target_id}_incremental.btrfs
if [ -z $remote ]; then
cmd="btrfs send $btrfs_verbose_flag -p $snapper_common_sync_snapshot $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 -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"
fi
;;
esac
if [ $verbose -ge 2 ]; then
printf "%b" "${GREEN}btrfs send${NO_COLOR} is using\n" \
" parent snapshot id: ${GREEN}'$snapper_common_sync_id'${NO_COLOR} (path: ${GREEN}'$snapper_common_sync_snapshot'${NO_COLOR}) and\n" \
" snapshot id: ${GREEN}'$snapper_source_id'${NO_COLOR} (path: ${GREEN}'$snapper_source_snapshot'${NO_COLOR})\n" \
" to construct an incremental data-stream ...\n"
fi
if [ $verbose -ge 3 ]; then
printf "${GREEN}btrfs command:${NO_COLOR} '%s'\n" "$cmd"
fi
$(eval $cmd)
ret=$?
case $ret in
0)
;;
1)
# empty stream, error, no changes
printf "${MAGENTA}btrfs pipe return-code: ${RED}'%s'${NO_COLOR}\n" "$ret"
printf "${RED}%s${NO_COLOR}\n" "$(cat <$BTRFS_PIPE)"
run_cleanup ${selected_config}
# go for next configuration
i=$(($i+1))
continue
;;
127)
printf "${MAGENTA}btrfs pipe return-code: ${GREEN}'%s'${NO_COLOR}\n" "$ret"
;;
*)
printf "${RED}btfs pipe ERROR: '%s'\n" "$ret"
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}\n" "$(cat <$BTRFS_PIPE)"
run_cleanup ${selected_config}
# go for next configuration
i=$(($i+1))
continue
;;
esac
else
# is this clause possible?
if [ $verbose -ge 3 ]; then
printf "${RED}Error: ${MAGENTA}No commen sync snapshot ${GREEN}'%s'${MAGENTA} on ${GREEN}source${MAGENTA} to sync metadata ...${NO_COLOR} \n" \
"$snapper_common_sync_snapshot"
error_count=$(($error_count+1))
# go for next configuration
i=$(($i+1))
continue
fi
fi
fi
else
printf "${MAGENTA}dryrun${NO_COLOR}: Would run btrfs send / btrfs receive pipe\n"
fi
# send the snapper info metadata
if [ $dryrun -eq 0 ]; then
if [ -z "$remote" ]; then
cp "$snapper_source_info" "$snapper_target_snapshot/info.xml"
else
cmd="$scp $snapper_source_info root@$remote:$snapper_target_snapshot/info.xml"
$(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:${NO_COLOR} 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/$snapper_snapshot_name
#$ssh rm -rf $backup_root/$snapper_snapshots/$snapper_target_sync_id
else
printf "${MAGENTA}dryrun${NO_COLOR}: Would cleanup 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
printf "${RED}Error: ${MAGENTA}Selected snapper configuration ${GREEN}'$selected_config'${MAGENTA} does not exist${NO_COLOR}"
return 1
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)
# 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 dependencies
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 2>/dev/null)
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 \
--cleanup-algorithm \"dsnap-sync\" \
--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} -gt 1 ]; 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
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" "$cmd"
fi
ret=$(eval "$cmd")
if [ "$ret" ]; then
if [ $verbose -ge 3 ]; then
printf "return: '%s'\n" "$?"
fi
else
printf "${RED}ERROR: ${MAGENTA}Updating snapper metadata for source snapshot id ${GREEN}'%s'${NO_COLOR}${MAGENTA} failed.\n" \
"Please check for sufficiant space.${NO_COLOR}\n" \
"$snapper_source_id"
fi
sync
fi
if [ ${#snapper_source_sync_id} -gt 0 ]; then
# TODO: no snapper 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/$snapper_snapshot_name
else
snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_source_id/$snapper_snapshot_name
fi
else
# dry-run output
case $snapper_backup_type in
btrfs-archive)
printf "${MAGENTA}dryrun${NO_COLOR}: %s snapper --config %s modify\\ \n \
\t--cleanup-algorithm 'dsnap-sync'\\ \n \
\t--userdata 'tapeid=%s'\\ \n \
\t'snapper_source_id'\n" \
"$ssh" "$selected_config" "$volume_name"
;;
btrfs-snapshot)
printf "${MAGENTA}dryrun${NO_COLOR}: %s snapper --config %s modify \\ \n \
\t--description '%s' \\ \n \
\t--cleanup-algorithm 'dsnap-sync' \\ \n \
\t--userdata 'host=%s, subvolid=%s, uuid=%s' \\ \n \
\t'snapper_source_id'\n" \
"$ssh" "$selected_config" "$snap_description_finished" \
"$src_host" "src_subvolid" "$src_uuid"
;;
esac
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)
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
--mode Force backup mode <full>
-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}ERROR: ${MAGENTA}Cancel path snapshot creation: Can't create path ${GREEN}'%s'${MAGENTA} to store target snapshot${NO_COLOR}\n" \
"$backup_root/$snapper_id"
return 1
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
printf "${RED}Error: ${MAGENTA}Can't backup selected snapper configuration ${GREEN}'$selected_config'${MAGENTA}, that does not exist${NO_COLOR}\n"
return 1
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."
$snapper_activate_$i="no"
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
if [ $remote ]; then
printf "${MAGENTA}Verify snapper filesystem structure${NO_COLOR} on target ${GREEN}'%s'${NO_COLOR}...\n" \
"$remote"
else
printf "${MAGENTA}Verify snapper filesystem structure${NO_COLOR} on target ${GREEN}'%s'${NO_COLOR}...\n" \
"$backup_root"
fi
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
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
ret=$(eval $ssh mkdir --mode=0700 --parents $base_path)
if [ $? -eq 1 ]; then
if [ -z "$remote" ]; then
printf "${RED}Error: Can't create${NO_COLOR} new snapper capable BTRFS ${MAGENTA}subvolume ${GREEN}'%s'${NO_COLOR} ...\n" \
"$backup_root"
else
printf "${RED}Error: Can't 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
return 1
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}Error: ${MAGENTA}Missing a snapper template %s${MAGENTA} to configure the snapper subvolume ${GREEN}%s${MAGENTA} in ${GREEN}%s${MAGENTA} on ${GREEN}%s${NO_COLOR}\n" \
"$snapper_subvolume_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host"
printf "${RED}Error: ${NO_COLOR}Did you miss to install the dsnap-sync's default snapper template on %s?\n" \
"$remote"
return 1
#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"
ret=$(eval $cmd)
if [ $? -ne 0 ]; then
printf "${RED}Error: ${MAGENTA}Creation of BTRFS subvolume (backup-root) ${GREEN}%s:%s${MAGENTA} failed.${NO_COLOR}\n" \
"$remote_host" "$backup_root"
return 1
else
cmd="$ssh chmod 0700 $backup_root"
ret=$(eval $cmd)
if [ $? -ne 0 ]; then
printf "${RED}Error: ${MAGENTA}Changing directory-mode for BTRFS subvolume (backup-root) ${GREEN}%s:%s${MAGENTA} failed. Return-Code: '%s'${NO_COLOR}\n" \
"$remote_host" "$backup_root" "$ret"
return 1
fi
fi
# 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
elif [ $? -eq 0 ]; then
# 256: a btrfs subvolume
if [ $ret -ne 256 ]; then
printf "${RED}Error: ${GREEN}%s ${MAGENTA}needs to be a BTRFS subvolume. But given ${GREEN}%s ${MAGENTA}is just a directory.${NO_COLOR}\n" \
"$snapper_config" "$backup_root"
return 1
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
printf "${RED}Error: ${MAGENTA}Creation of snapper capable config ${GREEN}%s${MAGENTA} on ${GREEN}%s${MAGENTA} failed${NO_COLOR}\n" \
"$backup_root" "$remote_host"
else
printf "${RED}Error: ${MAGENTA}Creation of snapper capable config ${GREEN}%s${MAGENTA} failed${NO_COLOR}\n" \
"$backup_root"
fi
return 1
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 ]; then
printf "${RED}Error: ${MAGENTA}Creation of snapper subvolume ${GREEN}%s${MAGENTA} failed${NO_COLOR}\n" \
"$backup_root/$snapper_snapshots"
return 1
fi
else
if [ $ret -ne 256 ]; then
printf "${RED}Error: ${GREEN}%s ${MAGENTA}needs to be a BTRFS subvolume. But given ${GREEN}%s${MAGENTA} is just a directory${NO_COLOR}\n" \
"$snapper_config" "$backup_root"
return 1
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"
return 1
fi
else
cmd="$ssh stat --format %i $backup_root/$snapper_snapshots/$snapper_id/$snapper_snapshot_name 2>/dev/null"
ret=$(eval $cmd)
if [ $? -eq 0 ] && [ $ret -ne 256 ]; then
# a snapshot path exists, but is not a btrfs snapshot
if [ -z "$remote" ]; then
printf "${RED}Cancel snapshot creation${NO_COLOR}: Directory with id ${GREEN}'%s'${NO_COLOR} already exist in ${BLUE}'%s'${NO_COLOR}, but isn't a btrfs snapshot\n" \
"$snapper_id" "$backup_root/$snapper_snapshots"
else
printf "${RED}Cancel snapshot creation${NO_COLOR}: Directory with id ${GREEN}'%s'${NO_COLOR} already exists on ${BLUE}'%s'${NO_COLOR} in ${BLUE}'%s'${NO_COLOR}, but isn't a btrfs snapshot\n" \
"$snapper_id" "$remote" "$backup_root/$snapper_snapshots"
fi
# cleanup generated snapper entry
check_snapper_failed_ids $selected_config $batch
return 1
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 ]; then
if [ $verbose -ge 2 ]; then
printf "${MAGENTA}Cleanup temporary directory ${GREEN}'%s'${NO_COLOR}\n" \
$TMPDIR
fi
rm -rf $TMPDIR || die "Failed to cleanup temporary directory '%s'\n" "$TMPDIR"
fi
printf "${BLUE}Backup run completed!${NO_COLOR}\n"
# close the read file descriptor
#exec 3>&-
exec 4>&-
if [ $donotify -gt 0 ]; then
if [ "$target_cmdline" != "none" ]; then
if [ -z "$remote" ]; then
notify_info "Finished" "Backup run to target '$target_cmdline' completed!"
else
notify_info "Finished" "Backup run to remote '$remote' target '$target_cmdline' completed!"
fi
elif [ "$uuid_cmdline" != "none" ]; then
if [ -z "$remote" ]; then
notify_info "Finished" "Backup run to uuid '$uuid_cmdline' completed!"
else
notify_info "Finished" "Backup run to remote '$remote' uuid '$uuid_cmdline' completed!"
fi
else
if [ -z "$remote" ]; then
notify_info "Finished" "Backup run to target '$selected_target' completed!"
else
notify_info "Finished" "Backup run to remote '$remote' target '$selected_target' completed!"
fi
fi
fi
if [ $error_count -ge 1 ]; then
return 1
fi