diff --git a/bin/dsnap-sync b/bin/dsnap-sync new file mode 100644 index 0000000..fe513f6 --- /dev/null +++ b/bin/dsnap-sync @@ -0,0 +1,2876 @@ +#! /bin/dash + +# dsnap-sync +# https://github.com/rzerres/dsnap-sync +# Copyright (C) 2016, 2017 James W. Barnett +# Copyright (C) 2017, 2018 Ralf Zerres + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., + +# ------------------------------------------------------------------------- + +# Takes snapshots of each snapper configuration. It then sends the snapshot to +# a location on an external drive. After the initial transfer, it does +# incremental snapshots on later calls. It's important not to delete the +# snapshot created on your system since that will be used to determine the +# difference for the next incremental snapshot. + +progname="${0##*/}" +version="0.6.0" + +# The following lines are modified by the Makefile or +# find_snapper_config script +#SNAPPER_CONFIG=/etc/conf.d/snapper +SNAPPER_CONFIG=/etc/default/snapper +SNAPPER_TEMPLATE_DIR=/etc/snapper/config-templates +SNAPPER_CONFIG_DIR=/etc/snapper/configs + +# define fifo pipes +TMPDIR_PIPE=$(mktemp --tmpdir=$XDG_RUNTIME_DIR/$progname -d) +PIPE=$TMPDIR_PIPE/$progname.out +test ! -d $XDG_RUNTIME_DIR/$progname && mkdir -p $XDG_RUNTIME_DIR/$progname +mkfifo $PIPE +systemd-cat --identifier="$progname" < $PIPE & + +BTRFS_PIPE=$TMPDIR_PIPE/btrfs.out +#mkfifo $BTRFS_PIPE +#systemd-cat --identifier="btrfs-pipe" < $BTRFS_PIPE & + +# redirect descriptors to given pipes +exec 3>$PIPE 4>$BTRFS_PIPE + +# global variables +args= +answer=no +archive_type=full +batch=0 +btrfs_quota=0 +#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='' +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_subvolume_template="dsnap-sync" +snapper_backup_type='none' +#snapper_config_postfix="."`hostname` +snapper_config_postfix= +snap_cleanup_algorithm="timeline" +verbose=0 + +# ascii color +BLUE= +GREEN= +MAGENTA= +RED= +YELLOW= +NO_COLOR= + +### +# functions +### + +check_prerequisites () { + # requested binaries: + which awk >/dev/null 2>&1 || { printf "'awk' is not installed." && exit 1; } + which sed >/dev/null 2>&1 || { printf "'sed' is not installed." && exit 1; } + which 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; } + + if [ $(id -u) -ne 0 ] ; then printf "$progname: must be run as root\n" ; exit 1 ; fi + + if [ -n "$remote" ]; then + $ssh which sh >/dev/null 2>&1 || \ + { printf "'remote shell' is not working!\n \ + Please correct your public authentication and try again.\n" && exit 1; } + fi + + if [ ! -r "$SNAPPER_CONFIG" ]; then + die "$progname: $SNAPPER_CONFIG does not exist." + fi +} + +check_snapper_failed_ids () { + local 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 + snapper_source_id=$(snapper --config "$selected_config" create \ + --print-number \ + --description "$snap_description_running" \ + --userdata "host=$HOSTNAME") + else + snapper_source_id=$(snapper --config "$selected_config" create \ + --print-number \ + --description "$snap_description_running" \ + --userdata "host=$remote") + fi + if [ $SUBVOLUME = "/" ]; then + SUBVOLUME="" + fi + snapper_source_snapshot=$SUBVOLUME/.snapshots/$snapper_source_id/snapshot + 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}' " + ;; + *) + cmd="find ${selected_target}/${backupdir}/${snapper_target_config} -name *_${archive_type}.btrfs \ + | awk -F '.*/' '{ gsub(/_${archive_type}.btrfs\$/,"_"); print \$2}' " + ;; + esac + fi + + ret=$(eval $run_ssh "$cmd" 2>/dev/null) + if [ ${#ret} -ge 1 ]; then + snapper_sync_id=$ret + 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 + $ssh tape-admin --verbose=$verbose --mount ${mediapool_name} ${volume_name} + 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 + else + printf "${RED}Error: ${NO_COLOR}Can't mount volume ${GREEN}'%s'${NO_COLOR} from MediaPool ${GREEN}'%s'${NO_COLOR}\n" \ + "$volume_name" "$mediapool_name" + die "Can't mount 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 + die "Changing the backupdir for an already existing target-config is not supported yet.\n" + 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 + exclude_uuid=$($ssh findmnt --noheadings --nofsroot --mountpoint / --output UUID) + disk_uuids=$($ssh "findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list \ + | grep -v \"$exclude_uuid\" \ + | awk '{print \$1}'") + disk_targets=$($ssh "findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list \ + | grep -v \"$exclude_uuid\" \ + | awk '{print \$2}'") + fs_options=$($ssh "findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list \ + | grep -v \"$exclude_uuid\" \ + | awk '{print \$2}'") + fi + else + # target location is not mounted as root subvolume + disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID --list) + disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output TARGET --list) + fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | awk '{print $2}') + fi + + # we need at least one target disk + if [ ${#disk_targets} -eq 0 ]; then die "no suitable target disk found"; fi + + # Posix Shells do not support Array. Therefore using ... + # Pseudo-Arrays (assumption: equal number of members) + # Pseudo-Array: disk_uuid_$i + # Pseudo-Array: disk_target_$i + # Pseudo-Array: fs_options_$i + # Pseudo-Array: 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 "no suitable LTFS tape mounted yet\n" + 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 remote=${5##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" + 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 yes.${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}'" + 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/snapshot + else + # no snapshot found, try 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}'" + 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/snapshot + 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/' | 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 + ;; + --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 + ;; + --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" + 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 " 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=" "remote=" + ;; + btrfs-clone) + get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \ + "snapper_uuid=" "snapper_subvolid=" "remote=" + ;; + btrfs-archive) + get_snapper_last_sync_id "snapper_config=${selected_config}" "snapper_description=${snap_description_synced}" \ + "snapper_uuid=" "snapper_subvolid=" "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/snapshot + 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}" + snapper_target_sync_id=$snapper_sync_id + ;; + *) + get_snapper_last_sync_id "snapper_config=${snapper_target_config}" \ + "snapper_description=${snap_description_synced}" \ + "snapper_uuid=" "snapper_subvolid=" "remote=${remote}" + snapper_target_sync_id=$snapper_sync_id + ;; + esac + + # 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/snapshot + 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 + #printf "no commen sync id found.\n" + 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 + + # 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 + snapper_target_sync_snapshot=$value/.snapshots/$snapper_target_sync_id/snapshot + backup_root=$value + 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 + die "Selected snapper configuration '$selected_config' does not exist." + 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 + 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) + + # create the needed snapper structure on the target + if [ $verbose -ge 3 ]; then + printf "Config_type '%s'\n" "$snapper_backup_type" + fi + case $snapper_backup_type in + btrfs-snapshot) + create_snapshot + + # 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/snapshot + 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) + # check for last common snapshot + snapper_source_id=$snapper_source_sync_id + snapper_source_snapshot=$SUBVOLUME/.snapshots/$snapper_source_sync_id/snapshot + snapper_source_info=$SUBVOLUME/.snapshots/$snapper_source_sync_id/info.xml + + # 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 [ $dryrun -eq 0 ]; then + 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 [ $verbose -ge 3 ]; then + printf "${MAGENTA}BTRFS qgroup show result: ${GREEN}'%s'\b${NO_COLOR}'%s'\n" \ + "$?" "$snapper_source_snapshot_size" + fi + #if [ $? -eq 1 ]; then + # subvolume is not configured for quota, (temporary?) enable that + #btrfs_quota_tmp=1 + #btrfs quota enable $snapper_source_snapshot 2>/dev/null + #btrfs quota rescan -w $snapper_source_snapshot 2>/dev/null + #fi + + # need to substitue btrfs 'x.yyGiB' suffix, since pv will need 'xG' + #if [ $snapper_source_snapshot_size -ge 1073741824 ]; then + 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 2>$BTRFS_PIPE" + ;; + *) + # 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>$BTRFS_PIPE \ + | $cmd_pv \ + $cmd_ionice cat > $snapper_target_snapshot/$snapper_target_stream 2>$BTRFS_PIPE" + else + cmd="btrfs send $btrfs_verbose_flag $snapper_source_snapshot 2>$BTRFS_PIPE \ + | $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 + i=$(($i+1)) + continue + fi + ;; + esac + if [ "$dryrun" -eq 0 ]; then + if [ "$snapper_source_sync_id" -eq 0 ] || [ "$snapper_target_sync_id" -eq 0 ]; then + # target never received any snapshot before + if [ $verbose -ge 2 ]; then + printf "${MAGENTA}Sending ${GREEN}first 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" + 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) 1>/dev/null + if [ "$?" -gt 0 ]; then + printf "${RED}BTRFS_PIPE: %s${NO_COLOR}\n" "$(cat $BTRFS_PIPE)" + die "btrfs pipe error." + fi + else + # source holds synced snapshots + if [ $verbose -ge 2 ]; then + printf "${MAGENTA}Sending ${GREEN}incremental snapshot${NO_COLOR} (id: ${GREEN}'%s'${NO_COLOR}) for snapper config ${GREEN}'%s'${NO_COLOR} ...\n" \ + "$snapper_target_id" "$selected_config" + fi + if [ $verbose -ge 3 ]; then + 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 "New ${GREEN}source${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \ + "$snapper_source_id" "$snapper_source_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" + printf "New ${GREEN}target${NO_COLOR} snapshot id: ${GREEN}'%s'${NO_COLOR} (path: ${GREEN}'%s'${NO_COLOR})\n" \ + "$snapper_target_id" "$snapper_target_snapshot/snapshot" + printf "Common synced snapshot id: ${GREEN}'%s'${NO_COLOR}\n" \ + "$snapper_common_sync_id" + fi + # verify that we have a matching source and target snapshot-id + if [ $snapper_common_sync_id -eq 0 ]; then + if [ ${snapper_source_sync_id} != ${snapper_target_sync_id} ]; then + if [ $snapper_target_sync_id -lt $snapper_target_id ]; then + # select commen sync id + get_snapper_sync_id "snapper_config=${snapper_source_config}" "remote=" + if [ $? -eq 0 ]; then + snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot + else + printf "no commen sync id.\n" + fi + fi + else + snapper_common_sync_id=$snapper_source_id + snapper_common_sync_snapshot=$snapper_source_sync_snapshot + 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 -p (parent) 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 2>$BTRFS_PIPE" + ;; + *) + # Can't use btrfs receive, since target filesystem can't support btrfs snapshot feature + snapper_target_stream=${snapper_target_id}_incremental.btrfs + 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" + ;; + esac + if [ $verbose -ge 3 ]; then + printf "${GREEN}btrfs send${NO_COLOR} is using snapshot ${GREEN}'%s'${NO_COLOR} from ${GREEN}source${NO_COLOR} to sync metadata for new snapshot ${GREEN}'%s'${NO_COLOR} ...\n" \ + "$snapper_source_sync_snapshot" "$snapper_source_snapshot" + printf "${GREEN}btrfs command:${NO_COLOR} '%s'\n" "$cmd" + fi + eval $cmd 1>/dev/null + if [ "$?" -gt 0 ]; then + printf "${RED}BTRFS_PIPE: %s${NO_COLOR}\n" "$(cat $BTRFS_PIPE)" + die "btrfs pipe error." + fi + else + # is this clause possible? + if [ $verbose -ge 3 ]; then + printf "${RED}Error: ${NO_COLOR}No commen sync snapshot ${GREEN}'%s'${NO_COLOR} on ${GREEN}source${NO_COLOR} to sync metadata ...\n" \ + "$snapper_source_sync_snapshot" + die "btrfs send/recieve error." + fi + fi + fi + else + printf "${MAGENTA}dryrun${NO_COLOR}: Would run btrfs send / btrfs receive pipe\n" + #printf "dryrun: '%s'\n" "$cmd" + fi + + # send the snapper info metadata + if [ $dryrun -eq 0 ]; then + if [ -z "$remote" ]; then + cp "$snapper_source_info" "$snapper_target_snapshot" + else + cmd="$scp $snapper_source_info root@$remote:$snapper_target_snapshot/" + $(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} + + 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/snapshot + #$ssh rm -rf $backup_root/$snapper_snapshots/$snapper_target_sync_id + + else + printf "${MAGENTA}dryrun${NO_COLOR}: Would cleanup TEMPDIR and 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 + + #i=-1 + #for selected_config in $selected_configs; do + # i=$(($i+1)) + + SNAP_SYNC_EXCLUDE=no + + if [ -f "/etc/snapper/configs/$selected_config" ]; then + . /etc/snapper/configs/$selected_config + else + die "Selected snapper configuration '$selected_config' does not exist." + fi + + cont_backup=$(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) + #tape_id=$(eval echo \$tape_id_$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 interdependencies + 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) + + 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 \ + --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 -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 + 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" "$(eval $cmd)" + printf "calling: '%s'\n" "$cmd" + fi + ret=$(eval "$cmd") + if [ $verbose -ge 3 ]; then + printf "return: '%s'\n" "$?" + fi + sync + fi + if [ ${#snapper_source_sync_id} -gt 0 ]; then + # TODO: no 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/snapshot + else + snapper_source_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_source_id/snapshot + fi + else + # dry-run output + cmd="$ssh snapper --verbose --config $snapper_target_config modify -description $snap_description_finished --userdata $target_userdata --cleanup-algorithm $snap_cleanup_algorithm $snapper_sync_id" + printf "${MAGENTA}dryrun${NO_COLOR}: %s\n" "$cmd" + 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 + + if [ $verbose -ge 1 ]; then + printf "Backup complete: id=${GREEN}'%s'${NO_COLOR}, config=${GREEN}'%s'${NO_COLOR}\n" \ + "$snapper_source_id" "$selected_config" + fi + #done +} + +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) + volume_name=$(attr -g ltfs.volumeName ${selected_target}) + volume_name=$(echo ${volume_name##*:} | sed -e 's/\r\n//g') + 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 + -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}Cancel path snapshot creation${NO_COLOR}: Can't create path '%s' to store target snapshot.\n" \ + "$backup_root/$snapper_id" + die "Can't create snapshot dir on target." + 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 + die "Can't backup snapper configuration '$selected_config' that does not exist." + 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." + selected_configs=$(eval echo $selected_configs | sed -e "s/\($selected_config\)//") + 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 + printf "${MAGENTA}Verify snapper filesystem structure${NO_COLOR} on target ${GREEN}'%s'${NO_COLOR}...\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 + 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 + $($ssh mkdir --mode=0700 --parents $base_path) + 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 + 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}Missing a snapper template %s${NO_COLOR} to configure the snapper subvolume %s in %s on %s.\n" \ + "$snapper_subvolume_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host" + printf "Did you miss to install the dsnap-sync's default snapper template on %s?\n" \ + "$remote" + die "snapper template %s to configure the snapper subvolume %s is missing in %s on %s.\n" \ + "$snapper_subvolume_template" "$snapper_config" "$SNAPPER_TEMPLATE_DIR" "$remote_host" + fi + # create the non existing remote BTRFS subvolume for given config + cmd="$ssh btrfs subvolume create $backup_root 1>/dev/null" + $(eval $cmd) || die "Creation of BTRFS subvolume (backup-root) %s:%s failed.\n" \ + "$remote_host" "$backup_root" + cmd="$ssh chmod 0700 $backup_root" + $(eval $cmd) || die "Changing the directory mode for %s on %s failed.\n" \ + "$backup_root" "$remote_host" + + # 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 + else + if [ $ret -ne 256 ]; then + die "%s needs to be a BTRFS subvolume. But given %s is just a directory.\n" \ + "$snapper_config" "$backup_root" + 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 + die "Creation of snapper capable config %s on %s failed.\n" \ + "$backup_root" "$remote_host" + else + die "Creation of snapper capable config %s failed.\n" \ + "$backup_root" + fi + 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 ] && [ $ret -ne 256 ]; then + die "Creation of snapper subvolume %s failed.\n" \ + "$backup_root/$snapper_snapshots" + fi + else + if [ $ret -ne 256 ]; then + die "%s needs to be a BTRFS subvolume. But given %s is just a directory.\n" \ + "$snapper_config" "$backup_root" + 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" + die "Can't create snapshot dir on target." + fi + else + cmd="$ssh stat --format %i $backup_root/$snapper_snapshots/$snapper_id/snapshot 2>/dev/null" + ret=$(eval $cmd) + if [ $? -eq 0 ] && [ $ret -eq 256 ]; then + # a former btrfs snapshot already exists + if [ -z "$remote" ]; then + printf "${RED}Cancel snapshot creation${NO_COLOR}: Former snapshot with id ${GREEN}'%s'${NO_COLOR} already exist in ${BLUE}'%s'${NO_COLOR}\n" \ + "$snapper_id" "$backup_root/$snapper_snapshots" + else + printf "${RED}Cancel snapshot creation${NO_COLOR}: Former snapshot with id ${GREEN}'%s'${NO_COLOR} already exists on ${BLUE}'%s'${NO_COLOR} in ${BLUE}'%s'${NO_COLOR}\n" \ + "$snapper_id" "$remote" "$backup_root/$snapper_snapshots" + fi + + # cleanup generated snapper entry + check_snapper_failed_ids $selected_config $batch + die "Can't backup to existing snapshot-id ($snapper_id)!" + 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_PIPE ]; then + rm -rf $TMPDIR_PIPE || die "Failed to cleanup temporary directory '%s'\n" "$TMPDIR_PIPE" +fi + +printf "${BLUE}Backups done!${NO_COLOR}\n" +exec 3>&- + +if [ $donotify -gt 0 ]; then + if [ "$uuid_cmdline" != "none" ]; then + notify_info "Finished" "Backups to $uuid_cmdline complete!" + else + notify_info "Finished" "Backups complete!" + fi +fi diff --git a/etc/dsnap-sync/MediaPools_Test.json b/etc/dsnap-sync/MediaPools_Test.json new file mode 100644 index 0000000..144961e --- /dev/null +++ b/etc/dsnap-sync/MediaPools_Test.json @@ -0,0 +1,214 @@ +{ + "version": "0.0.3", + "MediaPools": "Media-Pools", + "MediaPool": [ + { + "_comment": "MediaPolicy: append | overwrite", + "Name": "Pool 1", + "DefaultRetensionDays": "0", + "Tape-Type": "LTO7", + "Tape": [ + { + "TapeId": "Tape1", + "TapeAttributes": [ + { + "FileSystem": "LTFS", + "VolumeName": "Tape-0001", + "VolumeId": "000001", + "Slot": "1", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + } + ] + }, + { + "TapeId": "Tape2", + "TapeAttributes": [ + { + "FileSystem": "LTFS", + "VolumeName": "Tape-0002", + "VolumeId": "000002", + "Slot": "2", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + } + ] + }, + { + "TapeId": "Tape3", + "TapeAttributes": [ + { + "FileSystem": "LTFS", + "VolumeName": "Tape-0003", + "VolumeId": "000003", + "Slot": "3", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + } + ] + }, + { + "TapeId": "Tape4", + "TapeAttributes": [ + { + "FileSystem": "LTFS", + "VolumeName": "Tape-0004", + "VolumeId": "000004", + "Slot": "4", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + } + ] + }, + { + "TapeId": "Tape5", + "TapeAttributes": [ + { + "FileSystem": "LTFS", + "VolumeName": "Tape-0005", + "VolumeId": "000005", + "Slot": "5", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "20180912205500" + } + ] + } + ], + "Member": [ + { + "VolumeName": "Tape-0001", + "VolumeId": "000001", + "Slot": "2", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0002", + "VolumeId": "000002", + "Slot": "2", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0003", + "VolumeId": "000003", + "Slot": "3", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0004", + "VolumeId": "000004", + "Slot": "4", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0005", + "VolumeId": "000005", + "Slot": "5", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0006", + "VolumeId": "000006", + "Slot": "6", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0007", + "VolumeId": "000007", + "Slot": "7", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0008", + "VolumeId": "000008", + "Slot": "8", + "MediaPolicy": "append", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + } + ] + }, + { + "_comment": "MediaPolicy: append | overwrite", + "Name": "Pool 2", + "DefaultRetensionDays": "7", + "Tape-Type": "LTO7", + "Member": [ + { + "VolumeName": "Tape-0009", + "VolumeId": "000001", + "Slot": "9", + "MediaPolicy": "overwrite", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0010", + "VolumeId": "000002", + "Slot": "10", + "MediaPolicy": "overwrite", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0011", + "VolumeId": "000003", + "Slot": "11", + "MediaPolicy": "overwrite", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0012", + "VolumeId": "000004", + "Slot": "12", + "MediaPolicy": "overwrite", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0013", + "VolumeId": "000005", + "Slot": "13", + "MediaPolicy": "overwrite", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0014", + "VolumeId": "000006", + "Slot": "14", + "MediaPolicy": "overwrite", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + }, + { + "VolumeName": "Tape-0015", + "VolumeId": "000007", + "Slot": "15", + "MediaPolicy": "overwrite", + "RetensionDate": "20180824000000", + "LastWrite": "2018082400000" + } + ] + } + ] +} diff --git a/src/LICENSE b/src/LICENSE deleted file mode 100644 index 23cb790..0000000 --- a/src/LICENSE +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {description} - Copyright (C) {year} {fullname} - - 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., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - {signature of Ty Coon}, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/src/Makefile b/src/Makefile deleted file mode 100644 index 1c957e5..0000000 --- a/src/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -# dsnap-sync -# https://github.com/rzerres/dsnap-sync -# Copyright (C) 2016, 2017 James W. Barnett -# Copyright (C) 2017, 2018 Ralf Zerres - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., - -PKGNAME = dsnap-sync -PREFIX ?= /usr -SNAPPER_CONFIG ?= /etc/default/snapper -SNAPPER_TEMPLATES ?= /etc/snapper/config-templates -DSNAP_SYNC_EXAMPLES = usr/share/doc/dsnap-sync - -BIN_DIR = $(DESTDIR)$(PREFIX)/bin -ETC_DIR = $(DESTDIR)/etc -SYSTEMD_DIR = $(DESTDIR)$(PREFIX)/lib/systemd/system -DOC_DIR = $(DESTDIR)$(PREFIX)/share/doc/dsnap-sync - -.PHONY: install - -install: - @./find_snapper_config || sed -i 's@^SNAPPER_CONFIG=.*@SNAPPER_CONFIG='$(SNAPPER_CONFIG)'@g' bin/$(PKGNAME) - @install -Dm755 bin/* -t $(BIN_DIR)/ - @install -dm755 $(ETC_DIR)/dsnap-sync - @install -Dm644 etc/dsnap-sync/* -t $(ETC_DIR)/dsnap-sync/ - @install -dm755 $(ETC_DIR)/snapper/config-templates - @install -Dm644 etc/snapper/config-templates/* -t $(ETC_DIR)/snapper/config-templates/ - @install -Dm644 $(DSNAP_SYNC_EXAMPLES)/* -t $(DOC_DIR)/ diff --git a/src/TODO.md b/src/TODO.md deleted file mode 100644 index 586986c..0000000 --- a/src/TODO.md +++ /dev/null @@ -1,20 +0,0 @@ -# snap-sync TODO # - -## open tasks ## - -- snap-sync: parallel tasks per config -- snap-sync: introduce snapper function: important snapshots - Important snapshots have important=yes in the userdata - let snapper cleanup/timeline mechanisms respect this - -## finished tasks ## - -- snap-sync: refine backupdir with --interactive -- snap-sync: visualize backup progress (using pv) -- snap-sync: use snapper to administer target synced snapshots -- snap-sync: introduce selectable subvolid option -- snap-sync: refine paramteter parsing -- snap-sync: refine functions structure -- snap-sync: port as posix compatible -- snap-sync: introduce selectable subvolid option -- snap-sync: use snapper to administer target synced snapshots diff --git a/src/bin/dsnap-sync b/src/bin/dsnap-sync deleted file mode 100644 index e69de29..0000000 diff --git a/src/bin/tape-admin b/src/bin/tape-admin deleted file mode 100755 index 6058cc4..0000000 --- a/src/bin/tape-admin +++ /dev/null @@ -1,1737 +0,0 @@ -#! /bin/dash - -# tape-admin -# https://github.com/rzerres/dsnap-sync -# Copyright (C) 2017, 2018 Ralf Zerres - -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., - -# ------------------------------------------------------------------------- - -# Helper routines for tape handling - -progname="${0##*/}" -version="0.0.5" - -# global variables -color=0 -date_cmd="date -u +%Y%m%d%H%M%S" # use universial time -date_iso="" -date_seconds="" -default_changer_device="/dev/changer" -dryrun=0 -ltfs_mountpoint="/media/tape" -ltfs_devname="/dev/sg9" -mediapools_json="/etc/dsnap-sync/MediaPools.json" -mediapool_name="" -timezone="Europe/Berlin" -use_mtx=0 -verbose=0 -volume_name="" -volume_name_active="" -volume_name_next="" - -# helper programms -ATTR=attr -JQ=jq -LTFS=ltfs -LTFSCK=ltfsck -MKLTFS=mkltfs -MTX=mtx -SSH=ssh - -# ascii color -BLUE= -GREEN= -MAGENTA= -RED= -YELLOW= -NO_COLOR= - -### -# functions -### - -check_prerequisites () { - # requested binaries: - which ${ATTR} >/dev/null 2>&1 || { printf "'%s' is not installed.\n" ${ATTR} && exit 1; } - which ${MTX} >/dev/null 2>&1 || { printf "'%s' is not installed.\n" ${MTX} && exit 1; } - which ${LTFS} >/dev/null 2>&1 || { printf "'%s' is not installed.\n" ${LTFS} && exit 1; } - which findmnt >/dev/null 2>&1 || { printf "'findmnt' is not installed." && exit 1; } - which ${MKLTFS} >/dev/null 2>&1 || { printf "'%s' is not installed.\n" ${MKLTFS} && exit 1; } - - which ${JQ} >/dev/null 2>&1 || { printf "'%s' is not installed.\n" ${JQ} && exit 1; } - which perl >/dev/null 2>&1 || { printf "'%s' is not installed.\n" perl && exit 1; } - which awk >/dev/null 2>&1 || { printf "'%s' is not installed.\n" awk && exit 1; } - which sed >/dev/null 2>&1 || { printf "'%s' is not installed.\n" sed && exit 1; } - -} - -compare_date () { - local date1=${1} - local date2=${2} - - if [ $date1 -eq $date2 ]; then - if [ $verbose -ge 2 ]; then - printf "${MAGENTA}date1 ${GREEN}'%s'${MAGENTA} is equal to date2 ${GREEN}'%s'${NO_COLOR}\n" \ - "$date1" "$date2" - fi - return 0 - elif [ $date1 -lt $date2 ]; then - if [ $verbose -ge 2 ]; then - printf "${MAGENTA}date1 ${GREEN}'%s'${MAGENTA} is lower then date2 ${GREEN}'%s'${NO_COLOR}\n" \ - "$date1" "$date2" - fi - return 1 - elif [ $date1 -gt $date2 ]; then - if [ $verbose -ge 2 ]; then - printf "${MAGENTA}date1 ${GREEN}'%s'${MAGENTA} is greater then date2 ${GREEN}'%s'${NO_COLOR}\n" \ - "$date1" "$date2" - fi - return 2 - fi -} - -date_iso_to_seconds () { - local date_string=$1 - - year=$(echo $date_string | cut -c 1-4) - month=$(echo $date_string | cut -c 5-6) - day=$(echo $date_string | cut -c 7-8) - hour=$(echo $date_string | cut -c 9-10) - minute=$(echo $date_string | cut -c 11-12) - second=$(echo $date_string | cut -c 13-14) - - test ${month#0} -gt 12 && printf "${RED}Error:${NO_COLOR} Invalid argument month: '%s'\n" $month && return 1 - test ${day#0} -gt 31 && printf "${RED}Error:${NO_COLOR} Invalid argument day: '%s'\n" $day && return 1 - test ${hour#0} -gt 23 && printf "${RED}Error:${NO_COLOR} Invalid argument hour: '%s'\n" $hour && return 1 - test ${minute#0} -gt 59 && printf "${RED}Error:${NO_COLOR} Invalid argument minute: '%s'\n" $minute && return 1 - test ${second#0} -gt 50 && printf "${RED}Error:${NO_COLOR} Invalid argument second: '%s'\n" $second && return 1 - - date_iso=$(printf "%s-%s-%s %s:%s:%s\n" \ - ${year} ${month} ${day} ${hour} ${minute} ${second}) - date_seconds=$(date --date="$date_iso" +"%s") -} - -date_seconds_to_iso () { - local date_string=$1 - - date_iso=$(date --date="$date_string" +"%Y-%M-%d %H:%M:%S") -} - -date_string_to_iso () { - local date_string=$1 - - year=$(echo $date_string | cut -c 1-4) - month=$(echo $date_string | cut -c 5-6) - day=$(echo $date_string | cut -c 7-8) - hour=$(echo $date_string | cut -c 9-10) - minute=$(echo $date_string | cut -c 11-12) - second=$(echo $date_string | cut -c 13-14) - - test ${month#0} -gt 12 && printf "${RED}Error:${NO_COLOR} Invalid argument month: '%s'\n" $month && return 1 - test ${day#0} -gt 31 && printf "${RED}Error:${NO_COLOR} Invalid argument day: '%s'\n" $day && return 1 - test ${hour#0} -gt 23 && printf "${RED}Error:${NO_COLOR} Invalid argument hour: '%s'\n" $hour && return 1 - test ${minute#0} -gt 59 && printf "${RED}Error:${NO_COLOR} Invalid argument minute: '%s'\n" $minute && return 1 - test ${second#0} -gt 59 && printf "${RED}Error:${NO_COLOR} Invalid argument second: '%s'\n" $second && return 1 - - date_iso=$(printf "%s-%s-%s %s:%s:%s\n" \ - ${year} ${month} ${day} ${hour} ${minute} ${second}) -} - -die () { - error "$@" - exit 1 -} - -error () { - printf "\n==> ERROR: %s\n" "$@" - notify_error 'Error' 'Check journal for more information.' -} >&2 - -get_lastwrite () { - local mediapools_json=${mediapools:-$mediapools_json} - local mediapool_name=${1} - #local volume_name=${2} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}get_lastwrite...${NO_COLOR}\n" - fi - - if test ! -r ${mediapools_json}; then - if [ $verbose -ge 1 ]; then - printf "${RED}Error:${MAGENTA} media-pool file ${GREEN}'%s'${MAGENTA} can't be opend!${NO_COLOR}\n" \ - "$mediapools_json" - fi - exit 1 - fi - - if [ ${#volume_name} -ge 1 ]; then - # select last write date for given volume - cmd="jq --monochrome-output --ascii-output ' .MediaPool[] \ - | select(.Name == \"${mediapool_name}\") \ - | .Member[] \ - | select(.VolumeName == \"${volume_name}\") \ - | .LastWrite ' \ - ${mediapools_json}" - else - # select volume with latest write date - cmd="jq --monochrome-output --ascii-output ' .MediaPool[] \ - | select(.Name == \"${mediapool_name}\") \ - | .Member \ - | map({ \"VolumeName\" : .VolumeName, \"LastWrite\" : ( .LastWrite | scan(\"[0-9]{14}\")) }) \ - | sort_by(.LastWrite) \ - | .[-1] \ - | .VolumeName ' \ - ${mediapools_json}" - volume_name=$(eval $cmd) - volume_name=$(echo $volume_name | sed -e 's/"//g') - cmd="jq --monochrome-output --ascii-output ' .MediaPool[] \ - | select(.Name == \"${mediapool_name}\") \ - | .Member \ - | map({ \"VolumeName\" : .VolumeName, \"LastWrite\" : ( .LastWrite | scan(\"[0-9]{14}\")) }) \ - | sort_by(.LastWrite) \ - | .[-1] \ - | .LastWrite ' \ - ${mediapools_json}" - fi - volume_lastwrite=$(eval $cmd) - if [ ${#volume_lastwrite} -gt 1 ]; then - volume_lastwrite=$(echo $volume_lastwrite | sed -e 's/"//g') - if [ $verbose -ge 3 ]; then - printf "${MAGENTA}volume_lastwrite for for volume ${GREEN}'%s'${MAGENTA} in media-pool ${GREEN}'%s'${MAGENTA} is: ${NO_COLOR}%s${NO_COLOR}\n" \ - "$volume_name" "$mediapool_name" "$volume_lastwrite" - fi - else - return 1 - fi -} - -get_mediapolicy () { - local mediapools_json=${mediapools:-$mediapools_json} - local mediapool_name=${1} - local volume_name=${2} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}get_mediapolicy...${NO_COLOR}\n" - fi - - if test ! -r ${mediapools_json}; then - if [ $verbose -ge 1 ]; then - printf "${RED}Error:${MAGENTA} media-pool file ${GREEN}'%s'${MAGENTA} can't be opend!${NO_COLOR}\n" \ - "$mediapools_json" - fi - exit 1 - fi - - cmd="jq --monochrome-output --ascii-output ' .MediaPool[] \ - | select(.Name == \"${mediapool_name}\") \ - | .Member[] \ - | select(.VolumeName == \"${volume_name}\") \ - | .MediaPolicy ' \ - ${mediapools_json}" - volume_mediapolicy=$(eval $cmd) - volume_mediapolicy=$(echo $volume_mediapolicy | sed -e 's/"//g') - - if [ $verbose -ge 3 ]; then - printf "${MAGENTA}MediaPolicy for volume_name ${GREEN}'%s'${MAGENTA} in media-pool ${GREEN}'%s'${MAGENTA}: ${NO_COLOR}%s${NO_COLOR}\n" \ - "$volume_name" "$mediapool_name" "$volume_mediapolicy" - fi -} - -get_mediapool_names () { - local mediapools_json=${mediapools:-$mediapools_json} - #local mediapool_name=${1} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}get_mediapools...${NO_COLOR}\n" - fi - - if test ! -r ${mediapools_json}; then - if [ $verbose -ge 1 ]; then - printf "${RED}Error:${MAGENTA} media-pool file ${GREEN}'%s'${MAGENTA} can't be opend!${NO_COLOR}\n" \ - "$mediapools_json" - fi - exit 1 - fi - - cmd="jq --monochrome-output --ascii-output ' .MediaPool[] \ - | .Name ' \ - ${mediapools_json}" - mediapool_names=$(eval $cmd) - mediapool_names=$(echo $mediapool_names | sed -e 's/"//g') - - if [ $verbose -ge 3 ]; then - printf "${MAGENTA}Media-pool names: ${GREEN}'%s'${NO_COLOR}\n" \ - "$mediapool_names" - fi -} - -get_poolmember () { - # return: 0-> volume_name match; 1 -> volume_name does not match - local mediapools_json=${mediapools:-$mediapools_json} - local mediapool_name=${1} - local volume_name=${2:-any} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}get_poolmember...${NO_COLOR}\n" - fi - - if test ! -r ${mediapools_json}; then - if [ $verbose -ge 1 ]; then - printf "${RED}Error:${MAGENTA} media-pool file ${GREEN}'%s'${MAGENTA} can't be opend!${NO_COLOR}\n" \ - "$mediapools_json" - fi - exit 1 - fi - - #cmd="jq --monochrome-output --join-output --ascii-output '.MediaPool[] \ - cmd="jq --monochrome-output --ascii-output '.MediaPool[] \ - | select(.Name == \"${mediapool_name}\") \ - | .Member[].VolumeName' \ - ${mediapools_json}" - poolmember=$(eval $cmd) - poolmember=$(echo $poolmember | sed -e 's/"//g') - - if [ $verbose -ge 2 ]; then - printf "${MAGENTA}poolmembers for media-pool ${GREEN}'%s'${MAGENTA} are:\n${NO_COLOR}%s${NO_COLOR}\n" \ - "$mediapool_name" "$poolmember" - fi - - for i in $poolmember ; do - if test "$i" = "${volume_name}"; then - if [ $verbose -ge 3 ]; then - printf "${MAGENTA}volume_name ${GREEN}'%s'${MAGENTA} is member of media-pool ${GREEN}'%s'${NO_COLOR}\n" \ - "$i" "$mediapool_name" - fi - return 0 - break - fi - done - return 1 -} - -get_poolmember_next () { - local mediapools_json=${mediapools:-$mediapools_json} - local mediapool_name=${1} - local volume_name=${2:-"none"} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}get_poolmember_next...${NO_COLOR}\n" - fi - - if test ! -r ${mediapools_json}; then - if [ $verbose -ge 1 ]; then - printf "${RED}Error:${MAGENTA} media-pool file ${GREEN}'%s'${MAGENTA} can't be opend!${NO_COLOR}\n" \ - "$mediapools_json" - fi - exit 1 - fi - - cmd="jq --monochrome-output --ascii-output ' .MediaPool[] \ - | select(.Name == \"${mediapool_name}\") \ - | .Member \ - | map({ \"VolumeName\" : .VolumeName, \"LastWrite\" : ( .LastWrite | scan(\"[0-9]{14}\")) }) \ - | sort_by(.LastWrite) \ - | .[0] \ - | .VolumeName ' \ - ${mediapools_json}" - volume_poolmember=$(eval $cmd) - volume_poolmember=$(echo $volume_poolmember | sed -e 's/"//g') - - i=0 - volume_index=-1 - for member in $volume_poolmember; do - if [ $i -eq 0 ]; then - volume_name_next=$(echo $member | sed -e 's/"//g') - fi - if [ $i -eq $volume_index ]; then - if [ ${#member} -ge 1 ]; then - volume_name_next=$(echo $member | sed -e 's/"//g') - break; - fi - fi - if [ ${#member} -ge 1 ]; then - if [ $member = $volume_name ]; then - volume_index=$(($i+1)) - fi - fi - i=$(($i + 1)) - done - - if [ $verbose -ge 3 ]; then - printf "${MAGENTA}poolmember_next for media-pool ${GREEN}'%s'${MAGENTA} is: ${NO_COLOR}%s${NO_COLOR}\n" \ - "$mediapool_name" "$volume_name_next" - fi -} - -get_slot () { - local mediapools_json=${mediapools:-$mediapools_json} - local mediapool_name=${1} - local volume_name=${2} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}get_slot...${NO_COLOR}\n" - fi - - if test ! -r ${mediapools_json}; then - if [ $verbose -ge 1 ]; then - printf "${RED}Error:${MAGENTA} media-pool file ${GREEN}'%s'${MAGENTA} can't be opend!${NO_COLOR}\n" \ - "$mediapools_json" - fi - exit 1 - fi - - cmd="jq --monochrome-output --ascii-output ' .MediaPool[] \ - | select(.Name == \"${mediapool_name}\") \ - | .Member[] \ - | select(.VolumeName == \"${volume_name}\") \ - | .Slot ' \ - ${mediapools_json}" - volume_slot=$(eval $cmd) - volume_slot=$(echo $volume_slot | sed -e 's/"//g') - - if [ $verbose -ge 3 ]; then - printf "${MAGENTA}Slot for volume_name ${GREEN}'%s'${MAGENTA} from media-pool ${GREEN}'%s'${MAGENTA}: ${NO_COLOR}%s${NO_COLOR}\n" \ - "$volume_name" "$mediapool_name" "$volume_slot" - fi -} - -get_retensiondate () { - local mediapools_json=${mediapools:-$mediapools_json} - local mediapool_name=${1} - local volume_name=${2} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}get_retensiondate...${NO_COLOR}\n" - fi - - if test ! -r ${mediapools_json}; then - if [ $verbose -ge 1 ]; then - printf "${RED}Error:${MAGENTA} media-pool file ${GREEN}'%s'${MAGENTA} can't be opend!${NO_COLOR}\n" \ - "$mediapools_json" - fi - exit 1 - fi - - cmd="jq --monochrome-output --ascii-output '(.MediaPool[] \ - | select(.Name == \"${mediapool_name}\") \ - | .Member[] \ - | select(.VolumeName == \"${volume_name}\")) \ - | .RetensionDate ' \ - ${mediapools_json}" - volume_retensiondate=$(eval $cmd) - if [ ${#volume_retensiondate} -gt 1 ]; then - volume_retensiondate=$(echo $volume_retensiondate | sed -e 's/"//g') - if [ $verbose -ge 3 ]; then - printf "${MAGENTA}RetensionDate for volume_name ${GREEN}'%s'${MAGENTA} in media-pool ${GREEN}'%s'${MAGENTA}: ${NO_COLOR}%s${NO_COLOR}\n" \ - "$volume_name" "$mediapool_name" "$volume_retensiondate" - fi - else - return 1 - fi -} - -ltfs_get_attribute () { - local ltfs_devname=${ltfs_devname} - local ltfs_attribute_name=${1:-volumeName} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}ltfs_get_attribute...${NO_COLOR}\n" - fi - - make_err_file - # Reference: https://www.ibm.com/support/knowledgecenter/en/STQNYL_2.4.0/ltfs_hints_virtual_extended_attribute.html - # eg: ltfs.indexCreator - # ltfs.volumeBlocksize - # ltfs.volumeName, ltfs.volumeSerial, ltfs.volumeUUID - ltfs_attribute_value=$(${ATTR} -g ltfs.$ltfs_attribute_name $ltfs_mountpoint 2>${ERRFILE}) - rm -f ${ERRFILE} - if [ $? -eq 0 ]; then - ltfs_attribute_value=$(echo ${ltfs_attribute_value##*:} | sed -e 's/\r\n//g') - if [ $verbose -ge 2 ]; then - printf "${MAGENTA}LTFS attribute ${GREEN}'%s'${MAGENTA} is: ${GREEN}'%s'${NO_COLOR}\n" \ - "$ltfs_attribute_name" "$ltfs_attribute_value" - fi - return 0 - else - if [ $verbose -ge 2 ]; then - printf "${MAGENTA}LTFS attribute ${GREEN}'%s'${MAGENTA} isn't valid${NO_COLOR}\n" \ - "$ltfs_attribute_name" - fi - fi - return $? -} - -ltfs_format () { - local ltfs_devname=${ltfs_devname} - local volume_name=${1} - local volume_serial=${2} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}ltfs_format...${NO_COLOR}\n" - fi - - make_err_file - ${MKLTFS} --device=$ltfs_devname --volume-name=${volume_name} --tape-serial=${volume_serial} 2>${ERRFILE} - RET=$? - rm -f ${ERRFILE} - return $RET -} - -ltfs_is_mounted () { - local ltfs_devname - - if [ $verbose -ge 1 ]; then - printf "${BLUE}ltfs_is_mounted...${NO_COLOR}\n" - fi - - ltfs_devname=$(findmnt -n -T $ltfs_mountpoint -o source | awk -F ':' '{print $2}') - if [ ${#ltfs_devname} -gt 1 ]; then - if [ $verbose -ge 2 ]; then - printf "${MAGENTA}LTFS tape mounted via ${GREEN}'%s'${MAGENTA} at ${GREEN}'%s'${NO_COLOR}\n" \ - "$ltfs_devname" "$ltfs_mountpoint" - fi - return 0 - else - return 1 - fi -} - -ltfs_mount () { - if [ $verbose -ge 1 ]; then - printf "${BLUE}ltfs_mount...${NO_COLOR}\n" - fi - - ltfs_is_mounted - if [ $? -eq 1 ]; then - if [ ! -d $ltfs_mountpoint ]; then - mkdir -p $ltfs_mountpoint - fi - make_err_file - if [ $verbose -ge 2 ]; then - printf "${MAGENTA}LTFS mounting tape ${GREEN}'%s'${MAGENTA} to ${GREEN}'%s'${NO_COLOR}\n" \ - "$ltfs_devname" "$ltfs_mountpoint" - ${LTFS} -o devname=$ltfs_devname -o verbose=$verbose $ltfs_mountpoint 2>${ERRFILE} - else - ${LTFS} -o devname=$ltfs_devname -o verbose=0 $ltfs_mountpoint 2>${ERRFILE} - fi - if [ $? -eq 0 ]; then - return 0 - else - # format tape if needed - if [ -z $volume_name_active ]; then - slot_source="0" - mtx_getlabel $slot_source - fi - need_format=$(grep "medium is not partitioned" ${ERRFILE}) - rm -f ${ERRFILE} - if [ ${#need_format} -ge 1 ]; then - tape_id=$(echo $volume_name_active | sed -e 's/\([[:alpha:]]*-\)//g') - # TODO: tape_id needs to be exactly 6 character long - volume_serial=$(printf "%0.s0" $(seq 1 $((6 - ${#tape_id})))) - volume_serial="${volume_serial}${tape_id}" - ltfs_format ${volume_name_active} ${volume_serial} - ltfs_mount - fi - return $? - fi - else - if [ $verbose -ge 2 ]; then - printf "${MAGENTA}LTFS tape ${GREEN}'%s'${MAGENTA} is mounted to ${GREEN}'%s'${NO_COLOR}\n" \ - "$ltfs_devname" "$ltfs_mountpoint" - fi - fi -} - -ltfs_umount () { - if [ $verbose -ge 1 ]; then - printf "${BLUE}ltfs_umount...${NO_COLOR}\n" - - fi - - ltfs_is_mounted - if [ $? -eq 0 ]; then - ret=$(umount $ltfs_mountpoint 2>/dev/null) - if [ $? -eq 0 ]; then - if [ $verbose -ge 2 ]; then - printf "${MAGENTA}LTFS tape ${GREEN}'%s'${MAGENTA} unmounted.${NO_COLOR}\n" \ - "$ltfs_devname" - fi - return 0 - else - return 1 - fi - fi -} - -ltfs_wipe () { - local ltfs_devname=${ltfs_devname} - local volume_name=${1} - local tape_id=${2} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}ltfs_wipe...${NO_COLOR}\n" - fi - - rm -rf $ltfs_mountpoint/* -} - -make_err_file() { - ERRFILE=`mktemp $XDG_RUNTIME_DIR/mtx.err.XXXXXXXXXX` - if test x${ERRFILE} = x; then - ERRFILE="$XDG_RUNTIME_DIR/mtx.err.$$" - if test -f ${ERRFILE}; then - echo "ERROR: Temp file security problem on: ${ERRFILE}" - exit 1 - fi - fi -} - -make_temp_file() { - TMPFILE=`mktemp $XDG_RUNTIME_DIR/mtx.XXXXXXXXXX` - if test x${TMPFILE} = x; then - TMPFILE="$XDG_RUNTIME_DIR/mtx.$$" - if test -f ${TMPFILE}; then - echo "ERROR: Temp file security problem on: ${TMPFILE}" - exit 1 - fi - fi -} - -mtx_exchange () { - local changer_device=${changer_device:-$default_changer_device} - #export changer_device - local slot_source=${1} - local slot_target=${2} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}mtx_exchange...${NO_COLOR}\n" - fi - - make_err_file - ${MTX} -f $changer_device exchange $slot_source $slot_target 2>${ERRFILE} - RET=$? - rm -f ${ERRFILE} -} - -mtx_getlabel () { - local changer_device=${changer_device:-$default_changer_device} - local slot_source=${slot_source:-99} - export slot_source - - if [ $verbose -ge 1 ]; then - printf "${BLUE}Tape get-label...${NO_COLOR}\n" - fi - - make_temp_file - ${MTX} -f $changer_device status >${TMPFILE} - RET=$? - - case ${slot_source} in - 0) - if [ $verbose -ge 2 ]; then - printf "Calling: ${GREEN}%s${MAGENTA} -f ${GREEN}%s${MAGENTA} %s${NO_COLOR}\n" \ - "${MTX}" "${changer_device}" "${slot_source}" - fi - volume_name_active=$(perl -ne ' - /Data Transfer Element (\d+):Full \(Storage Element (\d+) Loaded\)(:VolumeTag =\s*(.+))?/ && print "$4\n";' ${TMPFILE}) - volume_name_active=$(echo $volume_name_active | sed -e 's/ *$//g') - ;; - [0-9][0-8] | [0-9]) - if [ $verbose -ge 2 ]; then - printf "Calling: ${GREEN}%s${MAGENTA} -f ${GREEN}%s${MAGENTA} %s${NO_COLOR}\n" \ - "${MTX}" "${changer_device}" "${slot_source}" - fi - perl -ne ' - /Storage Element ($ENV{"slot_source"}):Full( :VolumeTag=(.+))?/ && print "$3\n";' ${TMPFILE} - ;; - *) - if [ $verbose -ge 2 ]; then - printf "Calling: ${GREEN}%s${MAGENTA} -f ${GREEN}%s${MAGENTA} -- to list all slots${NO_COLOR}\n" \ - "${MTX}" "${changer_device}" - fi - # can be converted to awk+sed+cut, see below - perl -ne ' - /Data Transfer Element (\d+):Empty/ && print "Drive:$1:empty\n"; - /Data Transfer Element (\d+):Full \(Storage Element (\d+) Loaded\)(:VolumeTag =\s*(.+))?/ && print "Drive:$1:Slot:$2:$4\n"; - /Storage Element (\d+):Empty/ && print "Slot:$1:empty\n"; - /Storage Element (\d+):Full( :VolumeTag=(.+))?/ && print "Slot:$1:$3\n"; - /Storage Element (\d+) IMPORT.EXPORT:Empty/ && print "Import:$1:empty\n"; - /Storage Element (\d+) IMPORT.EXPORT:Full( :VolumeTag=(.+))?/ && print "Import:$1:$3\n";' ${TMPFILE} - ;; - esac - # If perl isn't installed, you can use by those commands - #cat ${TMPFILE} | grep "Data Transfer Element" | awk "{print \"D:\"\$4 \$7 \$9 \$10}" | sed "s/=/:/" | sed "s/Full/F:/" | sed "s/Empty/E/" - #cat ${TMPFILE} | grep -v "Data Transfer Element" | grep "Storage Element" | grep -v "IMPORT/EXPORT" | awk "{print \"S:\"\$3 \$4 \$5}" | sed "s/IMPORT\/EXPORT//" | sed "s/Full *:VolumeTag=/F:/" | sed "s/Empty/E/" - #cat ${TMPFILE} | grep -v "Data Transfer Element" | grep "Storage Element" | grep "IMPORT/EXPORT" | awk "{print \"I:\"\$3 \$4 \$5}" | sed "s/IMPORT\/EXPORT//" | sed "s/Full *:VolumeTag=/F:/" | sed "s/Empty/E/" - - rm -f ${TMPFILE} >/dev/null 2>&1 - return $RET -} - -mtx_inventory () { - local changer_device=${changer_device:-$default_changer_device} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}mtx_inventory...${NO_COLOR}\n" - fi - - ${MTX} -f $changer_device inventory - RET=$? -} - -mtx_load () { - local changer_device=${changer_device:-$default_changer_device} - local slot_source=${1} - local drive=${2} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}mtx_load...${NO_COLOR}\n" - fi - - make_err_file - ${MTX} -f $changer_device load $slot_source $drive 2>${ERRFILE} - RET=$? - rm -f ${ERRFILE} -} - -mtx_status () { - local changer_device=${changer_device:-$default_changer_device} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}Tape status...${NO_COLOR}\n" - fi - - ${MTX} -f $changer_device status -} - -mtx_transfer () { - local changer_device=${changer_device:-$default_changer_device} - #export changer_device - local slot_source=${1##slot_source=} - local slot_target=${2##slot_target=} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}Tape transfer...${NO_COLOR}\n" - fi - - make_err_file - ${MTX} -f $changer_device transfer $slot_source $slot_target 2>${ERRFILE} - RET=$? - rm -f ${ERRFILE} -} - -mtx_unload () { - local changer_device=${changer_device:-$default_changer_device} - local slot_source=${1} - local drive=${2} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}Tape unload...${NO_COLOR}\n" - fi - - make_err_file - for i in 1 2 3 4 5 ; do - ${MTX} -f $changer_device unload $slot_source $drive 2>${ERRFILE} - RET=$? - if test $RET -eq 0; then - break - fi - grep "Error Code=" ${ERRFILE} 2>/dev/null 1>/dev/null - if test $? -ne 0 ; then - break - fi - sleep $i - done - cat ${ERRFILE} - rm -f ${ERRFILE} >/dev/null 2>&1 - if test $RET -ne 0 ; then - if [ $verbose -ge 2 ]; then - printf "${RED}Fail: %s -f %s unload slot=%s drive=%s${NO_COLOR}\n" \ - "${MTX}" "${changer_device}" "${slot_source}" "${drive}" - fi - fi - return $RET -} - -parse_params () { - #printf "\n${BLUE}Parse arguments...${NO_COLOR}\n" - - # Evaluate given call parameters - while [ $# -gt 0 ]; do - key="$1" - case $key in - -h | --help | \-\? | --usage) - # Call usage() function. - usage - ;; - --dry-run|--dryrun) - dryrun=1 - shift 1 - ;; - --get-lastwrite) - shift 1 - pool_params=${*} - pool_params="${pool_params%% -*}" - params=$* - set -- $pool_params - count=$# - test $count -ge 1 && mediapool_name="$1" - test $count -ge 2 && volume_name="$2" - set -- $params - shift $count - cmd=get-lastwrite - ;; - --get-mediapools) - shift 1 - cmd=get-mediapools - ;; - --get-poolmember) - shift 1 - pool_params=${*} - pool_params="${pool_params%% -*}" - params=$* - set -- $pool_params - count=$# - test $count -ge 1 && mediapool_name="$1" - test $count -ge 2 && volume_name="$2" - set -- $params - shift $count - cmd=get-poolmember - ;; - --get-poolmember-next) - shift 1 - pool_params=${*} - pool_params="${pool_params%% -*}" - params=$* - set -- $pool_params - count=$# - test $count -ge 1 && mediapool_name="$1" - test $count -ge 2 && volume_name="$2" - set -- $params - shift $count - cmd=get-poolmember-next - ;; - --get-retensiondate) - shift 1 - pool_params=${*} - pool_params="${pool_params%% -*}" - params=$* - set -- $pool_params - count=$# - test $count -ge 1 && mediapool_name="$1" - test $count -ge 2 && volume_name="$2" - set -- $params - shift $count - cmd=get-retensiondate - ;; - --get-slot) - shift 1 - pool_params=${*} - pool_params="${pool_params%% -*}" - params=$* - set -- $pool_params - count=$# - test $count -ge 1 && mediapool_name="$1" - test $count -ge 2 && volume_name="$2" - set -- $params - shift $count - cmd=get-slot - ;; - --mount) - shift 1 - tape_params=${*} - tape_params="${tape_params%% -*}" - params=$* - set -- $tape_params - count=$# - test $count -lt 1 && usage - mediapool_name="$1" - test $count -ge 2 && volume_name="$2" - set -- $params - shift $count - cmd=mount - ;; - -p|--port) - port=$2 - shift 2 - ;; - --remote) - remote=$2 - shift 2 - ;; - --use-mtx) - use_mtx=1 - shift 1 - ;; - --ltfs-devname) - ltfs_devname="$2" - shift 2 - ;; - --ltfs-getattribute) - shift 1 - ltfs_params=${*} - ltfs_params="${ltfs_params%% -*}" - params=$* - set -- $ltfs_params - count=$# - test $count -eq 0 && ltfs_attribute_name="volumeName" - shift $count - cmd=ltfs-getattribute - ;; - --ltfs-format) - shift 1 - ltfs_params=${*} - ltfs_params="${ltfs_params%% -*}" - params=$* - set -- $ltfs_params - count=$# - test $count -ge 1 && volume_name="$1" - test $count -ge 2 && tape_id="$2" - set -- $params - shift $count - cmd=ltfs-format - ;; - --ltfs-is-mounted) - shift 1 - cmd=ltfs-is-mounted - ;; - --ltfs-mount) - shift 1 - cmd=ltfs-mount - ;; - --ltfs-mountpoint) - ltfs_mountpoint="$2" - shift 2 - ;; - --ltfs-umount) - shift 1 - cmd=ltfs-umount - ;; - --mtx-exchange) - shift - slots=${*} - slots="${slots%% -*}" - params=$* - set -- $slots - count=$# - test $count -lt 2 && usage - slot_source="$1" - slot_target="$2" - set -- $params - shift $count - cmd=mtx-exchange - ;; - --mtx-getlabel) - shift - slots=${*} - slots="${slots%% -*}" - params=$* - set -- $slots - count=$# - test $count -eq 1 && slot_source="$1" - set -- $params - shift $count - slot_getlabel=1 - cmd=mtx-getlabel - ;; - --mtx-inventory|mtx-inquiry) - shift - cmd=mtx-inventory - ;; - --mtx-load) - shift - slots=${*} - slots="${slots%% -*}" - params=$* - set -- $slots - count=$# - test $count -lt 1 && usage - slot_source="$1" - test $count -eq 2 && drive="$2" - set -- $params - shift $count - cmd=mtx-load - ;; - --mtx-transfer) - shift - slots=${*} - slots="${slots%% -*}" - params=$* - set -- $slots - count=$# - test $count -lt 2 && usage - slot_source="${1}" - slot_target="${2}" - set -- $params - shift $count - cmd=mtx-transfer - ;; - --mtx-unload) - shift - slots=${*} - slots="${slots%% -*}" - params=$* - set -- $slots - count=$# - test $count -lt 1 && usage - slot_source="$1" - test $count -eq 2 && drive="$2" - set -- $params - shift $count - cmd=mtx-unload - ;; - --update-lastwrite) - shift 1 - pool_params=${*} - pool_params="${pool_params%% -*}" - params=$* - set -- $pool_params - count=$# - test $count -lt 2 && usage - mediapool_name="$1" - volume_name="$2" - test $count -ge 3 && date_string="$3" - set -- $params - shift $count - cmd=update-lastwrite - ;; - --update-retensiondate) - shift 1 - pool_params=${*} - pool_params="${pool_params%% -*}" - params=$* - set -- $pool_params - count=$# - test $count -lt 2 && usage - mediapool_name="$1" - volume_name="$2" - test $count -ge 3 && date_string="$3" - set -- $params - shift $count - cmd=update-retensiondate - ;; - -v|--verbose) - verbose=$(($verbose + 1)) - shift 1 - ;; - --version) - printf "%s v%s\n" "$progname" "$version" - exit 0 - ;; - --ltfs-devname=*) - ltfs_devname="${1#*=}" - shift - ;; - --ltfs-mountpoint=*) - ltfs_mountpoint="${1#*=}" - shift - ;; - --color=*) - case ${1#*=} in - yes | Yes | True | true) - color=1; - ;; - *) - ;; - esac - shift - ;; - --remote=*) - remote=${1#*=} - shift - ;; - --v=* | --verbose=*) - verbose=${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 - - if [ -z "$remote" ]; then - ssh="" - else - ssh="ssh -T $remote" - if [ ! -z "$port" ]; then - ssh="$ssh -p $port" - fi - fi - - if [ "$color" ]; then - # ascii color - BLUE='\033[0;34m' - GREEN='\033[0;32m' - MAGENTA='\033[0;35m' - RED='\033[0;31m' - YELLOW='\033[0;33m' - NO_COLOR='\033[0m' - fi - - if [ $verbose -ge 2 ]; then - printf "${BLUE}$progname (runtime arguments)...${NO_COLOR}\n" - printf "LTFS Settings\n" - printf " ltfs device-name: '%s'\n" "$ltfs_devname" - printf " ltfs mount-point: '%s'\n" "$ltfs_mountpoint" - printf "MTX Settings\n" - printf " def changer-name: '%s'\n" "$default_changer_device" - - if [ $verbose -ge 1 ]; then tape_options="verbose_level=$verbose"; fi - if [ $dryrun -eq 1 ]; then tape_options="${tape_options} dry-run=true"; fi - if [ $color -eq 1 ]; then tape_options="${tape_options} color=true"; fi - if [ $use_mtx -eq 1 ]; then tape_options="${tape_options} use-mtx=true"; fi - printf "Options: '%s'\n\n" "${tape_options}" - fi -} - -mount_tape () { - local mediapool_name=${1} - local volume_name=${2} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}mount_tape...${NO_COLOR}\n" - fi - - ltfs_is_mounted - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}LTFS Tape is mounted.${NO_COLOR}\n" - fi - - # check that mounted tape is member of selected mediapool - mtx_getlabel 0 - if [ $? -eq 0 ]; then - get_poolmember ${mediapool_name} ${volume_name_active} - if [ $? -eq 1 ]; then - # unmount and recurse execution - ltfs_umount - if [ $? -eq 0 ]; then - mtx_unload - if [ $? -eq 0 ]; then - mount_tape "${mediapool_name}" "${volume_name}" - return 0 - else - return $? - fi - else - return $? - fi - fi - fi - # get date from last written tape in given mediapool - if [ "${#volume_name}" -eq 0 ]; then - get_lastwrite ${mediapool_name} - if [ $? != 0 ]; then - exit 1 - fi - fi - # get label from tape in given drive slot - slot_source="0" - mtx_getlabel $slot_source - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}Tape Label: ${GREEN}%s${NO_COLOR}\n" \ - "${volume_name_active}" - fi - if [ "${#volume_name_active}" -ge 1 ] && [ "${volume_name_active}" != "${volume_name}" ]; then - ltfs_umount - if [ $? -eq 0 ]; then - mtx_unload - if [ $? -eq 0 ]; then - mount_tape "${mediapool_name}" "${volume_name}" - return 0 - else - return $? - fi - else - return $? - fi - fi - # check if given tape is poolmember of selected pool - get_poolmember ${mediapool_name} ${volume_name} - if [ $? -eq 0 ]; then - # volume_name is member of given mediapool - get_mediapolicy ${mediapool_name} ${volume_name} - if [ ${#volume_mediapolicy} -gt 0 ]; then - if [ ${volume_mediapolicy} = "append" ] ; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}Valid tape mediapolicy: ${GREEN}%s${NO_COLOR}\n" \ - "${volume_mediapolicy}" - fi - volume_retensiondate="20180101000000" - get_retensiondate ${mediapool_name} ${volume_name} - if [ $? -eq 0 ]; then - date_now=$($date_cmd) - compare_date $date_now $volume_retensiondate - if [ $? -eq 2 ]; then - # retensiondate has exposed: use it - return 0 - fi - if [ $? -eq 1 ]; then - # respect active retensiondate: unload given tape - ltfs_umount - mtx_unload - # use next volume_name from pool - get_poolmember_next ${mediapool_name} ${volume_name} - get_slot ${mediapool_name} ${volume_name_next} - mtx_load ${volume_slot} - ltfs_mount - if [ $? -eq 0 ]; then - return 0 - fi - fi - return 0 - fi - if [ ${volume_mediapolicy} = "overwrite" ] ; then - volume_retensiondate="20180101000000" - get_retensiondate ${mediapool_name} ${volume_name} - date_now=$($date_cmd) - compare_date $date_now $volume_retensiondate - if [ $? -eq 2 ]; then - # retensiondate has exposed: wipe given tape - ltfs_wipe - if [ $? -eq 0 ]; then - return 0 - fi - fi - if [ $? -eq 1 ]; then - # respect active retensiondate: unload given tape - ltfs_umount - mtx_unload - # use next volume_name from pool - get_poolmember_next ${mediapool_name} ${volume_name} - get_slot ${mediapool_name} ${volume_name_next} - mtx_load ${volume_slot} - ltfs_mount - if [ $? -eq 0 ]; then - return 0 - fi - fi - fi - fi - fi - else - # volume_name is not member of given pool - ltfs_umount - if [ $? -eq 0 ]; then - mtx_unload - if [ $? -eq 0 ]; then - if [ "${#volume_name}" -eq 0 ]; then - get_lastwrite ${mediapool_name} - fi - get_poolmember_next ${mediapool_name} ${volume_name} - if [ $? -eq 0 ]; then - mtx load ${volume_name_next} - ltfs_mount - fi - else - return $? - fi - else - return $? - fi - fi - else - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}No LTFS Tape mounted...${NO_COLOR}\n" - fi - if [ "${#volume_name}" -eq 0 ]; then - # set volume_name and last write date - get_lastwrite ${mediapool_name} - fi - # check label from tape in given drive slot - slot_source="0" - mtx_getlabel $slot_source - if [ $? -eq 0 ] && [ "${#volume_name_active}" -gt 1 ]; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}Tape Label: ${GREEN}%s${NO_COLOR}\n" \ - "${volume_name_active}" - fi - if [ "${#volume_name_active}" -gt 1 ] && [ "${volume_name_active}" != "${volume_name}" ]; then - mtx_unload - if [ $? -eq 0 ]; then - mount_tape "${mediapool_name}" "${volume_name}" - return 0 - else - return $? - fi - fi - else - # mount new tape - get_slot ${mediapool_name} ${volume_name} - mtx_load ${volume_slot} - fi - - # volume_name is member of given mediapool - get_mediapolicy ${mediapool_name} ${volume_name} - if [ ${#volume_mediapolicy} -gt 0 ]; then - if [ ${volume_mediapolicy} = "append" ] ; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}Valid tape mediapolicy: ${GREEN}%s${NO_COLOR}\n" \ - "${volume_mediapolicy}" - fi - volume_retensiondate="20180101000000" - get_retensiondate ${mediapool_name} ${volume_name} - date_now=$($date_cmd) - compare_date $date_now $volume_retensiondate - if [ $? -eq 2 ]; then - # retensiondate has exposed: use it - ltfs_mount - return 0 - fi - fi - if [ ${volume_mediapolicy} = "overwrite" ] ; then - volume_retensiondate="20180101000000" - get_retensiondate ${mediapool_name} ${volume_name} - date_now=$($date_cmd) - compare_date $date_now $volume_retensiondate - if [ $? -eq 2 ]; then - # retensiondate has exposed: wipe given tape - ltfs_mount - if [ $? -eq 0 ]; then - ltfs_wipe - if [ $? -eq 0 ]; then - return 0 - fi - fi - fi - if [ $? -eq 1 ]; then - # respect active retensiondate: unload given tape - mtx_unload - if [ $? -eq 0 ]; then - # use next volume_name from pool - get_poolmember_next ${mediapool_name} ${volume_name} - mount_tape "${mediapool_name}" "${volume_name}" - return 0 - else - return $? - fi - fi - fi - fi - 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 - exit 0 -} - -update_lastwrite () { - local mediapools_json=${mediapools:-$mediapools_json} - local mediapool_name=${1:-0} - local volume_name=${2} - local date_string=${3:-now} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}update_lastwrite...${NO_COLOR}\n" - fi - - if test ! -r ${mediapools_json}; then - if [ $verbose -ge 1 ]; then - printf "${RED}Error:${MAGENTA} media-pool file ${GREEN}'%s'${MAGENTA} can't be opend!${NO_COLOR}\n" \ - "$mediapools_json" - fi - exit 1 - else - json_path=${mediapools_json%/*} - json_file=${mediapools_json##*/} - cp "$mediapools_json" $XDG_RUNTIME_DIR/$json_file - fi - - if [ ${#volume_name} -ge 1 ]; then - # update last write date for given volume (date field in utc) - if [ "${date_string}" = "now" ]; then - volume_lastwrite=$($date_cmd) - else - volume_lastwrite=$date_string - fi - - cmd="jq --monochrome-output --ascii-output '.MediaPool[] \ - | select(.Name == \"$mediapool_name\") \ - | .Member \ - | map(.VolumeName == \"$volume_name\") | index(true)' \ - ${mediapools_json}" - tape_index=$(eval $cmd) - if [ $? -eq 0 ]; then - cmd="jq --monochrome-output --ascii-output '(.MediaPool[] \ - | select(.Name == \"$mediapool_name\") \ - | .Member[$tape_index] \ - | .LastWrite) \ - |= \"$volume_lastwrite\" ' \ - ${mediapools_json} > $XDG_RUNTIME_DIR/$json_file" - $(eval $cmd) - if [ $? -eq 0 ]; then - cp "$XDG_RUNTIME_DIR/$json_file" "$mediapools_json" - else - return 1 - fi - else - return 1 - fi - - if [ $verbose -ge 3 ]; then - printf "${MAGENTA}LastWrite for for volume ${GREEN}'%s'${MAGENTA} is: ${NO_COLOR}%s${NO_COLOR}\n" \ - "$volume_name" "$volume_lastwrite" - fi - fi -} - -update_retensiondate () { - local mediapools_json=${mediapools:-$mediapools_json} - local mediapool_name=${1:-0} - local volume_name=${2} - local date_string=${3:-now} - - if [ $verbose -ge 1 ]; then - printf "${BLUE}update_retensiondate...${NO_COLOR}\n" - fi - - if test ! -r ${mediapools_json}; then - if [ $verbose -ge 1 ]; then - printf "${RED}Error:${MAGENTA} media-pool file ${GREEN}'%s'${MAGENTA} can't be opend!${NO_COLOR}\n" \ - "$mediapools_json" - fi - exit 1 - else - json_path=${mediapools_json%/*} - json_file=${mediapools_json##*/} - cp "$mediapools_json" $XDG_RUNTIME_DIR/$json_file - fi - - if [ ${#volume_name} -ge 1 ]; then - # update retension date for given volume (date field in utc) - if [ "${date_string}" = "now" ]; then - volume_retensiondate=$($date_cmd) - else - volume_retensiondate=$date_string - fi - - cmd="jq --monochrome-output --ascii-output '.MediaPool[] \ - | select(.Name == \"$mediapool_name\") \ - | .Member \ - | map(.VolumeName == \"$volume_name\") | index(true)' \ - ${mediapools_json}" - tape_index=$(eval $cmd) - if [ $? -eq 0 ]; then - cmd="jq --monochrome-output --ascii-output '(.MediaPool[] \ - | select(.Name == \"$mediapool_name\") \ - | .Member[$tape_index] \ - | .RetensionDate) \ - |= \"$volume_retensiondate\" ' \ - ${mediapools_json} > $XDG_RUNTIME_DIR/$json_file" - $(eval $cmd) - if [ $? -eq 0 ]; then - cp "$XDG_RUNTIME_DIR/$json_file" "$mediapools_json" - else - return 1 - fi - else - return 1 - fi - - if [ $verbose -ge 3 ]; then - printf "${MAGENTA}RetensionDate for for volume ${GREEN}'%s'${MAGENTA} is: ${NO_COLOR}%s${NO_COLOR}\n" \ - "$volume_name" "$volume_retensiondate" - fi - fi -} - - -usage () { - cat < [volume_name]) - without given volume_name, extract Pool member that was last witten to - --get-mediapools extract MediaPool names Pool configuration (JSON-File) - --get-mediapolicy extract MediaPolicy attribute from Pool configuration (JSON-File) - (input attribute: [volume_name]) - --get-poolmember extract VolumeName attribute from Pool configuration (JSON-File) - (input attribute: [volume_name]) - --get-poolmember-next extract VolumeName for next usable Pool member (JSON-File) - (input attribute: [volume_name]) - --get-retensiondate extract RetensionDate attribute from Pool configuration (JSON-File) - (input attribute: [volume_name]) - --get-slot extract Slot attribute from Pool configuration (JSON-File) - (input attribute: [volume_name]) - --ltfs-getattribute report ltfs extended attribute for mounted tape - (input attribute: ) - --ltfs-format format tape - (input attribute: ) - --ltfs-is-mounted returns true, if ltfs tape is already mounted - --ltfs-mount mount a ltfs tape (returns true on success) - --ltfs-umount unmount a ltfs tape (returns true on success) - --mtx-exchange exchange tapes in slots - (input attribute: ) - --mtx-getlabel report tape label/barcode for tape in drive - (input attribute: ) - --mtx-inventory run inquriy task for tape-changer - --mtx-load load a tape to target slot - (input attribute: ) - --mtx-status list changer slot status - --mtx-transfer transfer a tape to target slot - (input attribute: ) - --mtx-unload unload a tape - (input attribute: slot_source drive) - --mount make tape accessible for OS - (input attribute: []) - --update-lastwrite update LastWrite attribute for given TapeName in Pool (JSON-File) - (input attribute: [ ) - --update-retensiondate update RetensionDate attribute for given TapeName in Pool (JSON-File) - (input attribute: [ ) - --use-mtx use mtx loader handling. If not specified, all mtx commands will use - default device ($default_changer_device) - -v, --verbose Be verbose on what's going on (min: --verbose=1, max: --verbose=3) - --version show program version -EOF - #-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. - #-p, --port The remote port. - # --dry-run perform a trial run where no changes are made. - - exit 0 -} - -### -# Main -### - -cwd=`pwd` - -# can't be ported to dash (ERR is not supported) -trap trapkill TERM INT - -check_prerequisites - -# validate commandline options, set resonable defaults -parse_params $@ - -case $cmd in - get-mediapolicy) - get_mediapolicy "${mediapool_name}" "${volume_name}" - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}MediaPolicy for ${GREEN}'%s'${MAGENTA} is:${NO_COLOR} %s.\n" \ - "${volume_name}" "${volume_mediapolicy}" - fi - fi - return $? - ;; - get-mediapools) - get_mediapool_names - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}MediaPool names:${NO_COLOR} %s.\n" \ - "${mediapool_names}" - fi - fi - return $? - ;; - get-poolmember) - valid_member=0 - get_poolmember "${mediapool_name}" "${volume_name}" - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}Volume-Name ${GREEN}'%s'${MAGENTA} is a valid mediapool member.${NO_COLOR}\n" \ - "${volume_name}" - fi - valid_member=1 - fi - return $? - ;; - get-lastwrite) - valid_member=0 - get_lastwrite "${mediapool_name}" "${volume_name}" - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - date_string_to_iso ${volume_lastwrite} - if [ $? -eq 0 ]; then - volume_date=$(date --date="${date_iso}Z") - printf "${MAGENTA}Last write date for Volume ${GREEN}'%s'${MAGENTA} from media-pool ${GREEN}'%s'${MAGENTA} is :${NO_COLOR}'%s'.\n" \ - "${volume_name}" "${mediapool_name}" "${volume_date}" - fi - fi - else - return 1 - fi - ;; - get-poolmember-next) - valid_member=0 - get_poolmember_next "${mediapool_name}" "${volume_name}" - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}Next Volume-Name from media-pool ${GREEN}'%s'${MAGENTA} is:${NO_COLOR} %s.\n" \ - "${mediapool_name}" "${volume_name_next}" - fi - fi - return $? - ;; - get-retensiondate) - get_retensiondate "${mediapool_name}" "${volume_name}" - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}RetensionDate for ${GREEN}'%s'${MAGENTA} is:${NO_COLOR} %s.\n" \ - "${volume_name}" "${volume_retensiondate}" - fi - else - return 1 - fi - ;; - get-slot) - get_slot "${mediapool_name}" "${volume_name}" - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}Slot location for ${GREEN}'%s'${MAGENTA} is:${NO_COLOR} %s.\n" \ - "${volume_name}" "${volume_slot}" - fi - fi - return $? - ;; - ltfs-format) - ltfs_format "${volume_name}" "${tape_id}" - ;; - ltfs-getattribute) - ltfs_is_mounted - if test $? -eq 0; then - ltfs_get_attribute "${ltfs_attribute_name}" - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}LTFS attribute ${GREEN}'%s'${MAGENTA} is: ${GREEN}'%s'${NO_COLOR}\n" \ - "${ltfs_attribute_name}" "${ltfs_attribute_value}" - fi - fi - fi - return $? - ;; - ltfs-is-mounted) - ltfs_is_mounted - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}LTFS Tape is-mounted: ${GREEN}true${NO_COLOR}\n" - fi - fi - return $? - ;; - ltfs-mount) - ltfs_mount - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}LTFS Tape mount: ${GREEN}true${NO_COLOR}\n" - fi - fi - return $? - ;; - ltfs-umount) - ltfs_umount - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}LTFS Tape unmount: ${GREEN}true${NO_COLOR}\n" - fi - fi - return $? - ;; - mtx-exchange) - mtx_exchange "${slot_source}" "${slot_target}" - return $? - ;; - mtx-getlabel) - mtx_getlabel $slot_source - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}Tape Label: ${GREEN}%s${NO_COLOR}\n" \ - "${volume_name_active}" - fi - return $? - ;; - mtx-inventory) - mtx_inventory - return $? - ;; - mtx-load) - mtx_load "${slot_source}" "${drive}" - return $? - ;; - mtx-transfer) - mtx_transfer "${slot_source}" "${slot_target}" - return $? - ;; - mtx-unload) - mtx_unload "${slot_source}" "${drive}" - return $? - ;; - mount) - mount_tape "${mediapool_name}" "${volume_name}" - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - printf "${MAGENTA}Mount tape ${GREEN}'%s'${MAGENTA} for ${GREEN}'%s'${MAGENTA}: ${NO_COLOR}true.\n" \ - "${volume_name_active}" "${mediapool_name}" - fi - fi - return $? - ;; - update-lastwrite) - update_lastwrite "${mediapool_name}" "${volume_name}" "${date_string}" - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - date_string_to_iso "${volume_lastwrite}" - if [ $? -eq 0 ]; then - volume_date=$(date --date="${date_iso}Z") - printf "${MAGENTA}LastWrite date for ${GREEN}'%s'${MAGENTA} is: ${NO_COLOR}'%s'.\n" \ - "${volume_name}" "${volume_date}" - fi - fi - fi - return $? - ;; - update-retensiondate) - update_retensiondate "${mediapool_name}" "${volume_name}" "${date_string}" - if test $? -eq 0; then - if [ $verbose -ge 1 ]; then - date_string_to_iso "${volume_retensiondate}" - if [ $? -eq 0 ]; then - volume_date=$(date --date="${date_iso}Z") - printf "${MAGENTA}RetensionDate date for ${GREEN}'%s'${MAGENTA} is: ${NO_COLOR}'%s'.\n" \ - "${volume_name}" "${volume_date}" - fi - fi - fi - return $? - ;; -esac diff --git a/src/etc/dsnap-sync/MediaPools.json b/src/etc/dsnap-sync/MediaPools.json deleted file mode 100644 index ce59dfd..0000000 --- a/src/etc/dsnap-sync/MediaPools.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "version": "0.0.4", - "_comment": "MediaPolicy: append, overwrite; Date values in UTC", - "MediaPools": "Media-Pools", - "MediaPool": [ - { - "Name": "Pool-1", - "DefaultRetensionDays": "0", - "Tape-Type": "LTO7", - "_comment": "MediaPool append", - "Member": [ - { "VolumeName": "Tape-0001", "VolumeId": "000001", "Slot": "1", "MediaPolicy": "append", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0002", "VolumeId": "000002", "Slot": "2", "MediaPolicy": "append", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0003", "VolumeId": "000003", "Slot": "3", "MediaPolicy": "append", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0004", "VolumeId": "000004", "Slot": "4", "MediaPolicy": "append", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0005", "VolumeId": "000005", "Slot": "5", "MediaPolicy": "append", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0006", "VolumeId": "000006", "Slot": "6", "MediaPolicy": "append", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0007", "VolumeId": "000007", "Slot": "7", "MediaPolicy": "append", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0008", "VolumeId": "000008", "Slot": "8", "MediaPolicy": "append", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" } - ] - }, - { - "Name": "Pool-2", - "DefaultRetensionDays": "7", - "Tape-Type": "LTO7", - "_comment": "MediaPool overwrite", - "Member": [ - { "VolumeName": "Tape-0009", "VolumeId": "000009", "Slot": "9", "MediaPolicy": "overwrite", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0010", "VolumeId": "000010", "Slot": "10", "MediaPolicy": "overwrite", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0011", "VolumeId": "000011", "Slot": "11", "MediaPolicy": "overwrite", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0012", "VolumeId": "000012", "Slot": "12", "MediaPolicy": "overwrite", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0013", "VolumeId": "000013", "Slot": "13", "MediaPolicy": "overwrite", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0014", "VolumeId": "000014", "Slot": "14", "MediaPolicy": "overwrite", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" }, - { "VolumeName": "Tape-0015", "VolumeId": "000015", "Slot": "15", "MediaPolicy": "overwrite", "RetensionDate": "20180824000000", "LastWrite": "20180824000000" } - ] - } - ] -} diff --git a/src/etc/snapper/config-templates/dsnap-sync b/src/etc/snapper/config-templates/dsnap-sync deleted file mode 100644 index e69de29..0000000 diff --git a/src/find_snapper_config b/src/find_snapper_config deleted file mode 100755 index 746e741..0000000 --- a/src/find_snapper_config +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -etcdirs="sysconfig default conf.d" - -for x in $etcdirs; do - d=/etc/$x/snapper - if [ -f $d ]; then - sed -i 's@^SNAPPER_CONFIG=.*@SNAPPER_CONFIG='$d'@g' bin/dsnap-sync - exit 0 - fi -done - -printf "==> Unable to find snapper configuration file in a standard location.\n" -printf "==> Using SNAPPER_CONFIG make variable.\n" -exit 1