Phase 0 of the migration-system refresh. Replaces the 77-line
scripts/migrate/ with a properly-shaped kernel that Phase 1 (WebUI) and
Phase 3 (direct peer SSH) can both build on.
New module layout (6 files):
migrate_progress.sh — migrateEmit JSON-per-line helper; opt-in via
MIGRATE_JSON_PROGRESS=1, writes to fd 3 if open
(clean WebUI streaming channel) else stdout.
migrate_discover.sh — migrateDiscoverHosts / migrateDiscoverApps /
migrateDiscoverAppDetail (JSON {snapshots, latest_*}).
Old migrateDiscoverAppsForHost kept as back-compat.
migrate_preflight.sh — migratePreflight emits one JSON object with
snapshot{id,date}, destination{installed,running,
disk_free_kb}, collision{occurs,default_action,
pre_backup_default}, url_rewrite{default_action,
per_app_opt_out}, warnings[], errors[].
Exit 0 on usable preflight, 1 on hard error.
migrate_url_rewrite.sh— Host-bound CFG_<APP>_* fields (URL/HOST/DOMAIN/
DOMAIN_PREFIX/HOSTNAME/PUBLIC_URL) get rewritten
from the destination's install-template after
restore — so a moved app stops claiming the
source's hostnames. Per-app opt-out via
CFG_<APP>_MIGRATE_URL_REWRITE=false. All other
fields (DB passwords, API keys, prefs) carry
over from the source unchanged.
migrate_pre_backup.sh — migratePreBackupDestination takes a snapshot of
the destination's existing <app> (tagged
pre-migrate=<UTC timestamp>) before the wipe.
Default ON; opt-out with --no-pre-backup. Safety
net for the always-replace collision policy.
migrate_apply.sh — migrateApplyApp / migrateApplySystem. Parses
--no-pre-backup / --keep-urls / --json-progress
opts, runs preflight → pre-backup → restoreAppStart
(existing flow) → URL rewrite → re-deploy compose.
migrateApp / migrateSystem kept as shims so the
old CLI surface still works.
CLI dispatcher (cli_restore_commands.sh + cli_restore_header.sh):
Existing 'restore migrate app/system/discover' calls all still work.
New verbs:
restore migrate list <host> [loc_idx]
restore migrate preflight <host> <app> [loc_idx] ← JSON, for the WebUI
Design choices baked in (per the spec):
- Always-replace collision (no multi-install of an app), safety net is the
on-by-default pre-migrate backup.
- URL rewrite by host-bound suffix list, not per-field allowlist — works
out-of-the-box for new apps without extra config.
- migrateEmit fd-3 contract is what Phase 1's WebUI will stream; falls
back to stdout in interactive CLI so dev/debug just works.
- Transport-agnostic: nothing in this kernel knows whether the backup
location is local/SSH/S3/Connect — engineSnapshotsJson + engineBackupApp
do that, so Connect (the future blind-relay) plugs in as 'just another
location kind' with zero kernel changes.
Smoke-tested: all 13 public functions register; JSON emit produces correct
escaping (quoted strings vs bare numerics) and respects MIGRATE_JSON_PROGRESS.
Signed-off-by: librelad <librelad@digitalangels.vip>
190 lines
7.0 KiB
Bash
190 lines
7.0 KiB
Bash
#!/bin/bash
|
|
|
|
# Apply a cross-host migrate. Wraps restoreAppStart with the migrate-specific
|
|
# scaffolding (pre-backup of destination, URL rewrite of host-bound CFG_*
|
|
# fields, structured progress) so the WebUI can drive it cleanly.
|
|
#
|
|
# Two entry points:
|
|
# migrateApplyApp <source_host> <app> [loc_idx] [opts...]
|
|
# migrateApplySystem <source_host> [loc_idx] [opts...]
|
|
#
|
|
# Opts (any order, all defaults match the WebUI defaults):
|
|
# --no-pre-backup Skip the safety snapshot of destination's <app>
|
|
# --keep-urls Don't rewrite host-bound CFG_<APP>_* fields
|
|
# --json-progress Emit one JSON record per stage on fd 3 / stdout
|
|
# (sets MIGRATE_JSON_PROGRESS=1 for nested helpers)
|
|
#
|
|
# Exit: 0 on success, non-zero if the migrate failed at any stage.
|
|
|
|
# Strip our --foo opts out of the positional args and set MIGRATE_OPT_* flags.
|
|
# Mutates the caller's $1.. via printf-and-reread; returns the cleaned list
|
|
# via the global _MIGRATE_REMAINING_ARGS array.
|
|
_migrateParseOpts()
|
|
{
|
|
MIGRATE_OPT_PRE_BACKUP=1
|
|
MIGRATE_OPT_KEEP_URLS=0
|
|
MIGRATE_OPT_JSON_PROGRESS=0
|
|
_MIGRATE_REMAINING_ARGS=()
|
|
|
|
local a
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--no-pre-backup) MIGRATE_OPT_PRE_BACKUP=0 ;;
|
|
--keep-urls) MIGRATE_OPT_KEEP_URLS=1 ;;
|
|
--json-progress) MIGRATE_OPT_JSON_PROGRESS=1; export MIGRATE_JSON_PROGRESS=1 ;;
|
|
--) : ;; # arg separator, ignore
|
|
*) _MIGRATE_REMAINING_ARGS+=("$a") ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
migrateApplyApp()
|
|
{
|
|
_migrateParseOpts "$@"
|
|
local source_host="${_MIGRATE_REMAINING_ARGS[0]}"
|
|
local app="${_MIGRATE_REMAINING_ARGS[1]}"
|
|
local idx="${_MIGRATE_REMAINING_ARGS[2]}"
|
|
|
|
if [[ -z "$source_host" || -z "$app" ]]; then
|
|
isError "migrateApplyApp: source_host and app required"
|
|
return 1
|
|
fi
|
|
|
|
[[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1)
|
|
if [[ -z "$idx" ]]; then
|
|
isError "No backup locations enabled — cannot migrate"
|
|
return 1
|
|
fi
|
|
|
|
local started_at
|
|
started_at=$(date +%s)
|
|
isHeader "Migrate $app from host=$source_host → this host"
|
|
migrateEmit phase=start status=running app="$app" source_host="$source_host" loc_idx="$idx"
|
|
|
|
# ---- 1. Preflight (informational; we already trust the caller) ------------
|
|
migrateEmit phase=preflight status=running
|
|
local pf
|
|
pf=$(migratePreflight "$source_host" "$app" "$idx")
|
|
local pf_rc=$?
|
|
if (( pf_rc != 0 )); then
|
|
isError "Preflight failed: $pf"
|
|
migrateEmit phase=preflight status=failed detail="$pf"
|
|
return 1
|
|
fi
|
|
migrateEmit phase=preflight status=complete
|
|
|
|
# ---- 2. Pre-migrate backup of destination (default ON) --------------------
|
|
if (( MIGRATE_OPT_PRE_BACKUP )); then
|
|
migratePreBackupDestination "$app" "$idx"
|
|
else
|
|
isNotice "Pre-migrate backup skipped (--no-pre-backup)"
|
|
migrateEmit phase=pre-backup status=skipped reason=user-opt-out app="$app"
|
|
fi
|
|
|
|
# ---- 3. The actual restore (reuses existing restoreAppStart) --------------
|
|
# restoreAppStart already wipes the app folder, restores the snapshot,
|
|
# re-runs the install-time tag pipeline, and starts the container. The
|
|
# host_filter arg makes it pull the SOURCE's snapshot, not this host's.
|
|
migrateEmit phase=restore status=running app="$app" source_host="$source_host"
|
|
if ! restoreAppStart "$app" "latest" "$idx" "$source_host"; then
|
|
isError "Restore stage failed for $app"
|
|
migrateEmit phase=restore status=failed app="$app"
|
|
return 1
|
|
fi
|
|
migrateEmit phase=restore status=complete app="$app"
|
|
|
|
# ---- 4. URL rewrite (default ON) ------------------------------------------
|
|
if (( MIGRATE_OPT_KEEP_URLS )); then
|
|
isNotice "URL rewrite skipped (--keep-urls)"
|
|
migrateEmit phase=url-rewrite status=skipped reason=user-opt-out app="$app"
|
|
else
|
|
migrateEmit phase=url-rewrite status=running app="$app"
|
|
migrateApplyUrlRewrite "$app"
|
|
# restoreAppStart already restarted the app once; if URL rewrite
|
|
# changed anything, a quick re-deploy of the compose picks them up.
|
|
# dockerComposeUpdateAndStartApp is idempotent (per the project's
|
|
# idempotent-setup preference) so re-running is cheap.
|
|
if declare -f dockerComposeUpdateAndStartApp >/dev/null 2>&1; then
|
|
dockerComposeUpdateAndStartApp "$app" install >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
|
|
local finished_at
|
|
finished_at=$(date +%s)
|
|
local duration=$((finished_at - started_at))
|
|
isSuccessful "Migrate of $app from $source_host complete in ${duration}s"
|
|
migrateEmit phase=done status=complete app="$app" duration_seconds="$duration"
|
|
return 0
|
|
}
|
|
|
|
migrateApplySystem()
|
|
{
|
|
_migrateParseOpts "$@"
|
|
local source_host="${_MIGRATE_REMAINING_ARGS[0]}"
|
|
local idx="${_MIGRATE_REMAINING_ARGS[1]}"
|
|
|
|
if [[ -z "$source_host" ]]; then
|
|
isError "migrateApplySystem: source_host required"
|
|
return 1
|
|
fi
|
|
[[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1)
|
|
if [[ -z "$idx" ]]; then
|
|
isError "No backup locations enabled — cannot migrate"
|
|
return 1
|
|
fi
|
|
|
|
isHeader "Migrate every app from host=$source_host → this host"
|
|
migrateEmit phase=system-start status=running source_host="$source_host" loc_idx="$idx"
|
|
|
|
local apps=()
|
|
while IFS= read -r a; do
|
|
[[ -n "$a" ]] && apps+=("$a")
|
|
done < <(migrateDiscoverApps "$source_host" "$idx")
|
|
|
|
if (( ${#apps[@]} == 0 )); then
|
|
isError "No apps found for host=$source_host in $(resticLocationName "$idx")"
|
|
migrateEmit phase=system-start status=failed reason=no-apps
|
|
return 1
|
|
fi
|
|
|
|
isNotice "Apps to migrate: ${apps[*]}"
|
|
migrateEmit phase=system-plan status=info apps_count="${#apps[@]}"
|
|
|
|
# Forward the same opts to each per-app run. Note we re-pass the parsed
|
|
# flags as literal --foo so each migrateApplyApp re-parses cleanly.
|
|
local forwarded=()
|
|
(( MIGRATE_OPT_PRE_BACKUP == 0 )) && forwarded+=(--no-pre-backup)
|
|
(( MIGRATE_OPT_KEEP_URLS == 1 )) && forwarded+=(--keep-urls)
|
|
(( MIGRATE_OPT_JSON_PROGRESS == 1 )) && forwarded+=(--json-progress)
|
|
|
|
local ok=0 fail=0
|
|
local a
|
|
for a in "${apps[@]}"; do
|
|
if migrateApplyApp "${forwarded[@]}" "$source_host" "$a" "$idx"; then
|
|
((ok++))
|
|
else
|
|
((fail++))
|
|
fi
|
|
done
|
|
|
|
isSuccessful "System migrate finished: $ok ok / $fail failed"
|
|
migrateEmit phase=system-done status=complete ok="$ok" failed="$fail"
|
|
(( fail == 0 ))
|
|
}
|
|
|
|
# ---- Backwards-compat shims ---------------------------------------------------
|
|
# Pre-existing callers (cli_restore_commands.sh, restoreFirstRunBulk, etc.)
|
|
# used the old names with the old arg order. Forward to the new functions.
|
|
migrateApp()
|
|
{
|
|
# Old signature: migrateApp <app_name> <source_host> [idx]
|
|
local app="$1"; local source_host="$2"; local idx="$3"
|
|
migrateApplyApp "$source_host" "$app" "$idx"
|
|
}
|
|
|
|
migrateSystem()
|
|
{
|
|
# Old signature: migrateSystem <source_host> [idx]
|
|
migrateApplySystem "$@"
|
|
}
|