Merge claude/1
This commit is contained in:
commit
f2dc3f27d9
@ -177,10 +177,31 @@ is a dev-only escape hatch.
|
||||
`containers/<app>/tools/<app>_<tool_id>.sh` (function `app<App><PascalToolId>`).
|
||||
- **Logic helpers** (anything that isn't a Tools-tab action — e.g. the auth
|
||||
adapter, post-install fixups): `containers/<app>/scripts/<app>_*.sh`.
|
||||
- **Static data / container payloads:** `containers/<app>/resources/` — it's
|
||||
**pruned** by the container scan (never sourced), so it may nest freely AND is
|
||||
the home for any `.sh` that *executes on load* (a payload run inside a
|
||||
container, e.g. headscale's `tailscale.sh`). Rule: a sourced function file →
|
||||
`scripts/`; a script that runs when invoked → `resources/`.
|
||||
- The container scan **live-sources** every `.sh` under `containers/<app>/`
|
||||
(`tools/` and `scripts/`), and `webui_tools.sh` **auto-merges** the
|
||||
`.tools.json`. So dropping the folder onto an install is all it takes — no
|
||||
central edits, no array regen.
|
||||
- **Reserved folder names:** `tools/`, `scripts/`, `resources/` are
|
||||
LibrePortal's. An app's compose must **not** bind-mount to `./tools`,
|
||||
`./scripts`, or `./resources` — name app mount dirs anything else (`./data`,
|
||||
`./config`, …) or nest them under `resources/`. (The scan is `maxdepth 3`, so
|
||||
one sourced subfolder level is the cap — keep `scripts/` flat; name files
|
||||
`<app>_<purpose>.sh` instead of nesting.)
|
||||
- **What gets backed up** (so you know what's safe to regenerate vs. preserve):
|
||||
per-app backups capture only the live data dir `<containers>/<app>/` — the
|
||||
deployed compose, the live `<app>.config` (settings + secrets), and the mounted
|
||||
data (DBs dumped logically; raw DB dirs excluded). The **system config**
|
||||
(`<system>/configs` — global settings, WebUI creds, **backup-location creds**)
|
||||
is captured by a separate system-config snapshot. **Not** backed up (reproducible
|
||||
from the release): `scripts/`, `tools/`, install templates, and the `libreportal`
|
||||
WebUI app itself (its frontend + generated JSON regenerate; its only state, the
|
||||
login, lives in the system config). Restore = reinstall the code, then restore
|
||||
the system config + each app's data dir.
|
||||
- **New runtime script?** Add it under `scripts/<area>/…` and run
|
||||
`scripts/source/files/generate_arrays.sh run` (or `libreportal regen arrays`) so
|
||||
it's sourced (build/standalone tooling under `scripts/release` and
|
||||
|
||||
@ -14,14 +14,27 @@ backupAllApps()
|
||||
app_names+=("$name")
|
||||
done < <(runInstallOp sqlite3 "$docker_dir/$db_file" "SELECT name FROM apps WHERE status = 1;")
|
||||
|
||||
if [[ ${#app_names[@]} -eq 0 ]]; then
|
||||
isNotice "No installed applications found — nothing to back up"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local done_count=0
|
||||
for name in "${app_names[@]}"; do
|
||||
# The libreportal WebUI app is reproducible — frontend + generated JSON
|
||||
# regenerate on deploy, and its only state (the login) lives in the system
|
||||
# config, captured by backupSystemConfig below. Nothing in its data dir is
|
||||
# worth a snapshot.
|
||||
if [[ "$name" == "libreportal" ]]; then
|
||||
isNotice "Skipping '$name' — WebUI is reproducible; its state is in the system config"
|
||||
continue
|
||||
fi
|
||||
backupAppStart "$name"
|
||||
((done_count++))
|
||||
done
|
||||
|
||||
isSuccessful "Backup pass complete — ${#app_names[@]} apps"
|
||||
# System config snapshot (global settings, WebUI creds, backup-location creds)
|
||||
# so a bare-metal restore is self-sufficient — this is why the system root is
|
||||
# its own backup-able tree. The code/install tree is reproducible from the
|
||||
# release, so it is deliberately NOT included.
|
||||
if declare -f backupSystemConfig >/dev/null 2>&1; then
|
||||
backupSystemConfig
|
||||
fi
|
||||
|
||||
isSuccessful "Backup pass complete — ${done_count} app(s) + system config"
|
||||
}
|
||||
|
||||
@ -48,6 +48,8 @@ engineEnvExport() { local i="$1"; engineDispatch "$(engineForLocation
|
||||
engineEnvUnset() { local i="$1"; engineDispatch "$(engineForLocation "${i:-1}")EnvUnset"; }
|
||||
|
||||
engineBackupApp() { local i="$1"; shift; backupLocationLocalGuard "$i" || return 1; engineDispatch "$(engineForLocation "$i")BackupAppToLocation" "$i" "$@"; }
|
||||
engineBackupSystem() { local i="$1"; shift; backupLocationLocalGuard "$i" || return 1; engineDispatch "$(engineForLocation "$i")BackupSystemToLocation" "$i" "$@"; }
|
||||
engineRestoreSystemLatest() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")RestoreSystemLatest" "$i" "$@"; }
|
||||
engineRestoreSnapshot() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")RestoreSnapshot" "$i" "$@"; }
|
||||
engineSnapshotLatestId() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotLatestId" "$i" "$@"; }
|
||||
engineSnapshotsJson() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotsJson" "$i" "$@"; }
|
||||
|
||||
@ -60,6 +60,48 @@ resticBackupAppToLocation()
|
||||
return $rc
|
||||
}
|
||||
|
||||
resticBackupSystemToLocation()
|
||||
{
|
||||
local idx="$1"
|
||||
local source_path="${configs_dir%/}"
|
||||
|
||||
if [[ ! -d "$source_path" ]]; then
|
||||
isError "System config path missing: $source_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
resticEnvExport "$idx" || return 1
|
||||
|
||||
local host_tag="${CFG_INSTALL_NAME:-libreportal}"
|
||||
local loc_name
|
||||
loc_name=$(resticLocationName "$idx")
|
||||
isNotice "Snapshotting system config → $loc_name" >&2
|
||||
local output
|
||||
output=$(runBackupOp restic backup \
|
||||
--host "$host_tag" \
|
||||
--tag "system=config" \
|
||||
--tag "host=$host_tag" \
|
||||
--tag "engine=libreportal" \
|
||||
--exclude-caches \
|
||||
--json \
|
||||
"$source_path" 2>&1)
|
||||
local rc=$?
|
||||
|
||||
local snapshot_id
|
||||
snapshot_id=$(echo "$output" | grep -o '"snapshot_id":"[^"]*"' | tail -1 | cut -d'"' -f4)
|
||||
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
isSuccessful "System config backed up to $loc_name: ${snapshot_id:0:8}" >&2
|
||||
echo "$snapshot_id"
|
||||
else
|
||||
isError "System config backup to $loc_name failed" >&2
|
||||
echo "$output" | tail -10 >&2
|
||||
fi
|
||||
|
||||
resticEnvUnset
|
||||
return $rc
|
||||
}
|
||||
|
||||
resticBackupAppAllLocations()
|
||||
{
|
||||
local app_name="$1"
|
||||
|
||||
@ -44,3 +44,26 @@ resticRestoreAppLatest()
|
||||
local include_path="$containers_dir$app_name"
|
||||
resticRestoreSnapshot "$idx" "$snapshot_id" "$target_dir" "$include_path"
|
||||
}
|
||||
|
||||
resticRestoreSystemLatest()
|
||||
{
|
||||
local idx="$1"
|
||||
local target_dir="$2"
|
||||
local host="${3:-$CFG_INSTALL_NAME}"
|
||||
|
||||
resticEnvExport "$idx" || return 1
|
||||
local snapshot_id
|
||||
snapshot_id=$(runBackupOp restic snapshots \
|
||||
--tag "system=config" --host "$host" \
|
||||
--latest 1 --json --no-lock 2>/dev/null | \
|
||||
grep -o '"short_id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
resticEnvUnset
|
||||
|
||||
if [[ -z "$snapshot_id" ]]; then
|
||||
isError "No system-config snapshot found in $(resticLocationName "$idx") for host=$host"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Whole-snapshot restore (the snapshot is just the config tree) into staging.
|
||||
resticRestoreSnapshot "$idx" "$snapshot_id" "$target_dir"
|
||||
}
|
||||
|
||||
74
scripts/backup/system/backup_system.sh
Normal file
74
scripts/backup/system/backup_system.sh
Normal file
@ -0,0 +1,74 @@
|
||||
#!/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
|
||||
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:-}"
|
||||
[[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1)
|
||||
if [[ -z "$idx" ]]; then
|
||||
isError "No enabled backup location to restore the system config from"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local staging="${restore_dir%/}/system-config"
|
||||
runFileOp mkdir -p "$staging"
|
||||
|
||||
isHeader "Restoring system config (to staging — live config is untouched)"
|
||||
if engineRestoreSystemLatest "$idx" "$staging"; then
|
||||
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
|
||||
fi
|
||||
isError "System config restore failed"
|
||||
return 1
|
||||
}
|
||||
@ -48,6 +48,7 @@ backup_scripts=(
|
||||
"backup/manifest/manifest_collect.sh"
|
||||
"backup/manifest/manifest_read.sh"
|
||||
"backup/manifest/manifest_write.sh"
|
||||
"backup/system/backup_system.sh"
|
||||
"backup/verify/backup_verify.sh"
|
||||
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user