Files
dsnap-sync/bin/dsnap-sync
2025-08-04 10:27:18 +02:00

3409 lines
146 KiB
Bash
Executable File

#!/bin/sh
# dsnap-sync
# https://github.com/rzerres/dsnap-sync
# Copyright (C) 2016, 2017 James W. Barnett
# Copyright (C) 2017 - 2023 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.,
# -------------------------------------------------------------------------
# `dsnap-sync` is designed to backup btrfs formated filesystems. It
# takes advantage of the specific snapshots functionality btrfs offers
# and combines it with managemnet functionality of `snapper`.
# `dsnap-sync` creates backups as btrfs-snapshots on a selectable
# target device. Plug in and mount any btrfs-formatted device to your
# system. Supported targets may be either local attached USB drives,
# automountable RAID devices or LTFS aware tapes. All supported
# targets can be located on a remote host. If possible the backup
# process will send incremental snapshots to the target drive. If the
# snapshot will be stored on a remote host, the transport will be
# secured with ssh.
# The tool is implemented as a posix shell script (dash), to keep the
# footprint small. `dsnap-sync` will support interactive and time
# scheduled backup processes. Scheduling should be implemented as a
# pair of systemd service and timer-units.
progname="${0##*/}"
version="0.6.9"
# global variables
args=
answer=no
archive_type=full
batch=0
btrfs_quota=0
btrfs_quota_tmp=1
btrfs_verbose_flag=
calculate_btrfs_size=0
color=0
donotify=0
dryrun=0
disk_uuid_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_config_dir=/etc/snapper/configs
snapper_config_postfix=
snapper_template_dir=/etc/snapper/config-templates
snapper_template_dsnap_sync="dsnap-sync"
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'
#snap_cleanup_algorithm="timeline"
transfer_size=0
verbose=0
volume_name=
# # ascii color
# BLUE=
# GREEN=
# MAGENTA=
# RED=
# YELLOW=
# NO_COLOR=
###
# functions
###
check_prerequisites () {
# Create TMPDIR
if [ -d "$XDG_RUNTIME_DIR" ]; then
test ! -d "$XDG_RUNTIME_DIR/$progname" && mkdir -p "$XDG_RUNTIME_DIR/$progname"
TMPDIR=$(mktemp --tmpdir="$XDG_RUNTIME_DIR/$progname" -d)
elif [ -d "$TEMP" ]; then
test ! -d "$TEMP/$progname" && mkdir -p "$XDG_RUNTIME_DIR/$progname"
TMPDIR=$(mktemp --tmpdir="$TEMP/$progname" -d)
elif [ -d /var/tmp ]; then
test ! -d "/var/tmp/$progname" && mkdir -p "/var/tmp/$progname"
TMPDIR=$(mktemp --tmpdir="/var/tmp/$progname" -d)
fi
# define fifo pipe
BTRFS_PIPE=$TMPDIR/btrfs.fifo
test -p "$BTRFS_PIPE" && mkfifo "$BTRFS_PIPE"
# requested binaries
command -pv awk >/dev/null 2>&1 || { printf "'awk' is not installed." && exit 1; }
command -pv sed >/dev/null 2>&1 || { printf "'sed' is not installed." && exit 1; }
command -pv ssh >/dev/null 2>&1 || { printf "'ssh' is not installed." && exit 1; }
command -pv scp >/dev/null 2>&1 || { printf "'scp' is not installed." && exit 1; }
command -pv btrfs >/dev/null 2>&1 || { printf "'btrfs' is not installed." && exit 1; }
command -pv findmnt >/dev/null 2>&1 || { printf "'findmnt' is not installed." && exit 1; }
command -pv systemd-cat >/dev/null 2>&1 || { printf "'systemd-cat' is not installed." && exit 1; }
command -pv snapper >/dev/null 2>&1 || { printf "'snapper' is not installed." && exit 1; }
# optional binaries
command -pv attr >/dev/null 2>&1 || { printf "'attr' is not installed." && exit 1; }
command -pv ionice >/dev/null 2>&1 && { do_ionice_cmd=1; }
command -pv notify-send >/dev/null 2>&1 && { donotify=1; }
command -pv pv >/dev/null 2>&1 && { do_pv_cmd=1; }
command -pv tape-admin >/dev/null 2>&1 && { tape_admin_cmd=1; }
command -pv nc >/dev/null 2>&1 && { nc_cmd=1; }
if [ "$(id -u)" -ne 0 ] ; then printf "%s: must be run as root\n", "$progname" ; exit 1 ; fi
# if [ ! -r "$SNAPPER_CONFIG" ]; then
# die "$progname: $SNAPPER_CONFIG does not exist."
# fi
}
check_target_subvol () {
source_snapshot=${1##source_snapshot=}
target_snapshot=${2##target_snapshot=}
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}check_target_subvol() ...${NO_COLOR}\n"
fi
# WIP: check if target subvolume already exists
# batch: keep it and terminate
# interactive: ask if it should be deleted to preceed with an update
}
check_transfer_size () {
source_snapshot=${1##source_snapshot=}
clone_snapshot=${2##clone_snapshot=}
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}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" \
"$source_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
if [ "$verbose" -ge 2 ]; then
printf "${MAGENTA}Temporarily enable btrfs quota for snapshot (source=${GREEN}'%s'${MAGENTA})${NO_COLOR} ...\n" \
"$source_snapshot"
fi
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
}
cleanup_snapper_failed_ids () {
selected_config=${1}
batch=${2:-0}
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}cleanup_snapper_failed_ids() ...${NO_COLOR}\n"
fi
# active, non finished snapshot backups are marked beeing in progress.
# 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 ${GREEN}%s${MAGENTA} previous failed sync runs for ${GREEN}'%s'${NO_COLOR}\n" \
"$snapper_failed_ids" "$selected_config"
answer=no
get_answer_yes_no "Delete failed backup snapshots [y/N]? " "$answer"
fi
if [ "$answer" = "yes" ]; then
failed_id_first=$(snapper --config "$selected_config" list --type single \
| awk -F '|' ' /'"$snap_description_running"'/' \
| awk ' NR==1 {print $1} ')
failed_id_last=$(snapper --config "$selected_config" list --type single \
| awk -F '|' ' /'"$snap_description_running"'/' \
| awk ' END {print $1} ')
cmd="snapper --config $selected_config delete"
if [ "$failed_id_first" -lt "$failed_id_last" ]; then
$(eval "$cmd" "$failed_id_first"-"$failed_id_last")
else
$(eval "$cmd" "$failed_id_first")
fi
fi
fi
}
create_pv_cmd () {
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}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]\" "
if [ $calculate_btrfs_size -eq 1 ]; then
pv_size_option="--size ${transfer_size}"
fi
cmd_pv="pv $pv_size_option $pv_options | "
#cmd_pv="pv $pv_size_option $pv_options | dialog --gauge \"$progname: Progress for config '$selected_config'\" 6 85 |"
else
cmd_pv=''
fi
}
create_snapshot () {
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}create_snapshot() ...${NO_COLOR}\n" "$snapper_config"
fi
# acting on source system
if [ $dryrun -eq 0 ]; then
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, assign negative shapshot-id and return
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 for snapper config ${GREEN}'%s'${NO_COLOR} ...\n" "$selected_config"
snapper_source_id=999999
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 () {
message="${1:-'Do you want to proceed [y/N]? '}"
i="none"
# hack: answer is a global variable, using it for preselection
while [ "$i" = "none" ]; do
printf "%s" "$message"
read -r 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 () {
snapper_config=${1#snapper_config=}
archive_type=${2##archive_type=}
remote=${3##remote=}
run_ssh=''
snapper_sync_id=0
if [ ${#remote} -ge 1 ]; then run_ssh="$ssh"; fi
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}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 () {
backup_dir="$1"
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}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 "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_disk_infos () {
disk_uuid=""
disk_target=""
fs_option=""
if [ "$verbose" -ge 2 ]; 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_media_infos () {
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}get_media_infos() ...${NO_COLOR}\n"
fi
# 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 () {
snapper_config=$1
snapper_config_type='none'
#key=""
#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 2 ]; then
printf "${BLUE}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 and parse ourself
IFS="="
while read -r -p 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 target non btrfs-filesystem
snapper_backup_type=btrfs-archive
snapper_target_config=archive-"$snapper_config"
;;
child|Child)
# clone to target 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 () {
remote_host=${1##remote=}
config_file=${2##snapper_config=}
config_key=${3##config_key=}
run_ssh=''
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}get_snapper_config_value() ...${NO_COLOR}\n"
printf "${MAGENTA}config_file: ${GREEN}'%s'${NO_COLOR}\n" "$config_file"
fi
if [ ${#remote_host} -gt 1 ]; then
run_ssh="$ssh"
fi
if [ "$verbose" -ge 3 ]; then
if [ ${#remote_host} -gt 1 ]; then
printf "Snapper ${GREEN}remote host${NO_COLOR}: '%s'\n" \
"$remote_host"
fi
printf "Snapper ${GREEN}config file${NO_COLOR}: '%s'\n" \
"$config_file"
printf "Snapper key ${GREEN}'%s'${NO_COLOR}: '%s'\n" \
"$config_key" "$value"
fi
value=$(eval "$run_ssh" cat "$config_file" \
| awk '/'"$config_key"'/' \
| awk -F "=" '{ gsub("\"",""); print $2}')
if [ "$verbose" -ge 3 ]; then
printf "Snapper key ${GREEN}'%s'${NO_COLOR}: '%s'\n" \
"$config_key" "$value"
fi
}
get_snapper_last_sync_id () {
snapper_config=${1#snapper_config=}
snapper_description=${2##snapper_description=}
snapper_uuid=${3##snapper_uuid=}
snapper_subvolid=${4##snapper_subvolid=}
snapper_tapeid=${5##snapper_tapeid=}
snapper_backupdir=${6##snapper_backupdir=}
remote_host=${7##remote=}
run_ssh=
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}get_snapper_last_sync_id() ...${NO_COLOR}\n"
fi
snapper_sync_id=0
if [ ${#remote_host} -ge 1 ]; then
run_ssh="$ssh"
fi
# only process, if snapper config file does exist
# return value for 'cmd':
# 0 -> filepath is valid
# 1 -> filepath does not exist
cmd="stat --format %n $snapper_config_dir/$snapper_config 2>/dev/null"
#printf "${MAGENTA}Check for given snapper config ${GREEN}'%s'${MAGENTA} (remote host: ${GREEN}'%s'${MAGENTA}).${NO_COLOR}\n" \
# "$cmd" "$remote_host"
if [ ! $(eval "$run_ssh" "$cmd") ]; then
if [ "$verbose" -ge 3 ]; then
if [ "${#remote_host}" -ge 1 ]; then
printf "${MAGENTA}snapper config ${GREEN}'%s'${MAGENTA} on remote ${GREEN}'%s'${MAGENTA} does not exist yet.${NO_COLOR}\n" \
"$snapper_config" "$remote_host"
else
printf "${MAGENTA}snapper config ${GREEN}'%s'${MAGENTA} on source host does not exist.${NO_COLOR}\n" \
"$snapper_config"
fi
fi
return 1
fi
# filter sync process id
if [ "${#snapper_subvolid}" -ge 1 ] && [ "${#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=/' \
| awk 'END {print \$1}'"
else
cmd="snapper --config $snapper_config list --type single \
| awk '/$snapper_description/' \
| awk 'END {print \$1}'"
fi
if [ "$verbose" -ge 3 ]; then
if [ "${#remote_host}" -ge 1 ]; then
printf "${MAGENTA}remote_host: ${GREEN}'%s'${NO_COLOR}\n" \
"${remote_host}"
fi
printf "${MAGENTA}snapper_config: ${GREEN}'%s'${NO_COLOR}\n" \
"${snapper_config}"
fi
snapper_sync_id=$(eval "$run_ssh" "$cmd")
if [ "$verbose" -ge 4 ]; then
if [ -z "$remote_host" ]; then
printf "${MAGENTA}Last snapper ${GREEN}sync_id${MAGENTA} found on ${GREEN}source${MAGENTA} is ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \
"$snapper_sync_id"
else
printf "${MAGENTA}Last snapper ${GREEN}sync_id${MAGENTA} found on ${GREEN}'%s'${MAGENTA} is ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \
"$remote_host" "$snapper_sync_id"
fi
fi
if [ "${#snapper_sync_id}" -ge 1 ]; then
# ok, matching snapshot found
if [ "$verbose" -ge 3 ]; then
if [ "${#remote_host}" -ge 1 ]; then
printf "${MAGENTA}Get last sync_id for snapper config ${GREEN}'%s'${MAGENTA} on remote ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \
"$snapper_config" "$remote_host"
else
printf "${MAGENTA}Get last sync_id for snapper config ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \
"$snapper_config"
fi
fi
if [ "$SUBVOLUME" = "/" ]; then
SUBVOLUME=""
fi
snapper_sync_snapshot="$SUBVOLUME/.snapshots/$snapper_sync_id/$snapper_snapshot_name"
else
# no snapshot found, identify latest successfull sync saved on source
if [ "${#snapper_subvolid}" -ge 1 ] && [ "${#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
if [ "$verbose" -ge 3 ]; then
printf "${MAGENTA}Search sync_id cmd=${GREEN}'%s'${NO_COLOR}\n" \
"$run_ssh $cmd"
fi
snapper_sync_id=$(eval "$run_ssh" "$cmd")
if [ "$verbose" -ge 2 ]; then
if [ "${#remote_host}" -ge 1 ]; then
printf "${MAGENTA}Matching snapper ${GREEN}sync_id${MAGENTA} found on ${GREEN}'%s'${MAGENTA} is ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \
"$remote_host" "$snapper_sync_id"
else
printf "${MAGENTA}Matching snapper ${GREEN}sync_id${MAGENTA} found on ${GREEN}source${MAGENTA} is ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \
"$snapper_sync_id"
fi
fi
if [ ${#snapper_sync_id} -ge 1 ]; then
# ok, matching snapshot found
if [ "$SUBVOLUME" = "/" ]; then
SUBVOLUME=""
fi
snapper_sync_snapshot="$SUBVOLUME"/.snapshots/"$snapper_sync_id"/"$snapper_snapshot_name"
if [ "$verbose" -ge 3 ]; then
printf "${MAGENTA}snapper_sync_snapshot=${GREEN}'%s'${NO_COLOR}\n" "$snapper_sync_snapshot" \
"$run_ssh $cmd"
fi
else
# no snapshot available
snapper_sync_id=0
fi
fi
}
get_snapper_sync_id () {
snapper_config=${1#snapper_config=}
remote_host=${2##remote=}
run_ssh=''
ret=
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}get_snapper_sync_id() ...${NO_COLOR}\n"
fi
if [ "$verbose" -ge 3 ]; then
if [ ${#remote_host} -ge 1 ]; 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_host} -ge 1 ] && run_ssh="$ssh"
cmd="snapper --config $snapper_config list --type single \
| awk -F '|' ' \$1 == $snapper_sync_id { gsub(/ /,_); print \$1} '"
if [ "$verbose" -ge 3 ]; then
printf "${MAGENTA}Search cmd: '%s'${NO_COLOR}\n" "$cmd"
fi
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
}
get_snapper_target_backupdir () {
backupdir_cmdline=$1
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}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_tape_infos () {
tape_id
tape_target
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
}
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
;;
--calculate-btrfs_size)
calulate_size=1
shift 1
;;
-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
;;
--no-btrfs-quota)
btrfs_quota=0
shift 1
;;
--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
;;
--calculate-btrfs_size=*)
case ${1#*=} in
yes | Yes | True | true)
calculate_btrfs_size=1;
;;
*)
;;
esac
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
cmd="snapper list-configs \
| awk -F '|' 'FNR>2 {print \$1} '"
SNAPPER_CONFIGS=$(eval "$cmd")
#. "$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:-''}
if [ -n "$remote" ]; then
ssh="ssh $remote"
scp="scp"
if [ -n "$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
eval "$ssh" command -pv 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"
printf "Temporary Dir: '%s'\n" "$TMPDIR"
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 [ "$calculate_btrfs_size" -eq 1 ]; then
snap_sync_options="${snap_sync_options} calculate-btrfs-size=true"
else
snap_sync_options="${snap_sync_options} calculate-btrfs-size=false"
fi
if [ "$btrfs_quota" -eq 1 ]; then
snap_sync_options="${snap_sync_options} use-btrfs-quota=true"
else
snap_sync_options="${snap_sync_options} use-btrfs-quota=false"
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"
if [ $? -eq 0 ]; then
# cleanup failed former runs
cleanup_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"
if [ -z "$remote" ]; then
eval "backup_host='localhost'"
else
eval "backup_host='$remote'"
fi
# get latest successfully finished snapshot on source
# WIP: parse 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 synced 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 snapper snapshot-path for source
get_snapper_config_value "remote=" \
"snapper_config=$snapper_config_dir/$selected_config" \
"config_key=SUBVOLUME"
if [ $? -eq 0 ]; then
if [ "$value" = "/" ]; then
snapper_source_sync_snapshot="/.snapshots/$snapper_source_sync_id/$snapper_snapshot_name"
else
snapper_source_sync_snapshot="$value/.snapshots/$snapper_source_sync_id/$snapper_snapshot_name"
fi
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
# get latest successfully finished snapshot on 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=${backup_host}"
;;
*)
get_snapper_last_sync_id "snapper_config=${snapper_target_config}" \
"snapper_description=${snap_description_synced}" \
"snapper_uuid=" "snapper_subvolid=" "snapper_tapeid=" \
"snapper_backupdir=" \
"remote=${backup_host}"
;;
esac
if [ $? -eq 0 ]; then
snapper_target_sync_id="$snapper_sync_id"
else
snapper_target_sync_id=0
fi
# if source and target sync id's do not match:
# check for last common sync id's
if [ "$snapper_target_sync_id" -ne "$snapper_source_sync_id" ]; then
snapper_common_sync_id=0
# 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
else
snapper_common_sync_id=$snapper_source_sync_id
fi
if [ "$snapper_target_sync_id" -eq 0 ]; then
if [ "$verbose" -ge 2 ]; then
if [ -z "$remote" ]; then
printf "${MAGENTA}No synced ${GREEN}target snapshot${MAGENTA} available for snapper config ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \
"$selected_config"
else
printf "${MAGENTA}No synced ${GREEN}target snapshot${MAGENTA} available for snapper config ${GREEN}'%s'${MAGENTA} on '%s' ...${NO_COLOR}\n" \
"$selected_config" "$remote"
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 [ -z "$remote" ]; then
printf "${MAGENTA}backupdir: ${GREEN}'%s'${NO_COLOR}\n" \
"$backupdir"
else
printf "${MAGENTA}backupdir on remote '%s': ${GREEN}'%s'${NO_COLOR}\n" \
"$remote" "$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'"
eval "backup_host_$i='$backup_host'"
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
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 "Performing backup for config ${GREEN}'%s'${NO_COLOR}\n" \
"${selected_config}"
fi
SNAP_SYNC_EXCLUDE=no
if [ -f "/etc/snapper/configs/$selected_config" ]; then
. "$snapper_config_dir/$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)"
cont_backup=\$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)
backup_host=$(eval echo \$backup_host_$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"
printf "${MAGENTA}backup_host: ${GREEN}'%s'${NO_COLOR}\n" "$backup_host"
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
# couldn't create n new snapshot -> terminate with error
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
# send full snapshot to target
if [ $calculate_btrfs_size -eq 1 ]; then
# get size of stream that needs to be transfered
check_transfer_size "source_snapshot=$snapper_source_snapshot"
fi
# 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
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='${GREEN}%s${MAGENTA}', size='${GREEN}%s${MAGENTA}')${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='${GREEN}%s${MAGENTA}')${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
# send incremental snapshot to target, since 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 2 ]; 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
# return value for 'cmd'
# 256 -> valid btrfs snapshot
# '' -> snapshot does not exits
cmd="stat --format %i $snapper_source_snapshot 2>/dev/null"
if [ "$(eval "$cmd")" -eq 256 ]; then
# get size of stream that needs to be transfered
if [ "$calculate_btrfs_size" -eq 1 ]; then
check_transfer_size "source_snapshot=$snapper_source_snapshot" "clone_snapshot=$snapper_common_sync_snapshot"
fi
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
# TODO: handle error if snapshot already exists
# cmd="$ssh stat --format %i $snapper_target_snapshot 2>/dev/null"
# if [ "$(eval "$cmd")" -eq 256 ]; then
# printf "${RED}Error: ${MAGENTA}target snapshot ${GREEN}'%i'${MAGENTA} already exists.${NO_COLOR}\n"
# 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 common 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 "$backup_host" ] && [ "$backup_host" != "localhost" ]; then
if [ "$verbose" -ge 2 ]; then
printf "${MAGENTA}Send snapper source metadata ${GREEN}'%s'${MAGENTA} to target snapshot ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \
"$snapper_source_info" "$snapper_target_snapshot/info.xml"
fi
cp "$snapper_source_info" "$snapper_target_snapshot/info.xml"
else
if [ "$verbose" -ge 2 ]; then
printf "${MAGENTA}Send snapper source metadata ${GREEN}'%s'${MAGENTA} to remote target snapshot ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \
"$snapper_source_info" "$backup_host:$snapper_target_snapshot/info.xml"
fi
cmd="$scp $snapper_source_info root@$backup_host:$snapper_target_snapshot/info.xml"
if [ "$verbose" -ge 2 ]; then
printf "${MAGENTA}cmd: ${GREEN}'%s'${NO_COLOR}\n" \
"$cmd"
fi
$(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 () {
selected_config=${1}
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
cleanup_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 () {
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 "$snapper_config_dir/$selected_config" ]; then
. "$snapper_config_dir/$selected_config"
else
printf "${RED}Error: ${MAGENTA}Selected snapper configuration ${GREEN}'$selected_config'${MAGENTA} does not exist in '$snapper_config_dir'!\n${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
return
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")
backup_host=$(eval echo \$backup_host_"$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.
ii=1
ii_max=20
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 running ${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}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 [ "$backup_host" = "localhost" ]; then
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")
else
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"\')
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 "${MAGENTA}calling: ${GREEN}'%s'${NO_COLOR}\n" "$cmd"
fi
eval "$cmd"
if [ $? -gt 0 ]; then
printf "${RED}ERROR: ${MAGENTA}Updating snapper metadata for source snapshot id ${GREEN}'%s'${NO_COLOR}${MAGENTA} failed.${NO_COLOR}\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 "${MAGENTA}calling: ${GREEN}'%s'${NO_COLOR}\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 () {
i=0
target_id=0
subvolid=''
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 ${GREEN}'localhost'${NO_COLOR}.\n"
else
printf "Selecting a mounted device for backups on ${GREEN}'%s'${NO_COLOR}.\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
if [ "$interactive" -eq 0 ] && [ "$target_count" -gt 1 ] ; then
printf "${RED}Error: ${MAGENTA}Can't determine unique target in batch mode. Got ${GREEN}'%i'${MAGENTA} possible targets.${NO_COLOR}\n" "$target_count"
printf "Please run in ${GREEN}interactive mode${NO_COLOR}, or define unique target ${GREEN}'UUID'${NO_COLOR} or ${GREEN}'Subvolume'${NO_COLOR} via cmdline arguments.\n"
exit 1
fi
# present all mounted BTRFS filesystems
if [ "$verbose" -ge 3 ]; then
printf "${MAGENTA}Found ${GREEN}'%s'${MAGENTA} possible targets.${NO_COLOR}\n" \
"$target_count"
fi
while [ "$target_id" -lt "$target_count" ]; do
# 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
;;
*)
# TODO: implement a timer that will stop exececution, if input isn't given in an appropriate time-frame
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 () {
i=0
disk_id=0
disk_selected_count=0
subvolid=''
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 () {
i=0
tape_id=0
tape_selected_count=0
subvolid=''
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"
printf "Enter a number: "
read -r 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(){
config=${1:-/etc/snapper/config-templates/"$snapper_subvolume_template"}
config_key=${2:-SUBVOLUME}
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 <archive | child | parent>, default: parent
archive: btrfs-snapshot to non btrfs-filesystem
child: clone the btrfs-snapshot to target btrfs-filesystem
parent: disk2disk the btrfs-snapshot to target btrfs-filesystem
--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"
--calculate-btrfs_size Enable calculation of sync-size for given btrfs snapshots
--color Enable colored output messages
-c, --config <config> Specify <multiple> snapper configurations.
Default: Perform snapshots for each available snapper configuration.
(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>
--no-btrfs-quota don't consume btrfs-quota to estimate snapshot size
-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 () {
backup_root=${1##backup_root=}
snapper_config=${2##snapper_target_config=}
snapper_id=${3##snapper_target_sync_id=}
remote_host=${4##remote=}
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}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 "$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 [ "$("$ssh" mkdir --mode=0700 "$backup_root/$snapper_id")" -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 2 ]; then
printf "${BLUE}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 2 ]; then
printf "${BLUE}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
. "$snapper_config_dir/$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"
return
fi
fi
}
verify_snapper_structure () {
backup_root=${1##backup_root=}
snapper_config=${2##snapper_target_config=}
snapper_id=${3##snapper_target_id=}
remote_host=${4##remote=}
if [ "$verbose" -ge 2 ]; then
printf "${BLUE}verify-snapper_structure() ...${NO_COLOR}\n"
fi
if [ "$verbose" -ge 3 ]; then
if [ "$remote_host" ]; then
printf "${MAGENTA}Verify snapper filesystem structure${NO_COLOR} on remote target ${GREEN}'%s'${NO_COLOR}...\n" \
"$remote_host"
else
printf "${MAGENTA}Verify snapper filesystem structure${NO_COLOR} on local 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
# if available: install dsnap-sync default config
if [ -f "$snapper_template_dir/$snapper_template_dsnap_sync" ]; then
cp "$snapper_template/$snapper_template_dsnap-sync" "$snapper_config_dir/$snapper_subvolume_template"
else
printf "${MAGENTA}Missing a snapper template ${GREEN}%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
fi
#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_snapshots"
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 [ -z "$ret" ]; 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
if ! $(eval "$ssh" mkdir --mode=0700 \
"$backup_root/$snapper_snapshots/$snapper_id"); 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
cleanup_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 that needed binaries and structures are available
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
exit 1
fi