LibrePortal/scripts/cli/commands/system/cli_system_commands.sh
librelad 20f8ca2eb5 feat(network): detect + heal apps stranded off the docker subnet
Closes the gap behind the vpn-recreate bug: when the shared network is
recreated with a different /24, every app's stored static IP is left
outside it and adoptDockerSubnet only realigns CFG, not the apps.

- networkScanConflicts (network_conflicts.sh): read-only scan diffing each
  active network_resources IP against docker's real subnet (via ipInSubnet).
  Per-service routing-aware — skips gateway-routed services whose ipv4 is
  commented out in the deployed compose, so gluetun apps don't false-positive.
  Distinguishes 'daemon down' (benign) from 'network missing' (real).

- webuiSystemNetworkCheck (webui_system_network.sh): self-throttled generator
  that writes frontend/data/system/network_status.json (modelled on
  verify_status.json). Wired into webuiSystemUpdate AND run unconditionally
  every ~60s from the task-processor poll (regen webui is mtime-gated and
  would never fire on drift, which touches no source file).

- networkHealConflicts (network_heal.sh) + 'libreportal system network
  check|heal [app]': the heal adopts docker's subnet in-process, then re-IPs
  stranded apps with reset_network=ip (ports preserved), gluetun first.
  Mutating path runs only through the task system (dual-mode, like update
  apply); read-only check runs inline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:03:53 +01:00

137 lines
4.5 KiB
Bash
Executable File

#!/bin/bash
# System Commands Handler
# Handles all system subcommands by calling core functions
# Safe disk reclaim: clear the whole build cache (-a; it's pure cache, always
# safe to drop) and remove dangling images. Never touches volumes or in-use
# images. runFileOp hits the right daemon (rootless: as the install user).
reclaimDockerSpace()
{
isHeader "Reclaiming Space"
runFileOp docker builder prune -af >/dev/null 2>&1
checkSuccess "Cleared build cache"
runFileOp docker image prune -f >/dev/null 2>&1
checkSuccess "Removed dangling images"
isSuccessful "Done"
}
# Remove specific Docker images by id/ref. Args:
# $1 force flag — "-f" to force-remove (e.g. in-use images), else empty
# $2 comma-separated list of image refs (the WebUI sends full sha256:… ids)
# The WebUI Storage page calls this through the task system (see the
# system_image_rm action) — never a direct API. Each ref is validated before it
# reaches docker; removal continues past per-image failures and reports a tally.
removeDockerImages()
{
local force_flag="$1" ids_csv="$2"
isHeader "Removing Images"
if [[ -z "$ids_csv" ]]; then
isError "No images specified."
return 1
fi
local removed=0 failed=0 id
local IFS=','
local -a ids=($ids_csv)
unset IFS
for id in "${ids[@]}"; do
id="${id//[[:space:]]/}"
[[ -n "$id" ]] || continue
# Accept only a sha256 digest or a conservative repo[:tag] ref — no shell
# metacharacters can reach docker even though this arrives via the task
# command string.
if [[ ! "$id" =~ ^sha256:[a-f0-9]{12,64}$ && ! "$id" =~ ^[A-Za-z0-9][A-Za-z0-9._/:@-]*$ ]]; then
isError "Skipping invalid image ref: $id"
failed=$((failed + 1)); continue
fi
# $force_flag is intentionally unquoted: it's either empty or "-f".
if runFileOp docker image rm $force_flag "$id" >/dev/null 2>&1; then
isSuccessful "Removed $id"
removed=$((removed + 1))
else
isError "Could not remove $id (in use, or has dependent child images)"
failed=$((failed + 1))
fi
done
isNotice "Done — removed ${removed}, failed/skipped ${failed}."
[[ "$failed" -eq 0 ]]
}
cliHandleSystemCommands()
{
local action="$initial_command2"
case "$action" in
"status")
tagsValidateShowSystemStatus
;;
"update")
checkUpdates
;;
"reset")
runReinstall
;;
"reclaim")
reclaimDockerSpace
;;
"network")
# libreportal system network check [force] (read-only, rewrites
# network_status.json — used by the task-processor poll + WebUI)
# libreportal system network heal [<app>] (mutating — re-IPs
# stranded apps from the corrected subnet, ports preserved; routes
# through the task system like update apply)
case "$initial_command3" in
"check")
webuiSystemNetworkCheck "${initial_command4:-force}"
;;
"heal")
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
networkHealConflicts "$initial_command4"
else
cliTaskRun "libreportal system network heal${initial_command4:+ $initial_command4}" "system_network_heal" "" ""
fi
;;
*)
isNotice "Invalid network command: $initial_command3"
cliShowSystemHelp
;;
esac
;;
"image")
# libreportal system image rm [--force] <comma-separated ids>
case "$initial_command3" in
"rm"|"remove")
local img_force="" img_ids=""
if [[ "$initial_command4" == "--force" || "$initial_command4" == "-f" ]]; then
img_force="-f"; img_ids="$initial_command5"
else
img_ids="$initial_command4"
fi
removeDockerImages "$img_force" "$img_ids"
;;
*)
isNotice "Invalid image command: $initial_command3"
cliShowSystemHelp
;;
esac
;;
*)
isNotice "Invalid system command: $action"
cliShowSystemHelp
;;
esac
}