mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
- Generate and ship a verified 389-app tracking catalog with 23 runtime overrides, fallback detectors, ports, logos, and Docker Hub tag previews. - Support modern Proxmox VE Helper-Scripts markers, historical installations, and official or manual app deployments. - Rework the LXC App and Updates tabs with cached suggestions, explicit discovery, version tracking, web links, custom updaters, and complete i18n. - Add independent OS, app, Docker Engine, Docker image, bulk, and scheduled update targets. - Add digest-based Docker inventory, Compose dependency grouping, safe standalone-container recreation with rollback, and package-scoped Docker Engine updates. - Refresh per-LXC caches after lifecycle and update tasks, then emit idempotent notifications based on the verified final state. - Harden Coral USB recovery by removing orphaned gasket DKMS registrations and validating that dpkg is healthy before reporting success.
338 lines
13 KiB
Bash
Executable File
338 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==========================================================
|
|
# ProxMenux — Apply Updates to an LXC container
|
|
# ==========================================================
|
|
# Runs inside the Monitor's terminal streamer (PTY over WS).
|
|
# Input via env vars (all set by the frontend before launch):
|
|
#
|
|
# VMID — target container id (required)
|
|
# TARGET — "os" | "app" | "both" (required)
|
|
# BACKUP — "1" to snapshot with vzdump first, "0" to skip
|
|
# BACKUP_STORAGE — PVE storage name for vzdump (required when BACKUP=1)
|
|
# RESTART — "1" to `pct reboot` after update, "0" to skip
|
|
# RUN_HELPER — "1" to run the verified community-scripts
|
|
# updater referenced by /usr/bin/update, "0"
|
|
# to leave it alone. Never inferred from names.
|
|
# UPDATE_COMMAND — optional user-defined bash string. When set
|
|
# and TARGET is "app" or "both", the script
|
|
# runs it VIA sh -c inside the CT. A custom
|
|
# command always replaces RUN_HELPER for safety.
|
|
# This IS the one place we
|
|
# intentionally use sh -c with a variable
|
|
# payload — the threat model matches "user
|
|
# typed it via pct exec themselves"; ProxMenux
|
|
# does not compose or interpret the command.
|
|
# ALLOW_HELPER_WITH_CUSTOM — "1" only for an explicit multi-app plan
|
|
# where RUN_HELPER belongs to one registered app and
|
|
# UPDATE_COMMAND contains other registered apps. The
|
|
# default "0" preserves the custom-replaces-helper rule
|
|
# for single-app and legacy callers.
|
|
# DOCKER_STANDALONE_TARGETS — optional comma-separated Docker container
|
|
# names. Each is recreated transactionally by the
|
|
# protected host-side Docker recreation helper.
|
|
# UPDATE_DOCKER_ENGINE — "1" to update only the installed Docker Engine
|
|
# package stack, without upgrading unrelated OS packages.
|
|
#
|
|
# Exit codes:
|
|
# 0 everything requested completed OK
|
|
# 1 CT not found on this node
|
|
# 2 CT could not be started
|
|
# 3 pre-update backup failed (abort so the user still has a rollback)
|
|
# 4 OS update failed OR OS family not supported for automated updates
|
|
# 5 TARGET=app requested but no update method (neither UPDATE_COMMAND
|
|
# nor explicitly-enabled verified helper) available in the CT
|
|
# 6 post-update restart failed
|
|
# 7 another ProxMenux update is already running for this CT
|
|
#
|
|
# The frontend surfaces exit code + duration in a follow-up POST to
|
|
# /api/lxc-updates/<vmid>/applied so the notification event fires with
|
|
# the correct result field.
|
|
# ==========================================================
|
|
|
|
set -o pipefail
|
|
|
|
: "${VMID:?VMID is required}"
|
|
: "${TARGET:?TARGET is required}"
|
|
BACKUP="${BACKUP:-0}"
|
|
RESTART="${RESTART:-0}"
|
|
RUN_HELPER="${RUN_HELPER:-0}"
|
|
UPDATE_COMMAND="${UPDATE_COMMAND:-}"
|
|
ALLOW_HELPER_WITH_CUSTOM="${ALLOW_HELPER_WITH_CUSTOM:-0}"
|
|
DOCKER_STANDALONE_TARGETS="${DOCKER_STANDALONE_TARGETS:-}"
|
|
UPDATE_DOCKER_ENGINE="${UPDATE_DOCKER_ENGINE:-0}"
|
|
|
|
if [[ ! "$VMID" =~ ^[1-9][0-9]*$ ]]; then
|
|
echo "ERROR: VMID must be a positive integer." >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$TARGET" != "os" && "$TARGET" != "app" && "$TARGET" != "both" ]]; then
|
|
echo "ERROR: TARGET must be os, app, or both." >&2
|
|
exit 4
|
|
fi
|
|
if [[ "$RUN_HELPER" != "0" && "$RUN_HELPER" != "1" ]]; then
|
|
echo "ERROR: RUN_HELPER must be 0 or 1." >&2
|
|
exit 5
|
|
fi
|
|
if [[ "$ALLOW_HELPER_WITH_CUSTOM" != "0" && "$ALLOW_HELPER_WITH_CUSTOM" != "1" ]]; then
|
|
echo "ERROR: ALLOW_HELPER_WITH_CUSTOM must be 0 or 1." >&2
|
|
exit 5
|
|
fi
|
|
if [[ "$UPDATE_DOCKER_ENGINE" != "0" && "$UPDATE_DOCKER_ENGINE" != "1" ]]; then
|
|
echo "ERROR: UPDATE_DOCKER_ENGINE must be 0 or 1." >&2
|
|
exit 5
|
|
fi
|
|
|
|
# One update per CT at a time, regardless of whether it came from the
|
|
# UI or the scheduler. The descriptor remains open for this process.
|
|
LOCK_DIR="${PROXMENUX_LOCK_DIR:-/run/lock}"
|
|
exec 9>"${LOCK_DIR}/proxmenux-lxc-update-${VMID}.lock"
|
|
if ! flock -n 9; then
|
|
echo "ERROR: another ProxMenux update is already running for CT $VMID." >&2
|
|
exit 7
|
|
fi
|
|
|
|
STARTED_AT=$(date -Iseconds)
|
|
NODE=$(hostname)
|
|
echo "=== ProxMenux LXC update — CT $VMID on $NODE ==="
|
|
echo "Started: $STARTED_AT"
|
|
echo "Target: $TARGET"
|
|
echo "Backup: $BACKUP${BACKUP_STORAGE:+ (storage: $BACKUP_STORAGE)}"
|
|
echo "Restart: $RESTART"
|
|
echo "Helper: $RUN_HELPER"
|
|
echo
|
|
|
|
# 1) CT must exist on this node.
|
|
if ! pct list | awk 'NR>1 {print $1}' | grep -qE "^${VMID}$"; then
|
|
echo "ERROR: CT $VMID is not on this node." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 2) CT must be running for pct exec. Auto-start stopped CTs, then
|
|
# restore their original stopped state on every exit path.
|
|
STATE=$(pct status "$VMID" | awk '{print $2}')
|
|
STARTED_BY_PROXMENUX=0
|
|
restore_original_state() {
|
|
local rc=$?
|
|
trap - EXIT INT TERM
|
|
if [[ "$STARTED_BY_PROXMENUX" == "1" ]]; then
|
|
echo
|
|
echo "Restoring original state: stopping CT $VMID…"
|
|
if ! pct shutdown "$VMID" --timeout 60; then
|
|
echo "ERROR: update finished but CT $VMID could not be returned to its original stopped state." >&2
|
|
if [[ "$rc" -eq 0 ]]; then
|
|
rc=6
|
|
fi
|
|
fi
|
|
fi
|
|
exit "$rc"
|
|
}
|
|
trap restore_original_state EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
if [[ "$STATE" != "running" ]]; then
|
|
echo "CT is $STATE. Starting it before applying updates…"
|
|
if ! pct start "$VMID"; then
|
|
echo "ERROR: failed to start CT $VMID." >&2
|
|
exit 2
|
|
fi
|
|
STARTED_BY_PROXMENUX=1
|
|
# give the CT a moment for services to come up
|
|
sleep 3
|
|
fi
|
|
|
|
# 3) Optional pre-update snapshot. Uses vzdump (not `pct snapshot`)
|
|
# because most homelab storages support vzdump snapshots (including
|
|
# directory + PBS) whereas `pct snapshot` requires the underlying
|
|
# storage type to expose it. Abort on backup failure so the user
|
|
# always has a rollback point when they asked for one.
|
|
if [[ "$BACKUP" == "1" ]]; then
|
|
: "${BACKUP_STORAGE:?BACKUP_STORAGE is required when BACKUP=1}"
|
|
echo "--- Creating vzdump snapshot on '$BACKUP_STORAGE' ---"
|
|
if ! vzdump "$VMID" --mode snapshot --storage "$BACKUP_STORAGE" --compress zstd --notes-template "pre-update {{guestname}} {{node}}"; then
|
|
echo "ERROR: pre-update backup failed. Aborting so you keep a rollback point." >&2
|
|
exit 3
|
|
fi
|
|
echo
|
|
fi
|
|
|
|
# 4) Detect OS family for the OS-update branch.
|
|
OS_FAMILY="unknown"
|
|
if OS_LINE=$(pct exec "$VMID" -- sh -c 'grep -E "^ID=" /etc/os-release 2>/dev/null | head -1' 2>/dev/null); then
|
|
OS_FAMILY=$(echo "$OS_LINE" | sed -e 's/^ID=//' -e 's/^"//' -e 's/"$//')
|
|
fi
|
|
echo "OS family: $OS_FAMILY"
|
|
echo
|
|
|
|
OS_FAILED=0
|
|
APP_FAILED=0
|
|
|
|
# 5) OS package updates.
|
|
if [[ "$TARGET" == "os" || "$TARGET" == "both" ]]; then
|
|
echo "--- Applying OS package updates ---"
|
|
case "$OS_FAMILY" in
|
|
debian|ubuntu)
|
|
# `apt-get` (not `apt`) for machine-friendly output; the
|
|
# DEBIAN_FRONTEND avoids interactive dpkg prompts on config
|
|
# conflicts (dpkg keeps the local version by default with
|
|
# --force-confold).
|
|
if ! pct exec "$VMID" -- env DEBIAN_FRONTEND=noninteractive \
|
|
apt-get -y -o Dpkg::Options::="--force-confold" dist-upgrade; then
|
|
echo "ERROR: apt-get dist-upgrade failed inside CT $VMID." >&2
|
|
OS_FAILED=1
|
|
fi
|
|
;;
|
|
alpine)
|
|
if ! pct exec "$VMID" -- apk upgrade --no-cache; then
|
|
echo "ERROR: apk upgrade failed inside CT $VMID." >&2
|
|
OS_FAILED=1
|
|
fi
|
|
;;
|
|
*)
|
|
echo "OS family '$OS_FAMILY' isn't supported for automated OS updates." >&2
|
|
OS_FAILED=1
|
|
;;
|
|
esac
|
|
echo
|
|
fi
|
|
|
|
# 6) Application update. Explicit methods only:
|
|
# a) RUN_HELPER=1 + a valid /usr/bin/update wrapper →
|
|
# parses the ct/<slug>.sh URL from the wrapper, canonicalises it
|
|
# to the official repository, then runs the current helper
|
|
# inside the CT with PHS_SILENT=1.
|
|
# b) UPDATE_COMMAND set → run it verbatim via `sh -c`
|
|
# inside the CT. The one intentional shell-exec-with-variable
|
|
# in ProxMenux — see header comment for threat-model rationale.
|
|
# UPDATE_COMMAND always wins if a legacy caller also sets RUN_HELPER.
|
|
# A hostname/tag/cache guess is never executable evidence.
|
|
if [[ "$TARGET" == "app" || "$TARGET" == "both" ]]; then
|
|
APP_METHOD_RAN=0
|
|
if [[ -n "$UPDATE_COMMAND" && "$RUN_HELPER" == "1" && "$ALLOW_HELPER_WITH_CUSTOM" != "1" ]]; then
|
|
echo "Custom update command configured; skipping Proxmox VE Helper-Scripts updater."
|
|
RUN_HELPER=0
|
|
fi
|
|
if [[ "$UPDATE_DOCKER_ENGINE" == "1" ]]; then
|
|
echo "--- Updating Docker Engine only ---"
|
|
if ! python3 /usr/local/share/proxmenux/monitor-app/usr/bin/update_docker_engine.py \
|
|
--vmid "$VMID"; then
|
|
echo "ERROR: Docker Engine update failed." >&2
|
|
APP_FAILED=1
|
|
fi
|
|
APP_METHOD_RAN=1
|
|
echo
|
|
fi
|
|
if [[ "$RUN_HELPER" == "1" ]]; then
|
|
UPDATE_URL=""
|
|
RESOLVED_SLUG=""
|
|
if pct exec "$VMID" -- test -f /usr/bin/update 2>/dev/null; then
|
|
UPDATE_URL=$(pct exec "$VMID" -- cat /usr/bin/update 2>/dev/null | grep -oE 'https?://[^"'"'"' ]+ct/[a-zA-Z0-9._-]+\.sh' | head -1)
|
|
RESOLVED_SLUG=$(echo "$UPDATE_URL" | sed -nE 's|.*/ct/([a-zA-Z0-9._-]+)\.sh$|\1|p')
|
|
fi
|
|
case "$RESOLVED_SLUG" in
|
|
alpine|archlinux|archlinux-vm|debian|fedora|gentoo|opensuse|ubuntu)
|
|
echo "ERROR: /usr/bin/update references the base-OS helper '$RESOLVED_SLUG', not an application updater." >&2
|
|
APP_FAILED=1
|
|
RESOLVED_SLUG=""
|
|
;;
|
|
esac
|
|
if [[ -z "$RESOLVED_SLUG" ]]; then
|
|
if [[ "$APP_FAILED" -eq 0 ]]; then
|
|
echo "ERROR: RUN_HELPER=1 but /usr/bin/update contains no valid community-scripts app reference." >&2
|
|
APP_FAILED=1
|
|
fi
|
|
else
|
|
# Never execute the arbitrary URL embedded in the CT. The slug is
|
|
# constrained by the parser; fetch the canonical upstream path.
|
|
UPDATE_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${RESOLVED_SLUG}.sh"
|
|
echo "--- Running community-scripts helper (slug: $RESOLVED_SLUG) ---"
|
|
# Community-scripts' build.func in start() dispatches on
|
|
# `command -v pveversion`: present → install_script (whiptail
|
|
# "Default Install / Advanced / Settings" menu); absent → the
|
|
# PHS_SILENT=1 branch runs update_script silently. Since
|
|
# pveversion only exists on the Proxmox host, we run the script
|
|
# INSIDE the CT so the framework picks the silent update path.
|
|
# The CT must have wget or curl — every modern helper install
|
|
# ships one of them.
|
|
IN_CT_FETCH=""
|
|
if pct exec "$VMID" -- sh -c 'command -v wget >/dev/null 2>&1'; then
|
|
IN_CT_FETCH="wget -qLO - '$UPDATE_URL'"
|
|
elif pct exec "$VMID" -- sh -c 'command -v curl >/dev/null 2>&1'; then
|
|
IN_CT_FETCH="curl -fsSL '$UPDATE_URL'"
|
|
fi
|
|
if [[ -z "$IN_CT_FETCH" ]]; then
|
|
echo "ERROR: CT $VMID has neither wget nor curl — cannot fetch the helper." >&2
|
|
APP_FAILED=1
|
|
else
|
|
if ! pct exec "$VMID" -- bash -c "PHS_SILENT=1 bash -c \"\$($IN_CT_FETCH)\""; then
|
|
echo "ERROR: community-scripts helper returned non-zero." >&2
|
|
APP_FAILED=1
|
|
fi
|
|
fi
|
|
APP_METHOD_RAN=1
|
|
echo
|
|
fi
|
|
fi
|
|
if [[ -n "$UPDATE_COMMAND" ]]; then
|
|
echo "--- Running user-defined update command ---"
|
|
echo "\$ $UPDATE_COMMAND"
|
|
if ! pct exec "$VMID" -- sh -c "$UPDATE_COMMAND"; then
|
|
echo "ERROR: user-defined update command returned non-zero." >&2
|
|
APP_FAILED=1
|
|
fi
|
|
APP_METHOD_RAN=1
|
|
echo
|
|
fi
|
|
if [[ -n "$DOCKER_STANDALONE_TARGETS" ]]; then
|
|
IFS=',' read -r -a DOCKER_TARGETS <<< "$DOCKER_STANDALONE_TARGETS"
|
|
for DOCKER_CONTAINER in "${DOCKER_TARGETS[@]}"; do
|
|
if [[ ! "$DOCKER_CONTAINER" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$ ]]; then
|
|
echo "ERROR: invalid Docker container target '$DOCKER_CONTAINER'." >&2
|
|
APP_FAILED=1
|
|
continue
|
|
fi
|
|
echo "--- Recreating standalone Docker container: $DOCKER_CONTAINER ---"
|
|
if ! python3 /usr/local/share/proxmenux/monitor-app/usr/bin/recreate_docker_container.py \
|
|
--vmid "$VMID" --container "$DOCKER_CONTAINER"; then
|
|
echo "ERROR: protected Docker recreation failed for '$DOCKER_CONTAINER'." >&2
|
|
APP_FAILED=1
|
|
fi
|
|
APP_METHOD_RAN=1
|
|
echo
|
|
done
|
|
fi
|
|
if [[ "$APP_METHOD_RAN" -eq 0 ]]; then
|
|
if [[ "$TARGET" == "app" ]]; then
|
|
echo "ERROR: TARGET=app but no update method was explicitly selected for CT $VMID." >&2
|
|
exit 5
|
|
else
|
|
echo "No app update method available in this CT — skipping app update step."
|
|
echo
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# 7) If either branch failed, abort here BEFORE the optional reboot so
|
|
# the CT stays in the pre-update state and the user can inspect it.
|
|
if (( OS_FAILED || APP_FAILED )); then
|
|
echo "=== Update FAILED. ==="
|
|
exit 4
|
|
fi
|
|
|
|
# 8) Optional post-update reboot. Handy for kernel/library upgrades and
|
|
# OCI-style CTs whose PID 1 is a user entrypoint (a plain `apt
|
|
# upgrade` doesn't restart the app; a CT reboot does).
|
|
if [[ "$RESTART" == "1" ]]; then
|
|
echo "--- Rebooting CT $VMID ---"
|
|
if ! pct reboot "$VMID"; then
|
|
echo "ERROR: pct reboot failed." >&2
|
|
exit 6
|
|
fi
|
|
fi
|
|
|
|
FINISHED_AT=$(date -Iseconds)
|
|
echo
|
|
echo "=== Update complete — CT $VMID ==="
|
|
echo "Started: $STARTED_AT"
|
|
echo "Finished: $FINISHED_AT"
|
|
exit 0
|