Skip to content
Draft
7 changes: 7 additions & 0 deletions incus/tools.func
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions lib/hwaccel.func
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
}
Expand Down Expand Up @@ -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"
Expand Down
200 changes: 161 additions & 39 deletions pve/backend.func
Original file line number Diff line number Diff line change
Expand Up @@ -2349,24 +2349,84 @@ 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" ;;
# 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
}

alpine:*) echo "${2:-$ALPINE_DEFAULT_VERSION}" ;;
# 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 -o "${1:?index path}" \
"https://images.linuxcontainers.org/streams/v1/images.json"
}

*) return 1 ;;
# 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
}

# Downloads an ARM64 LXC rootfs template to $1.
# 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 == $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 // .["root.squashfs"].path // empty)
' "$index" 2>/dev/null | head -1)
if [[ -n "$path" ]]; then
printf '%s' "$path"
return 0
fi
done
return 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
Expand All @@ -2391,26 +2451,72 @@ 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 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() {
download_catalog_template() {
local dest="$1" url

mkdir -p "$(dirname "$dest")" || {
msg_error "Cannot create template dir."
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"
msg_info "Downloading ${PCT_OSTYPE^} ${CUSTOM_TEMPLATE_VARIANT} template (${ARCH})"
local patched="${dest%/*}/.${dest##*/}.patched"
(
flock -x 200
if [[ -f "$patched" ]] && [[ -s "$dest" ]] && xz -t "$dest" 2>/dev/null; then
exit 0
fi
if ! curl -fsSL -o "$dest" "$url"; then
msg_error "Failed to download ARM64 template from: $url"
if [[ "$url" == *.squashfs ]]; then
_lxc_squashfs_to_tarball "$url" "$dest" || exit 208
elif ! curl -fsSL -o "$dest" "$url"; then
msg_error "Failed to download template from: $url"
exit 208
fi
if ! tar -tJf "$dest" 2>/dev/null | grep -q '/etc/network/$'; then
Expand All @@ -2422,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
Expand All @@ -2432,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'"
Expand Down Expand Up @@ -2611,17 +2717,28 @@ 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"
if ! _pveam_offers_template; then
msg_info "No Proxmox template for ${PCT_OSTYPE} ${PCT_OSVERSION:-} on ${ARCH}, trying linuxcontainers.org"

CUSTOM_TEMPLATE_VARIANT=$(arm64_template_variant "$PCT_OSTYPE" "${PCT_OSVERSION:-}") || {
msg_error "No ARM64 template mapping for ${PCT_OSTYPE} ${PCT_OSVERSION:-latest}"
exit 225
}
_lxc_catalog_index="$(mktemp)"
if ! _lxc_catalog_fetch "$_lxc_catalog_index"; then
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
rm -f "$_lxc_catalog_index"
fi

TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-rootfs.tar.xz"
TEMPLATE_SOURCE="custom-arm64"
# 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-catalog"

# Resolve template path
TEMPLATE_PATH="$(pvesm path "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" 2>/dev/null || true)"
Expand All @@ -2637,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
Expand Down Expand Up @@ -2706,9 +2823,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 |
Expand All @@ -2720,6 +2837,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
Expand Down Expand Up @@ -2769,7 +2889,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
Expand Down Expand Up @@ -2949,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
Expand Down Expand Up @@ -3035,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
Expand Down
3 changes: 0 additions & 3 deletions pve/vm-core.func
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
Loading