A snapshot is one app's data, or the settings tree — never a machine. A
four-snapshot repository is typically two apps plus two versions of the
settings, not four backups to pick between. So the choice belongs on Contents,
after unlocking, where each snapshot has a name and a date rather than being a
hash.
Every row with more than one snapshot gets a picker, defaulting to the newest.
A row with one shows its date as text: a dropdown holding a single entry is a
control that cannot be operated, and it makes a repository with one backup look
like it is hiding something.
The chain already supported this. restorePickSnapshot has always passed any
value that is not the string "latest" straight through as an id; nothing ever
offered the choice. What was missing:
- restoreInspect returns every snapshot per app and for the settings, not
just the newest.
- restoreFirstRunBulk reads an optional RESTORE_SNAPSHOT_CHOICE map instead
of hardcoding "latest". An associative array rather than an argument,
because the CLI wrapper pads argv to nine slots and a per-app map cannot
survive it; the map reaches the host as base64 JSON, validated at the route
against restic short ids and app names since both hit a command line.
- backupRestoreSystemConfig takes a snapshot AND a host.
That host was a real bug. It defaulted to this machine's install name, which is
right for "recover my own settings" and wrong for a rebuild — the snapshots
carry the name of the machine being rebuilt FROM. It surfaced the moment a
restore adopted a config with a different install name and the next lookup
found nothing at all.
Verified by restoring both settings snapshots and diffing: 28bedbb0 brings back
a config carrying example.com, cc5b6bcf one with no domains.
Two CSS traps on the picker: appearance stayed `auto`, so the browser painted
its own control and ignored the colours entirely while the computed styles
looked right; and a `background:` shorthand later in the rule silently reset the
background-image, wiping the arrow set three lines above it.
lp-restore-adopt-test asserted configs/* were mode 0755 and started failing on
configs/webui, which libreportal-ownership sets to 0751:container on purpose —
tighter, and perfectly traversable. It asserts "the container user can traverse
it" now. A test that pins an incidental number reports a regression every time
someone improves the thing it is watching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
113 lines
4.6 KiB
Bash
113 lines
4.6 KiB
Bash
#!/bin/bash
|
|
|
|
# System-config backup.
|
|
#
|
|
# Snapshots the system config tree (<system>/configs — global settings, WebUI
|
|
# credentials, and crucially the BACKUP-LOCATION credentials) to every enabled
|
|
# backup location, so a bare-metal restore is self-sufficient. Without this the
|
|
# location creds live only on the box: lose it and you can't even reach your own
|
|
# remote backups (chicken-and-egg). It is a lightweight, static-dir snapshot — no
|
|
# container quiescing or DB dumps (those are per-app concerns), so it does NOT go
|
|
# through backupAppStart. The install tree (code) is reproducible from the release
|
|
# and is deliberately NOT included; per-app data is handled by backupAppStart.
|
|
|
|
backupSystemConfig()
|
|
{
|
|
local source_path="${configs_dir%/}"
|
|
if [[ ! -d "$source_path" ]]; then
|
|
isNotice "System config dir not found ($source_path) — skipping system backup"
|
|
return 0
|
|
fi
|
|
if [[ -z "$(resticEnabledLocations)" ]]; then
|
|
isNotice "No backup locations enabled — skipping system config backup"
|
|
return 0
|
|
fi
|
|
|
|
isHeader "Backing up system config"
|
|
engineEnsureAllLocationsReady
|
|
|
|
local idx ok=0 fail=0
|
|
while IFS= read -r idx; do
|
|
[[ -z "$idx" ]] && continue
|
|
if engineBackupSystem "$idx" >/dev/null; then
|
|
ok=$((ok + 1))
|
|
else
|
|
fail=$((fail + 1))
|
|
fi
|
|
done < <(resticEnabledLocations)
|
|
|
|
if [[ $ok -eq 0 ]]; then
|
|
isError "System config backup failed on all locations"
|
|
return 1
|
|
fi
|
|
|
|
# Apply retention so system snapshots don't accumulate (respects append-only
|
|
# locations; bypasses backupAppStart's per-app forget, so do it here).
|
|
while IFS= read -r idx; do
|
|
[[ -z "$idx" ]] && continue
|
|
engineForgetSystem "$idx" >/dev/null 2>&1 || true
|
|
done < <(resticEnabledLocations)
|
|
|
|
if [[ $fail -gt 0 ]]; then
|
|
isNotice "System config backed up to $ok location(s), failed on $fail"
|
|
else
|
|
isSuccessful "System config backed up to $ok location(s)"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Restore the latest system-config snapshot from a location into a STAGING dir.
|
|
# Deliberately does NOT overwrite live config — recovering creds/settings is a
|
|
# review-then-copy step, never an automatic blast over a running control plane.
|
|
backupRestoreSystemConfig()
|
|
{
|
|
local idx="${1:-}"
|
|
# Optional: which system-config snapshot. Empty or "latest" keeps the old
|
|
# behaviour, which is what every existing caller wants.
|
|
local want_snapshot="${2:-}"
|
|
# Optional: WHOSE snapshot. Defaults to this machine's name, which is right
|
|
# for "recover my own settings" and wrong for a rebuild — the snapshots
|
|
# were tagged with the DEAD machine's name, and the moment a restore adopts
|
|
# a config carrying a different install name, a later lookup under the new
|
|
# name finds nothing at all.
|
|
local want_host="${3:-}"
|
|
[[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1)
|
|
if [[ -z "$idx" ]]; then
|
|
isError "No enabled backup location to restore the system config from"
|
|
return 1
|
|
fi
|
|
|
|
# Root has to make this, because the manager owns restore_dir but restic
|
|
# writes into the staging tree as the container user. `runFileOp mkdir`
|
|
# here was denied every time, unchecked — and restic reports a permission
|
|
# denial as "ignoring error …" and still exits 0, so this reported
|
|
# "System config restored to: <path>" for a path that was never created.
|
|
# That is the step the whole restore ordering depends on for credentials.
|
|
local staging="${restore_dir%/}/system-config"
|
|
if ! runOwnership restore-stage "$staging"; then
|
|
isError "Could not create the staging directory at $staging"
|
|
return 1
|
|
fi
|
|
|
|
isHeader "Restoring system config (to staging — live config is untouched)"
|
|
if ! engineRestoreSystemLatest "$idx" "$staging" "$want_host" "$want_snapshot"; then
|
|
isError "System config restore failed"
|
|
return 1
|
|
fi
|
|
|
|
# Restic reports a permission denial as "ignoring error …" and still exits
|
|
# 0, so its status alone is not evidence that anything landed. Look for the
|
|
# tree as the user that wrote it — the staging dir is the container user's,
|
|
# and the manager can traverse it but not read inside.
|
|
local landed
|
|
landed=$(runFileOp find "$staging" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)
|
|
if [[ -z "$landed" ]]; then
|
|
isError "System config restore wrote nothing to $staging"
|
|
return 1
|
|
fi
|
|
|
|
isSuccessful "System config restored to: $staging"
|
|
isNotice "Review it, then copy what you need into ${configs_dir} (backup-location creds, logins, settings). Live config was NOT overwritten."
|
|
return 0
|
|
}
|