3409 lines
146 KiB
Bash
Executable File
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
|