1613 lines
57 KiB
Bash
Executable File
1613 lines
57 KiB
Bash
Executable File
#! /bin/dash
|
|
|
|
# dsnap-sync
|
|
# https://github.com/rzerres/dsnap-sync
|
|
# Copyright (C) 2016, 2017 James W. Barnett
|
|
# Copyright (C) 2017, 2018 Ralf Zerres
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
# -------------------------------------------------------------------------
|
|
|
|
# Takes snapshots of each snapper configuration. It then sends the snapshot to
|
|
# a location on an external drive. After the initial transfer, it does
|
|
# incremental snapshots on later calls. It's important not to delete the
|
|
# snapshot created on your system since that will be used to determine the
|
|
# difference for the next incremental snapshot.
|
|
|
|
progname="${0##*/}"
|
|
version="0.5.5"
|
|
|
|
# The following lines are modified by the Makefile or
|
|
# find_snapper_config script
|
|
#SNAPPER_CONFIG=/etc/conf.d/snapper
|
|
SNAPPER_CONFIG=/etc/default/snapper
|
|
SNAPPER_TEMPLATE_DIR=/etc/snapper/config-templates
|
|
SNAPPER_CONFIG_DIR=/etc/snapper/configs
|
|
|
|
# define fifo pipes
|
|
TMPDIR_PIPE=$(mktemp -d)
|
|
PIPE=$TMPDIR_PIPE/$progname.out
|
|
mkfifo $PIPE
|
|
systemd-cat --identifier="$progname" < $PIPE &
|
|
|
|
BTRFS_PIPE=$TMPDIR_PIPE/btrfs.out
|
|
#mkfifo $BTRFS_PIPE
|
|
#systemd-cat --identifier="btrfs-pipe" < $BTRFS_PIPE &
|
|
|
|
# redirect descriptors to given pipes
|
|
exec 3>$PIPE 4>$BTRFS_PIPE
|
|
|
|
# global variables
|
|
args=
|
|
donotify=0
|
|
answer=no
|
|
disk_count=-1
|
|
disk_uuid_match_count=0
|
|
disk_target_match_count=0
|
|
disk_subvolid_match_count=0
|
|
disk_uuid_match=''
|
|
selected_uuid='none'
|
|
selected_target='none'
|
|
selected_subvol='none'
|
|
snapper_snapshots=".snapshots" # hardcoded in snapper
|
|
snapper_subvolume_template="dsnap-sync"
|
|
snapper_config_type='none'
|
|
#snapper_config_postfix="."`hostname`
|
|
snapper_config_postfix=
|
|
snap_cleanup_algorithm="timeline"
|
|
verbose=0
|
|
|
|
# ascii color
|
|
BLUE=
|
|
GREEN=
|
|
MAGENTA=
|
|
RED=
|
|
YELLOW=
|
|
NO_COLOR=
|
|
|
|
###
|
|
# functions
|
|
###
|
|
|
|
check_prerequisites () {
|
|
|
|
# requested binaries:
|
|
which awk >/dev/null 2>&1 || { printf "'awk' is not installed." && exit 1; }
|
|
which sed >/dev/null 2>&1 || { printf "'sed' is not installed." && exit 1; }
|
|
which tee >/dev/null 2>&1 || { printf "'tee' is not installed." && exit 1; }
|
|
which btrfs >/dev/null 2>&1 || { printf "'btrfs' is not installed." && exit 1; }
|
|
which findmnt >/dev/null 2>&1 || { printf "'findmnt' is not installed." && exit 1; }
|
|
which systemd-cat >/dev/null 2>&1 || { printf "'systemd-cat' is not installed." && exit 1; }
|
|
which snapper >/dev/null 2>&1 || { printf "'snapper' is not installed." && exit 1; }
|
|
which wc >/dev/null 2>&1 || { printf "'wc' is not installed." && exit 1; }
|
|
|
|
# optional binaries:
|
|
which notify-send >/dev/null 2>&1 && { donotify=1; }
|
|
which pv >/dev/null 2>&1 && { do_pv_cmd=true; }
|
|
|
|
if [ $(id -u) -ne 0 ] ; then printf "$progname: must be run as root\n" ; exit 1 ; fi
|
|
|
|
if [ -z "$remote" ]; then
|
|
$ssh which sh >/dev/null 2>&1 || \
|
|
{ printf "'remote shell' is not working!\n \
|
|
Please correct your public authentication and try again.\n" && exit 1; }
|
|
fi
|
|
|
|
if [ ! -r "$SNAPPER_CONFIG" ]; then
|
|
die "$progname: $SNAPPER_CONFIG does not exist."
|
|
fi
|
|
}
|
|
|
|
check_snapper_failed_ids () {
|
|
local batch=${1:-$false}
|
|
|
|
# active, non finished snapshot backups are marked with a reasonable string
|
|
# default: $(snap_description_running -> "$progname backup in progress" (userdata: host=$source)
|
|
snapper_failed_ids=$(eval snapper --config $selected_config list --type single \
|
|
| awk '/'"$snap_description_running"'/ {cnt++} END {print cnt}')
|
|
#snapper_failed_ids="snapper --config $selected_config list --type single \
|
|
# | awk '/'"$snap_description_running"'/' \
|
|
# | awk ' /'host='"$remote"'/ {cnt++} END {print cnt}'"
|
|
|
|
if [ ${#snapper_failed_ids} -gt 0 ]; then
|
|
if [ "$batch" ]; then
|
|
answer="yes"
|
|
else
|
|
#printf "\nNOTE: Found %s previous failed sync runs for '%s'.\n" "${snapper_failed_ids}" "$selected_config" | tee $PIPE
|
|
printf "${MAGENTA}Found %s previous failed sync runs for '%s'${NO_COLOR}\n" "${snapper_failed_ids}" "$selected_config"
|
|
answer=no
|
|
get_answer_yes_no "Delete failed backup snapshots [y/N]? " "$answer"
|
|
fi
|
|
if [ "$answer" = "yes" ]; then
|
|
cmd2="snapper --config \"$selected_config\" list | awk '/'\"$snap_description_running\"'/ {print \$3}'"
|
|
#cmd2="snapper --config \"$selected_config\" list \
|
|
# | awk '/'\"$snap_description_running\"'/' \
|
|
# | awk '/'host='\"$remote\"'/ {print \$3}'"
|
|
cmd="snapper --config \"$selected_config\" delete "
|
|
$(eval $cmd $(eval $cmd2))
|
|
fi
|
|
fi
|
|
}
|
|
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "Verify snapper configuration type for %s...\n" $snapper_config
|
|
fi
|
|
|
|
# WIP -> exit now
|
|
# Patch snapper to parse for new pairs: CONFIG_TYPE="child | root"; CONFIG_PARENT="<string>"
|
|
##config_type=$(eval snapper --config $1 get-config | awk '/'"CONFIG_PARENT=$snap_description_running"'/ {cnt++} END {print cnt}')
|
|
# for now, we cut an parse ourself
|
|
IFS="="
|
|
while read -r key value
|
|
do
|
|
if [ "$key" = "CONFIG_TYPE" ]; then
|
|
snapper_config_type=$(eval echo $value | sed -e 's/\"\(.*\)\"/\1/')
|
|
break
|
|
fi
|
|
done < $SNAPPER_CONFIG_DIR/$snapper_config
|
|
|
|
case $snapper_config_type in
|
|
child|Child)
|
|
# d2d2d backup
|
|
snapper_config_type="child2"
|
|
snapper_target_config=d2d-$snapper_config
|
|
;;
|
|
parent|Parent)
|
|
# d2d backup
|
|
snapper_config_type="parent2disk"
|
|
snapper_target_config=$snapper_config
|
|
;;
|
|
*)
|
|
# d2s backup (default)
|
|
snapper_config_type="parent2disk"
|
|
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 1 ]; then
|
|
printf "Snappper configuration type: '%s'\n" $snapper_config_type
|
|
printf "Snappper target configuration: '%s'\n" $snapper_target_config
|
|
fi
|
|
}
|
|
|
|
die () {
|
|
error "$@"
|
|
exit 1
|
|
}
|
|
|
|
error () {
|
|
printf "\n==> ERROR: %s\n" "$@"
|
|
notify_error 'Error' 'Check journal for more information.'
|
|
} >&2
|
|
|
|
get_answer_yes_no () {
|
|
local message="${1:-'Do you want to proceed [y/N]? '}"
|
|
local i="none"
|
|
|
|
# hack: answer is a global variable, using it for preselection
|
|
while [ "$i" = "none" ]; do
|
|
read -r -p "$message" i
|
|
|
|
case $i in
|
|
y|Y|yes|Yes)
|
|
answer="yes"
|
|
break
|
|
;;
|
|
n|N|no|No)
|
|
answer="no"
|
|
break
|
|
;;
|
|
*)
|
|
if [ -n "$answer" ]; then
|
|
i="$answer"
|
|
else
|
|
i="none"
|
|
printf "Select 'y' or 'n'.\n"
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
get_config () {
|
|
local config=${1:-"$TEMPLATE_DIR/$snapper_snapsync_template"}
|
|
#local config_key=${2:-SUBVOLUME}
|
|
local config_key=${2:-CONFIG_TYPE}
|
|
|
|
IFS="="
|
|
while read -r name value
|
|
do
|
|
if [ "$name" = "$config_key" ]; then
|
|
value="$value"
|
|
key="$value"
|
|
return $true
|
|
break
|
|
fi
|
|
done < $config
|
|
|
|
return $false
|
|
}
|
|
|
|
get_disk_infos () {
|
|
local disk_uuid
|
|
local disk_target
|
|
local fs_option
|
|
|
|
# get mounted BTRFS infos
|
|
if [ "$($ssh findmnt --noheadings --nofsroot --mountpoint / --output FSTYPE)" = "btrfs" ]; then
|
|
# root filesystem is never seen as valid target location
|
|
exclude_uuid=$($ssh findmnt --noheadings --nofsroot --types btrfs --mountpoint / --output UUID)
|
|
disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list | grep -v $exclude_uuid | awk '{print $1}')
|
|
disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list | grep -v $exclude_uuid | awk '{print $2}')
|
|
fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | grep -v $exclude_uuid | awk '{print $2}')
|
|
else
|
|
disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID --list)
|
|
disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output TARGET --list)
|
|
fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | awk '{print $2}')
|
|
fi
|
|
|
|
# we need at least one target disk
|
|
if [ ${#disk_targets} -eq 0 ]; then die "no suitable target disk found"; fi
|
|
|
|
# Posix Shells do not support Array. Therefore using ...
|
|
# Pseudo-Arrays (assumption: equal number of members)
|
|
# Pseudo-Array: disk_uuid_$i
|
|
# Pseudo-Array: disk_target_$i
|
|
# Pseudo-Array: fs_options_$i
|
|
# Pseudo-Array: disk_selected_$i (reference to $i element)
|
|
# List: disk_uuid_match (reference to matching preselected uuids)
|
|
# List: disk_target_match (reference to matching preselected targets)
|
|
|
|
# initialize our structures
|
|
i=0
|
|
for disk_uuid in $disk_uuids; do
|
|
if [ "$disk_uuid" = "$uuid_cmdline" ]; then
|
|
if [ ${#disk_uuid_match} -gt 0 ]; then
|
|
disk_uuid_match="${disk_uuid_match} $i"
|
|
else
|
|
disk_uuid_match="$i"
|
|
fi
|
|
disk_uuid_match_count=$(($disk_uuid_match_count+1))
|
|
fi
|
|
eval "disk_uuid_$i='$disk_uuid'"
|
|
disk_count=$(($disk_count+1))
|
|
i=$((i+1))
|
|
done
|
|
i=0
|
|
for disk_target in $disk_targets; do
|
|
if [ "$disk_target" = "$target_cmdline" ]; then
|
|
disk_target_match="$i"
|
|
disk_target_match_count=$(($disk_target_match_count+1))
|
|
fi
|
|
eval "disk_target_$i='$disk_target'"
|
|
i=$((i+1))
|
|
done
|
|
i=0
|
|
for fs_option in $fs_options; do
|
|
subvolid=$(eval echo \$fs_option | sed -e 's/.*subvolid=\([0-9]*\).*/\1/')
|
|
if [ "$subvolid" = "$subvolid_cmdline" ]; then
|
|
disk_subvolid_match="$i"
|
|
disk_subvolid_match_count=$(($disk_subvolid_match_count+1))
|
|
fi
|
|
eval "fs_options_$i='$fs_option'"
|
|
i=$((i+1))
|
|
done
|
|
}
|
|
|
|
|
|
notify () {
|
|
# estimation: batch calls should just log
|
|
if [ $donotify -gt 0 ]; then
|
|
for u in $(users | sed 's/ /\n/' | sort -u); do
|
|
sudo -u $u DISPLAY=:0 \
|
|
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(sudo -u $u id -u)/bus \
|
|
notify-send -a $progname "$progname: $1" "$2" --icon="dialog-$3"
|
|
done
|
|
else
|
|
printf "%s\n" "$2"
|
|
fi
|
|
}
|
|
|
|
notify_error () {
|
|
notify "$1" "$2" "error"
|
|
}
|
|
|
|
notify_info () {
|
|
notify "$1" "$2" "information"
|
|
}
|
|
|
|
parse_params () {
|
|
#printf "\n${BLUE}Parse arguments...${NO_COLOR}\n"
|
|
|
|
# Evaluate given call parameters
|
|
while [ $# -gt 0 ]; do
|
|
key="$1"
|
|
case $key in
|
|
-h | --help | \-\? | --usage)
|
|
# Call usage() function.
|
|
usage
|
|
;;
|
|
-b|--backupdir)
|
|
backupdir_cmdline="$2"
|
|
shift 2
|
|
;;
|
|
-c|--config)
|
|
if [ ${#selected_config} -gt 0 ]; then
|
|
selected_configs="${selected_configs} ${2}"
|
|
else
|
|
selected_configs="$2"
|
|
fi
|
|
shift 2
|
|
;;
|
|
--config-postfix)
|
|
snapper_config_postfix="$2"
|
|
shift 2
|
|
;;
|
|
--description-finished)
|
|
shift
|
|
snap_description_finished="${*}"
|
|
snap_description_finished="${snap_description_finished%% -*}"
|
|
params=$*
|
|
set -- $snap_description_finished
|
|
count=$#
|
|
set -- $params
|
|
shift $count
|
|
;;
|
|
--description-running)
|
|
shift
|
|
snap_description_running=${*}
|
|
snap_description_running="${snap_description_running%% -*}"
|
|
params=$*
|
|
set -- $snap_description_running
|
|
count=$#
|
|
set -- $params
|
|
shift $count
|
|
;;
|
|
-d|--description|--description-synced)
|
|
shift
|
|
snap_description_synced="${*}"
|
|
snap_description_synced="${snap_description_synced%% -*}"
|
|
params=$*
|
|
set -- $snap_description_synced
|
|
count=$#
|
|
set -- $params
|
|
shift $count
|
|
;;
|
|
--dry-run|--dryrun)
|
|
dryrun=1
|
|
shift 1
|
|
;;
|
|
-i|--interactive)
|
|
interactive=1
|
|
donotify=1
|
|
shift
|
|
;;
|
|
-n|--noconfirm|--batch)
|
|
batch=1
|
|
do_pv_cmd=false
|
|
donotify=0
|
|
shift
|
|
;;
|
|
--color)
|
|
color=1
|
|
shift 1
|
|
;;
|
|
--nonotify)
|
|
donotify=0
|
|
shift 1
|
|
;;
|
|
--nopv)
|
|
do_pv_cmd=false
|
|
shift 1
|
|
;;
|
|
-p|--port)
|
|
port=$2
|
|
shift 2
|
|
;;
|
|
--remote)
|
|
remote=$2
|
|
shift 2
|
|
;;
|
|
-s|--subvolid|--SUBVOLID)
|
|
subvolid_cmdline="$2"
|
|
shift 2
|
|
;;
|
|
-t|--target|--TARGET)
|
|
target_cmdline="$2"
|
|
shift 2
|
|
;;
|
|
-u|--uuid|--UUID)
|
|
uuid_cmdline="$2"
|
|
shift 2
|
|
;;
|
|
-v|--verbose)
|
|
verbose=$(($verbose + 1))
|
|
shift 1
|
|
;;
|
|
--version)
|
|
printf "%s v%s\n" "$progname" "$version"
|
|
exit 0
|
|
;;
|
|
--backupdir=*)
|
|
backupdir_cmdline=${1#*=}
|
|
shift
|
|
;;
|
|
--config=*)
|
|
if [ ${#selected_config} -gt 0 ]; then
|
|
selected_config="${selected_config} ${1#*=}"
|
|
else
|
|
selected_config="${1#*=}"
|
|
fi
|
|
shift
|
|
;;
|
|
--config-postfix=*)
|
|
snapper_config_postfix="${1#*=}"
|
|
shift
|
|
;;
|
|
--color=*)
|
|
case ${1#*=} in
|
|
yes | Yes | True | true)
|
|
color=1;
|
|
;;
|
|
*)
|
|
;;
|
|
esac
|
|
shift
|
|
;;
|
|
--description-finished=*)
|
|
snap_description_finished="${*#*=}"
|
|
snap_description_finished="${snap_description_finished%% -*}"
|
|
params_new=${*#*=}
|
|
params_new=${params_new##${snap_description_finished}}
|
|
if [ ${#params_new} -gt 0 ]; then
|
|
set -- $snap_description_finished
|
|
count=$#
|
|
set -- $params_new
|
|
fi
|
|
;;
|
|
--description-running=*)
|
|
snap_description_running="${*#*=}"
|
|
snap_description_running="${snap_description_running%% -*}"
|
|
params_new=${*#*=}
|
|
params_new=${params_new##${snap_description_running}}
|
|
params=$#
|
|
if [ ${#params_new} -gt 0 ]; then
|
|
set -- $snap_description_running
|
|
count=$#
|
|
set -- $params_new
|
|
fi
|
|
;;
|
|
-d=*|--description=*|--description-synced=*)
|
|
snap_description_synced="${*#*=}"
|
|
snap_description_synced="${snap_description_synced%% -*}"
|
|
params_new=${*#*=}
|
|
params_new=${params_new##${snap_description_synced}}
|
|
if [ ${#params_new} -gt 0 ]; then
|
|
set -- $snap_description_synced
|
|
count=$#
|
|
set -- $params_new
|
|
else
|
|
break
|
|
fi
|
|
;;
|
|
--port=*)
|
|
port=${1#*=}
|
|
shift
|
|
;;
|
|
--remote=*)
|
|
remote=${1#*=}
|
|
shift
|
|
;;
|
|
--subvolid=*|--SUBVOLID=*)
|
|
subvolid_cmdline=${1#*=}
|
|
shift
|
|
;;
|
|
--target=*|--TARGET=*)
|
|
target_cmdline=${1#*=}
|
|
shift
|
|
;;
|
|
--uuid=*|--UUID=*)
|
|
uuid_cmdline=${1#*=}
|
|
shift
|
|
;;
|
|
--) # End of all options
|
|
shift
|
|
break
|
|
;;
|
|
-*)
|
|
printf "WARN: Unknown option (ignored): $1" >&2
|
|
die "Unknown option"
|
|
;;
|
|
*)
|
|
printf "Unknown option: %s\nRun '%s -h' for valid options.\n" $key $progname
|
|
die "Unknown option"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Set reasonable defaults
|
|
. $SNAPPER_CONFIG
|
|
selected_configs=${selected_configs:-$SNAPPER_CONFIGS}
|
|
|
|
# message-text used for snapper fields
|
|
snap_description_finished=${snap_description_finished:-"dsnap-sync backup"}
|
|
snap_description_running=${snap_description_running:-"dsnap-sync in progress"}
|
|
snap_description_synced=${snap_description_synced:-"dsnap-sync last incremental"}
|
|
|
|
uuid_cmdline=${uuid_cmdline:-"none"}
|
|
target_cmdline=${target_cmdline:-"none"}
|
|
subvolid_cmdline=${subvolid_cmdline:-"none"}
|
|
backupdir_cmdline=${backupdir_cmdline:-"none"}
|
|
|
|
if [ -z "$remote" ]; then
|
|
ssh=""
|
|
else
|
|
ssh="ssh $remote"
|
|
if [ ! -z "$port" ]; then
|
|
ssh="$ssh -p $port"
|
|
fi
|
|
fi
|
|
|
|
if [ "$color" ]; then
|
|
# ascii color
|
|
BLUE='\033[0;34m'
|
|
GREEN='\033[0;32m'
|
|
MAGENTA='\033[0;35m'
|
|
RED='\033[0;31m'
|
|
YELLOW='\033[0;33m'
|
|
NO_COLOR='\033[0m'
|
|
fi
|
|
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "${BLUE}$progname (runtime arguments)...${NO_COLOR}\n"
|
|
printf "for backup-source:\n"
|
|
printf " selected configs: '%s'\n" "$selected_configs"
|
|
printf "for backup-target:\n"
|
|
printf " disk UUID: '%s'\n" "$uuid_cmdline"
|
|
printf " disk TARGET: '%s'\n" "$target_cmdline"
|
|
printf " disk SUBVOLID: '%s'\n" "$subvolid_cmdline"
|
|
printf " disk Backupdir: '%s'\n" "$backupdir_cmdline"
|
|
printf " config postfix: '%s'\n" "$snapper_config_postfix"
|
|
printf " remote host: '%s'\n" "$remote"
|
|
printf " ssh options: '%s'\n" "$ssh"
|
|
|
|
printf "Snapper Descriptions\n"
|
|
printf " backup finished: '%s'\n" "$snap_description_finished"
|
|
printf " backup running: '%s'\n" "$snap_description_running"
|
|
printf " backup synced: '%s'\n" "$snap_description_synced"
|
|
|
|
if [ $verbose -ge 1 ]; then snap_sync_options="verbose_level=$verbose"; fi
|
|
if [ "$dryrun" ]; then snap_sync_options="${snap_sync_options} dry-run=true"; fi
|
|
if [ "$nonotify" ]; then snap_sync_options="${snap_sync_options} donotify=0"; fi
|
|
if [ "$color" ]; then snap_sync_options="${snap_sync_options} color=true"; fi
|
|
if [ "$batch" ]; then
|
|
snap_sync_options="${snap_sync_options} batch=true do_pv_cmd=false"
|
|
else
|
|
snap_sync_options="${snap_sync_options} do_pv_cmd=$do_pv_cmd"
|
|
fi
|
|
if [ "$interactive" ]; then snap_sync_options="${snap_sync_options} interactive=true batch=false"; fi
|
|
printf "Options: '%s'\n\n" "${snap_sync_options}"
|
|
fi
|
|
}
|
|
|
|
run_config () {
|
|
printf "${BLUE}Verify configuration...${NO_COLOR}\n"
|
|
|
|
SNAP_SYNC_EXCLUDE=no
|
|
# loop though selected snapper configurations
|
|
# Pseudo Arrays $i -> store associated elements of selected_config
|
|
i=0
|
|
for selected_config in $selected_configs; do
|
|
# only process existing dsnap-sync configurations
|
|
if [ ! -f "/etc/snapper/configs/$selected_config" ]; then
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "Did you forget to create the snapper configuration for config '%s' on source?\n" \
|
|
"$selected_config"
|
|
printf "You can create it with following command:\n"
|
|
printf "${MAGENTA}snapper --config ${GREEN}%s${MAGENTA} create-config --template ${GREEN}%s${MAGENTA} <btrfs-subvolume-path>${NO_COLOR}\n" \
|
|
"$selected_config" "$snapper_subvolume_template"
|
|
fi
|
|
die "Can't backup snapper configuration '$selected_config' that does not exist."
|
|
else
|
|
. /etc/snapper/configs/$selected_config
|
|
if [ "$SUBVOLUME" = "/" ]; then
|
|
SUBVOLUME=''
|
|
fi
|
|
count=$(eval snapper --config $selected_config list --type single | \
|
|
awk '/'"$snap_description_synced"'/' | \
|
|
awk '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {cnt++} END {print cnt}')
|
|
if [ -n "$count" ] && [ "$count" -gt 1 ]; then
|
|
printf "%s entries are marked as '%s' for snapper config '%s'\n" \
|
|
"$count" "$snap_description_synced" "$selected_config"
|
|
printf "Pointing to target with UUID '%s' and SUBVOLID '%s'. Skipping configuration '%s'.\n" \
|
|
"$selected_uuid" "$selected_subvol" "$selected_config"
|
|
printf "Please cleanup for further processing.\n"
|
|
error "Skipping configuration $selected_config."
|
|
selected_configs=$(echo $selected_configs | sed -e "s/\($selected_config\)//")
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
# cleanup failed former runs
|
|
check_snapper_failed_ids $batch
|
|
|
|
if [ $SNAP_SYNC_EXCLUDE = "yes" ]; then
|
|
continue
|
|
fi
|
|
|
|
# get latest successfully finished snapshot
|
|
# (tagged with userdata key/value pairs)
|
|
snapper_sync_id=$(eval snapper --config "$selected_config" list --type single | \
|
|
awk '/'"$snap_description_synced"'/' | \
|
|
awk '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {print $1}')
|
|
if [ ${#snapper_sync_id} -gt 0 ]; then
|
|
snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot
|
|
fi
|
|
|
|
eval "snapper_sync_id_$i='$snapper_sync_id'"
|
|
eval "snapper_sync_snapshot_$i='$snapper_sync_snapshot'"
|
|
|
|
# verify backupdir
|
|
if [ -z "$snapper_sync_id" ]; then
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "No backups have been performed for snapper config '%s' on target disk.\n" \
|
|
"$selected_config"
|
|
fi
|
|
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
|
|
else
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "Last syncronized Snapshot-ID for '%s': %s\n" "$selected_config" "$snapper_sync_id"
|
|
printf "Last syncronized Snapshot-Path for '%s': %s\n" "$selected_config" "$snapper_sync_snapshot"
|
|
fi
|
|
|
|
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 [ "$interactive" ]; then
|
|
if [ -z "$backupdir"]; then
|
|
answer=yes
|
|
get_answer_yes_no "Keep empty backupdir [Y/n]? " "$answer"
|
|
else
|
|
get_answer_yes_no "Keep backupdir '$backupdir' [Y/n]? " "$answer"
|
|
fi
|
|
if [ "$answer" = "no" ]; then
|
|
read -r -p "Enter name of directory to store backups, relative to $selected_target (to be created if not existing): " backupdir
|
|
if [ -z "$backupdir" ]; then
|
|
backup_root="$selected_target"
|
|
else
|
|
backup_root="$selected_target/$backupdir"
|
|
fi
|
|
fi
|
|
fi
|
|
if [ -z "$backupdir" ]; then
|
|
backup_root="$selected_target"
|
|
else
|
|
backup_root="$selected_target/$backupdir"
|
|
fi
|
|
fi
|
|
|
|
eval "backup_root_$i=$backup_root"
|
|
eval "backup_dir_$i=$backupdir"
|
|
|
|
if [ $verbose -ge 1 ]; then
|
|
if [ -n "$remote" ];then
|
|
printf "Backup-Path on remote %s: %s\n" "$remote" "$backup_root"
|
|
else
|
|
printf "Backup-Path: %s\n" "$backup_root"
|
|
fi
|
|
fi
|
|
|
|
run_snapshot
|
|
i=$(($i+1))
|
|
|
|
done
|
|
}
|
|
|
|
run_snapshot () {
|
|
printf "${BLUE}Prepare snapshot...${NO_COLOR}\n"
|
|
|
|
# acting on source system
|
|
if [ ! $dryrun ]; then
|
|
#printf "Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "Creating new snapshot with snapper config '%s' ...\n" "$selected_config"
|
|
fi
|
|
snapper_new_id=$(snapper --config "$selected_config" create --print-number --description "$snap_description_running" --userdata "host=$remote")
|
|
snapper_new_snapshot=$SUBVOLUME/.snapshots/$snapper_new_id/snapshot
|
|
snapper_new_info=$SUBVOLUME/.snapshots/$snapper_new_id/info.xml
|
|
sync
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "Snapper snapshot %s created\n" "$snapper_new_id"
|
|
answer=yes
|
|
get_answer_yes_no "Continue [Y/n]? " "$answer"
|
|
if [ "$answer" = "no" ]; then
|
|
die "Exit on user request."
|
|
fi
|
|
fi
|
|
else
|
|
#printf "dryrun: Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE
|
|
printf "dryrun: Creating new snapshot with snapper config '%s' ...\n" "$selected_config"
|
|
snapper_new_id="<new_snapper_id>"
|
|
fi
|
|
|
|
# Snapshot types: d2d and cloning
|
|
# d2d: 1st stage snapshots: CHILD_CONFIG="false" or missing
|
|
# clone: 2nd stage snapshots: CHILD_CONFIG="true" PARENT_CONFIG="<snapper config name>"
|
|
# parse selected_config and return with $snapper_target_config set appropriately
|
|
set_snapper_target_config $selected_config
|
|
|
|
# if we want 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_snapshot=$backup_root/$snapper_target_config/.snapshots/$snapper_new_id
|
|
if [ $verbose -ge 1 ]; then
|
|
if [ -z "$remote" ]; then
|
|
printf "Will backup '%s' to '%s'\n" "$snapper_new_snapshot" "$snapper_target_snapshot/snapshot"
|
|
else
|
|
printf "Will backup '%s' to '%s'\n" "$snapper_new_snapshot" "$remote":"$snapper_target_snapshot/snapshot"
|
|
fi
|
|
fi
|
|
|
|
# save in config specific infos in pseudo Arrays
|
|
eval "snapper_new_id_$i='$snapper_new_id'"
|
|
eval "snapper_new_snapshot_$i='$snapper_new_snapshot'"
|
|
eval "snapper_new_info_$i='$snapper_new_info'"
|
|
eval "snapper_sync_snapshot_$i='$snapper_sync_snapshot'"
|
|
eval "snapper_config_$i='$selected_config'"
|
|
eval "snapper_target_config_$i='$snapper_target_config'"
|
|
eval "snapper_target_snapshot_$i='$snapper_target_snapshot'"
|
|
|
|
cont_backup="K"
|
|
eval "snapper_activate_$i=yes"
|
|
if [ $batch ]; then
|
|
cont_backup="yes"
|
|
else
|
|
answer=yes
|
|
get_answer_yes_no "Continue with backup [Y/n]? " "$answer"
|
|
if [ "$answer" = "no" ]; then
|
|
eval "snapper_activate_$i=no"
|
|
printf "Aborting backup for this configuration.\n"
|
|
snapper --config $selected_config delete $snapper_new_id
|
|
fi
|
|
fi
|
|
|
|
}
|
|
|
|
run_cleanup () {
|
|
batch="1"
|
|
|
|
# cleanup failed runs
|
|
check_snapper_failed_ids "$batch"
|
|
|
|
# cleanup target
|
|
#$ssh btrfs subvolume delete $backup_root/$snapper_target_config/$snapper_snapshots/$snapper_new_id/snapshot
|
|
#$ssh rm -rf $backup_root/$snapper_target_config/$snapper_snapshots/$snapper_new_id
|
|
|
|
# cleanup TEMPDIR
|
|
if [ -d $TMPDIR_PIPE ]; then
|
|
rm -rf $TMPDIR_PIPE || die "Failed to cleanup temporary directory '%s'\n" "$TMPDIR_PIPE"
|
|
fi
|
|
}
|
|
|
|
run_backup () {
|
|
printf "${BLUE}Performing backups...${NO_COLOR}\n"
|
|
|
|
i=-1
|
|
for selected_config in $selected_configs; do
|
|
|
|
i=$(($i+1))
|
|
|
|
SNAP_SYNC_EXCLUDE=no
|
|
|
|
if [ -f "/etc/snapper/configs/$selected_config" ]; then
|
|
. /etc/snapper/configs/$selected_config
|
|
else
|
|
die "Selected snapper configuration '$selected_config' does not exist."
|
|
fi
|
|
|
|
cont_backup=$(eval echo \$snapper_activate_$i)
|
|
if [ "$cont_backup" = "no" ] || [ "$SNAP_SYNC_EXCLUDE" = "yes" ]; then
|
|
if [ $donotify -gt 0 ]; then
|
|
notify_info "Backup in progress" "NOTE: Skipping '$selected_config' configuration."
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
notify_info "Backup in progress" "Backing up data for configuration '$selected_config'."
|
|
|
|
# retrieve config specific infos from pseudo Arrays
|
|
snapper_config=$(eval echo \$snapper_config_$i)
|
|
backup_root=$(eval echo \$backup_root_$i)
|
|
backup_dir=$(eval echo \$backup_dir_$i)
|
|
snapper_sync_id=$(eval echo \$snapper_sync_id_$i)
|
|
snapper_new_id=$(eval echo \$snapper_new_id_$i)
|
|
snapper_sync_snapshot=$(eval echo \$snapper_sync_snapshot_$i)
|
|
snapper_new_snapshot=$(eval echo \$snapper_new_snapshot_$i)
|
|
snapper_new_info=$(eval echo \$snapper_new_info_$i)
|
|
snapper_target_config=$(eval echo \$snapper_target_config_$i)
|
|
snapper_target_snapshot=$(eval echo \$snapper_target_snapshot_$i)
|
|
|
|
# create the needed snapper structure on the target
|
|
verify_snapper_structure "backup_root=$backup_root" "snapper_target_config=$snapper_target_config" "snapper_new_id=$snapper_new_id" "remote=$remote"
|
|
|
|
# TODO: to report correct values btrfs-quota must be active for the source subvol!
|
|
snapper_target_snapshot_size=$(du --sum $snapper_new_snapshot 2>/dev/null | awk -F ' ' '{print $1}')
|
|
|
|
# settings for interactive progress status
|
|
if [ "$do_pv_cmd" = "true" ]; then
|
|
pv_options="--delay-start 2 --interval 5 --timer --rate --bytes --fineta --no-splice --buffer-percent --progress"
|
|
cmd_pv="pv $pv_options --size $snapper_target_snapshot_size |"
|
|
#cmd_pv="pv $pv_options --size $snapper_target_snapshot_size | dialog --gauge \"$progname: Progress for config '$selected_config'\" 6 85 |"
|
|
else
|
|
cmd_pv=''
|
|
fi
|
|
|
|
if [ -z "$snapper_sync_id" ]; then
|
|
# target never received any snapshot before
|
|
cmd="btrfs send $verbose_flag $snapper_new_snapshot 2>$BTRFS_PIPE | $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>$BTRFS_PIPE"
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "${MAGENTA}Sending first snapshot${NO_COLOR} for snapper config ${MAGENTA}'%s' (size=%s)${NO_COLOR} ...\n" "$selected_config" "$snapper_target_snapshot_size"
|
|
fi
|
|
if [ ! "$dryrun" ]; then
|
|
# the actual data sync to the target
|
|
# this may take a while, depending on datasize and line-speed
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "cmd: '%s'\n" "$cmd"
|
|
fi
|
|
$(eval $cmd) 1>/dev/null
|
|
if [ "$?" -gt 0 ]; then
|
|
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}" "$(eval cat $BTRFS_PIPE)"
|
|
die "btrfs pipe error."
|
|
fi
|
|
else
|
|
printf "dryrun: %s\n" "$cmd"
|
|
fi
|
|
else
|
|
# target holds synced snapshots
|
|
# checking if parent snapshot-id (as saved on source) is also available on target
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "${MAGENTA}Sending incremental snapshot${NO_COLOR} for snapper config ${MAGENTA}'%s'${NO_COLOR} ...\n" "$selected_config"
|
|
fi
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "Old synced snapshot: '%s' (id: %s)\n" "$snapper_sync_snapshot" "$snapper_sync_id"
|
|
printf "New source snapshot: '%s' (id: %s)\n" "$snapper_new_snapshot" "$snapper_new_id"
|
|
printf "New target snapshot: '%s' (id: %s)\n" "$snapper_target_snapshot/snapshot" "$snapper_new_id"
|
|
fi
|
|
|
|
cmd="$ssh stat --format %i $backup_root/$snapper_target_config/$snapper_snapshots/$snapper_source_sync_id 2>/dev/null"
|
|
ret=$(eval $cmd)
|
|
if [ $? -eq 0 ]; then
|
|
# Sends the difference between the new snapshot and old synced snapshot to the
|
|
# backup location. Using the -c (clone-source) flag instead of -p (parent) tells it
|
|
# that there is an identical subvolume to the synced snapshot at the receiving
|
|
# location where it can get its data. This helps speed up the transfer.
|
|
cmd="btrfs send $verbose_flag -c $snapper_sync_snapshot $snapper_new_snapshot 2>$BTRFS_PIPE | $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>$BTRFS_PIPE"
|
|
if [ ! "$dryrun" ]; then
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "${GREEN}btrfs-send${NO_COLOR} will use snapshot ${GREEN}'%s' on target${NO_COLOR} to sync metadata for %s ...\n" "$snapper_sync_snapshot" "$snapper_new_snapshot"
|
|
printf "cmd: '%s'\n" "$cmd"
|
|
fi
|
|
eval $cmd 1>/dev/null
|
|
if [ "$?" -gt 0 ]; then
|
|
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}" "$(eval cat $BTRFS_PIPE)"
|
|
die "btrfs pipe error."
|
|
fi
|
|
else
|
|
printf "dryrun: '%s'" "cmd"
|
|
#printf "dryrun: btrfs send %s -c %s %s | %s btrfs receive %s %s\n" \
|
|
# "$verbose_flag" "$snapper_sync_snapshot" "$snapper_new_snapshot" \
|
|
# "$remote" "$verbose_flag" "$snapper_target_snapshot"
|
|
printf "dryrun: snapper --config %s delete %s\n" "$selected_config" "$snapper_sync_id"
|
|
fi
|
|
else
|
|
# need to use source snapshot to provide metadata for target
|
|
cmd="btrfs send $verbose_flag -p $snapper_sync_snapshot snapper_new_snapshot 2>$BTRFS_PIPE | $cmd_pv $ssh btrfs receive $verbose_flag $snapper_target_snapshot 2>BTRFS_PIPE"
|
|
if [ ! "$dryrun" ]; then
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "${GREEN}btrfs-send${NO_COLOR} is using snapshot ${GREEN}'%s' from source${NO_COLOR} to read metadata ...\n" "$snapper_sync_snapshot"
|
|
printf "cmd: '%s'\n" "$cmd"
|
|
fi
|
|
$(eval $cmd)
|
|
if [ "$?" -gt 0 ]; then
|
|
printf "${RED}BTRFS_PIPE: %s${NO_COLOR}" "$(eval cat $BTRFS_PIPE)"
|
|
die "btrfs pipe error."
|
|
fi
|
|
#printf "btrfs returns: '%i'\n" "$ret"
|
|
else
|
|
printf "dryrun: Would run btrfs-send / btrfs-recieve\n"
|
|
#printf "dryrun: btrfs send %s -p %s %s | %s %s btrfs receive %s %s\n" \
|
|
# "$verbose_flag" "$snapper_sync_snapshot" "$snapper_new_snapshot" \
|
|
# "$cmd_pv" "$ssh" \
|
|
# "$remote" "$verbose_flag" "$snapper_target_snapshot"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# finally: send the snapper info metadata
|
|
if [ -z "$remote" ]; then
|
|
if [ ! "$dryrun" ]; then
|
|
cp "$snapper_new_info" "$snapper_target_snapshot"
|
|
else
|
|
cmd="cp $snapper_new_info $snapper_target_snapshot"
|
|
printf "dryrun: %s\n" "$cmd"
|
|
fi
|
|
else
|
|
if [ ! "$dryrun" ]; then
|
|
if [ -n "$port" ]; then
|
|
rsync -avzq -e "ssh -p $port" "$snapper_new_info" "$remote:$snapper_target_snapshot"
|
|
else
|
|
rsync -avzq "$snapper_new_info" "$remote":"$snapper_target_snapshot"
|
|
fi
|
|
else
|
|
if [ -n "$port" ]; then
|
|
cmd="rsync -avzq -e \"ssh -p $port\" $snapper_new_info $remote:$snapper_target_snapshot"
|
|
else
|
|
cmd="rsync -avzq $snapper_new_info $remote:$snapper_target_snapshot"
|
|
fi
|
|
printf "dryrun: %s\n" "$cmd"
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
run_finalize () {
|
|
# Actual backing up
|
|
printf "${BLUE}Finalize backups...${NO_COLOR}\n"
|
|
|
|
i=-1
|
|
for selected_config in $selected_configs; do
|
|
|
|
i=$(($i+1))
|
|
|
|
SNAP_SYNC_EXCLUDE=no
|
|
|
|
if [ -f "/etc/snapper/configs/$selected_config" ]; then
|
|
. /etc/snapper/configs/$selected_config
|
|
else
|
|
die "Selected snapper configuration '$selected_config' does not exist."
|
|
fi
|
|
|
|
cont_backup=$(eval echo \$snapper_activate_$i)
|
|
if [ "$cont_backup" = "no" ] || [ "$SNAP_SYNC_EXCLUDE" = "yes" ]; then
|
|
if [ $donotify -gt 0 ]; then
|
|
notify_info "Finalize backup" "NOTE: Skipping '$selected_config' configuration."
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
if [ $donotify -gt 0 ]; then
|
|
notify_info "Finalize backup" "Cleanup tasks for configuration '$selected_config'."
|
|
fi
|
|
|
|
# retrieve config specific infos from pseudo Arrays
|
|
snapper_config=$(eval echo \$snapper_config_$i)
|
|
backup_root=$(eval echo \$backup_root_$i)
|
|
backup_dir=$(eval echo \$backup_dir_$i)
|
|
snapper_sync_id=$(eval echo \$snapper_sync_id_$i)
|
|
snapper_new_id=$(eval echo \$snapper_new_id_$i)
|
|
snapper_sync_snapshot=$(eval echo \$snapper_sync_snapshot_$i)
|
|
snapper_new_snapshot=$(eval echo \$snapper_new_snapshot_$i)
|
|
snapper_new_info=$(eval echo \$snapper_new_info_$i)
|
|
snapper_target_config=$(eval echo \$snapper_target_config_$i)
|
|
snapper_target_snapshot=$(eval echo \$snapper_target_snapshot_$i)
|
|
|
|
# It's important not to change the values of the snapper key/value pairs ($userdata)
|
|
# which is stored in snappers info.xml file of the source snapshot.
|
|
# This is how we find the parent.
|
|
|
|
src_host=$(eval cat /etc/hostname)
|
|
src_uuid=$(eval findmnt --noheadings --output UUID --target $SUBVOLUME)
|
|
src_subvolid=$(eval findmnt --noheadings --output OPTIONS $SUBVOLUME | sed -e 's/.*subvolid=\([0-9]*\).*/\1/')
|
|
|
|
# Tag new snapshots key/value parameter
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "${MAGENTA}Tagging target ...${NO_COLOR}\n"
|
|
fi
|
|
if [ ! "$dryrun" ]; then
|
|
# target snapshot
|
|
# 1) wait for target snapshot to show up in snapper list
|
|
# find "$snapper_new_id" -> marked as "$snap_descrition_running"
|
|
# 2) toggle metadata -> mark as "$snap_description_finished", reference "$target_userdata" (subvolid, uuid, host)
|
|
|
|
# !!! ugly hack !!!: wait for snapper to list target snapshot in database.
|
|
# Problem: how to trigger that database is synced? -> a feature request is send to snapper upstream source
|
|
# Solution: 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)
|
|
local ii=1
|
|
local ii_max=20
|
|
local ii_sleep=15
|
|
|
|
# Solution2: kill running snapperd
|
|
# -> will restart and sync
|
|
$(eval $ssh killall -SIGTERM snapperd)
|
|
#printf "Killall '%s'\n" "$?"
|
|
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "${YELLOW}Identify snapper id ${GREEN}'%s'${YELLOW} on target for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
|
|
"$snapper_new_id" "$snapper_target_config"
|
|
fi
|
|
|
|
# construct snapper match command
|
|
cmd="$ssh snapper --verbose --config \"$snapper_target_config\" list --type single \
|
|
| awk ' /'\"$snap_description_running\"'/ ' \
|
|
| awk -F '|' ' \$1 == $snapper_new_id {print \$1} ' "
|
|
|
|
while [ "$ii" -le "$ii_max" ]; do
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "calling: '%s'\n" "$cmd"
|
|
fi
|
|
ret=$(eval $cmd)
|
|
#ret=$ssh snapper --verbose --config \"$snapper_target_config\" list --type single \
|
|
# | awk ' /'\"$snap_description_running\"'/ ' \
|
|
# | awk -F '|' ' $1 == "$snapper_new_id" {print $1} '
|
|
#printf "return: '%s'\n" "$?"
|
|
if [ $? -eq 0 ]; then
|
|
#printf "return: snapper_new_id '%s'\n" "$ret"
|
|
if [ $ret -eq $snapper_new_id ]; then
|
|
# got snapshot as $snapper_new_id
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "${YELLOW}Found${NO_COLOR} snapper id ${GREEN}'%s'${NO_COLOR} on target for configuration ${GREEN}'%s'${NO_COLOR}\n" \
|
|
"$snapper_new_id" "$snapper_target_config"
|
|
fi
|
|
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "${YELLOW}Tagging metadata${NO_COLOR} for snapper id ${GREEN}'%s'${NO_COLOR} on target for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
|
|
"$snapper_new_id" "$snapper_target_config"
|
|
#printf "calling: '%s'\n" "$cmd"
|
|
fi
|
|
|
|
# call command (respect needed quotes)
|
|
if [ $remote ]; then
|
|
ret=$(eval $ssh snapper --config \\\'$snapper_target_config\\\' modify \
|
|
--description \\\'$snap_description_finished\\\' \
|
|
--userdata \\\'host=$src_host, subvolid=$src_subvolid, uuid=$src_uuid\\\' \
|
|
--cleanup-algorithm \'timeline\' \
|
|
\'$snapper_new_id\')
|
|
else
|
|
ret=$(snapper --config $snapper_target_config modify \
|
|
--description \"$snap_description_finished\" \
|
|
--userdata \'"subvolid=$src_subvolid, uuid=$src_uuid, host=$src_host"\' \
|
|
--cleanup-algorithm \"timeline\" \
|
|
$snapper_new_id)
|
|
|
|
fi
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "return: '%s'\n" "$ret"
|
|
fi
|
|
break
|
|
fi
|
|
fi
|
|
if [ $verbose -ge 2 ]; 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 1 ]; then
|
|
printf "${MAGENTA}Tagging source ...${NO_COLOR}\n"
|
|
fi
|
|
|
|
cmd="snapper --config $selected_config modify \
|
|
--description \"$snap_description_synced\" \
|
|
--userdata \"backupdir=$backup_dir, subvolid=$selected_subvol, uuid=$selected_uuid, host=$remote\" \
|
|
--cleanup-algorithm \"timeline\" \
|
|
$snapper_new_id"
|
|
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "${YELLOW}Tagging snapper metadata${NO_COLOR} for snapper id ${GREEN}'%s'${NO_COLOR} on source for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
|
|
"$snapper_new_id" "$selected_config"
|
|
printf "calling: '%s'\n" "$cmd"
|
|
fi
|
|
ret=$(eval "$cmd")
|
|
# !!working!!
|
|
#snapper --config $snapper_config modify \
|
|
# --description \"$snap_description_synced\" \
|
|
# --cleanup-algorithm \"timeline\" \
|
|
# --userdata "\'backupdir=$backup_dir, subvolid=$selected_subvol, uuid=$selected_uuid, host=$remote\'" \
|
|
# $snapper_new_id
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "return: '%s'\n" "$?"
|
|
fi
|
|
sync
|
|
|
|
if [ ${#snapper_sync_id} -gt 0 ]; then
|
|
cmd="snapper --config $selected_config modify \
|
|
--description \"$snap_description_finished\" \
|
|
$snapper_sync_id"
|
|
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "${YELLOW}Tagging snapper metadata${NO_COLOR} for snapper sync id ${GREEN}'%s'${NO_COLOR} on source for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \
|
|
"$snapper_sync_id" "$selected_config"
|
|
printf "calling: '%s'\n" "$cmd"
|
|
fi
|
|
ret=$(eval "$cmd")
|
|
snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot
|
|
fi
|
|
else
|
|
# dry-run output
|
|
cmd="$ssh snapper --verbose --config $snapper_target_config modify -description $snap_description_finished --userdata $target_userdata --cleanup-algorithm $snap_cleanup_algorithm $snapper_new_id"
|
|
printf "dryrun: %s\n" "$cmd"
|
|
cmd="snapper --config $selected_config modify --description $snap_description_synced --userdata $userdata $snapper_new_id"
|
|
printf "dryrun: %s\n" "$cmd"
|
|
cmd="snapper --config $selected_config modify --description $snap_description_finished $snapper_sync_id"
|
|
printf "dryrun: %s\n" "$cmd"
|
|
fi
|
|
|
|
printf "Backup complete for snapper configuration '%s'.\n" "$selected_config" > $PIPE
|
|
|
|
done
|
|
}
|
|
|
|
select_target_disk () {
|
|
local i=0
|
|
local disk_id=0
|
|
|
|
#local disk_selected_ids=''
|
|
local disk_selected_count=0
|
|
local subvolid=''
|
|
local subvol=''
|
|
|
|
printf "${BLUE}Select target disk${NO_COLOR} on target %s...\n" \
|
|
"$remote"
|
|
|
|
# print selection table
|
|
if [ $verbose -ge 1 ]; 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 1 ]; then
|
|
printf "%s mount points were found with UUID '%s'.\n" "$disk_uuid_match_count" "$uuid_cmdline"
|
|
fi
|
|
for disk_uuid in $disk_uuid_match; do
|
|
# Pseudo-Array: disk_selected_$i (reference to $disk_uuid element)
|
|
eval "disk_selected_$i='$disk_uuid'"
|
|
disk=$(eval echo \$disk_uuid_$disk_uuid)
|
|
fs_options=$(eval echo \$fs_options_$disk_uuid | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
|
|
printf "%4s) %s (uuid=%s,%s)\n" "$i" "$target" "$disk" "$fs_options"
|
|
i=$((i+1))
|
|
done
|
|
else
|
|
while [ "$disk_id" -le $disk_count ]; do
|
|
# present all mounted BTRFS filesystems
|
|
# Pseudo-Array: disk_selected_$i (reference to $disk_id element)
|
|
eval disk_selected_$i="$disk_id"
|
|
disk=$(eval echo \$disk_uuid_$disk_id)
|
|
target=$(eval echo \$disk_target_$disk_id)
|
|
fs_options=$(eval echo \$fs_options_$disk_id | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
|
|
printf "%4s) %s (uuid=%s,%s)\n" "$i" "$target" "$disk" "$fs_options"
|
|
i=$((i+1))
|
|
disk_id=$(($disk_id+1))
|
|
done
|
|
fi
|
|
printf "%4s) Exit\n" "x"
|
|
read -r -p "Enter a number: " disk_selected
|
|
case $disk_selected in
|
|
x)
|
|
break
|
|
;;
|
|
[0-9][0-9]|[0-9])
|
|
if [ "$disk_selected" -gt "$disk_count" ]; then
|
|
disk_id=0
|
|
i=0
|
|
else
|
|
break
|
|
fi
|
|
;;
|
|
*)
|
|
printf "\nNo disk selected. Select a disk to continue.\n"
|
|
disk_id=0
|
|
i=0
|
|
;;
|
|
esac
|
|
done
|
|
if [ "$disk_selected" = x ]; then
|
|
exit 0
|
|
fi
|
|
|
|
selected_uuid=$(eval echo \$disk_uuid_$disk_selected)
|
|
selected_target=$(eval echo \$disk_target_$disk_selected)
|
|
selected_subvol=$(eval echo \$fs_options_$disk_selected | sed -e 's/.*subvolid=\([0-9]*\).*/\1/')
|
|
if [ "$dryrun" ]; then
|
|
printf "Selected Subvol-ID=%s: %s on %s\n" "$selected_subvol" "$selected_target" "$selected_uuid"
|
|
fi
|
|
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "\nYou selected the disk with UUID %s (subvolid=%s).\n" "$selected_uuid" "$selected_subvol"
|
|
if [ -z "$remote" ]; then
|
|
printf "The disk is mounted at %s.\n" "$selected_target"
|
|
else
|
|
printf "The disk is mounted at %s:%s.\n" "$remote" "$selected_target"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
set_config(){
|
|
local config=${1:-/etc/snapper/config-templates/"$snapper_subvolume_template"}
|
|
local config_key=${2:-SUBVOLUME}
|
|
local config_value=${3:-/var/lib/dsnap-sync}
|
|
|
|
if [ -n "$remote" ]; then
|
|
$ssh sed -i \'"s#^\($config_key\s*=\s*\).*\$#\1\"$config_value\"#"\' $config
|
|
else
|
|
sed -i "s#^\($config_key\s*=\s*\).*\$#\1\"$config_value\"#" $config
|
|
fi
|
|
}
|
|
|
|
traperror () {
|
|
printf "Exited due to error on line %s.\n" $1
|
|
printf "exit status: %s\n" "$2"
|
|
#printf "command: %s\n" "$3"
|
|
#printf "bash line: %s\n" "$4"
|
|
#printf "function name: %s\n" "$5"
|
|
exit 1
|
|
}
|
|
|
|
trapkill () {
|
|
printf "Exited due to user intervention.\n"
|
|
run_cleanup 1
|
|
exit 0
|
|
}
|
|
|
|
usage () {
|
|
cat <<EOF
|
|
$progname $version
|
|
Usage: $progname [options]
|
|
|
|
Options:
|
|
-b, --backupdir <prefix> backupdir is a relative path that will be appended to target backup-root
|
|
-d, --description <desc> Change the snapper description. Default: "latest incremental backup"
|
|
--label-finished <desc> snapper description tagging successful jobs. Default: "dsnap-sync backup"
|
|
--label-running <desc> snapper description tagging active jobs. Default: "dsnap-sync in progress"
|
|
--label-synced <desc> snapper description tagging last synced jobs.
|
|
Default: "dsnap-sync last incremental"
|
|
--color Enable colored output messages
|
|
-c, --config <config> Specify the snapper configuration to use. Otherwise will perform for each snapper
|
|
configuration. Can list multiple configurations within quotes, space-separated
|
|
(e.g. -c "root home").
|
|
--config-postfix <name> Specify a postfix that will be appended to the destination snapper config name.
|
|
-n, --noconfirm Do not ask for confirmation for each configuration. Will still prompt for backup
|
|
--batch directory name on first backup"
|
|
--nonotify Disable graphical notification (via dbus)
|
|
--nopv Disable graphical progress output (disable pv)
|
|
-r, --remote <address> Send the snapshot backup to a remote machine. The snapshot will be sent via ssh.
|
|
You should specify the remote machine's hostname or ip address. The 'root' user
|
|
must be permitted to login on the remote machine.
|
|
-p, --port <port> The remote port.
|
|
-s, --subvolid <subvlid> Specify the subvolume id of the mounted BTRFS subvolume to back up to. Defaults to 5.
|
|
-u, --uuid <UUID> Specify the UUID of the mounted BTRFS subvolume to back up to. Otherwise will prompt."
|
|
If multiple mount points are found with the same UUID, will prompt user."
|
|
-t, --target <target> Specify the mountpoint of the BTRFS subvolume to back up to.
|
|
--remote <address> Send the snapshot backup to a remote machine. The snapshot will be sent via ssh. You
|
|
should specify the remote machine's hostname or ip address. The 'root' user must be
|
|
permitted to login on the remote machine.
|
|
--dry-run perform a trial run where no changes are made.
|
|
-v, --verbose Be more verbose on what's going on.
|
|
--version show program version
|
|
EOF
|
|
|
|
exit 1
|
|
}
|
|
|
|
verify_snapper_structure () {
|
|
local backup_root=${1##backup_root=}
|
|
local snapper_config=${2##snapper_target_config=}
|
|
local snapper_id=${3##snapper_new_id=}
|
|
local remote_host=${4##remote=}
|
|
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "${MAGENTA}Verify snapper filesystem structure${NO_COLOR} on target %s...\n" \
|
|
"$remote"
|
|
fi
|
|
|
|
# if not accessible, create backup-path
|
|
cmd="$ssh stat --format %i $backup_root 2>/dev/null"
|
|
if [ -z $(eval $cmd) ]; then
|
|
if [ ! "$dryrun" ]; then
|
|
if [ $verbose -ge 1 ]; then
|
|
if [ -z $remote_host ]; then
|
|
printf "Create backup-path %s ...\n" \
|
|
"$backup_root"
|
|
else
|
|
printf "Create backup-path %s:%s ...\n" \
|
|
"$remote_host" "$backup_root"
|
|
fi
|
|
fi
|
|
if [ ! $dryrun ]; then
|
|
$(eval $ssh mkdir --mode=0700 --parents $backup_root)
|
|
else
|
|
if [ -z $remote_host ]; then
|
|
printf "dryrun: Would create backup-path %s ...\n" \
|
|
"$backup_root"
|
|
else
|
|
printf "dryrun: Would create backup-path %s on remote host %s ...\n" \
|
|
"$remote_host" "$backup_root"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# verify that we have a snapper compatible structure for selected config on target (a btrfs subvolume)
|
|
cmd="$ssh stat --format %i $backup_root/$snapper_config 2>/dev/null"
|
|
if [ -z $(eval $cmd) ]; then
|
|
if [ $verbose -ge 1 ]; then
|
|
if [ -z "$remote" ]; then
|
|
printf "${MAGENTA}Create${NO_COLOR} new snapper capable BTRFS subvolume ${MAGENTA}'%s'${NO_COLOR} ...\n" \
|
|
"$backup_root/$snapper_config"
|
|
else
|
|
printf "${MAGENTA}Create${NO_COLOR} new snapper capable BTRFS subvolume '%s' on ${MAGENTA}remote host '%s'${NO_COLOR} ...\n" \
|
|
"$remote_host" "$backup_root/$snapper_config"
|
|
fi
|
|
fi
|
|
if [ ! "$dryrun" ]; then
|
|
# verify that we can use the correct snapper template
|
|
cmd="$ssh stat --format %i $SNAPPER_TEMPLATE_DIR/$snapper_subvolume_template 2>/dev/null"
|
|
if [ -z $(eval $cmd) ]; then
|
|
printf "${RED}Missing a snapper template %s${NO_COLOR} to configure the snapper subvolume %s in %s on %s.\n" \
|
|
"$snapper_subvolume_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host"
|
|
printf "Did you miss to install the dsnap-sync's default snapper template on %s?\n" \
|
|
"$remote"
|
|
die "snapper template %s to configure the snapper subvolume %s is missing in %s on %s.\n" \
|
|
"$snapper_subvolume_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host"
|
|
fi
|
|
# create the non existing remote BTRFS subvolume for given config
|
|
cmd="$ssh btrfs subvolume create $backup_root/$snapper_config 1>/dev/null"
|
|
$(eval $cmd) || die "Creation of BTRFS subvolume %s:%s failed.\n" \
|
|
"$remote_host" "$backup_root/$snapper_config"
|
|
cmd="$ssh chmod 0700 $backup_root/$snapper_config"
|
|
$(eval $cmd) || die "Changing the directory mode for %s on %s failed.\n" \
|
|
"$backup_root/$snapper_config" "$remote_host"
|
|
|
|
# create the non existing remote BTRFS subvolume for given snapshot
|
|
cmd="$ssh stat --format %i $backup_root/$snapper_config/$snapper_snapshot 2>/dev/null"
|
|
if [ -z $(eval $cmd) ]; then
|
|
cmd="$ssh btrfs subvolume create $backup_root/$snapper_config/$snapper_snapshot 1>/dev/null"
|
|
$(eval $cmd) || \
|
|
die "Creation of BTRFS subvolume $remote_host: $backup_root/$snapper_config failed."
|
|
cmd="$ssh chmod 0700 $backup_root/$snapper_config 1>/dev/null"
|
|
$(eval $cmd) || \
|
|
die "Changing the directory mode for '$backup_root/$snapper_config' on '$remote_host'."
|
|
fi
|
|
else
|
|
printf "dryrun: Would create new snapper configuration from template %s ...\n" "$snapper_subvolume_template"
|
|
printf "dryrun: Would create new snapper subvolume '%s' ...\n" "$backup_root/$snapper_config/$snapper_snapshot"
|
|
fi
|
|
else
|
|
cmd="$ssh stat --format %i $backup_root/$snapper_config 2>/dev/null"
|
|
if [ $(eval $cmd) -ne 256 ]; then
|
|
die "%s needs to be a BTRFS subvolume. But given %s is just a directory.\n" "$snapper_config" "$backup_root/$snapper_config"
|
|
fi
|
|
if [ $verbose -ge 1 ]; 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 $(. /etc/snapper/configs/$snapper_config)
|
|
#get_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME"
|
|
#if $ssh [ "$SUBVOLUME" != \"$backup_root/$snapper_config\" ]; then
|
|
# SUBVOLUME="$backup_root/$snapper_config"
|
|
# set_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" "$SUBVOLUME"
|
|
#fi
|
|
fi
|
|
|
|
# verify that we have a valid snapper config
|
|
if [ ! "$dryrun" ]; then
|
|
cmd="$ssh stat --format %i $SNAPPER_CONFIG_DIR/$snapper_config 2>/dev/null"
|
|
if [ -z $(eval $cmd) ]; then
|
|
# snapper will create new structure at $backup_root/$snapper_config/.snapshots
|
|
cmd="$ssh snapper --config $snapper_config create-config --template $snapper_subvolume_template --fstype btrfs $backup_root/$snapper_config"
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "create new snapper_config '%s' using template '%s'\n" $snapper_config $snapper_subvolume_template
|
|
fi
|
|
$(eval $cmd) || die "Creation of snapper capable config %s on %s failed.\n" \
|
|
"$backup_root/$snapper_config" "$remote_host"
|
|
else
|
|
# verify if SUBVOLUME needs to be updated for given snapper config
|
|
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}'"
|
|
#cmd="$ssh snapper list-configs | awk '/'\"$snapper_config\"'/'"
|
|
#ret=$(eval $cmd)
|
|
#if [ -z $ret ]; then
|
|
if [ -n $(eval $cmd) ]; then
|
|
# if changed, adapt targets SUBVOLUME path
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "${RED}TODO:${NO_COLOR} Check if value for key 'SUBVOLUME' needs an update in snapper config %s\n" \
|
|
"$snapper_config"
|
|
fi
|
|
#get_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME"
|
|
#if $ssh [ "$SUBVOLUME" != \"$backup_root/$snapper_config\" ]; then
|
|
# SUBVOLUME="$backup_root/$snapper_config"
|
|
# set_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" "$SUBVOLUME"
|
|
# cmd="btrfs subvolume create $backup_root/$snapper_config/$snapper_snapshots"
|
|
# $ssh $cmd || die "Can't create subvolume %s in %s to hold target snapshots.\n" "$snapper_snapshots" "$backup_root/$snapper_config"
|
|
#fi
|
|
fi
|
|
# verify existence of SUBVOLUME $backup_root/$snapper_config/.snapshots
|
|
cmd="$ssh stat --format %i $backup_root/$snapper_config/$snapper_snapshots 2>/dev/null"
|
|
ret=$(eval $cmd)
|
|
if [ -z $ret ]; then
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "create new BTRFS subvolume '%s'\n" $backup_root/$snapper_config/$snapper_snapshots
|
|
fi
|
|
cmd="$ssh btrfs subvolume create $backup_root/$snapper_config/$snapper_snapshots"
|
|
$(eval $cmd) || die "Creation of snapper subvolume %s failed.\n" \
|
|
"$backup_root/$snapper_config/$snapper_snapshots"
|
|
else
|
|
if [ $ret -ne 256 ]; then
|
|
die "%s needs to be a BTRFS subvolume. But given %s is just a directory.\n" \
|
|
"$snapper_config" "$backup_root/$snapper_config"
|
|
fi
|
|
fi
|
|
fi
|
|
else
|
|
printf "dryrun: Would check/create for valid snapper config %s ...\n" \
|
|
"$snapper_config"
|
|
fi
|
|
|
|
# verify that target snapshot can take the new snapshot data id
|
|
if [ ! "$dryrun" ]; then
|
|
if [ $verbose -ge 1 ]; then
|
|
printf "${MAGENTA}Verify existence of path '%s'.${NO_COLOR}\n" \
|
|
"$backup_root/$snapper_config/$snapper_snapshots/$snapper_id"
|
|
fi
|
|
cmd="$ssh stat --format %i $backup_root/$snapper_config/$snapper_snapshots/$snapper_id 2>/dev/null"
|
|
if [ -z "$(eval $cmd)" ]; then
|
|
if [ $verbose -ge 2 ]; then
|
|
printf "${MAGENTA}Create path %s${NO_COLOR} to store target snapshot.\n" \
|
|
"$backup_root/$snapper_config/$snapper_snapshots/$snapper_id"
|
|
fi
|
|
|
|
$(eval $ssh mkdir --mode=0700 \
|
|
$backup_root/$snapper_config/$snapper_snapshots/$snapper_id)
|
|
if [ $? -ne 0 ]; then
|
|
printf "${RED}Cancel path snapshot creation${NO_COLOR}: Can't create path '%s' to store target snapshot.\n" \
|
|
"$backup_root/$snapper_config/$snapper_snapshots/$snapper_id"
|
|
die "Can't create snapshot dir on target."
|
|
fi
|
|
else
|
|
if [ -z "$remote" ]; then
|
|
printf "${RED}Cancel snapshot creation${NO_COLOR}: Former snapshot with id ${GREEN}'%s'${NO_COLOR} already exist in ${BLUE}'%s'${NO_COLOR}\n" \
|
|
"$snapper_id" "$backup_root/$snapper_config/$snapper_snapshots"
|
|
else
|
|
printf "${RED}Cancel snapshot creation${NO_COLOR}: Former snapshot with id ${GREEN}'%s'${NO_COLOR} already exist on ${BLUE}'%s' in '%s'${NO_COLOR}\n" \
|
|
"$snapper_id" "$remote" "$backup_root/$snapper_config/$snapper_snapshots"
|
|
fi
|
|
|
|
# cleanup generated snapper entry
|
|
check_snapper_failed_ids $batch
|
|
die "Can't create new snapshot with given snapshot-id!"
|
|
fi
|
|
else
|
|
printf "dryrun: Would check/create path %s to store target snapshot ...\n" \
|
|
"$backup_root/$snapper_config/$snapper_snapshots/$snapper_id"
|
|
fi
|
|
}
|
|
|
|
###
|
|
# Main
|
|
###
|
|
|
|
cwd=`pwd`
|
|
ssh=""
|
|
|
|
# can't be ported to dash (ERR is not supported)
|
|
#trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCNAME[@]}"' ERR
|
|
trap trapkill TERM INT
|
|
|
|
check_prerequisites
|
|
|
|
# validate commandline options, set resonable defaults
|
|
parse_params $@
|
|
|
|
# read mounted BTRFS structures
|
|
get_disk_infos
|
|
|
|
if [ $donotify -gt 0 ]; then
|
|
if [ "$target_cmdline" != "none" ]; then
|
|
if [ -z "$remote" ]; then
|
|
notify_info "Backup started" "Starting backups to '$target_cmdline' ..."
|
|
else
|
|
notify_info "Backup started" "Starting backups to '$target_cmdline' at $remote ..."
|
|
fi
|
|
elif [ "$uuid_cmdline" != "none" ]; then
|
|
if [ -z "$remote" ]; then
|
|
notify_info "Backup started" "Starting backups to $uuid_cmdline..."
|
|
else
|
|
notify_info "Backup started" "Starting backups to $uuid_cmdline at $remote..."
|
|
fi
|
|
else
|
|
if [ -z "$remote" ]; then
|
|
notify_info "Backup started" "Starting backups. Use command line menu to select disk."
|
|
else
|
|
notify_info "Backup started" "Starting backups. Use command line menu to select disk on $remote."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# select the target BTRFS subvol
|
|
select_target_disk
|
|
|
|
# create and initialize structures for snapper configs
|
|
run_config
|
|
|
|
# run backups using btrfs-send -> btrfs-receive
|
|
run_backup
|
|
|
|
# finalize backup tasks
|
|
run_finalize
|
|
|
|
printf "${BLUE}Done!${NO_COLOR}\n"
|
|
exec 3>&-
|
|
|
|
# cleanup
|
|
run_cleanup
|
|
|
|
if [ $donotify -gt 0 ]; then
|
|
if [ "$uuid_cmdline" != "none" ]; then
|
|
notify_info "Finished" "Backups to $uuid_cmdline complete!"
|
|
else
|
|
notify_info "Finished" "Backups complete!"
|
|
fi
|
|
fi
|