Files
dsnap-sync/bin/snap-sync
Ralf Zerres f99b720a21 snap-sync: support selection of SUBVOLID
- either use --subvolid 'id' or --SUBVOLID 'id'
- or --subvolid='id' or --SUBVOLID='id'

Signed-off-by: Ralf Zerres <ralf.zerres@networkx.de>
2017-11-22 21:59:18 +01:00

829 lines
28 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.4"
# 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_subvolid_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'"
subvolid=$(eval echo \$fs_options | 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 "disk_target_$i='$disk_target'"
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
;;
-s|--subvolid|--SUBVOLID)
subvolid_cmdline="$2"
shift 2
;;
-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
;;
--subvolid=*|--SUBVOLID=*)
subvolid_cmdline=${1#*=}
shift
;;
--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 '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {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 '/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'"
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, subvolid=$selected_subvol, uuid=$selected_uuid"
# 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_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_target_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" ]; 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