LibrePortal/scripts/restore/restore_first_run.sh
librelad 089ac8827c network: allocate ports the host isn't already listening on
portFindNextAvailablePort consulted LibrePortal's own network_resources table
plus a hardcoded list (8080, 6060) and CFG_RESERVED_PORTS_EXTRA — while the
comment above it claimed a picked port "can never collide with a host service at
compose-up time". It can: the list only covers what someone thought to write
down. Ask the kernel instead, via ss, read once per allocation rather than per
candidate. No ss => empty set => exactly the old behaviour.

Found while restoring 13 apps onto a desktop, though not the cause there:
stoat's livekit publishes a FIXED udp range (50000-50100, which it advertises to
clients and so cannot be re-rolled), and kdeconnectd held 50016. That collision
needs its own answer; this fixes the randomly-allocated ports, which had the
same exposure with no reason to.

Also make the bulk restore stop reporting a half-running app as a clean success.
continue-on-error lets a failed compose-up log and carry on, so restoreAppStart
returns 0 either way — which is how that run printed "13 apps restored" while
four of stoat's containers had exited 101. checkSuccess already appends every
failure to error_report.log, so watch it grow across each app and name the ones
that were noisy.

scripts/dev/lp-port-host-test binds a real socket and asserts the allocator
refuses that port; verified it fails when the check is removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:33:28 +01:00

103 lines
3.9 KiB
Bash

#!/bin/bash
restoreFirstRunDiscover()
{
local idx="$1"
if ! resticLocationEnabled "$idx"; then
isError "Location $idx is not enabled"
return 1
fi
resticEnvExport "$idx" || return 1
# Via runBackupOp rather than its own sudo: this was the one backup-engine
# call bypassing that funnel, so it silently missed the -E fix for sudo-rs
# (and the -H that puts restic's cache under the backup user's HOME).
runBackupOp restic snapshots --tag engine=libreportal --json --no-lock 2>/dev/null
local rc=$?
resticEnvUnset
return $rc
}
# Restore a host's apps onto this machine.
#
# With no app list this is a WHOLE-HOST restore: the apps are discovered from
# the repository and filtered through the preflight. Both halves matter.
#
# Discovery, because an explicit list has to survive the CLI wrapper's fixed
# positional slots to get here — a 13-app restore arrived as four, restored
# those, and reported success. The wrapper now forwards the real argv, but a
# whole-host restore that never builds a list cannot be truncated at all.
#
# The preflight, because the installer prints its report in a separate process,
# so the decision it made there is gone by the time this runs. Without
# re-applying it, an app the user was told would be skipped — one this version
# no longer ships, or one too big for the disk — gets restored anyway.
restoreFirstRunBulk()
{
local idx="$1"
local source_host="$2"
shift 2
local apps_to_restore=("$@")
local -i preflighted=0
if [[ ${#apps_to_restore[@]} -eq 0 ]]; then
restorePreflightReport "$idx" "$source_host" >/dev/null 2>&1
apps_to_restore=("${RESTORE_PREFLIGHT_OK[@]}")
preflighted=1
fi
if [[ ${#apps_to_restore[@]} -eq 0 ]]; then
isError "No apps to restore for '$source_host' in this repository"
return 1
fi
isHeader "First-run bulk restore from $(resticLocationName "$idx") (host=$source_host)"
(( preflighted )) && isNotice "Restoring ${#apps_to_restore[@]} apps the preflight approved."
# Count what actually landed. A per-app failure must not be reported as a
# complete restore — that is how "4 apps restored" read as success when
# nine had gone missing.
#
# An app can also come back only half-running: continue-on-error (the
# default) lets a failed compose-up log and carry on, so restoreAppStart
# still returns 0. That is how a restore reported thirteen successes while
# stoat's livekit had lost a port race and four containers that depended on
# it exited 101. checkSuccess appends every such failure to error_report.log,
# so watch that file grow across each app and name the noisy ones.
local _errlog="${logs_dir%/}/error_report.log"
_restoreErrLines() { wc -l < "$_errlog" 2>/dev/null || echo 0; }
local app
local -i ok=0 bad=0 before=0 after=0
local -a failed=() noisy=()
for app in "${apps_to_restore[@]}"; do
before=$(_restoreErrLines)
if restoreAppStart "$app" "latest" "$idx" "$source_host"; then
ok=$(( ok + 1 ))
after=$(_restoreErrLines)
(( after > before )) && noisy+=("$app")
else
bad=$(( bad + 1 )); failed+=("$app")
fi
done
unset -f _restoreErrLines
if (( bad > 0 )); then
isError "First-run restore finished with failures — $ok of ${#apps_to_restore[@]} restored"
isNotice "Failed: ${failed[*]}"
(( ${#noisy[@]} )) && isNotice "Restored but reported errors: ${noisy[*]}"
return 1
fi
if (( ${#noisy[@]} )); then
isSuccessful "First-run restore complete — $ok apps restored"
isNotice "${#noisy[@]} reported errors while starting: ${noisy[*]}"
isNotice "They are restored, but check them: $_errlog"
return 0
fi
isSuccessful "First-run restore complete — $ok apps restored"
return 0
}