Offer retry/fall-back-to-SFTP/skip when Garage isn't found for a mirror

Previously, if the additional-mirror S3/Garage check couldn't find
~/docker/garage/.env on the remote box, it just warned and silently
dropped the mirror — forcing a full re-run (and re-entering every
already-answered prompt: destinations, passwords, schedule, B2,
DR-spare, etc.) once Garage was actually installed.

Wrap the S3/SFTP branch in a loop so the "Garage isn't installed yet"
case now offers a real 3-way choice:
  1) install Garage in another session, then retry the same .env check
     without leaving this script
  2) fall back to SFTP for this one mirror, reusing the already-resolved
     destination host/port/user/mirror-name with no re-prompting
  3) skip just this mirror (default — safe for UNATTENDED, which
     resolves to this automatically since prompt_text returns its
     default without blocking)

Everything else install_backup() has already collected lives outside
this loop, so none of it is at risk regardless of which of the three
exits it via.

Verified against a standalone harness reproducing the state machine
with a mocked ssh (empty .env vs. populated .env after a simulated
install) and prompt_text, covering all three interactive choices, the
blank/Enter default, and UNATTENDED mode (confirms the blocking
"press Enter to retry" read is unreachable there since prompt_text
resolves choice 1's prompt to default "3" without waiting on stdin).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
This commit is contained in:
Claude
2026-08-15 12:45:29 +00:00
parent 74cab14f86
commit f61a2717bf
+116 -73
View File
@@ -992,30 +992,67 @@ install_backup() {
if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$_SFTP_DEST" true 2>/dev/null; then if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$_SFTP_DEST" true 2>/dev/null; then
log_warning " Couldn't SSH to $_SFTP_DEST without a password — not adding this" log_warning " Couldn't SSH to $_SFTP_DEST without a password — not adding this"
log_warning " mirror until that works: ssh-copy-id $_SFTP_DEST" log_warning " mirror until that works: ssh-copy-id $_SFTP_DEST"
elif [ "$_MIRROR_TYPE_CHOICE" = "2" ]; then else
# ── S3 (Garage) ────────────────────────────────────────────── # Loop so "Garage isn't installed yet" can offer a real retry/
# Read the real bucket/key/port straight from the remote # fall-back-to-SFTP/skip choice instead of just dropping the
# instance's own .env rather than asking the operator to # whole mirror — $_SFTP_DEST and $_MIRROR_NAME are already
# retype them here — those values are generated once by # resolved above, so none of that has to be re-entered no
# services/garage.sh and never touched again on its own # matter which way this loop exits. Nothing collected earlier
# Update runs, so this is always reading the box's actual # in this function (destinations, passwords, schedule, B2,
# current configuration, not something baked in here. # DR-spare, ...) is inside this loop at all, so choosing to
local _garage_env # skip here never loses any of that either — it's already
_garage_env="$(ssh -o BatchMode=yes -o ConnectTimeout=5 "$_SFTP_DEST" "cat ~/docker/garage/.env 2>/dev/null")" # sitting in local variables the "Write backup.conf" step
if [ -z "$_garage_env" ]; then # below reads regardless of what happens with this one mirror.
log_warning " Garage isn't installed on $_SFTP_DEST yet — not adding this mirror." while true; do
log_warning " Install it there first, then re-run this installer:" if [ "$_MIRROR_TYPE_CHOICE" = "2" ]; then
log_warning " sudo ./setup.sh garage (or: sudo bash garage.sh, on a box without this repo)" # ── S3 (Garage) ──────────────────────────────────────
else # Read the real bucket/key/port straight from the
local _g_bucket _g_key_id _g_key_secret _g_port # remote instance's own .env rather than asking the
_g_bucket="$(echo "$_garage_env" | sed -nE "s/^GARAGE_BUCKET='?([^']*)'?\$/\1/p")" # operator to retype them here — those values are
_g_key_id="$(echo "$_garage_env" | sed -nE "s/^GARAGE_ACCESS_KEY_ID='?([^']*)'?\$/\1/p")" # generated once by services/garage.sh and never
_g_key_secret="$(echo "$_garage_env" | sed -nE "s/^GARAGE_ACCESS_KEY_SECRET='?([^']*)'?\$/\1/p")" # touched again on its own Update runs, so this is
_g_port="$(echo "$_garage_env" | sed -nE "s/^GARAGE_S3_API_PORT=([0-9]+)\$/\1/p")" # always reading the box's actual current
if [ -z "$_g_bucket" ] || [ -z "$_g_key_id" ] || [ -z "$_g_key_secret" ] || [ -z "$_g_port" ]; then # configuration, not something baked in here.
log_warning " Garage is installed on $_SFTP_DEST but its .env is missing something" local _garage_env
log_warning " expected — check ~/docker/garage/.env there. Not adding this mirror." _garage_env="$(ssh -o BatchMode=yes -o ConnectTimeout=5 "$_SFTP_DEST" "cat ~/docker/garage/.env 2>/dev/null")"
else if [ -z "$_garage_env" ]; then
echo ""
log_warning " Garage isn't installed on $_SFTP_DEST yet."
echo " 1) Install it now, then retry this check"
echo " (in another session on $_SFTP_DEST: sudo ./setup.sh garage"
echo " — or: sudo bash garage.sh, on a box without this repo)"
echo " 2) Use SFTP instead for this mirror (same destination/name)"
echo " 3) Skip this mirror — everything else you've entered stays"
echo ""
local _GARAGE_MISSING_CHOICE=""
prompt_text " Choice [3]:" "3" _GARAGE_MISSING_CHOICE
case "${_GARAGE_MISSING_CHOICE:-3}" in
1)
read -r -p " Press Enter once Garage is installed on $_SFTP_DEST to retry (or Ctrl-C to give up): " _
continue
;;
2)
_MIRROR_TYPE_CHOICE="1"
continue
;;
*)
log_info " Skipping this mirror."
break
;;
esac
fi
local _g_bucket _g_key_id _g_key_secret _g_port
_g_bucket="$(echo "$_garage_env" | sed -nE "s/^GARAGE_BUCKET='?([^']*)'?\$/\1/p")"
_g_key_id="$(echo "$_garage_env" | sed -nE "s/^GARAGE_ACCESS_KEY_ID='?([^']*)'?\$/\1/p")"
_g_key_secret="$(echo "$_garage_env" | sed -nE "s/^GARAGE_ACCESS_KEY_SECRET='?([^']*)'?\$/\1/p")"
_g_port="$(echo "$_garage_env" | sed -nE "s/^GARAGE_S3_API_PORT=([0-9]+)\$/\1/p")"
if [ -z "$_g_bucket" ] || [ -z "$_g_key_id" ] || [ -z "$_g_key_secret" ] || [ -z "$_g_port" ]; then
log_warning " Garage is installed on $_SFTP_DEST but its .env is missing something"
log_warning " expected — check ~/docker/garage/.env there. Not adding this mirror."
break
fi
local _g_endpoint="${_SFTP_HOSTNAME}:${_g_port}" local _g_endpoint="${_SFTP_HOSTNAME}:${_g_port}"
log_info "Verifying S3 (Garage) mirror at ${_g_endpoint}, bucket '$_g_bucket' (dry-run)..." log_info "Verifying S3 (Garage) mirror at ${_g_endpoint}, bucket '$_g_bucket' (dry-run)..."
local _s3_err local _s3_err
@@ -1038,57 +1075,63 @@ install_backup() {
log_warning " S3 (Garage) dry-run failed — not adding this mirror:" log_warning " S3 (Garage) dry-run failed — not adding this mirror:"
log_warning " $_s3_err" log_warning " $_s3_err"
fi fi
fi break
fi
else
# ── SFTP ─────────────────────────────────────────────────────
# Suggest a subdirectory of the DR-spare's own path (if one is
# configured) rather than an unrelated default — reusing the
# same spare location the operator already picked, but in its
# own /kopia-data subdirectory so the actual repository data
# (Kopia's own blob-store files) doesn't end up visually mixed
# in with the two plain config files the DR-spare sync writes
# directly into DR_SYNC_PATH itself.
local _SFTP_PATH_DEFAULT="~/backups/kopia-mirror"
[ -n "${DR_SYNC_PATH:-}" ] && _SFTP_PATH_DEFAULT="${DR_SYNC_PATH%/}/kopia-data"
local _SFTP_PATH=""
prompt_text " Remote path for the repo:" "$_SFTP_PATH_DEFAULT" _SFTP_PATH
_SFTP_PATH="${_SFTP_PATH:-$_SFTP_PATH_DEFAULT}"
# sync-to sftp doesn't shell out to the system ssh client, so
# it needs an explicit key/known_hosts file rather than
# picking up whatever plain `ssh` already trusts automatically.
_backup_ensure_root_ssh_key "$_SFTP_DEST"
local _SFTP_KEYFILE="$_ROOT_SSH_KEYFILE"
if [ -z "$_SFTP_KEYFILE" ]; then
log_warning " No SSH key available for root — can't add this mirror."
else
log_info "Verifying SFTP mirror (dry-run sync against the 'default' repo)..."
local _sftp_err
if _sftp_err="$(env KOPIA_PASSWORD="${DEST_PASSWORDS[default]}" "$KOPIA_BIN" \
--config-file="${DEST_CONFIGS[default]}" repository sync-to sftp \
--host="$_SFTP_HOSTNAME" --port="$_SFTP_PORT" --username="$_SFTP_USER" --path="$_SFTP_PATH" \
--keyfile="$_SFTP_KEYFILE" --known-hosts=/root/.ssh/known_hosts \
--dry-run 2>&1)"; then
EXTRA_MIRROR_TYPE["$_MIRROR_NAME"]="sftp"
EXTRA_MIRROR_ARGS["$_MIRROR_NAME"]="--host=$_SFTP_HOSTNAME --port=$_SFTP_PORT --username=$_SFTP_USER --path=$_SFTP_PATH --keyfile=$_SFTP_KEYFILE --known-hosts=/root/.ssh/known_hosts"
# Reusing an existing mirror name reconfigures it (the
# associative-array assignments above already do that)
# without duplicating it in the space-separated name list.
if [[ " $EXTRA_MIRROR_NAMES " != *" $_MIRROR_NAME "* ]]; then
if [ -z "$EXTRA_MIRROR_NAMES" ]; then
EXTRA_MIRROR_NAMES="$_MIRROR_NAME"
else
EXTRA_MIRROR_NAMES="$EXTRA_MIRROR_NAMES $_MIRROR_NAME"
fi
fi
log_success " SFTP mirror '$_MIRROR_NAME' verified — will run after every backup."
else else
log_warning " SFTP dry-run failed — not adding this mirror:" # ── SFTP ─────────────────────────────────────────────
log_warning " $_sftp_err" # Suggest a subdirectory of the DR-spare's own path (if
# one is configured) rather than an unrelated default
# — reusing the same spare location the operator
# already picked, but in its own /kopia-data
# subdirectory so the actual repository data (Kopia's
# own blob-store files) doesn't end up visually mixed
# in with the two plain config files the DR-spare sync
# writes directly into DR_SYNC_PATH itself.
local _SFTP_PATH_DEFAULT="~/backups/kopia-mirror"
[ -n "${DR_SYNC_PATH:-}" ] && _SFTP_PATH_DEFAULT="${DR_SYNC_PATH%/}/kopia-data"
local _SFTP_PATH=""
prompt_text " Remote path for the repo:" "$_SFTP_PATH_DEFAULT" _SFTP_PATH
_SFTP_PATH="${_SFTP_PATH:-$_SFTP_PATH_DEFAULT}"
# sync-to sftp doesn't shell out to the system ssh
# client, so it needs an explicit key/known_hosts file
# rather than picking up whatever plain `ssh` already
# trusts automatically.
_backup_ensure_root_ssh_key "$_SFTP_DEST"
local _SFTP_KEYFILE="$_ROOT_SSH_KEYFILE"
if [ -z "$_SFTP_KEYFILE" ]; then
log_warning " No SSH key available for root — can't add this mirror."
break
fi
log_info "Verifying SFTP mirror (dry-run sync against the 'default' repo)..."
local _sftp_err
if _sftp_err="$(env KOPIA_PASSWORD="${DEST_PASSWORDS[default]}" "$KOPIA_BIN" \
--config-file="${DEST_CONFIGS[default]}" repository sync-to sftp \
--host="$_SFTP_HOSTNAME" --port="$_SFTP_PORT" --username="$_SFTP_USER" --path="$_SFTP_PATH" \
--keyfile="$_SFTP_KEYFILE" --known-hosts=/root/.ssh/known_hosts \
--dry-run 2>&1)"; then
EXTRA_MIRROR_TYPE["$_MIRROR_NAME"]="sftp"
EXTRA_MIRROR_ARGS["$_MIRROR_NAME"]="--host=$_SFTP_HOSTNAME --port=$_SFTP_PORT --username=$_SFTP_USER --path=$_SFTP_PATH --keyfile=$_SFTP_KEYFILE --known-hosts=/root/.ssh/known_hosts"
# Reusing an existing mirror name reconfigures it
# (the associative-array assignments above already
# do that) without duplicating it in the
# space-separated name list.
if [[ " $EXTRA_MIRROR_NAMES " != *" $_MIRROR_NAME "* ]]; then
if [ -z "$EXTRA_MIRROR_NAMES" ]; then
EXTRA_MIRROR_NAMES="$_MIRROR_NAME"
else
EXTRA_MIRROR_NAMES="$EXTRA_MIRROR_NAMES $_MIRROR_NAME"
fi
fi
log_success " SFTP mirror '$_MIRROR_NAME' verified — will run after every backup."
else
log_warning " SFTP dry-run failed — not adding this mirror:"
log_warning " $_sftp_err"
fi
break
fi fi
fi done
fi fi
fi fi
fi fi