feat(uninstall): add 'init.sh uninstall' — full, guarded teardown

A single 'sudo bash init.sh uninstall' that permanently removes the whole
LibrePortal footprint, behind a typed 'DELETE LIBREPORTAL' confirmation:
- stops + removes the task-processor service
- best-effort graceful container removal, then tears down the rootless
  docker setup + the install user's session (linger/terminate/pkill)
- removes the out-of-/docker footprint (/usr/local/lib/libreportal +
  /usr/local/bin/libreportal, /etc/sudoers.d, the systemd unit, the
  sysctl drop-ins, restic/kopia/ufw-docker, /root/init.sh)
- rm -rf /docker
- removes the libreportal + dockerinstall users + subuid/subgid ranges

Runs as root (the entrypoint root-check enforces it — and the scoped
sudoers can no longer self-remove anyway); self-contained (only init.sh's
inline helpers, so it works as it deletes /docker); ordered so containers/
daemon stop before the users are removed. Leaves docker/compose/apt deps
and SSH config in place (no lockout). Mirrors FOOTPRINT.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-24 19:54:11 +01:00
parent 65937e8108
commit 93284cdb39

84
init.sh
View File

@ -176,7 +176,7 @@ fi
# is EXECUTED directly (install time) — start.sh sources init.sh for its function
# definitions at runtime (Model A, as the manager), and this block must not run
# then (it would print a spurious "Auto-detected ..." and rewrite the config).
if [[ "${BASH_SOURCE[0]}" == "$0" && -z "$param7" ]]; then
if [[ "${BASH_SOURCE[0]}" == "$0" && "$param1" != "uninstall" && -z "$param7" ]]; then
# A reinstall that doesn't re-pass the git args must not silently
# downgrade an existing git install to local (that disables the updater
# and blanks the saved creds). Honor a git URL already saved from a
@ -1270,6 +1270,86 @@ completeInitMessage()
fi
}
# FULL uninstall: permanently remove everything LibrePortal placed on the host.
# Runs as root (the entrypoint root-check below enforces it). Order matters —
# containers run AS the docker-install user and the rootless daemon is that user's
# systemd --user service, so stop those BEFORE removing the users. Self-contained:
# uses only init.sh's inline helpers, so it still works as it deletes /docker.
# Keep in sync with FOOTPRINT.md.
runFullUninstall()
{
local mgr="${sudo_user_name:-libreportal}"
local iuser
iuser=$(grep -h '^CFG_DOCKER_INSTALL_USER=' /docker/configs/general/general_docker_install 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}')
iuser="${iuser:-dockerinstall}"
isHeader "LibrePortal — FULL Uninstall"
isError "This PERMANENTLY removes EVERYTHING — there is no undo:"
echo " - all containers + images + the rootless docker setup"
echo " - /docker (ALL app data, configs, database)"
echo " - the '$mgr' and '$iuser' users + their home directories"
echo " - /usr/local/lib/libreportal/ + the /usr/local/bin/libreportal command"
echo " - /etc/sudoers.d/$mgr, the systemd service, the sysctl drop-ins"
echo " - the restic / kopia / ufw-docker binaries LibrePortal installed"
echo ""
isNotice "LEFT IN PLACE: the docker engine, docker-compose, apt-installed deps,"
isNotice "and your SSH config (so you can't get locked out)."
echo ""
isQuestion "Type exactly DELETE LIBREPORTAL to confirm:"
local confirm; read -r confirm
if [[ "$confirm" != "DELETE LIBREPORTAL" ]]; then
isNotice "Aborted — nothing was removed."
return 0
fi
isHeader "Tearing down LibrePortal"
# 1. Stop + remove the task-processor service.
systemctl disable --now libreportal.service >/dev/null 2>&1
rm -f /etc/systemd/system/libreportal.service
systemctl daemon-reload >/dev/null 2>&1
isSuccessful "Stopped + removed the task-processor service"
# 2. Best-effort graceful container removal, then tear down the rootless
# user's session (stops the rootless dockerd + any survivors).
local uid; uid=$(id -u "$iuser" 2>/dev/null)
if [[ -n "$uid" ]]; then
sudo -u "$iuser" env XDG_RUNTIME_DIR="/run/user/$uid" \
DOCKER_HOST="unix:///run/user/$uid/docker.sock" \
bash -c 'docker ps -aq | xargs -r docker rm -f' >/dev/null 2>&1 || true
sudo -u "$iuser" env XDG_RUNTIME_DIR="/run/user/$uid" \
dockerd-rootless-setuptool.sh uninstall >/dev/null 2>&1 || true
loginctl disable-linger "$iuser" >/dev/null 2>&1 || true
loginctl terminate-user "$iuser" >/dev/null 2>&1 || true
pkill -9 -u "$iuser" >/dev/null 2>&1 || true
isSuccessful "Stopped containers + rootless docker for '$iuser'"
fi
# 3. Remove the out-of-/docker footprint (see FOOTPRINT.md).
rm -f /usr/local/bin/libreportal
rm -rf /usr/local/lib/libreportal
rm -f "/etc/sudoers.d/$mgr"
rm -f /etc/sysctl.d/99-libreportal-hardening.conf /etc/sysctl.d/99-libreportal-rootless.conf
sysctl --system >/dev/null 2>&1
rm -f /usr/local/bin/restic /usr/local/bin/kopia /usr/local/bin/ufw-docker
rm -f /root/init.sh
isSuccessful "Removed the system-integration footprint"
# 4. Remove all app data.
rm -rf /docker
isSuccessful "Removed /docker"
# 5. Remove the LibrePortal users + their subuid/subgid ranges.
pkill -9 -u "$mgr" >/dev/null 2>&1 || true
userdel -r "$mgr" >/dev/null 2>&1 || true
userdel -r "$iuser" >/dev/null 2>&1 || true
sed -i "/^${mgr}:/d;/^${iuser}:/d" /etc/subuid /etc/subgid 2>/dev/null || true
isSuccessful "Removed users '$mgr' + '$iuser'"
isHeader "LibrePortal uninstalled"
isNotice "Left in place: docker engine, docker-compose, apt deps, SSH config."
}
# Only run the installer entrypoint (root check + init flow) when init.sh is
# EXECUTED directly. When it's SOURCED — start.sh loads init.sh for its function
# defs at runtime, and under Model A start.sh runs as the manager, not root — the
@ -1310,5 +1390,7 @@ else
initLibrePortalCommand
initUpdateConfigs
completeInitMessage
elif [[ "$param1" == "uninstall" ]]; then
runFullUninstall
fi
fi