- introduce commandline option --dry-run perform a trial run where no changes are made. - introduce option --TARGET Specify the mountpoint of the BTRFS subvolume to back up to. This makes it possible, to have multiple subvolumes on target disks, and select them as needed to store backup configurations. Signed-off-by: Ralf Zerres <ralf.zerres@networkx.de>
805 lines
27 KiB
Bash
Executable File
805 lines
27 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
# snap-sync
|
|
# https://github.com/wesbarnett/snap-sync
|
|
# Copyright (C) 2016, 2017 James W. Barnett
|
|
|
|
# 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="`basename v$0`"
|
|
version="0.4.3"
|
|
|
|
# The following line is modified by the Makefile or
|
|
# find_snapper_config script
|
|
SNAPPER_CONFIG=/etc/conf.d/snapper
|
|
|
|
TMPDIR=$(mktemp -d)
|
|
PIPE=$TMPDIR/$progname.out
|
|
mkfifo $PIPE
|
|
systemd-cat -t "$progname" < $PIPE &
|
|
exec 3>$PIPE
|
|
|
|
# global variables
|
|
disk_count=-1
|
|
disk_uuid_match_count=0
|
|
disk_target_match_count=0
|
|
disk_uuid_match=''
|
|
selected_uuid='none'
|
|
selected_target='none'
|
|
selected_subvol='none'
|
|
|
|
###
|
|
# functions
|
|
###
|
|
|
|
|
|
check_prerequisites () {
|
|
|
|
# requested binaries:
|
|
which awk >/dev/null 2>&1 || { echo "'awk' is not installed." && exit 1; }
|
|
which sed >/dev/null 2>&1 || { echo "'sed' is not installed." && exit 1; }
|
|
which tee >/dev/null 2>&1 || { echo "'tee' is not installed." && exit 1; }
|
|
which btrfs >/dev/null 2>&1 || { echo "'btrfs' is not installed." && exit 1; }
|
|
which findmnt >/dev/null 2>&1 || { echo "'findmnt' is not installed." && exit 1; }
|
|
which systemd-cat >/dev/null 2>&1 || { echo "'systemd-cat' is not installed." && exit 1; }
|
|
which wc >/dev/null 2>&1 || { echo "'wc' is not installed." && exit 1; }
|
|
which notify-send >/dev/null 2>&1 || { echo "'notify-send' is not installed." && exit 1; }
|
|
|
|
if [ $(id -u) -ne 0 ] ; then echo "Script must be run as root" ; exit 1 ; fi
|
|
|
|
if [ ! -r "$SNAPPER_CONFIG" ]; then
|
|
die "$SNAPPER_CONFIG does not exist."
|
|
fi
|
|
}
|
|
|
|
die () {
|
|
error "$@"
|
|
exit 1
|
|
}
|
|
|
|
error () {
|
|
printf "==> ERROR: %s\n" "$@"
|
|
notify_error 'Error' 'Check journal for more information.'
|
|
} >&2
|
|
|
|
get_disk_infos () {
|
|
local disk_uuid
|
|
local disk_target
|
|
local fs_option
|
|
|
|
# get mounted BTRFS infos
|
|
if [ "$(findmnt --noheadings --nofsroot --target / --output FSTYPE)" = "btrfs" ]; then
|
|
exclude_uuid=$(findmnt --noheadings --nofsroot --types btrfs --target / --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 \n"die
|
|
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_$y (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
|
|
eval "fs_options_$i='$fs_option'"
|
|
i=$((i+1))
|
|
done
|
|
}
|
|
|
|
notify () {
|
|
# estimation: batch calls should just log
|
|
if [ nonotify ]; then
|
|
printf "%s %s\n" "$progname" "$2"
|
|
else
|
|
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 "$1" "$2" --icon="dialog-$3"
|
|
done
|
|
fi
|
|
}
|
|
|
|
notify_info () {
|
|
notify "$1" "$2" "information"
|
|
}
|
|
|
|
notify_error() {
|
|
notify "$1" "$2" "error"
|
|
}
|
|
|
|
parse_params () {
|
|
###
|
|
# Evaluate given call parameters
|
|
###
|
|
while [ $# -gt 0 ]; do
|
|
key="$1"
|
|
case $key in
|
|
-h | --help | \-\? | --usage)
|
|
# Call usage() function.
|
|
usage
|
|
;;
|
|
-c|--config)
|
|
selected_config="$2"
|
|
shift 2
|
|
;;
|
|
-d|--description)
|
|
description="$2"
|
|
shift 2
|
|
;;
|
|
--dry-run)
|
|
dryrun=1
|
|
shift 1
|
|
;;
|
|
-l|--TARGET)
|
|
target_cmdline="$2"
|
|
shift 2
|
|
;;
|
|
-n|--noconfirm)
|
|
noconfirm=1
|
|
nonotify=1
|
|
shift
|
|
;;
|
|
-u|--UUID)
|
|
uuid_cmdline="$2"
|
|
shift 2
|
|
;;
|
|
-v|--verbose)
|
|
verbose=1
|
|
shift 1
|
|
;;
|
|
--config=*)
|
|
selected_config=${1#*=}
|
|
shift
|
|
;;
|
|
--remote)
|
|
remote=$2
|
|
ssh="ssh $remote"
|
|
shift 2
|
|
;;
|
|
--TARGET=*)
|
|
target_cmdline=${1#*=}
|
|
shift
|
|
;;
|
|
--UUID=*)
|
|
uuid_cmdline=${1#*=}
|
|
shift
|
|
;;
|
|
--) # End of all options
|
|
shift
|
|
break
|
|
;;
|
|
-*)
|
|
echo "WARN: Unknown option (ignored): $1" >&2
|
|
#shift
|
|
exit 1
|
|
;;
|
|
*)
|
|
die "Unknown option: $key\nRun '$progname -h' for valid options.\n"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Set reasonable defaults
|
|
. $SNAPPER_CONFIG
|
|
selected_configs=${selected_configs:-$SNAPPER_CONFIGS}
|
|
|
|
description=${description:-"latest incremental backup"}
|
|
uuid_cmdline=${uuid_cmdline:-"none"}
|
|
target_cmdline=${target_cmdline:-"none"}
|
|
if [ -z $remote ]; then
|
|
ssh=""
|
|
fi
|
|
|
|
if [ "$verbose" ]; then
|
|
echo "Snap UUID : '$uuid_cmdline'"
|
|
echo "Snap TARGET: '$target_cmdline'"
|
|
echo "Snap Description: '$description'"
|
|
echo "Snap Config: '$selected_config'"
|
|
echo "Snap Remote: '$ssh'"
|
|
|
|
if [ "$verbose" ]; then snap_sync_options="verbose=true"; fi
|
|
if [ "$dryrun" ]; then snap_sync_options="${snap_sync_options} dry-run=true"; fi
|
|
if [ "$noconfirm" ]; then snap_sync_options="${snap_sync_options} noconfirm=true"; fi
|
|
echo "Options: ${snap_sync_options}"
|
|
fi
|
|
}
|
|
|
|
run_config () {
|
|
|
|
printf "\nVerify configuration...\n" | tee $PIPE
|
|
|
|
# commandline selection takes precedence
|
|
if [ -n "$selected_config" ]; then
|
|
selected_configs=$selected_config
|
|
fi
|
|
|
|
# Pseudo Arrays $i -> store associated elements of selected_config
|
|
i=0
|
|
for selected_config in $selected_configs; do
|
|
count=$(eval snapper -c $selected_config list -t single | awk '/uuid='"$selected_uuid"'/, /subvolid'="$selected_subvol"'/ {cnt++} END {print cnt}')
|
|
#count=$(eval snapper -c $selected_config list -t single | grep -c -e "subvolid=$selected_subvol" -e 'uuid=$selected_uuid')
|
|
if [ -n "$count" ] && [ "$count" -gt 1 ]; then
|
|
error "More than one snapper entry found with UUID $selected_uuid and SUBVOL $selected_subvol for configuration '$selected_config'. Skipping configuration '$selected_config'."
|
|
selected_configs=$(echo $selected_configs | sed -e "s/\($selected_config*\)//")
|
|
if [ "$verbose" ]; then
|
|
printf "Counter=%s" "$count"
|
|
fi
|
|
continue
|
|
fi
|
|
# active, non finished snapshot backups are marked with following string
|
|
# "$progname backup in progress" (snapper description field)
|
|
snapper_failed_ids=$(eval snapper -c $selected_config list -t single | awk '/'"$progname"' backup in progress/ {cnt++} END {print cnt}')
|
|
if [ -n "$snapper_failed_ids" ]; then
|
|
printf "\nNOTE: Previous failed %s backup snapshots found for '%s'.\n" "$progname" "$selected_config" | tee $PIPE
|
|
read -r -p "Delete failed backup snapshots [y/N]? " delete_failed
|
|
#get_answer_yes_no
|
|
while [ -n "$delete_failed" ] &&
|
|
[ "$delete_failed" != "Yes" ] &&
|
|
[ "$delete_failed" != "yes" ] &&
|
|
[ "$delete_failed" != "Y" ] &&
|
|
[ "$delete_failed" != "y" ] &&
|
|
[ "$delete_failed" != "No" ] &&
|
|
[ "$delete_failed" != "no" ] &&
|
|
[ "$delete_failed" != "N" ] &&
|
|
[ "$delete_failed" != "n" ]; do
|
|
read -r -p "Delete failed backup snapshots [y/N]? " delete_failed
|
|
if [ -n "$delete_failed" ] &&
|
|
[ "$delete_failed" != "Yes" ] &&
|
|
[ "$delete_failed" != "yes" ] &&
|
|
[ "$delete_failed" != "Y" ] &&
|
|
[ "$delete_failed" != "y" ] &&
|
|
[ "$delete_failed" != "No" ] &&
|
|
[ "$delete_failed" != "no" ] &&
|
|
[ "$delete_failed" != "N" ] &&
|
|
[ "$delete_failed" != "n" ]; then
|
|
printf "Select 'y' or 'N'.\n"
|
|
fi
|
|
done
|
|
if [ "$delete_failed" = "Yes" ] ||
|
|
[ "$delete_failed" = "yes" ] ||
|
|
[ "$delete_failed" = "Y" ] ||
|
|
[ "$delete_failed" = "y" ]; then
|
|
snapper -c $selected_config delete $(snapper -c $selected_config list | awk '/'"$progname"' backup in progress/ {print $3}')
|
|
fi
|
|
fi
|
|
|
|
SNAP_SYNC_EXCLUDE=no
|
|
|
|
if [ -f "/etc/snapper/configs/$selected_config" ]; then
|
|
. /etc/snapper/configs/$selected_config
|
|
if [ "$SUBVOLUME" = "/" ]; then
|
|
SUBVOLUME=''
|
|
fi
|
|
else
|
|
die "Selected snapper configuration $selected_config does not exist."
|
|
fi
|
|
|
|
if [ $SNAP_SYNC_EXCLUDE = "yes" ]; then
|
|
continue
|
|
fi
|
|
|
|
printf "\n"
|
|
|
|
# processed snapshot backup is marked with userdata key/value pairs
|
|
# backupdir, uuid
|
|
snapper_sync_id=$(eval snapper -c "$selected_config" list -t single | awk '/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'"
|
|
|
|
if [ -z "$snapper_sync_id" ]; then
|
|
printf "No backups have been performed for snapper config '%s' on target disk.\n" "$selected_config"
|
|
if [ $noconfirm ]; then
|
|
# go with defaults
|
|
if [ "$verbose" ]; then
|
|
printf "using defaults\n"
|
|
fi
|
|
else
|
|
read -r -p "Enter name of directory to store backups, relative to $selected_target (to be created if not existing): " backupdir
|
|
fi
|
|
if [ -z "$backupdir" ]; then
|
|
backup_root="$selected_target"
|
|
else
|
|
backup_root="$selected_target/$backupdir"
|
|
fi
|
|
else
|
|
if [ "$verbose" ]; 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 -c "$selected_config" list -t single | awk -F "|" '/'"$selected_uuid"'/, /subvolid'="$selected_subvol"'/ {print $5}' | awk -F "," '/backupdir/ {print $1}' | awk -F"=" '{print $2}')
|
|
if [ -z "$backupdir" ]; then
|
|
backup_root="$selected_target"
|
|
else
|
|
backup_root="$selected_target/$backupdir"
|
|
fi
|
|
$ssh test -d $backup_root || die "%s is not a directory on %s.\n" "$backup_root" "$selected_uuid"
|
|
fi
|
|
|
|
eval "backup_root_$i=$backup_root"
|
|
eval "backup_dir_$i=$backupdir"
|
|
|
|
if [ "$verbose" ]; then
|
|
if [ -n "$ssh" ];then
|
|
printf "Backup-Path on remote %s: %s/%s\n" "$remote" "$backup_root" "$backupdir"
|
|
else
|
|
printf "Backup-Path: %s/%s\n" "$backup_root" "$backupdir"
|
|
fi
|
|
fi
|
|
|
|
# acting on source system
|
|
if [ ! $dryrun ]; then
|
|
printf "Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE
|
|
snapper_new_id=$(snapper -c "$selected_config" create --print-number -d "$progname backup in progress")
|
|
snapper_new_snapshot=$SUBVOLUME/.snapshots/$snapper_new_id/snapshot
|
|
snapper_new_info=$SUBVOLUME/.snapshots/$snapper_new_id/info.xml
|
|
sync
|
|
else
|
|
printf "dryrun: Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE
|
|
fi
|
|
|
|
# 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_subvol=.snapshots
|
|
snapper_target_snapshot=$backup_root/$selected_config/$snapper_target_subvol/$snapper_new_id
|
|
if [ -z "$ssh" ]; then
|
|
printf "Will backup %s to %s\n" "$snapper_new_snapshot" "$snapper_target_snapshot/snapshot" | tee $PIPE
|
|
else
|
|
printf "Will backup %s to %s\n" "$snapper_new_snapshot" "$remote":"$snapper_target_snapshot/snapshot" | tee $PIPE
|
|
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_config_$i='$selected_config'"
|
|
eval "snapper_target_subvol_$i='$snapper_target_subvol'"
|
|
eval "snapper_target_snapshot_$i='$snapper_target_snapshot'"
|
|
|
|
cont_backup="K"
|
|
eval "snapper_activate_$i=yes"
|
|
if [ $noconfirm ]; then
|
|
cont_backup="yes"
|
|
else
|
|
while [ -n "$cont_backup" ] &&
|
|
[ "$cont_backup" != "Yes" ] &&
|
|
[ "$cont_backup" != "yes" ] &&
|
|
[ "$cont_backup" != "Y" ] &&
|
|
[ "$cont_backup" != "y" ] &&
|
|
[ "$cont_backup" != "No" ] &&
|
|
[ "$cont_backup" != "no" ] &&
|
|
[ "$cont_backup" != "N" ] &&
|
|
[ "$cont_backup" != "n" ]; do
|
|
read -r -p "Continue with backup [Y/n]? " cont_backup
|
|
if [ -n "$cont_backup" ] &&
|
|
[ "$cont_backup" != "Yes" ] &&
|
|
[ "$cont_backup" != "yes" ] &&
|
|
[ "$cont_backup" != "Y" ] &&
|
|
[ "$cont_backup" != "y" ] &&
|
|
[ "$cont_backup" != "No" ] &&
|
|
[ "$cont_backup" != "no" ] &&
|
|
[ "$cont_backup" != "N" ] &&
|
|
[ "$cont_backup" != "n" ]; then
|
|
printf "Select 'Y' or 'n'.\n"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ "$cont_backup" != "Yes" ] &&
|
|
[ "$cont_backup" != "yes" ] &&
|
|
[ "$cont_backup" != "Y" ] &&
|
|
[ "$cont_backup" != "y" ] &&
|
|
[ -n "$cont_backup" ]; then
|
|
eval "snapper_activate_$i=no"
|
|
printf "Aborting backup for this configuration.\n"
|
|
snapper -c $selected_config delete $snapper_new_id
|
|
fi
|
|
|
|
i=$(($i+1))
|
|
|
|
done
|
|
}
|
|
|
|
run_backup () {
|
|
# Actual backing up
|
|
printf "\nPerforming backups...\n" | tee $PIPE
|
|
|
|
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
|
|
notify_info "Backup in progress" "NOTE: Skipping '$selected_config' configuration."
|
|
continue
|
|
fi
|
|
|
|
notify_info "Backup in progress" "Backing up data for configuration '$selected_config'."
|
|
|
|
printf "\n"
|
|
|
|
# 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_subvol=$(eval echo \$snapper_target_subvol_$i)
|
|
snapper_target_snapshot=$(eval echo \$snapper_target_snapshot_$i)
|
|
|
|
if [ ! "$dryrun" ]; then
|
|
verify_snapper_structure $backup_root $snapper_config $snapper_target_subvol $snapper_new_id
|
|
else
|
|
cmd="verify_snapper_structure $backup_root $snapper_config $snapper_target_subvol $snapper_new_id"
|
|
printf "dryrun: %s\n" "$cmd"
|
|
fi
|
|
|
|
if [ -z "$snapper_sync_id" ]; then
|
|
cmd="btrfs send $snapper_new_snapshot | $ssh btrfs receive $snapper_target_snapshot"
|
|
printf "Sending first snapshot for snapper config '%s' ...\n" "$selected_config" | tee $PIPE
|
|
if [ "$verbose" ]; then
|
|
echo "btrfs send $snapper_new_snapshot | $ssh btrfs receive $snapper_target_snapshot"
|
|
cmd="btrfs send -v $snapper_new_snapshot | $ssh btrfs receive -v $snapper_target_snapshot"
|
|
fi
|
|
if [ ! "$dryrun" ]; then
|
|
btrfs send "$snapper_new_snapshot" | $ssh btrfs receive "$snapper_target_snapshot" &>/dev/null
|
|
else
|
|
cmd="btrfs send -v $snapper_new_snapshot | $ssh btrfs receive -v $snapper_target_snapshot"
|
|
printf "dryrun: %s\n" "$cmd"
|
|
fi
|
|
else
|
|
printf "Sending incremental snapshot for snapper config '%s' ...\n" "$selected_config" | tee $PIPE
|
|
# Sends the difference between the new snapshot and old snapshot to the
|
|
# backup location. Using the -c flag instead of -p tells it that there
|
|
# is an identical subvolume to the old snapshot at the receiving
|
|
# location where it can get its data. This helps speed up the transfer.
|
|
verbose_flag="-v"
|
|
if [ ! "$dryrun" ]; then
|
|
btrfs send "$verbose_flag" -c "$snapper_sync_snapshot" "$snapper_new_snapshot" | $ssh btrfs receive "$verbose_flag" "$snapper_target_snapshot"
|
|
if [ "$verbose" ]; then
|
|
printf "Deleting sync snapshot for %s ...\n" "$selected_config" | tee $PIPE
|
|
fi
|
|
snapper -c "$selected_config" delete "$snapper_sync_id"
|
|
else
|
|
printf "dryrun: btrfs send %s -c %s %s | %s btrfs receive %s %s\n" \
|
|
"$verbose_flag" "$snapper_sync_snapshot" "$snapper_new_snapshot" \
|
|
"$ssh" "$verbose_flag" "$snapper_target_snapshot"
|
|
printf "dryrun: snapper -c %s delete %a\n" "$selected_config" "$snapper_sync_id"
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$ssh" ]; 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
|
|
rsync -avzq "$snapper_new_info" "$remote":"$snapper_target_snapshot"
|
|
else
|
|
cmd="rsync -avzq $snapper_new_info $remote:$snapper_target_snapshot"
|
|
printf "dryrun: %s\n" "$cmd"
|
|
fi
|
|
fi
|
|
|
|
# It's important not to change the values of the key/value pairs ($userdata)
|
|
# which is stored in snappers info.xml file of the source snapshot.
|
|
# This is how we find the parent.
|
|
|
|
userdata="backupdir=$backup_dir, uuid=$selected_uuid, subvolid=$selected_subvol"
|
|
|
|
# Tag new snapshot as the latest
|
|
printf "Tagging new snapshot as latest backup for '%s' ...\n" "$selected_config" | tee $PIPE
|
|
if [ ! "$dryrun" ]; then
|
|
snapper -v -c "$selected_config" modify -d "$description" -u "$userdata" "$snapper_new_id"
|
|
else
|
|
cmd="snapper -v -c $selected_config modify -d $description -u $userdata $snapper_new_id"
|
|
printf "dryrun: %s\n" "$cmd"
|
|
fi
|
|
printf "Backup complete for snapper configuration '%s'.\n" "$selected_config" > $PIPE
|
|
done
|
|
|
|
exec 3>&-
|
|
}
|
|
|
|
select_target_disk () {
|
|
local i=0
|
|
local disk_id=0
|
|
|
|
#local disk_selected_ids=''
|
|
local disk_selected_count=0
|
|
local subvolid=''
|
|
local subvol=''
|
|
|
|
# print selection table
|
|
if [ -z "$ssh" ]; 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
|
|
while [ "$disk_id" -eq -1 ] || [ "$disk_id" -le $disk_count ]; do
|
|
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" ]; 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 %s (%s)\n" "$i" "$disk" "$target" "$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 %s (%s)\n" "$disk_id" "$disk" "$target" "$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 x
|
|
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/')
|
|
#fs_options=$(eval echo \$fs_options_$disk_selected | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/')
|
|
if [ "$verbose" ]; then
|
|
printf "Selected Subvol-ID=%s: %s on %s\n" "$selected_subvol" "$selected_target" "$selected_uuid"
|
|
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 () {
|
|
die "Exited due to user intervention."
|
|
}
|
|
|
|
usage () {
|
|
cat <<EOF
|
|
$progname $version
|
|
Usage: $progname [options]
|
|
|
|
Options:
|
|
-d, --description <desc> Change the snapper description. Default: "latest incremental backup"
|
|
-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").
|
|
-n, --noconfirm Do not ask for confirmation for each configuration. Will still prompt for backup
|
|
directory name on first backup"
|
|
-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."
|
|
-l, --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.
|
|
|
|
EOF
|
|
|
|
exit 1
|
|
}
|
|
|
|
verify_snapper_structure () {
|
|
local backup_root=$1
|
|
local snapper_config=$2
|
|
local snapper_subvol=$3
|
|
local snapper_id=$4
|
|
|
|
if [ "$verbose" ]; then
|
|
echo "Verify snapper filesystem structure on target ..."
|
|
fi
|
|
|
|
# if not accessible, create backup-path
|
|
if $ssh [ ! -d $backup_root/$snapper_config ]; then
|
|
if [ "$verbose" ]; then
|
|
echo "Create backup-path $backup_root/$snapper_config"
|
|
fi
|
|
$ssh mkdir --mode=0700 --parents $backup_root/$snapper_config
|
|
fi
|
|
|
|
# if not accessible, create subvolume to hold snappers snapshot structure
|
|
create_subvol="btrfs subvolume create $backup_root/$snapper_config/$snapper_subvol"
|
|
|
|
# check if given snapper_subvol is a subvol
|
|
if $ssh [ ! -d $backup_root/$snapper_config/$snapper_subvol ]; then
|
|
if [ "$verbose" ]; then
|
|
echo "Create new subvolume $backup_root/$snapper_config/$snapper_subvol"
|
|
fi
|
|
$ssh $create_subvol || die "BTRFS subvolume %s to hold snapshots for config %s could not be created in directory on %s.\n" "$snapper_subvol" "$snapper_config" "$backup_root"
|
|
else
|
|
if $ssh [ `stat --format=%i $backup_root/$snapper_config/$snapper_subvol` -ne 256 ]; then
|
|
die "%s needs to be a BTRFS subvolume. But given %s is just a directory.\n" "$snapper_subvol" "$backup_root/$snapper_config/$snapper_subvol"
|
|
fi
|
|
fi
|
|
|
|
if $ssh [ ! -d $backup_root/$snapper_config/$snapper_subvol/$snapper_id ]; then
|
|
if [ "$verbose" ]; then
|
|
echo "Create backup-path $backup_root/$snapper_config/$snapper_subvol/$snapper_id"
|
|
fi
|
|
$ssh mkdir --mode=0700 $backup_root/$snapper_config/$snapper_subvol/$snapper_id
|
|
fi
|
|
}
|
|
|
|
###
|
|
# Main
|
|
###
|
|
|
|
cwd=`pwd`
|
|
ssh=""
|
|
|
|
# this bashism has to be adapted
|
|
#trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCNAME[@]}"' ERR
|
|
trap 'traperror ${LINENO} $?"' ERR
|
|
trap trapkill SIGTERM SIGINT
|
|
|
|
parse_params $@
|
|
|
|
check_prerequisites
|
|
|
|
# read mounted BTRFS structures
|
|
get_disk_infos
|
|
|
|
if [ "target_cmdline" != "none" ]; then
|
|
if [ -z "$ssh" ]; 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 $ssh ]; 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 "$ssh" ]; 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
|
|
|
|
# select the target BTRFS subvol
|
|
select_target_disk
|
|
|
|
printf "\nYou selected the disk with UUID %s.\n" "$selected_uuid" | tee $PIPE
|
|
if [ -z "$ssh" ]; then
|
|
printf "The disk is mounted at %s.\n" "$selected_target" | tee $PIPE
|
|
else
|
|
printf "The disk is mounted at %s:%s.\n" "$remote" "$selected_target" | tee $PIPE
|
|
fi
|
|
|
|
# create and initialize structures for snapper configs
|
|
run_config
|
|
|
|
# run backups using btrfs-send -> btrfs-receive
|
|
run_backup
|
|
|
|
if [ "$uuid_cmdline" != "none" ]; then
|
|
notify_info "Finished" "Backups to $uuid_cmdline complete!"
|
|
else
|
|
notify_info "Finished" "Backups complete!"
|
|
fi
|