From a5c60d456ea43a6ea1e5a471a1e2ee22c20b48f8 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:40:47 +0200 Subject: [PATCH 01/21] test: resolve fallback templates from the linuxcontainers catalog The fallback only knew what arm64_template_variant() hardcoded: debian 12/13, ubuntu 24.04/26.04, alpine. Everything else exited 225, and the Jenkins URL it built is a legacy endpoint. Resolves against the simplestreams index instead, which is what Incus already uses. The index carries aliases (debian/13 -> trixie), so the mapping table is gone rather than extended. Opens up 22 distributions and 42 arm64 images against 2 arm64 templates in a live pveam catalog: almalinux, alt, amazonlinux, archlinux, busybox, centos, devuan, kali, mint, nixos, openeuler, opensuse, openwrt, oracle, plamo, rockylinux, slackware, voidlinux and the four already covered. Only reached when pveam has nothing, so amd64 behaviour is unchanged for anything Proxmox ships. Needs jq and installs it when missing. Verified against the live catalog: lookup returns correct paths for debian, ubuntu, alpine, rockylinux, fedora, archlinux and voidlinux, refuses unknown releases, and the resulting URLs are real tarballs (debian trixie arm64 86 MB, rockylinux 9 arm64 99 MB). Not run on a Proxmox host. --- pve/backend.func | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/pve/backend.func b/pve/backend.func index d64d9f7..2d417fb 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -2350,20 +2350,21 @@ create_lxc_container() { ARCH="$(dpkg --print-architecture)" # Maps OS type + version to the release variant name used by ARM64 template sources. - arm64_template_variant() { - case "$1:$2" in - debian:12) echo "bookworm" ;; - debian:13) echo "trixie" ;; - debian:) echo "$DEBIAN_DEFAULT_CODENAME" ;; - - ubuntu:24.04) echo "noble" ;; - ubuntu:26.04) echo "questing" ;; - ubuntu:) echo "$UBUNTU_DEFAULT_CODENAME" ;; - - alpine:*) echo "${2:-$ALPINE_DEFAULT_VERSION}" ;; - - *) return 1 ;; - esac + _lxc_catalog_path() { + local os="${1,,}" ver="$2" arch="$3" + command -v jq >/dev/null 2>&1 || { + $STD apt-get update + $STD apt-get install -y jq || return 1 + } + curl -fsSL --max-time 30 "https://images.linuxcontainers.org/streams/v1/images.json" 2>/dev/null | + jq -r --arg os "$os" --arg ver "$ver" --arg arch "$arch" ' + .products | to_entries[] + | select(.value.arch == $arch and .value.variant == "default") + | select((.value.os | ascii_downcase) == $os) + | select(.value.release == $ver or ((.value.aliases // "") | split(",") | index($os + "/" + $ver))) + | .value.versions | to_entries | sort_by(.key) | last + | .value.items["root.tar.xz"].path // empty + ' 2>/dev/null | head -1 } # Downloads an ARM64 LXC rootfs template to $1. @@ -2400,7 +2401,7 @@ create_lxc_container() { exit 217 } - url="https://jenkins.linuxcontainers.org/job/image-${PCT_OSTYPE}/architecture=arm64,release=${CUSTOM_TEMPLATE_VARIANT},variant=default/lastStableBuild/artifact/rootfs.tar.xz" + url="https://images.linuxcontainers.org/${CUSTOM_TEMPLATE_PATH}" msg_info "Downloading ${PCT_OSTYPE^} ${CUSTOM_TEMPLATE_VARIANT} ARM64 template" local patched="${dest%/*}/.${dest##*/}.patched" @@ -2611,16 +2612,19 @@ create_lxc_container() { # Template discovery & validation # ------------------------------------------------------------------------------ CUSTOM_TEMPLATE_VARIANT="" + CUSTOM_TEMPLATE_PATH="" if [[ "$ARCH" == "arm64" ]] && ! _pveam_offers_template; then msg_info "No Proxmox template for ${PCT_OSTYPE} on ${ARCH}, using linuxcontainers.org" - CUSTOM_TEMPLATE_VARIANT=$(arm64_template_variant "$PCT_OSTYPE" "${PCT_OSVERSION:-}") || { - msg_error "No ARM64 template mapping for ${PCT_OSTYPE} ${PCT_OSVERSION:-latest}" + CUSTOM_TEMPLATE_PATH=$(_lxc_catalog_path "$PCT_OSTYPE" "${PCT_OSVERSION:-}" "$ARCH") + [[ -n "$CUSTOM_TEMPLATE_PATH" ]] || { + msg_error "linuxcontainers.org has no ${PCT_OSTYPE} ${PCT_OSVERSION:-latest} for ${ARCH}" exit 225 } - TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-rootfs.tar.xz" + CUSTOM_TEMPLATE_VARIANT=$(awk -F/ '{print $3}' <<<"$CUSTOM_TEMPLATE_PATH") + TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-${ARCH}-rootfs.tar.xz" TEMPLATE_SOURCE="custom-arm64" # Resolve template path From 9c22b37b70db4707fd57865158480c91c5cb897d Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:16:56 +0200 Subject: [PATCH 02/21] Find the images a distro actually publishes, not just the default variant Gentoo ships only openrc and systemd, so a hardcoded variant == "default" filter could never find it. Nixos publishes a squashfs where everyone else has a tarball. Take a per-distro variant preference list, and fall back from root.tar.xz to root.squashfs, unpacking that into the tarball pct wants. Two things the catalog switch had lost on the way: an empty version resolved to nothing, because the hardcoded codename table went away with it -- the UI defaults cover debian/ubuntu/alpine again, and a distro with a single release answers for itself rather than being guessed at. And fetching the index is now separate from querying it, so an unreachable catalog reports as a network failure instead of claiming the image does not exist. --- pve/backend.func | 136 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 12 deletions(-) diff --git a/pve/backend.func b/pve/backend.func index 2d417fb..1b1dff6 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -2349,22 +2349,81 @@ create_lxc_container() { ARCH="$(dpkg --print-architecture)" - # Maps OS type + version to the release variant name used by ARM64 template sources. - _lxc_catalog_path() { - local os="${1,,}" ver="$2" arch="$3" + # A distro appears in the catalog under whichever variant names it actually + # builds. Nearly all use "default"; Gentoo publishes only openrc and systemd, + # so a hardcoded "default" filter made it permanently unfindable. First match + # wins, so the init system the engine expects leads. + _lxc_catalog_variants() { + case "${1,,}" in + gentoo) echo "openrc systemd" ;; + *) echo "default" ;; + esac + } + + # Pulls the catalog index to $1. Kept apart from the lookup so an unreachable + # catalog reports as a network failure instead of "no such image". + _lxc_catalog_fetch() { command -v jq >/dev/null 2>&1 || { $STD apt-get update $STD apt-get install -y jq || return 1 } - curl -fsSL --max-time 30 "https://images.linuxcontainers.org/streams/v1/images.json" 2>/dev/null | - jq -r --arg os "$os" --arg ver "$ver" --arg arch "$arch" ' + curl -fsSL --max-time 30 -o "${1:?index path}" \ + "https://images.linuxcontainers.org/streams/v1/images.json" + } + + # No version asked for. The catalog carries no stable/latest marker -- Debian + # lists forky (14, unreleased) beside trixie -- so picking by build date would + # hand out a testing release. Use the codenames the UI already defaults to, + # and otherwise only answer when the distro ships a single release at all. + _lxc_catalog_default_release() { + local os="${1,,}" arch="$2" variant="$3" index="$4" + case "$os" in + debian) + printf '%s' "${DEBIAN_DEFAULT_CODENAME:-}" + return + ;; + ubuntu) + printf '%s' "${UBUNTU_DEFAULT_CODENAME:-}" + return + ;; + alpine) + printf '%s' "${ALPINE_DEFAULT_VERSION:-}" + return + ;; + esac + jq -r --arg os "$os" --arg arch "$arch" --arg variant "$variant" ' + [ .products[] + | select(.arch == $arch and .variant == $variant) + | select((.os | ascii_downcase) == $os) + | .release ] | unique + | if length == 1 then .[0] else empty end + ' "$index" 2>/dev/null + } + + # Echoes the catalog path of a usable rootfs image for os/version/arch. + # Prefers the plain tarball. A squashfs counts as usable -- it just has to be + # unpacked first -- and is what Nixos ships instead of a tarball. + _lxc_catalog_path() { + local os="${1,,}" ver="$2" arch="$3" index="${4:?index path}" + local variant want path + for variant in $(_lxc_catalog_variants "$os"); do + want="$ver" + [[ -z "$want" ]] && want="$(_lxc_catalog_default_release "$os" "$arch" "$variant" "$index")" + [[ -z "$want" ]] && continue + path=$(jq -r --arg os "$os" --arg ver "$want" --arg arch "$arch" --arg variant "$variant" ' .products | to_entries[] - | select(.value.arch == $arch and .value.variant == "default") + | select(.value.arch == $arch and .value.variant == $variant) | select((.value.os | ascii_downcase) == $os) | select(.value.release == $ver or ((.value.aliases // "") | split(",") | index($os + "/" + $ver))) - | .value.versions | to_entries | sort_by(.key) | last - | .value.items["root.tar.xz"].path // empty - ' 2>/dev/null | head -1 + | .value.versions | to_entries | sort_by(.key) | last | .value.items + | (.["root.tar.xz"].path // .["root.squashfs"].path // empty) + ' "$index" 2>/dev/null | head -1) + if [[ -n "$path" ]]; then + printf '%s' "$path" + return 0 + fi + done + return 1 } # Downloads an ARM64 LXC rootfs template to $1. @@ -2392,6 +2451,50 @@ create_lxc_container() { grep -qE "^${search}.*${pattern}" } + # pct wants a rootfs tarball, and a few distros (Nixos) publish only a + # squashfs. Unpacking beside the destination rather than in /tmp keeps a + # multi-GB rootfs off the root filesystem, and xz -1 because this recompresses + # what upstream already compressed once -- speed beats a smaller cache file. + _lxc_squashfs_to_tarball() { + local url="$1" dest="$2" sqfs rootdir rc=0 + + command -v unsquashfs >/dev/null 2>&1 || { + $STD apt-get update + $STD apt-get install -y squashfs-tools || { + msg_error "squashfs-tools is required to unpack the ${PCT_OSTYPE} template" + return 1 + } + } + + sqfs="${dest%.tar.xz}.squashfs" + if ! curl -fsSL -o "$sqfs" "$url"; then + rm -f "$sqfs" + msg_error "Failed to download ARM64 template from: $url" + return 1 + fi + + rootdir="$(mktemp -d "${dest%/*}/.unsquash.XXXXXX")" || { + rm -f "$sqfs" + msg_error "Cannot create unpack directory next to $dest" + return 1 + } + + if ! $STD unsquashfs -f -d "$rootdir" "$sqfs"; then + msg_error "Failed to unpack squashfs image" + rc=1 + else + tar -C "$rootdir" -cf - . | xz -1 -T0 -c >"$dest" + if ((PIPESTATUS[0] != 0 || PIPESTATUS[1] != 0)); then + msg_error "Failed to repack squashfs image as a tarball" + rc=1 + fi + fi + + rm -rf "$rootdir" "$sqfs" + ((rc == 0)) || rm -f "$dest" + return "$rc" + } + # Only reached when pveam has nothing for this arch. download_arm64_template() { local dest="$1" url @@ -2410,7 +2513,9 @@ create_lxc_container() { if [[ -f "$patched" ]] && [[ -s "$dest" ]] && xz -t "$dest" 2>/dev/null; then exit 0 fi - if ! curl -fsSL -o "$dest" "$url"; then + if [[ "$url" == *.squashfs ]]; then + _lxc_squashfs_to_tarball "$url" "$dest" || exit 208 + elif ! curl -fsSL -o "$dest" "$url"; then msg_error "Failed to download ARM64 template from: $url" exit 208 fi @@ -2617,11 +2722,18 @@ create_lxc_container() { if [[ "$ARCH" == "arm64" ]] && ! _pveam_offers_template; then msg_info "No Proxmox template for ${PCT_OSTYPE} on ${ARCH}, using linuxcontainers.org" - CUSTOM_TEMPLATE_PATH=$(_lxc_catalog_path "$PCT_OSTYPE" "${PCT_OSVERSION:-}" "$ARCH") - [[ -n "$CUSTOM_TEMPLATE_PATH" ]] || { + _lxc_catalog_index="$(mktemp)" + if ! _lxc_catalog_fetch "$_lxc_catalog_index"; then + rm -f "$_lxc_catalog_index" + msg_error "Could not reach the linuxcontainers.org image catalog" + exit 225 + fi + CUSTOM_TEMPLATE_PATH=$(_lxc_catalog_path "$PCT_OSTYPE" "${PCT_OSVERSION:-}" "$ARCH" "$_lxc_catalog_index") || { + rm -f "$_lxc_catalog_index" msg_error "linuxcontainers.org has no ${PCT_OSTYPE} ${PCT_OSVERSION:-latest} for ${ARCH}" exit 225 } + rm -f "$_lxc_catalog_index" CUSTOM_TEMPLATE_VARIANT=$(awk -F/ '{print $3}' <<<"$CUSTOM_TEMPLATE_PATH") TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-${ARCH}-rootfs.tar.xz" From 6729e36a2f44784f5406dde61a7c68ebee563182 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:23:18 +0200 Subject: [PATCH 03/21] Fix script slug generation for descriptions Update container and VM description builders to derive script slugs from `SCRIPT_SLUG` or `NSAPP` before falling back to `APP`, matching how scripts are identified elsewhere. Slugs are now lowercased with spaces removed (not dashed), preventing broken links like `arch-linux` and preserving VM-specific suffixes such as `-vm`. --- pve/backend.func | 6 +++++- pve/vm-core.func | 10 ++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pve/backend.func b/pve/backend.func index 1b1dff6..b8ce377 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -3296,8 +3296,12 @@ description() { # every Incus container to a repository it has nothing to do with. local repo_slug="${REPO_SLUG:-community-scripts/ProxmoxVE}" local repo_url="https://github.com/${repo_slug}" + # NSAPP is the slug the website serves, and what menu.func and the update + # entrypoint already look the app up by. Dashing the spaces instead of + # dropping them pointed "Arch Linux" at /scripts/arch-linux, which 404s. local script_slug - script_slug="$(printf '%s' "${APP}" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')" + script_slug="${SCRIPT_SLUG:-${NSAPP:-$APP}}" + script_slug="$(printf '%s' "$script_slug" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" # A script under test asks for feedback here too: the description shows in the # Proxmox UI without entering the container and outlives the install output. diff --git a/pve/vm-core.func b/pve/vm-core.func index 1461a5d..7e0577b 100644 --- a/pve/vm-core.func +++ b/pve/vm-core.func @@ -1793,10 +1793,12 @@ check_hostname_conflict() { } set_description() { - local app_name script_slug script_url donate_url - app_name=$(echo "${APP,,}" | tr ' ' '-') - script_slug="${SCRIPT_SLUG:-${app_name}}" - script_slug="$(echo "$script_slug" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')" + local script_slug script_url donate_url + # A VM script sets NSAPP itself (archlinux-vm, haos-vm) and that is the slug + # the website serves. Rebuilding it from APP dropped the -vm suffix and dashed + # the spaces, pointing "Arch Linux" at /scripts/arch-linux, which 404s. + script_slug="${SCRIPT_SLUG:-${NSAPP:-$APP}}" + script_slug="$(echo "$script_slug" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" script_url="https://community-scripts.org/scripts/${script_slug}" donate_url="https://community-scripts.org/donate" local description_title="${APP:-${NSAPP} VM}" From aead01443eeb40adabf57220d02875001b4b7cfe Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:40:47 +0200 Subject: [PATCH 04/21] test: resolve fallback templates from the linuxcontainers catalog The fallback only knew what arm64_template_variant() hardcoded: debian 12/13, ubuntu 24.04/26.04, alpine. Everything else exited 225, and the Jenkins URL it built is a legacy endpoint. Resolves against the simplestreams index instead, which is what Incus already uses. The index carries aliases (debian/13 -> trixie), so the mapping table is gone rather than extended. Opens up 22 distributions and 42 arm64 images against 2 arm64 templates in a live pveam catalog: almalinux, alt, amazonlinux, archlinux, busybox, centos, devuan, kali, mint, nixos, openeuler, opensuse, openwrt, oracle, plamo, rockylinux, slackware, voidlinux and the four already covered. Only reached when pveam has nothing, so amd64 behaviour is unchanged for anything Proxmox ships. Needs jq and installs it when missing. Verified against the live catalog: lookup returns correct paths for debian, ubuntu, alpine, rockylinux, fedora, archlinux and voidlinux, refuses unknown releases, and the resulting URLs are real tarballs (debian trixie arm64 86 MB, rockylinux 9 arm64 99 MB). Not run on a Proxmox host. --- pve/backend.func | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/pve/backend.func b/pve/backend.func index e1b893f..99a42af 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -2350,20 +2350,21 @@ create_lxc_container() { ARCH="$(dpkg --print-architecture)" # Maps OS type + version to the release variant name used by ARM64 template sources. - arm64_template_variant() { - case "$1:$2" in - debian:12) echo "bookworm" ;; - debian:13) echo "trixie" ;; - debian:) echo "$DEBIAN_DEFAULT_CODENAME" ;; - - ubuntu:24.04) echo "noble" ;; - ubuntu:26.04) echo "questing" ;; - ubuntu:) echo "$UBUNTU_DEFAULT_CODENAME" ;; - - alpine:*) echo "${2:-$ALPINE_DEFAULT_VERSION}" ;; - - *) return 1 ;; - esac + _lxc_catalog_path() { + local os="${1,,}" ver="$2" arch="$3" + command -v jq >/dev/null 2>&1 || { + $STD apt-get update + $STD apt-get install -y jq || return 1 + } + curl -fsSL --max-time 30 "https://images.linuxcontainers.org/streams/v1/images.json" 2>/dev/null | + jq -r --arg os "$os" --arg ver "$ver" --arg arch "$arch" ' + .products | to_entries[] + | select(.value.arch == $arch and .value.variant == "default") + | select((.value.os | ascii_downcase) == $os) + | select(.value.release == $ver or ((.value.aliases // "") | split(",") | index($os + "/" + $ver))) + | .value.versions | to_entries | sort_by(.key) | last + | .value.items["root.tar.xz"].path // empty + ' 2>/dev/null | head -1 } # Downloads an ARM64 LXC rootfs template to $1. @@ -2400,7 +2401,7 @@ create_lxc_container() { exit 217 } - url="https://jenkins.linuxcontainers.org/job/image-${PCT_OSTYPE}/architecture=arm64,release=${CUSTOM_TEMPLATE_VARIANT},variant=default/lastStableBuild/artifact/rootfs.tar.xz" + url="https://images.linuxcontainers.org/${CUSTOM_TEMPLATE_PATH}" msg_info "Downloading ${PCT_OSTYPE^} ${CUSTOM_TEMPLATE_VARIANT} ARM64 template" local patched="${dest%/*}/.${dest##*/}.patched" @@ -2611,16 +2612,19 @@ create_lxc_container() { # Template discovery & validation # ------------------------------------------------------------------------------ CUSTOM_TEMPLATE_VARIANT="" + CUSTOM_TEMPLATE_PATH="" if [[ "$ARCH" == "arm64" ]] && ! _pveam_offers_template; then msg_info "No Proxmox template for ${PCT_OSTYPE} on ${ARCH}, using linuxcontainers.org" - CUSTOM_TEMPLATE_VARIANT=$(arm64_template_variant "$PCT_OSTYPE" "${PCT_OSVERSION:-}") || { - msg_error "No ARM64 template mapping for ${PCT_OSTYPE} ${PCT_OSVERSION:-latest}" + CUSTOM_TEMPLATE_PATH=$(_lxc_catalog_path "$PCT_OSTYPE" "${PCT_OSVERSION:-}" "$ARCH") + [[ -n "$CUSTOM_TEMPLATE_PATH" ]] || { + msg_error "linuxcontainers.org has no ${PCT_OSTYPE} ${PCT_OSVERSION:-latest} for ${ARCH}" exit 225 } - TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-rootfs.tar.xz" + CUSTOM_TEMPLATE_VARIANT=$(awk -F/ '{print $3}' <<<"$CUSTOM_TEMPLATE_PATH") + TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-${ARCH}-rootfs.tar.xz" TEMPLATE_SOURCE="custom-arm64" # Resolve template path From ac005d56d0940d50a822380022babcc5d1ae2853 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:16:56 +0200 Subject: [PATCH 05/21] Find the images a distro actually publishes, not just the default variant Gentoo ships only openrc and systemd, so a hardcoded variant == "default" filter could never find it. Nixos publishes a squashfs where everyone else has a tarball. Take a per-distro variant preference list, and fall back from root.tar.xz to root.squashfs, unpacking that into the tarball pct wants. Two things the catalog switch had lost on the way: an empty version resolved to nothing, because the hardcoded codename table went away with it -- the UI defaults cover debian/ubuntu/alpine again, and a distro with a single release answers for itself rather than being guessed at. And fetching the index is now separate from querying it, so an unreachable catalog reports as a network failure instead of claiming the image does not exist. --- pve/backend.func | 136 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 12 deletions(-) diff --git a/pve/backend.func b/pve/backend.func index 99a42af..b8ce377 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -2349,22 +2349,81 @@ create_lxc_container() { ARCH="$(dpkg --print-architecture)" - # Maps OS type + version to the release variant name used by ARM64 template sources. - _lxc_catalog_path() { - local os="${1,,}" ver="$2" arch="$3" + # A distro appears in the catalog under whichever variant names it actually + # builds. Nearly all use "default"; Gentoo publishes only openrc and systemd, + # so a hardcoded "default" filter made it permanently unfindable. First match + # wins, so the init system the engine expects leads. + _lxc_catalog_variants() { + case "${1,,}" in + gentoo) echo "openrc systemd" ;; + *) echo "default" ;; + esac + } + + # Pulls the catalog index to $1. Kept apart from the lookup so an unreachable + # catalog reports as a network failure instead of "no such image". + _lxc_catalog_fetch() { command -v jq >/dev/null 2>&1 || { $STD apt-get update $STD apt-get install -y jq || return 1 } - curl -fsSL --max-time 30 "https://images.linuxcontainers.org/streams/v1/images.json" 2>/dev/null | - jq -r --arg os "$os" --arg ver "$ver" --arg arch "$arch" ' + curl -fsSL --max-time 30 -o "${1:?index path}" \ + "https://images.linuxcontainers.org/streams/v1/images.json" + } + + # No version asked for. The catalog carries no stable/latest marker -- Debian + # lists forky (14, unreleased) beside trixie -- so picking by build date would + # hand out a testing release. Use the codenames the UI already defaults to, + # and otherwise only answer when the distro ships a single release at all. + _lxc_catalog_default_release() { + local os="${1,,}" arch="$2" variant="$3" index="$4" + case "$os" in + debian) + printf '%s' "${DEBIAN_DEFAULT_CODENAME:-}" + return + ;; + ubuntu) + printf '%s' "${UBUNTU_DEFAULT_CODENAME:-}" + return + ;; + alpine) + printf '%s' "${ALPINE_DEFAULT_VERSION:-}" + return + ;; + esac + jq -r --arg os "$os" --arg arch "$arch" --arg variant "$variant" ' + [ .products[] + | select(.arch == $arch and .variant == $variant) + | select((.os | ascii_downcase) == $os) + | .release ] | unique + | if length == 1 then .[0] else empty end + ' "$index" 2>/dev/null + } + + # Echoes the catalog path of a usable rootfs image for os/version/arch. + # Prefers the plain tarball. A squashfs counts as usable -- it just has to be + # unpacked first -- and is what Nixos ships instead of a tarball. + _lxc_catalog_path() { + local os="${1,,}" ver="$2" arch="$3" index="${4:?index path}" + local variant want path + for variant in $(_lxc_catalog_variants "$os"); do + want="$ver" + [[ -z "$want" ]] && want="$(_lxc_catalog_default_release "$os" "$arch" "$variant" "$index")" + [[ -z "$want" ]] && continue + path=$(jq -r --arg os "$os" --arg ver "$want" --arg arch "$arch" --arg variant "$variant" ' .products | to_entries[] - | select(.value.arch == $arch and .value.variant == "default") + | select(.value.arch == $arch and .value.variant == $variant) | select((.value.os | ascii_downcase) == $os) | select(.value.release == $ver or ((.value.aliases // "") | split(",") | index($os + "/" + $ver))) - | .value.versions | to_entries | sort_by(.key) | last - | .value.items["root.tar.xz"].path // empty - ' 2>/dev/null | head -1 + | .value.versions | to_entries | sort_by(.key) | last | .value.items + | (.["root.tar.xz"].path // .["root.squashfs"].path // empty) + ' "$index" 2>/dev/null | head -1) + if [[ -n "$path" ]]; then + printf '%s' "$path" + return 0 + fi + done + return 1 } # Downloads an ARM64 LXC rootfs template to $1. @@ -2392,6 +2451,50 @@ create_lxc_container() { grep -qE "^${search}.*${pattern}" } + # pct wants a rootfs tarball, and a few distros (Nixos) publish only a + # squashfs. Unpacking beside the destination rather than in /tmp keeps a + # multi-GB rootfs off the root filesystem, and xz -1 because this recompresses + # what upstream already compressed once -- speed beats a smaller cache file. + _lxc_squashfs_to_tarball() { + local url="$1" dest="$2" sqfs rootdir rc=0 + + command -v unsquashfs >/dev/null 2>&1 || { + $STD apt-get update + $STD apt-get install -y squashfs-tools || { + msg_error "squashfs-tools is required to unpack the ${PCT_OSTYPE} template" + return 1 + } + } + + sqfs="${dest%.tar.xz}.squashfs" + if ! curl -fsSL -o "$sqfs" "$url"; then + rm -f "$sqfs" + msg_error "Failed to download ARM64 template from: $url" + return 1 + fi + + rootdir="$(mktemp -d "${dest%/*}/.unsquash.XXXXXX")" || { + rm -f "$sqfs" + msg_error "Cannot create unpack directory next to $dest" + return 1 + } + + if ! $STD unsquashfs -f -d "$rootdir" "$sqfs"; then + msg_error "Failed to unpack squashfs image" + rc=1 + else + tar -C "$rootdir" -cf - . | xz -1 -T0 -c >"$dest" + if ((PIPESTATUS[0] != 0 || PIPESTATUS[1] != 0)); then + msg_error "Failed to repack squashfs image as a tarball" + rc=1 + fi + fi + + rm -rf "$rootdir" "$sqfs" + ((rc == 0)) || rm -f "$dest" + return "$rc" + } + # Only reached when pveam has nothing for this arch. download_arm64_template() { local dest="$1" url @@ -2410,7 +2513,9 @@ create_lxc_container() { if [[ -f "$patched" ]] && [[ -s "$dest" ]] && xz -t "$dest" 2>/dev/null; then exit 0 fi - if ! curl -fsSL -o "$dest" "$url"; then + if [[ "$url" == *.squashfs ]]; then + _lxc_squashfs_to_tarball "$url" "$dest" || exit 208 + elif ! curl -fsSL -o "$dest" "$url"; then msg_error "Failed to download ARM64 template from: $url" exit 208 fi @@ -2617,11 +2722,18 @@ create_lxc_container() { if [[ "$ARCH" == "arm64" ]] && ! _pveam_offers_template; then msg_info "No Proxmox template for ${PCT_OSTYPE} on ${ARCH}, using linuxcontainers.org" - CUSTOM_TEMPLATE_PATH=$(_lxc_catalog_path "$PCT_OSTYPE" "${PCT_OSVERSION:-}" "$ARCH") - [[ -n "$CUSTOM_TEMPLATE_PATH" ]] || { + _lxc_catalog_index="$(mktemp)" + if ! _lxc_catalog_fetch "$_lxc_catalog_index"; then + rm -f "$_lxc_catalog_index" + msg_error "Could not reach the linuxcontainers.org image catalog" + exit 225 + fi + CUSTOM_TEMPLATE_PATH=$(_lxc_catalog_path "$PCT_OSTYPE" "${PCT_OSVERSION:-}" "$ARCH" "$_lxc_catalog_index") || { + rm -f "$_lxc_catalog_index" msg_error "linuxcontainers.org has no ${PCT_OSTYPE} ${PCT_OSVERSION:-latest} for ${ARCH}" exit 225 } + rm -f "$_lxc_catalog_index" CUSTOM_TEMPLATE_VARIANT=$(awk -F/ '{print $3}' <<<"$CUSTOM_TEMPLATE_PATH") TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-${ARCH}-rootfs.tar.xz" From 0fc1f962b0caf7e4bdbd65e4848ac6a8fa061671 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:19:11 +0200 Subject: [PATCH 06/21] Only offer another template version when there is one The version list was built after the offer was printed, so a distro pveam carries no template for at all -- Oracle, for one -- announced "select a different available version or press Enter to cancel" and then exited on the next line without ever prompting. Build the list first and only speak if it has something in it. The dead end now says which distro and architecture Proxmox came up empty for, and how to check the catalog. --- pve/backend.func | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pve/backend.func b/pve/backend.func index b8ce377..1f4fe18 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -2822,9 +2822,9 @@ create_lxc_container() { exit 225 fi - msg_warn "No ${TEMPLATE_PATTERN#-} template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" - msg_custom "ℹ️" "${YW}" "Select a different available version or press Enter to cancel." - + # Build the list before offering a choice: pveam carries no template at all + # for some distros, and announcing "select a different version" first left + # the user reading an offer that was never going to appear. AVAILABLE_VERSIONS=() mapfile -t AVAILABLE_VERSIONS < <( _pveam_available | @@ -2836,6 +2836,9 @@ create_lxc_container() { ) if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then + msg_warn "No ${TEMPLATE_PATTERN#-} template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" + msg_custom "ℹ️" "${YW}" "Select a different available version or press Enter to cancel." + echo "" echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" for i in "${!AVAILABLE_VERSIONS[@]}"; do @@ -2885,7 +2888,9 @@ create_lxc_container() { exit 0 fi else - msg_error "No ${PCT_OSTYPE} templates available at all" + msg_error "Proxmox offers no ${PCT_OSTYPE} template for ${ARCH}" + msg_custom "ℹ️" "${YW}" "Check what this host offers:" + echo -e "${TAB} pveam update && pveam available -section system | grep '${PCT_OSTYPE}-'" exit 225 fi fi From 9592069aaf638b5d557128877909e94188a9a93d Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:21:07 +0200 Subject: [PATCH 07/21] Track detected HW accel vendor in setup Expose `HWACCEL_VENDOR` consistently during hardware-acceleration setup so downstream logic can rely on the detected GPU vendor. Incus now sets vendor hints from available NVIDIA/AMD devices, and the shared hwaccel flow initializes vendor state, records when NVIDIA is actually usable, and derives the final vendor from selected GPU types (with usable NVIDIA taking priority). Also removes an outdated explanatory comment in `pve/vm-core.func`. --- incus/tools.func | 7 +++++++ lib/hwaccel.func | 20 ++++++++++++++++++++ pve/vm-core.func | 3 --- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/incus/tools.func b/incus/tools.func index 75debe1..1da68c4 100644 --- a/incus/tools.func +++ b/incus/tools.func @@ -114,6 +114,13 @@ update_motd_ip() { setup_hwaccel() { # GPU/device passthrough is applied at create time (incus config device add gpu …). # Inside the CT we only verify visibility and permissions. + export HWACCEL_VENDOR="none" + if ldconfig -p 2>/dev/null | grep -q 'libcuda\.so\.1' && compgen -G '/dev/nvidia[0-9]*' &>/dev/null; then + export HWACCEL_VENDOR="nvidia" + elif [[ -e /dev/kfd ]]; then + export HWACCEL_VENDOR="amd" + fi + if [[ -d /dev/dri ]] && compgen -G '/dev/dri/*' &>/dev/null; then msg_ok "Hardware acceleration devices present under /dev/dri" return 0 diff --git a/lib/hwaccel.func b/lib/hwaccel.func index 025dde4..9dff67d 100644 --- a/lib/hwaccel.func +++ b/lib/hwaccel.func @@ -50,6 +50,9 @@ _cs_apt_install_optional() { setup_hwaccel() { local service_user="${1:-}" + export HWACCEL_VENDOR="none" + _cs_nvidia_usable=0 + # Check if user explicitly disabled GPU in advanced settings # ENABLE_GPU is exported from build.func if [[ "${ENABLE_GPU:-no}" == "no" ]]; then @@ -314,6 +317,22 @@ setup_hwaccel() { # ═══════════════════════════════════════════════════════════════════════════ _setup_gpu_permissions "$in_ct" "$service_user" + local _gpu_type + for idx in "${SELECTED_INDICES[@]}"; do + _gpu_type="${GPU_TYPES[$idx]}" + if [[ "$_gpu_type" == "NVIDIA" && $_cs_nvidia_usable -eq 1 ]]; then + HWACCEL_VENDOR="nvidia" + break + fi + if [[ "$HWACCEL_VENDOR" == "none" ]]; then + case "$_gpu_type" in + AMD) HWACCEL_VENDOR="amd" ;; + INTEL*) HWACCEL_VENDOR="intel" ;; + esac + fi + done + export HWACCEL_VENDOR + cache_installed_version "hwaccel" "1.0" msg_ok "Setup Hardware Acceleration" } @@ -882,6 +901,7 @@ NVIDIA_PIN ldconfig -p 2>/dev/null | grep -q 'libcuda\.so\.1' && have_libcuda=1 if [[ $have_libcuda -eq 1 && ${#nvidia_nodes[@]} -gt 0 ]]; then + _cs_nvidia_usable=1 msg_ok "NVIDIA GPU configured (host driver ${nvidia_host_version}, ${#nvidia_nodes[@]} device nodes)" elif [[ ${#nvidia_nodes[@]} -eq 0 ]]; then msg_error "NVIDIA GPU not usable: no /dev/nvidia* device nodes in this container" diff --git a/pve/vm-core.func b/pve/vm-core.func index 7e0577b..c855d59 100644 --- a/pve/vm-core.func +++ b/pve/vm-core.func @@ -1794,9 +1794,6 @@ check_hostname_conflict() { set_description() { local script_slug script_url donate_url - # A VM script sets NSAPP itself (archlinux-vm, haos-vm) and that is the slug - # the website serves. Rebuilding it from APP dropped the -vm suffix and dashed - # the spaces, pointing "Arch Linux" at /scripts/arch-linux, which 404s. script_slug="${SCRIPT_SLUG:-${NSAPP:-$APP}}" script_slug="$(echo "$script_slug" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" script_url="https://community-scripts.org/scripts/${script_slug}" From 074f3cee26243b4ea967e48d55d26a5c0bf3442b Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:22:08 +0200 Subject: [PATCH 08/21] Reach the image catalog on every architecture, not just arm64 ARCH == arm64 stood in for "pveam has nothing", which held while the catalog was only ever consulted on ARM64 hosts. On amd64 it meant Proxmox's own catalog was the single source, so a distro pveam does not carry -- Oracle, Kali -- had no path at all and the run ended at "No oracle templates available at all". Every one of those five checks now asks whether a catalog template was actually resolved, and download_arm64_template, which is no longer arch-specific, is download_catalog_template. A catalog miss stops ending the run as well. Asking for a version that does not exist is far more common than asking for a distro nobody publishes, and only the pveam path can offer the versions this host does have -- so a miss falls through to it instead of exiting. --- pve/backend.func | 51 ++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/pve/backend.func b/pve/backend.func index 1f4fe18..dc91ac8 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -2426,7 +2426,7 @@ create_lxc_container() { return 1 } - # Downloads an ARM64 LXC rootfs template to $1. + # Downloads a linuxcontainers rootfs template to $1. # Does Proxmox offer this OS/version/arch? Local first, then the catalog. _pveam_offers_template() { local search pattern @@ -2469,7 +2469,7 @@ create_lxc_container() { sqfs="${dest%.tar.xz}.squashfs" if ! curl -fsSL -o "$sqfs" "$url"; then rm -f "$sqfs" - msg_error "Failed to download ARM64 template from: $url" + msg_error "Failed to download template from: $url" return 1 fi @@ -2496,7 +2496,7 @@ create_lxc_container() { } # Only reached when pveam has nothing for this arch. - download_arm64_template() { + download_catalog_template() { local dest="$1" url mkdir -p "$(dirname "$dest")" || { @@ -2506,7 +2506,7 @@ create_lxc_container() { url="https://images.linuxcontainers.org/${CUSTOM_TEMPLATE_PATH}" - msg_info "Downloading ${PCT_OSTYPE^} ${CUSTOM_TEMPLATE_VARIANT} ARM64 template" + msg_info "Downloading ${PCT_OSTYPE^} ${CUSTOM_TEMPLATE_VARIANT} template (${ARCH})" local patched="${dest%/*}/.${dest##*/}.patched" ( flock -x 200 @@ -2516,7 +2516,7 @@ create_lxc_container() { if [[ "$url" == *.squashfs ]]; then _lxc_squashfs_to_tarball "$url" "$dest" || exit 208 elif ! curl -fsSL -o "$dest" "$url"; then - msg_error "Failed to download ARM64 template from: $url" + msg_error "Failed to download template from: $url" exit 208 fi if ! tar -tJf "$dest" 2>/dev/null | grep -q '/etc/network/$'; then @@ -2528,7 +2528,7 @@ create_lxc_container() { rm -rf "$fixdir" else rm -rf "$fixdir" "$tmptar" - msg_error "Failed to patch ARM64 template (missing /etc/network)" + msg_error "Failed to patch template (missing /etc/network)" exit 208 fi fi @@ -2538,13 +2538,13 @@ create_lxc_container() { if [[ $dl_rc -ne 0 ]]; then exit "$dl_rc" fi - msg_ok "Downloaded ARM64 LXC template" + msg_ok "Downloaded LXC template from linuxcontainers.org" } download_template() { local dest="${1:-$TEMPLATE_PATH}" - if [[ "$ARCH" == "arm64" ]]; then - download_arm64_template "$dest" + if [[ -n "$CUSTOM_TEMPLATE_PATH" ]]; then + download_catalog_template "$dest" else pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 || { msg_error "Failed to download template '$TEMPLATE' to storage '$TEMPLATE_STORAGE'" @@ -2719,25 +2719,26 @@ create_lxc_container() { CUSTOM_TEMPLATE_VARIANT="" CUSTOM_TEMPLATE_PATH="" - if [[ "$ARCH" == "arm64" ]] && ! _pveam_offers_template; then - msg_info "No Proxmox template for ${PCT_OSTYPE} on ${ARCH}, using linuxcontainers.org" + if ! _pveam_offers_template; then + msg_info "No Proxmox template for ${PCT_OSTYPE} ${PCT_OSVERSION:-} on ${ARCH}, trying linuxcontainers.org" _lxc_catalog_index="$(mktemp)" if ! _lxc_catalog_fetch "$_lxc_catalog_index"; then - rm -f "$_lxc_catalog_index" - msg_error "Could not reach the linuxcontainers.org image catalog" - exit 225 + msg_warn "Could not reach the linuxcontainers.org image catalog" + elif ! CUSTOM_TEMPLATE_PATH=$(_lxc_catalog_path "$PCT_OSTYPE" "${PCT_OSVERSION:-}" "$ARCH" "$_lxc_catalog_index"); then + CUSTOM_TEMPLATE_PATH="" + msg_warn "linuxcontainers.org has no ${PCT_OSTYPE} ${PCT_OSVERSION:-latest} for ${ARCH}" fi - CUSTOM_TEMPLATE_PATH=$(_lxc_catalog_path "$PCT_OSTYPE" "${PCT_OSVERSION:-}" "$ARCH" "$_lxc_catalog_index") || { - rm -f "$_lxc_catalog_index" - msg_error "linuxcontainers.org has no ${PCT_OSTYPE} ${PCT_OSVERSION:-latest} for ${ARCH}" - exit 225 - } rm -f "$_lxc_catalog_index" + fi + # A catalog miss falls through to pveam rather than ending the run: asking for + # a version that does not exist is far more common than asking for a distro + # nobody publishes, and only the pveam path can offer what this host does have. + if [[ -n "$CUSTOM_TEMPLATE_PATH" ]]; then CUSTOM_TEMPLATE_VARIANT=$(awk -F/ '{print $3}' <<<"$CUSTOM_TEMPLATE_PATH") TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-${ARCH}-rootfs.tar.xz" - TEMPLATE_SOURCE="custom-arm64" + TEMPLATE_SOURCE="custom-catalog" # Resolve template path TEMPLATE_PATH="$(pvesm path "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" 2>/dev/null || true)" @@ -2753,11 +2754,11 @@ create_lxc_container() { # Download if missing, too small, or corrupt if [[ ! -f "$TEMPLATE_PATH" ]]; then - download_arm64_template "$TEMPLATE_PATH" + download_catalog_template "$TEMPLATE_PATH" elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]] || ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then msg_warn "Local template invalid - re-downloading." rm -f "$TEMPLATE_PATH" - download_arm64_template "$TEMPLATE_PATH" + download_catalog_template "$TEMPLATE_PATH" else msg_ok "Template ${BL}$TEMPLATE${CL} found locally." fi @@ -3070,7 +3071,7 @@ create_lxc_container() { download_template msg_ok "Template downloaded" elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ "$ARCH" == "arm64" || -n "$ONLINE_TEMPLATE" ]]; then + if [[ -n "$CUSTOM_TEMPLATE_PATH" || -n "$ONLINE_TEMPLATE" ]]; then msg_info "Template appears corrupted – re-downloading" rm -f "$TEMPLATE_PATH" download_template @@ -3156,8 +3157,8 @@ create_lxc_container() { if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then msg_ok "Trying local storage fallback" msg_info "Downloading template to local" - if [[ "$ARCH" == "arm64" ]]; then - download_arm64_template "$LOCAL_TEMPLATE_PATH" + if [[ -n "$CUSTOM_TEMPLATE_PATH" ]]; then + download_catalog_template "$LOCAL_TEMPLATE_PATH" else pveam download local "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 fi From 18d830edb91215c86d67c1d44f1cc5bfb13a4ff5 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:05:35 +0200 Subject: [PATCH 09/21] Translate the releases Proxmox and the image catalog disagree on The catalog calls Arch's rolling release current, Devuan 5 daedalus and CentOS 10 10-Stream, so those three resolved to nothing on either architecture while every other distro we ship worked. --- pve/backend.func | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pve/backend.func b/pve/backend.func index 8676092..0dc4b95 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -2441,11 +2441,32 @@ create_lxc_container() { # Echoes the catalog path of a usable rootfs image for os/version/arch. # Prefers the plain tarball. A squashfs counts as usable -- it just has to be # unpacked first -- and is what Nixos ships instead of a tarball. + # var_version has to keep matching the pveam template name (archlinux-base, devuan-5), + # and the catalog names those releases differently. + _lxc_catalog_release_alias() { + case "${1,,}:${2}" in + archlinux:base) printf 'current' ;; + devuan:5 | devuan:5.0) printf 'daedalus' ;; + *) printf '%s' "$2" ;; + esac + } + + # CentOS publishes 10 as "10-Stream". Only a suffix after a dash counts, so asking for 4 + # never lands on 43. + _lxc_catalog_suffixed_release() { + jq -r --arg os "${1,,}" --arg ver "$2" --arg arch "$3" --arg variant "$4" ' + [ .products[] + | select(.arch == $arch and .variant == $variant) + | select((.os | ascii_downcase) == $os) + | select(.release | startswith($ver + "-")) | .release ] | unique | last // empty + ' "$5" 2>/dev/null + } + _lxc_catalog_path() { local os="${1,,}" ver="$2" arch="$3" index="${4:?index path}" local variant want path for variant in $(_lxc_catalog_variants "$os"); do - want="$ver" + want="$(_lxc_catalog_release_alias "$os" "$ver")" [[ -z "$want" ]] && want="$(_lxc_catalog_default_release "$os" "$arch" "$variant" "$index")" [[ -z "$want" ]] && continue path=$(jq -r --arg os "$os" --arg ver "$want" --arg arch "$arch" --arg variant "$variant" ' @@ -2460,6 +2481,20 @@ create_lxc_container() { printf '%s' "$path" return 0 fi + want="$(_lxc_catalog_suffixed_release "$os" "$want" "$arch" "$variant" "$index")" + [[ -z "$want" ]] && continue + path=$(jq -r --arg os "$os" --arg ver "$want" --arg arch "$arch" --arg variant "$variant" ' + .products | to_entries[] + | select(.value.arch == $arch and .value.variant == $variant) + | select((.value.os | ascii_downcase) == $os) + | select(.value.release == $ver) + | .value.versions | to_entries | sort_by(.key) | last | .value.items + | (.["root.tar.xz"].path // .["root.squashfs"].path // empty) + ' "$index" 2>/dev/null | head -1) + if [[ -n "$path" ]]; then + printf '%s' "$path" + return 0 + fi done return 1 } From c1098e97433088ced5b5765d2912d5ee2bff3f24 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:40:48 +0200 Subject: [PATCH 10/21] Create Amazon Linux containers and keep the MOTD working without hostname PVE resolves no ostype for ID 'amzn' and its fallback detection finds nothing either, so pct create aborted before configuring anything; forcing ostype centos needs the same /etc/redhat-release that openEuler already gets. RHEL-family minimal images also ship no hostname binary, which broke the MOTD line and left self-signed certs without a CN. --- incus/tools.func | 2 +- lib/system.func | 7 +++++-- lxc/install.func | 2 +- pve/backend.func | 24 +++++++++++------------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/incus/tools.func b/incus/tools.func index 4696a13..4ae3abb 100644 --- a/incus/tools.func +++ b/incus/tools.func @@ -273,7 +273,7 @@ motd_ssh() { local ip ip=$(get_lxc_ip) cat </etc/motd - 🚀 Incus Container: $(hostname) + 🚀 Incus Container: $(uname -n) 🖥️ OS: $(grep ^PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"') 📡 IP Address: ${ip} EOF diff --git a/lib/system.func b/lib/system.func index 764ccc0..41e6eec 100644 --- a/lib/system.func +++ b/lib/system.func @@ -2098,8 +2098,11 @@ verify_gpg_fingerprint() { create_self_signed_cert() { local APP_NAME="${1:-${APPLICATION}}" local EXTRA_SAN="${2:-}" - local HOSTNAME="$(hostname -f)" - local IP="$(hostname -I | awk '{print $1}')" + # RHEL-family minimal images ship no hostname binary, which left CN and SAN empty. + local HOSTNAME IP + HOSTNAME="$(hostname -f 2>/dev/null || uname -n)" + IP="$(hostname -I 2>/dev/null | awk '{print $1}')" + [[ -n "$IP" ]] || IP="$(ip -4 route get 1 2>/dev/null | sed -n 's/.* src \([0-9.]\+\).*/\1/p' | head -1)" local APP_NAME_LC=$(echo "${APP_NAME,,}" | tr -d ' ') local CERT_DIR="/etc/ssl/${APP_NAME_LC}" local CERT_KEY="${CERT_DIR}/${APP_NAME_LC}.key" diff --git a/lxc/install.func b/lxc/install.func index 16c9a4d..c523828 100644 --- a/lxc/install.func +++ b/lxc/install.func @@ -1519,7 +1519,7 @@ EOF echo -e "${GATEWAY:-}${YW:-}Provided by: ${GN:-}community-scripts ORG ${YW:-}| GitHub: ${GN:-}https://github.com/community-scripts/ProxmoxVE${CL:-}" echo "" echo -e "${OS:-}${YW:-}OS: ${GN:-}${os_name} - Version: ${os_version}${CL:-}" -echo -e "${HOSTNAME:-}${YW:-}Hostname: ${GN:-}\$(hostname)${CL:-}" +echo -e "${HOSTNAME:-}${YW:-}Hostname: ${GN:-}\$(uname -n)${CL:-}" echo -e "${INFO:-}${YW:-}IP Address: ${GN:-}\$(ip -4 route get 1 2>/dev/null | sed -n 's/.* src \\([0-9.]\\+\\).*/\\1/p' | head -1)${CL:-}" EOF diff --git a/pve/backend.func b/pve/backend.func index 0dc4b95..04d0ab7 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -393,8 +393,9 @@ $PCT_OPTIONS_STRING" # PVE derives ostype from the template name, then rejects its own value when # reading the config back - which breaks every later pct call. + # Amazon Linux also needs it to be created at all: PVE resolves no ostype for ID 'amzn'. case "${var_os:-}" in - openeuler | rockylinux | almalinux) + openeuler | rockylinux | almalinux | amazonlinux) PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING -ostype centos" ;; @@ -1132,7 +1133,7 @@ EOF # 3. /etc/profile.d/99-pve-console-term.sh: re-asserts TERM=linux AFTER all other # profile.d scripts, so readline never thinks CPR is supported (fixes R;80R garbage) case "$var_os" in - fedora | rocky | rockylinux | alma | almalinux | centos | openeuler | opensuse | archlinux | arch) + fedora | rocky | rockylinux | alma | almalinux | centos | openeuler | amazonlinux | oraclelinux | opensuse | archlinux | arch) pct exec "$CTID" -- bash -c ' mkdir -p /etc/systemd/system/console-getty.service.d printf "[Service]\nEnvironment=TERM=linux\n" >/etc/systemd/system/console-getty.service.d/pve-console-term.conf @@ -1161,7 +1162,7 @@ PROFILE # Ensure curl is present for install.func bootstrap (most templates have it, but be safe) case "$var_os" in - fedora | rocky | rockylinux | alma | almalinux | centos | openeuler) + fedora | rocky | rockylinux | alma | almalinux | centos | openeuler | amazonlinux | oraclelinux) pct exec "$CTID" -- bash -c "command -v curl >/dev/null 2>&1 || (command -v dnf >/dev/null 2>&1 && dnf install -y curl >/dev/null 2>&1) || (command -v yum >/dev/null 2>&1 && yum install -y curl >/dev/null 2>&1)" || true ;; opensuse) @@ -3145,26 +3146,23 @@ create_lxc_container() { fi fi - # openEuler templates are detected by PVE as ostype 'centos' but ship without - # /etc/redhat-release, which makes PVE::LXC::Setup::post_create_hook abort with - # "can't open '/etc/redhat-release' - No such file or directory". - # Patch the cached template once to inject a CentOS-compatible release file. - if [[ "${var_os:-}" == "openeuler" ]]; then + # Both run as ostype centos, whose PVE plugin reads a /etc/redhat-release neither ships. + if [[ "${var_os:-}" == "openeuler" || "${var_os:-}" == "amazonlinux" ]]; then if ! tar -tf "$TEMPLATE_PATH" 2>/dev/null | grep -qE '^\.?/?etc/redhat-release$'; then - msg_info "Patching openEuler template (adding /etc/redhat-release)" + msg_info "Patching ${var_os} template (adding /etc/redhat-release)" local _oe_tmp _oe_tmp=$(mktemp -d) || _oe_tmp="" if [[ -n "$_oe_tmp" ]] && mkdir -p "$_oe_tmp/etc"; then # Content is parsed by PVE's CentOS setup plugin (expects " release ") - local _oe_ver="${var_version:-25.03}" - echo "CentOS Linux release ${_oe_ver} (openEuler)" >"$_oe_tmp/etc/redhat-release" + local _oe_ver="${var_version:-9}" + echo "CentOS Linux release ${_oe_ver} (${var_os})" >"$_oe_tmp/etc/redhat-release" if xz -dc "$TEMPLATE_PATH" >"$_oe_tmp/template.tar" 2>/dev/null && tar -C "$_oe_tmp" --append -f "$_oe_tmp/template.tar" etc/redhat-release 2>/dev/null && xz -f "$_oe_tmp/template.tar" 2>/dev/null && mv -f "$_oe_tmp/template.tar.xz" "$TEMPLATE_PATH"; then - msg_ok "Patched openEuler template" + msg_ok "Patched ${var_os} template" else - msg_warn "Failed to patch openEuler template – pct create may fail in post_create_hook" + msg_warn "Failed to patch ${var_os} template – pct create may fail in post_create_hook" fi rm -rf "$_oe_tmp" fi From 3b5af6dbd581ee19fa03d183ceefd097bc64c50f Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:00:16 +0200 Subject: [PATCH 11/21] Give the patched template a release PVE actually accepts PVE::LXC::Setup::CentOS rejects every version outside 5..10, so writing the real version into /etc/redhat-release made pct create fail with 'unsupported centos version' for Amazon Linux 2023 and, unnoticed until now, openEuler 25.03. The guard also has to read the file instead of only checking that it exists, or an already-patched cache keeps its rejected version forever. --- pve/backend.func | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pve/backend.func b/pve/backend.func index 04d0ab7..7f2c11d 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -3148,15 +3148,19 @@ create_lxc_container() { # Both run as ostype centos, whose PVE plugin reads a /etc/redhat-release neither ships. if [[ "${var_os:-}" == "openeuler" || "${var_os:-}" == "amazonlinux" ]]; then - if ! tar -tf "$TEMPLATE_PATH" 2>/dev/null | grep -qE '^\.?/?etc/redhat-release$'; then - msg_info "Patching ${var_os} template (adding /etc/redhat-release)" + local _oe_rel="" + _oe_rel="$(tar -xOf "$TEMPLATE_PATH" --wildcards '*etc/redhat-release' 2>/dev/null | head -1)" + # The content, not just the file: that plugin rejects every version outside 5..10. + if [[ ! "$_oe_rel" =~ release[[:space:]]+([5-9]|10)([.[:space:]]|$) ]]; then + msg_info "Patching ${var_os} template (/etc/redhat-release)" local _oe_tmp _oe_tmp=$(mktemp -d) || _oe_tmp="" if [[ -n "$_oe_tmp" ]] && mkdir -p "$_oe_tmp/etc"; then - # Content is parsed by PVE's CentOS setup plugin (expects " release ") - local _oe_ver="${var_version:-9}" - echo "CentOS Linux release ${_oe_ver} (${var_os})" >"$_oe_tmp/etc/redhat-release" + # 10, not the real version: both images configure their own network via systemd-networkd. + echo "CentOS Linux release 10 (${var_os})" >"$_oe_tmp/etc/redhat-release" if xz -dc "$TEMPLATE_PATH" >"$_oe_tmp/template.tar" 2>/dev/null && + { tar --delete -f "$_oe_tmp/template.tar" ./etc/redhat-release 2>/dev/null || + tar --delete -f "$_oe_tmp/template.tar" etc/redhat-release 2>/dev/null || true; } && tar -C "$_oe_tmp" --append -f "$_oe_tmp/template.tar" etc/redhat-release 2>/dev/null && xz -f "$_oe_tmp/template.tar" 2>/dev/null && mv -f "$_oe_tmp/template.tar.xz" "$TEMPLATE_PATH"; then From 4ae0ccd0a885891b602eff9ff7351359cb57ce34 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:21:49 +0200 Subject: [PATCH 12/21] Install dependencies with the container's own package manager --- lib/system.func | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lib/system.func b/lib/system.func index 0895c7a..ffbf08b 100644 --- a/lib/system.func +++ b/lib/system.func @@ -1181,6 +1181,41 @@ ensure_dependencies() { return 0 fi + # dnf/zypper/pacman/emerge: no cheap name-to-binary map, so go by command and + # let the manager skip whatever is already there. + local family="" + declare -F _cs_os_family >/dev/null 2>&1 && family="$(_cs_os_family)" + case "$family" in + rhel | suse | arch | gentoo) + local -a pm=() + case "$family" in + rhel) + if command -v dnf >/dev/null 2>&1; then pm=(dnf install -y); else pm=(yum install -y); fi + ;; + suse) pm=(zypper --non-interactive install --no-recommends) ;; + arch) pm=(pacman -S --noconfirm --needed) ;; + gentoo) pm=(emerge --quiet --noreplace) ;; + esac + + for dep in "${deps[@]}"; do + command -v "$dep" >/dev/null 2>&1 || missing+=("$dep") + done + ((${#missing[@]})) || return 0 + + $STD "${pm[@]}" "${missing[@]}" || { + local failed=() + for pkg in "${missing[@]}"; do + $STD "${pm[@]}" "$pkg" 2>/dev/null || failed+=("$pkg") + done + if ((${#failed[@]})); then + msg_error "Failed to install dependencies: ${failed[*]}" + return 1 + fi + } + return 0 + ;; + esac + # Debian/Ubuntu: Fast batch check using dpkg-query local installed_pkgs installed_pkgs=$(dpkg-query -W -f='${Package}\n' 2>/dev/null | sort -u) From 8ae4fa1b91597d67e2755b83e0a4e68aaa6d0309 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:47:17 +0200 Subject: [PATCH 13/21] Close the linuxcontainers.org lookup message The lookup opened a message block that only the found-locally path closed. After a download every later msg_ok resumed its spinner, which kept running into the install and flickered against the container's own spinner. --- pve/backend.func | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pve/backend.func b/pve/backend.func index 9d2053b..77dbe0a 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -3138,6 +3138,8 @@ create_lxc_container() { elif ! CUSTOM_TEMPLATE_PATH=$(_lxc_catalog_path "$PCT_OSTYPE" "${PCT_OSVERSION:-}" "$ARCH" "$_lxc_catalog_index"); then CUSTOM_TEMPLATE_PATH="" msg_warn "linuxcontainers.org has no ${PCT_OSTYPE} ${PCT_OSVERSION:-latest} for ${ARCH}" + else + msg_ok "Using the linuxcontainers.org ${PCT_OSTYPE} ${PCT_OSVERSION:-} image" fi rm -f "$_lxc_catalog_index" fi From 74635f5bfdc7120e871ee62dcc157aae56e676d7 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:35:27 +0200 Subject: [PATCH 14/21] Stop early when the image has no build for this architecture An alias resolves even when the image server has nothing for the host's architecture, so Amazon Linux on arm64 went through three failing launches. The resolver now checks the image's architecture and the run ends with exit 106. The retry list no longer tries the same image twice. --- incus/backend.func | 48 +++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/incus/backend.func b/incus/backend.func index f0282a1..48b5aa4 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -104,8 +104,18 @@ _incus_restart_ct() { [[ "$(incus_instance_field "${CT_NAME}" s)" == "RUNNING" ]] } +# An alias resolves whether or not the image exists for this host's +# architecture; the launch then fails with "architecture isn't supported". +_incus_image_fits_host() { + local arch + arch=$(incus image info "$1" 2>/dev/null | awk '/^Architecture:/ {print $2; exit}' || true) + [[ -n "$arch" ]] || return 2 + [[ "$arch" == "$(uname -m)" ]] +} + _incus_resolve_launch_image() { # Tries images:OS/VER (+ /cloud, codename alias, local cache). Sets INCUS_LAUNCH_IMAGE. + # Returns 2 when the image exists, but not for this architecture. local os="${IMAGE_OS:-debian}" ver="${IMAGE_VERSION:-13}" alias="" os="${os,,}" alias="$(incus_get_image_alias "$os" "$ver" 2>/dev/null || true)" @@ -129,28 +139,34 @@ _incus_resolve_launch_image() { for cand in "${candidates[@]}"; do if [[ -n "$local_aliases" ]] && grep -qxF "${cand#images:}" <<<"$local_aliases" && - incus image info "${cand#images:}" &>/dev/null; then + _incus_image_fits_host "${cand#images:}"; then INCUS_LAUNCH_IMAGE="${cand#images:}" return 0 fi done + local fit other_arch=0 for cand in "${candidates[@]}"; do - if incus image info "$cand" &>/dev/null; then + fit=0 + _incus_image_fits_host "$cand" || fit=$? + if ((fit == 0)); then INCUS_LAUNCH_IMAGE="$cand" return 0 fi + ((fit == 1)) && other_arch=1 done # Not on the image server under any name we know: take its newest version. local newest - newest=$(incus image alias list images: "${os}/" -f csv 2>/dev/null | cut -d, -f1 | - awk -F/ 'NF == 2 && $2 ~ /^[0-9][0-9.]*$/ {print $2}' | sort -uV | tail -n1 || true) - if [[ -n "$newest" ]] && incus image info "images:${os}/${newest}" &>/dev/null; then - msg_warn "${os} ${ver} is not on the image server; using ${os} ${newest}" - INCUS_LAUNCH_IMAGE="images:${os}/${newest}" - return 0 - fi + for newest in $(incus image alias list images: "${os}/" -f csv 2>/dev/null | cut -d, -f1 | + awk -F/ 'NF == 2 && $2 ~ /^[0-9][0-9.]*$/ {print $2}' | sort -urV || true); do + if _incus_image_fits_host "images:${os}/${newest}"; then + msg_warn "${os} ${ver} is not on the image server; using ${os} ${newest}" + INCUS_LAUNCH_IMAGE="images:${os}/${newest}" + return 0 + fi + done + ((other_arch)) && return 2 # Last resort: try remote launch of primary (incus may pull on demand) INCUS_LAUNCH_IMAGE="images:${os}/${ver}" @@ -365,7 +381,12 @@ incus_create_lxc_container() { msg_info "Resolving container image" local image_name="" - if _incus_resolve_launch_image; then + local resolve_rc=0 + _incus_resolve_launch_image || resolve_rc=$? + if ((resolve_rc == 2)); then + msg_error "${IMAGE_OS} ${IMAGE_VERSION} has no $(uname -m) image on the image server" + exit 106 + elif ((resolve_rc == 0)); then image_name="${INCUS_LAUNCH_IMAGE}" msg_ok "Image ${BL}${image_name}${CL}" else @@ -430,12 +451,13 @@ incus_create_lxc_container() { if ! incus launch "${image_name}" "${CT_NAME}" "${launch_args[@]}" >>"$LOGFILE" 2>&1; then # Retry alternate candidates once if primary pull failed - local retry_img="" launched=0 + local retry_img="" launched=0 tried=" ${image_name} " for retry_img in "images:${IMAGE_OS}/${IMAGE_VERSION}/cloud" \ "images:${IMAGE_OS}/$(incus_get_image_alias "${IMAGE_OS}" "${IMAGE_VERSION}" 2>/dev/null || true)" \ "images:${IMAGE_OS}/$(incus_get_image_alias "${IMAGE_OS}" "${IMAGE_VERSION}" 2>/dev/null || true)/cloud"; do - [[ -z "$retry_img" || "$retry_img" == "$image_name" || "$retry_img" == */ ]] && continue - msg_warn "Launch failed with ${image_name} — retrying ${retry_img}" + [[ -z "$retry_img" || "$retry_img" == */ || "$tried" == *" ${retry_img} "* ]] && continue + tried+="${retry_img} " + msg_warn "Launch failed, retrying with ${retry_img}" if incus launch "${retry_img}" "${CT_NAME}" "${launch_args[@]}" >>"$LOGFILE" 2>&1; then image_name="$retry_img" launched=1 From 3a2cda3a0d978be4b8136a6d7eadf5592a1ac386 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:36:04 +0200 Subject: [PATCH 15/21] Set up Arch Linux ARM containers The arm64 Arch image reports ID=archarm, so detect_os found no package manager and the update step failed. Its keyring is archlinuxarm, so populating only archlinux left the packages unverifiable; all shipped keyrings are populated now. --- lxc/install.func | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lxc/install.func b/lxc/install.func index 5c3b5a0..4c9c7be 100644 --- a/lxc/install.func +++ b/lxc/install.func @@ -267,7 +267,7 @@ detect_os() { OS_FAMILY="suse" PKG_MANAGER="zypper" ;; - arch | archlinux) + arch | archlinux | archarm) OS_TYPE="arch" OS_FAMILY="arch" PKG_MANAGER="pacman" @@ -1216,7 +1216,7 @@ setting_up_container() { # upgrade). The Arch base template ships without an initialized keyring. if [[ ! -f /etc/pacman.d/gnupg/pubring.gpg ]] || [[ ! -s /etc/pacman.d/gnupg/pubring.gpg ]]; then $STD pacman-key --init - $STD pacman-key --populate archlinux + $STD pacman-key --populate fi fi From 9f05d664b497c9fe9fe1edcc41f31c57309790ab Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:56:15 +0200 Subject: [PATCH 16/21] Autoremove before dnf clean all clean all dropped the metadata update_os had just fetched, so autoremove downloaded it all again: 33s instead of 2s on Oracle Linux 9, silently after Customized Container, and the 234 MB it fetched stayed in the container the cleanup was meant to shrink. --- lxc/install.func | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lxc/install.func b/lxc/install.func index 4c9c7be..8b9330e 100644 --- a/lxc/install.func +++ b/lxc/install.func @@ -869,8 +869,8 @@ pkg_clean() { $STD apk cache clean ;; dnf) - $STD dnf clean all $STD dnf autoremove -y + $STD dnf clean all ;; yum) $STD yum clean all From f6c5b82463fe148667e18d99fc1598e6e9934ac7 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:56:55 +0200 Subject: [PATCH 17/21] Drop the second MOTD on Incus The host pushed its own profile.d MOTD before the install, whose motd_ssh then wrote the regular one, so every login printed two. The extra one read the IP from hostname -I, which images such as Oracle Linux do not ship, and showed it empty. build.func still recognises the file in containers that have it. --- incus/backend.func | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/incus/backend.func b/incus/backend.func index 48b5aa4..9618616 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -1048,26 +1048,6 @@ EOF" msg_ok "Customized LXC Container" } -incus_setup_motd_pve_style() { - msg_info "Setting up MOTD" - local motd_file - motd_file=$(mktemp) - cat >"$motd_file" </dev/null | awk '{print \$1}')" -echo '' -MOTDEOF - incus file push "$motd_file" "${CT_NAME}/etc/profile.d/99-community-scripts-motd.sh" >>"${LOGFILE:-$INCUS_BUILD_LOG}" 2>&1 - incus_ct_exec chmod +x /etc/profile.d/99-community-scripts-motd.sh >>"${LOGFILE:-$INCUS_BUILD_LOG}" 2>&1 - rm -f "$motd_file" - msg_ok "MOTD configured" -} - _incus_push_functions_and_run_install() { # Sets INCUS_LAST_INSTALL_EXIT (do not capture stdout — msg_* writes there). INCUS_LAST_INSTALL_EXIT=0 @@ -1511,7 +1491,6 @@ incus_build_container() { incus_fix_debian13_root_ownership incus_customize_container install_ssh_keys_into_ct - incus_setup_motd_pve_style incus_run_install_script_with_recovery } From b4e5e679c003a233518814f9c7a06d06912e6629 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:58:40 +0200 Subject: [PATCH 18/21] Stop leaked message blocks from spinning over the install Since message blocks resume, a msg_info nothing closed came back after every later msg_ok and kept spinning into the container's install, flickering against its own spinner. Two template messages did that on Proxmox: "Template not present locally" on every first download, and the arm64 linuxcontainers.org notice. Both are fixed, and the host clears its blocks before handing the terminal to the install. --- incus/backend.func | 2 ++ pve/backend.func | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/incus/backend.func b/incus/backend.func index 9618616..92054d1 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -1068,6 +1068,8 @@ INNEREOF" >>"${LOGFILE:-$INCUS_BUILD_LOG}" 2>&1 || { msg_error "Failed to push f # prompts - the spinner kept redrawing over them, which is why Immich's # machine-learning menu could not be read and the keystroke landed in the # spinner line. A static line, and the script owns the terminal from here. + # Emptied first, or msg_custom would resume a block the host left open. + _MSG_BLOCKS=() msg_custom "🚀" "${GN}" "Starting application installation" local _install_script _run_env lxc_exit=0 install_exit_code=0 # Checked here: inside $(...) below, stdout is the capture pipe, never the terminal. diff --git a/pve/backend.func b/pve/backend.func index 77dbe0a..cd164e9 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -1405,6 +1405,9 @@ PROFILE fi exit 115 fi + # The install owns the terminal now; a block left open on the host would resume its spinner over it. + stop_spinner + _MSG_BLOCKS=() lxc-attach -n "$CTID" -- bash -c "$_install_script" local lxc_exit=$? @@ -1780,6 +1783,8 @@ PROFILE msg_error "Could not fetch install/${var_install}.sh for the retry" exit 115 fi + stop_spinner + _MSG_BLOCKS=() lxc-attach -n "$CTID" -- bash -c "$_install_script" local apt_retry_exit=$? set -Eeuo pipefail @@ -3336,7 +3341,6 @@ create_lxc_container() { NEED_DOWNLOAD=0 if [[ ! -f "$TEMPLATE_PATH" ]]; then - msg_info "Template not present locally – will download." NEED_DOWNLOAD=1 elif [[ ! -r "$TEMPLATE_PATH" ]]; then msg_error "Template file exists but is not readable – check permissions." From 98e8d7afcc11d4ed047a03a3a91e2d513e70e1af Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:46:30 +0200 Subject: [PATCH 19/21] Survive the Kali, Oracle and Amazon Linux images on the update path Their linuxcontainers images ship without hostname and free, RHEL's lang.sh reads LC_ALL unguarded under nounset, and the Incus parity helpers ran apt on every distribution. --- core/core.func | 6 ++++ incus/build.func | 2 +- incus/core.func | 7 +++- incus/tools.func | 89 ++++++++++++++++++++++++++++++++++++++++-------- ui/build-ui.func | 2 +- ui/menu.func | 4 +-- ui/validate.func | 2 +- 7 files changed, 91 insertions(+), 21 deletions(-) diff --git a/core/core.func b/core/core.func index 4f71369..db34ff9 100644 --- a/core/core.func +++ b/core/core.func @@ -194,6 +194,11 @@ ensure_profile_loaded() { # false conditional makes `source` return that status, which as the final # command of an && list is not exempt from errexit -- that took whole updates # with it. + # RHEL's lang.sh reads LC_ALL unguarded; under nounset that ends the shell + # before `|| true` gets a say. + local nounset=0 + [[ $- == *u* ]] && nounset=1 + set +u if [[ -d /etc/profile.d ]]; then for script in /etc/profile.d/*.sh; do [[ -r "$script" ]] || continue @@ -207,6 +212,7 @@ ensure_profile_loaded() { if [[ -r "${HOME:-/root}/.profile" ]]; then source "${HOME:-/root}/.profile" >/dev/null 2>&1 || true fi + ((nounset)) && set -u # Also ensure /usr/local/bin is in PATH (common install location) if [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then diff --git a/incus/build.func b/incus/build.func index 327293e..a0f5a14 100644 --- a/incus/build.func +++ b/incus/build.func @@ -65,7 +65,7 @@ incus_variables() { NSAPP=$(echo "${APP,,}" | tr -d ' ') var_install="${NSAPP}-install" INTEGER='^[0-9]+([.][0-9]+)?$' - HOST_NAME=$(hostname) + HOST_NAME=$(hostname 2>/dev/null || uname -n) PVEHOST_NAME="$HOST_NAME" DIAGNOSTICS="yes" METHOD="default" diff --git a/incus/core.func b/incus/core.func index da0a6d2..76974cb 100644 --- a/incus/core.func +++ b/incus/core.func @@ -835,11 +835,16 @@ incus_ensure_profile_loaded() { [[ -n "${_INCUS_PROFILE_LOADED:-}" ]] && return command -v incusd &>/dev/null && return + local nounset=0 + [[ $- == *u* ]] && nounset=1 + set +u if [[ -d /etc/profile.d ]]; then for script in /etc/profile.d/*.sh; do - [[ -r "$script" ]] && source "$script" + [[ -r "$script" ]] || continue + source "$script" >/dev/null 2>&1 || true done fi + ((nounset)) && set -u if [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then export PATH="/usr/local/bin:$PATH" diff --git a/incus/tools.func b/incus/tools.func index 6b364e0..06b8e21 100644 --- a/incus/tools.func +++ b/incus/tools.func @@ -61,11 +61,17 @@ detect_os() { case "$OS_TYPE" in debian | ubuntu | devuan) OS_FAMILY="debian"; PKG_MANAGER="apt" ;; alpine) OS_FAMILY="alpine"; PKG_MANAGER="apk" ;; - fedora | rocky | rockylinux | alma | almalinux | centos | rhel | openeuler) OS_FAMILY="rhel"; PKG_MANAGER="dnf" ;; + fedora | rocky | rockylinux | alma | almalinux | centos | rhel | openeuler | oracle | ol | amzn | amazonlinux) OS_FAMILY="rhel"; PKG_MANAGER="dnf" ;; opensuse* | sles) OS_TYPE="opensuse"; OS_FAMILY="suse"; PKG_MANAGER="zypper" ;; arch | archlinux) OS_TYPE="arch"; OS_FAMILY="arch"; PKG_MANAGER="pacman" ;; gentoo) OS_FAMILY="gentoo"; PKG_MANAGER="emerge" ;; - *) OS_FAMILY="unknown"; PKG_MANAGER="apt" ;; + *) + OS_FAMILY="unknown" + PKG_MANAGER="apt" + for pm in apt-get dnf apk zypper pacman; do + command -v "$pm" &>/dev/null && PKG_MANAGER="${pm%-get}" && break + done + ;; esac if command -v systemctl &>/dev/null && [[ -d /run/systemd/system ]]; then @@ -196,20 +202,70 @@ network_check() { update_os() { msg_info "Updating Container OS" - apt_update_safe - $STD apt upgrade -y + pkg_update + pkg_upgrade msg_ok "Updated Container OS" } # install.func parity (safe Incus subset) -pkg_update() { $STD apt update; } -pkg_upgrade() { $STD apt upgrade -y; } -pkg_install() { $STD apt install -y "$@"; } -pkg_remove() { $STD apt remove -y "$@"; } +pkg_update() { + case "${PKG_MANAGER:-apt}" in + apt) apt_update_safe ;; + apk) $STD apk update ;; + dnf) $STD dnf -y makecache ;; + zypper) $STD zypper -n refresh ;; + pacman) $STD pacman -Sy --noconfirm ;; + *) : ;; + esac +} +pkg_upgrade() { + case "${PKG_MANAGER:-apt}" in + apt) $STD apt upgrade -y ;; + apk) $STD apk upgrade ;; + dnf) $STD dnf -y upgrade ;; + zypper) $STD zypper -n update ;; + pacman) $STD pacman -Su --noconfirm ;; + *) : ;; + esac +} +pkg_install() { + case "${PKG_MANAGER:-apt}" in + apt) $STD apt install -y "$@" ;; + apk) $STD apk add "$@" ;; + dnf) $STD dnf -y install "$@" ;; + zypper) $STD zypper -n install "$@" ;; + pacman) $STD pacman -S --noconfirm --needed "$@" ;; + emerge) $STD emerge "$@" ;; + *) msg_error "pkg_install: unsupported package manager '${PKG_MANAGER:-}'"; return 1 ;; + esac +} +pkg_remove() { + case "${PKG_MANAGER:-apt}" in + apt) $STD apt remove -y "$@" ;; + apk) $STD apk del "$@" ;; + dnf) $STD dnf -y remove "$@" ;; + zypper) $STD zypper -n remove "$@" ;; + pacman) $STD pacman -Rs --noconfirm "$@" ;; + emerge) $STD emerge --deselect "$@" ;; + *) : ;; + esac +} pkg_clean() { - $STD apt -y autoremove 2>/dev/null || true - $STD apt -y autoclean 2>/dev/null || true - $STD apt -y clean 2>/dev/null || true + case "${PKG_MANAGER:-apt}" in + apt) + $STD apt -y autoremove 2>/dev/null || true + $STD apt -y autoclean 2>/dev/null || true + $STD apt -y clean 2>/dev/null || true + ;; + apk) $STD apk cache clean 2>/dev/null || true ;; + dnf) + $STD dnf -y autoremove 2>/dev/null || true + $STD dnf clean all 2>/dev/null || true + ;; + zypper) $STD zypper clean 2>/dev/null || true ;; + pacman) $STD pacman -Sc --noconfirm 2>/dev/null || true ;; + *) : ;; + esac } svc_enable() { systemctl enable -q "$@"; } @@ -293,10 +349,13 @@ EOF } cleanup_lxc() { - $STD apt -y autoremove 2>/dev/null || true - $STD apt -y autoclean 2>/dev/null || true - rm -f /tmp/incus-functions 2>/dev/null || true - rm -f /tmp/incus-funcs.sh 2>/dev/null || true + if declare -f pkg_clean >/dev/null; then + pkg_clean + else + $STD apt -y autoremove 2>/dev/null || true + $STD apt -y autoclean 2>/dev/null || true + fi + rm -f /tmp/incus-functions /tmp/incus-funcs.sh 2>/dev/null || true } # check_container_storage / check_container_resources: diff --git a/ui/build-ui.func b/ui/build-ui.func index 017147c..dedb792 100644 --- a/ui/build-ui.func +++ b/ui/build-ui.func @@ -63,7 +63,7 @@ variables() { NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces. var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP. INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern. - PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase + PVEHOST_NAME=$(hostname 2>/dev/null || uname -n) # Oracle and Amazon Linux images ship without hostname DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call. METHOD="default" # sets the METHOD variable to "default", used for the API call. RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable. diff --git a/ui/menu.func b/ui/menu.func index fd9c170..1b5cbb2 100644 --- a/ui/menu.func +++ b/ui/menu.func @@ -432,8 +432,8 @@ settings_menu() { # - Warns if under-provisioned and asks user to continue or abort # ------------------------------------------------------------------------------ check_container_resources() { - current_ram=$(free -m | awk 'NR==2{print $2}') - current_cpu=$(nproc) + current_ram=$(awk '/^MemTotal:/ {printf "%d", $2 / 1024}' /proc/meminfo) + current_cpu=$(nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo) if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}" diff --git a/ui/validate.func b/ui/validate.func index d3c8b44..2bf129c 100644 --- a/ui/validate.func +++ b/ui/validate.func @@ -104,7 +104,7 @@ update_motd_ip() { # Get current values local current_os="$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - Version: $(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')" - local current_hostname="$(hostname)" + local current_hostname="$(hostname 2>/dev/null || uname -n)" # get_current_ip, not `hostname -I`: BusyBox's hostname has no -I, so on # Alpine this aborted the update with "unrecognized option: I". The helper # a few lines above already handles that and is used for the MOTD line. From 33cabbd90cd9f3eb88254ac0a73fab7acefa9d16 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:54:52 +0200 Subject: [PATCH 20/21] Clean dnf, zypper and pacman containers on the Proxmox update path too cleanup_lxc in core.func ran apt unconditionally; silent() turns the resulting 127 into an abort, so every non-Debian update ended there. --- core/core.func | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/core.func b/core/core.func index db34ff9..47d6de6 100644 --- a/core/core.func +++ b/core/core.func @@ -2275,7 +2275,14 @@ cleanup_lxc() { # problem worth reporting. $STD apk cache clean 2>/dev/null || true rm -rf /var/cache/apk/* - else + elif command -v dnf &>/dev/null; then + $STD dnf -y autoremove 2>/dev/null || msg_warn "dnf autoremove failed (non-critical)" + $STD dnf clean all 2>/dev/null || msg_warn "dnf clean failed (non-critical)" + elif command -v zypper &>/dev/null; then + $STD zypper clean 2>/dev/null || msg_warn "zypper clean failed (non-critical)" + elif command -v pacman &>/dev/null; then + $STD pacman -Sc --noconfirm 2>/dev/null || msg_warn "pacman clean failed (non-critical)" + elif command -v apt &>/dev/null; then $STD apt -y autoremove 2>/dev/null || msg_warn "apt autoremove failed (non-critical)" $STD apt -y autoclean 2>/dev/null || msg_warn "apt autoclean failed (non-critical)" $STD apt -y clean 2>/dev/null || msg_warn "apt clean failed (non-critical)" From 787a011d4d473218b917236c67f3ef43b0844925 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:25:56 +0200 Subject: [PATCH 21/21] Pass the terminal type to the tty1 autologin getty The override put '-' in agetty's term slot, so the type stayed unknown and util-linux 2.40+ probed the console with ESC[6n. --- lxc/install.func | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lxc/install.func b/lxc/install.func index 8b9330e..fc9f74f 100644 --- a/lxc/install.func +++ b/lxc/install.func @@ -1660,7 +1660,9 @@ customize() { # above the login. --noissue keeps it from reprinting the issue on respawn. local _agetty_opts='--autologin root --noissue --keep-baud' - # Pin TERM=linux on the getty AND pass 'linux' as the agetty term type. + # Pin TERM=linux on the getty AND pass 'linux' as the agetty term type + # (positional order is port, baud list, term -- a stray '-' in the term + # slot left the type unknown, which is what triggers the probe). # util-linux >= 2.40 agetty auto-probes the terminal with ESC[6n (CPR) when # the term type is unknown; on the PVE noVNC console (tty1 = container-getty@1) # the response leaks into the shell as "R;80R" garbage. A known TERM stops the @@ -1672,7 +1674,7 @@ customize() { [Service] Environment=TERM=linux ExecStart= -ExecStart=-/sbin/agetty ${_agetty_opts} tty%I 115200,38400,9600 - linux +ExecStart=-/sbin/agetty ${_agetty_opts} tty%I 115200,38400,9600 linux EOF fi