snap-sync: 0.4.3 version bump (inital posix version)

- use dash as reference shell
- refine all bashism to posix calls
- convert bash arrays to posix "pseudo arrays"
  yes, posix does not support handy arrays
- introduce global/local variables
- using (hopefully) more descriptive variable names
- new function verify_snapper_structure()
  that takes care to construct target snapper file-structure
  as needed
- 0.4.3 bump version

Signed-off-by: Ralf Zerres <ralf.zerres@networkx.de>
This commit is contained in:
2017-11-09 09:46:31 +01:00
parent 2ed161c8db
commit 4b6a06f885

View File

@@ -1,4 +1,5 @@
#!/bin/bash #!/bin/sh
# snap-sync # snap-sync
# https://github.com/wesbarnett/snap-sync # https://github.com/wesbarnett/snap-sync
# Copyright (C) 2016, 2017 James W. Barnett # Copyright (C) 2016, 2017 James W. Barnett
@@ -24,14 +25,12 @@
# snapshot created on your system since that will be used to determine the # snapshot created on your system since that will be used to determine the
# difference for the next incremental snapshot. # difference for the next incremental snapshot.
set -o errtrace progname="`basename v$0`"
version="0.4.3"
version="0.4.2"
progname="snap-sync"
# The following line is modified by the Makefile or # The following line is modified by the Makefile or
# find_snapper_config script # find_snapper_config script
SNAPPER_CONFIG=/etc/sysconfig/snapper SNAPPER_CONFIG=/etc/conf.d/snapper
TMPDIR=$(mktemp -d) TMPDIR=$(mktemp -d)
PIPE=$TMPDIR/$progname.out PIPE=$TMPDIR/$progname.out
@@ -39,10 +38,20 @@ mkfifo $PIPE
systemd-cat -t "$progname" < $PIPE & systemd-cat -t "$progname" < $PIPE &
exec 3>$PIPE exec 3>$PIPE
# global variables
#disk_uuid_$i
disk_count=-1
disk_uuid_match_count=0
disk_uuid_match=''
selected_uuid='none'
selected_target='none'
selected_subvol='none'
### ###
# functions # functions
### ###
check_prerequisites () { check_prerequisites () {
# requested binaries: # requested binaries:
@@ -71,43 +80,63 @@ error() {
} >&2 } >&2
get_disk_infos () { get_disk_infos () {
if [[ "$(findmnt -n -v --target / -o FSTYPE)" == "btrfs" ]]; then local disk_uuid
EXCLUDE_UUID=$(findmnt -n -v -t btrfs --target / -o UUID) local disk_target
TARGETS=$($ssh findmnt -n -v -t btrfs -o UUID,TARGET --list | grep -v $EXCLUDE_UUID | awk '{print $2}') local fs_option
UUIDS=$($ssh findmnt -n -v -t btrfs -o UUID,TARGET --list | grep -v $EXCLUDE_UUID | awk '{print $1}')
# 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}')
else else
TARGETS=$($ssh findmnt -n -v -t btrfs -o TARGET --list) disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID --list)
UUIDS=$($ssh findmnt -n -v -t btrfs -o UUID --list) disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list)
fi fi
declare -a TARGETS_ARRAY # we need at least one target disk
declare -a UUIDS_ARRAY 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: disk_selected_$y (reference to $i element)
# List: disk_uuid_match (reference to matching preselected uuids)
# initialize our structures
i=0 i=0
disk=-1 for disk_uuid in $disk_uuids; do
disk_count=0 if [ "$disk_uuid" = "$uuid_cmdline" ]; then
for x in $UUIDS; do if [ ${#disk_uuid_match} -gt 0 ]; then
UUIDS_ARRAY[$i]=$x disk_uuid_match="${disk_uuid_match} $i"
if [[ "$x" == "$uuid_cmdline" ]]; then else
disk=$i disk_uuid_match="$i"
disk_count=$(($disk_count+1))
fi 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)) i=$((i+1))
done done
i=0 i=0
for x in $TARGETS; do for disk_target in $disk_targets; do
TARGETS_ARRAY[$i]=$x eval "disk_target_$i='$disk_target'"
i=$((i+1)) i=$((i+1))
done done
} }
notify () { notify () {
if [ nonotify ]; then
printf "%s %s\n" "$progname" "$2"
else
for u in $(users | sed 's/ /\n/' | sort -u); do for u in $(users | sed 's/ /\n/' | sort -u); do
sudo -u $u DISPLAY=:0 \ sudo -u $u DISPLAY=:0 \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(sudo -u $u id -u)/bus \ DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(sudo -u $u id -u)/bus \
notify-send -a $progname "$1" "$2" --icon="dialog-$3" notify-send -a $progname "$1" "$2" --icon="dialog-$3"
done done
fi
} }
notify_info () { notify_info () {
@@ -179,7 +208,7 @@ parse_params () {
done done
# Set reasonable defaults # Set reasonable defaults
source $SNAPPER_CONFIG . $SNAPPER_CONFIG
selected_configs=${selected_configs:-$SNAPPER_CONFIGS} selected_configs=${selected_configs:-$SNAPPER_CONFIGS}
description=${description:-"latest incremental backup"} description=${description:-"latest incremental backup"}
@@ -202,117 +231,188 @@ parse_params () {
} }
run_config () { run_config () {
declare -a BACKUPDIRS_ARRAY
declare -a MYBACKUPDIR_ARRAY
declare -a OLD_NUM_ARRAY
declare -a OLD_SNAP_ARRAY
declare -a NEW_NUM_ARRAY
declare -a NEW_SNAP_ARRAY
declare -a NEW_INFO_ARRAY
declare -a BACKUPLOC_ARRAY
declare -a CONT_BACKUP_ARRAY
printf "\nInitial configuration...\n" | tee $PIPE printf "\nVerify configuration...\n" | tee $PIPE
# Initial configuration of where backup directories are # commandline selection takes precedence
i=0 if [ -n "$selected_config" ]; then
for x in $selected_configs; do selected_configs=$selected_config
if [[ "$(snapper -c $x list -t single | awk '/'"$selected_uuid"'/ {cnt++} END {print cnt}')" -gt 1 ]]; then
error "More than one snapper entry found with UUID $selected_uuid for configuration $x. Skipping configuration $x."
continue
fi fi
if [[ "$(snapper -c $x list -t single | awk '/'$progname' backup in progress/ {cnt++} END {print cnt}')" -gt 0 ]]; then # Pseudo Arrays $i -> store associated elements of selected_config
printf "\nNOTE: Previous failed %s backup snapshots found for '%s'.\n" "$progname" "$x" | tee $PIPE 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 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 read -r -p "Delete failed backup snapshots [y/N]? " delete_failed
while [[ -n "$delete_failed" && "$delete_failed" != [Yy]"es" && #get_answer_yes_no
"$delete_failed" != [Yy] && "$delete_failed" != [Nn]"o" && while [ -n "$delete_failed" ] &&
"$delete_failed" != [Nn] ]]; do [ "$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 read -r -p "Delete failed backup snapshots [y/N]? " delete_failed
if [[ -n "$delete_failed" && "$delete_failed" != [Yy]"es" && if [ -n "$delete_failed" ] &&
"$delete_failed" != [Yy] && "$delete_failed" != [Nn]"o" && [ "$delete_failed" != "Yes" ] &&
"$delete_failed" != [Nn] ]]; then [ "$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" printf "Select 'y' or 'N'.\n"
fi fi
done done
if [[ "$delete_failed" == [Yy]"es" || "$delete_failed" == [Yy] ]]; then if [ "$delete_failed" = "Yes" ] ||
snapper -c $x delete $(snapper -c $x list | awk '/'$progname' backup in progress/ {print $3}') [ "$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
fi fi
SNAP_SYNC_EXCLUDE=no SNAP_SYNC_EXCLUDE=no
if [[ -f "/etc/snapper/configs/$x" ]]; then if [ -f "/etc/snapper/configs/$selected_config" ]; then
source /etc/snapper/configs/$x . /etc/snapper/configs/$selected_config
if [ "$SUBVOLUME" = "/" ]; then
SUBVOLUME=''
fi
else else
die "Selected snapper configuration $x does not exist." die "Selected snapper configuration $selected_config does not exist."
fi fi
if [[ $SNAP_SYNC_EXCLUDE == "yes" ]]; then if [ $SNAP_SYNC_EXCLUDE = "yes" ]; then
continue continue
fi fi
printf "\n" printf "\n"
old_num=$(snapper -c "$x" list -t single | awk '/'"$selected_uuid"'/ {print $1}') # processed snapshot backup is marked with userdata key/value pairs
old_snap=$SUBVOLUME/.snapshots/$old_num/snapshot # backupdir, uuid
snapper_sync_id=$(eval snapper -c "$selected_config" list -t single | awk '/uuid='"$selected_uuid"'/ {print $1}')
OLD_NUM_ARRAY[$i]=$old_num if [ ${#snapper_sync_id} -gt 0 ]; then
OLD_SNAP_ARRAY[$i]=$old_snap snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot
if [[ -z "$old_num" ]]; then
printf "No backups have been performed for '%s' on this disk.\n" "$x"
read -r -p "Enter progname of directory to store backups, relative to $selected_mnt (to be created if not existing): " mybackupdir
printf "This will be the initial backup for snapper configuration '%s' to this disk. This could take awhile.\n" "$x"
BACKUPDIR="$selected_mnt/$mybackupdir"
$ssh mkdir -p -m700 "$BACKUPDIR"
else
mybackupdir=$(snapper -c "$x" list -t single | awk -F"|" '/'"$selected_uuid"'/ {print $5}' | awk -F "," '/backupdir/ {print $1}' | awk -F"=" '{print $2}')
BACKUPDIR="$selected_mnt/$mybackupdir"
$ssh test -d $BACKUPDIR || die "%s is not a directory on %s.\n" "$BACKUPDIR" "$selected_uuid"
fi fi
BACKUPDIRS_ARRAY[$i]="$BACKUPDIR"
MYBACKUPDIR_ARRAY[$i]="$mybackupdir"
printf "Creating new snapshot for %s...\n" "$x" | tee $PIPE eval "snapper_sync_id_$i='$snapper_sync_id'"
new_num=$(snapper -c "$x" create --print-number -d "$progname backup in progress") eval "snapper_sync_snapshot_$i='$snapper_sync_snapshot'"
new_snap=$SUBVOLUME/.snapshots/$new_num/snapshot
new_info=$SUBVOLUME/.snapshots/$new_num/info.xml 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
### Todo: if interactive, e.g use pv to show progress
printf "Please be pacient, the initial backup could take awhile.\n"
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"'/ {print $5}' | awk -F "," '/backupdir/ {print $1}' | awk -F"=" '{print $2}')
backup_root="$selected_target/$backupdir"
$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
printf "Creating new snapshot for 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 sync
backup_location=$BACKUPDIR/$x/$new_num/
if [[ -z $ssh ]]; then snapper_target_snapshot=$backup_root/$selected_config/$snapper_new_id
printf "Will backup %s to %s\n" "$new_snap" "$backup_location/snapshot" | tee $PIPE if [ -z "$ssh" ]; then
printf "Will backup %s to %s\n" "$snapper_new_snapshot" "$snapper_target_snapshot/snapshot" | tee $PIPE
else else
printf "Will backup %s to %s\n" "$new_snap" "$remote":"$backup_location/snapshot" | tee $PIPE printf "Will backup %s to %s\n" "$snapper_new_snapshot" "$remote":"$snapper_target_snapshot/snapshot" | tee $PIPE
fi fi
NEW_NUM_ARRAY[$i]="$new_num" # save in config specific infos in pseudo Arrays
NEW_SNAP_ARRAY[$i]="$new_snap" eval "snapper_new_id_$i='$snapper_new_id'"
NEW_INFO_ARRAY[$i]="$new_info" eval "snapper_new_snapshot_$i='$snapper_new_snapshot'"
BACKUPLOC_ARRAY[$i]="$backup_location" 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" cont_backup="K"
CONT_BACKUP_ARRAY[$i]="yes" eval "snapper_activate_$i=yes"
if [[ $noconfirm == "yes" ]]; then if [ $noconfirm ]; then
cont_backup="yes" cont_backup="yes"
else else
while [[ -n "$cont_backup" && "$cont_backup" != [Yy]"es" && while [ -n "$cont_backup" ] &&
"$cont_backup" != [Yy] && "$cont_backup" != [Nn]"o" && [ "$cont_backup" != "Yes" ] &&
"$cont_backup" != [Nn] ]]; do [ "$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 read -r -p "Continue with backup [Y/n]? " cont_backup
if [[ -n "$cont_backup" && "$cont_backup" != [Yy]"es" && if [ -n "$cont_backup" ] &&
"$cont_backup" != [Yy] && "$cont_backup" != [Nn]"o" && [ "$cont_backup" != "Yes" ] &&
"$cont_backup" != [Nn] ]]; then [ "$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" printf "Select 'Y' or 'n'.\n"
fi fi
done done
fi fi
if [[ "$cont_backup" != [Yy]"es" && "$cont_backup" != [Yy] && -n "$cont_backup" ]]; then if [ "$cont_backup" != "Yes" ] &&
CONT_BACKUP_ARRAY[$i]="no" [ "$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" printf "Aborting backup for this configuration.\n"
snapper -c $x delete $new_num snapper -c $selected_config delete $snapper_new_id
fi fi
i=$(($i+1)) i=$(($i+1))
@@ -323,114 +423,155 @@ run_config () {
run_backup () { run_backup () {
# Actual backing up # Actual backing up
printf "\nPerforming backups...\n" | tee $PIPE printf "\nPerforming backups...\n" | tee $PIPE
i=-1 i=-1
for x in $selected_configs; do for selected_config in $selected_configs; do
i=$(($i+1)) i=$(($i+1))
SNAP_SYNC_EXCLUDE=no SNAP_SYNC_EXCLUDE=no
if [[ -f "/etc/snapper/configs/$x" ]]; then if [ -f "/etc/snapper/configs/$selected_config" ]; then
source /etc/snapper/configs/$x . /etc/snapper/configs/$selected_config
else else
die "Selected snapper configuration $x does not exist." die "Selected snapper configuration '$selected_config' does not exist."
fi fi
cont_backup=${CONT_BACKUP_ARRAY[$i]} cont_backup=$(eval echo \$snapper_activate_$i)
if [[ $cont_backup == "no" || $SNAP_SYNC_EXCLUDE == "yes" ]]; then if [ "$cont_backup" = "no" ] || [ "$SNAP_SYNC_EXCLUDE" = "yes" ]; then
notify_info "Backup in progress" "NOTE: Skipping $x configuration." notify_info "Backup in progress" "NOTE: Skipping '$selected_config' configuration."
continue continue
fi fi
notify_info "Backup in progress" "Backing up $x configuration." notify_info "Backup in progress" "Backing up data for configuration '$selected_config'."
printf "\n" printf "\n"
old_num="${OLD_NUM_ARRAY[$i]}" # retrieve config specific infos from pseudo Arrays
old_snap="${OLD_SNAP_ARRAY[$i]}" snapper_config=$(eval echo \$snapper_config_$i)
BACKUPDIR="${BACKUPDIRS_ARRAY[$i]}" backup_root=$(eval echo \$backup_root_$i)
mybackupdir="${MYBACKUPDIR_ARRAY[$i]}" backup_dir=$(eval echo \$backup_dir_$i)
new_num="${NEW_NUM_ARRAY[$i]}" snapper_sync_id=$(eval echo \$snapper_sync_id_$i)
new_snap="${NEW_SNAP_ARRAY[$i]}" snapper_new_id=$(eval echo \$snapper_new_id_$i)
new_info="${NEW_INFO_ARRAY[$i]}" snapper_sync_snapshot=$(eval echo \$snapper_sync_snapshot_$i)
backup_location="${BACKUPLOC_ARRAY[$i]}" snapper_new_snapshot=$(eval echo \$snapper_new_snapshot_$i)
snapper_new_info=$(eval echo \$snapper_new_info_$i)
snapper_target_snapshot=$(eval echo \$snapper_target_snapshot_$i)
$ssh mkdir -p $backup_location # verify target structure for backupdir
verify_snapper_structure $backup_root $snapper_config $snapper_new_id
if [[ -z "$old_num" ]]; then
printf "Sending first snapshot for %s...\n" "$x" | tee $PIPE
btrfs send "$new_snap" | $ssh btrfs receive "$backup_location" &>/dev/null
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
printf "$cmd"
fi
btrfs send "$snapper_new_snapshot" | $ssh btrfs receive "$snapper_target_snapshot" &>/dev/null
else else
printf "Sending incremental snapshot for snapper config '%s'...\n" "$selected_config" | tee $PIPE
printf "Sending incremental snapshot for %s...\n" "$x" | tee $PIPE
# Sends the difference between the new snapshot and old snapshot to the # 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 # backup location. Using the -c flag instead of -p tells it that there
# is an identical subvolume to the old snapshot at the receiving # is an identical subvolume to the old snapshot at the receiving
# location where it can get its data. This helps speed up the transfer. # location where it can get its data. This helps speed up the transfer.
btrfs send -c "$old_snap" "$new_snap" | $ssh btrfs receive "$backup_location" cmd="btrfs send -c $snapper_sync_snapshot $snapper_new_snapshot | $ssh btrfs receive $snapper_target_snapshot"
printf "Modifying data for old snapshot for %s...\n" "$x" | tee $PIPE $cmd &>/dev/null
snapper -v -c "$x" modify -d "old snap-sync snapshot (you may remove)" -u "backupdir=,uuid=" -c "number" "$old_num" if [ "$verbose" ]; then
printf "$cmd"
printf "Deleting sync snapshot for %s...\n" "$selected_config" | tee $PIPE
fi
btrfs send -c "$snapper_sync_snapshot" "$snapper_new_snapshot" | $ssh btrfs receive "$snapper_target_snapshot" &>/dev/null
snapper -c "$selected_config" delete "$snapper_sync_id"
fi fi
if [[ -z $ssh ]]; then if [ -z "$ssh" ]; then
cp "$new_info" "$backup_location" cp "$snapper_new_info" "$snapper_target_snapshot"
else else
rsync -avzq "$new_info" "$remote":"$backup_location" rsync -avzq "$snapper_new_info" "$remote":"$snapper_target_snapshot"
fi fi
# It's important not to change this userdata in the snapshots, since that's how # It's important not to change the values of the key/value pairs ($userdata)
# we find the previous one. # which is stored in snappers info.xml file of the source snapshot.
# This is how we find the parent.
userdata="backupdir=$mybackupdir, uuid=$selected_uuid" userdata="backupdir=$backup_dir, uuid=$selected_uuid"
# Tag new snapshot as the latest # Tag new snapshot as the latest
printf "Tagging new snapshot as latest backup for %s...\n" "$x" | tee $PIPE printf "Tagging new snapshot as latest backup for '%s' ...\n" "$selected_config" | tee $PIPE
snapper -v -c "$x" modify -d "$description" -u "$userdata" "$new_num" snapper -v -c "$selected_config" modify -d "$description" -u "$userdata" "$snapper_new_id"
printf "Backup complete for snapper configuration '%s'.\n" "$selected_config" > $PIPE
printf "Backup complete for configuration %s.\n" "$x" > $PIPE
done done
printf "\nDone!\n" | tee $PIPE
exec 3>&- exec 3>&-
} }
select_target_disk () { select_target_disk () {
if [[ "${#UUIDS_ARRAY[$@]}" -eq 0 ]]; then local i=0
die "No external btrfs subvolumes found to backup to." local disk_id=0
fi
if [[ "$disk_count" > 1 ]]; then #local disk_selected_ids=''
printf "Multiple mount points were found with UUID %s.\n" "$uuid_cmdline" local disk_selected_count=0
disk="-1"
fi
if [[ "$disk" == -1 ]]; then # print selection table
if [[ "$disk_count" == 0 && "$uuid_cmdline" != "none" ]]; then if [ -z "$ssh" ]; then
error "A device with UUID $uuid_cmdline was not found to be mounted, or it is not a BTRFS device." printf "Selecting a mounted BTRFS device for backups on your local machine.\n"
fi
if [[ -z $ssh ]]; then
printf "Select a mounted BTRFS device on your local machine to backup to.\n"
else else
printf "Select a mounted BTRFS device on %s to backup to.\n" "$remote" printf "Selecting a mounted BTRFS device for backups on %s.\n" "$remote"
fi fi
while [[ $disk -lt 0 || $disk -gt $i ]]; do while [ "$disk_id" -eq -1 ] || [ "$disk_id" -le $disk_count ]; do
for x in "${!TARGETS_ARRAY[@]}"; do if [ "$disk_uuid_match_count" -gt 1 ]; then
printf "%4s) %s (%s)\n" "$((x+1))" "${UUIDS_ARRAY[$x]}" "${TARGETS_ARRAY[$x]}" # 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)
target=$(eval echo \$disk_target_$disk_uuid)
printf "%4s) %s %s\n" "$i" "$disk" "$target"
i=$((i+1))
done done
printf "%4s) Exit\n" "0" else
read -r -p "Enter a number: " disk while [ "$disk_id" -le $disk_count ]; do
if ! [[ $disk == ?(-)+([0-9]) ]]; then # 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)
printf "%4s) %s %s\n" "$disk_id" "$disk" "$target"
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" printf "\nNo disk selected. Select a disk to continue.\n"
disk=-1 disk_id=0
fi i=0
;;
esac
done done
if [[ $disk == 0 ]]; then if [ "$disk_selected" = x ]; then
exit 0 exit x
fi
disk=$(($disk-1))
fi fi
selected_uuid=$(eval echo \$disk_uuid_$disk_selected)
selected_target=$(eval echo \$disk_target_$disk_selected)
} }
traperror () { traperror () {
@@ -438,7 +579,7 @@ traperror() {
printf "exit status: %s\n" "$2" printf "exit status: %s\n" "$2"
printf "command: %s\n" "$3" printf "command: %s\n" "$3"
printf "bash line: %s\n" "$4" printf "bash line: %s\n" "$4"
printf "function progname: %s\n" "$5" printf "function name: %s\n" "$5"
exit 1 exit 1
} }
@@ -446,6 +587,10 @@ trapkill() {
die "Exited due to user intervention." die "Exited due to user intervention."
} }
# bashism
#trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCNAME[@]}"' ERR
#trap trapkill SIGTERM SIGINT
usage () { usage () {
cat <<EOF cat <<EOF
$progname $version $progname $version
@@ -466,15 +611,39 @@ Options:
-v, --verbose Be more verbose on what's going on. -v, --verbose Be more verbose on what's going on.
EOF EOF
exit 1
}
verify_snapper_structure () {
local backup_root=$1
local snapper_config=$2
local snapper_id=$3
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 $ssh [ ! -d $backup_root/$snapper_config/$snapper_id ]; then
if [ "$verbose" ]; then
echo "Create backup-path $backup_root/$snapper_config/$snapper_id"
fi
$ssh mkdir --mode=0700 $backup_root/$snapper_config/$snapper_id
fi
} }
### ###
# Main # Main
### ###
trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCPROGNAME[@]}"' ERR
trap trapkill SIGTERM SIGINT
cwd=`pwd` cwd=`pwd`
ssh="" ssh=""
@@ -482,33 +651,31 @@ parse_params $@
check_prerequisites check_prerequisites
if [[ "$uuid_cmdline" != "none" ]]; then # read mounted BTRFS structures
if [[ -z $ssh ]]; then get_disk_infos
if [ "$uuid_cmdline" != "none" ]; then
if [ -z $ssh ]; then
notify_info "Backup started" "Starting backups to $uuid_cmdline..." notify_info "Backup started" "Starting backups to $uuid_cmdline..."
else else
notify_info "Backup started" "Starting backups to $uuid_cmdline at $remote..." notify_info "Backup started" "Starting backups to $uuid_cmdline at $remote..."
fi fi
else else
if [[ -z $ssh ]]; then if [ -z $ssh ]; then
notify_info "Backup started" "Starting backups. Use command line menu to select disk." notify_info "Backup started" "Starting backups. Use command line menu to select disk."
else else
notify_info "Backup started" "Starting backups. Use command line menu to select disk on $remote." notify_info "Backup started" "Starting backups. Use command line menu to select disk on $remote."
fi fi
fi fi
# read mounted BTRFS structures
get_disk_infos
# select the target BTRFS subvol # select the target BTRFS subvol
select_target_disk select_target_disk
selected_uuid="${UUIDS_ARRAY[$((disk))]}"
selected_mnt="${TARGETS_ARRAY[$((disk))]}"
printf "\nYou selected the disk with UUID %s.\n" "$selected_uuid" | tee $PIPE printf "\nYou selected the disk with UUID %s.\n" "$selected_uuid" | tee $PIPE
if [[ -z $ssh ]]; then if [ -z $ssh ]; then
printf "The disk is mounted at %s.\n" "$selected_mnt" | tee $PIPE printf "The disk is mounted at %s.\n" "$selected_target" | tee $PIPE
else else
printf "The disk is mounted at %s:%s.\n" "$remote" "$selected_mnt" | tee $PIPE printf "The disk is mounted at %s:%s.\n" "$remote" "$selected_target" | tee $PIPE
fi fi
# create and initialize structures for snapper configs # create and initialize structures for snapper configs
@@ -517,7 +684,7 @@ run_config
# run backups using btrfs-send -> btrfs-receive # run backups using btrfs-send -> btrfs-receive
run_backup run_backup
if [[ "$uuid_cmdline" != "none" ]]; then if [ "$uuid_cmdline" != "none" ]; then
notify_info "Finished" "Backups to $uuid_cmdline complete!" notify_info "Finished" "Backups to $uuid_cmdline complete!"
else else
notify_info "Finished" "Backups complete!" notify_info "Finished" "Backups complete!"