mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-09 06:56:37 +00:00
The installation record of an OCI application travels with its container: a copy inside the container and another in /etc/pve, written together with the one kept on the host. A container restored on a newly installed Proxmox, restored with another ID or moved to another node of a cluster is recognised and registered again, with its private network, hookscript, Rclone mount, host firewall rule and NVIDIA runtime. The Monitor offers the same recovery from the Updates tab. AMD GPUs are offered by generation. A GPU the ROCm image supports takes the profile as it is; one of a supported family (Radeon 680M, 780M) is an experimental option that asks for confirmation and is never proposed; an older one is not offered. The GPU is checked with a real inference before the installation accepts it. Recreate changes what runs recognition in an installed Immich, between the CPU and a GPU of the host. Updates: - A failed update that is restored and checked removes its temporary container and the disks of the failed attempt. - Every container volume is part of the backups, so Jellyfin, Plex and Hugo update with their default installation. - An image published with a Docker-format manifest is recognised by its layers and build time and updates. - The Proxmox notes of a multi-container application link to its LAN address.
142 lines
4.6 KiB
Bash
Executable File
142 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
|
|
|
die() {
|
|
printf 'ERROR: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
find_snippet_storage() {
|
|
local storage
|
|
if pvesm status --content snippets 2>/dev/null \
|
|
| awk 'NR > 1 && $1 == "local" && $3 == "active" {found=1} END {exit !found}'; then
|
|
printf 'local'
|
|
return
|
|
fi
|
|
storage=$(pvesm status --content snippets 2>/dev/null \
|
|
| awk 'NR > 1 && $3 == "active" {print $1; exit}')
|
|
if [[ -z $storage ]]; then
|
|
# A new Proxmox installation accepts no snippets on any storage.
|
|
enable_local_snippets || die "No active storage supports snippets"
|
|
storage=local
|
|
fi
|
|
printf '%s' "$storage"
|
|
}
|
|
|
|
enable_local_snippets() {
|
|
local content
|
|
content=$(pvesh get /storage/local --output-format json 2>/dev/null | jq -r '.content // empty')
|
|
[[ -n $content ]] || return 1
|
|
pvesm set local --content "${content},snippets" >/dev/null 2>&1 || return 1
|
|
pvesm status --content snippets 2>/dev/null \
|
|
| awk 'NR > 1 && $1 == "local" && $3 == "active" {found=1} END {exit !found}'
|
|
}
|
|
|
|
install_hook() {
|
|
local main_id=${1:?missing main VMID} source_config=${2:?missing lifecycle JSON}
|
|
local storage hook_volume hook_path target_config
|
|
[[ $main_id =~ ^[0-9]+$ ]] || die "Invalid main VMID"
|
|
jq -e '
|
|
.schema == 1 and
|
|
(.dependencies | type == "array") and
|
|
(.dependencies | length > 0) and
|
|
(all(.dependencies[];
|
|
(.vmid | type == "number") and
|
|
(.label | type == "string") and
|
|
(.healthcheck.type | IN("exec", "http", "running")) and
|
|
(.healthcheck.timeout_seconds | type == "number")
|
|
))
|
|
' "$source_config" >/dev/null || die "Invalid dependency contract"
|
|
|
|
storage=$(find_snippet_storage)
|
|
hook_volume="${storage}:snippets/proxmenux-stack-dependencies.sh"
|
|
hook_path=$(pvesm path "$hook_volume")
|
|
install -D -m 0755 "$0" "$hook_path"
|
|
|
|
target_config="/etc/pve/priv/proxmenux-stack-${main_id}.json"
|
|
umask 077
|
|
cat "$source_config" >"$target_config"
|
|
pct set "$main_id" --hookscript "$hook_volume" >/dev/null
|
|
printf 'Proxmox hookscript installed: CT %s starts its dependencies through %s\n' \
|
|
"$main_id" "$hook_volume"
|
|
}
|
|
|
|
dependency_is_healthy() {
|
|
local id=$1 healthcheck=$2 type url
|
|
type=$(jq -r '.type' <<<"$healthcheck")
|
|
case "$type" in
|
|
running)
|
|
[[ $(pct status "$id" 2>/dev/null || true) == "status: running" ]]
|
|
;;
|
|
exec)
|
|
local -a command=()
|
|
mapfile -t command < <(jq -r '.argv[]' <<<"$healthcheck")
|
|
((${#command[@]} > 0)) || return 1
|
|
pct exec "$id" -- "${command[@]}" >/dev/null 2>&1
|
|
;;
|
|
http)
|
|
url=$(jq -r '.url' <<<"$healthcheck")
|
|
curl -fsS --max-time 3 "$url" >/dev/null 2>&1
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
start_dependencies() {
|
|
local main_id=$1 config="/etc/pve/priv/proxmenux-stack-${1}.json"
|
|
local encoded dependency id label healthcheck timeout elapsed
|
|
[[ -r $config ]] || die "Missing dependency contract for main CT $main_id"
|
|
|
|
exec 9>"/run/lock/proxmenux-stack-${main_id}.lock"
|
|
flock 9
|
|
while IFS= read -r encoded; do
|
|
[[ -n $encoded ]] || continue
|
|
dependency=$(base64 -d <<<"$encoded")
|
|
id=$(jq -r '.vmid' <<<"$dependency")
|
|
label=$(jq -r '.label' <<<"$dependency")
|
|
healthcheck=$(jq -c '.healthcheck' <<<"$dependency")
|
|
timeout=$(jq -r '.healthcheck.timeout_seconds' <<<"$dependency")
|
|
[[ $id =~ ^[0-9]+$ && $timeout =~ ^[0-9]+$ && $timeout -gt 0 ]] \
|
|
|| die "Invalid dependency in $config"
|
|
pct config "$id" >/dev/null 2>&1 \
|
|
|| die "Dependency $label (CT $id) does not exist"
|
|
|
|
if [[ $(pct status "$id" 2>/dev/null || true) != "status: running" ]]; then
|
|
printf 'Starting dependency %s (CT %s)...\n' "$label" "$id"
|
|
pct start "$id" || die "Could not start $label (CT $id)"
|
|
else
|
|
printf 'Dependency %s (CT %s) was already running.\n' "$label" "$id"
|
|
fi
|
|
|
|
elapsed=0
|
|
while (( elapsed < timeout )); do
|
|
if dependency_is_healthy "$id" "$healthcheck"; then
|
|
printf 'Dependency %s (CT %s): ready.\n' "$label" "$id"
|
|
break
|
|
fi
|
|
[[ $(pct status "$id" 2>/dev/null || true) == "status: running" ]] \
|
|
|| die "$label (CT $id) stopped while starting"
|
|
sleep 2
|
|
elapsed=$((elapsed + 2))
|
|
done
|
|
(( elapsed < timeout )) \
|
|
|| die "$label (CT $id) did not pass its health check within ${timeout}s"
|
|
done < <(jq -r '.dependencies[] | @base64' "$config")
|
|
}
|
|
|
|
if [[ ${1:-} == "--install" ]]; then
|
|
shift
|
|
install_hook "$@"
|
|
exit 0
|
|
fi
|
|
|
|
vmid=${1:?missing VMID}
|
|
phase=${2:?missing lifecycle phase}
|
|
case "$phase" in
|
|
pre-start) start_dependencies "$vmid" ;;
|
|
post-start|pre-stop|post-stop) ;;
|
|
*) die "Unknown lifecycle phase: $phase" ;;
|
|
esac
|