#!/bin/sh # dsnap-sync # https://github.com/rzerres/dsnap-sync # Copyright (C) 2016, 2017 James W. Barnett # Copyright (C) 2017 - 2019 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.6.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 # Create TEMPDIR test ! -d $XDG_RUNTIME_DIR/$progname && mkdir -p $XDG_RUNTIME_DIR/$progname TMPDIR=$(mktemp --tmpdir=$XDG_RUNTIME_DIR/$progname -d) # define fifo pipes BTRFS_PIPE=$TMPDIR/btrfs.fifo test -p $BTRFS_PIPE && mkfifo $BTRFS_PIPE # global variables args= answer=no archive_type=full batch=0 btrfs_quota=0 btrfs_quota_tmp=1 #btrfs_verbose_flag=--verbose btrfs_verbose_flag= color=0 donotify=0 dryrun=0 #disk_count=-1 disk_uuid_match_count=0 #disk_target_match_count=0 disk_subvolid_match_count=0 disk_uuid_match='' error_count=0 ltfs_mountpoint="/media/tape" target_count=0 target_match_count=0 tape_match='' interactive=1 selected_configs=0 selected_subvol='none' selected_target='none' selected_uuid='none' snapper_sync_id=0 snapper_snapshots=".snapshots" # hardcoded in snapper snapper_snapshot_name="snapshot" # hardcoded in snapper snapper_subvolume_template="dsnap-sync" snapper_backup_type='none' #snapper_config_postfix="."`hostname` snapper_config_postfix= snap_cleanup_algorithm="timeline" verbose=0 volume_name= # 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 ssh >/dev/null 2>&1 || { printf "'ssh' is not installed." && exit 1; } which scp >/dev/null 2>&1 || { printf "'scp' 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; } # optional binaries: which attr >/dev/null 2>&1 || { printf "'attr' is not installed." && exit 1; } which ionice >/dev/null 2>&1 && { do_ionice_cmd=1; } which notify-send >/dev/null 2>&1 && { donotify=1; } which pv >/dev/null 2>&1 && { do_pv_cmd=1; } which tape-admin >/dev/null 2>&1 && { tape_admin_cmd=1; } which nc >/dev/null 2>&1 && { nc_cmd=1; } if [ $(id -u) -ne 0 ] ; then printf "$progname: must be run as root\n" ; exit 1 ; fi if [ ! -r "$SNAPPER_CONFIG" ]; then die "$progname: $SNAPPER_CONFIG does not exist." fi } check_snapper_failed_ids () { local selected_config=${1} local batch=${2:-0} if [ $verbose -ge 3 ]; then printf "${MAGENTA}check_snapper_failed_ids()...${NO_COLOR}\n" fi # 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=$(snapper --config $selected_config list --type single \ | awk '/'"$snap_description_running"'/ {cnt++} END {print cnt}') #snapper_failed_ids="snapper --config $selected_config list --type single \ # | awk '/'"$snap_description_running"'/' \ # | awk ' /'host='"$remote"'/ {cnt++} END {print cnt}'" if [ ${#snapper_failed_ids} -gt 0 ]; then if [ "$batch" ]; then answer="yes" else printf "${MAGENTA}Found %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 failed_id_first=$(snapper --config "$selected_config" list --type single \ | awk -F '|' ' /'"$snap_description_running"'/' \ | awk ' NR==1 {print $1} ') failed_id_last=$(snapper --config "$selected_config" list --type single \ | awk -F '|' ' /'"$snap_description_running"'/' \ | awk ' END {print $1} ') cmd="snapper --config $selected_config delete" if [ $failed_id_first -lt $failed_id_last ]; then $(eval $cmd $failed_id_first-$failed_id_last) else $(eval $cmd $failed_id_first) fi fi fi } create_snapshot () { if [ $verbose -ge 3 ]; then printf "${MAGENTA}create_snapshot()...${NO_COLOR}\n" $snapper_config fi # acting on source system if [ $dryrun -eq 0 ]; then #printf "Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE if [ $verbose -ge 3 ]; then printf "${MAGENTA}Create new snapshot using snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ "$selected_config" fi if [ -z $remote ]; then ret=$(snapper --config "$selected_config" create \ --print-number \ --description "$snap_description_running" \ --userdata "host=$HOSTNAME") else ret=$(snapper --config "$selected_config" create \ --print-number \ --description "$snap_description_running" \ --userdata "host=$remote") fi if [ "$ret" ]; then snapper_source_id=$ret else # if snapper call fails, return value will do be a snapshot-id printf "${RED}Creation of snapper source snapshot failed${NO_COLOR}.\n" "$snapper_source_id" snapper_source_id=-1 return 1 fi if [ $SUBVOLUME = "/" ]; then SUBVOLUME="" fi snapper_source_snapshot=$SUBVOLUME/.snapshots/$snapper_source_id/$snapper_snapshot_name snapper_source_info=$SUBVOLUME/.snapshots/$snapper_source_id/info.xml #btrfs quota enable $snapper_source_snapshot sync if [ $verbose -ge 3 ]; then printf "${MAGENTA}Snapper source snapshot ${GREEN}'%s'${MAGENTA} created${NO_COLOR}\n" "$snapper_source_id" fi else printf "${MAGENTA}dryrun${NO_COLOR}: Would create source snapshot with snapper config '%s' ...\n" "$selected_config" snapper_source_sync_id="" fi } die () { error "$@" exit 1 } error () { printf "\n==> ERROR: %s\n" "$@" notify_error 'Error' 'Check journal for more information.' } >&2 get_answer_yes_no () { 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_archive_last_sync_id () { local snapper_config=${1#snapper_config=} local archive_type=${2##archive_type=} local remote=${3##remote=} local run_ssh='' snapper_sync_id=0 if [ ${#remote} -ge 1 ]; then run_ssh=$ssh; fi if [ $verbose -ge 3 ]; then printf "${MAGENTA}get_archive_last_sync_id()...${NO_COLOR}\n" fi if [ $verbose -ge 3 ]; then if [ $remote ]; then printf "${MAGENTA}Get sync-ID of ${GREEN}last '%s' btrfs-stream${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} on remote ${GREEN}'%s'${NO_COLOR}\n" \ $archive_type $snapper_config $remote else printf "${MAGENTA}Get sync-ID of ${GREEN}last '%s' btrfs-stream${MAGENTA} for snapper config ${GREEN}'%s'${NO_COLOR}\n" \ $archive_type $snapper_config fi fi # target is LTFS tape if [ ${#volume_name} -gt 1 ]; then case ${archive_type} in #incremental) # cmd="find ${selected_target}/${backupdir}/${snapper_target_config} -name *_${archive_type}.btrfs \ # | awk -F '.*/' '{ gsub(/_${archive_type}.btrfs\$/,"_"); print \$2}' \ # | awk ' \$1 == $snapper_source_sync_id {print \$1} ' " # ;; *) cmd="find ${selected_target}/${backupdir}/${snapper_target_config} -name *_${archive_type}.btrfs \ | awk -F '.*/' '{ gsub(/_${archive_type}.btrfs\$/,"_"); cnt++} END {print cnt}'" ret=$(eval $run_ssh "$cmd" 2>/dev/null) if [ ${#ret} -ge 1 ]; then if [ $verbose -ge 3 ]; then printf "${MAGENTA}Found ${GREEN}'%s'${MAGENTA} previous synced archives for config ${GREEN}'%s'${NO_COLOR}\n" \ "${ret}" "$selected_config" fi # get last sync-id cmd="find ${selected_target}/${backupdir}/${snapper_target_config} -name *_${archive_type}.btrfs \ | awk -F '.*/' '{ gsub(/_${archive_type}.btrfs\$/,"_") } END {print \$2}'" else return 1 fi ;; esac fi ret=$(eval $run_ssh "$cmd" 2>/dev/null) if [ ${#ret} -ge 1 ]; then # ok, matching snapshot found snapper_sync_id=$ret if [ $verbose -ge 3 ]; then printf "Got archive snapshot: ${GREEN}'%s'${NO_COLOR} (id: ${GREEN}'%s'${NO_COLOR})\n" \ $snapper_target_config $snapper_sync_id fi return 0 else # no snapshot found return 1 fi } get_backupdir () { local backup_dir=$1 if [ $verbose -ge 3 ]; then printf "${MAGENTA}get_backupdir()...${NO_COLOR}\n" $snapper_config fi backupdir=$backup_dir if [ ! $batch ]; then answer=yes if [ -z "$backupdir" ]; then get_answer_yes_no "Keep empty backupdir [Y/n]? " "$answer" else get_answer_yes_no "Keep backupdir '$backupdir' [Y/n]? " "$answer" fi if [ "$answer" = "no" ]; then read -r -p "Enter name of directory to store backups, relative to $selected_target (to be created if not existing): " backupdir fi fi if [ $verbose -ge 2 ] && [ -n "$backupdir" ]; then printf "${MAGENTA}Backup-Dir is ${GREEN}'%s'${NO_COLOR}\n" \ "$backupdir" fi } get_media_infos () { # select the target LTFS tape if [ ${#mediapool_name} -gt 1 ] || [ ${#volume_name} -gt 1 ]; then # read mounted LTFS structures if [ -z $remote ]; then tape-admin --verbose=$verbose --mount ${mediapool_name} ${volume_name} else $ssh tape-admin --version $ssh tape-admin --verbose=$verbose --mount ${mediapool_name} ${volume_name} fi if [ $? -eq 0 ]; then target_cmdline=$ltfs_mountpoint if [ ${#volume_name} -eq 0 ]; then # parse out volume-name (fuse - extended attribute) volume_name=$($ssh attr -g ltfs.volumeName $ltfs_mountpoint) volume_name=$(echo ${volume_name##*:} | sed -e 's/\r\n//g') fi get_tape_infos if [ $? -ne 0 ]; then printf "${RED}Error: ${NO_COLOR}Can't use valid volume from MediaPool ${GREEN}'%s'${NO_COLOR}\n" \ "$mediapool_name" die "Can't use valid tape." fi else if [ ${#volume_name} -gt 0 ]; then printf "${RED}Error: ${NO_COLOR}Can't use volume ${GREEN}'%s'${NO_COLOR} from MediaPool ${GREEN}'%s'${NO_COLOR}\n" \ "$volume_name" "$mediapool_name" else printf "${RED}Error: ${NO_COLOR}Can't use valid volume from MediaPool ${GREEN}'%s'${NO_COLOR}\n" \ "$mediapool_name" fi die "Can't use valid tape." fi else # read mounted BTRFS structures get_disk_infos fi } get_snapper_backup_type () { local snapper_config=$1 local snapper_config_type='none' local key local value # Snapshot types: btrfs-snapshot, btrfs-clone, btrfs-archive # snapshot: 1st stage: CHILD_CONFIG="false" or missing # clone: 2nd stage: CHILD_CONFIG="true" PARENT_CONFIG="" # archive: 3nd stage: CHILD_CONFIG="true" PARENT_CONFIG="" # parse selected_config and return with $snapper_target_config set appropriately if [ $verbose -ge 3 ]; then printf "${MAGENTA}get_snapper_backup_type()...${NO_COLOR}\n" fi if [ ${#backuptype_cmdline} -gt 1 ]; then snapper_backup_type=${backuptype_cmdline} else if [ $verbose -ge 2 ]; then printf "${MAGENTA}Get snapper backup type for config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" $snapper_config fi # WIP -> create a PR for snapper # Patch snapper to parse for new pairs: CONFIG_TYPE="child | root"; CONFIG_PARENT="" ##config_type=$(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 case $key in CONFIG_TYPE) snapper_backup_type=$(echo $value | sed -e 's/\"\(.*\)\"/\1/') continue ;; CHILD_CONFIG) snapper_target_config=$(echo $value | sed -e 's/\"\(.*\)\"/\1/') continue ;; PARENT_CONFIG) snapper_parent_config=$(echo $value | sed -e 's/\"\(.*\)\"/\1/') continue ;; *) # value is not relevant continue ;; esac done < $SNAPPER_CONFIG_DIR/$snapper_config fi case $snapper_backup_type in archive|Archive) # archive btrfs-snapshot to non btrfs-filesystem snapper_backup_type=btrfs-archive snapper_target_config=archive-$snapper_config ;; child|Child) # clone to btrfs-snapshot snapper_backup_type=btrfs-clone snapper_target_config=clone-$snapper_config ;; parent|Parent) # disk2disk btrfs-snapshot snapper_backup_type=btrfs-snapshot snapper_target_config=$snapper_config ;; *) # disk2disk btrfs-snapshot (default) snapper_backup_type=btrfs-snapshot snapper_target_config=$snapper_config ;; esac if [ -n $snapper_config_postfix ]; then snapper_target_config=${snapper_target_config}${snapper_config_postfix} fi if [ $verbose -ge 2 ]; then printf "Snapper backup type: '%s'\n" $snapper_backup_type printf "Snapper target configuration: '%s'\n" $snapper_target_config printf "Snapper parent configuration: '%s'\n" $snapper_parent_config fi } get_snapper_config_value () { local remote=${1##remote=} local config_file=${2##snapper_config=} local config_key=${3##config_key=} local run_ssh='' if [ $verbose -ge 3 ]; then printf "${MAGENTA}get_snapper_config_value()...${NO_COLOR}\n" fi [ ${#remote} -gt 0 ] && run_ssh=$ssh value=$(eval $run_ssh cat $config_file \ | awk '/'"$config_key"'/' \ | awk -F "=" '{ gsub("\"",""); print $2}') if [ $verbose -ge 3 ]; then printf "Snapper ${GREEN}config file${NO_COLOR}: '%s'\n" \ "$config_file" printf "Snapper key ${GREEN}'%s'${NO_COLOR}: '%s'\n" \ "$config_key" "$value" fi } get_snapper_target_backupdir () { local backupdir_cmdline=$1 if [ $verbose -ge 3 ]; then printf "${MAGENTA}get_snapper_target_backupdir()...${NO_COLOR}\n" $snapper_config fi if [ $snapper_target_sync_id -gt 0 ]; then # get metadata for backupdir from sync snapshot backupdir=$(snapper --config "$selected_config" list --type single \ | awk -F "|" '/'"$snap_description_synced"'/' \ | awk -F "|" '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {print $5}' \ | awk -F "," '/backupdir/ {print $1}' \ | awk -F"=" '{print $2}') # if not found, get metadata for backupdir from finished snapshot if [ -z "$backupdir" ]; then backupdir=$(snapper --config "$selected_config" list --type single \ | awk -F "|" '/'"$snap_description_finished"'/' \ | awk -F "|" '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {print $5}' \ | awk -F "," '/backupdir/ {print $1}' \ | awk -F"=" 'END {print $2}') fi if [ "$backupdir_cmdline" != 'none' ] && [ "$backupdir_cmdline" != "$backupdir" ] \ && [ "${#backupdir}" -gt 0 ] ; then if [ $verbose -ge 2 ]; then # WIP: we need to adapt existing target config SUBVOLUME to reflect the new backupdir printf "${RED}TODO: ${MAGENTA}Reset backupdir ${GREEN}'%s'${MAGENTA} as requested per commandline.${NO_COLOR}\n" \ "$backupdir" printf "${RED}TODO: ${NO_COLOR}Need to adapt ${GREEN}SUBVOLUME${NO_COLOR} for existing ${GREEN}target-config${NO_COLOR}, to reflect the new backupdir.\n" fi backupdir=$backupdir_cmdline #printf "${RED}Error: ${MAGENTA}Changing the backupdir for an already existing target-config is not supported yet${NO_COLOR}\n" #return 1 elif [ "$backupdir_cmdline" != 'none' ] && [ -z "$backupdir" ] ; then backupdir=$backupdir_cmdline fi fi } get_disk_infos () { local disk_uuid local disk_target local fs_option if [ $verbose -ge 3 ]; then printf "${BLUE}get_disk_infos()...${NO_COLOR}\n" fi # wakeup automounter units if [ ${#automount_path} -gt 0 ]; then if [ $verbose -ge 3 ]; then printf "${MAGENTA}Mount automounter unit ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ "$automount_path" fi if [ $remote ]; then ret=$(systemctl --host=$remote status *.automount \ | awk -F ': ' '$0 ~ pattern { print $2 }' \ pattern="Where: $automount_path") else ret=$(systemctl status *.automount \ | awk -F ': ' '$0 ~ pattern { print $2 }' \ pattern="Where: $automount_path") fi if [ ${#ret} -gt 0 ]; then ret=$($ssh stat --file-system --terse $ret) fi fi # get mounted BTRFS infos if [ "$($ssh findmnt --noheadings --nofsroot --mountpoint / --output FSTYPE)" = "btrfs" ]; then # target location is mounted as root subvolume if [ -z $remote ]; then # local root filesystem will be excluded as a valid target location exclude_uuid=$(findmnt --noheadings --nofsroot --mountpoint / --output UUID) disk_uuids=$(findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list \ | grep -v $exclude_uuid \ | awk '{print $1}') disk_targets=$(findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list \ | grep -v $exclude_uuid \ | awk '{print $2}') fs_options=$(findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list \ | grep -v $exclude_uuid \ | awk '{print $2}') else # remote root filesystem will be excluded as a valid target location sleep 0.2 exclude_uuid=$($ssh findmnt --noheadings --nofsroot --mountpoint / --output UUID) sleep 0.2 disk_uuids=$($ssh "findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list \ | grep -v \"$exclude_uuid\" \ | awk '{print \$1}'") sleep 0.2 disk_targets=$($ssh "findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list \ | grep -v \"$exclude_uuid\" \ | awk '{print \$2}'") sleep 0.2 fs_options=$($ssh "findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list \ | grep -v \"$exclude_uuid\" \ | awk '{print \$2}'") fi else # target location is not mounted as root subvolume sleep 0.2 disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID --list) sleep 0.2 disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output TARGET --list) sleep 0.2 fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | awk '{print $2}') sleep 0.2 fi # we need at least one target disk if [ ${#disk_targets} -eq 0 ]; then printf "${RED}Error: ${MAGENTA}No suitable target disk found${NO_COLOR}" return 1 fi # Posix Shells do not support Array. Therefore using ... # Pseudo-Arrays (assumption: equal number of members) # Pseudo-Array: disk_uuid_$i # Pseudo-Array: disk_target_$i # Pseudo-Array: fs_options_$i # Pseudo-Array: fs_type_$i # Pseudo-Array: disk_selected_$i (reference to $i element) # List: disk_uuid_match (reference to matching preselected uuids) # List: disk_target_match (reference to matching preselected targets) # initialize our structures i=0 eval "fs_type_$i='btrfs'" for disk_uuid in $disk_uuids; do if [ "$disk_uuid" = "$uuid_cmdline" ]; then if [ ${#disk_uuid_match} -gt 0 ]; then disk_uuid_match="${disk_uuid_match} $i" else disk_uuid_match="$i" fi disk_uuid_match_count=$(($disk_uuid_match_count+1)) fi eval "disk_uuid_$i='$disk_uuid'" eval "fs_type_$i='btrfs'" target_count=$(($target_count+1)) i=$((i+1)) done i=0 for disk_target in $disk_targets; do if [ "$disk_target" = "$target_cmdline" ]; then target_match="$i" target_match_count=$(($target_match_count+1)) fi eval "target_$i='$disk_target'" i=$((i+1)) done i=0 for fs_option in $fs_options; do subvolid=$(eval echo \$fs_option | sed -e 's/.*subvolid=\([0-9]*\).*/\1/') if [ "$subvolid" = "$subvolid_cmdline" ]; then disk_subvolid_match="$i" disk_subvolid_match_count=$(($disk_subvolid_match_count+1)) fi eval "fs_options_$i='$fs_option'" i=$((i+1)) done } get_tape_infos () { local tape_id local tape_target local tape_fs_option if [ $verbose -ge 3 ]; then printf "${BLUE}get_tape_infos()...${NO_COLOR}\n" fi # get infos from mounted LTFS tapes if [ -z $remote ]; then # on localhost if [ "$(findmnt --noheadings --nofsroot --types fuse --output SOURCE | awk -F ':' '{print $1}')" = "ltfs" ]; then tape_ids=$(findmnt --noheadings --nofsroot --types fuse --output SOURCE --list \ | awk -F ':' '{print $2}') tape_targets=$(findmnt --noheadings --nofsroot --types fuse --output TARGET --list \ | awk '{print $1}') fs_options=$(findmnt --noheadings --nofsroot --types fuse --output SOURCE,OPTIONS --list \ | awk '{print $2}') fi else # on remote host if [ "$($ssh findmnt --noheadings --nofsroot --types fuse --output SOURCE --list | awk -F ':' '{print $1}')" = "ltfs" ]; then tape_ids=$($ssh findmnt --noheadings --nofsroot --types fuse --output SOURCE --list \ | awk -F ':' '{print $2}') tape_targets=$($ssh findmnt --noheadings --nofsroot --types fuse --output TARGET --list \ | awk '{print $1}') fs_options=$($ssh findmnt --noheadings --nofsroot --types fuse --output SOURCE,OPTIONS --list \ | awk '{print $2}') fi fi # we need at least one target disk if [ ${#tape_targets} -eq 0 ]; then printf "${RED}Error: ${MAGENTA}No suitable LTFS tape mounted yet${NO_COLOR}\n" return 1 fi # Posix Shells do not support Array. Therefore using ... # Pseudo-Arrays (assumption: equal number of members) # Pseudo-Array: tape_id_$i # Pseudo-Array: tape_target_$i # Pseudo-Array: fs_options_$i # Pseudo-Array: fs_type_$i # Pseudo-Array: tape_selected_$i (reference to $i element) # List: tape_id_match (reference to matching preselected uuids) # List: tape_target_match (reference to matching preselected targets) # initialize our structures #i=0 y=$(($target_count+1)) i=$y for tape_id in $tape_ids; do if [ "$tape_id" = "$tapeid_cmdline" ]; then if [ ${#tape_id_match} -gt 0 ]; then tape_id_match="${tape_id_match} $i" else tape_id_match="$i" fi tape_id_match_count=$(($tape_id_match_count+1)) fi eval "tape_id_$i='$tape_id'" eval "fs_type_$i='ltfs'" target_count=$(($target_count+1)) i=$((i+1)) done i=$y for tape_target in $tape_targets; do if [ "$tape_target" = "$target_cmdline" ]; then target_match="$i" target_match_count=$(($target_match_count+1)) fi eval "target_$i='$tape_target'" i=$((i+1)) done #i=0 i=$y for fs_option in $fs_options; do eval "fs_options_$i='$fs_option'" i=$((i+1)) done } get_snapper_last_sync_id () { local snapper_config=${1#snapper_config=} local snapper_description=${2##snapper_description=} local snapper_uuid=${3##snapper_uuid=} local snapper_subvolid=${4##snapper_subvolid=} local snapper_tapeid=${5##snapper_tapeid=} local snapper_backupdir=${6##snapper_backupdir=} local remote=${7##remote=} local run_ssh='' snapper_sync_id=0 if [ ${#remote} -ge 1 ];then run_ssh=$ssh; fi if [ $verbose -ge 3 ]; then printf "${MAGENTA}get_snapper_last_sync_id()...${NO_COLOR}\n" #printf "${MAGENTA}snapper_description: ${GREEN}'%s'${NO_COLOR}\n" \ # "${snapper_description}" fi # only process, if config does exist cmd="$run_ssh stat --format %i $SNAPPER_CONFIG_DIR/$snapper_config 2>/dev/null" if [ -z $(eval $cmd) ]; then if [ $verbose -ge 3 ]; then if [ $remote ]; then printf "${MAGENTA}snapper config ${GREEN}'%s'${MAGENTA} on remote ${GREEN}'%s'${MAGENTA} does not exist yet.${NO_COLOR}\n" \ $snapper_config $remote else printf "${MAGENTA}snapper config ${GREEN}'%s'${MAGENTA} does not exist yet.${NO_COLOR}\n" \ $snapper_config fi fi return 1 fi if [ $verbose -ge 3 ]; then if [ $remote ]; then printf "${MAGENTA}Get last sync-ID for snapper config ${GREEN}'%s'${MAGENTA} on remote ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ $snapper_config $remote else printf "${MAGENTA}Get last sync-ID for snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ $snapper_config fi fi if [ ${#snapper_subvolid} -ge 1 -a ${#snapper_uuid} -ge 1 ]; then cmd="snapper --config $snapper_config list --type single \ | awk '/$snapper_description/' \ | awk '/subvolid="$snapper_subvolid", uuid="$snapper_uuid"/' \ | awk 'END {print \$1}'" elif [ ${#snapper_tapeid} -ge 1 ]; then # | awk '/"$snapper_description"' '/"$snap_description_finished/"' \ cmd="snapper --config $snapper_config list --type single \ | awk '/$snapper_description/' \ | awk '/tapeid="$snapper_tapeid"/' \ | awk 'END {print \$1}'" elif [ ${#backup_dir} -ge 1 ]; then cmd="snapper --config $snapper_config list --type single \ | awk '/$snapper_description/' \ | awk '/backupdir="$backup_dir"/' \ | awk 'END {print \$1}'" else cmd="snapper --config $snapper_config list --type single \ | awk '/$snapper_description/' \ | awk 'END {print \$1}'" fi snapper_sync_id=$(eval $run_ssh "$cmd") if [ ${#snapper_sync_id} -ge 1 ]; then # ok, matching snapshot found snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/$snapper_snapshot_name else # no snapshot found, grap latest successfull sync if [ ${#snapper_subvolid} -ge 1 -a ${#snapper_uuid} -ge 1 ]; then cmd="snapper --config $snapper_config list --type single \ | awk '/$snap_description_finished/' \ | awk '/subvolid="$selected_subvol", uuid="$selected_uuid"/' \ | awk 'END {print \$1}'" elif [ ${#snapper_tapeid} -ge 1 ]; then cmd="snapper --config $snapper_config list --type single \ | awk '/$snap_description_finished/' \ | awk '/tapeid="$snapper_tapeid"/' \ | awk 'END {print \$1}'" else cmd="snapper --config $snapper_config list --type single \ | awk '/$snap_description_finished/' \ | awk 'END {print \$1}'" fi snapper_sync_id=$(eval $run_ssh "$cmd") if [ ${#snapper_sync_id} -ge 1 ]; then # ok, matching snapshot found snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/$snapper_snapshot_name else # no snapshot available snapper_sync_id=0 fi fi } get_snapper_sync_id () { local snapper_config=${1#snapper_config=} local remote=${2##remote=} local run_ssh='' local ret= if [ $verbose -ge 3 ]; then printf "${MAGENTA}get_snapper_sync_id()...${NO_COLOR}\n" fi if [ $verbose -ge 3 ]; then if [ $remote ]; then printf "${MAGENTA}Get sync-ID ${GREEN}'%s'${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} on remote ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ $snapper_sync_id $snapper_config $remote else printf "${MAGENTA}Get sync-ID ${GREEN}'%s'${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ $snapper_sync_id $snapper_config fi fi [ ${#remote} -gt 0 ] && run_ssh=$ssh cmd="snapper --config "$snapper_config" list --type single \ | awk -F '|' ' \$1 == $snapper_sync_id { gsub(/ /,_); print \$1} '" ret=$(eval $run_ssh "$cmd") if [ ${#ret} -ge 1 ]; then # ok, matching snapshot found snapper_sync_id=$ret if [ $verbose -ge 3 ]; then printf "Got source snapshot: ${GREEN}'%s'${NO_COLOR} (id: ${GREEN}'%s'${NO_COLOR})\n" \ $snapper_config $snapper_sync_id fi return 0 else # no snapshot found return 1 fi } notify () { # estimation: batch calls should just log if [ $donotify -gt 0 ]; then for u in $(users | sed 's/ /\n/g' | sort -u); do sudo -u $u DISPLAY=:0 \ DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(sudo -u $u id -u)/bus \ notify-send -a $progname "$progname: $1" "$2" --icon="dialog-$3" done else printf "%s\n" "$2" fi } notify_error () { notify "$1" "$2" "error" } notify_info () { notify "$1" "$2" "information" } parse_params () { #printf "\n${BLUE}Parse arguments...${NO_COLOR}\n" # Evaluate given call parameters i=0 while [ $# -gt 0 ]; do key="$1" case $key in -h | --help | \-\? | --usage) # Call usage() function. usage ;; -a|--automount) automount_path="$2" shift 2 ;; -b|--backupdir) backupdir_cmdline="$2" shift 2 ;; --backuptype) backuptype_cmdline="$2" shift 2 ;; -c|--config) # Pseudo-Array: selected_config_$i eval "selected_config_$i='${2}'" i=$(($i+1)) selected_configs=$i shift 2 ;; --color) color=1 shift 1 ;; --config-postfix) snapper_config_postfix="$2" shift 2 ;; --description-finished) shift snap_description_finished="${*}" snap_description_finished="${snap_description_finished%% -*}" params=$* set -- $snap_description_finished count=$# set -- $params shift $count ;; --description-running) shift snap_description_running=${*} snap_description_running="${snap_description_running%% -*}" params=$* set -- $snap_description_running count=$# set -- $params shift $count ;; -d|--description|--description-synced) shift snap_description_synced="${*}" snap_description_synced="${snap_description_synced%% -*}" params=$* set -- $snap_description_synced count=$# set -- $params shift $count ;; --dry-run|--dryrun) dryrun=1 shift 1 ;; -i|--interactive) interactive=1 donotify=1 shift ;; -n|--noconfirm|--batch) batch=1 interactive=0 do_pv_cmd=0 donotify=0 shift ;; --mediapool) mediapool_name="$2" shift 2 ;; --mode) backup_mode="$2" shift 2 ;; --nonotify) donotify=0 shift 1 ;; --nopv) do_pv_cmd=0 shift 1 ;; --noionice) do_ionice_cmd=0 shift 1 ;; -p|--port) port=$2 shift 2 ;; --remote) remote=$2 shift 2 ;; -s|--subvolid|--SUBVOLID) subvolid_cmdline="$2" shift 2 ;; -t|--target|--TARGET) target_cmdline="$2" shift 2 ;; --volumename) volume_name="$2" shift 2 ;; -u|--uuid|--UUID) uuid_cmdline="$2" shift 2 ;; --use-btrfs-quota) btrfs_quota=1 shift 1 ;; -v|--verbose) verbose=$(($verbose + 1)) shift 1 ;; --version) printf "%s v%s\n" "$progname" "$version" exit 0 ;; --automount=*) automount_path=${1#*=} shift ;; --backupdir=*) backupdir_cmdline=${1#*=} shift ;; --backuptype=*) backuptype_cmdline=${1#*=} shift ;; --config=*) # Pseudo-Array: selected_config_$i eval "selected_config_$i='${1#*=}'" i=$(($i+1)) selected_configs=$i shift ;; --config-postfix=*) snapper_config_postfix="${1#*=}" shift ;; --color=*) case ${1#*=} in yes | Yes | True | true) color=1; ;; *) ;; esac shift ;; --description-finished=*) snap_description_finished="${*#*=}" snap_description_finished="${snap_description_finished%% -*}" params_new=${*#*=} params_new=${params_new##${snap_description_finished}} if [ ${#params_new} -gt 0 ]; then set -- $snap_description_finished count=$# set -- $params_new fi ;; --description-running=*) snap_description_running="${*#*=}" snap_description_running="${snap_description_running%% -*}" params_new=${*#*=} params_new=${params_new##${snap_description_running}} params=$# if [ ${#params_new} -gt 0 ]; then set -- $snap_description_running count=$# set -- $params_new fi ;; -d=*|--description=*|--description-synced=*) snap_description_synced="${*#*=}" snap_description_synced="${snap_description_synced%% -*}" params_new=${*#*=} params_new=${params_new##${snap_description_synced}} if [ ${#params_new} -gt 0 ]; then set -- $snap_description_synced count=$# set -- $params_new else break fi ;; --mediapool=*) mediapool_name=${1#*=} shift ;; --mode=*) backup_mode=${1#*=} shift ;; --port=*) port=${1#*=} shift ;; --remote=*) remote=${1#*=} shift ;; --subvolid=*|--SUBVOLID=*) subvolid_cmdline=${1#*=} shift ;; --target=*|--TARGET=*) target_cmdline=${1#*=} shift ;; --uuid=*|--UUID=*) uuid_cmdline=${1#*=} shift ;; --v=* | --verbose=*) verbose=${1#*=} shift ;; --volumename=*) volume_name=${1#*=} shift ;; --) # End of all options shift break ;; -*) printf "WARN: Unknown option (ignored): $1" >&2 die "Unknown option" ;; *) printf "Unknown option: %s\nRun '%s -h' for valid options.\n" $key $progname die "Unknown option" ;; esac done # Set reasonable defaults if [ $selected_configs -eq 0 ]; then . $SNAPPER_CONFIG i=0 for selected_config in $SNAPPER_CONFIGS; do # Pseudo-Array: target_selected_$i (reference to $disk_uuid element) eval "selected_config_$i='$selected_config'" i=$(($i+1)) selected_configs=$i done fi # message-text used for snapper fields snap_description_finished=${snap_description_finished:-"dsnap-sync backup"} snap_description_running=${snap_description_running:-"dsnap-sync in progress"} snap_description_synced=${snap_description_synced:-"dsnap-sync last incremental"} uuid_cmdline=${uuid_cmdline:-"none"} target_cmdline=${target_cmdline:-"none"} subvolid_cmdline=${subvolid_cmdline:-"none"} backupdir_cmdline=${backupdir_cmdline:-"none"} if [ -n "$remote" ]; then ssh="ssh $remote" scp="scp" if [ ! -z "$port" ]; then ssh="$ssh -p $port" scp="$scp -P $port" else port=22 fi if [ $nc_cmd ]; then nc -w 3 -z $remote $port > /dev/null || \ die "Can't connect to remote host." else $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 fi if [ "$color" ]; then # ascii color BLUE='\033[0;34m' GREEN='\033[0;32m' MAGENTA='\033[0;35m' RED='\033[0;31m' YELLOW='\033[0;33m' NO_COLOR='\033[0m' fi if [ $verbose -ge 2 ]; then printf "${BLUE}$progname (runtime arguments)...${NO_COLOR}\n" printf "for backup-source:\n" #printf " selected configs: '%s'\n" "$selected_configs" i=0 while [ $i -lt $selected_configs ]; do printf " selected config: '%s'\n" "$(eval echo \$selected_config_$i)" i=$(($i+1)) done printf "for backup-target:\n" printf " disk UUID: '%s'\n" "$uuid_cmdline" printf " disk SUBVOLID: '%s'\n" "$subvolid_cmdline" printf " tape MediaPool: '%s'\n" "$mediapool_name" printf " tape VolumeName: '%s'\n" "$volume_name" printf " TARGET name: '%s'\n" "$target_cmdline" printf " Backupdir: '%s'\n" "$backupdir_cmdline" printf " Backup Type: '%s'\n" "$backuptype_cmdline" printf " Backup Mode: '%s'\n" "$backup_mode" printf " config postfix: '%s'\n" "$snapper_config_postfix" printf " remote host: '%s'\n" "$remote" printf " ssh options: '%s'\n" "$ssh" printf "Snapper Descriptions\n" printf " backup finished: '%s'\n" "$snap_description_finished" printf " backup running: '%s'\n" "$snap_description_running" printf " backup synced: '%s'\n" "$snap_description_synced" if [ $verbose -ge 2 ]; then snap_sync_options="verbose_level=$verbose"; fi if [ $dryrun -eq 1 ]; then snap_sync_options="${snap_sync_options} dry-run=true"; fi if [ $donotify -eq 1 ]; then snap_sync_options="${snap_sync_options} donotify=true"; fi if [ $color -eq 1 ]; then snap_sync_options="${snap_sync_options} color=true"; fi if [ $btrfs_quota -eq 1 ]; then snap_sync_options="${snap_sync_options} use-btrfs-quota=true"; fi if [ $batch -eq 1 ]; then snap_sync_options="${snap_sync_options} batch=true do_pv_cmd=$do_pv_cmd" else snap_sync_options="${snap_sync_options} interactive=true do_pv_cmd=$do_pv_cmd" fi #if [ "$interactive" -eq 1 ]; then snap_sync_options="${snap_sync_options} interactive=true"; fi printf "Options: '%s'\n\n" "${snap_sync_options}" fi } quote_args () { # quote command in ssh call to prevent remote side from expanding any arguments # using dash's buildin printf args= if [ $# -gt 0 ]; then # no need to make COMMAND an array - ssh will merge it anyway COMMAND= while [ $# -gt 0 ]; do arg=$(printf "%s" "$1") COMMAND="${COMMAND}${arg} " shift done args="${args}${COMMAND}" fi } run_config_preparation () { if [ $verbose -ge 1 ]; then printf "${BLUE}Prepare configuration structures...${NO_COLOR}\n" fi SNAP_SYNC_EXCLUDE=no # loop though selected snapper configurations # Pseudo Arrays $i -> store associated elements of selected_config i=0 while [ $i -lt $selected_configs ]; do # only process on existing configurations selected_config=$(eval echo \$selected_config_$i) verify_snapper_config $selected_config # cleanup failed former runs check_snapper_failed_ids $selected_config $batch if [ $SNAP_SYNC_EXCLUDE = "yes" ]; then continue fi # parse selected_config and set $snapper_target_config appropriately get_snapper_backup_type $selected_config # parse backupdir get_backupdir $backupdir_cmdline # get latest successfully finished snapshot # verify source # WIP: metadata from last snapshot! case $snapper_backup_type in btrfs-snapshot) get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \ "snapper_uuid=" "snapper_subvolid=" "snapper_tapeid=" \ "snapper_backupdir=${backupdir}" "remote=" ;; btrfs-clone) get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \ "snapper_uuid=" "snapper_subvolid=" "snapper_tapeid=" "snapper_backupdir=" "remote=" ;; btrfs-archive) get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \ "snapper_uuid=" "snapper_subvolid=" "snapper_tapeid=" "snapper_backupdir=" "remote=" #"snapper_uuid=" "snapper_subvolid=" "snapper_tapeid=${volume_name}" "remote=" ;; *) if [ $verbose -ge 3 ]; then printf "${RED}TODO:${NO_COLOR} what is needed for config_type '%s'?\n" "$snapper_backup_type" fi ;; esac snapper_source_sync_id=$snapper_sync_id if [ $snapper_sync_id -eq 0 ]; then if [ $verbose -ge 2 ]; then printf "${MAGENTA}No previous snapshot available for snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ "$selected_config" fi snapper_target_sync_id=0 snapper_source_sync_snapshot='none' snapper_target_sync_snapshot='none' backup_root=$selected_target/$backupdir/$snapper_target_config else # Set snapshot-path for source get_snapper_config_value "remote=" \ "snapper_config=$SNAPPER_CONFIG_DIR/$selected_config" \ "config_key=SUBVOLUME" if [ $? -eq 0 ]; then snapper_source_sync_snapshot=$value/.snapshots/$snapper_source_sync_id/$snapper_snapshot_name fi if [ $verbose -ge 2 ]; then printf "${MAGENTA}Last synced ${GREEN}source snapshot${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} is ${GREEN}'%s'${MAGENTA} ...${NO_COLOR}\n" \ "$selected_config" "$snapper_sync_id" fi # verfiy target case $snapper_backup_type in btrfs-archive) # set snapper_target_sync_id get_archive_last_sync_id "snapper_config=${snapper_target_config}" \ "archive_type=full" \ "remote=${remote}" ;; *) get_snapper_last_sync_id "snapper_config=${snapper_target_config}" \ "snapper_description=${snap_description_synced}" \ "snapper_uuid=" "snapper_subvolid=" "snapper_tapeid=" \ "snapper_backupdir=" \ "remote=${remote}" ;; esac if [ $? -eq 0 ]; then snapper_target_sync_id=$snapper_sync_id else snapper_target_sync_id=0 fi # check for corresponding source and target sync id's snapper_common_sync_id=0 if [ $snapper_target_sync_id -ne $snapper_source_sync_id ]; then # select commen sync id get_snapper_sync_id "snapper_config=${selected_config}" "remote=" if [ $? -eq 0 ]; then snapper_common_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/$snapper_snapshot_name snapper_common_sync_id=$snapper_sync_id if [ $verbose -ge 2 ]; then if [ $remote ]; then printf "${MAGENTA}Common ${GREEN}synced snapshot${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} on '%s': ${GREEN}'%s'${NO_COLOR}\n" \ "$selected_config" "$remote" "$snapper_common_sync_id" else printf "${MAGENTA}Common ${GREEN}synced snapshot${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA}: ${GREEN}'%s'${NO_COLOR}\n" \ "$selected_config" "$snapper_common_sync_id" fi fi else # no commen sync id found snapper_target_sync_id=0 fi fi if [ $snapper_target_sync_id -eq 0 ]; then if [ $verbose -ge 2 ]; then if [ $remote ]; then printf "${MAGENTA}No synced ${GREEN}target snapshot${MAGENTA} available for snapper config ${GREEN}'%s'${MAGENTA} on '%s'...${NO_COLOR}\n" \ "$selected_config" "$remote" else printf "${MAGENTA}No synced ${GREEN}target snapshot${MAGENTA} available for snapper config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ "$selected_config" fi fi backup_root=$selected_target/$backupdir/$snapper_target_config else case $selected_fstype in btrfs) # get backupdir from snapper target get_snapper_target_backupdir $backupdir if [ $verbose -ge 3 ]; then if [ $remote ]; then printf "${MAGENTA}backupdir on remote '%s': ${GREEN}'%s'${NO_COLOR}\n" \ "$remote" "$backupdir" else printf "${MAGENTA}backupdir: ${GREEN}'%s'${NO_COLOR}\n" \ "$backupdir" fi fi # set target sync_snapshot path get_snapper_config_value "remote=$remote" \ "snapper_config=$SNAPPER_CONFIG_DIR/$snapper_target_config" \ "config_key=SUBVOLUME" if [ $? -eq 0 ]; then backup_root=$value snapper_target_sync_snapshot=$value/.snapshots/$snapper_target_sync_id/$snapper_snapshot_name fi # commandline settings for backupdir on selected_target will have priority if [ "$backup_root" != "$selected_target/$backupdir" ]; then backup_root=$selected_target/$backupdir/$snapper_target_config snapper_target_sync_snapshot=$backup_root/.snapshots/$snapper_target_sync_id/$snapper_snapshot_name fi if [ $verbose -ge 3 ]; then if [ $remote ]; then printf "${MAGENTA}backup_root on remote '%s': ${GREEN}'%s'${NO_COLOR}\n" \ "$remote" "$backup_root" else printf "${MAGENTA}backup_root: ${GREEN}'%s'${NO_COLOR}\n" \ "$backup_root" fi fi ;; *) # use snapper_target_snapshot backup_root=$selected_target/$backupdir/$snapper_target_config ;; esac if [ $verbose -ge 2 ]; then if [ $remote ]; then printf "${MAGENTA}Last synced ${GREEN}target snapshot${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} on remote '%s' is ${GREEN}'%s'${NO_COLOR}\n" \ "$snapper_target_config" "$remote" "$snapper_target_sync_id" else printf "${MAGENTA}Last synced ${GREEN}target snapshot${MAGENTA} for snapper config ${GREEN}'%s'${MAGENTA} is ${GREEN}'%s'${NO_COLOR}\n" \ "$snapper_target_config" "$snapper_target_sync_id" fi fi fi fi # save values in config specific pseudo arrays eval "snapper_source_config_$i='$selected_config'" eval "snapper_target_config_$i='$snapper_target_config'" eval "snapper_backup_type_$i='$snapper_backup_type'" eval "snapper_source_sync_id_$i='$snapper_source_sync_id'" eval "snapper_source_sync_snapshot_$i='$snapper_source_sync_snapshot'" eval "snapper_target_sync_id_$i='$snapper_target_sync_id'" eval "snapper_target_sync_snapshot_$i='$snapper_target_sync_snapshot'" eval "snapper_common_sync_id_$i='$snapper_common_sync_id'" eval "backupdir_$i='$backupdir'" eval "backup_root_$i='$backup_root'" cont_backup="K" eval "snapper_activate_$i='yes'" if [ $batch ]; then cont_backup="yes" else answer=yes get_answer_yes_no "Continue with backup [Y/n]? " "$answer" if [ "$answer" = "no" ]; then snapper_activate_$i="no" printf "Aborting backup for this configuration.\n" #snapper --config $selected_config delete $snapper_sync_id fi fi i=$(($i+1)) done } run_backup () { if [ $verbose -ge 1 ]; then printf "${BLUE}Performing backups...${NO_COLOR}\n" fi i=0 selected_config= while [ $i -lt $selected_configs ]; do # only process on existing configurations selected_config=$(eval echo \$selected_config_$i) if [ $verbose -ge 1 ]; then printf "${MAGENTA}Performing backup for config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ "${selected_config}" fi SNAP_SYNC_EXCLUDE=no if [ -f "/etc/snapper/configs/$selected_config" ]; then . /etc/snapper/configs/$selected_config else printf "${RED}Error: ${MAGENTA}Selected snapper configuration ${GREEN}'$selected_config'${MAGENTA} does not exist${NO_COLOR}\n" # go for next configuration i=$(($i+1)) continue fi cont_backup=$(echo \$snapper_activate_$i) if [ "$cont_backup" = "no" ] || [ "$SNAP_SYNC_EXCLUDE" = "yes" ]; then if [ $donotify -gt 0 ]; then notify_info "Backup in progress" "NOTE: Skipping '$selected_config' configuration." fi # go for next configuration i=$(($i+1)) continue fi if [ $donotify -gt 0 ]; then notify_info "Backup in progress" "Backing up data for configuration '$selected_config'." fi # retrieve config specific infos from pseudo Arrays snapper_source_config=$(eval echo \$snapper_source_config_$i) snapper_target_config=$(eval echo \$snapper_target_config_$i) snapper_backup_type=$(eval echo \$snapper_backup_type_$i) snapper_source_sync_id=$(eval echo \$snapper_source_sync_id_$i) snapper_source_sync_snapshot=$(eval echo \$snapper_source_sync_snapshot_$i) snapper_target_sync_id=$(eval echo \$snapper_target_sync_id_$i) snapper_common_sync_id=$(eval echo \$snapper_common_sync_id_$i) backup_dir=$(eval echo \$backupdir_$i) backup_root=$(eval echo \$backup_root_$i) if [ $verbose -ge 3 ]; then printf "${MAGENTA}snapper_source_config: ${GREEN}'%s'${NO_COLOR}\n" \ "$snapper_source_config" printf "${MAGENTA}snapper_target_config: ${GREEN}'%s'${NO_COLOR}\n" \ "$snapper_target_config" printf "${MAGENTA}snapper_backup_type: ${GREEN}'%s'${NO_COLOR}\n" \ "$snapper_backup_type" printf "${MAGENTA}snapper_source_sync_id: ${GREEN}'%s'${NO_COLOR}\n" \ "$snapper_source_sync_id" printf "${MAGENTA}snapper_common_sync_id: ${GREEN}'%s'${NO_COLOR}\n" \ "$snapper_common_sync_id" printf "${MAGENTA}backup_dir: ${GREEN}'%s'${NO_COLOR}\n" "$backup_dir" printf "${MAGENTA}backup_root: ${GREEN}'%s'${NO_COLOR}\n" "$backup_root" fi # create the needed snapper structure on the target case $snapper_backup_type in btrfs-snapshot) create_snapshot if [ $snapper_source_id -lt 0 ]; then return 1 fi # to use snapper on the target to supervise the synced snapshots # the backup_location needs to be in a subvol ".snapshots" inside $selected_config (hardcoded in snapper) snapper_target_id=$snapper_source_id snapper_target_snapshot=$backup_root/.snapshots/$snapper_target_id # create btrfs-snapshot on target verify_snapper_structure "backup_root=$backup_root" "snapper_target_config=$snapper_target_config" "snapper_target_id=$snapper_target_id" "remote=$remote" ;; btrfs-clone) # check for last common snapshot snapper_source_id=$snapper_source_sync_id snapper_source_snapshot=$SUBVOLUME/.snapshots/$snapper_source_sync_id/$snapper_snapshot_name snapper_source_info=$SUBVOLUME/.snapshots/$snapper_source_sync_id/info.xml # to use snapper on the target to supervise the synced snapshots # the backup_location needs to be in a subvol ".snapshots" inside $selected_config (hardcoded in snapper) snapper_target_id=$snapper_source_id snapper_target_snapshot=$backup_root/.snapshots/$snapper_target_id # create btrfs-snapshot on target verify_snapper_structure "backup_root=$backup_root" "snapper_target_config=$snapper_target_config" "snapper_target_id=$snapper_target_id" "remote=$remote" ;; btrfs-archive) if [ $snapper_source_sync_id -eq 0 ]; then create_snapshot if [ $snapper_source_id -lt 0 ]; then return 1 fi else # check for last common snapshot snapper_source_id=$snapper_source_sync_id snapper_source_snapshot=$SUBVOLUME/.snapshots/$snapper_source_sync_id/$snapper_snapshot_name snapper_source_info=$SUBVOLUME/.snapshots/$snapper_source_sync_id/info.xml fi # targets backup location will save the snapshots in the subdirectory (snapshot-id) # the snapshot is either the base snapshot (flat-file), or an incremental snapshot (btrfs-send stream) snapper_target_id=$snapper_source_id snapper_target_snapshot=$target_cmdline/$backupdir/$snapper_target_config/$snapper_target_id # archive btrfs-snapshot to non btrfs-filesystem on target (e.g tape, ext4) verify_archive_structure "backup_root=$backup_root" "snapper_target_config=$snapper_target_config" "snapper_target_sync_id=$snapper_target_id" "remote=$remote" ;; *) if [ $verbose -ge 3 ]; then printf "${RED}WIP:${NO_COLOR} what is needed for config_type '%s'\n" "$snapper_backup_type" fi ;; esac if [ $? -gt 0 ]; then error_count=$(($error_count+1)) # go for next configuration i=$(($i+1)) continue fi if [ $dryrun -eq 0 ]; then snapper_source_snapshot_size=0 if [ "$interactive" -eq 1 ]; then if [ $verbose -ge 2 ]; then printf "${MAGENTA}Get size for given source snapshot (id=${GREEN}'%s'${MAGENTA}, path=${GREEN}'%s'${MAGENTA})${NO_COLOR} ...\n" \ "$snapper_source_id" "$snapper_source_snapshot" fi if [ $btrfs_quota -eq 1 ]; then # qgroup for given path, exclude ancestrals # qgroup identifiers conform to level/id where level 0 is reserved to the qgroups associated with subvolumes snapper_source_snapshot_size=$(btrfs qgroup show -f --raw $snapper_source_snapshot 2>/dev/null \ | awk 'FNR>2 {print $2}') if [ $? -eq 1 ]; then # subvolume is not configured for quota, (temporary?) enable that if [ $btrfs_quota_tmp -eq 1 ]; then btrfs quota enable $snapper_source_snapshot 2>/dev/null btrfs quota rescan -w $snapper_source_snapshot 2>/dev/null snapper_source_snapshot_size=$(btrfs qgroup show -f --raw $snapper_source_snapshot 2>/dev/null \ | awk 'FNR>2 {print $2}') fi fi if [ $verbose -ge 3 ]; then printf "${MAGENTA}BTRFS qgroup show result: ${GREEN}'%s'\b${NO_COLOR}'%s'\n" \ "$?" "$snapper_source_snapshot_size" fi # need to substitue btrfs 'x.yyGiB' suffix, since pv will need 'xG' if [ $snapper_source_snapshot_size -ge 1048576 ]; then snapper_source_snapshot_size=$(btrfs qgroup show -f --gbytes $snapper_source_snapshot 2>/dev/null \ | awk 'FNR>2 { gsub(/.[0-9][0-9]GiB/,"G"); print $2}') fi if [ $verbose -ge 2 ]; then printf "${MAGENTA}BTRFS quota size for ${GREEN}source snapshot${MAGENTA}: id=${GREEN}'%s'${MAGENTA}, size=${GREEN}'%s'${NO_COLOR} ...\n" \ "$snapper_source_id" "$snapper_source_snapshot_size" fi # should we disable quota usage again? if [ $btrfs_quota_tmp -eq 1 ]; then btrfs quota disable $snapper_source_snapshot; fi else snapper_source_snapshot_size=$(du --one-file-system --summarize $snapper_source_snapshot 2>/dev/null \ | awk -F ' ' '{print $1}') if [ $snapper_source_snapshot_size -ge 1048576 ]; then snapper_source_snapshot_size=$(($snapper_source_snapshot_size / 1024 / 1024))G fi if [ $verbose -ge 2 ]; then printf "${MAGENTA}BTRFS subvolume size for ${GREEN}source snapshot${MAGENTA}: id=${GREEN}'%s'${MAGENTA}, size=${GREEN}'%s'${NO_COLOR} ...\n" \ "$snapper_source_id" "$snapper_source_snapshot_size" fi fi fi else if [ $verbose -ge 2 ]; then printf "${MAGENTA}dryrun: Would calculate BTRFS subvolume size for ${GREEN}source snapshot${NO_COLOR} ...\n" \ "$snapper_source_id" fi fi # setting process I/O scheduling options if [ $do_ionice_cmd -eq 1 ]; then # class: best-efford, priority: medium #cmd_ionice="ionice --class 2 --classlevel 5" # class: idle cmd_ionice="ionice --class 3" else cmd_ionice='' fi # settings for interactive progress status if [ $do_pv_cmd -eq 1 ]; then pv_options="--delay-start 2 --interval 5 --format \"time elapsed [%t] | avg rate %a | rate %r | transmitted [%b] | %p | time remaining [%e]\" " cmd_pv="pv --size $snapper_source_snapshot_size $pv_options | " #cmd_pv="pv $pv_options --size ${snapper_source_snapshot_size} | dialog --gauge \"$progname: Progress for config '$selected_config'\" 6 85 |" else cmd_pv='' fi case $selected_fstype in btrfs) cmd="btrfs send $btrfs_verbose_flag $snapper_source_snapshot 2>$BTRFS_PIPE \ | $cmd_pv \ $cmd_ionice $ssh btrfs receive $btrfs_verbose_flag $snapper_target_snapshot/ 1>$BTRFS_PIPE 2>&1" ;; *) # Can't use btrfs receive, since target filesystem can't support btrfs snapshot feature snapper_target_stream=${snapper_target_id}_${archive_type}.btrfs if [ ! -f $snapper_target_snapshot/$snapper_target_stream ]; then if [ -z $remote ]; then cmd="btrfs send $btrfs_verbose_flag $snapper_source_snapshot 2>/dev/null \ | $cmd_pv \ $cmd_ionice cat > $snapper_target_snapshot/$snapper_target_stream" else cmd="btrfs send $btrfs_verbose_flag $snapper_source_snapshot 2>/dev/null \ | $cmd_pv \ $cmd_ionice $ssh 'cat > $snapper_target_snapshot/$snapper_target_stream' 2>$BTRFS_PIPE" fi else if [ $verbose -ge 2 ]; then printf "${RED}BTRFS_Stream: %s${NO_COLOR} already saved.\n" \ "$snapper_target_snapshot/$snapper_target_stream" fi # go for next configuration i=$(($i+1)) continue fi ;; esac if [ "$dryrun" -eq 0 ]; then if [ "$snapper_source_sync_id" -eq 0 ] \ || [ "$snapper_target_sync_id" -eq 0 ] \ || [ "$backup_mode" = "full" ] ; then # send full snapshot to target if [ $verbose -ge 2 ]; then if [ ${#snapper_source_snapshot_size} -gt 0 ]; then printf "${MAGENTA}Sending ${GREEN}snapshot${NO_COLOR} for snapper config ${GREEN}'%s' ${MAGENTA}(id='%s', size='%s')${NO_COLOR} ...\n" \ "$selected_config" "$snapper_source_id" "$snapper_source_snapshot_size" else printf "${MAGENTA}Sending ${GREEN}snapshot${NO_COLOR} for snapper config ${GREEN}'%s' ${MAGENTA}(id='%s')${NO_COLOR} ...\n" \ "$selected_config" fi fi # the actual data sync to the target # this may take a while, depending on datasize and line-speed if [ $verbose -ge 3 ]; then printf "cmd: '%s'\n" "$cmd" fi $(eval $cmd) if [ "$?" -gt 0 ]; then printf "${RED}BTRFS_PIPE: %s${NO_COLOR}\n" "$(cat <$BTRFS_PIPE)" error_count=$(($error_count+1)) # go for next configuration i=$(($i+1)) continue fi else # source holds synced snapshots if [ $verbose -ge 3 ]; then printf "New ${GREEN}source${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \ "$snapper_source_id" "$snapper_source_snapshot" printf "New ${GREEN}target${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \ "$snapper_target_id" "$snapper_target_snapshot/$snapper_snapshot_name" printf "Common synced snapshot id: ${GREEN}'%s'${NO_COLOR}\n" \ "$snapper_common_sync_id" printf "Last synced ${GREEN}source${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \ "$snapper_source_sync_id" "$snapper_source_sync_snapshot" printf "Last synced ${GREEN}target${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \ "$snapper_target_sync_id" "$snapper_target_sync_snapshot" fi if [ $verbose -ge 2 ]; then printf "${MAGENTA}Prepare ${GREEN}incremental snapshot${NO_COLOR} (id: ${GREEN}'%s'${NO_COLOR}) for snapper config ${GREEN}'%s'${NO_COLOR} ...\n" \ "$snapper_target_id" "$selected_config" fi # verify that we have a matching source and target snapshot-id if [ $snapper_common_sync_id -eq 0 ]; then if [ ${snapper_source_id} -eq ${snapper_target_sync_id} ]; then # nothing to do, snapshot already in sync if [ $verbose -ge 3 ]; then printf "${MAGENTA}Nothing to do! Source and target snapshot (id: ${GREEN}'%s'${MAGENTA}) are in sync.${NO_COLOR}\n" \ "$snapper_target_sync_id" fi # go for next configuration i=$(($i+1)) continue elif [ ${snapper_source_sync_id} != ${snapper_target_sync_id} ]; then if [ $snapper_target_sync_id -lt $snapper_target_id ]; then # try to find last target_sync_id in source_config get_snapper_sync_id "snapper_config=${snapper_source_config}" "remote=" if [ $? -eq 0 ]; then snapper_source_sync_id=$snapper_sync_id snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/$snapper_snapshot_name else printf "${RED}Error: ${MAGENTA}No common sync id found. Aborting backup for config ${GREEN}'%s'${NO_COLOR}\n" error_count=$(($error_count+1)) # go for next configuration i=$(($i+1)) continue fi fi elif [ ${snapper_source_id} != ${snapper_source_sync_id} ]; then snapper_common_sync_id=$snapper_source_sync_id snapper_common_sync_snapshot=$snapper_source_sync_snapshot else # use source_id as common sync-id snapper_common_sync_id=$snapper_source_id snapper_common_sync_snapshot=$snapper_source_sync_snapshot fi else # we have a common sync_id if [ ${snapper_source_sync_id} != ${snapper_target_sync_id} ]; then # btrfs send: use common sync_id as a valid parent snapper_source_sync_id=$snapper_common_sync_id snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_source_sync_id/$snapper_snapshot_name fi fi cmd="$ssh stat --format %i $snapper_target_snapshot 2>/dev/null" ret=$(eval $cmd) if [ $? -eq 0 ]; then case $selected_fstype in btrfs) # Sends the difference between the new snapshot and old synced snapshot. # Using the flag -c (clone-src) will require the availibility of an identical readonly # subvolume on the source and the receiving location (the parent-id). # using "btrfs send -p" instead of "btrfs send -c", then no parent search would be # needed (Andreij explained in: https://www.spinics.net/lists/linux-btrfs/msg69369.html) cmd="btrfs send $btrfs_verbose_flag -p $snapper_common_sync_snapshot $snapper_source_snapshot 2>$BTRFS_PIPE \ | $cmd_pv \ $cmd_ionice $ssh btrfs receive $btrfs_verbose_flag $snapper_target_snapshot/ 1>$BTRFS_PIPE 2>&1" ;; *) # Can't use btrfs receive, since target filesystem can't support btrfs snapshot feature snapper_target_stream=${snapper_target_id}_incremental.btrfs if [ -z $remote ]; then cmd="btrfs send $btrfs_verbose_flag -p $snapper_common_sync_snapshot $snapper_source_snapshot 2>$BTRFS_PIPE \ | $cmd_pv \ $cmd_ionice cat > $snapper_target_snapshot/$snapper_target_stream 2>$BTRFS_PIPE" else cmd="btrfs send $btrfs_verbose_flag -p $snapper_common_sync_snapshot $snapper_source_snapshot 2>$BTRFS_PIPE \ | $cmd_pv \ $cmd_ionice $ssh 'cat > $snapper_target_snapshot/$snapper_target_stream' 2>$BTRFS_PIPE" fi ;; esac if [ $verbose -ge 2 ]; then printf "%b" "${GREEN}btrfs send${NO_COLOR} is using\n" \ " parent snapshot id: ${GREEN}'$snapper_common_sync_id'${NO_COLOR} (path: ${GREEN}'$snapper_common_sync_snapshot'${NO_COLOR}) and\n" \ " snapshot id: ${GREEN}'$snapper_source_id'${NO_COLOR} (path: ${GREEN}'$snapper_source_snapshot'${NO_COLOR})\n" \ " to construct an incremental data-stream ...\n" fi if [ $verbose -ge 3 ]; then printf "${GREEN}btrfs command:${NO_COLOR} '%s'\n" "$cmd" fi $(eval $cmd) ret=$? case $ret in 0) ;; 1) # empty stream, error, no changes printf "${MAGENTA}btrfs pipe return-code: ${RED}'%s'${NO_COLOR}\n" "$ret" printf "${RED}%s${NO_COLOR}\n" "$(cat <$BTRFS_PIPE)" run_cleanup ${selected_config} # go for next configuration i=$(($i+1)) continue ;; 127) printf "${MAGENTA}btrfs pipe return-code: ${GREEN}'%s'${NO_COLOR}\n" "$ret" ;; *) printf "${RED}btfs pipe ERROR: '%s'\n" "$ret" printf "${RED}BTRFS_PIPE: %s${NO_COLOR}\n" "$(cat <$BTRFS_PIPE)" run_cleanup ${selected_config} # go for next configuration i=$(($i+1)) continue ;; esac else # is this clause possible? if [ $verbose -ge 3 ]; then printf "${RED}Error: ${MAGENTA}No commen sync snapshot ${GREEN}'%s'${MAGENTA} on ${GREEN}source${MAGENTA} to sync metadata ...${NO_COLOR} \n" \ "$snapper_common_sync_snapshot" error_count=$(($error_count+1)) # go for next configuration i=$(($i+1)) continue fi fi fi else printf "${MAGENTA}dryrun${NO_COLOR}: Would run btrfs send / btrfs receive pipe\n" fi # send the snapper info metadata if [ $dryrun -eq 0 ]; then if [ -z "$remote" ]; then cp "$snapper_source_info" "$snapper_target_snapshot/info.xml" else cmd="$scp $snapper_source_info root@$remote:$snapper_target_snapshot/info.xml" $(eval $cmd) 1>/dev/null fi else printf "${MAGENTA}dryrun${NO_COLOR}: Would copy info metadata '%s' to target.\n" \ "$snapper_source_info" fi # Save config specific values in pseudo arrays eval "snapper_source_id_$i='$snapper_source_id'" eval "snapper_source_snapshot_$i='$snapper_source_snapshot'" eval "snapper_source_info_$i='$snapper_source_info'" eval "snapper_target_id_$i='$snapper_source_id'" eval "snapper_target_snapshot_$i='$snapper_target_snapshot'" # finalize backup tasks run_finalize ${selected_config} run_cleanup ${selected_config} if [ $verbose -ge 1 ]; then printf "${MAGENTA}Backup complete:${NO_COLOR} id=${GREEN}'%s'${NO_COLOR}, config=${GREEN}'%s'${NO_COLOR}\n" \ "$snapper_source_id" "$selected_config" fi i=$(($i+1)) done } run_cleanup () { local selected_config=${1} local batch=${2:-$false} if [ $verbose -ge 1 ]; then printf "${MAGENTA}Performing cleanup for config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ ${selected_config} fi if [ $dryrun -eq 0 ]; then # cleanup failed runs check_snapper_failed_ids "$selected_config" "$batch" # cleanup target #$ssh btrfs subvolume delete $backup_root/$snapper_snapshots/$snapper_target_sync_id/$snapper_snapshot_name #$ssh rm -rf $backup_root/$snapper_snapshots/$snapper_target_sync_id else printf "${MAGENTA}dryrun${NO_COLOR}: Would cleanup failed snapshot IDs ...\n" fi } run_finalize () { local selected_config=${1} if [ $verbose -ge 1 ]; then printf "${MAGENTA}Finalize backups for config ${GREEN}'%s'${MAGENTA}...${NO_COLOR}\n" \ ${selected_config} fi SNAP_SYNC_EXCLUDE=no if [ -f "/etc/snapper/configs/$selected_config" ]; then . /etc/snapper/configs/$selected_config else printf "${RED}Error: ${MAGENTA}Selected snapper configuration ${GREEN}'$selected_config'${MAGENTA} does not exist${NO_COLOR}" return 1 fi cont_backup=$(echo \$snapper_activate_$i) if [ "$cont_backup" = "no" ] || [ "$SNAP_SYNC_EXCLUDE" = "yes" ]; then if [ $donotify -gt 0 ]; then notify_info "Finalize backup" "NOTE: Skipping '$selected_config' configuration." fi 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_source_config=$(eval echo \$snapper_source_config_$i) backupdir=$(eval echo \$backupdir_$i) backup_root=$(eval echo \$backup_root_$i) snapper_backup_type=$(eval echo \$snapper_backup_type_$i) snapper_source_sync_id=$(eval echo \$snapper_source_sync_id_$i) snapper_target_sync_id=$(eval echo \$snapper_target_sync_id_$i) snapper_source_sync_snapshot=$(eval echo \$snapper_source_sync_snapshot_$i) snapper_source_id=$(eval echo \$snapper_source_id_$i) snapper_source_snapshot=$(eval echo \$snapper_source_snapshot_$i) snapper_source_info=$(eval echo \$snapper_source_info_$i) snapper_target_config=$(eval echo \$snapper_target_config_$i) snapper_target_snapshot=$(eval echo \$snapper_target_snapshot_$i) # It's important not to change the values of the snapper key/value pairs ($userdata) # which is stored in snappers info.xml file of the source snapshot. # This is how we find the parent. src_host=$(cat /etc/hostname) src_uuid=$(findmnt --noheadings --output UUID --target $SUBVOLUME) src_subvolid=$(findmnt --noheadings --output OPTIONS --target $SUBVOLUME | sed -e 's/.*subvolid=\([0-9]*\).*/\1/') # Tag new snapshots key/value parameter if [ $verbose -ge 2 ]; then printf "${MAGENTA}Tagging target ...${NO_COLOR}\n" fi if [ "$dryrun" -eq 0 ]; then # target snapshot # 1) wait for target snapshot to show up in snapper list # find "$snapper_target_sync_id" -> marked as "$snap_description_running" # 2) toggle metadata -> mark as "$snap_description_finished", reference "$target_userdata" (host, uuid, subvolid) # snapper orders userdata pairs lexical ascending # !!! ugly hack !!!: # Problem: how to trigger that database is synced? -> a feature request is send to snapper upstream source # Solution1: right now, it is no-deterministic, when the entry in the listing will show up # -> wait ii_max * ii_sleep seconds ( 20*15 = 300 -> 5 Min) # for snapper to list target snapshot in database. local ii=1 local ii_max=20 local ii_sleep=15 # Solution2: kill running snapperd # -> will restart and sync any unseen dependencies snapperd_pid=$(eval $ssh pgrep snapperd) if [ $verbose -ge 3 ]; then printf "${MAGENTA}Kill runnint ${GREEN}snapperd${MAGENTA} on target id:${GREEN}'%s'${NO_COLOR} ...\n" \ "$snapperd_pid" fi $(eval $ssh killall -SIGTERM snapperd 2>/dev/null) if [ $verbose -ge 3 ]; then printf "${MAGENTA}Identify snapper id ${GREEN}'%s'${MAGENTA} on target for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \ "$snapper_target_id" "$snapper_target_config" fi # construct snapper match command case $snapper_backup_type in btrfs-archive) # archive btrfs-snapshot to non btrfs-filesystem on target (e.g tape, ext4) # save target-id if [ $snapper_source_id -gt 0 ]; then cmd="snapper --config $selected_config modify \ --cleanup-algorithm \"dsnap-sync\" \ --userdata \"tapeid=$volume_name\" \ $snapper_source_id" if [ $verbose -ge 3 ]; then printf "${MAGENTA}Tagging snapper metadata${NO_COLOR} for snapper id ${GREEN}'%s'${NO_COLOR} on source for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \ "$snapper_source_id" "$selected_config" printf "calling: '%s'\n" "$(eval $cmd)" fi ret=$(eval "$cmd") if [ $verbose -ge 3 ]; then printf "return: '%s'\n" "$?" fi sync # update tape attributes if [ ${#mediapool_name} -gt 1 ] && [ ${#volume_name} -gt 1 ]; then # read mounted LTFS structures $ssh tape-admin --verbose=$verbose --update-lastwrite ${mediapool_name} ${volume_name} $ssh tape-admin --verbose=$verbose --update-retensiondate ${mediapool_name} ${volume_name} fi fi return 0 ;; btrfs-clone) # no tagging needed return 0 ;; btrfs-snapshot) # create btrfs-snapshot on target cmd="$ssh snapper --config \"$snapper_target_config\" list --type single \ | awk ' /'\"$snap_description_running\"'/ ' \ | awk -F '|' ' \$1 == $snapper_target_id {print \$1} ' " ;; esac while [ "$ii" -le "$ii_max" ]; do if [ $verbose -ge 3 ]; then printf "calling: '%s'\n" "$cmd" fi ret=$(eval "$cmd") if [ $? -eq 0 ]; then if [ ${#ret} -gt 1 ]; then if [ $ret -eq $snapper_target_id ]; then # got snapshot as $snapper_target_id if [ $verbose -ge 3 ]; then printf "${MAGENTA}Found${NO_COLOR} snapper id ${GREEN}'%s'${NO_COLOR} on target for configuration ${GREEN}'%s'${NO_COLOR}\n" \ "$snapper_target_id" "$snapper_target_config" fi if [ $verbose -ge 3 ]; then printf "${MAGENTA}Tagging metadata${NO_COLOR} for snapper id ${GREEN}'%s'${NO_COLOR} on target for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \ "$snapper_target_id" "$snapper_target_config" #printf "calling: '%s'\n" "$($cmd)" fi # call command (respect needed quotes) if [ $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_target_id\') else ret=$(eval snapper --config $snapper_target_config modify \ --description "$snap_description_finished" \ --userdata "host=$src_host, subvolid=$src_subvolid, uuid=$src_uuid" \ --cleanup-algorithm "timeline" \ $snapper_target_id) fi if [ $verbose -ge 3 ]; then printf "return: '%s'\n" "$ret" fi break fi fi fi if [ $verbose -ge 3 ]; then printf "%s/%s: ${RED}Waiting another '%s' seconds${NO_COLOR} for snappers database update on target ...\n" \ "$ii" "$ii_max" "$ii_sleep" fi sleep $ii_sleep ii=$(($ii + 1)) done # source snapshot if [ $verbose -ge 2 ]; then printf "${MAGENTA}Tagging source ...${NO_COLOR}\n" fi if [ $snapper_source_id -gt 0 ]; then cmd="snapper --config $selected_config modify \ --description \"$snap_description_synced\" \ --userdata \"backupdir=$backupdir, important=yes, host=$remote, subvolid=$selected_subvol, uuid=$selected_uuid\" \ --cleanup-algorithm \"timeline\" \ $snapper_source_id" if [ $verbose -ge 3 ]; then printf "${MAGENTA}Tagging snapper metadata${NO_COLOR} for snapper id ${GREEN}'%s'${NO_COLOR} on source for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \ "$snapper_source_id" "$selected_config" printf "calling: '%s'\n" "$cmd" fi ret=$(eval "$cmd") if [ "$ret" ]; then if [ $verbose -ge 3 ]; then printf "return: '%s'\n" "$?" fi else printf "${RED}ERROR: ${MAGENTA}Updating snapper metadata for source snapshot id ${GREEN}'%s'${NO_COLOR}${MAGENTA} failed.\n" \ "Please check for sufficiant space.${NO_COLOR}\n" \ "$snapper_source_id" fi sync fi if [ ${#snapper_source_sync_id} -gt 0 ]; then # TODO: no snapper method to remove userdata pair, use awk cmd="snapper --config $selected_config modify \ --description \"$snap_description_finished\" \ --userdata \"important=no\" \ $snapper_source_sync_id" if [ $verbose -ge 3 ]; then printf "${MAGENTA}Tagging snapper metadata${NO_COLOR} for snapper sync id ${GREEN}'%s'${NO_COLOR} on source for configuration ${GREEN}'%s'${NO_COLOR} ...\n" \ "$snapper_source_sync_id" "$selected_config" printf "calling: '%s'\n" "$cmd" fi ret=$(eval "$cmd") snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_source_sync_id/$snapper_snapshot_name else snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_source_id/$snapper_snapshot_name fi else # dry-run output case $snapper_backup_type in btrfs-archive) printf "${MAGENTA}dryrun${NO_COLOR}: %s snapper --config %s modify\\ \n \ \t--cleanup-algorithm 'dsnap-sync'\\ \n \ \t--userdata 'tapeid=%s'\\ \n \ \t'snapper_source_id'\n" \ "$ssh" "$selected_config" "$volume_name" ;; btrfs-snapshot) printf "${MAGENTA}dryrun${NO_COLOR}: %s snapper --config %s modify \\ \n \ \t--description '%s' \\ \n \ \t--cleanup-algorithm 'dsnap-sync' \\ \n \ \t--userdata 'host=%s, subvolid=%s, uuid=%s' \\ \n \ \t'snapper_source_id'\n" \ "$ssh" "$selected_config" "$snap_description_finished" \ "$src_host" "src_subvolid" "$src_uuid" ;; esac cmd="snapper --config $selected_config modify --description '$snap_description_synced' --userdata '$userdata' $snapper_sync_id" printf "${MAGENTA}dryrun${NO_COLOR}: %s\n" "$cmd" cmd="snapper --config $selected_config modify --description '$snap_description_finished' $snapper_source_sync_id" printf "${MAGENTA}dryrun${NO_COLOR}: %s\n" "$cmd" fi } select_target () { local i=0 local target_id=0 local target_selected_count=0 local subvolid='' local subvol='' if [ $verbose -ge 1 ]; then printf "${BLUE}Select backup target...${NO_COLOR}\n" fi # print selection table if [ $verbose -ge 2 ]; then if [ -z "$remote" ]; then printf "Selecting a mounted device for backups on your local machine.\n" else printf "Selecting a mounted device for backups on %s.\n" "$remote" fi fi while [ "$target_id" -eq 0 ] || [ "$target_id" -le $target_count ]; do if [ "$disk_subvolid_match_count" -eq 1 ]; then # matching SUBVOLID selection from commandline if [ $verbose -ge 3 ]; then printf "%s mount points were found with SUBVOLID '%s'.\n" \ "$disk_subvolid_match_count" "$subvolid_cmdline" fi # Pseudo-Array: target_selected_$i (reference to $disk_uuid element) eval "target_selected_$i='$disk_subvolid_match'" disk=$(eval echo \$disk_uuid_$disk_subvolid_match) subvolid=$(eval echo \$disk_subvolid_$disk_subvolid_match) fs_options=$(eval echo \$fs_options_$disk_subvolid_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') target_selected=$disk_subvolid_match break fi if [ "$target_match_count" -eq 1 ]; then # matching TARGET selection from commandline if [ $verbose -ge 3 ]; then printf "%s mount points were found with TARGET '%s'.\n" \ "$target_match_count" "$target_cmdline" fi # Pseudo-Array: target_selected_$i (reference to $disk_uuid element) eval "target_selected_$i='$disk_target_match'" disk=$(eval echo \$disk_uuid_$target_match) #target=$(eval echo \$disk_target_$disk_target_match) target=$(eval echo \$target_$target_match) fs_options=$(eval echo \$fs_options_$target_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') target_selected=$target_match break fi if [ "$disk_uuid_match_count" -ge 1 ]; then # matching UUID selection from commandline target_count=$disk_uuid_match_count if [ $verbose -ge 3 ]; then printf "%s mount points were found with UUID '%s'.\n" \ "$disk_uuid_match_count" "$uuid_cmdline" fi for disk_uuid in $disk_uuid_match; do # Pseudo-Array: disk_selected_$i (reference to $disk_uuid element) eval "target_selected_$i='$disk_uuid'" disk=$(eval echo \$disk_uuid_$disk_uuid) fs_options=$(eval echo \$fs_options_$disk_uuid | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') if [ $disk_uuid_match -gt 1 ]; then printf "%4s) %s (uuid=%s,%s)\n" "$i" "$target" "$disk" "$fs_options" i=$((i+1)) else target_selected=$disk_uuid_match break fi done else while [ "$target_id" -lt $target_count ]; do # present all mounted BTRFS filesystems if [ $verbose -ge 3 ]; then printf "Present selection for '%s' available targets\n" \ "$target_count" fi # Pseudo-Array: target_selected_$i (reference to $target_id element) eval "target_selected_$i='$target_id'" disk=$(eval echo \$disk_uuid_$target_id) target=$(eval echo \$target_$target_id) fs_options=$(eval echo \$fs_options_$target_id | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') fs_type=$(eval echo \$fs_type_$target_id) case $fs_type in btrfs) printf "%4s) %s (type=%s,uuid=%s,%s)\n" "$i" "$target" "$fs_type" "$disk" "$fs_options" ;; ltfs) printf "%4s) %s (type=%s,%s)\n" "$i" "$target" "$fs_type" "$fs_options" ;; esac i=$((i+1)) target_id=$(($target_id+1)) done fi printf "%4s) Exit\n" "x" read -r -p "Enter a number: " target_selected case $target_selected in x) break ;; [0-9][0-9]|[0-9]) if [ "$target_selected" -gt "$target_count" ]; then target_id=0 i=0 else break fi ;; *) printf "\nNo disk selected. Select a disk to continue.\n" target_id=0 i=0 ;; esac done if [ "$target_selected" = x ]; then exit 0 fi selected_target=$(eval echo \$target_$target_selected) selected_fstype=$(eval echo \$fs_type_$target_selected) selected_uuid=$(eval echo \$disk_uuid_$target_selected) selected_subvol=$(eval echo \$fs_options_$target_selected | sed -e 's/.*subvolid=\([0-9]*\).*/\1/') if [ $verbose -ge 2 ]; then case $selected_fstype in btrfs) printf "${MAGENTA}You selected %s target with UUID ${GREEN}'%s'${MAGENTA} (subvolid=${GREEN}'%s'${MAGENTA})${NO_COLOR}.\n" \ "$selected_fstype" "$selected_uuid" "$selected_subvol" ;; ltfs) printf "${MAGENTA}You selected %s target (VolumeName=${GREEN}'%s'${MAGENTA})${NO_COLOR}.\n" \ "$selected_fstype" "$volume_name" ;; esac if [ -z "$remote" ]; then printf "${MAGENTA}Target is mounted at ${GREEN}'%s'${MAGENTA}.${NO_COLOR}\n" \ "$selected_target" else printf "${MAGENTA}Target disk is mounted on host ${GREEN}'%s'${MAGENTA} at ${GREEN}'%s'${MAGENTA}.${NO_COLOR}\n" \ "$remote" "$selected_target" fi fi if [ $donotify -gt 0 ]; then if [ "$target_cmdline" != "none" ]; then if [ -z "$remote" ]; then notify_info "Target selected" "Using target '$target_cmdline'..." else notify_info "Target selected" "Using target '$target_cmdline' at '$remote'..." fi elif [ "$uuid_cmdline" != "none" ]; then if [ -z "$remote" ]; then notify_info "Target selected" "Using targets uuid '$uuid_cmdline'..." else notify_info "Target selected" "Using targets uuid '$uuid_cmdline' at '$remote'..." fi else if [ -z "$remote" ]; then notify_info "Target selected" "Use command line menu to select target disk..." else notify_info "Target selected" "Use command line menu to select target disk on $remote..." fi fi fi } select_target_disk () { local i=0 local disk_id=0 local disk_selected_count=0 local subvolid='' local subvol='' if [ $verbose -ge 1 ]; then printf "${BLUE}Select backup target...${NO_COLOR}\n" fi # print selection table if [ $verbose -ge 2 ]; then if [ -z "$remote" ]; then printf "Selecting a mounted BTRFS device for backups on your local machine.\n" else printf "Selecting a mounted BTRFS device for backups on %s.\n" "$remote" fi fi while [ "$disk_id" -eq -1 ] || [ "$disk_id" -le $disk_count ]; do if [ "$disk_subvolid_match_count" -eq 1 ]; then # matching SUBVOLID selection from commandline # Pseudo-Array: disk_selected_$i (reference to $disk_uuid element) eval "disk_selected_$i='$disk_subvolid_match'" disk=$(eval echo \$disk_uuid_$disk_subvolid_match) subvolid=$(eval echo \$disk_subvolid_$disk_subvolid_match) fs_options=$(eval echo \$fs_options_$disk_subvolid_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') disk_selected=$disk_subvolid_match break fi if [ "$disk_target_match_count" -eq 1 ]; then # matching TARGET selection from commandline # Pseudo-Array: disk_selected_$i (reference to $disk_uuid element) eval "disk_selected_$i='$disk_target_match'" disk=$(eval echo \$disk_uuid_$disk_target_match) target=$(eval echo \$disk_target_$disk_target_match) fs_options=$(eval echo \$fs_options_$disk_target_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') disk_selected=$disk_target_match break fi if [ "$disk_uuid_match_count" -gt 1 ]; then # got UUID selection from commandline disk_count=$disk_uuid_match_count if [ $verbose -ge 2 ]; then printf "%s mount points were found with UUID '%s'.\n" "$disk_uuid_match_count" "$uuid_cmdline" fi for disk_uuid in $disk_uuid_match; do # Pseudo-Array: disk_selected_$i (reference to $disk_uuid element) eval "disk_selected_$i='$disk_uuid'" disk=$(eval echo \$disk_uuid_$disk_uuid) fs_options=$(eval echo \$fs_options_$disk_uuid | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') printf "%4s) %s (uuid=%s,%s)\n" "$i" "$target" "$disk" "$fs_options" i=$((i+1)) done else while [ "$disk_id" -le $disk_count ]; do # present all mounted BTRFS filesystems # Pseudo-Array: disk_selected_$i (reference to $disk_id element) eval "disk_selected_$i='$disk_id'" disk=$(eval echo \$disk_uuid_$disk_id) target=$(eval echo \$disk_target_$disk_id) fs_options=$(eval echo \$fs_options_$disk_id | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') printf "%4s) %s (uuid=%s,%s)\n" "$i" "$target" "$disk" "$fs_options" i=$((i+1)) disk_id=$(($disk_id+1)) done fi read -r -p "Enter a number: " disk_selected case $disk_selected in x) break ;; [0-9][0-9]|[0-9]) if [ "$disk_selected" -gt "$disk_count" ]; then disk_id=0 i=0 else break fi ;; *) printf "\nNo disk selected. Select a disk to continue.\n" disk_id=0 i=0 ;; esac done if [ "$disk_selected" = x ]; then exit 0 fi selected_uuid=$(eval echo \$disk_uuid_$disk_selected) selected_target=$(eval echo \$disk_target_$disk_selected) selected_subvol=$(eval echo \$fs_options_$disk_selected | sed -e 's/.*subvolid=\([0-9]*\).*/\1/') if [ $verbose -ge 2 ]; then printf "${MAGENTA}You selected the disk with UUID ${GREEN}'%s'${MAGENTA} (subvolid=${GREEN}'%s'${MAGENTA})${NO_COLOR}.\n" \ "$selected_uuid" "$selected_subvol" if [ -z "$remote" ]; then printf "${MAGENTA}Target disk is mounted at ${GREEN}'%s'${MAGENTA}.${NO_COLOR}\n" \ "$selected_target" else printf "${MAGENTA}Target disk is mounted on host ${GREEN}'%s'${MAGENTA} at ${GREEN}'%s'${MAGENTA}.${NO_COLOR}\n" \ "$remote" "$selected_target" fi fi if [ $donotify -gt 0 ]; then if [ "$target_cmdline" != "none" ]; then if [ -z "$remote" ]; then notify_info "Target selected" "Using target '$target_cmdline'..." else notify_info "Target selected" "Using target '$target_cmdline' at '$remote'..." fi elif [ "$uuid_cmdline" != "none" ]; then if [ -z "$remote" ]; then notify_info "Target selected" "Using targets uuid '$uuid_cmdline'..." else notify_info "Target selected" "Using targets uuid '$uuid_cmdline' at '$remote'..." fi else if [ -z "$remote" ]; then notify_info "Target selected" "Use command line menu to select target disk..." else notify_info "Target selected" "Use command line menu to select target disk on $remote..." fi fi fi } select_target_tape () { local i=0 local tape_id=0 local tape_selected_count=0 local subvolid='' local subvol='' if [ $verbose -ge 1 ]; then printf "${BLUE}Select target tape...${NO_COLOR}\n" fi # print selection table if [ $verbose -ge 2 ]; then if [ -z "$remote" ]; then printf "Selecting a mounted LTFS tape for backups on your local machine.\n" else printf "Selecting a mounted LTFS tape for backups on %s.\n" "$remote" fi fi while [ "$tape_id" -eq -1 ] || [ "$tape_id" -le $tape_count ]; do if [ "$tape_match_count" -eq 1 ]; then # matching LTFS selection from commandline # Pseudo-Array: tape_selected_$i (reference to $tape_uuid element) eval "tape_selected_$i='$tape_id_match'" tape=$(eval echo \$tape_id_$tape_id_match) #subvolid=$(eval echo \$tape_id_$tape_id_match) #fs_options=$(eval echo \$tape_fs_options_$tape_id_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') tape_selected=$tape_id_match break fi if [ "$tape_target_match_count" -eq 1 ]; then # matching TARGET selection from commandline # Pseudo-Array: tape_selected_$i (reference to $tape_id element) eval "tape_selected_$i='$tape_target_match'" tape=$(eval echo \$tape_id_$tape_target_match) target=$(eval echo \$tape_target_$tape_target_match) #fs_options=$(eval echo \$fs_options_$tape_target_match | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') tape_selected=$tape_target_match break fi if [ "$tape_id_match_count" -gt 1 ]; then # got LTFS ID selection from commandline tape_count=$tape_id_match_count if [ $verbose -ge 2 ]; then printf "%s mount points were found with ID '%s'.\n" "$tape_id_match_count" "$uuid_cmdline" fi for tape_id in $tape_id_match; do # Pseudo-Array: tape_selected_$i (reference to $tape_uuid element) eval "tape_selected_$i='$tape_id'" tape=$(eval echo \$tape_id_$tape_id) tape_fs_options=$(eval echo \$fs_options_$tape_id | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') printf "%4s) %s (id=%s,%s)\n" "$i" "$target" "$tape" "$tape_fs_options" i=$((i+1)) done else while [ "$tape_id" -le $tape_count ]; do # present all mounted BTRFS filesystems # Pseudo-Array: tape_selected_$i (reference to $tape_id element) eval "tape_selected_$i='$tape_id'" tape=$(eval echo \$tape_id_$tape_id) target=$(eval echo \$tape_target_$tape_id) #tape_fs_options=$(eval echo \$target_fs_options_$tape_id | sed -e 's/.*,\(subvolid=[0-9]*\).*,\(subvol=[0-9]*\)/\1,\2/') printf "%4s) %s\n" "$i" "$target" i=$((i+1)) tape_id=$(($tape_id+1)) done fi printf "%4s) Exit\n" "x" read -r -p "Enter a number: " tape_selected case $tape_selected in x) break ;; [0-9][0-9]|[0-9]) if [ "$tape_selected" -gt "$tape_count" ]; then tape_id=0 i=0 else break fi ;; *) printf "\nNo LTFS tape selected. Select a tape to continue.\n" tape_id=0 i=0 ;; esac done if [ "$tape_selected" = x ]; then exit 0 fi selected_tape_id=$(eval echo \$tape_id_$tape_selected) selected_tape_target=$(eval echo \$tape_target_$tape_selected) #selected_subvol=$(eval echo \$fs_options_$tape_selected | sed -e 's/.*subvolid=\([0-9]*\).*/\1/') if [ $verbose -ge 2 ]; then printf "${MAGENTA}You selected the LTFS tape with ID ${GREEN}'%s'${MAGENTA}${NO_COLOR}.\n" \ "$selected_tape_id" if [ -z "$remote" ]; then printf "${MAGENTA}Target tape is mounted at ${GREEN}'%s'${MAGENTA}.${NO_COLOR}\n" \ "$selected_tape_target" else printf "${MAGENTA}Target tape is mounted on host ${GREEN}'%s'${MAGENTA} at ${GREEN}'%s'${MAGENTA}.${NO_COLOR}\n" \ "$remote" "$selected_tape_target" fi fi if [ $donotify -gt 0 ]; then if [ "$target_cmdline" != "none" ]; then if [ -z "$remote" ]; then notify_info "Target selected" "Using target '$target_cmdline'..." else notify_info "Target selected" "Using target '$target_cmdline' at '$remote'..." fi elif [ "$tape_id_cmdline" != "none" ]; then if [ -z "$remote" ]; then notify_info "LTFS Target selected" "Using LTFS targets '$tape_id_cmdline'..." else notify_info "LTFS Target selected" "Using LTFS targets uuid '$tape_id_cmdline' at '$remote'..." fi else if [ -z "$remote" ]; then notify_info "Target selected" "Use command line menu to select target disk..." else notify_info "Target selected" "Use command line menu to select target disk on $remote..." fi fi fi } set_config(){ 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 $selected_config exit 0 } usage () { cat < start automount for given path to get a valid target mountpoint -b, --backupdir backupdir is a relative path that will be appended to target backup-root --backuptype Specify backup type --batch no user interaction -d, --description Change the snapper description. Default: "latest incremental backup" --label-finished snapper description tagging successful jobs. Default: "dsnap-sync backup" --label-running snapper description tagging active jobs. Default: "dsnap-sync in progress" --label-synced snapper description tagging last synced jobs Default: "dsnap-sync last incremental" --color Enable colored output messages -c, --config Specify the snapper configuration to use. Otherwise will perform for each snapper configuration. You can select multiple configurations (e.g. -c "root" -c "home"; --config root --config home) --config-postfix Specify a postfix that will be appended to the destination snapper config name --dry-run perform a trial run (no changes are written) --mediapool Specify the name of the tape MediaPool --mode Force backup mode -n, --noconfirm Do not ask for confirmation for each configuration. Will still prompt for backup --nonotify Disable graphical notification (via dbus) --nopv Disable graphical progress output (disable pv) --noionice Disable setting of I/O class and priority options on target -p, --port The remote port -r, --remote
Send the snapshot backup to a remote machine. The snapshot will be sent via ssh You should specify the remote machine's hostname or ip address. The 'root' user must be permitted to login on the remote machine -s, --subvolid Specify the subvolume id of the mounted BTRFS subvolume to back up to. Defaults to 5 --use-btrfs-quota use btrfs-quota to calculate snapshot size -u, --uuid Specify the UUID of the mounted BTRFS subvolume to back up to. Otherwise will prompt If multiple mount points are found with the same UUID, will prompt user selection -t, --target Specify the mountpoint of the backup device --volumename Specify the name of the tape volume -v, --verbose Be verbose on what's going on (min: --verbose=1, max: --verbose=3) --version show program version EOF exit 0 } verify_archive_structure () { local backup_root=${1##backup_root=} local snapper_config=${2##snapper_target_config=} local snapper_id=${3##snapper_target_sync_id=} local remote_host=${4##remote=} if [ $verbose -ge 3 ]; then printf "${MAGENTA}verify-archive_structure()...${NO_COLOR}\n" fi if [ $verbose -ge 3 ]; then printf "${MAGENTA}Verify archive filesystem structure${NO_COLOR} on target %s...\n" \ "$remote" fi # if not accessible, create backup-path cmd="$ssh stat --format %i $backup_root 2>/dev/null" ret=$(eval $cmd) if [ $? -eq 1 ]; then # strip last dir from backup_root #base_path=${backup_root%/*}; echo $base_path if [ $dryrun -eq 0 ]; then #$($ssh mkdir --mode=0700 --parents $base_path) $($ssh mkdir --mode=0700 --parents $backup_root) if [ $verbose -ge 3 ]; then if [ -z $remote_host ]; then printf "${MAGENTA}Create${NO_COLOR} new backup base-path ${GREEN}'%s'${NO_COLOR}...\n" \ "$backup_root" else printf "${MAGENTA}Create${NO_COLOR} new backup base-path ${GREEN}'%s'${NO_COLOR} on ${MAGENTA}remote host ${GREEN}'%s'${NO_COLOR=} ...\n" \ "$backup_root" "$remote_host" fi fi else if [ -z $remote_host ]; then printf "${MAGENTA}dryrun${NO_COLOR}: Would create backup-path %s ...\n" \ "$base_path" else printf "${MAGENTA}dryrun${NO_COLOR}: Would create backup-path %s on remote host %s ...\n" \ "$remote_host" "$base_path" fi fi fi # archive type: full or incremental # full snyc: save a btrfs full-stream # incremental: save a btrfs incremental-stream. Stream will depend on parent # restore process: # 1) copy in last full to btrfs filesystem # 2) loop though ordered incremental path: "cat | btrfs recieve" # verify that target can take the new archive for given snapshot id if [ "$dryrun" -eq 0 ]; then if [ $verbose -ge 2 ]; then printf "${MAGENTA}Verify existence of path ${GREEN}'%s'${NO_COLOR}\n" \ "$backup_root/$snapper_id" fi cmd="$ssh stat --format %i $backup_root/$snapper_id 2>/dev/null" ret=$(eval $cmd) if [ $? -eq 1 ]; then # Path does not exist if [ $verbose -ge 3 ]; then printf "${MAGENTA}Create${NO_COLOR} path ${GREEN}'%s'${NO_COLOR} to store target snapshot.\n" \ "$backup_root/$snapper_id" fi ret=$(eval $ssh mkdir --mode=0700 \ $backup_root/$snapper_id) if [ $? -ne 0 ]; then printf "${RED}ERROR: ${MAGENTA}Cancel path snapshot creation: Can't create path ${GREEN}'%s'${MAGENTA} to store target snapshot${NO_COLOR}\n" \ "$backup_root/$snapper_id" return 1 fi fi else printf "${MAGENTA}dryrun${NO_COLOR}: Would check/create path %s to store target snapshot ...\n" \ "$backup_root/$snapper_snapshots/$snapper_id" fi } verify_backupdir () { if [ $verbose -ge 3 ]; then printf "${MAGENTA}verify_backupdir()...${NO_COLOR}\n" fi # verify backupdir if [ "$snapper_target_sync_id" -eq 0 ]; then # first backup run snapper_target_sync_id=0 if [ "$backupdir_cmdline" != "none" ]; then backupdir=$backupdir_cmdline backup_root="$selected_target/$backupdir" else if [ ! $batch ]; then read -r -p "Enter name of directory to store backups, relative to $selected_target (to be created if not existing): " backupdir if [ -z "$backupdir" ]; then backup_root="$selected_target" else backup_root="$selected_target/$backupdir" fi else # use sane default if [ -z "$backup_root" ]; then backup_root="$selected_target" fi fi fi fi } verify_snapper_config () { if [ $verbose -ge 3 ]; then printf "${MAGENTA}verify_snapper_config()...${NO_COLOR}\n" fi if [ ! -f "/etc/snapper/configs/$selected_config" ]; then if [ $verbose -ge 2 ]; then printf "Did you forget to create the snapper configuration for config '%s' on source?\n" \ "$selected_config" printf "You can create it with following command:\n" printf "${MAGENTA}snapper --config ${GREEN}%s${MAGENTA} create-config --template ${GREEN}%s${MAGENTA} ${NO_COLOR}\n" \ "$selected_config" "$snapper_subvolume_template" fi printf "${RED}Error: ${MAGENTA}Can't backup selected snapper configuration ${GREEN}'$selected_config'${MAGENTA}, that does not exist${NO_COLOR}\n" return 1 else . /etc/snapper/configs/$selected_config if [ "$SUBVOLUME" = "/" ]; then SUBVOLUME='' fi count=$(snapper --config $selected_config list --type single | \ awk '/'"$snap_description_synced"'/' | \ awk '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {cnt++} END {print cnt}') if [ -n "$count" ] && [ "$count" -gt 1 ]; then printf "${RED}Error: ${GREEN}%s${NO_COLOR} entries are ${RED}marked as ${GREEN}'%s'${NO_COLOR} for snapper config ${RED}'%s'${NO_COLOR}\n" \ "$count" "$snap_description_synced" "$selected_config" printf "Pointing to target with UUID ${GREEN}'%s'${NO_COLOR} and SUBVOLID ${GREEN}'%s'${NO_COLOR}. Skipping configuration ${GREEN}'%s'${NO_COLOR}.\n" \ "$selected_uuid" "$selected_subvol" "$selected_config" printf "Please cleanup for further processing.\n" error "Skipping configuration $selected_config." $snapper_activate_$i="no" continue fi fi } verify_snapper_structure () { local backup_root=${1##backup_root=} local snapper_config=${2##snapper_target_config=} local snapper_id=${3##snapper_target_id=} local remote_host=${4##remote=} if [ $verbose -ge 3 ]; then printf "${MAGENTA}verify-snapper_structure()...${NO_COLOR}\n" fi if [ $verbose -ge 3 ]; then if [ $remote ]; then printf "${MAGENTA}Verify snapper filesystem structure${NO_COLOR} on target ${GREEN}'%s'${NO_COLOR}...\n" \ "$remote" else printf "${MAGENTA}Verify snapper filesystem structure${NO_COLOR} on target ${GREEN}'%s'${NO_COLOR}...\n" \ "$backup_root" fi fi # if not accessible, create backup-path cmd="$ssh stat --format %i $backup_root 2>/dev/null" ret=$(eval $cmd) if [ $? -eq 1 ]; then if [ "$dryrun" -eq 0 ]; then if [ $verbose -ge 3 ]; then if [ -z $remote_host ]; then printf "${MAGENTA}Create${NO_COLOR} new backup-path ${GREEN}'%s'${NO_COLOR}...\n" \ "$backup_root" else printf "${MAGENTA}Create${NO_COLOR} new backup-path ${GREEN}'%s'${NO_COLOR} on ${MAGENTA}remote host ${GREEN}'%s'${NO_COLOR=} ...\n" \ "$backup_root" "$remote_host" fi fi # strip last dir from backup_root base_path=${backup_root%/*} if [ ${#base_path} -ge 1 ]; then if [ $dryrun -eq 0 ]; then if [ $verbose -ge 3 ]; then if [ -z $remote_host ]; then printf "${MAGENTA}Create${NO_COLOR} new backup-path ${GREEN}'%s'${NO_COLOR}...\n" \ "$backup_root" else printf "${MAGENTA}Create${NO_COLOR} new backup-path ${GREEN}'%s'${NO_COLOR} on ${MAGENTA}remote host ${GREEN}'%s'${NO_COLOR=} ...\n" \ "$backup_root" "$remote_host" fi fi ret=$(eval $ssh mkdir --mode=0700 --parents $base_path) if [ $? -eq 1 ]; then if [ -z "$remote" ]; then printf "${RED}Error: Can't create${NO_COLOR} new snapper capable BTRFS ${MAGENTA}subvolume ${GREEN}'%s'${NO_COLOR} ...\n" \ "$backup_root" else printf "${RED}Error: Can't create${NO_COLOR} new snapper capable BTRFS ${MAGENTA}subvolume ${GREEN}'%s'${NO_COLOR} on ${MAGENTA}remote host ${GREEN}'%s'${NO_COLOR} ...\n" \ "$backup_root" "$remote_host" fi return 1 fi else if [ -z $remote_host ]; then printf "${MAGENTA}dryrun${NO_COLOR}: Would create backup-path %s ...\n" \ "$base_path" else printf "${MAGENTA}dryrun${NO_COLOR}: Would create backup-path %s on remote host %s ...\n" \ "$remote_host" "$base_path" fi fi fi fi fi # verify that we have a snapper compatible structure for target config (a btrfs subvolume) cmd="$ssh stat --format %i $backup_root 2>/dev/null" ret=$(eval $cmd) if [ $? -eq 1 ]; then # no inode for given backup_root if [ $dryrun -eq 0 ]; then if [ $verbose -ge 2 ]; then if [ -z "$remote" ]; then printf "${MAGENTA}Create${NO_COLOR} new snapper capable BTRFS ${MAGENTA}subvolume ${GREEN}'%s'${NO_COLOR} ...\n" \ "$backup_root" else printf "${MAGENTA}Create${NO_COLOR} new snapper capable BTRFS ${MAGENTA}subvolume ${GREEN}'%s'${NO_COLOR} on ${MAGENTA}remote host ${GREEN}'%s'${NO_COLOR} ...\n" \ "$backup_root" "$remote_host" fi fi # verify that we can use the correct snapper template cmd="$ssh stat --format %i $SNAPPER_TEMPLATE_DIR/$snapper_subvolume_template 2>/dev/null" if [ -z $(eval $cmd) ]; then printf "${RED}Error: ${MAGENTA}Missing a snapper template %s${MAGENTA} to configure the snapper subvolume ${GREEN}%s${MAGENTA} in ${GREEN}%s${MAGENTA} on ${GREEN}%s${NO_COLOR}\n" \ "$snapper_subvolume_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host" printf "${RED}Error: ${NO_COLOR}Did you miss to install the dsnap-sync's default snapper template on %s?\n" \ "$remote" return 1 #die "snapper template %s to configure the snapper subvolume %s is missing in %s on %s.\n" \ # "$snapper_subvolume_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host" fi # create the non existing remote BTRFS subvolume for given config cmd="$ssh btrfs subvolume create $backup_root 1>/dev/null" ret=$(eval $cmd) if [ $? -ne 0 ]; then printf "${RED}Error: ${MAGENTA}Creation of BTRFS subvolume (backup-root) ${GREEN}%s:%s${MAGENTA} failed.${NO_COLOR}\n" \ "$remote_host" "$backup_root" return 1 else cmd="$ssh chmod 0700 $backup_root" ret=$(eval $cmd) if [ $? -ne 0 ]; then printf "${RED}Error: ${MAGENTA}Changing directory-mode for BTRFS subvolume (backup-root) ${GREEN}%s:%s${MAGENTA} failed. Return-Code: '%s'${NO_COLOR}\n" \ "$remote_host" "$backup_root" "$ret" return 1 fi fi # create the non existing remote BTRFS subvolume for given snapshot #cmd="$ssh btrfs subvolume create $backup_root/$snapper_snapshot 1>/dev/null" #$($cmd) || \ # die "Creation of BTRFS subvolume (snapshot): %s:%s failed.\n" \ # "$remote_host" "$backup_root" "$remote_host" # cmd="$ssh chmod 0700 $backup_root 1>/dev/null" # $($cmd) || \ # die "Changing the directory mode for '$backup_root' on '$remote_host'." else printf "${MAGENTA}dryrun${NO_COLOR}: Would create new snapper configuration from template %s ...\n" \ "$snapper_subvolume_template" printf "${MAGENTA}dryrun${NO_COLOR}: Would create new snapper subvolume '%s' ...\n" \ "$backup_root/$snapper_snapshot" fi elif [ $? -eq 0 ]; then # 256: a btrfs subvolume if [ $ret -ne 256 ]; then printf "${RED}Error: ${GREEN}%s ${MAGENTA}needs to be a BTRFS subvolume. But given ${GREEN}%s ${MAGENTA}is just a directory.${NO_COLOR}\n" \ "$snapper_config" "$backup_root" return 1 fi if [ $verbose -ge 3 ]; then printf "${RED}TODO:${NO_COLOR} check and adapt SUBVOLUME in given config '%s', since mount path might have changed meanwhile\n" "$snapper_config" fi #$ssh $(. $SNAPPER_CONFIG_DIR/$snapper_config) #get_snapper_config "$SNAPPER_CONFIG_DIR/$snapper_config" "SUBVOLUME" #if $ssh [ "$SUBVOLUME" != \"$backup_root\" ]; then # SUBVOLUME="$backup_root" # set_config "$SNAPPER_CONFIG_DIR/$snapper_config" "SUBVOLUME" "$SUBVOLUME" #fi fi # verify that we have a valid snapper config if [ $dryrun -eq 0 ]; then cmd="$ssh stat --format %i $SNAPPER_CONFIG_DIR/$snapper_config 2>/dev/null" ret=$(eval $cmd) if [ $? -eq 1 ]; then # path does not exist, let snapper create the structure # and path $backup_root/.snapshots cmd="$ssh snapper --config $snapper_config create-config \ --template $snapper_subvolume_template \ --fstype btrfs $backup_root" if [ $verbose -ge 2 ]; then printf "${MAGENTA}Create new BTRFS subvolume ${GREEN}'%s'${NO_COLOR} using template ${GREEN}'%s'${NO_COLOR}\n" \ $snapper_config $snapper_subvolume_template fi $(eval $cmd) if [ $? -ne 0 ]; then if [ $remote ]; then printf "${RED}Error: ${MAGENTA}Creation of snapper capable config ${GREEN}%s${MAGENTA} on ${GREEN}%s${MAGENTA} failed${NO_COLOR}\n" \ "$backup_root" "$remote_host" else printf "${RED}Error: ${MAGENTA}Creation of snapper capable config ${GREEN}%s${MAGENTA} failed${NO_COLOR}\n" \ "$backup_root" fi return 1 fi else # WIP: # snapper_config exist, now verify if SUBVOLUME needs to be updated cmd="$ssh snapper list-configs | awk -F '|' '/'\"^$snapper_config\"'/ {print \$1}'" #cmd="$ssh snapper list-configs | awk '/'\"^$snapper_config\"'/' | awk -F '|' ' /'\$1 == "$snapper_config"'/ {print \$1}'" if [ -n $(eval $cmd) ]; then # if changed, adapt targets SUBVOLUME path if [ $verbose -ge 3 ]; then printf "${RED}TODO:${NO_COLOR} Check if value for key 'SUBVOLUME' needs an update in snapper config %s\n" \ "$snapper_config" fi # WIP: Update SUBVOLUME, if backupdir has changed #get_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" #if $ssh [ "$SUBVOLUME" != \"$backup_root\" ]; then # SUBVOLUME="$backup_root" # set_config "/etc/snapper/configs/$snapper_config" "SUBVOLUME" "$SUBVOLUME" # cmd="btrfs subvolume create $backup_root/$snapper_snapshots" # $ssh $cmd || die "Can't create subvolume %s in %s to hold target snapshots.\n" "$snapper_snapshots" "$backup_root/$snapper_config" #fi fi # verify existence of SUBVOLUME $backup_root/.snapshots cmd="$ssh stat --format %i $backup_root/$snapper_snapshots 2>/dev/null" ret=$(eval $cmd) if [ -z $ret ]; then if [ $verbose -ge 2 ]; then printf "${MAGENTA}Create new BTRFS subvolume ${GREEN}'%s'${NO_COLOR}\n" \ $backup_root/$snapper_snapshots fi cmd="$ssh btrfs subvolume create $backup_root/$snapper_snapshots 2>/dev/null" ret=$(eval $cmd) if [ $? -ne 0 ]; then printf "${RED}Error: ${MAGENTA}Creation of snapper subvolume ${GREEN}%s${MAGENTA} failed${NO_COLOR}\n" \ "$backup_root/$snapper_snapshots" return 1 fi else if [ $ret -ne 256 ]; then printf "${RED}Error: ${GREEN}%s ${MAGENTA}needs to be a BTRFS subvolume. But given ${GREEN}%s${MAGENTA} is just a directory${NO_COLOR}\n" \ "$snapper_config" "$backup_root" return 1 fi fi fi else printf "${MAGENTA}dryrun${NO_COLOR}: Would check/create for valid snapper config %s ...\n" \ "$snapper_config" fi # verify that target snapshot can take the new snapshot data id if [ $dryrun -eq 0 ]; then if [ $verbose -ge 2 ]; then printf "${MAGENTA}Verify existence of path ${GREEN}'%s'${NO_COLOR}...\n" \ "$backup_root/$snapper_snapshots/$snapper_id" fi cmd="$ssh stat --format %i $backup_root/$snapper_snapshots/$snapper_id 2>/dev/null" ret=$(eval $cmd) if [ $? -eq 1 ]; then # Path does not exist if [ $verbose -ge 3 ]; then printf "${MAGENTA}Create${NO_COLOR} path ${GREEN}'%s'${NO_COLOR} to store target snapshot.\n" \ "$backup_root/$snapper_snapshots/$snapper_id" fi ret=$(eval $ssh mkdir --mode=0700 \ $backup_root/$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_snapshots/$snapper_id" return 1 fi else cmd="$ssh stat --format %i $backup_root/$snapper_snapshots/$snapper_id/$snapper_snapshot_name 2>/dev/null" ret=$(eval $cmd) if [ $? -eq 0 ] && [ $ret -ne 256 ]; then # a snapshot path exists, but is not a btrfs snapshot if [ -z "$remote" ]; then printf "${RED}Cancel snapshot creation${NO_COLOR}: Directory with id ${GREEN}'%s'${NO_COLOR} already exist in ${BLUE}'%s'${NO_COLOR}, but isn't a btrfs snapshot\n" \ "$snapper_id" "$backup_root/$snapper_snapshots" else printf "${RED}Cancel snapshot creation${NO_COLOR}: Directory with id ${GREEN}'%s'${NO_COLOR} already exists on ${BLUE}'%s'${NO_COLOR} in ${BLUE}'%s'${NO_COLOR}, but isn't a btrfs snapshot\n" \ "$snapper_id" "$remote" "$backup_root/$snapper_snapshots" fi # cleanup generated snapper entry check_snapper_failed_ids $selected_config $batch return 1 fi fi else printf "${MAGENTA}dryrun${NO_COLOR}: Would check/create path %s to store target snapshot ...\n" \ "$backup_root/$snapper_snapshots/$snapper_id" fi } ### # Main ### cwd=`pwd` ssh="" # can't be ported to dash (ERR is not supported) #trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCNAME[@]}"' ERR trap trapkill TERM INT check_prerequisites # validate commandline options, set resonable defaults parse_params $@ # parse infos for backup medium get_media_infos # select the backup target select_target # create and initialize structures for snapper configs run_config_preparation # run backups (Snapshot types: btrfs-snapshot, btrfs-clone, btrfs-archive) run_backup # cleanup if [ -d $TMPDIR ]; then if [ $verbose -ge 2 ]; then printf "${MAGENTA}Cleanup temporary directory ${GREEN}'%s'${NO_COLOR}\n" \ $TMPDIR fi rm -rf $TMPDIR || die "Failed to cleanup temporary directory '%s'\n" "$TMPDIR" fi printf "${BLUE}Backup run completed!${NO_COLOR}\n" # close the read file descriptor #exec 3>&- exec 4>&- if [ $donotify -gt 0 ]; then if [ "$target_cmdline" != "none" ]; then if [ -z "$remote" ]; then notify_info "Finished" "Backup run to target '$target_cmdline' completed!" else notify_info "Finished" "Backup run to remote '$remote' target '$target_cmdline' completed!" fi elif [ "$uuid_cmdline" != "none" ]; then if [ -z "$remote" ]; then notify_info "Finished" "Backup run to uuid '$uuid_cmdline' completed!" else notify_info "Finished" "Backup run to remote '$remote' uuid '$uuid_cmdline' completed!" fi else if [ -z "$remote" ]; then notify_info "Finished" "Backup run to target '$selected_target' completed!" else notify_info "Finished" "Backup run to remote '$remote' target '$selected_target' completed!" fi fi fi if [ $error_count -ge 1 ]; then return 1 fi