#!/bin/sh # snap-sync # https://github.com/wesbarnett/snap-sync # Copyright (C) 2016, 2017 James W. Barnett # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # ------------------------------------------------------------------------- # Takes snapshots of each snapper configuration. It then sends the snapshot to # a location on an external drive. After the initial transfer, it does # incremental snapshots on later calls. It's important not to delete the # snapshot created on your system since that will be used to determine the # difference for the next incremental snapshot. progname="`basename v$0`" version="0.4.4" # The following lines are modified by the Makefile or # find_snapper_config script SNAPPER_CONFIG=/etc/conf.d/snapper SNAPPER_TEMPLATES=/etc/snapper/config-templates TMPDIR=$(mktemp -d) PIPE=$TMPDIR/$progname.out mkfifo $PIPE systemd-cat -t "$progname" < $PIPE & exec 3>$PIPE # global variables donotify=0 answer=no disk_count=-1 disk_uuid_match_count=0 disk_target_match_count=0 disk_subvolid_match_count=0 disk_uuid_match='' selected_uuid='none' selected_target='none' selected_subvol='none' ### # functions ### check_prerequisites () { # requested binaries: which awk >/dev/null 2>&1 || { printf "'awk' is not installed." && exit 1; } which sed >/dev/null 2>&1 || { printf "'sed' is not installed." && exit 1; } which tee >/dev/null 2>&1 || { printf "'tee' 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 wc >/dev/null 2>&1 || { printf "'wc' is not installed." && exit 1; } # optional binaries: which notify-send >/dev/null 2>&1 && { donotify=1; } if [ $(id -u) -ne 0 ] ; then printf "Script must be run as root" ; exit 1 ; fi if [ ! -r "$SNAPPER_CONFIG" ]; then die "$SNAPPER_CONFIG does not exist." fi } check_snapper_failed_ids () { # active, non finished snapshot backups are marked with following string # "$progname backup in progress" (snapper description field) snapper_failed_ids=$(eval snapper -c $selected_config list -t single | awk '/'"$progname"' backup in progress/ {cnt++} END {print cnt}') if [ -n "$snapper_failed_ids" ]; then printf "\nNOTE: Previous failed %s backup snapshots found for '%s'.\n" "$progname" "$selected_config" | tee $PIPE get_answer_yes_no "Delete failed backup snapshots [y/N]? " "no" if [ "$answer" = "yes" ]; then snapper -c $selected_config delete $(snapper -c $selected_config list | awk '/'"$progname"' backup in progress/ {print $3}') fi fi } die () { error "$@" exit 1 } error () { printf "\n==> ERROR: %s\n" "$@" notify_error 'Error' 'Check journal for more information.' } >&2 get_disk_infos () { local disk_uuid local disk_target local fs_option # get mounted BTRFS infos if [ "$(findmnt --noheadings --nofsroot --target / --output FSTYPE)" = "btrfs" ]; then # root filesystem is never seen as valid target location exclude_uuid=$(findmnt --noheadings --nofsroot --types btrfs --target / --output UUID) disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list | grep -v $exclude_uuid | awk '{print $1}') disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,TARGET --list | grep -v $exclude_uuid | awk '{print $2}') fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | grep -v $exclude_uuid | awk '{print $2}') else disk_uuids=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID --list) disk_targets=$($ssh findmnt --noheadings --nofsroot --types btrfs --output TARGET --list) fs_options=$($ssh findmnt --noheadings --nofsroot --types btrfs --output UUID,OPTIONS --list | awk '{print $2}') fi # we need at least one target disk if [ ${#disk_targets} -eq 0 ]; then die "no suitable target disk found \n"die fi # Posix Shells do not support Array. Therefore using ... # Pseudo-Arrays (assumption: equal number of members) # Pseudo-Array: disk_uuid_$i # Pseudo-Array: disk_target_$i # Pseudo-Array: fs_options_$i # Pseudo-Array: disk_selected_$y (reference to $i element) # List: disk_uuid_match (reference to matching preselected uuids) # List: disk_target_match (reference to matching preselected targets) # initialize our structures i=0 for disk_uuid in $disk_uuids; do if [ "$disk_uuid" = "$uuid_cmdline" ]; then if [ ${#disk_uuid_match} -gt 0 ]; then disk_uuid_match="${disk_uuid_match} $i" else disk_uuid_match="$i" fi disk_uuid_match_count=$(($disk_uuid_match_count+1)) fi eval "disk_uuid_$i='$disk_uuid'" disk_count=$(($disk_count+1)) i=$((i+1)) done i=0 for disk_target in $disk_targets; do if [ "$disk_target" = "$target_cmdline" ]; then disk_target_match="$i" disk_target_match_count=$(($disk_target_match_count+1)) fi eval "disk_target_$i='$disk_target'" i=$((i+1)) done i=0 for fs_option in $fs_options; do 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_answer_yes_no () { local message="${1:-'Do you want to proceed [y/N]? '}" local i="none" #printf "Pre-selected answer: %s\n" $answer 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 ;; *) i="none" printf "Select 'y' or 'n'.\n" ;; esac done #printf "Selected answer: %s\n" $answer } notify () { # estimation: batch calls should just log if [ $donotify ]; 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 %s\n" "$progname" "$2" fi } notify_info () { notify "$1" "$2" "information" } notify_error() { notify "$1" "$2" "error" } parse_params () { ### # Evaluate given call parameters ### while [ $# -gt 0 ]; do key="$1" case $key in -h | --help | \-\? | --usage) # Call usage() function. usage ;; -b|--backupdir) backupdir_cmdline="$2" shift 2 ;; -c|--config) selected_config="$2" shift 2 ;; -d|--description) description="$2" shift 2 ;; -n|--noconfirm) noconfirm=1 donotify=0 shift ;; -s|--subvolid|--SUBVOLID) subvolid_cmdline="$2" shift 2 ;; -t|--target|--TARGET) target_cmdline="$2" shift 2 ;; -u|--uuid|--UUID) uuid_cmdline="$2" shift 2 ;; -v|--verbose) verbose=1 shift 1 ;; --backupdir=*) backupdir_cmdline=${1#*=} shift ;; --config=*) selected_config=${1#*=} shift ;; --dry-run) dryrun=1 shift 1 ;; --remote) remote=$2 ssh="ssh $remote" shift 2 ;; --subvolid=*|--SUBVOLID=*) subvolid_cmdline=${1#*=} shift ;; --target=*|TARGET=*) target_cmdline=${1#*=} shift ;; --uuid=*|UUID=*) uuid_cmdline=${1#*=} shift ;; --) # End of all options shift break ;; -*) printf "WARN: Unknown option (ignored): $1" >&2 #shift exit 1 ;; *) die "Unknown option: $key\nRun '$progname -h' for valid options.\n" ;; esac done # Set reasonable defaults . $SNAPPER_CONFIG selected_configs=${selected_configs:-$SNAPPER_CONFIGS} description=${description:-"latest incremental backup"} uuid_cmdline=${uuid_cmdline:-"none"} target_cmdline=${target_cmdline:-"none"} subvolid_cmdline=${subvolid_cmdline:-"none"} backupdir_cmdline=${backupdir_cmdline:-"none"} if [ -z $remote ]; then ssh="" fi if [ "$verbose" ]; then printf "Snap UUID: '%s'\n" "$uuid_cmdline" printf "Snap TARGET: '%s'\n" "$target_cmdline" printf "Snap SUBVOLID: '%s'\n" "$subvolid_cmdline" printf "Snap Backupdir: '%s'\n" "$backupdir_cmdline" printf "Snap Description: '%s'\n" "$description" printf "Snap Config: '%s'\n" "$selected_config" printf "Snap Remote: '%s'\n" "$ssh" if [ "$verbose" ]; then snap_sync_options="verbose=true"; fi if [ "$dryrun" ]; then snap_sync_options="${snap_sync_options} dry-run=true"; fi if [ "$noconfirm" ]; then snap_sync_options="${snap_sync_options} noconfirm=true"; fi printf "Options: '%s'\n" "${snap_sync_options}" fi } run_config () { printf "\nVerify configuration...\n" | tee $PIPE # commandline selection takes precedence if [ -n "$selected_config" ]; then selected_configs=$selected_config fi # Pseudo Arrays $i -> store associated elements of selected_config i=0 for selected_config in $selected_configs; do count=$(eval snapper -c $selected_config list -t single | awk '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {cnt++} END {print cnt}') #count=$(eval snapper -c $selected_config list -t single | grep -c -e "subvolid=$selected_subvol" -e 'uuid=$selected_uuid') if [ -n "$count" ] && [ "$count" -gt 1 ]; then error "More than one snapper entry found with UUID $selected_uuid and SUBVOLID $selected_subvol for configuration '$selected_config'. Skipping configuration '$selected_config'." selected_configs=$(echo $selected_configs | sed -e "s/\($selected_config*\)//") if [ "$verbose" ]; then printf "Counter=%s" "$count" fi continue fi # cleanup failed former runs check_snapper_failed_ids cleanup SNAP_SYNC_EXCLUDE=no if [ -f "/etc/snapper/configs/$selected_config" ]; then . /etc/snapper/configs/$selected_config if [ "$SUBVOLUME" = "/" ]; then SUBVOLUME='' fi else die "Selected snapper configuration $selected_config does not exist." fi if [ $SNAP_SYNC_EXCLUDE = "yes" ]; then continue fi printf "\n" # processed snapshot backup is marked with userdata key/value pairs # backupdir, uuid snapper_sync_id=$(eval snapper -c "$selected_config" list -t single | awk '/subvolid='"$selected_subvol"'/, /uuid='"$selected_uuid"'/ {print $1}') if [ ${#snapper_sync_id} -gt 0 ]; then snapper_sync_snapshot=$SUBVOLUME/.snapshots/$snapper_sync_id/snapshot fi eval "snapper_sync_id_$i='$snapper_sync_id'" eval "snapper_sync_snapshot_$i='$snapper_sync_snapshot'" # verify backupdir if [ -z "$snapper_sync_id" ]; then printf "No backups have been performed for snapper config '%s' on target disk.\n" "$selected_config" if [ "$backupdir_cmdline" != "none" ]; then backupdir=$backupdir_cmdline backup_root="$selected_target/$backupdir" else if [ ! $noconfirm ]; 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 fi fi else if [ "$verbose" ]; then printf "Last syncronized Snapshot-ID for '%s': %s\n" "$selected_config" "$snapper_sync_id" printf "Last syncronized Snapshot-Path for '%s': %s\n" "$selected_config" "$snapper_sync_snapshot" fi backupdir=$(snapper -c "$selected_config" list -t single | awk -F "|" '/'"$selected_uuid"'/, /subvolid'="$selected_subvol"'/ {print $5}' | awk -F "," '/backupdir/ {print $1}' | awk -F"=" '{print $2}') if [ -z "$backupdir" ]; then backup_root="$selected_target" else backup_root="$selected_target/$backupdir" fi $ssh test -d $backup_root || die "%s is not a directory on %s.\n" "$backup_root" "$selected_uuid" fi eval "backup_root_$i=$backup_root" eval "backup_dir_$i=$backupdir" if [ "$verbose" ]; then if [ -n "$ssh" ];then printf "Backup-Path on remote %s: %s\n" "$remote" "$backup_root" else printf "Backup-Path: %s\n" "$backup_root" fi fi # acting on source system if [ ! $dryrun ]; then printf "Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE snapper_new_id=$(snapper -c "$selected_config" create --print-number -d "$progname backup in progress") snapper_new_snapshot=$SUBVOLUME/.snapshots/$snapper_new_id/snapshot snapper_new_info=$SUBVOLUME/.snapshots/$snapper_new_id/info.xml sync else printf "dryrun: Creating new snapshot with snapper config '%s' ...\n" "$selected_config" | tee $PIPE snapper_new_id="" fi # if we want to use snapper on the target to supervise the synced snapshots # the backup_location needs to be in a subvol ".snapshots" inside $selected_config (hardcoded in snapper) snapper_target_subvol="snap-$selected_config" snapper_target_snapshots=$backup_root/$snapper_target_subvol/.snapshots/$snapper_new_id if [ -z "$ssh" ]; then printf "Will backup %s to %s\n" "$snapper_new_snapshot" "$snapper_target_snapshots/snapshot" | tee $PIPE else printf "Will backup %s to %s\n" "$snapper_new_snapshot" "$remote":"$snapper_target_snapshots/snapshot" | tee $PIPE fi # save in config specific infos in pseudo Arrays eval "snapper_new_id_$i='$snapper_new_id'" eval "snapper_new_snapshot_$i='$snapper_new_snapshot'" eval "snapper_new_info_$i='$snapper_new_info'" eval "snapper_config_$i='$selected_config'" eval "snapper_target_subvol_$i='$snapper_target_subvol'" eval "snapper_target_snapshots_$i='$snapper_target_snapshots'" cont_backup="K" eval "snapper_activate_$i=yes" if [ $noconfirm ]; then cont_backup="yes" else get_answer_yes_no "Continue with backup [Y/n]? " "yes" if [ "$answer" = "no" ]; then eval "snapper_activate_$i=no" printf "Aborting backup for this configuration.\n" snapper -c $selected_config delete $snapper_new_id fi fi i=$(($i+1)) done } run_backup () { # Actual backing up printf "\nPerforming backups...\n" | tee $PIPE i=-1 for selected_config in $selected_configs; do i=$(($i+1)) SNAP_SYNC_EXCLUDE=no if [ -f "/etc/snapper/configs/$selected_config" ]; then . /etc/snapper/configs/$selected_config else die "Selected snapper configuration '$selected_config' does not exist." fi cont_backup=$(eval echo \$snapper_activate_$i) if [ "$cont_backup" = "no" ] || [ "$SNAP_SYNC_EXCLUDE" = "yes" ]; then notify_info "Backup in progress" "NOTE: Skipping '$selected_config' configuration." continue fi notify_info "Backup in progress" "Backing up data for configuration '$selected_config'." printf "\n" # retrieve config specific infos from pseudo Arrays snapper_config=$(eval echo \$snapper_config_$i) backup_root=$(eval echo \$backup_root_$i) backup_dir=$(eval echo \$backup_dir_$i) snapper_sync_id=$(eval echo \$snapper_sync_id_$i) snapper_new_id=$(eval echo \$snapper_new_id_$i) snapper_sync_snapshot=$(eval echo \$snapper_sync_snapshot_$i) snapper_new_snapshot=$(eval echo \$snapper_new_snapshot_$i) snapper_new_info=$(eval echo \$snapper_new_info_$i) snapper_target_subvol=$(eval echo \$snapper_target_subvol_$i) snapper_target_snapshots=$(eval echo \$snapper_target_snapshots_$i) if [ ! "$dryrun" ]; then verify_snapper_structure $backup_root $snapper_config $snapper_target_subvol $snapper_new_id else cmd="verify_snapper_structure $backup_root $snapper_config $snapper_target_subvol $snapper_new_id" printf "dryrun: %s\n" "$cmd" fi if [ -z "$snapper_sync_id" ]; then cmd="btrfs send $snapper_new_snapshot | $ssh btrfs receive $snapper_target_snapshots" printf "Sending first snapshot for snapper config '%s' ...\n" "$selected_config" | tee $PIPE if [ "$verbose" ]; then printf "btrfs send %s | %s btrfs receive %s\n" "$snapper_new_snapshot" "$ssh" "$snapper_target_snapshots" cmd="btrfs send -v $snapper_new_snapshot | $ssh btrfs receive -v $snapper_target_snapshots" fi if [ ! "$dryrun" ]; then btrfs send "$snapper_new_snapshot" | $ssh btrfs receive "$snapper_target_snapshots" &>/dev/null else cmd="btrfs send -v $snapper_new_snapshot | $ssh btrfs receive -v $snapper_target_snapshots" printf "dryrun: %s\n" "$cmd" fi else printf "Sending incremental snapshot for snapper config '%s' ...\n" "$selected_config" | tee $PIPE # Sends the difference between the new snapshot and old snapshot to the # backup location. Using the -c flag instead of -p tells it that there # is an identical subvolume to the old snapshot at the receiving # location where it can get its data. This helps speed up the transfer. verbose_flag="-v" if [ ! "$dryrun" ]; then btrfs send "$verbose_flag" -c "$snapper_sync_snapshot" "$snapper_new_snapshot" | $ssh btrfs receive "$verbose_flag" "$snapper_target_snapshots" if [ "$verbose" ]; then printf "Deleting sync snapshot for %s ...\n" "$selected_config" | tee $PIPE fi snapper -c "$selected_config" delete "$snapper_sync_id" else printf "dryrun: btrfs send %s -c %s %s | %s btrfs receive %s %s\n" \ "$verbose_flag" "$snapper_sync_snapshot" "$snapper_new_snapshot" \ "$ssh" "$verbose_flag" "$snapper_target_snapshots" printf "dryrun: snapper --config %s delete %s\n" "$selected_config" "$snapper_sync_id" fi fi if [ -z "$ssh" ]; then if [ ! "$dryrun" ]; then cp "$snapper_new_info" "$snapper_target_snapshots" else cmd="cp $snapper_new_info $snapper_target_snapshots" printf "dryrun: %s\n" "$cmd" fi else if [ ! "$dryrun" ]; then rsync -avzq "$snapper_new_info" "$remote":"$snapper_target_snapshots" else cmd="rsync -avzq $snapper_new_info $remote:$snapper_target_snapshots" printf "dryrun: %s\n" "$cmd" fi fi # It's important not to change the values of the key/value pairs ($userdata) # which is stored in snappers info.xml file of the source snapshot. # This is how we find the parent. userdata="backupdir=$backup_dir, subvolid=$selected_subvol, uuid=$selected_uuid" src_host=$(eval cat /etc/hostname) src_uuid=$(eval findmnt --noheadings --output UUID $SUBVOLUME) src_subvolid=$(eval findmnt --noheadings --output OPTIONS $SUBVOLUME | sed -e 's/.*subvolid=\([0-9]*\).*/\1/') target_description="snap-sync backup" target_userdata="subvolid=$src_subvolid, uuid=$src_uuid, host=$src_host" # Tag new snapshot as the latest printf "Tagging snapper metadata for configuration '%s' ...\n" "$selected_config" | tee $PIPE if [ ! "$dryrun" ]; then snapper -v -c "$selected_config" modify -d \"$description\" -u \"$userdata\" "$snapper_new_id" $ssh snapper -v -c "$snapper_target_subvol" modify -d \"$target_description\" -u \"$target_userdata\" "$snapper_new_id" else cmd="snapper -v -c $selected_config modify -d $description -u $userdata $snapper_new_id" printf "dryrun: %s\n" "$cmd" cmd="$ssh snapper -v -c $snapper_target_subvol modify -d $target_description -u $target_userdata $snapper_new_id" printf "dryrun: %s\n" "$cmd" fi # Cleanup old source snapshots source_description="snap-sync backup" $(eval snapper --verbose --config "$selected_config" modify --description \"$source_description\" --cleanup timeline "$snapper_sync_id") #snapper -c "$selected_config" delete "$snapper_sync_id" sync printf "Backup complete for snapper configuration '%s'.\n" "$selected_config" > $PIPE done } select_target_disk () { local i=0 local disk_id=0 #local disk_selected_ids='' local disk_selected_count=0 local subvolid='' local subvol='' # print selection table if [ -z "$ssh" ]; then printf "Selecting a mounted BTRFS device for backups on your local machine.\n" else printf "Selecting a mounted BTRFS device for backups on %s.\n" "$remote" fi while [ "$disk_id" -eq -1 ] || [ "$disk_id" -le $disk_count ]; do if [ "$disk_subvolid_match_count" -eq 1 ]; then # matching SUBVOLID selection from commandline # Pseudo-Array: disk_selected_$i (reference to $disk_uuid element) eval "disk_selected_$i='$disk_subvolid_match'" disk=$(eval echo \$disk_uuid_$disk_subvolid_match) subvolid=$(eval echo \$disk_subvolid_$disk_subvolid_match) fs_options=$(eval echo \$fs_options_$disk_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" ]; 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 printf "%4s) Exit\n" "x" read -r -p "Enter a number: " disk_selected case $disk_selected in x) break ;; [0-9][0-9]|[0-9]) if [ "$disk_selected" -gt "$disk_count" ]; then disk_id=0 i=0 else break fi ;; *) printf "\nNo disk selected. Select a disk to continue.\n" disk_id=0 i=0 ;; esac done if [ "$disk_selected" = x ]; then exit 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" ]; then printf "Selected Subvol-ID=%s: %s on %s\n" "$selected_subvol" "$selected_target" "$selected_uuid" fi } traperror () { printf "Exited due to error on line %s.\n" $1 printf "exit status: %s\n" "$2" #printf "command: %s\n" "$3" #printf "bash line: %s\n" "$4" #printf "function name: %s\n" "$5" exit 1 } trapkill () { die "Exited due to user intervention." } usage () { cat < Change the snapper description. Default: "latest incremental backup" -c, --config Specify the snapper configuration to use. Otherwise will perform for each snapper configuration. Can list multiple configurations within quotes, space-separated (e.g. -c "root home"). -n, --noconfirm Do not ask for confirmation for each configuration. Will still prompt for backup directory name on first backup" -s, --subvolid Specify the subvolume id of the mounted BTRFS subvolume to back up to. Defaults to 5. -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." -t, --TARGET Specify the mountpoint of the BTRFS subvolume to back up to. --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. --dry-run perform a trial run where no changes are made. -v, --verbose Be more verbose on what's going on. EOF exit 1 } verify_snapper_structure () { local backup_root=$1 local snapper_config=$2 local snapper_subvol=$3 local snapper_id=$4 local snapper_snapshots=".snapshots" if [ "$verbose" ]; then echo "Verify snapper filesystem structure on target ..." fi # if not accessible, create backup-path if $ssh [ ! -d $backup_root ]; then if [ "$verbose" ]; then echo "Create backup-path $backup_root ..." fi $ssh mkdir --mode=0700 --parents $backup_root fi if $ssh [ ! -d $backup_root/$snapper_subvol ]; then if $ssh [ ! -f $SNAPPER_TEMPLATES/snap-sync ]; then die "A snapper template %s to configure the snapper subvolume %s is missing in %s. Did you miss to install the package default template?\n" "snap-sync" "$snapper_config" "$SNAPPER_TEMPLATES" fi if [ "$verbose" ]; then printf "Create new snapper capable subvolume in '%s' ...\n" "$backup_root/$snapper_subvol" fi create_subvol="btrfs subvolume create $backup_root/$snapper_subvol" $ssh $create_subvol || die "BTRFS subvolume %s to hold snapshots for config %s could not be created in directory on %s.\n" "$snapper_subvol" "$snapper_config" "$backup_root" $ssh snapper --config $snapper_subvol create-config --template snap-sync $backup_root/$snapper_subvol $ssh chmod --mode=0700 $backup_root/$snapper_config else if $ssh [ `stat --format=%i $backup_root/$snapper_subvol` -ne 256 ]; then die "%s needs to be a BTRFS subvolume. But given %s is just a directory.\n" "$snapper_subvol" "$backup_root/$snapper_subvol" fi fi if $ssh [ ! -d $backup_root/$snapper_subvol/$snapper_snapshots/$snapper_id ]; then if [ "$verbose" ]; then echo "Create backup-path $backup_root/$snapper_subvol/$snapper_snapshots/$snapper_id" fi $ssh mkdir --mode=0700 $backup_root/$snapper_subvol/$snapper_snapshots/$snapper_id fi } ### # Main ### cwd=`pwd` ssh="" # this bashism and can't be ported to dash (ERR is not supported) #trap 'traperror ${LINENO} $? "$BASH_COMMAND" $BASH_LINENO "${FUNCNAME[@]}"' ERR #trap 'traperror ${LINENO} $?' ERR trap trapkill TERM INT parse_params $@ check_prerequisites # read mounted BTRFS structures get_disk_infos if [ "$target_cmdline" != "none" ]; then if [ -z "$ssh" ]; then notify_info "Backup started" "Starting backups to '$target_cmdline' ..." else notify_info "Backup started" "Starting backups to '$target_cmdline' at $remote ..." fi elif [ "$uuid_cmdline" != "none" ]; then if [ -z "$ssh" ]; then notify_info "Backup started" "Starting backups to $uuid_cmdline..." else notify_info "Backup started" "Starting backups to $uuid_cmdline at $remote..." fi else if [ -z "$ssh" ]; then notify_info "Backup started" "Starting backups. Use command line menu to select disk." else notify_info "Backup started" "Starting backups. Use command line menu to select disk on $remote." fi fi # select the target BTRFS subvol select_target_disk printf "\nYou selected the disk with UUID %s.\n" "$selected_uuid" | tee $PIPE if [ -z "$ssh" ]; then printf "The disk is mounted at %s.\n" "$selected_target" | tee $PIPE else printf "The disk is mounted at %s:%s.\n" "$remote" "$selected_target" | tee $PIPE fi # create and initialize structures for snapper configs run_config # run backups using btrfs-send -> btrfs-receive run_backup printf "\nDone!\n" | tee $PIPE exec 3>&- if [ "$uuid_cmdline" != "none" ]; then notify_info "Finished" "Backups to $uuid_cmdline complete!" else notify_info "Finished" "Backups complete!" fi