From 2c736c45a5c1dcb504efa1378de2e0cd67f6e3eb Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Mon, 1 Sep 2025 16:18:15 -0700 Subject: [PATCH 01/27] Add deprecation warning for --config and CONFIG/PATH [#10] Going to simplify the interface where you specify source and destination. --- baksnapper.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/baksnapper.sh b/baksnapper.sh index e18af82..4f9ffbf 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -146,10 +146,12 @@ function read-config { CONFIG*=*) get-value "$line" p_config=${p_config-"$_value"} + warning "CONFIG is deprecated, use SOURCE/DEST." ;; PATH*=*) get-value "$line" parse-full-path "$_value" + warning "PATH is deprecated, use SOURCE/DEST." ;; DAEMON*=*) get-value "$line" @@ -244,6 +246,7 @@ case $key in ;; --config) p_config="$2" + warning "--config is deprecated, use position arguments SOURCE DEST." shift 2 ;; --configfile) From 153f271a3f92827a7f06543a07d09d508fde39c0 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Mon, 1 Sep 2025 16:19:18 -0700 Subject: [PATCH 02/27] Add deprecation warning for --type [#10] Going to simplify the interface where you specify source and destination. --- baksnapper.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/baksnapper.sh b/baksnapper.sh index 4f9ffbf..09ed996 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -276,6 +276,7 @@ case $key in --type) p_type=$2 shift 2 + warning "--type is deprecated, use position arguments SOURCE DEST." ;; --link) p_link=1 From a4ac855fcb2c9679492f7bf11fe4c039a5f3546a Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 14 Sep 2025 15:33:11 -0700 Subject: [PATCH 03/27] Add deprecation warning for TYPE [#10] --- baksnapper.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/baksnapper.sh b/baksnapper.sh index 09ed996..d348c32 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -180,6 +180,7 @@ function read-config { TYPE*=*) get-value "$line" p_type=${p_type-$_value} + warning "TYPE is deprecated, use SOURCE/DEST." ;; PRIVATE_KEY*=*) get-value "$line" From 50e5bea8fe184b056d818136c35755c6ffe63d45 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Mon, 1 Sep 2025 16:19:49 -0700 Subject: [PATCH 04/27] Update help with new usage [#10] It will still show the help for the deprecated workflow using --config/--type. --- baksnapper.sh | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/baksnapper.sh b/baksnapper.sh index d348c32..6ad3676 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -10,10 +10,15 @@ # SPDX-License-Identifier: GPL-3.0-or-later read -rd '' help < Date: Wed, 10 Sep 2025 20:52:50 -0700 Subject: [PATCH 05/27] Remove parse-full-path function --- baksnapper.sh | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/baksnapper.sh b/baksnapper.sh index 6ad3676..661fedd 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -143,18 +143,6 @@ function warning { echo -e "[Warning] $1" 1>&2 } - -function parse-full-path { - if [[ $1 =~ \(.*?\):\(.*\) ]] - then - p_ssh_address=${BASH_REMATCH[1]} - ssh=${ssh-"ssh $p_ssh_address"} - p_dest=${BASH_REMATCH[2]} - else - p_dest=${p_dest-"$1"} - fi -} - function read-config { function get-value { @@ -179,7 +167,7 @@ function read-config { ;; PATH*=*) get-value "$line" - parse-full-path "$_value" + p_dest=${p_dest-"$_value"} warning "PATH is deprecated, use SOURCE/DEST." ;; DAEMON*=*) @@ -328,17 +316,7 @@ esac done ## Setup ####################################################################### -while [[ $# -gt 0 ]] -do - case $1 in - *) - parse-full-path "$1" - shift - break - ;; - esac -done - +p_dest=$1 if [[ $p_verbose == 1 ]] then From dde4c77078125f0cf5b76821937d2e64cc979b7d Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Wed, 10 Sep 2025 21:56:47 -0700 Subject: [PATCH 06/27] Add tests for using the config file [#10] --- meson.build | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/meson.build b/meson.build index 269c61a..eafba88 100644 --- a/meson.build +++ b/meson.build @@ -117,6 +117,41 @@ test( ], ) +latest_test_config = custom_target( + input: configure_file( + output: 'latest.bsconf.meson', + configuration: { + 'CONFIG': 'root', + 'DAEMON': baksnapperd[0].full_path(), + 'PRUNE': 'NO', + 'ALL': 'NO', + 'VERBOSE': 'NO', + 'LINK': 'YES', + } + ), + output: 'latest.bsconf', + command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], + capture: true +) + +test( + 'latest-configfile', + runner, + env: test_env, + args: [ + '--sender', '1,2,3', + '--expected', '3', + '--expected-latest', '3', + '--', + baksnapper[0].full_path(), + '--configfile', latest_test_config.full_path(), + ], + depends: [ + baksnapper, + baksnapperd, + latest_test_config, + ], +) test( 'backup-2', @@ -157,6 +192,42 @@ test( ], ) +prune_test_config = custom_target( + input: configure_file( + output: 'prune.bsconf.meson', + configuration: { + 'CONFIG': 'home', + 'DAEMON': baksnapperd[0].full_path(), + 'PRUNE': 'YES', + 'ALL': 'NO', + 'VERBOSE': 'NO', + } + ), + output: 'prune.bsconf', + command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], + capture: true +) + +test( + 'prune-configfile', + runner, + env: test_env, + args: [ + '--sender', '1,2,3,6,7,8', + '--receiver', '1,2,3,4,5,6', + '--expected', '1,2,3,6,8:p=6', + '--', + baksnapper[0].full_path(), + '--configfile', prune_test_config.full_path(), + '--config', 'root', + ], + depends: [ + baksnapper, + baksnapperd, + prune_test_config, + ], +) + test( 'delete-1', runner, @@ -197,6 +268,41 @@ test( ], ) +delete_1_test_config = custom_target( + input: configure_file( + output: 'delete-1.bsconf.meson', + configuration: { + 'CONFIG': 'root', + 'DAEMON': baksnapperd[0].full_path(), + 'PRUNE': 'NO', + 'ALL': 'NO', + 'VERBOSE': 'NO', + } + ), + output: 'delete-1.bsconf', + command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], + capture: true +) + +test( + 'delete-1-configfile', + runner, + env: test_env, + args: [ + '--sender', '1,2,3', + '--receiver', '1,2,3', + '--expected', '2,3', + '--', + baksnapper[0].full_path(), + '--configfile', delete_1_test_config.full_path(), + '--delete', '1', + ], + depends: [ + baksnapper, + baksnapperd, + delete_1_test_config, + ], +) # https://github.com/plattfot/baksnapper/issues/27 test( 'backup-all-with-right-parent', @@ -218,6 +324,40 @@ test( ], ) +all_test_config = custom_target( + input: configure_file( + output: 'all.bsconf.meson', + configuration: { + 'CONFIG': 'root', + 'DAEMON': baksnapperd[0].full_path(), + 'ALL': 'YES', + 'VERBOSE': 'NO', + } + ), + output: 'all.bsconf', + command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], + capture: true +) + +test( + 'backup-all-configfile', + runner, + env: test_env, + args: [ + '--sender', '64,65,76,83,91,92,93', + '--receiver', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83', + '--expected', '64,65:p=64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83,93:p=92', + '--', + baksnapper[0].full_path(), + '--configfile', all_test_config.full_path(), + ], + depends: [ + baksnapper, + baksnapperd, + all_test_config, + ], +) + test( 'backup-with-right-parent', runner, From c61d1786e3143b51a94a741b7b3520ea4be2428b Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 14 Sep 2025 15:30:43 -0700 Subject: [PATCH 07/27] Simplify parsing of config by using nameref That allows to use an input variable as an output. Avoiding the use of the functions setting global `_` variables. Instead designated variables can be used directly. With that it can check if things are set or not in the function call. --- baksnapper.sh | 70 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/baksnapper.sh b/baksnapper.sh index 661fedd..b19e55d 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -145,63 +145,81 @@ function warning { function read-config { + # Get value from line + # 1 [out]: output variable + # 2 [in]: line to parse value from function get-value { - _value=$(echo "$1" | sed -Ee 's/^[A-Z_ ]+=[ ]*(.*?)/\1/' -e 's/#.*//') + declare -n out=$1 + out=$(echo "$2" | sed -Ee 's/^[A-Z_ ]+=[ ]*(.*?)/\1/' -e 's/#.*//') } + # Only get value from line if output is unset + # 1 [out]: output variable + # 2 [in]: line to parse value from + function get-value-if-not-set { + declare -n out=$1 + local value + get-value value "$2" + out=${out-$value} + } + + # Parse value as boolean + # 1 [out]: output variable + # 2 [ini: value to interpret as boolean function parse-bool { - if [[ "$1" =~ YES|yes|Yes|1 ]] + declare -n out=$1 + if [[ "$2" =~ YES|yes|Yes|1 ]] then - _bool=1 + out=1 else - _bool=0 + out=0 fi } + # Only get boolean from line if output is unset + # 1 [out]: output variable + # 2 [in]: line to parse as bool + function get-bool-if-not-set { + declare -n out=$1 + local value + get-value value "$2" + local bool + parse-bool bool "$value" + out=${out-$bool} + } + while read -r line; do case $line in CONFIG*=*) - get-value "$line" - p_config=${p_config-"$_value"} + get-value-if-not-set p_config "$line" warning "CONFIG is deprecated, use SOURCE/DEST." ;; PATH*=*) - get-value "$line" - p_dest=${p_dest-"$_value"} + get-value-if-not-set p_dest "$line" warning "PATH is deprecated, use SOURCE/DEST." ;; DAEMON*=*) - get-value "$line" - p_baksnapperd=${p_baksnapperd-$_value} + get-value-if-not-set p_baksnapperd "$line" ;; PRUNE*=*) - get-value "$line" - parse-bool "$_value" - p_prune=${p_prune-$_bool} + get-bool-if-not-set p_prune "$line" ;; ALL*=*) - get-value "$line" - parse-bool "$_value" - p_all=${p_all-$_bool} + get-bool-if-not-set p_all "$line" ;; VERBOSE*=*) - get-value "$line" - parse-bool "$_value" - p_verbose=${p_verbose-$_bool} + get-bool-if-not-set p_verbose "$line" ;; LINK*=*) - get-value "$line" - parse-bool "$_value" - p_link=${p_link-$_bool} + get-bool-if-not-set p_link "$line" ;; TYPE*=*) - get-value "$line" - p_type=${p_type-$_value} + get-value-if-not-set p_type "$line" warning "TYPE is deprecated, use SOURCE/DEST." ;; PRIVATE_KEY*=*) - get-value "$line" - p_ssh_args=${p_ssh_args-" -i $_value"} + get-value value "$line" + p_ssh_args=${p_ssh_args-" -i $value"} ;; *) ;; From cc9a4b5c541e64f03eefdded55e4c8d4ba319b67 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 14 Sep 2025 16:03:21 -0700 Subject: [PATCH 08/27] Fix typo in print-statistics docstring: ist -> is --- baksnapper.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/baksnapper.sh b/baksnapper.sh index b19e55d..682862a 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -442,7 +442,7 @@ $receiver create-config "$dest_root" || error "Problem creating config at backup # - First argument ... start-time to calculate the duration # - Second argument ... snapper id -# Function ist used by single-backup and incremental-backup +# Function is used by single-backup and incremental-backup function print-statistics { local start_time local end_time From dc0cf1b17d0cfb8d237e911819752a9afa9f5054 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 14 Sep 2025 16:03:47 -0700 Subject: [PATCH 09/27] Group deprecated --config setup [#10] Moved all commands that uses snapper to configure the source and destination. And sender and receiver. Will make it easier to clean up once the --config option is removed. --- baksnapper.sh | 151 +++++++++++++++++++++++++------------------------- 1 file changed, 75 insertions(+), 76 deletions(-) diff --git a/baksnapper.sh b/baksnapper.sh index 682862a..b4067df 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -130,7 +130,7 @@ function cleanup { function exit-msg { cleanup - notify-send -u critical "Done backing up $p_config. Safe to turn off computer." + notify-send -u critical "Done backing up $src_root. Safe to turn off computer." } @@ -334,38 +334,97 @@ esac done ## Setup ####################################################################### -p_dest=$1 - if [[ $p_verbose == 1 ]] then set -x fi - p_baksnapperd=${p_baksnapperd=baksnapperd} p_all=${p_all=0} -# Error checks -#[[ $USER != root ]] && error "Need to be root to run this script!" -[[ -z $p_config ]] && error "You need to specify the config name to backup!" -[[ -z $p_dest ]] && error "No path specified!" +#### Deprecated config way of setup ############################################ +if [[ -n $p_config ]] +then + [[ -z "$1" ]] && error "No path specified!" + p_dest=${p_dest-$1} + regex='(.*?):(.*)' + if [[ $p_dest =~ $regex ]] + then + address="${BASH_REMATCH[1]}" + ssh="ssh $p_ssh_args $address" + dest="${BASH_REMATCH[2]}" + else + dest=$p_dest + fi -regex='(.*?):(.*)' -if [[ $p_dest =~ $regex ]] -then - address="${BASH_REMATCH[1]}" - ssh="ssh $p_ssh_args $address" - dest="${BASH_REMATCH[2]}" + if [[ -n "$ssh" ]] + then + # Sanity check for ssh + $ssh -q test-connection || error "Unable to connect to $address" + fi + + case ${p_type="push"} in + pull|PULL) + sender=${ssh-$p_baksnapperd} + receiver=$p_baksnapperd + ;; + push|PUSH) + sender=$p_baksnapperd + receiver=${ssh-$p_baksnapperd} + ;; + *) + error "Unknown type! $p_type" + ;; + esac + + if ! receiver_version=$($receiver version) + then + receiver_version=1 + fi + + if ! sender_version=$($sender version) + then + sender_version=1 + fi + + if [[ "$receiver_version" -gt 3 ]] + then + error "receiver is too new, need to use version 1-3" + fi + + if [[ "$sender_version" -gt 3 ]] + then + error "sender is too new, need to use version 1-3" + fi + + # Get the subvolume to backup + case $sender_version in + 2|3) + subvolume=$($sender get-snapper-root "$p_config") + ;; + 1) + if ! subvolume=$($sender list-snapper-snapshots "$p_config") + then + error "Something went wrong when fetching the snapper root from sender" + fi + ;; + *) + error "Unknown version for sender '$sender_version'" + ;; + esac + + src_root=${subvolume:?}/.snapshots + dest_root="$dest/$p_config" else - dest=$p_dest + error "You need to specify the SOURCE and DEST or the config name to backup!" fi ## Setup signals ############################################################### # EXIT --> if notify-send is installed, inform the user; always cleanup if hash notify-send 2> /dev/null then - notify-send -u critical "Backing up $p_config. Do not turn off computer!" + notify-send -u critical "Backing up $src_root. Do not turn off computer!" trap exit-msg EXIT else trap cleanup EXIT @@ -376,66 +435,6 @@ p_temp_dir=$(mktemp -d "${TEMP:-/tmp/}$(basename "$0").XXXXX") p_summary="${p_temp_dir}/summary.txt" printf "dest_root\tsrc\tdest\tbytes\tstart\tend\tduration\n" >"${p_summary}" - -if [[ -n "$ssh" ]] -then - # Sanity check for ssh - $ssh -q test-connection || error "Unable to connect to $address" -fi - -case ${p_type="push"} in - pull|PULL) - sender=${ssh-$p_baksnapperd} - receiver=$p_baksnapperd - ;; - push|PUSH) - sender=$p_baksnapperd - receiver=${ssh-$p_baksnapperd} - ;; - *) - error "Unknown type! $p_type" - ;; -esac - -if ! receiver_version=$($receiver version) -then - receiver_version=1 -fi - -if ! sender_version=$($sender version) -then - sender_version=1 -fi - -if [[ "$receiver_version" -gt 3 ]] -then - error "receiver is too new, need to use version 1-3" -fi - -if [[ "$sender_version" -gt 3 ]] -then - error "sender is too new, need to use version 1-3" -fi - -# Get the subvolume to backup -case $sender_version in - 2|3) - subvolume=$($sender get-snapper-root "$p_config") - ;; - 1) - if ! subvolume=$($sender list-snapper-snapshots "$p_config") - then - error "Something went wrong when fetching the snapper root from sender" - fi - ;; - *) - error "Unknown version for sender '$sender_version'" - ;; -esac - -src_root=${subvolume:?}/.snapshots -dest_root="$dest/$p_config" - $receiver create-config "$dest_root" || error "Problem creating config at backup location" ## Function definitions ######################################################## From 8383dfc4e2dc802fd73cc1731a1924ff3ba7242a Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 14 Sep 2025 17:01:23 -0700 Subject: [PATCH 10/27] Add support for specifying SOURCE and DEST [#10] --- baksnapper.sh | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/baksnapper.sh b/baksnapper.sh index b4067df..aaee784 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -299,6 +299,8 @@ case $key in ;; --private-key) p_ssh_args=" -i $2" + p_dest_ssh_args=("-i" "$2") + p_src_ssh_args=("-i" "$2") shift 2 ;; -p|--prune) @@ -416,6 +418,57 @@ then src_root=${subvolume:?}/.snapshots dest_root="$dest/$p_config" +elif [[ $# -ge 2 ]] +then + # Setup the endpoint's directory and transfer command + # 1 [out]: will store the root of the endpoint + # 2 [out]: will store the transfer command for the endpoint + # 3 [in]: source/dest input + # 4 [in]...: ssh arguments + function setup-endpoint { + declare -n root=$1 + declare -n transfer=$2 + local regex='(.*?):(.*)' + local ssh + local address + local input=$3 + shift 3 + if [[ $input =~ $regex ]] + then + address="${BASH_REMATCH[1]}" + ssh="ssh $* $address" + root="${BASH_REMATCH[2]}" + transfer=$ssh + + # Sanity check for ssh + $ssh -q test-connection || error "Unable to connect to $address" + else + # shellcheck disable=SC2034 + root=$input + # shellcheck disable=SC2034 + transfer=$p_baksnapperd + fi + + } + setup-endpoint src_root sender "$1" "${p_src_ssh_args[@]}" + setup-endpoint dest_root receiver "$2" "${p_dest_ssh_args[@]}" + + # Get the version of the daemon + # 1: save the version to this variable + # 2: variable that contains the baksnapperd command + function get-daemon-version { + declare -n version=$1 + if ! version=$(${!2} version) + then + version=1 + fi + if [[ "$version" -gt 3 ]] + then + error "$2 is too new, need to use version 1-3" + fi + } + get-daemon-version sender_version sender + get-daemon-version receiver_version receiver else error "You need to specify the SOURCE and DEST or the config name to backup!" fi From e5c1ff268a580e2a2d6e1702d91f6c008d068a4c Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 14 Sep 2025 17:01:43 -0700 Subject: [PATCH 11/27] Add --src-dest flag to test runner [#10] To be able to test when given SOURCE and DEST rather than --config. --- tests/runner.scm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/runner.scm b/tests/runner.scm index eb143e2..56a1c25 100755 --- a/tests/runner.scm +++ b/tests/runner.scm @@ -229,6 +229,7 @@ If EXPECTED-LATEST is #f skip any check and simply return true." (receiver (single-char #\r) (value #t) (predicate ,check-snapshot-input)) (expected (single-char #\e) (value #t) (predicate ,check-snapshot-input)) (expected-latest (single-char #\L) (value #t)) + (src-dest (value #f)) (type (single-char #\t) (value #t)) (help (single-char #\h) (value #f)))) (options (getopt-long args option-spec)) @@ -253,6 +254,7 @@ Options: -e, --expected E0[:EM0][,E1[:EM1],…] Snapshots expected after running baksnapper. -L, --expected-latest E The expected `latest` snapshot after running baksnapper. -t, --type TYPE Snapshot type, default is snapper. + --src-dest Give baksnapper source and dest locations. Where SN, RN and EN are name of snapshots at the respective location/stage. RMN and EMN are metadata associated with a snapshot @@ -336,7 +338,10 @@ Fredrik \"PlaTFooT\" Salomonsson (and-let* ((latest (option-ref options 'latest #f))) (symlinkat receiver-dir latest "latest")) ;; Run command - (let ((command-with-path (append command (list receiver-root-dir)))) + (let ((command-with-path + (append command (if (option-ref options 'src-dest #f) + (list sender-dir receiver-dir) + (list receiver-root-dir))))) (format #t "Running command: ~a…~%" command-with-path) (setenv "BAKSNAPPER_TEST_RUNNER_SENDER_ROOT" test-dir) (let ((pipe (apply open-pipe* OPEN_READ command-with-path))) From a361fb75337267f61570fae8c7cfd4f4c565398c Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 14 Sep 2025 17:02:35 -0700 Subject: [PATCH 12/27] Add test default-src-dest [#10] --- meson.build | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/meson.build b/meson.build index eafba88..8f81853 100644 --- a/meson.build +++ b/meson.build @@ -97,6 +97,24 @@ test( ], ) +test( + 'default-src-dest', + runner, + env: test_env, + args: [ + '--sender', '1,2,3', + '--expected', '3', + '--src-dest', + '--', + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + ], + depends: [ + baksnapper, + baksnapperd, + ], +) + test( 'latest', runner, From a7c6477a7288819107eae971eba8be3541a35229 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 14 Sep 2025 19:54:42 -0700 Subject: [PATCH 13/27] Wrap tests in double loop to test --config, src-dest and configfile [#10] Reducing a lot of the boilerplate in the testing code. --- meson.build | 510 ++++++++++++++++++++-------------------------------- 1 file changed, 196 insertions(+), 314 deletions(-) diff --git a/meson.build b/meson.build index 8f81853..2f796cf 100644 --- a/meson.build +++ b/meson.build @@ -79,318 +79,200 @@ test_env = environment() test_env.prepend('PATH', meson.current_source_dir() / 'tests/bin') runner = find_program('./tests/runner.scm') -test( - 'default', - runner, - env: test_env, - args: [ - '--sender', '1,2,3', - '--expected', '3', - '--', - baksnapper[0].full_path(), - '--config', 'root', - '--daemon', baksnapperd[0].full_path(), - ], - depends: [ - baksnapper, - baksnapperd, - ], -) - -test( - 'default-src-dest', - runner, - env: test_env, - args: [ - '--sender', '1,2,3', - '--expected', '3', - '--src-dest', - '--', - baksnapper[0].full_path(), - '--daemon', baksnapperd[0].full_path(), - ], - depends: [ - baksnapper, - baksnapperd, - ], -) - -test( - 'latest', - runner, - env: test_env, - args: [ - '--sender', '1,2,3', - '--expected', '3', - '--expected-latest', '3', - '--', - baksnapper[0].full_path(), - '--config', 'root', - '--daemon', baksnapperd[0].full_path(), - '--link', - ], - depends: [ - baksnapper, - baksnapperd, - ], -) - -latest_test_config = custom_target( - input: configure_file( - output: 'latest.bsconf.meson', - configuration: { - 'CONFIG': 'root', - 'DAEMON': baksnapperd[0].full_path(), - 'PRUNE': 'NO', - 'ALL': 'NO', - 'VERBOSE': 'NO', - 'LINK': 'YES', - } - ), - output: 'latest.bsconf', - command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], - capture: true -) - -test( - 'latest-configfile', - runner, - env: test_env, - args: [ - '--sender', '1,2,3', - '--expected', '3', - '--expected-latest', '3', - '--', - baksnapper[0].full_path(), - '--configfile', latest_test_config.full_path(), - ], - depends: [ - baksnapper, - baksnapperd, - latest_test_config, - ], -) - -test( - 'backup-2', - runner, - env: test_env, - args: [ - '--sender', '1,2,3', - '--expected', '2', - '--', - baksnapper[0].full_path(), - '--config', 'root', - '--daemon', baksnapperd[0].full_path(), - '--snapshot', '2', - ], - depends: [ - baksnapper, - baksnapperd, - ], -) - -test( - 'prune', - runner, - env: test_env, - args: [ - '--sender', '1,2,3,6,7,8', - '--receiver', '1,2,3,4,5,6', - '--expected', '1,2,3,6,8:p=6', - '--', - baksnapper[0].full_path(), - '--config', 'root', - '--daemon', baksnapperd[0].full_path(), - '--prune', - ], - depends: [ - baksnapper, - baksnapperd, - ], -) - -prune_test_config = custom_target( - input: configure_file( - output: 'prune.bsconf.meson', - configuration: { - 'CONFIG': 'home', - 'DAEMON': baksnapperd[0].full_path(), - 'PRUNE': 'YES', - 'ALL': 'NO', - 'VERBOSE': 'NO', - } - ), - output: 'prune.bsconf', - command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], - capture: true -) - -test( - 'prune-configfile', - runner, - env: test_env, - args: [ - '--sender', '1,2,3,6,7,8', - '--receiver', '1,2,3,4,5,6', - '--expected', '1,2,3,6,8:p=6', - '--', - baksnapper[0].full_path(), - '--configfile', prune_test_config.full_path(), - '--config', 'root', - ], - depends: [ - baksnapper, - baksnapperd, - prune_test_config, - ], -) - -test( - 'delete-1', - runner, - env: test_env, - args: [ - '--sender', '1,2,3', - '--receiver', '1,2,3', - '--expected', '2,3', - '--', - baksnapper[0].full_path(), - '--config', 'root', - '--daemon', baksnapperd[0].full_path(), - '--delete', '1', - ], - depends: [ - baksnapper, - baksnapperd, - ], -) - -test( - 'delete-1,3', - runner, - env: test_env, - args: [ - '--sender', '1,2,3', - '--receiver', '1,2,3', - '--expected', '2', - '--', - baksnapper[0].full_path(), - '--config', 'root', - '--daemon', baksnapperd[0].full_path(), - '--delete', '1,3', - ], - depends: [ - baksnapper, - baksnapperd, - ], -) - -delete_1_test_config = custom_target( - input: configure_file( - output: 'delete-1.bsconf.meson', - configuration: { - 'CONFIG': 'root', - 'DAEMON': baksnapperd[0].full_path(), - 'PRUNE': 'NO', - 'ALL': 'NO', - 'VERBOSE': 'NO', - } - ), - output: 'delete-1.bsconf', - command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], - capture: true -) - -test( - 'delete-1-configfile', - runner, - env: test_env, - args: [ - '--sender', '1,2,3', - '--receiver', '1,2,3', - '--expected', '2,3', - '--', - baksnapper[0].full_path(), - '--configfile', delete_1_test_config.full_path(), - '--delete', '1', - ], - depends: [ - baksnapper, - baksnapperd, - delete_1_test_config, - ], -) -# https://github.com/plattfot/baksnapper/issues/27 -test( - 'backup-all-with-right-parent', - runner, - env: test_env, - args: [ - '--sender', '64,65,76,83,91,92,93', - '--receiver', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83', - '--expected', '64,65:p=64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83,93:p=92', - '--', - baksnapper[0].full_path(), - '--config', 'root', - '--daemon', baksnapperd[0].full_path(), - '--all', - ], - depends: [ - baksnapper, - baksnapperd, - ], -) - -all_test_config = custom_target( - input: configure_file( - output: 'all.bsconf.meson', - configuration: { - 'CONFIG': 'root', - 'DAEMON': baksnapperd[0].full_path(), - 'ALL': 'YES', - 'VERBOSE': 'NO', +test_configuration = { + 'default': { + 'CONFIG': 'root', + 'DAEMON': baksnapperd[0].full_path(), + }, + 'latest': { + 'CONFIG': 'root', + 'DAEMON': baksnapperd[0].full_path(), + 'PRUNE': 'NO', + 'ALL': 'NO', + 'VERBOSE': 'NO', + 'LINK': 'YES', + }, + 'prune': { + 'CONFIG': 'home', + 'DAEMON': baksnapperd[0].full_path(), + 'PRUNE': 'YES', + 'ALL': 'NO', + 'VERBOSE': 'NO', + }, + 'all': { + 'CONFIG': 'root', + 'DAEMON': baksnapperd[0].full_path(), + 'ALL': 'YES', + 'VERBOSE': 'NO', + }, +} +foreach conf: [ + { + 'suffix': '-config', + 'runner-args': [], + 'baksnapper-args': ['--config', 'root'], + 'configfile': false, + }, + { + 'suffix': '-src-dest', + 'runner-args': ['--src-dest'], + 'baksnapper-args': [], + 'configfile': false, + }, + { + 'suffix': '-configfile', + 'runner-args': [], + 'baksnapper-args': [], + 'configfile': true, + }, +] + foreach test_entry: [ + { + 'name': 'default', + 'runner-args': [ + '--sender', '1,2,3', + '--expected', '3', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + ], + 'configfile': 'default', + 'configfile-baksnapper-args': [], + }, + { + 'name': 'latest', + 'runner-args': [ + '--sender', '1,2,3', + '--expected', '3', + '--expected-latest', '3', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--link', + ], + 'configfile': 'latest', + 'configfile-baksnapper-args': [], + }, + { + 'name': 'prune', + 'runner-args': [ + '--sender', '1,2,3,6,7,8', + '--receiver', '1,2,3,4,5,6', + '--expected', '1,2,3,6,8:p=6', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--prune', + ], + 'configfile': 'prune', + 'configfile-baksnapper-args': ['--config', 'root'], + }, + { + 'name': 'backup-2', + 'runner-args': [ + '--sender', '1,2,3', + '--expected', '2', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--snapshot', '2', + ], + 'configfile': 'default', + 'configfile-baksnapper-args': [], + }, + { + 'name': 'delete-1', + 'runner-args': [ + '--sender', '1,2,3', + '--receiver', '1,2,3', + '--expected', '2,3', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--delete', '1', + ], + 'configfile': 'default', + 'configfile-baksnapper-args': [], + }, + { + 'name': 'delete-1,3', + 'runner-args': [ + '--sender', '1,2,3', + '--receiver', '1,2,3', + '--expected', '2', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--delete', '1,3', + ], + 'configfile': 'default', + 'configfile-baksnapper-args': [], + }, + { + 'name': 'backup-with-right-parent', + 'runner-args': [ + '--sender', '64,65,76,83,91,92,93', + '--receiver', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83', + '--expected', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83,93:p=92', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + ], + 'configfile': 'default', + 'configfile-baksnapper-args': [], + }, + # https://github.com/plattfot/baksnapper/issues/27 + { + 'name': 'backup-all-with-right-parent', + 'runner-args': [ + '--sender', '64,65,76,83,91,92,93', + '--receiver', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83', + '--expected', '64,65:p=64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83,93:p=92', + ], + 'baksnapper-args': [ + baksnapper[0].full_path(), + '--daemon', baksnapperd[0].full_path(), + '--all', + ], + 'configfile': 'all', + 'configfile-baksnapper-args': [], } - ), - output: 'all.bsconf', - command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], - capture: true -) - -test( - 'backup-all-configfile', - runner, - env: test_env, - args: [ - '--sender', '64,65,76,83,91,92,93', - '--receiver', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83', - '--expected', '64,65:p=64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83,93:p=92', - '--', - baksnapper[0].full_path(), - '--configfile', all_test_config.full_path(), - ], - depends: [ - baksnapper, - baksnapperd, - all_test_config, - ], -) - -test( - 'backup-with-right-parent', - runner, - env: test_env, - args: [ - '--sender', '64,65,76,83,91,92,93', - '--receiver', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83', - '--expected', '64,66:p=64,76:p=66,83:p=76,91:p=83,92:p=83,93:p=92', - '--', - baksnapper[0].full_path(), - '--config', 'root', - '--daemon', baksnapperd[0].full_path(), - ], - depends: [ - baksnapper, - baksnapperd, - ], -) + ] + baksnapper_args = [] + test_depends = [] + if conf['configfile'] + test_config = custom_target( + input: configure_file( + output: test_entry['name'] + '.bsconf.meson', + configuration: test_configuration[test_entry['configfile']] + ), + output: test_entry['name'] + '.bsconf', + command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], + capture: true, + ) + baksnapper_args = [ + '--configfile', test_config.full_path() + ] + test_entry['configfile-baksnapper-args'] + test_depends = [test_config] + endif + test( + test_entry['name']+conf['suffix'], + runner, + env: test_env, + args: test_entry['runner-args'] + conf['runner-args'] + + ['--'] + + test_entry['baksnapper-args'] + conf['baksnapper-args'] + baksnapper_args, + depends: [ + baksnapper, + baksnapperd, + ] + test_depends, + ) + endforeach +endforeach From afcba0dbfbfb9124a947c34a6ed9f3d29f0d27e4 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 21 Sep 2025 20:26:13 -0700 Subject: [PATCH 14/27] Add tests/bin/config-to-bsconf script To convert meson's configure_file output to something baksnapper can parse. --- tests/bin/config-to-bsconf | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100755 tests/bin/config-to-bsconf diff --git a/tests/bin/config-to-bsconf b/tests/bin/config-to-bsconf new file mode 100755 index 0000000..05d8982 --- /dev/null +++ b/tests/bin/config-to-bsconf @@ -0,0 +1,8 @@ +#! /usr/bin/env bash + +sed -E \ + -e 's/#define (.*?) (.*)/\1 = \2/' \ + -e 's| \*/|#|' \ + -e 's|/\*|#|' \ + -e 's| \*|#|' \ + "$1" \ No newline at end of file From 2e83aaacff494223d238250a53b3929523f9e35d Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 21 Sep 2025 20:27:05 -0700 Subject: [PATCH 15/27] Add not-enough-inputs tests{-src-dest} [#10] To make sure it fails when not given enough position arguments. --- meson.build | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/meson.build b/meson.build index 2f796cf..648ec01 100644 --- a/meson.build +++ b/meson.build @@ -276,3 +276,24 @@ foreach conf: [ ) endforeach endforeach + +test( + 'not-enough-inputs', + baksnapper[0], + env: test_env, + depends: [ + baksnapper, + ], + should_fail: true, +) + +test( + 'not-enough-inputs-src-dest', + baksnapper[0], + args: ['.'], + env: test_env, + depends: [ + baksnapper, + ], + should_fail: true, +) From e4ab9cb8adcefd84e0c798491a97e6e27ac79383 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 21 Sep 2025 20:28:50 -0700 Subject: [PATCH 16/27] Add support for SOURCE/DEST in configfile [#10] --- baksnapper.sh | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/baksnapper.sh b/baksnapper.sh index aaee784..8aaa95d 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -123,6 +123,8 @@ This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. EOF +shopt -s extglob + function cleanup { exec 4>&- rm -rf "${p_temp_dir}" @@ -198,6 +200,12 @@ function read-config { get-value-if-not-set p_dest "$line" warning "PATH is deprecated, use SOURCE/DEST." ;; + (SOURCE*([[:blank:]])=*) + get-value-if-not-set p_src "$line" + ;; + (DEST*([[:blank:]])=*) + get-value-if-not-set p_dest "$line" + ;; DAEMON*=*) get-value-if-not-set p_baksnapperd "$line" ;; @@ -344,6 +352,12 @@ fi p_baksnapperd=${p_baksnapperd=baksnapperd} p_all=${p_all=0} +if [[ $# -ge 2 ]] +then + p_src=$1 + p_dest=$2 +fi + #### Deprecated config way of setup ############################################ if [[ -n $p_config ]] then @@ -418,7 +432,7 @@ then src_root=${subvolume:?}/.snapshots dest_root="$dest/$p_config" -elif [[ $# -ge 2 ]] +elif [[ -n "$p_src" && -n "$p_dest" ]] then # Setup the endpoint's directory and transfer command # 1 [out]: will store the root of the endpoint @@ -450,8 +464,8 @@ then fi } - setup-endpoint src_root sender "$1" "${p_src_ssh_args[@]}" - setup-endpoint dest_root receiver "$2" "${p_dest_ssh_args[@]}" + setup-endpoint src_root sender "$p_src" "${p_src_ssh_args[@]}" + setup-endpoint dest_root receiver "$p_dest" "${p_dest_ssh_args[@]}" # Get the version of the daemon # 1: save the version to this variable From 0521450f37cf1e67c57eaf706520b85b0e97d01d Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 21 Sep 2025 20:29:45 -0700 Subject: [PATCH 17/27] Add support for --{src,dest}-private-key [#10] To set the key used to connect to the source/destination remote location. --- baksnapper.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/baksnapper.sh b/baksnapper.sh index 8aaa95d..9ee5c45 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -32,6 +32,12 @@ Options: --private-key KEY Specify the private KEY file to use when connecting to a remote backup location. +--src-private-key KEY Specify the private KEY file to use when + connecting to the remote source location. + +--dest-private-key KEY Specify the private KEY file to use when + connecting to the remote destination location. + -p, --prune Prune the backups by deleting snapshots that isn't in the source directory. @@ -244,6 +250,8 @@ if ! _args=$(getopt --name "baksnapper" \ --long delete: \ --long daemon: \ --long private-key: \ + --long src-private-key: \ + --long dest-private-key: \ --long snapshot: \ --long type: \ --long all \ @@ -311,6 +319,14 @@ case $key in p_src_ssh_args=("-i" "$2") shift 2 ;; + --src-private-key) + p_src_ssh_args=("-i" "$2") + shift 2 + ;; + --dest-private-key) + p_dest_ssh_args=("-i" "$2") + shift 2 + ;; -p|--prune) p_prune=1 shift From 999069ce9c195594b2642afe4a7d9b2d5bbd5c68 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 21 Sep 2025 20:31:01 -0700 Subject: [PATCH 18/27] Add {SOURCE,DEST}_PRIVATE_KEY configfile option [#10] To set the SOURCE/DEST private key. --- baksnapper.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/baksnapper.sh b/baksnapper.sh index 9ee5c45..11c35e5 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -231,9 +231,19 @@ function read-config { get-value-if-not-set p_type "$line" warning "TYPE is deprecated, use SOURCE/DEST." ;; - PRIVATE_KEY*=*) + (PRIVATE_KEY*([[:blank:]])=*) get-value value "$line" p_ssh_args=${p_ssh_args-" -i $value"} + p_src_ssh_args=${p_src_ssh_args-" -i $value"} + p_dest_ssh_args=${p_dest_ssh_args-" -i $value"} + ;; + (SOURCE_PRIVATE_KEY*([[:blank:]])=*) + get-value value "$line" + p_src_ssh_args=${p_src_ssh_args-" -i $value"} + ;; + (DEST_PRIVATE_KEY*([[:blank:]])=*) + get-value value "$line" + p_dest_ssh_args=${p_dest_ssh_args-" -i $value"} ;; *) ;; From 9906f5e2421776904ef001ff5bd0395d5976ae35 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 21 Sep 2025 20:31:36 -0700 Subject: [PATCH 19/27] Add tests/bin/ssh [#10] Mock the ssh connection to be able to test that it works. --- tests/bin/ssh | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100755 tests/bin/ssh diff --git a/tests/bin/ssh b/tests/bin/ssh new file mode 100755 index 0000000..e72188c --- /dev/null +++ b/tests/bin/ssh @@ -0,0 +1,101 @@ +#! /usr/bin/env bash + +# Mock of the ssh binary for testing + +# SPDX-FileCopyrightText: 2025 Fredrik Salomonsson +# +# SPDX-License-Identifier: GPL-3.0-or-later + +read -rd '' help <&2 + exit 1 +fi + +eval set -- "$_args" +# Parse options +while [[ $# -gt 0 ]] +do + key=$1 + case $key in + -q) + test_connection=1 + shift 1 + ;; + -i) + ssh_key=$2 + shift 2 + ;; + -h|--help) + echo -e "$help" + exit 0 + ;; + --) + shift + break + ;; + esac +done + +if [[ -n "$test_connection" ]] +then + exit "${MOCK_SSH_CONNECTION-1}" +fi + +if [[ $# -lt 1 ]] +then + echo "no address given" 1>&2 + exit 1 +fi + +if [[ -n "$ssh_key" ]] +then + mapfile -td ':' key_pairs <<< "$MOCK_SSH_KEY" + declare -A keys + for key_value in "${key_pairs[@]}" + do + mapfile -td '=' pair <<< "$key_value" + keys[${pair[0]}]=${pair[1]} + done + if [[ -z "${keys[$ssh_key]}" ]] + then + echo "ssh key '$ssh_key' does not exit" 1>&2 + exit 1 + fi +fi + +if [[ $# -lt 2 ]] +then + echo "no command given" 1>&2 + exit 1 +fi +shift 1 + +if [[ -z "$MOCK_SSH_BAKSNAPPERD" ]] +then + echo "MOCK_SSH_BAKSNAPPERD not set" 1>&2 + exit 1 +fi + +$MOCK_SSH_BAKSNAPPERD "$@" From 8060561f710b7dc38fbe30d40cbf060df81a9d8f Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 21 Sep 2025 21:09:13 -0700 Subject: [PATCH 20/27] Add support to test ssh connection to runner [#10] --- tests/runner.scm | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/runner.scm b/tests/runner.scm index 56a1c25..2b842da 100755 --- a/tests/runner.scm +++ b/tests/runner.scm @@ -230,6 +230,8 @@ If EXPECTED-LATEST is #f skip any check and simply return true." (expected (single-char #\e) (value #t) (predicate ,check-snapshot-input)) (expected-latest (single-char #\L) (value #t)) (src-dest (value #f)) + (src-ssh (value #t)) + (dest-ssh (value #t)) (type (single-char #\t) (value #t)) (help (single-char #\h) (value #f)))) (options (getopt-long args option-spec)) @@ -255,6 +257,8 @@ Options: -L, --expected-latest E The expected `latest` snapshot after running baksnapper. -t, --type TYPE Snapshot type, default is snapper. --src-dest Give baksnapper source and dest locations. + --src-ssh ADDRESS Prepend ADDRESS: to source location. + --dest-ssh ADDRESS Prepend ADDRESS: to dest location. Where SN, RN and EN are name of snapshots at the respective location/stage. RMN and EMN are metadata associated with a snapshot @@ -338,10 +342,17 @@ Fredrik \"PlaTFooT\" Salomonsson (and-let* ((latest (option-ref options 'latest #f))) (symlinkat receiver-dir latest "latest")) ;; Run command - (let ((command-with-path - (append command (if (option-ref options 'src-dest #f) - (list sender-dir receiver-dir) - (list receiver-root-dir))))) + (let* ((append-ssh (lambda (address dir) + (if address + (string-append address ":" dir) + dir))) + (command-with-path + (append command + (if (option-ref options 'src-dest #f) + (list + (append-ssh (option-ref options 'src-ssh #f) sender-dir) + (append-ssh (option-ref options 'dest-ssh #f) receiver-dir)) + (list (append-ssh (option-ref options 'dest-ssh #f) receiver-root-dir)))))) (format #t "Running command: ~a…~%" command-with-path) (setenv "BAKSNAPPER_TEST_RUNNER_SENDER_ROOT" test-dir) (let ((pipe (apply open-pipe* OPEN_READ command-with-path))) From 613bfc3cdf08e516f5a66ee222c09945d632bdb3 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 21 Sep 2025 21:09:59 -0700 Subject: [PATCH 21/27] Add tests for ssh connection [#10] --- meson.build | 104 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 30 deletions(-) diff --git a/meson.build b/meson.build index 648ec01..c0edfa5 100644 --- a/meson.build +++ b/meson.build @@ -77,6 +77,10 @@ test( test_env = environment() test_env.prepend('PATH', meson.current_source_dir() / 'tests/bin') +test_env.set('MOCK_SSH_CONNECTION', '0') +test_env.set('MOCK_SSH_KEY', 'foo.key=foo.example.com:bar.key=bar.example.com') +test_env.set('MOCK_SSH_BAKSNAPPERD', baksnapperd[0].full_path()) + runner = find_program('./tests/runner.scm') test_configuration = { @@ -245,38 +249,78 @@ foreach conf: [ 'configfile-baksnapper-args': [], } ] - baksnapper_args = [] - test_depends = [] - if conf['configfile'] - test_config = custom_target( - input: configure_file( - output: test_entry['name'] + '.bsconf.meson', - configuration: test_configuration[test_entry['configfile']] - ), - output: test_entry['name'] + '.bsconf', - command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], - capture: true, - ) - baksnapper_args = [ - '--configfile', test_config.full_path() - ] + test_entry['configfile-baksnapper-args'] - test_depends = [test_config] - endif - test( - test_entry['name']+conf['suffix'], - runner, - env: test_env, - args: test_entry['runner-args'] + conf['runner-args'] + - ['--'] + - test_entry['baksnapper-args'] + conf['baksnapper-args'] + baksnapper_args, - depends: [ - baksnapper, - baksnapperd, - ] + test_depends, - ) + foreach ssh: [ + { + 'suffix': '', + 'args': [], + }, + { + 'suffix': '-ssh-src', + 'args': ['--src-ssh', 'foo.example.com'], + }, + { + 'suffix': '-ssh-dest', + 'args': ['--dest-ssh', 'bar.example.com'], + }, + { + 'suffix': '-ssh-both', + 'args': ['--src-ssh', 'foo.example.com', + '--dest-ssh', 'bar.example.com'], + }, + ] + foreach ssh_key: [ + { + 'suffix': '', + 'args': [], + }, + { + 'suffix': '-key-src', + 'args': ['--src-private-key', 'foo.key'], + }, + { + 'suffix': '-key-dest', + 'args': ['--dest-private-key', 'bar.key'], + }, + { + 'suffix': '-key-both', + 'args': ['--src-private-key', 'foo.key', + '--dest-private-key', 'bar.key'], + }, + ] + baksnapper_args = [] + test_depends = [] + if conf['configfile'] + test_config = custom_target( + input: configure_file( + output: test_entry['name'] + ssh['suffix'] + ssh_key['suffix'] + '.bsconf.meson', + configuration: test_configuration[test_entry['configfile']] + ), + output: test_entry['name'] + ssh['suffix'] + ssh_key['suffix'] + '.bsconf', + command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], + capture: true, + ) + baksnapper_args = [ + '--configfile', test_config.full_path() + ] + test_entry['configfile-baksnapper-args'] + test_depends = [test_config] + endif + test( + test_entry['name']+conf['suffix']+ssh['suffix']+ssh_key['suffix'], + runner, + env: test_env, + args: test_entry['runner-args'] + conf['runner-args'] + ssh['args'] + + ['--'] + + test_entry['baksnapper-args'] + conf['baksnapper-args'] + + ssh_key['args'] + baksnapper_args, + depends: [ + baksnapper, + baksnapperd, + ] + test_depends, + ) + endforeach + endforeach endforeach endforeach - test( 'not-enough-inputs', baksnapper[0], From 6d913632f84d6008d092b809f34134a8d46b90f6 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 28 Sep 2025 19:56:19 -0700 Subject: [PATCH 22/27] Add --{source,dest} option [#10] To be able to override source or dest from the config file. --- baksnapper.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/baksnapper.sh b/baksnapper.sh index 11c35e5..447c96e 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -10,7 +10,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later read -rd '' help < Date: Sun, 28 Sep 2025 19:57:15 -0700 Subject: [PATCH 23/27] Fix bug with using PATH from configfile --- baksnapper.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/baksnapper.sh b/baksnapper.sh index 447c96e..a697687 100755 --- a/baksnapper.sh +++ b/baksnapper.sh @@ -403,9 +403,13 @@ fi #### Deprecated config way of setup ############################################ if [[ -n $p_config ]] then - [[ -z "$1" ]] && error "No path specified!" + if [[ $# -ge 1 ]] + then + p_dest=$1 + fi + + [[ -z $p_dest ]] && error "No path specified!" - p_dest=${p_dest-$1} regex='(.*?):(.*)' if [[ $p_dest =~ $regex ]] then From 77550b39417c5019da9040c13b65c65180b259a9 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 28 Sep 2025 19:58:30 -0700 Subject: [PATCH 24/27] Add support in runner to test SOURCE, DEST and PATH from configfile [#10] --- tests/runner.scm | 81 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/tests/runner.scm b/tests/runner.scm index 2b842da..f42e4cc 100755 --- a/tests/runner.scm +++ b/tests/runner.scm @@ -221,6 +221,42 @@ If EXPECTED-LATEST is #f skip any check and simply return true." #f) (#t #t)))) +(define (configure-config-file configfile options test-dir sender-dir receiver-dir) + "Copy CONFIGFILE and append source and dest options. + +Where CONFIGFILE is either a path to a config file or #f if not set. + +OPTIONS are cli options where it will look if src-config, dest-config +or path-config are set. + +TEST-DIR where it should save the modified CONFIGFILE + +SENDER-DIR is the path to the sender directory. + +RECEIVER-DIR is the path to the receiver directory. + +Return the path to the modified CONFIGFILE if it is defined otherwise #f." + (if configfile + (let* ((test-config-file (path-join test-dir (basename configfile))) + (port (open-file test-config-file "w"))) + (call-with-input-file configfile + (lambda (proc) + (format port "~a" (get-string-all proc)))) + (when (option-ref options 'src-config #f) + (format port "SOURCE=~a~%" sender-dir)) + (when (option-ref options 'dest-config #f) + (format port "DEST=~a~%" receiver-dir)) + (when (option-ref options 'path-config #f) + (format port "PATH=~a~%" (dirname receiver-dir))) + (unless (option-ref options 'src-dest #f) + (format port "CONFIG=~a~%" (option-ref options 'config "root"))) + (close-port port) + (call-with-input-file test-config-file + (lambda (proc) + (format #t "-----config----~%~a~%--------------~%" (get-string-all proc)))) + test-config-file) + #f)) + (define (main args) (let* ((option-spec `((config (single-char #\c) (value #t)) @@ -232,6 +268,10 @@ If EXPECTED-LATEST is #f skip any check and simply return true." (src-dest (value #f)) (src-ssh (value #t)) (dest-ssh (value #t)) + (configfile (value #t)) + (src-config (value #f)) + (dest-config (value #f)) + (path-config (value #f)) (type (single-char #\t) (value #t)) (help (single-char #\h) (value #f)))) (options (getopt-long args option-spec)) @@ -259,6 +299,10 @@ Options: --src-dest Give baksnapper source and dest locations. --src-ssh ADDRESS Prepend ADDRESS: to source location. --dest-ssh ADDRESS Prepend ADDRESS: to dest location. + --configfile PATH PATH to config file to pass on to COMMAND. + --src-config Append `SOURCE=` to config file. + --dest-config Append `DEST=` to config file. + --path-config Append `PATH=` to config file. Where SN, RN and EN are name of snapshots at the respective location/stage. RMN and EMN are metadata associated with a snapshot @@ -298,7 +342,8 @@ Fredrik \"PlaTFooT\" Salomonsson "baksnapper-test-XXXXXX"))) (sender-dir (path-join test-dir config ".snapshots")) (receiver-root-dir (path-join test-dir "receiver")) - (receiver-dir (path-join receiver-root-dir config))) + (receiver-dir (path-join receiver-root-dir config)) + (configfile (option-ref options 'configfile #f))) ;; Needed for nftw to be able to read the directory (chmod test-dir #o744) (format #t "Test ~a~%" test-dir) @@ -346,13 +391,35 @@ Fredrik \"PlaTFooT\" Salomonsson (if address (string-append address ":" dir) dir))) + (if-set (lambda (check value) + (if check value '()))) + (prefix-if-set (lambda (check prefix value) + (if check (list prefix value) (list value)))) + (test-config-file (configure-config-file + configfile + options + test-dir + sender-dir + receiver-dir)) (command-with-path - (append command - (if (option-ref options 'src-dest #f) - (list - (append-ssh (option-ref options 'src-ssh #f) sender-dir) - (append-ssh (option-ref options 'dest-ssh #f) receiver-dir)) - (list (append-ssh (option-ref options 'dest-ssh #f) receiver-root-dir)))))) + (append + command + (if-set test-config-file (list "--configfile" test-config-file)) + (if (option-ref options 'src-dest #f) + (append + (if-set (not (option-ref options 'src-config #f)) + (prefix-if-set + (option-ref options 'dest-config #f) + "--source" + (append-ssh (option-ref options 'src-ssh #f) sender-dir))) + (if-set (not (option-ref options 'dest-config #f)) + (prefix-if-set + (option-ref options 'src-config #f) + "--dest" + (append-ssh (option-ref options 'dest-ssh #f) receiver-dir)))) + (if-set + (not (option-ref options 'path-config #f)) + (list (append-ssh (option-ref options 'dest-ssh #f) receiver-root-dir))))))) (format #t "Running command: ~a…~%" command-with-path) (setenv "BAKSNAPPER_TEST_RUNNER_SENDER_ROOT" test-dir) (let ((pipe (apply open-pipe* OPEN_READ command-with-path))) From f19c382eb98b9e33c548ab5e9e6bbd706711d17a Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 28 Sep 2025 19:59:25 -0700 Subject: [PATCH 25/27] Add tests for using SOURCE, DEST and PATH from configfile [#10] --- meson.build | 53 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/meson.build b/meson.build index c0edfa5..a026e94 100644 --- a/meson.build +++ b/meson.build @@ -85,11 +85,9 @@ runner = find_program('./tests/runner.scm') test_configuration = { 'default': { - 'CONFIG': 'root', 'DAEMON': baksnapperd[0].full_path(), }, 'latest': { - 'CONFIG': 'root', 'DAEMON': baksnapperd[0].full_path(), 'PRUNE': 'NO', 'ALL': 'NO', @@ -97,14 +95,12 @@ test_configuration = { 'LINK': 'YES', }, 'prune': { - 'CONFIG': 'home', 'DAEMON': baksnapperd[0].full_path(), 'PRUNE': 'YES', 'ALL': 'NO', 'VERBOSE': 'NO', }, 'all': { - 'CONFIG': 'root', 'DAEMON': baksnapperd[0].full_path(), 'ALL': 'YES', 'VERBOSE': 'NO', @@ -129,6 +125,36 @@ foreach conf: [ 'baksnapper-args': [], 'configfile': true, }, + { + 'suffix': '-configfile-path', + 'runner-args': ['--path-config'], + 'baksnapper-args': [], + 'configfile': true, + }, + { + 'suffix': '-configfile-src-dest', + 'runner-args': ['--src-dest'], + 'baksnapper-args': [], + 'configfile': true, + }, + { + 'suffix': '-configfile-src', + 'runner-args': ['--src-dest', '--src-config'], + 'baksnapper-args': [], + 'configfile': true, + }, + { + 'suffix': '-configfile-dest', + 'runner-args': ['--src-dest', '--dest-config'], + 'baksnapper-args': [], + 'configfile': true, + }, + { + 'suffix': '-configfile-both', + 'runner-args': ['--src-dest', '--src-config', '--dest-config'], + 'baksnapper-args': [], + 'configfile': true, + }, ] foreach test_entry: [ { @@ -172,7 +198,7 @@ foreach conf: [ '--prune', ], 'configfile': 'prune', - 'configfile-baksnapper-args': ['--config', 'root'], + 'configfile-baksnapper-args': [], }, { 'name': 'backup-2', @@ -287,28 +313,31 @@ foreach conf: [ '--dest-private-key', 'bar.key'], }, ] + runner_args = [] baksnapper_args = [] test_depends = [] if conf['configfile'] test_config = custom_target( input: configure_file( - output: test_entry['name'] + ssh['suffix'] + ssh_key['suffix'] + '.bsconf.meson', + output: test_entry['name'] + ssh['suffix'] + ssh_key['suffix'] + + conf['suffix'] + '.bsconf.meson', configuration: test_configuration[test_entry['configfile']] ), - output: test_entry['name'] + ssh['suffix'] + ssh_key['suffix'] + '.bsconf', + output: test_entry['name'] + ssh['suffix'] + ssh_key['suffix'] + + conf['suffix'] + '.bsconf', command: [find_program('tests/bin/config-to-bsconf'), '@INPUT@'], capture: true, ) - baksnapper_args = [ - '--configfile', test_config.full_path() - ] + test_entry['configfile-baksnapper-args'] - test_depends = [test_config] + runner_args += ['--configfile', test_config.full_path()] + baksnapper_args += test_entry['configfile-baksnapper-args'] + test_depends += [test_config] endif test( test_entry['name']+conf['suffix']+ssh['suffix']+ssh_key['suffix'], runner, env: test_env, - args: test_entry['runner-args'] + conf['runner-args'] + ssh['args'] + + args: test_entry['runner-args'] + conf['runner-args'] + + ssh['args'] + runner_args + ['--'] + test_entry['baksnapper-args'] + conf['baksnapper-args'] + ssh_key['args'] + baksnapper_args, From 8dd1bd3bcbe82a460ab089773516914a65f977dc Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Sun, 28 Sep 2025 20:05:26 -0700 Subject: [PATCH 26/27] Add changelog entries for the change in interface --- ...20250928T200407--deprecate-config-option__deprecated.md | 2 ++ .../20250928T200445--add-source-dest-feature__added.md | 2 ++ .../20250928T202816--add-source-dest-options__added.md | 7 +++++++ .../20250928T203013--change-private-key__changed.md | 2 ++ 4 files changed, 13 insertions(+) create mode 100644 CHANGELOG.d/20250928T200407--deprecate-config-option__deprecated.md create mode 100644 CHANGELOG.d/20250928T200445--add-source-dest-feature__added.md create mode 100644 CHANGELOG.d/20250928T202816--add-source-dest-options__added.md create mode 100644 CHANGELOG.d/20250928T203013--change-private-key__changed.md diff --git a/CHANGELOG.d/20250928T200407--deprecate-config-option__deprecated.md b/CHANGELOG.d/20250928T200407--deprecate-config-option__deprecated.md new file mode 100644 index 0000000..1450ca9 --- /dev/null +++ b/CHANGELOG.d/20250928T200407--deprecate-config-option__deprecated.md @@ -0,0 +1,2 @@ +### Deprecated +- `--config` and `--type` options. diff --git a/CHANGELOG.d/20250928T200445--add-source-dest-feature__added.md b/CHANGELOG.d/20250928T200445--add-source-dest-feature__added.md new file mode 100644 index 0000000..da01d1d --- /dev/null +++ b/CHANGELOG.d/20250928T200445--add-source-dest-feature__added.md @@ -0,0 +1,2 @@ +### Added +- Interface to specify `SOURCE` and `DEST` explicitly. diff --git a/CHANGELOG.d/20250928T202816--add-source-dest-options__added.md b/CHANGELOG.d/20250928T202816--add-source-dest-options__added.md new file mode 100644 index 0000000..be7b1af --- /dev/null +++ b/CHANGELOG.d/20250928T202816--add-source-dest-options__added.md @@ -0,0 +1,7 @@ +### Added +- `--source SOURCE` option, to override `SOURCE=` in configfile. +- `--dest DEST` option, to override `DEST=` in configfile. +- `SOURCE=` configfile option. +- `DEST=` configfile option. +- `--src-private-key` to set the ssh private key for `SOURCE`. +- `--dest-private-key` to set the ssh private key for `DEST`. diff --git a/CHANGELOG.d/20250928T203013--change-private-key__changed.md b/CHANGELOG.d/20250928T203013--change-private-key__changed.md new file mode 100644 index 0000000..5181f5a --- /dev/null +++ b/CHANGELOG.d/20250928T203013--change-private-key__changed.md @@ -0,0 +1,2 @@ +### Changed +- `--private-key` now also sets ssh private key for `SOURCE` and `DEST`. From 1e3e0a8e9b97c074a38482d8de5bc1b00852cd96 Mon Sep 17 00:00:00 2001 From: Fredrik Salomonsson Date: Tue, 30 Sep 2025 19:59:56 -0700 Subject: [PATCH 27/27] Update README.md with the new interface [#10] --- README.md | 111 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 92 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ab38b27..440e164 100644 --- a/README.md +++ b/README.md @@ -70,19 +70,53 @@ release. - [ ] Run as none root user. ## Usage +First set up snapper to take snapshots of one of your subvolumes, see this [page](https://wiki.archlinux.org/index.php/Snapper) on how to do it for Arch Linux. -First set up snapper to take snapshots of one of your subvolumes, see -this [page](https://wiki.archlinux.org/index.php/Snapper) on how to do -it for Arch Linux. +### Specify source and destination -For example if you want to back up you snapshots from your home -subvolumes to an external hdd mounted at /mnt/backup, run +If you want to back up your snapper snapshots for your home configuration, where the snapshots located in `/home/.snapshots`, to an external hdd mounted at `/mmt/backup`. + +```bash +$ baksnapper /home/.snapshots /mnt/backup/home +``` + +This will send the last snapshot of home to `/mnt/backup/home/'. + +To backup all snapshots run: +```bash +$ baksnapper /home/.snapshots --all /mnt/backup/home +``` + +This will mirror what is in `/home/.snapshots` to `/mnt/backup/home`. + +To remove backups that snapper has discarded add the flag -p/--prune: +```bash +$ baksnapper /home/.snapshots --all --prune /mnt/backup/home +``` + +To delete specific backups from the backup directory, run: +```bash +$ baksnapper /home/.snapshots /mnt/backup/home --delete 1,2,3,63 +``` + +This will only delete 1,2,3 and 63 if they only contain a info.xml +file and a snapshot dir. Otherwise it will issue a warning and skip +the directory. + +For help use the `-h`/`--help` flag +```bash +$ baksnapper --help +``` +### Snapper config interface (deprecated) + +For example if you want to back up your snapshots from your home +snapshots to an external hdd mounted at /mnt/backup, run ```bash $ baksnapper -c home /mnt/backup ``` -This will send the last snapshots of home to +This will send the last snapshot of home to /mnt/backup/home/ To backup all snapshots run: @@ -110,10 +144,10 @@ For help use the -h/--help flag ```bash $ baksnapper --help ``` -## Remote backup over ssh +### Remote backup over ssh In order to be able to backup to a remote location you first need to -install baksnapper on the remote machine, or atleast copy over the +install baksnapper on the remote machine, or at least copy over the baksnapperd script. Next up is to create a ssh key that baksnapper can use to access the @@ -153,25 +187,62 @@ It should print out "Connection works". If everything is working you can now use remote backup with baksnapper. It uses the same syntax as scp, -i.e.
:. For example backup to machine named foo: +i.e.
:. For example backup to machine named foo.example.com: ```bash -$ baksnapper --config home --all foo:/mnt/backup +$ baksnapper /home/.snapshots --all foo.example.com:/mnt/backup/home +``` + +And for the deprecated snapper config interface: + +```bash +$ baksnapper --config home --all foo.example.com:/mnt/backup ``` **Tip:** If your are using a nonstandard ssh port you can specify it in the .ssh/config, for example ```bash -Host foo +Host foo.example.com Port 666 ``` **Tip:** If you have multiple keys, you can specify what key -baksnapper should use either with the option +baksnapper should use either with the option: + +```bash +$ baksnapper /home/.snapshots --all foo.example.com:/mnt/backup/home --dest-private-key /root/.ssh/ +``` + +And for the deprecated snapper config interface: +```bash +$ baksnapper --config home --all foo.example.com:/mnt/backup --private-key /root/.ssh/ +``` + +Or specify that in the Config file using `DEST_PRIVATE_KEY` or `PRIVATE_KEY` for the deprecated snapper config interface. + +#### Backup from a remote machine over ssh + +You can also pull snapshots from a remote machine to a local location using + +```bash +$ baksnapper foo.example.com:/home/.snapshots /mnt/backup/home +``` + +And you can specify the private key using `--src-private-key` similar to what you do with `--dest-private-key`. + +The deprecated snapper config interface you can do the same but it is not as intuitive: + +```bash +$ baksnapper --config home foo.example.com:/mnt/backup --type=pull +``` + +Note that `/mnt/backup` is on the local machine, and `--config home` is for `foo.example.com` — There is a reason why this interface is deprecated. + +There is also possible to pull backups from one remote machine and send them to another remote machine: + ```bash -$ baksnapper config home --all foo:/mnt/backup --private-key /root/.ssh/ +$ baksnapper foo.example.com:/home/.snapshots bar.example.com:/mnt/backup/home ``` -Or specify that in the Config file using PRIVATE_KEY. ## Config file @@ -188,17 +259,19 @@ For boolean parameters YES, yes, yes and 1 are interpret as on/true and anything else as off/false. The supported options are: -* CONFIG: The snapper config to backup. Same as --config. -* PATH: Destination to backup to. For remote backup use +* CONFIG (deprecated): The snapper config to backup. Same as --config. (deprecated) +* PATH (deprecated): Destination to backup to. For remote backup use hostname:/backup/to. Where 'hostname' is the name of the server and '/backup/to' is the path to the location it should write the backups to on the remote host. +* SOURCE: Source of the snapshots directory to backup. Same as first positional argument and `--source`. For remote backup use `[user@]hostname:/backup/from`. Where `user` is the username — it is optional and if left out till will use the current user. And `hostname` is the name of the server and `/backup/from` is the path to the location where the snapshots are located. +* DEST: Destination for the backup. Same as second positional argument and `--dest`. For remote backup use `[user@]hostname:/backup/to`. Where `user` is the username — it is optional and if left out till will use the current user. And `hostname` is the name of the server and `/backup/to` is the path to the location where it should write the backups to on the remote host. * PRUNE: Prune the backups by deleting snapshots that isn't in the source directory. Same as -p, --prune * ALL: Send all snapshots in the soruce directory. Same as -a, --all * VERBOSE: Verbose print out, same as -v, --verbose. * DAEMON: Name of the baksnapper daemon, default is baksnapperd. Same as --daemon -* TYPE: Define if it should push the backup or pull. Default is push. +* TYPE (deprecated): Define if it should push the backup or pull. Default is push. Pull can be used to transfer a backup from a remote server to the current machine. * PRIVATE_KEY: The private key file to use when connecting to a remote @@ -206,8 +279,8 @@ The supported options are: **Note:** Command line options will take precendence over config file options. **Note:** No variable expansion is supported in the config files, for -example you cannot for example type ~/backup, instead you need to -expand ~ yourself i.e /home/bob/backup if $USER = bob. +example you cannot for example type `~/backup`, instead you need to +expand ~ yourself i.e /home/bob/backup if `$USER` expands to `bob`. ## Systemd