diff --git a/incus/backend.func b/incus/backend.func index f0282a1..92054d1 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 @@ -1026,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 @@ -1066,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. @@ -1489,7 +1493,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 } diff --git a/incus/tools.func b/incus/tools.func index fb29241..6b364e0 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 96d8251..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) @@ -2102,8 +2137,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 3ab5395..8b9330e 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" @@ -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 @@ -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 @@ -1518,7 +1518,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 cbf7553..eaf217e 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -586,8 +586,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" ;; @@ -1327,7 +1328,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 @@ -1356,7 +1357,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) @@ -1404,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=$? @@ -1779,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 @@ -2731,24 +2737,119 @@ 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" ;; + # 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 + } - ubuntu:24.04) echo "noble" ;; - ubuntu:26.04) echo "questing" ;; - ubuntu:) echo "$UBUNTU_DEFAULT_CODENAME" ;; + # 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" + } - alpine:*) echo "${2:-$ALPINE_DEFAULT_VERSION}" ;; + # 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 + } - *) return 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. + # 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 } - # Downloads an ARM64 LXC rootfs template to $1. + # 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="$(_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" ' + .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 + 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 + } + + # 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 @@ -2773,8 +2874,52 @@ 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")" || { @@ -2782,17 +2927,19 @@ 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" + 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 @@ -2804,7 +2951,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 @@ -2814,13 +2961,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'" @@ -2987,17 +3134,30 @@ 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}" + else + msg_ok "Using the linuxcontainers.org ${PCT_OSTYPE} ${PCT_OSVERSION:-} image" + 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)" @@ -3013,11 +3173,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 @@ -3183,7 +3343,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." @@ -3339,7 +3498,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 @@ -3349,26 +3508,27 @@ 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 - if ! tar -tf "$TEMPLATE_PATH" 2>/dev/null | grep -qE '^\.?/?etc/redhat-release$'; then - msg_info "Patching openEuler template (adding /etc/redhat-release)" + # Both run as ostype centos, whose PVE plugin reads a /etc/redhat-release neither ships. + if [[ "${var_os:-}" == "openeuler" || "${var_os:-}" == "amazonlinux" ]]; then + 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:-25.03}" - echo "CentOS Linux release ${_oe_ver} (openEuler)" >"$_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 - 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 @@ -3437,8 +3597,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