diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index 318ed02..93bc379 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -125,3 +125,6 @@ jobs: check ui/build-ui.func ui/API.txt || fail=1 check api/api.func api/API.txt || fail=1 exit $fail + + - name: Update mode prompt tests + run: bash tests/update-mode-prompt.sh diff --git a/core/core.func b/core/core.func index 4f71369..47d6de6 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 @@ -2269,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)" diff --git a/incus/backend.func b/incus/backend.func index f0282a1..cfdd843 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 @@ -897,7 +919,8 @@ incus_wait_for_network() { for retry in {1..3}; do if incus_ct_exec ping -c 1 -W 2 1.1.1.1 &>/dev/null || incus_ct_exec ping -c 1 -W 2 8.8.8.8 &>/dev/null || - incus_ct_exec ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null; then + incus_ct_exec ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null || + incus_ct_exec timeout 3 bash -c 'exec 3<>/dev/tcp/1.1.1.1/443' &>/dev/null; then ping_success=true break fi @@ -1026,26 +1049,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 +1069,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 +1494,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/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 fb29241..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 "$@"; } @@ -273,7 +329,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 @@ -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/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..ddc450c 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 @@ -1388,6 +1388,16 @@ EOF pkg_update pkg_upgrade + # PID 1 pins its executor at boot; a systemd upgrade removes the + # libsystemd-shared that pinned binary links against, and every service + # start then exits 127 (journald, getty) until PID 1 re-executes. Debian's + # postinst only tries via deb-systemd-invoke, which policy-rc.d can veto. + if [[ -d /run/systemd/system && "$(readlink /proc/1/exe 2>/dev/null)" == *"(deleted)"* ]]; then + systemctl daemon-reexec &>/dev/null || true + systemctl reset-failed &>/dev/null || true + systemctl start systemd-journald.service &>/dev/null || true + fi + # Remove Python EXTERNALLY-MANAGED restriction rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED 2>/dev/null || true @@ -1518,7 +1528,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 @@ -1660,19 +1670,21 @@ 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. - # 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 - # probe. The console-getty-only mitigation in pve/backend.func missed tty1, - # which is exactly what noVNC/pct console attaches to. + # container-getty@ hands agetty its pty as stdin (TTYPath=/dev/pts/%I, + # since systemd 252), so the line is "-" exactly like the stock unit; on + # Proxmox the instances are pts/1 and pts/2, i.e. tty1 and tty2. The + # override covers every instance. TERM=linux keeps readline from probing. + # A dead getty leaves systemd's own size probe (ESC[6n) echoing on the + # console as "24;80R"; the cause of the death was the executor, see update_os. if [[ -f /usr/lib/systemd/system/container-getty@.service ]]; then - mkdir -p /etc/systemd/system/container-getty@1.service.d - cat >/etc/systemd/system/container-getty@1.service.d/override.conf </dev/null || true + mkdir -p /etc/systemd/system/container-getty@.service.d + cat >/etc/systemd/system/container-getty@.service.d/override.conf </dev/null && systemctl restart container-getty@1.service &>/dev/null || true + # Each instance owns its own pty; a failed one (Kali) needs reset-failed first. + systemctl reset-failed 'container-getty@*.service' &>/dev/null || true + # The instances come from container_ttys (pts/1 pts/2 on Proxmox, i.e. + # tty1 and tty2); a dead one is unloaded and no glob finds it, so name each. + local _t + for _t in $(tr '\0' '\n' /dev/null | sed -n 's/^container_ttys=//p'); do + systemctl restart "container-getty@${_t#pts/}.service" &>/dev/null || true + done systemctl is-active console-getty.service &>/dev/null && systemctl restart console-getty.service &>/dev/null || true touch /root/.hushlogin ;; diff --git a/pve/backend.func b/pve/backend.func index cbf7553..47366b3 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" ;; @@ -1010,12 +1011,13 @@ EOF exit 118 fi - # Verify basic connectivity (ping test) + # Verify basic connectivity: ping, or a TCP connect where the image has no ping (Amazon Linux) local ping_success=false for retry in {1..3}; do if pct exec "$CTID" -- ping -c 1 -W 2 1.1.1.1 &>/dev/null || pct exec "$CTID" -- ping -c 1 -W 2 8.8.8.8 &>/dev/null || - pct exec "$CTID" -- ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null; then + pct exec "$CTID" -- ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null || + pct exec "$CTID" -- timeout 3 bash -c 'exec 3<>/dev/tcp/1.1.1.1/443' &>/dev/null; then ping_success=true break fi @@ -1327,7 +1329,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 +1358,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 +1406,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 +1784,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 +2738,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 +2875,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 +2928,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 +2952,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 +2962,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 +3135,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 +3174,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 +3344,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 +3499,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 +3509,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 +3598,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 diff --git a/tests/update-mode-prompt.sh b/tests/update-mode-prompt.sh new file mode 100644 index 0000000..0612996 --- /dev/null +++ b/tests/update-mode-prompt.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^_cs_ask_update_mode[(][)] [{]/,/^}/' "$CORE_DIR/ui/menu.func")" + +passed=0 failed=0 +export APP="Demo" YW="" GN="" CL="" +set_std_mode() { STD_MODE_SET="$VERBOSE"; } +_cs_clear() { :; } +exit_script() { echo "cancelled"; exit 0; } + +expect() { + if [[ "$2" == "$3" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + failed=$((failed + 1)) + fi +} +# $1 is what the user types; prints the VERBOSE value handed to set_std_mode +mode_for() { + ( + VERBOSE="" STD_MODE_SET="" + _cs_ask_update_mode >/dev/null <<<"$1" + echo "${STD_MODE_SET:-unset}" + ) +} + +expect "2 selects verbose" yes "$(mode_for 2)" +expect "1 selects silent" no "$(mode_for 1)" +expect "enter defaults to silent" no "$(mode_for "")" +expect "nonsense defaults to silent" no "$(mode_for x)" +expect "3 cancels" 1 "$( (_cs_ask_update_mode <<<"3") | grep -c cancelled)" +expect "the options are shown" 3 "$( (_cs_ask_update_mode <<<"") | grep -cE '^ [123][)]')" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0)) diff --git a/ui/API.txt b/ui/API.txt index f78ee3f..bedd962 100644 --- a/ui/API.txt +++ b/ui/API.txt @@ -1,6 +1,7 @@ _build_current_app_vars_tmp _build_vars_diff _container_write_failure +_cs_ask_update_mode _cs_clear _cs_conffile_list _cs_engine_ref_line 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..77d2000 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}" @@ -1133,6 +1133,26 @@ _cs_run_update() { exit 0 } +# The whiptail update menu in plain text; sets VERBOSE or leaves on cancel. +_cs_ask_update_mode() { + local choice="" + echo -e "${YW}Update ${APP:-this container}?${CL}" + echo -e " ${GN}1)${CL} YES (silent mode)" + echo -e " ${GN}2)${CL} YES (verbose mode)" + echo -e " ${GN}3)${CL} NO (cancel update)" + read -r -t 60 -p "Select option [1-3] (default: 1): " choice || choice="" + case "$choice" in + 2) VERBOSE="yes" ;; + 3) + _cs_clear + exit_script + exit + ;; + *) VERBOSE="no" ;; + esac + set_std_mode +} + start() { # Through the resolver, not a bare curl: this way a local checkout and the # prefetched copy are used. Hardcoding the URL here meant tools.func and its @@ -1161,11 +1181,15 @@ start() { VERBOSE="no" set_std_mode _cs_run_update - elif ! command -v whiptail &>/dev/null || ! [ -t 0 ] || [[ "$TERM" == "dumb" ]]; then + elif ! [ -t 0 ] || [[ "$TERM" == "dumb" ]]; then msg_custom "â„šī¸" "${YW}" "No interactive terminal detected – defaulting to silent update mode" VERBOSE="no" set_std_mode _cs_run_update + elif ! command -v whiptail &>/dev/null; then + # Oracle, Amazon and other dnf images ship no whiptail; ask in plain text. + _cs_ask_update_mode + _cs_run_update else CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ "Support/Update functions for ${APP} LXC. Choose an option:" \ 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.