diff --git a/.gitignore b/.gitignore index 3c51ccf26..228518d9c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ vm/debian-13-vm.sh vm/vm-manager.sh vm/vm-manager.sh vm/debian-13-vm.sh +.DS_Store diff --git a/misc/alpine-install.func b/misc/alpine-install.func index d299b5089..9f0a9b44b 100644 --- a/misc/alpine-install.func +++ b/misc/alpine-install.func @@ -1,22 +1,54 @@ # Copyright (c) 2021-2026 community-scripts ORG # Author: tteck (tteckster) # Co-Author: MickLesk -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/arm64-dev-build/LICENSE if ! command -v curl >/dev/null 2>&1; then apk update && apk add curl >/dev/null 2>&1 fi -COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main}" -source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/core.func") -source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/error_handler.func") +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/core.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/error_handler.func) load_functions catch_errors +# Persist diagnostics setting inside container (exported from build.func) +# so addon scripts running later can find the user's choice +if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then + mkdir -p /usr/local/community-scripts + echo "DIAGNOSTICS=${DIAGNOSTICS:-no}" >/usr/local/community-scripts/diagnostics +fi + +# Get LXC IP address (must be called INSIDE container, after network is up) +get_lxc_ip + +# ------------------------------------------------------------------------------ +# post_progress_to_api() +# +# - Lightweight progress ping from inside the container +# - Updates the existing telemetry record status +# - Arguments: +# * $1: status (optional, default: "configuring") +# - Signals that the installation is actively progressing (not stuck) +# - Fire-and-forget: never blocks or fails the script +# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set +# ------------------------------------------------------------------------------ +post_progress_to_api() { + command -v curl &>/dev/null || return 0 + [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 + [[ -z "${RANDOM_UUID:-}" ]] && return 0 + + local progress_status="${1:-configuring}" + + curl -fsS -m 5 -X POST "https://telemetry.community-scripts.org/telemetry" \ + -H "Content-Type: application/json" \ + -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"lxc\",\"nsapp\":\"${app:-unknown}\",\"status\":\"${progress_status}\"}" &>/dev/null || true +} + # This function enables IPv6 if it's not disabled and sets verbose mode verb_ip6() { set_std_mode # Set STD mode based on VERBOSE - if [ "$IPV6_METHOD" == "disable" ]; then + if [ "${IPV6_METHOD:-}" = "disable" ]; then msg_info "Disabling IPv6 (this may affect some services)" $STD sysctl -w net.ipv6.conf.all.disable_ipv6=1 $STD sysctl -w net.ipv6.conf.default.disable_ipv6=1 @@ -32,43 +64,6 @@ EOF fi } -set -Eeuo pipefail -trap 'error_handler $? $LINENO "$BASH_COMMAND"' ERR -trap on_exit EXIT -trap on_interrupt INT -trap on_terminate TERM - -error_handler() { - local exit_code="$1" - local line_number="$2" - local command="$3" - - # Exitcode 0 = kein Fehler → ignorieren - if [[ "$exit_code" -eq 0 ]]; then - return 0 - fi - - printf "\e[?25h" - echo -e "\n${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}\n" - exit "$exit_code" -} - -on_exit() { - local exit_code="$?" - [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile" - exit "$exit_code" -} - -on_interrupt() { - echo -e "\n${RD}Interrupted by user (SIGINT)${CL}" - exit 130 -} - -on_terminate() { - echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}" - exit 143 -} - # This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection setting_up_container() { msg_info "Setting up Container OS" @@ -84,10 +79,11 @@ setting_up_container() { if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" = "" ]; then echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}" echo -e "${NETWORK}Check Network Settings" - exit 1 + exit 121 fi msg_ok "Set up Container OS" msg_ok "Network Connected: ${BL}$(ip addr show | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | tail -n1)${CL}" + post_progress_to_api } # This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected @@ -103,7 +99,7 @@ network_check() { echo -e "${INFO}${RD}Expect Issues Without Internet${CL}" else echo -e "${NETWORK}Check Network Settings" - exit 1 + exit 122 fi fi RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }') @@ -120,38 +116,43 @@ network_check() { update_os() { msg_info "Updating Container OS" $STD apk -U upgrade - source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/alpine-tools.func") + local tools_content + tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/tools.func) || { + msg_error "Failed to download tools.func" + exit 115 + } + source /dev/stdin <<<"$tools_content" + if ! declare -f fetch_and_deploy_gh_release >/dev/null 2>&1; then + msg_error "tools.func loaded but incomplete — missing expected functions" + exit 115 + fi msg_ok "Updated Container OS" + post_progress_to_api } # This function modifies the message of the day (motd) and SSH settings motd_ssh() { echo "export TERM='xterm-256color'" >>/root/.bashrc - IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) - - if [ -f "/etc/os-release" ]; then - OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"') - else - OS_NAME="Alpine Linux" - OS_VERSION="Unknown" - fi PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" echo "echo -e \"\"" >"$PROFILE_FILE" - echo -e "echo -e \"${BOLD}${YW}${APPLICATION} LXC Container - DEV Repository${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${RD}WARNING: This is a DEVELOPMENT version (ProxmoxVED). Do NOT use in production!${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} IP Address: ${GN}${IP}${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} Repository: ${GN}https://github.com/community-scripts/ProxmoxVED${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVED${CL}\"" >>"$PROFILE_FILE" echo "echo \"\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}\$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(ip -4 addr show eth0 | awk '/inet / {print \$2}' | cut -d/ -f1 | head -n 1)${CL}\"" >>"$PROFILE_FILE" + # Configure SSH if enabled if [[ "${SSH_ROOT}" == "yes" ]]; then + # Enable sshd service $STD rc-update add sshd + # Allow root login via SSH sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config + # Start the sshd service $STD /etc/init.d/sshd start fi + post_progress_to_api } # Validate Timezone for some LXC's @@ -186,6 +187,7 @@ EOF msg_ok "Customized Container" fi - echo "bash -c \"\$(curl -fsSL https://github.com/community-scripts/ProxmoxVED/raw/main/ct/${app}.sh)\"" >/usr/bin/update + echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/ct/${app}.sh)\"" >/usr/bin/update chmod +x /usr/bin/update + post_progress_to_api } diff --git a/misc/alpine-tools.func b/misc/alpine-tools.func index 9386f737a..958c30680 100644 --- a/misc/alpine-tools.func +++ b/misc/alpine-tools.func @@ -1,21 +1,7 @@ #!/bin/ash # shellcheck shell=ash -# Erwartet vorhandene msg_* und optional $STD aus deinem Framework. - -# Fallbacks, wenn core.func nicht geladen wurde (Alpine/ash-safe) -if ! command -v msg_info >/dev/null 2>&1; then - msg_info() { echo "[INFO] $*"; } -fi -if ! command -v msg_ok >/dev/null 2>&1; then - msg_ok() { echo "[OK] $*"; } -fi -if ! command -v msg_warn >/dev/null 2>&1; then - msg_warn() { echo "[WARN] $*"; } -fi -if ! command -v msg_error >/dev/null 2>&1; then - msg_error() { echo "[ERROR] $*" >&2; } -fi +# Expects existing msg_* functions and optional $STD from the framework. # ------------------------------ # helpers @@ -23,145 +9,6 @@ fi lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; } has() { command -v "$1" >/dev/null 2>&1; } -# tools.func compatibility helpers (Alpine-safe) -cache_installed_version() { - local app="$1" version="$2" - mkdir -p /var/cache/app-versions - echo "$version" >"/var/cache/app-versions/${app}_version.txt" -} - -get_cached_version() { - local app="$1" - mkdir -p /var/cache/app-versions - if [ -f "/var/cache/app-versions/${app}_version.txt" ]; then - cat "/var/cache/app-versions/${app}_version.txt" - return 0 - fi - return 0 -} - -version_gt() { - # returns 0 if $1 > $2 - # BusyBox-safe version compare - awk -v a="$1" -v b="$2" ' - function splitver(v, arr) { n=split(v, arr, /\./); return n } - BEGIN { - na=splitver(a, A); nb=splitver(b, B); - n=(na>nb?na:nb); - for (i=1;i<=n;i++) { - va=(A[i]==""?0:A[i]); vb=(B[i]==""?0:B[i]); - if (va+0 > vb+0) exit 0; - if (va+0 < vb+0) exit 1; - } - exit 1; - }' -} - -get_system_arch() { - local arch - arch=$(uname -m 2>/dev/null || echo "") - [ "$arch" = "x86_64" ] && arch="amd64" - [ "$arch" = "aarch64" ] && arch="arm64" - echo "$arch" -} - -create_temp_dir() { - mktemp -d -} - -get_os_info() { - local field="${1:-all}" - [ -z "${_OS_ID:-}" ] && _OS_ID=$(awk -F= '/^ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release 2>/dev/null) - [ -z "${_OS_CODENAME:-}" ] && _OS_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{gsub(/"/,"",$2); print $2}' /etc/os-release 2>/dev/null) - [ -z "${_OS_VERSION:-}" ] && _OS_VERSION=$(awk -F= '/^VERSION_ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release 2>/dev/null) - case "$field" in - id) echo "$_OS_ID" ;; - codename) echo "$_OS_CODENAME" ;; - version | version_id) echo "$_OS_VERSION" ;; - all) echo "ID=$_OS_ID CODENAME=$_OS_CODENAME VERSION=$_OS_VERSION" ;; - *) echo "$_OS_ID" ;; - esac -} - -is_alpine() { [ "$(get_os_info id)" = "alpine" ]; } - -get_os_version_major() { - local v - v=$(get_os_info version) - echo "${v%%.*}" -} - -ensure_dependencies() { - need_tool "$@" -} - -download_file() { - local url="$1" output="$2" max_retries="${3:-3}" show_progress="${4:-false}" - local i=1 curl_opts="-fsSL" - [ "$show_progress" = "true" ] && curl_opts="-fL#" - while [ $i -le "$max_retries" ]; do - if curl $curl_opts -o "$output" "$url"; then - return 0 - fi - i=$((i + 1)) - [ $i -le "$max_retries" ] && sleep 2 - done - msg_error "Failed to download: $url" - return 1 -} - -github_api_call() { - local url="$1" output_file="${2:-/dev/stdout}" - local max_retries=3 retry_delay=2 attempt=1 - local header="" - [ -n "${GITHUB_TOKEN:-}" ] && header="-H Authorization:Bearer\ ${GITHUB_TOKEN}" - while [ $attempt -le $max_retries ]; do - http_code=$(curl -fsSL -w "%{http_code}" -o "$output_file" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - $header "$url" 2>/dev/null || echo 000) - case "$http_code" in - 200) return 0 ;; - 403) [ $attempt -lt $max_retries ] && sleep "$retry_delay" || { - msg_error "GitHub API rate limit exceeded" - return 1 - } ;; - 404) - msg_error "GitHub API endpoint not found: $url" - return 1 - ;; - *) [ $attempt -lt $max_retries ] && sleep "$retry_delay" || { - msg_error "GitHub API call failed with HTTP $http_code" - return 1 - } ;; - esac - retry_delay=$((retry_delay * 2)) - attempt=$((attempt + 1)) - done - return 1 -} - -extract_version_from_json() { - local json="$1" field="${2:-tag_name}" strip_v="${3:-true}" version - need_tool jq || return 1 - version=$(printf '%s' "$json" | jq -r ".${field} // empty") - [ -z "$version" ] && return 1 - [ "$strip_v" = "true" ] && printf '%s' "${version#v}" || printf '%s' "$version" -} - -get_latest_github_release() { - local repo="$1" strip_v="${2:-true}" tmp - tmp=$(mktemp) || return 1 - github_api_call "https://api.github.com/repos/${repo}/releases/latest" "$tmp" || { - rm -f "$tmp" - return 1 - } - extract_version_from_json "$(cat "$tmp")" "tag_name" "$strip_v" - rc=$? - rm -f "$tmp" - return $rc -} - need_tool() { # usage: need_tool curl jq unzip ... # setup missing tools via apk @@ -187,26 +34,28 @@ net_resolves() { } ensure_usr_local_bin_persist() { + # Login shells: /etc/profile.d/ local PROFILE_FILE="/etc/profile.d/10-localbin.sh" if [ ! -f "$PROFILE_FILE" ]; then echo 'case ":$PATH:" in *:/usr/local/bin:*) ;; *) export PATH="/usr/local/bin:$PATH";; esac' >"$PROFILE_FILE" chmod +x "$PROFILE_FILE" fi + + # Non-login shells (pct enter): /root/.profile and /root/.bashrc + for rc_file in /root/.profile /root/.bashrc; do + if [ -f "$rc_file" ] && ! grep -q '/usr/local/bin' "$rc_file"; then + echo 'export PATH="/usr/local/bin:$PATH"' >>"$rc_file" + fi + done } download_with_progress() { # $1 url, $2 dest - local url="$1" out="$2" content_length + local url="$1" out="$2" cl need_tool curl pv || return 1 - - content_length=$( - curl -fsSLI "$url" 2>/dev/null | - awk '(tolower($1) ~ /^content-length:/) && ($2 + 0 > 0) {print $2+0}' | - tail -1 | tr -cd '[:digit:]' || true - ) - - if [ -n "$content_length" ]; then - curl -fsSL "$url" | pv -s "$content_length" >"$out" || { + cl=$(curl -fsSLI "$url" 2>/dev/null | awk 'tolower($0) ~ /^content-length:/ {print $2}' | tr -d '\r') + if [ -n "$cl" ]; then + curl -fsSL "$url" | pv -s "$cl" >"$out" || { msg_error "Download failed: $url" return 1 } @@ -237,12 +86,7 @@ check_for_gh_release() { } need_tool curl jq || return 1 - github_api_call "https://api.github.com/repos/${source}/releases/latest" "/tmp/${app_lc}-release.json" || { - msg_error "Unable to fetch latest tag for $app" - return 1 - } - tag=$(cat "/tmp/${app_lc}-release.json" | jq -r '.tag_name // empty') - rm -f "/tmp/${app_lc}-release.json" + tag=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | jq -r '.tag_name // empty') [ -z "$tag" ] && { msg_error "Unable to fetch latest tag for $app" return 1 @@ -276,14 +120,14 @@ check_for_gh_release() { } # ------------------------------ -# GitHub: get Release & deployen (Alpine) +# GitHub: get Release & deploy (Alpine) # modes: tarball | prebuild | singlefile # ------------------------------ -fetch_and_deploy_gh_release() { +fetch_and_deploy_gh() { # $1 app, $2 repo, [$3 mode], [$4 version], [$5 target], [$6 asset_pattern local app="$1" repo="$2" mode="${3:-tarball}" version="${4:-latest}" target="${5:-/opt/$1}" pattern="${6:-}" local app_lc - app_lc=$(lower "$app" | tr -d ' ') + app_lc="$(lower "$app" | tr -d ' ')" local vfile="$HOME/.${app_lc}" local json url filename tmpd unpack @@ -294,27 +138,26 @@ fetch_and_deploy_gh_release() { need_tool curl jq tar || return 1 [ "$mode" = "prebuild" ] || [ "$mode" = "singlefile" ] && need_tool unzip >/dev/null 2>&1 || true - tmpd=$(mktemp -d) || return 1 + tmpd="$(mktemp -d)" || return 1 mkdir -p "$target" - # Release JSON (with token/rate-limit handling) + # Release JSON if [ "$version" = "latest" ]; then - github_api_call "https://api.github.com/repos/$repo/releases/latest" "$tmpd/release.json" || { + json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest")" || { msg_error "GitHub API failed" rm -rf "$tmpd" return 1 } else - github_api_call "https://api.github.com/repos/$repo/releases/tags/$version" "$tmpd/release.json" || { + json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/tags/$version")" || { msg_error "GitHub API failed" rm -rf "$tmpd" return 1 } fi - json=$(cat "$tmpd/release.json") # correct Version - version=$(printf '%s' "$json" | jq -r '.tag_name // empty') + version="$(printf '%s' "$json" | jq -r '.tag_name // empty')" version="${version#v}" [ -z "$version" ] && { @@ -323,15 +166,9 @@ fetch_and_deploy_gh_release() { return 1 } - get_url() { - printf '%s' "$json" | jq -r '.assets[].browser_download_url' | - awk -v p="$pattern" 'BEGIN{IGNORECASE=1} $0 ~ p {print; exit}' | - tr -d '[:cntrl:]' - } - case "$mode" in tarball | source) - url=$(printf '%s' "$json" | jq -r '.tarball_url // empty') + url="$(printf '%s' "$json" | jq -r '.tarball_url // empty')" [ -z "$url" ] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz" filename="${app_lc}-${version}.tar.gz" download_with_progress "$url" "$tmpd/$filename" || { @@ -343,8 +180,7 @@ fetch_and_deploy_gh_release() { rm -rf "$tmpd" return 1 } - unpack=$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1) - [ "${CLEAN_INSTALL:-0}" = "1" ] && rm -rf "${target:?}/"* + unpack="$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1)" # copy content of unpack to target (cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || { msg_error "copy failed" @@ -352,41 +188,16 @@ fetch_and_deploy_gh_release() { return 1 } ;; - binary) - [ -n "$pattern" ] || pattern="*.apk" - url=$(get_url) - [ -z "$url" ] && { - msg_error "binary asset not found for pattern: $pattern" - rm -rf "$tmpd" - return 1 - } - filename="${url##*/}" - download_with_progress "$url" "$tmpd/$filename" || { - rm -rf "$tmpd" - return 1 - } - case "$filename" in - *.apk) - apk add --no-cache --allow-untrusted "$tmpd/$filename" >/dev/null 2>&1 || { - msg_error "apk install failed: $filename" - rm -rf "$tmpd" - return 1 - } - ;; - *) - msg_error "Unsupported binary asset on Alpine: $filename" - rm -rf "$tmpd" - return 1 - ;; - esac - ;; prebuild) [ -n "$pattern" ] || { msg_error "prebuild requires asset pattern" rm -rf "$tmpd" return 1 } - url=$(get_url) + url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" ' + BEGIN{IGNORECASE=1} + $0 ~ p {print; exit} + ')" [ -z "$url" ] && { msg_error "asset not found for pattern: $pattern" rm -rf "$tmpd" @@ -417,10 +228,9 @@ fetch_and_deploy_gh_release() { return 1 ;; esac - [ "${CLEAN_INSTALL:-0}" = "1" ] && rm -rf "${target:?}/"* # top-level folder strippen if [ "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq 1 ] && [ -z "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type f | head -n1)" ]; then - unpack=$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d) + unpack="$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d)" (cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || { msg_error "copy failed" rm -rf "$tmpd" @@ -440,20 +250,21 @@ fetch_and_deploy_gh_release() { rm -rf "$tmpd" return 1 } - url=$(get_url) + url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" ' + BEGIN{IGNORECASE=1} + $0 ~ p {print; exit} + ')" [ -z "$url" ] && { msg_error "asset not found for pattern: $pattern" rm -rf "$tmpd" return 1 } filename="${url##*/}" - local target_file="$app" - [ "${USE_ORIGINAL_FILENAME:-false}" = "true" ] && target_file="$filename" - download_with_progress "$url" "$target/$target_file" || { + download_with_progress "$url" "$target/$app" || { rm -rf "$tmpd" return 1 } - chmod +x "$target/$target_file" + chmod +x "$target/$app" ;; *) msg_error "Unknown mode: $mode" @@ -468,11 +279,6 @@ fetch_and_deploy_gh_release() { msg_ok "Deployed $app ($version) → $target" } -# tools.func compatibility alias -fetch_and_deploy_gh() { - fetch_and_deploy_gh_release "$@" -} - # ------------------------------ # yq (mikefarah) – Alpine # ------------------------------ diff --git a/misc/api.func b/misc/api.func index 84d439d32..a68d43c33 100644 --- a/misc/api.func +++ b/misc/api.func @@ -1,6 +1,6 @@ # Copyright (c) 2021-2026 community-scripts ORG # Author: michelroegl-brunner | MickLesk -# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE +# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/LICENSE # ============================================================================== # API.FUNC - TELEMETRY & DIAGNOSTICS API @@ -12,7 +12,6 @@ # Features: # - Container/VM creation statistics # - Installation success/failure tracking -# - Pre-flight abort tracking ("aborted" status) # - Error code mapping and reporting # - Privacy-respecting anonymous telemetry # @@ -97,7 +96,7 @@ detect_repo_source() { "") # No URL detected — use hardcoded fallback # This value must match the repo: ProxmoxVE for production, ProxmoxVED for dev - REPO_SOURCE="ProxmoxVED" + REPO_SOURCE="ProxmoxVE" ;; *) # Fork or unknown repo @@ -125,6 +124,7 @@ detect_repo_source # * Generic/Shell errors (1-3, 10, 124-132, 134, 137, 139, 141, 143-146) # * curl/wget errors (4-8, 16, 18, 22-28, 30, 32-36, 39, 44-48, 51-52, 55-57, 59, 61, 63, 75, 78-79, 92, 95) # * Package manager errors (APT, DPKG: 100-102, 255) +# * Script Validation & Setup (103-123) # * BSD sysexits (64-78) # * Systemd/Service errors (150-154) # * Python/pip/uv errors (160-162) @@ -132,7 +132,9 @@ detect_repo_source # * MySQL/MariaDB errors (180-183) # * MongoDB errors (190-193) # * Proxmox custom codes (200-231) +# * Tools & Addon Scripts (232-238) # * Node.js/npm errors (239, 243, 245-249) +# * Application Install/Update errors (250-254) # - Returns description string for given exit code # ------------------------------------------------------------------------------ explain_exit_code() { @@ -190,7 +192,7 @@ explain_exit_code() { 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;; 102) echo "APT: Lock held by another process (dpkg/apt still running)" ;; - # --- Validation / Preflight (103-108) --- + # --- Script Validation & Setup (103-123) --- 103) echo "Validation: Shell is not Bash" ;; 104) echo "Validation: Not running as root (or invoked via sudo)" ;; 105) echo "Validation: Proxmox VE version not supported" ;; @@ -199,6 +201,19 @@ explain_exit_code() { 108) echo "Validation: Kernel key limits exceeded" ;; 109) echo "Proxmox: No available container ID after max attempts" ;; 110) echo "Proxmox: Failed to apply default.vars" ;; + 111) echo "Proxmox: App defaults file not available" ;; + 112) echo "Proxmox: Invalid install menu option" ;; + 113) echo "LXC: Under-provisioned — user aborted update" ;; + 114) echo "LXC: Storage too low — user aborted update" ;; + 115) echo "Download: install.func download failed or incomplete" ;; + 116) echo "Proxmox: Default bridge vmbr0 not found" ;; + 117) echo "LXC: Container did not reach running state" ;; + 118) echo "LXC: No IP assigned to container after timeout" ;; + 119) echo "Proxmox: No valid storage for rootdir content" ;; + 120) echo "Proxmox: No valid storage for vztmpl content" ;; + 121) echo "LXC: Container network not ready (no IP after retries)" ;; + 122) echo "LXC: No internet connectivity — user declined to continue" ;; + 123) echo "LXC: Local IP detection failed" ;; # --- BSD sysexits.h (64-78) --- 64) echo "Usage error (wrong arguments)" ;; @@ -287,8 +302,18 @@ explain_exit_code() { 223) echo "Proxmox: Template not available after download" ;; 224) echo "Proxmox: PBS storage is for backups only" ;; 225) echo "Proxmox: No template available for OS/Version" ;; + 226) echo "Proxmox: VM disk import or post-creation setup failed" ;; 231) echo "Proxmox: LXC stack upgrade failed" ;; + # --- Tools & Addon Scripts (232-238) --- + 232) echo "Tools: Wrong execution environment (run on PVE host, not inside LXC)" ;; + 233) echo "Tools: Application not installed (update prerequisite missing)" ;; + 234) echo "Tools: No LXC containers found or available" ;; + 235) echo "Tools: Backup or restore operation failed" ;; + 236) echo "Tools: Required hardware not detected" ;; + 237) echo "Tools: Dependency package installation failed" ;; + 238) echo "Tools: OS or distribution not supported for this addon" ;; + # --- Node.js / npm / pnpm / yarn (239-249) --- 239) echo "npm/Node.js: Unexpected runtime error or dependency failure" ;; 243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;; @@ -298,6 +323,13 @@ explain_exit_code() { 248) echo "Node.js: Invalid C++ addon / N-API failure" ;; 249) echo "npm/pnpm/yarn: Unknown fatal error" ;; + # --- Application Install/Update Errors (250-254) --- + 250) echo "App: Download failed or version not determined" ;; + 251) echo "App: File extraction failed (corrupt or incomplete archive)" ;; + 252) echo "App: Required file or resource not found" ;; + 253) echo "App: Data migration required — update aborted" ;; + 254) echo "App: User declined prompt or input timed out" ;; + # --- DPKG --- 255) echo "DPKG: Fatal internal error" ;; @@ -314,17 +346,20 @@ explain_exit_code() { # - Handles backslashes, quotes, newlines, tabs, and carriage returns # ------------------------------------------------------------------------------ json_escape() { - local s="$1" - # Strip ANSI escape sequences (color codes etc.) - s=$(printf '%s' "$s" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g') - s=${s//\\/\\\\} - s=${s//"/\\"/} - s=${s//$'\n'/\\n} - s=${s//$'\r'/} - s=${s//$'\t'/\\t} - # Remove any remaining control characters (0x00-0x1F except those already handled) - s=$(printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037') - printf '%s' "$s" + # Escape a string for safe JSON embedding using awk (handles any input size). + # Pipeline: strip ANSI → remove control chars → escape \ " TAB → join lines with \n + printf '%s' "$1" \ + | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \ + | tr -d '\000-\010\013\014\016-\037\177\r' \ + | awk ' + BEGIN { ORS = "" } + { + gsub(/\\/, "\\\\") # backslash → \\ + gsub(/"/, "\\\"") # double quote → \" + gsub(/\t/, "\\t") # tab → \t + if (NR > 1) printf "\\n" + printf "%s", $0 + }' } # ------------------------------------------------------------------------------ @@ -360,6 +395,11 @@ get_error_text() { logfile="$BUILD_LOG" fi + # Try SILENT_LOGFILE as last resort (captures $STD command output) + if [[ -z "$logfile" || ! -s "$logfile" ]] && [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then + logfile="$SILENT_LOGFILE" + fi + if [[ -n "$logfile" && -s "$logfile" ]]; then tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' fi @@ -405,6 +445,13 @@ get_full_log() { fi fi + # Fall back to SILENT_LOGFILE (captures $STD command output) + if [[ -z "$logfile" || ! -s "$logfile" ]]; then + if [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then + logfile="$SILENT_LOGFILE" + fi + fi + if [[ -n "$logfile" && -s "$logfile" ]]; then # Strip ANSI codes, carriage returns, and anonymize IP addresses (GDPR) sed 's/\r$//' "$logfile" 2>/dev/null | @@ -642,18 +689,23 @@ EOF [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Sending to: $TELEMETRY_URL" >&2 [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Payload: $JSON_PAYLOAD" >&2 - # Fire-and-forget: never block, never fail - local http_code - if [[ "${DEV_MODE:-}" == "true" ]]; then - http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ - -H "Content-Type: application/json" \ - -d "$JSON_PAYLOAD" -o /dev/stderr 2>&1) || true - echo "[DEBUG] HTTP response code: $http_code" >&2 - else - curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ - -H "Content-Type: application/json" \ - -d "$JSON_PAYLOAD" &>/dev/null || true - fi + # Send initial "installing" record with retry. + # This record MUST exist for all subsequent updates to succeed. + local http_code="" attempt + for attempt in 1 2 3; do + if [[ "${DEV_MODE:-}" == "true" ]]; then + http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "$JSON_PAYLOAD" -o /dev/stderr 2>&1) || http_code="000" + echo "[DEBUG] post_to_api attempt $attempt HTTP=$http_code" >&2 + else + http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000" + fi + [[ "$http_code" =~ ^2[0-9]{2}$ ]] && break + [[ "$attempt" -lt 3 ]] && sleep 1 + done POST_TO_API_DONE=true } @@ -744,10 +796,15 @@ post_to_api_vm() { EOF ) - # Fire-and-forget: never block, never fail - curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ - -H "Content-Type: application/json" \ - -d "$JSON_PAYLOAD" &>/dev/null || true + # Send initial "installing" record with retry (must succeed for updates to work) + local http_code="" attempt + for attempt in 1 2 3; do + http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000" + [[ "$http_code" =~ ^2[0-9]{2}$ ]] && break + [[ "$attempt" -lt 3 ]] && sleep 1 + done POST_TO_API_DONE=true } @@ -779,76 +836,6 @@ post_progress_to_api() { -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${telemetry_type}\",\"nsapp\":\"${app_name}\",\"status\":\"${progress_status}\"}" &>/dev/null || true } -# ------------------------------------------------------------------------------ -# post_preflight_to_api() -# -# - Reports preflight failure to telemetry with "aborted" status -# - Uses PREFLIGHT_FAILURES array from build.func for error details -# - Sends error_category "preflight" for analytics grouping -# - Fire-and-forget: never blocks or fails script execution -# ------------------------------------------------------------------------------ -post_preflight_to_api() { - # Silent fail - telemetry should never break scripts - command -v curl &>/dev/null || return 0 - [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 - [[ -z "${RANDOM_UUID:-}" ]] && return 0 - - # Set type for telemetry - TELEMETRY_TYPE="lxc" - - local pve_version="" - if command -v pveversion &>/dev/null; then - pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true - fi - - # Build error summary from preflight failures - local error_summary="" - for failure in "${PREFLIGHT_FAILURES[@]}"; do - local code="${failure%%|*}" - local msg="${failure#*|}" - if [[ -n "$error_summary" ]]; then - error_summary="${error_summary}\n" - fi - error_summary="${error_summary}[${code}] ${msg}" - done - error_summary=$(json_escape "$error_summary") - - local exit_code="${PREFLIGHT_EXIT_CODE:-1}" - local short_error - short_error=$(json_escape "$(explain_exit_code "$exit_code")") - local error_category="preflight" - - local JSON_PAYLOAD - JSON_PAYLOAD=$( - cat </dev/null || true -} - # ------------------------------------------------------------------------------ # post_update_to_api() # @@ -896,7 +883,7 @@ post_update_to_api() { # Get RAM info (if detected) local ram_speed="${RAM_SPEED:-}" - # Map status to telemetry values: installing, success, failed, aborted, unknown + # Map status to telemetry values: installing, success, failed, unknown case "$status" in done | success) pb_status="success" @@ -907,17 +894,14 @@ post_update_to_api() { failed) pb_status="failed" ;; - aborted) - pb_status="aborted" - ;; *) pb_status="unknown" ;; esac - # For failed/aborted/unknown status, resolve exit code and error description - local short_error="" - if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "aborted" ]] || [[ "$pb_status" == "unknown" ]]; then + # For failed/unknown status, resolve exit code and error description + local short_error="" medium_error="" + if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then exit_code="$raw_exit_code" else @@ -936,6 +920,18 @@ post_update_to_api() { short_error=$(json_escape "$(explain_exit_code "$exit_code")") error_category=$(categorize_error "$exit_code") [[ -z "$error" ]] && error="Unknown error" + + # Build medium error for attempt 2: explanation + last 100 log lines (≤16KB) + # This is the critical middle ground between full 120KB log and generic-only description + local medium_log="" + medium_log=$(get_full_log 16384) || true # 16KB max + if [[ -z "$medium_log" ]]; then + medium_log=$(get_error_text) || true + fi + local medium_full + medium_full=$(build_error_string "$exit_code" "$medium_log") + medium_error=$(json_escape "$medium_full") + [[ -z "$medium_error" ]] && medium_error="$short_error" fi # Calculate duration if timer was started @@ -952,6 +948,11 @@ post_update_to_api() { local http_code="" + # Strip 'G' suffix from disk size (VMs set DISK_SIZE=32G) + local DISK_SIZE_API="${DISK_SIZE:-0}" + DISK_SIZE_API="${DISK_SIZE_API%G}" + [[ ! "$DISK_SIZE_API" =~ ^[0-9]+$ ]] && DISK_SIZE_API=0 + # ── Attempt 1: Full payload with complete error text (includes full log) ── local JSON_PAYLOAD JSON_PAYLOAD=$( @@ -963,7 +964,7 @@ post_update_to_api() { "nsapp": "${NSAPP:-unknown}", "status": "${pb_status}", "ct_type": ${CT_TYPE:-1}, - "disk_size": ${DISK_SIZE:-0}, + "disk_size": ${DISK_SIZE_API}, "core_count": ${CORE_COUNT:-0}, "ram_size": ${RAM_SIZE:-0}, "os_type": "${var_os:-}", @@ -994,7 +995,7 @@ EOF return 0 fi - # ── Attempt 2: Short error text (no full log) ── + # ── Attempt 2: Medium error text (truncated log ≤16KB instead of full 120KB) ── sleep 1 local RETRY_PAYLOAD RETRY_PAYLOAD=$( @@ -1006,7 +1007,7 @@ EOF "nsapp": "${NSAPP:-unknown}", "status": "${pb_status}", "ct_type": ${CT_TYPE:-1}, - "disk_size": ${DISK_SIZE:-0}, + "disk_size": ${DISK_SIZE_API}, "core_count": ${CORE_COUNT:-0}, "ram_size": ${RAM_SIZE:-0}, "os_type": "${var_os:-}", @@ -1014,7 +1015,7 @@ EOF "pve_version": "${pve_version}", "method": "${METHOD:-default}", "exit_code": ${exit_code}, - "error": "${short_error}", + "error": "${medium_error}", "error_category": "${error_category}", "install_duration": ${duration}, "cpu_vendor": "${cpu_vendor}", @@ -1037,7 +1038,7 @@ EOF return 0 fi - # ── Attempt 3: Minimal payload (bare minimum to set status) ── + # ── Attempt 3: Minimal payload with medium error (bare minimum to set status) ── sleep 2 local MINIMAL_PAYLOAD MINIMAL_PAYLOAD=$( @@ -1049,7 +1050,7 @@ EOF "nsapp": "${NSAPP:-unknown}", "status": "${pb_status}", "exit_code": ${exit_code}, - "error": "${short_error}", + "error": "${medium_error}", "error_category": "${error_category}", "install_duration": ${duration} } @@ -1066,7 +1067,7 @@ EOF fi # All 3 attempts failed — do NOT set POST_UPDATE_DONE=true. - # This allows the EXIT trap (api_exit_script) to retry with 3 fresh attempts. + # This allows the EXIT trap (on_exit in error_handler.func) to retry. # No infinite loop risk: EXIT trap fires exactly once. } @@ -1078,7 +1079,7 @@ EOF # categorize_error() # # - Maps exit codes to error categories for better analytics -# - Categories: network, storage, dependency, permission, timeout, config, resource, preflight, unknown +# - Categories: network, storage, dependency, permission, timeout, config, resource, unknown # - Used to group errors in dashboard # ------------------------------------------------------------------------------ categorize_error() { @@ -1102,9 +1103,6 @@ categorize_error() { # Permission errors 126 | 152) echo "permission" ;; - # Validation / Preflight errors - 103 | 104 | 105 | 106 | 107 | 108) echo "preflight" ;; - # Configuration errors (Proxmox config, invalid args) 128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; @@ -1405,16 +1403,13 @@ post_update_to_api_extended() { failed) pb_status="failed" ;; - aborted) - pb_status="aborted" - ;; *) pb_status="unknown" ;; esac - # For failed/aborted/unknown status, resolve exit code and error description - if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "aborted" ]] || [[ "$pb_status" == "unknown" ]]; then + # For failed/unknown status, resolve exit code and error description + if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then exit_code="$raw_exit_code" else diff --git a/misc/build.func b/misc/build.func index 1b24c1f7c..f8dee7f9c 100644 --- a/misc/build.func +++ b/misc/build.func @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG # Author: tteck (tteckster) | MickLesk | michelroegl-brunner -# License: MIT | https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/LICENSE +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/arm64-dev-build/LICENSE # ============================================================================== # BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION @@ -42,13 +42,14 @@ variables() { 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 - DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call. + DIAGNOSTICS="no" # Safe default: no telemetry until user consents via diagnostics_check() 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. - - SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files - BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log - combined_log="/tmp/install-${SESSION_ID}-combined.log" # Combined log (build + install) for failed installations + EXECUTION_ID="${RANDOM_UUID}" # Unique execution ID for telemetry record identification (unique-indexed in PocketBase) + SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files + BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log + # NOTE: combined_log is constructed locally in build_container() and ensure_log_on_host() + # as "/tmp/${NSAPP}-${CTID}-${SESSION_ID}.log" (requires CTID, not available here) CTTYPE="${CTTYPE:-${CT_TYPE:-1}}" # Parse dev_mode early @@ -58,7 +59,6 @@ variables() { if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then mkdir -p /var/log/community-scripts BUILD_LOG="/var/log/community-scripts/create-lxc-${SESSION_ID}-$(date +%Y%m%d_%H%M%S).log" - combined_log="/var/log/community-scripts/install-${SESSION_ID}-combined-$(date +%Y%m%d_%H%M%S).log" fi # Get Proxmox VE version and kernel version @@ -83,20 +83,16 @@ variables() { fi } -# Configurable base URL for development — override with COMMUNITY_SCRIPTS_URL -# See docs/DEV_MODE.md for details -COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main}" - -source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/api.func") +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/api.func) if command -v curl >/dev/null 2>&1; then - source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/core.func") - source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/error_handler.func") + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/core.func) + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/error_handler.func) load_functions catch_errors elif command -v wget >/dev/null 2>&1; then - source <(wget -qO- "$COMMUNITY_SCRIPTS_URL/misc/core.func") - source <(wget -qO- "$COMMUNITY_SCRIPTS_URL/misc/error_handler.func") + source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/core.func) + source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/error_handler.func) load_functions catch_errors fi @@ -104,548 +100,58 @@ fi # ============================================================================== # SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION # ============================================================================== -# -# Runs comprehensive system checks BEFORE container creation to catch common -# issues early. This prevents users from going through the entire configuration -# menu only to have creation fail due to a system-level problem. -# -# Checks performed (via run_preflight): -# - Kernel: keyring limits (maxkeys/maxbytes for UID 100000) -# - Storage: rootdir support, vztmpl support, available space -# - Network: bridge availability, DNS resolution -# - Repos: enterprise repo subscription validation -# - Cluster: quorum status (if clustered) -# - Proxmox: LXC stack health, container ID availability -# - Template: download server reachability -# -# Design: -# - All checks run and results are collected (no exit on first failure) -# - Clear, actionable error messages with suggested fixes -# - Reports "aborted" status to telemetry (not "failed") -# - Uses existing exit codes for consistency with error_handler/api.func -# -# ============================================================================== - -# --- Preflight tracking globals --- -PREFLIGHT_PASSED=0 -PREFLIGHT_FAILED=0 -PREFLIGHT_WARNINGS=0 -PREFLIGHT_FAILURES=() -PREFLIGHT_EXIT_CODE=0 # ------------------------------------------------------------------------------ -# preflight_pass() / preflight_fail() / preflight_warn() -# -# - Track individual check results -# - preflight_fail stores message + exit_code for summary -# ------------------------------------------------------------------------------ -preflight_pass() { - local msg="$1" - ((PREFLIGHT_PASSED++)) || true - echo -e " ${CM} ${GN}${msg}${CL}" -} - -preflight_fail() { - local msg="$1" - local exit_code="${2:-1}" - ((PREFLIGHT_FAILED++)) || true - PREFLIGHT_FAILURES+=("${exit_code}|${msg}") - [[ "$PREFLIGHT_EXIT_CODE" -eq 0 ]] && PREFLIGHT_EXIT_CODE="$exit_code" - echo -e " ${CROSS} ${RD}${msg}${CL}" -} - -preflight_warn() { - local msg="$1" - ((PREFLIGHT_WARNINGS++)) || true - echo -e " ${INFO} ${YW}${msg}${CL}" -} - -# ------------------------------------------------------------------------------ -# preflight_maxkeys() +# maxkeys_check() # # - Reads kernel keyring limits (maxkeys, maxbytes) # - Checks current usage for LXC user (UID 100000) # - Warns if usage is close to limits and suggests sysctl tuning -# - https://cleveruptime.com/docs/files/proc-key-users -# - https://docs.kernel.org/security/keys/core.html +# - Exits if thresholds are exceeded +# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html # ------------------------------------------------------------------------------ -preflight_maxkeys() { - local per_user_maxkeys per_user_maxbytes + +maxkeys_check() { + # Read kernel parameters per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0) per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0) + # Exit if kernel parameters are unavailable if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then - preflight_fail "Unable to read kernel key parameters" 107 - echo -e " ${TAB}${INFO} Ensure proper permissions to /proc/sys/kernel/keys/" - return 0 + msg_error "Unable to read kernel key parameters. Ensure proper permissions." + exit 107 fi - local used_lxc_keys used_lxc_bytes + # Fetch key usage for user ID 100000 (typical for containers) used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0) used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0) - local threshold_keys=$((per_user_maxkeys - 100)) - local threshold_bytes=$((per_user_maxbytes - 1000)) - local new_limit_keys=$((per_user_maxkeys * 2)) - local new_limit_bytes=$((per_user_maxbytes * 2)) + # Calculate thresholds and suggested new limits + threshold_keys=$((per_user_maxkeys - 100)) + threshold_bytes=$((per_user_maxbytes - 1000)) + new_limit_keys=$((per_user_maxkeys * 2)) + new_limit_bytes=$((per_user_maxbytes * 2)) - local failure=0 + # Check if key or byte usage is near limits + failure=0 if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then + msg_warn "Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys})" + echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." failure=1 fi if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then + msg_warn "Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes})" + echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." failure=1 fi + # Provide next steps if issues are detected if [[ "$failure" -eq 1 ]]; then - preflight_fail "Kernel key limits near threshold (keys: ${used_lxc_keys}/${per_user_maxkeys}, bytes: ${used_lxc_bytes}/${per_user_maxbytes})" 108 - echo -e " ${TAB}${INFO} Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} and ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL}" - echo -e " ${TAB}${INFO} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}, then run: ${GN}sysctl --system${CL}" - return 0 + msg_error "Kernel key limits exceeded - see suggestions above" + exit 108 fi - preflight_pass "Kernel key limits OK (keys: ${used_lxc_keys}/${per_user_maxkeys})" - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_storage_rootdir() -# -# - Verifies at least one storage supports 'rootdir' content type -# - Without this, no LXC container can be created -# ------------------------------------------------------------------------------ -preflight_storage_rootdir() { - local count - count=$(pvesm status -content rootdir 2>/dev/null | awk 'NR>1 {count++} END {print count+0}') - - if [[ "$count" -eq 0 ]]; then - preflight_fail "No storage with 'rootdir' support found" 119 - echo -e " ${TAB}${INFO} Enable 'rootdir' content on a storage in Datacenter → Storage" - return 0 - fi - - preflight_pass "Storage with 'rootdir' support available (${count} storage(s))" - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_storage_vztmpl() -# -# - Verifies at least one storage supports 'vztmpl' content type -# - Required for downloading and storing OS templates -# ------------------------------------------------------------------------------ -preflight_storage_vztmpl() { - local count - count=$(pvesm status -content vztmpl 2>/dev/null | awk 'NR>1 {count++} END {print count+0}') - - if [[ "$count" -eq 0 ]]; then - preflight_fail "No storage with 'vztmpl' support found" 120 - echo -e " ${TAB}${INFO} Enable 'vztmpl' content on a storage in Datacenter → Storage" - return 0 - fi - - preflight_pass "Storage with 'vztmpl' support available (${count} storage(s))" - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_storage_space() -# -# - Checks if any rootdir-capable storage has enough free space -# - Uses the app-declared var_disk as minimum requirement -# ------------------------------------------------------------------------------ -preflight_storage_space() { - local required_gb="${var_disk:-4}" - local required_kb=$((required_gb * 1024 * 1024)) - local has_enough=0 - local best_storage="" - local best_free=0 - - while read -r storage_name _ _ _ _ free_kb _; do - [[ -z "$storage_name" || -z "$free_kb" ]] && continue - [[ "$free_kb" == "0" ]] && continue - - if [[ "$free_kb" -ge "$required_kb" ]]; then - has_enough=1 - if [[ "$free_kb" -gt "$best_free" ]]; then - best_free="$free_kb" - best_storage="$storage_name" - fi - fi - done < <(pvesm status -content rootdir 2>/dev/null | awk 'NR>1') - - if [[ "$has_enough" -eq 0 ]]; then - preflight_fail "No storage has enough space (need ${required_gb}GB for ${APP})" 214 - echo -e " ${TAB}${INFO} Free up disk space or add a new storage with sufficient capacity" - return 0 - fi - - local best_free_fmt - best_free_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$best_free" 2>/dev/null || echo "${best_free}KB") - preflight_pass "Sufficient storage space (${best_storage}: ${best_free_fmt} free, need ${required_gb}GB)" - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_network_bridge() -# -# - Checks if at least one network bridge exists (vmbr*) -# - Verifies vmbr0 specifically (default bridge used by most scripts) -# ------------------------------------------------------------------------------ -preflight_network_bridge() { - local bridges - bridges=$(ip -o link show type bridge 2>/dev/null | grep -oE 'vmbr[0-9]+' | sort -u) - - if [[ -z "$bridges" ]]; then - preflight_fail "No network bridge (vmbr*) found" 116 - echo -e " ${TAB}${INFO} Create a bridge in Network → Create → Linux Bridge" - return 0 - fi - - if echo "$bridges" | grep -qx "vmbr0"; then - preflight_pass "Default network bridge vmbr0 available" - else - local first_bridge - first_bridge=$(echo "$bridges" | head -1) - preflight_warn "Default bridge vmbr0 not found, but ${first_bridge} is available" - echo -e " ${TAB}${INFO} Scripts default to vmbr0 — use Advanced Settings to select ${first_bridge}" - fi - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_dns_resolution() -# -# - Tests if DNS resolution works (required for template downloads) -# - Tries multiple hosts to avoid false positives -# ------------------------------------------------------------------------------ -preflight_dns_resolution() { - local test_hosts=("download.proxmox.com" "raw.githubusercontent.com" "community-scripts.org") - local resolved=0 - - for host in "${test_hosts[@]}"; do - if getent hosts "$host" &>/dev/null; then - resolved=1 - break - fi - done - - if [[ "$resolved" -eq 0 ]]; then - for host in "${test_hosts[@]}"; do - if command -v nslookup &>/dev/null && nslookup "$host" &>/dev/null; then - resolved=1 - break - fi - done - fi - - if [[ "$resolved" -eq 0 ]]; then - preflight_fail "DNS resolution failed — cannot reach template servers" 222 - echo -e " ${TAB}${INFO} Check /etc/resolv.conf and network connectivity" - return 0 - fi - - preflight_pass "DNS resolution working" - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_repo_access() -# -# - Checks if Proxmox enterprise repos are enabled without a valid subscription -# - Scans /etc/apt/sources.list.d/ for enterprise.proxmox.com entries -# - Tests HTTP access to detect 401 Unauthorized -# - Warning only (not a blocker — packages come from no-subscription repo) -# ------------------------------------------------------------------------------ -preflight_repo_access() { - local enterprise_files - enterprise_files=$(grep -rlE '^\s*deb\s+https://enterprise\.proxmox\.com' /etc/apt/sources.list.d/ 2>/dev/null || true) - - if [[ -z "$enterprise_files" ]]; then - preflight_pass "No enterprise repositories enabled" - return 0 - fi - - # Enterprise repo found — test if subscription is valid - local http_code - http_code=$(curl -sS -o /dev/null -w "%{http_code}" -m 5 "https://enterprise.proxmox.com/debian/pve/dists/" 2>/dev/null) || http_code="000" - - if [[ "$http_code" == "401" || "$http_code" == "403" ]]; then - preflight_warn "Enterprise repo enabled without valid subscription (HTTP ${http_code})" - echo -e " ${TAB}${INFO} apt-get update will show '401 Unauthorized' errors" - echo -e " ${TAB}${INFO} Disable in ${GN}/etc/apt/sources.list.d/${CL} or add a subscription key" - return 0 - fi - - if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then - preflight_pass "Enterprise repository accessible (subscription valid)" - else - preflight_warn "Enterprise repo check inconclusive (HTTP ${http_code})" - fi - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_cluster_quorum() -# -# - Checks cluster quorum status (only if node is part of a cluster) -# - Skipped on standalone nodes -# ------------------------------------------------------------------------------ -preflight_cluster_quorum() { - if [[ ! -f /etc/pve/corosync.conf ]]; then - preflight_pass "Standalone node (no cluster quorum needed)" - return 0 - fi - - if pvecm status 2>/dev/null | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then - preflight_pass "Cluster is quorate" - return 0 - fi - - preflight_fail "Cluster is not quorate — container operations will fail" 210 - echo -e " ${TAB}${INFO} Ensure all cluster nodes are running, or configure a QDevice" - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_lxc_stack() -# -# - Validates pve-container and lxc-pve packages are installed -# - Checks for available updates (informational only) -# ------------------------------------------------------------------------------ -preflight_lxc_stack() { - local pve_container_ver lxc_pve_ver - - pve_container_ver=$(dpkg-query -W -f='${Version}\n' pve-container 2>/dev/null || echo "") - lxc_pve_ver=$(dpkg-query -W -f='${Version}\n' lxc-pve 2>/dev/null || echo "") - - if [[ -z "$pve_container_ver" ]]; then - preflight_fail "Package 'pve-container' is not installed" 231 - echo -e " ${TAB}${INFO} Run: apt-get install pve-container" - return 0 - fi - - if [[ -z "$lxc_pve_ver" ]]; then - preflight_fail "Package 'lxc-pve' is not installed" 231 - echo -e " ${TAB}${INFO} Run: apt-get install lxc-pve" - return 0 - fi - - local pve_container_cand lxc_pve_cand - pve_container_cand=$(apt-cache policy pve-container 2>/dev/null | awk '/Candidate:/ {print $2}') || true - lxc_pve_cand=$(apt-cache policy lxc-pve 2>/dev/null | awk '/Candidate:/ {print $2}') || true - - local update_available=0 - if [[ -n "$pve_container_cand" && "$pve_container_cand" != "none" ]]; then - if dpkg --compare-versions "$pve_container_cand" gt "$pve_container_ver" 2>/dev/null; then - update_available=1 - fi - fi - if [[ -n "$lxc_pve_cand" && "$lxc_pve_cand" != "none" ]]; then - if dpkg --compare-versions "$lxc_pve_cand" gt "$lxc_pve_ver" 2>/dev/null; then - update_available=1 - fi - fi - - if [[ "$update_available" -eq 1 ]]; then - preflight_warn "LXC stack update available (current: pve-container=${pve_container_ver}, lxc-pve=${lxc_pve_ver})" - echo -e " ${TAB}${INFO} An upgrade will be offered during container creation if needed" - else - preflight_pass "LXC stack is up to date (pve-container=${pve_container_ver})" - fi - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_container_id() -# -# - Verifies that container IDs can be allocated -# - Uses pvesh /cluster/nextid (cluster-aware) -# ------------------------------------------------------------------------------ -preflight_container_id() { - local nextid - nextid=$(pvesh get /cluster/nextid 2>/dev/null) || true - - if [[ -z "$nextid" || ! "$nextid" =~ ^[0-9]+$ ]]; then - preflight_fail "Cannot allocate container ID (pvesh /cluster/nextid failed)" 109 - echo -e " ${TAB}${INFO} Check Proxmox cluster health and datacenter.cfg ID ranges" - return 0 - fi - - preflight_pass "Container IDs available (next: ${nextid})" - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_template_connectivity() -# -# - Tests connectivity to the Proxmox template download server -# - Warns but does not fail (local templates may be available) -# ------------------------------------------------------------------------------ -preflight_template_connectivity() { - local http_code - http_code=$(curl -sS -o /dev/null -w "%{http_code}" -m 5 "http://download.proxmox.com/images/system/" 2>/dev/null) || http_code="000" - - if [[ "$http_code" =~ ^2[0-9]{2}$ || "$http_code" =~ ^3[0-9]{2}$ ]]; then - preflight_pass "Template server reachable (download.proxmox.com)" - return 0 - fi - - local local_count=0 - while read -r storage_name _; do - [[ -z "$storage_name" ]] && continue - local count - count=$(pveam list "$storage_name" 2>/dev/null | awk 'NR>1' | wc -l) - local_count=$((local_count + count)) - done < <(pvesm status -content vztmpl 2>/dev/null | awk 'NR>1 {print $1}') - - if [[ "$local_count" -gt 0 ]]; then - preflight_warn "Template server unreachable, but ${local_count} local template(s) available" - return 0 - fi - - preflight_fail "Template server unreachable and no local templates available" 222 - echo -e " ${TAB}${INFO} Check internet connectivity or manually upload templates" - return 0 -} - -# ------------------------------------------------------------------------------ -# preflight_template_available() -# -# - Validates that a template exists for the configured var_os/var_version -# - Checks both local templates and the online pveam catalog -# - Fails if no matching template can be found anywhere -# ------------------------------------------------------------------------------ -preflight_template_available() { - local os="${var_os:-}" - local version="${var_version:-}" - - # Skip if os/version not set (e.g. Alpine scripts set them differently) - if [[ -z "$os" || -z "$version" ]]; then - preflight_pass "Template check skipped (OS/version not configured yet)" - return 0 - fi - - local search_pattern="${os}-${version}" - - # Check local templates first - local local_match=0 - while read -r storage_name _; do - [[ -z "$storage_name" ]] && continue - if pveam list "$storage_name" 2>/dev/null | awk '{print $1}' | grep -qE "^${storage_name}:vztmpl/${search_pattern}"; then - local_match=1 - break - fi - done < <(pvesm status -content vztmpl 2>/dev/null | awk 'NR>1 {print $1}') - - if [[ "$local_match" -eq 1 ]]; then - preflight_pass "Template available locally for ${os} ${version}" - return 0 - fi - - # Check online catalog - local online_match=0 - if pveam available -section system 2>/dev/null | awk '{print $2}' | grep -qE "^${search_pattern}[.-]"; then - online_match=1 - fi - - if [[ "$online_match" -eq 1 ]]; then - preflight_pass "Template available online for ${os} ${version}" - return 0 - fi - - # Gather available versions for the hint - local available_versions - available_versions=$( - pveam available -section system 2>/dev/null | - awk '{print $2}' | - grep -oE "^${os}-[0-9]+(\.[0-9]+)?" | - sed "s/^${os}-//" | - sort -uV 2>/dev/null | tr '\n' ', ' | sed 's/,$//' | sed 's/,/, /g' - ) - - preflight_fail "No template found for ${os} ${version}" 225 - if [[ -n "$available_versions" ]]; then - echo -e " ${TAB}${INFO} Available ${os} versions: ${GN}${available_versions}${CL}" - fi - echo -e " ${TAB}${INFO} Check var_version in your CT script or use an available version" - return 0 -} - -# ------------------------------------------------------------------------------ -# run_preflight() -# -# - Executes all preflight checks and collects results -# - Displays a summary with pass/fail/warn counts -# - On failure: reports to telemetry with "aborted" status and exits cleanly -# - On success: brief pause (2s) then returns (caller shows next screen) -# - Called from install_script() after header_info() -# ------------------------------------------------------------------------------ -run_preflight() { - # Reset counters - PREFLIGHT_PASSED=0 - PREFLIGHT_FAILED=0 - PREFLIGHT_WARNINGS=0 - PREFLIGHT_FAILURES=() - PREFLIGHT_EXIT_CODE=0 - - echo -e "${INFO}${BOLD}${DGN} Running pre-flight checks...${CL}" - echo "" - - # --- Kernel checks --- - preflight_maxkeys - - # --- Storage checks --- - preflight_storage_rootdir - preflight_storage_vztmpl - preflight_storage_space - - # --- Network checks --- - preflight_network_bridge - preflight_dns_resolution - - # --- Repository checks --- - preflight_repo_access - - # --- Proxmox/Cluster checks --- - preflight_cluster_quorum - preflight_lxc_stack - preflight_container_id - - # --- Template availability --- - preflight_template_connectivity - preflight_template_available - - echo "" - - # --- Summary --- - if [[ "$PREFLIGHT_FAILED" -gt 0 ]]; then - echo -e "${CROSS}${BOLD}${RD} Pre-flight failed: ${PREFLIGHT_FAILED} error(s), ${PREFLIGHT_WARNINGS} warning(s), ${PREFLIGHT_PASSED} passed${CL}" - echo "" - - echo -e "${INFO}${BOLD}${DGN} Failure details:${CL}" - for failure in "${PREFLIGHT_FAILURES[@]}"; do - local code="${failure%%|*}" - local msg="${failure#*|}" - echo -e " ${CROSS} [Exit ${code}] ${msg}" - done - echo "" - echo -e "${INFO} Please resolve the above issues before creating a container." - echo -e "${INFO} Documentation: ${BL}https://community-scripts.github.io/ProxmoxVE/${CL}" - - # Report to telemetry (if consent was given) - post_preflight_to_api - - exit "$PREFLIGHT_EXIT_CODE" - fi - - # Success — brief pause so user can see results, then clear for next screen - if [[ "$PREFLIGHT_WARNINGS" -gt 0 ]]; then - echo -e "${CM}${BOLD}${GN} Pre-flight passed with ${PREFLIGHT_WARNINGS} warning(s) (${PREFLIGHT_PASSED} checks passed)${CL}" - else - echo -e "${CM}${BOLD}${GN} All pre-flight checks passed (${PREFLIGHT_PASSED}/${PREFLIGHT_PASSED})${CL}" - fi - sleep 2 + # Silent success - only show errors if they exist } # ============================================================================== @@ -692,9 +198,11 @@ get_current_ip() { # # - Updates /etc/motd with current container IP # - Removes old IP entries to avoid duplicates +# - Regenerates /etc/profile.d/00_lxc-details.sh with dynamic OS/IP info # ------------------------------------------------------------------------------ update_motd_ip() { MOTD_FILE="/etc/motd" + PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" if [ -f "$MOTD_FILE" ]; then # Remove existing IP Address lines to prevent duplication @@ -771,8 +279,9 @@ install_ssh_keys_into_ct() { # ------------------------------------------------------------------------------ # validate_container_id() # -# - Validates if a container ID is available for use -# - Checks if ID is already used by VM or LXC container +# - Validates if a container ID is available for use (CLUSTER-WIDE) +# - Checks cluster resources via pvesh for VMs/CTs on ALL nodes +# - Falls back to local config file check if pvesh unavailable # - Checks if ID is used in LVM logical volumes # - Returns 0 if ID is available, 1 if already in use # ------------------------------------------------------------------------------ @@ -784,11 +293,35 @@ validate_container_id() { return 1 fi - # Check if config file exists for VM or LXC + # CLUSTER-WIDE CHECK: Query all VMs/CTs across all nodes + # This catches IDs used on other nodes in the cluster + # NOTE: Works on single-node too - Proxmox always has internal cluster structure + # Falls back gracefully if pvesh unavailable or returns empty + if command -v pvesh &>/dev/null; then + local cluster_ids + cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | + grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true) + if [[ -n "$cluster_ids" ]] && echo "$cluster_ids" | grep -qw "$ctid"; then + return 1 + fi + fi + + # LOCAL FALLBACK: Check if config file exists for VM or LXC + # This handles edge cases where pvesh might not return all info if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then return 1 fi + # Check ALL nodes in cluster for config files (handles pmxcfs sync delays) + # NOTE: On single-node, /etc/pve/nodes/ contains just the one node - still works + if [[ -d "/etc/pve/nodes" ]]; then + for node_dir in /etc/pve/nodes/*/; do + if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then + return 1 + fi + done + fi + # Check if ID is used in LVM logical volumes if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then return 1 @@ -800,63 +333,30 @@ validate_container_id() { # ------------------------------------------------------------------------------ # get_valid_container_id() # -# - Returns a valid, unused container ID +# - Returns a valid, unused container ID (CLUSTER-AWARE) +# - Uses pvesh /cluster/nextid as starting point (already cluster-aware) # - If provided ID is valid, returns it -# - Otherwise increments from suggested ID until a free one is found +# - Otherwise increments until a free one is found across entire cluster # - Calls validate_container_id() to check availability # ------------------------------------------------------------------------------ get_valid_container_id() { - local suggested_id="${1:-$(pvesh get /cluster/nextid)}" - - while ! validate_container_id "$suggested_id"; do - suggested_id=$((suggested_id + 1)) - done - - echo "$suggested_id" -} - -# ------------------------------------------------------------------------------ -# validate_container_id() -# -# - Validates if a container ID is available for use -# - Checks if ID is already used by VM or LXC container -# - Checks if ID is used in LVM logical volumes -# - Returns 0 if ID is available, 1 if already in use -# ------------------------------------------------------------------------------ -validate_container_id() { - local ctid="$1" - - # Check if ID is numeric - if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then - return 1 - fi - - # Check if config file exists for VM or LXC - if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then - return 1 - fi - - # Check if ID is used in LVM logical volumes - if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then - return 1 - fi - - return 0 -} - -# ------------------------------------------------------------------------------ -# get_valid_container_id() -# -# - Returns a valid, unused container ID -# - If provided ID is valid, returns it -# - Otherwise increments from suggested ID until a free one is found -# - Calls validate_container_id() to check availability -# ------------------------------------------------------------------------------ -get_valid_container_id() { - local suggested_id="${1:-$(pvesh get /cluster/nextid)}" + local suggested_id="${1:-$(pvesh get /cluster/nextid 2>/dev/null || echo 100)}" + + # Ensure we have a valid starting ID + if ! [[ "$suggested_id" =~ ^[0-9]+$ ]]; then + suggested_id=$(pvesh get /cluster/nextid 2>/dev/null || echo 100) + fi + + local max_attempts=1000 + local attempts=0 while ! validate_container_id "$suggested_id"; do suggested_id=$((suggested_id + 1)) + attempts=$((attempts + 1)) + if [[ $attempts -ge $max_attempts ]]; then + msg_error "Could not find available container ID after $max_attempts attempts" + exit 109 + fi done echo "$suggested_id" @@ -1193,7 +693,7 @@ find_host_ssh_keys() { /^[[:space:]]*#/ {next} /^[[:space:]]*$/ {next} {print} - ' | grep -E -c '"$re"' || true) + ' | grep -E -c "$re" || true) if ((c > 0)); then files+=("$f") @@ -1485,7 +985,7 @@ base_settings() { fi MTU=${var_mtu:-""} - SD=${var_storage:-""} + SD=${var_searchdomain:-""} NS=${var_ns:-""} MAC=${var_mac:-""} VLAN=${var_vlan:-""} @@ -1532,8 +1032,8 @@ load_vars_file() { local VAR_WHITELIST=( var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_keyctl var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu - var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged - var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage + var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged + var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain ) # Whitelist check helper @@ -1651,6 +1151,10 @@ load_vars_file() { msg_warn "Invalid nesting value '$var_val' in $file (must be 0 or 1), ignoring" continue fi + # Warn about potential issues with systemd-based OS when nesting is disabled via vars file + if [[ "$var_val" == "0" && "${var_os:-debian}" != "alpine" ]]; then + msg_warn "Nesting disabled in $file - modern systemd-based distributions may require nesting for proper operation" + fi ;; var_keyctl) if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then @@ -1710,8 +1214,8 @@ default_var_settings() { local VAR_WHITELIST=( var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_keyctl var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu - var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged - var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage + var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged + var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage ) # Snapshot: environment variables (highest precedence) @@ -1872,8 +1376,8 @@ if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then declare -ag VAR_WHITELIST=( var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_gateway var_hostname var_ipv6_method var_mac var_mtu - var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged - var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage + var_net var_ns var_os var_pw var_ram var_tags var_tun var_unprivileged + var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage ) fi @@ -2045,6 +1549,8 @@ _build_current_app_vars_tmp() { echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" echo + echo "var_os=$(_sanitize_value "${var_os:-}")" + echo "var_version=$(_sanitize_value "${var_version:-}")" echo "var_unprivileged=$(_sanitize_value "$_unpriv")" echo "var_cpu=$(_sanitize_value "$_cpu")" echo "var_ram=$(_sanitize_value "$_ram")" @@ -2308,7 +1814,7 @@ advanced_settings() { if [[ -n "$BRIDGES" ]]; then while IFS= read -r bridge; do if [[ -n "$bridge" ]]; then - local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//') + local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//;s/^[- ]*//') BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }") fi done <<<"$BRIDGES" @@ -2347,7 +1853,7 @@ advanced_settings() { # ═══════════════════════════════════════════════════════════════════════════ # STEP 2: Root Password - # ═══════════════════════════════════════════════════════════════════════════ + # ════════════════════════════════════════════════════════════════════════���══ 2) if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "ROOT PASSWORD" \ @@ -2530,7 +2036,8 @@ advanced_settings() { ((STEP++)) else whiptail --msgbox "Default bridge 'vmbr0' not found!\n\nPlease configure a network bridge in Proxmox first." 10 58 - exit 1 + msg_error "Default bridge 'vmbr0' not found" + exit 116 fi else if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ @@ -2540,6 +2047,10 @@ advanced_settings() { "${BRIDGE_MENU_OPTIONS[@]}" \ 3>&1 1>&2 2>&3); then local bridge_test="${result:-vmbr0}" + # Skip separator entries (e.g., __other__) - re-display menu + if [[ "$bridge_test" == "__other__" || "$bridge_test" == -* ]]; then + continue + fi if validate_bridge "$bridge_test"; then _bridge="$bridge_test" ((STEP++)) @@ -2911,6 +2422,12 @@ advanced_settings() { else if [ $? -eq 1 ]; then _enable_nesting="0" + # Warn about potential issues with systemd-based OS when nesting is disabled + if [[ "$var_os" != "alpine" ]]; then + whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "⚠️ NESTING WARNING" \ + --msgbox "Modern systemd-based distributions (Debian 13+, Ubuntu 24.04+, etc.) may require nesting to be enabled for proper operation.\n\nWithout nesting, the container may start in a degraded state with failing services (error 243/CREDENTIALS).\n\nIf you experience issues, enable nesting in the container options." 14 68 + fi else ((STEP--)) continue @@ -3244,7 +2761,7 @@ Advanced: [[ -n "${CT_TIMEZONE:-}" ]] && echo -e "${INFO}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}" [[ "$APT_CACHER" == "yes" ]] && echo -e "${INFO}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}" echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" - echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}" + echo -e "${CREATING}${BOLD}${RD}Creating an LXC of ${APP} using the above advanced settings${CL}" # Log settings to file log_section "CONTAINER SETTINGS (ADVANCED) - ${APP}" @@ -3275,152 +2792,85 @@ Advanced: # diagnostics_check() # # - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics -# - Asks user whether to send anonymous diagnostic data +# - Asks user whether to send anonymous diagnostic data (first run only) # - Saves DIAGNOSTICS=yes/no in the config file -# - Creates file if missing with default DIAGNOSTICS=yes -# - Reads current diagnostics setting from file +# - Reads current diagnostics setting from existing file # - Sets global DIAGNOSTICS variable for API telemetry opt-in/out # ------------------------------------------------------------------------------ diagnostics_check() { - if ! [ -d "/usr/local/community-scripts" ]; then - mkdir -p /usr/local/community-scripts + local config_dir="/usr/local/community-scripts" + local config_file="${config_dir}/diagnostics" + + mkdir -p "$config_dir" + + if [[ -f "$config_file" ]]; then + DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' "$config_file") || true + DIAGNOSTICS="${DIAGNOSTICS:-no}" + return fi - if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then - cat </usr/local/community-scripts/diagnostics -DIAGNOSTICS=yes + local result + result=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "TELEMETRY & DIAGNOSTICS" \ + --ok-button "Confirm" --cancel-button "Exit" \ + --radiolist "\nHelp improve Community-Scripts by sharing anonymous data.\n\nWhat we collect:\n - Container resources (CPU, RAM, disk), OS & PVE version\n - Application name, install method and status\n\nWhat we DON'T collect:\n - No IP addresses, hostnames, or personal data\n\nYou can change this anytime in the Settings menu.\nPrivacy: https://github.com/community-scripts/telemetry-service/blob/main/docs/PRIVACY.md\n\nUse SPACE to select, ENTER to confirm." 22 76 2 \ + "yes" "Yes, share anonymous data" OFF \ + "no" "No, opt out" OFF \ + 3>&1 1>&2 2>&3) || result="no" -#This file is used to store the diagnostics settings for the Community-Scripts API. -#https://git.community-scripts.org/community-scripts/ProxmoxVED/discussions/1836 -#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes. -#You can review the data at https://community-scripts.github.io/ProxmoxVE/data -#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue. -#This will disable the diagnostics feature. -#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue. -#This will enable the diagnostics feature. -#The following information will be sent: -#"disk_size" -#"core_count" -#"ram_size" -#"os_type" -#"os_version" -#"nsapp" -#"method" -#"pve_version" -#"status" -#If you have any concerns, please review the source code at /misc/build.func + DIAGNOSTICS="${result:-no}" + + cat <"$config_file" +DIAGNOSTICS=${DIAGNOSTICS} + +# Community-Scripts Telemetry Configuration +# https://telemetry.community-scripts.org +# +# This file stores your telemetry preference. +# Set DIAGNOSTICS=yes to share anonymous installation data. +# Set DIAGNOSTICS=no to disable telemetry. +# +# You can also change this via the Settings menu during installation. +# +# Data collected (when enabled): +# disk_size, core_count, ram_size, os_type, os_version, +# nsapp, method, pve_version, status, exit_code +# +# No personal data (IPs, hostnames, passwords) is ever collected. +# Privacy: https://github.com/community-scripts/telemetry-service/blob/main/docs/PRIVACY.md EOF - DIAGNOSTICS="yes" - else - cat </usr/local/community-scripts/diagnostics -DIAGNOSTICS=no - -#This file is used to store the diagnostics settings for the Community-Scripts API. -#https://git.community-scripts.org/community-scripts/ProxmoxVED/discussions/1836 -#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes. -#You can review the data at https://community-scripts.github.io/ProxmoxVE/data -#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue. -#This will disable the diagnostics feature. -#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue. -#This will enable the diagnostics feature. -#The following information will be sent: -#"disk_size" -#"core_count" -#"ram_size" -#"os_type" -#"os_version" -#"nsapp" -#"method" -#"pve_version" -#"status" -#If you have any concerns, please review the source code at /misc/build.func -EOF - DIAGNOSTICS="no" - fi - else - DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics) - - fi -} - -dev_mode_menu() { - local motd=OFF keep=OFF trace=OFF pause=OFF breakpoint=OFF logs=OFF dryrun=OFF verbose=OFF - - IFS=',' read -r -a _modes <<<"$dev_mode" - for m in "${_modes[@]}"; do - case "$m" in - motd) motd=ON ;; - keep) keep=ON ;; - trace) trace=ON ;; - pause) pause=ON ;; - breakpoint) breakpoint=ON ;; - logs) logs=ON ;; - dryrun) dryrun=ON ;; - esac - done - - [[ "$var_verbose" == "yes" ]] && verbose=ON - - local selection - selection=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "DEV MODE" \ - --checklist "Choose one or more Options" 16 51 10 \ - "motd" "Early SSH/MOTD Setup" "$motd" \ - "keep" "Preserve Container on Failure" "$keep" \ - "trace" "Bash Command Tracing" "$trace" \ - "pause" "Step-by-Step Execution" "$pause" \ - "breakpoint" "Interactive Shell on Error" "$breakpoint" \ - "logs" "Persistent Logging" "$logs" \ - "dryrun" "Simulation Mode" "$dryrun" \ - "verbose" "Verbose logging" "$verbose" \ - 3>&1 1>&2 2>&3) || exit_script - - dev_mode="" - var_verbose="no" - local modes_out=() - - for tag in $selection; do - tag="${tag%\"}" - tag="${tag#\"}" - if [[ "$tag" == "verbose" ]]; then - var_verbose="yes" - else - modes_out+=("$tag") - fi - done - - dev_mode=$( - IFS=, - echo "${modes_out[*]}" - ) - unset DEV_MODE_MOTD DEV_MODE_KEEP DEV_MODE_TRACE DEV_MODE_PAUSE DEV_MODE_BREAKPOINT DEV_MODE_LOGS DEV_MODE_DRYRUN - parse_dev_mode - if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then - mkdir -p /var/log/community-scripts - BUILD_LOG="/var/log/community-scripts/create-lxc-${SESSION_ID}-$(date +%Y%m%d_%H%M%S).log" - combined_log="/var/log/community-scripts/install-${SESSION_ID}-combined-$(date +%Y%m%d_%H%M%S).log" - fi } diagnostics_menu() { - if [ "${DIAGNOSTICS:-no}" = "yes" ]; then + local current="${DIAGNOSTICS:-no}" + local status_text="DISABLED" + [[ "$current" == "yes" ]] && status_text="ENABLED" + + local dialog_text=( + "Telemetry is currently: ${status_text}\n\n" + "Anonymous data helps us improve scripts and track issues.\n" + "No personal data is ever collected.\n\n" + "More info: https://telemetry.community-scripts.org\n\n" + "Do you want to ${current:+change this setting}?" + ) + + if [[ "$current" == "yes" ]]; then if whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "DIAGNOSTIC SETTINGS" \ - --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ - --yes-button "No" --no-button "Back"; then + --title "TELEMETRY SETTINGS" \ + --yesno "${dialog_text[*]}" 14 64 \ + --yes-button "Disable" --no-button "Keep enabled"; then DIAGNOSTICS="no" sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics - whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 + whiptail --msgbox "Telemetry disabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58 fi else if whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "DIAGNOSTIC SETTINGS" \ - --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ - --yes-button "Yes" --no-button "Back"; then + --title "TELEMETRY SETTINGS" \ + --yesno "${dialog_text[*]}" 14 64 \ + --yes-button "Enable" --no-button "Keep disabled"; then DIAGNOSTICS="yes" sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics - whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 + whiptail --msgbox "Telemetry enabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58 fi fi } @@ -3431,6 +2881,7 @@ diagnostics_menu() { # - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.) # - Uses icons and formatting for readability # - Convert CT_TYPE to description +# - Also logs settings to log file for debugging # ------------------------------------------------------------------------------ echo_default() { CT_TYPE_DESC="Unprivileged" @@ -3472,7 +2923,7 @@ echo_default() { # install_script() # # - Main entrypoint for installation mode -# - Runs safety checks (pve_check, root_check, diagnostics_check, run_preflight) +# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check) # - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit) # - Applies chosen settings and triggers container build # ------------------------------------------------------------------------------ @@ -3482,6 +2933,7 @@ install_script() { root_check arch_check ssh_check + maxkeys_check diagnostics_check if systemctl is-active -q ping-instances.service; then @@ -3501,9 +2953,8 @@ install_script() { fi [[ "${timezone:-}" == Etc/* ]] && timezone="host" # pct doesn't accept Etc/* zones - # Show APP Header + run preflight checks + # Show APP Header header_info - run_preflight # --- Support CLI argument as direct preset (default, advanced, …) --- CHOICE="${mode:-${1:-}}" @@ -3574,7 +3025,7 @@ install_script() { 3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS) default_var_settings || { msg_error "Failed to apply default.vars" - exit 1 + exit 110 } defaults_target="/usr/local/community-scripts/default.vars" break @@ -3591,7 +3042,7 @@ install_script() { break else msg_error "No App Defaults available for ${APP}" - exit 1 + exit 111 fi ;; "$SETTINGS_OPTION" | settings | SETTINGS) @@ -3601,8 +3052,8 @@ install_script() { CHOICE="" ;; *) - echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" - exit 1 + msg_error "Invalid option: $CHOICE" + exit 112 ;; esac done @@ -3634,13 +3085,12 @@ settings_menu() { local settings_items=( "1" "Manage API-Diagnostic Setting" "2" "Edit Default.vars" - "3" "Configure dev mode" ) if [ -f "$(get_app_defaults_path)" ]; then - settings_items+=("4" "Edit App.vars for ${APP}") - settings_items+=("5" "Back to Main Menu") - else + settings_items+=("3" "Edit App.vars for ${APP}") settings_items+=("4" "Back to Main Menu") + else + settings_items+=("3" "Back to Main Menu") fi local choice @@ -3653,17 +3103,16 @@ settings_menu() { case "$choice" in 1) diagnostics_menu ;; - 2) nano /usr/local/community-scripts/default.vars ;; - 3) dev_mode_menu ;; - 4) + 2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;; + 3) if [ -f "$(get_app_defaults_path)" ]; then - nano "$(get_app_defaults_path)" + ${EDITOR:-nano} "$(get_app_defaults_path)" else # Back was selected (no app.vars available) return fi ;; - 5) + 4) # Back to main menu return ;; @@ -3682,13 +3131,13 @@ check_container_resources() { current_cpu=$(nproc) 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}" + msg_warn "Under-provisioned: Required ${var_cpu} CPU/${var_ram}MB RAM, Current ${current_cpu} CPU/${current_ram}MB RAM" echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n" echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? " - read -r prompt + read -r prompt 80% and asks user confirmation before proceeding # ------------------------------------------------------------------------------ check_container_storage() { - usage=$(df / -P | awk 'NR==2 {print $5}' | tr -d '%') - - if [ -z "$usage" ] || [ "$usage" -lt 0 ]; then - echo -e "${CROSS}${HOLD}${RD}Error: Failed to check disk usage.${CL}" - exit 1 - fi - - if [ "$usage" -gt 80 ]; then - echo -e "${INFO}${HOLD}${YWB}Warning: Storage is dangerously low (${usage}%).${CL}" - printf "Continue anyway? " - read -r prompt - - case "$prompt" in - [yY][eE][sS] | [yY]) ;; - *) - echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}" - exit 1 - ;; - esac + total_size=$(df /boot --output=size | tail -n 1) + local used_size=$(df /boot --output=used | tail -n 1) + usage=$((100 * used_size / total_size)) + if ((usage > 80)); then + msg_warn "Storage is dangerously low (${usage}% used on /boot)" + echo -ne "Continue anyway? " + read -r prompt /dev/null 2>&1; then install_script || return 0 return 0 @@ -3982,7 +3424,9 @@ start() { VERBOSE="no" set_std_mode ensure_profile_loaded + get_lxc_ip update_script + update_motd_ip cleanup_lxc else CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ @@ -4004,11 +3448,13 @@ start() { 3) clear exit_script - exit + exit 0 ;; esac ensure_profile_loaded + get_lxc_ip update_script + update_motd_ip cleanup_lxc fi } @@ -4103,10 +3549,16 @@ build_container() { # Build PCT_OPTIONS as string for export TEMP_DIR=$(mktemp -d) pushd "$TEMP_DIR" >/dev/null + local _func_url if [ "$var_os" == "alpine" ]; then - export FUNCTIONS_FILE_PATH="$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/alpine-install.func")" + _func_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/alpine-install.func" else - export FUNCTIONS_FILE_PATH="$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/install.func")" + _func_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/install.func" + fi + export FUNCTIONS_FILE_PATH="$(curl -fsSL "$_func_url")" + if [[ -z "$FUNCTIONS_FILE_PATH" || ${#FUNCTIONS_FILE_PATH} -lt 100 ]]; then + msg_error "Failed to download install functions from: $_func_url" + exit 115 fi # Core exports for install.func @@ -4132,17 +3584,17 @@ build_container() { export PCT_DISK_SIZE="$DISK_SIZE" export IPV6_METHOD="$IPV6_METHOD" export ENABLE_GPU="$ENABLE_GPU" - export APPLICATION_VERSION="${var_appversion:-}" # DEV_MODE exports (optional, for debugging) export BUILD_LOG="$BUILD_LOG" export INSTALL_LOG="/root/.install-${SESSION_ID}.log" - export COMMUNITY_SCRIPTS_URL="$COMMUNITY_SCRIPTS_URL" + # Keep host-side logging on BUILD_LOG (not exported — invisible to container) # Without this, get_active_logfile() would return INSTALL_LOG (a container path) # and all host msg_info/msg_ok/msg_error would write to /root/.install-SESSION.log # on the HOST instead of BUILD_LOG, causing incomplete telemetry logs. _HOST_LOGFILE="$BUILD_LOG" + export dev_mode="${dev_mode:-}" export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}" export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}" @@ -4152,10 +3604,6 @@ build_container() { export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}" export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}" - # MODE export for unattended detection in install scripts - # This tells install scripts whether to prompt for input or use defaults - export MODE="${METHOD:-default}" - # Build PCT_OPTIONS as multi-line string PCT_OPTIONS_STRING=" -hostname $HN" @@ -4171,7 +3619,7 @@ build_container() { $PCT_OPTIONS_STRING" fi - # Add storage if specified + # Add searchdomain if specified if [ -n "$SD" ]; then PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING $SD" @@ -4232,8 +3680,10 @@ $PCT_OPTIONS_STRING" fi create_lxc_container || exit $? + # Transition to 'configuring' — container created, now setting up OS/userland post_progress_to_api "configuring" + LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" # ============================================================================ @@ -4259,10 +3709,18 @@ $PCT_OPTIONS_STRING" NVIDIA_DEVICES=() # Store PCI info to avoid multiple calls - local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") + # grep returns exit 1 when no match — use || true to prevent ERR trap + local pci_vga_info + pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D" || true) + + # No GPU-related PCI devices at all? Skip silently. + if [[ -z "$pci_vga_info" ]]; then + msg_debug "No VGA/Display/3D PCI devices found" + return 0 + fi # Check for Intel GPU - look for Intel vendor ID [8086] - if echo "$pci_vga_info" | grep -q "\[8086:"; then + if grep -q "\[8086:" <<<"$pci_vga_info"; then msg_custom "🎮" "${BL}" "Detected Intel GPU" if [[ -d /dev/dri ]]; then for d in /dev/dri/renderD* /dev/dri/card*; do @@ -4272,7 +3730,7 @@ $PCT_OPTIONS_STRING" fi # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD) - if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then + if grep -qE "\[1002:|\[1022:" <<<"$pci_vga_info"; then msg_custom "🎮" "${RD}" "Detected AMD GPU" if [[ -d /dev/dri ]]; then # Only add if not already claimed by Intel @@ -4285,7 +3743,7 @@ $PCT_OPTIONS_STRING" fi # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] - if echo "$pci_vga_info" | grep -q "\[10de:"; then + if grep -q "\[10de:" <<<"$pci_vga_info"; then msg_custom "🎮" "${GN}" "Detected NVIDIA GPU" # Simple passthrough - just bind /dev/nvidia* devices if they exist @@ -4381,8 +3839,12 @@ EOF selected_gpu="${available_gpus[0]}" msg_ok "Automatically configuring ${selected_gpu} GPU passthrough" else - # Multiple GPUs - ask user (use first as default in unattended mode) - selected_gpu=$(prompt_select "Which GPU type to passthrough?" 1 60 "${available_gpus[@]}") + # Multiple GPUs - ask user + echo -e "\n${INFO} Multiple GPU types detected:" + for gpu in "${available_gpus[@]}"; do + echo " - $gpu" + done + read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu /dev/null || echo "unknown") + msg_error "LXC Container did not reach running state (status: ${ct_status})" + exit 117 fi done @@ -4499,13 +3963,13 @@ EOF if [ -z "$ip_in_lxc" ]; then msg_error "No IP assigned to CT $CTID after 20s" - echo -e "${YW}Troubleshooting:${CL}" + msg_custom "🔧" "${YW}" "Troubleshooting:" echo " • Verify bridge ${BRG} exists and has connectivity" echo " • Check if DHCP server is reachable (if using DHCP)" echo " • Verify static IP configuration (if using static IP)" echo " • Check Proxmox firewall rules" echo " • If using Tailscale: Disable MagicDNS temporarily" - exit 1 + exit 118 fi # Verify basic connectivity (ping test) @@ -4521,8 +3985,7 @@ EOF done if [ "$ping_success" = false ]; then - msg_warn "Network configured (IP: $ip_in_lxc) but connectivity test failed" - echo -e "${YW}Container may have limited internet access. Installation will continue...${CL}" + msg_warn "Network configured (IP: $ip_in_lxc) but connectivity test failed - installation will continue" else msg_ok "Network in LXC is reachable (ping)" fi @@ -4537,24 +4000,38 @@ EOF fix_gpu_gids # Fix Debian 13 LXC template bug where / is owned by nobody:nogroup - # This causes systemd-tmpfiles to fail with "unsafe path transition" errors - # We need to fix this from the host before any package installation - if [[ "$var_os" == "debian" && "$var_version" == "13" ]]; then - # Stop container, fix ownership, restart - pct stop "$CTID" >/dev/null 2>&1 || true - sleep 1 - # Get the actual rootfs path from pct mount - local rootfs_path - rootfs_path=$(pct mount "$CTID" 2>/dev/null | grep -oP 'mounted at \K.*' || echo "") - if [[ -n "$rootfs_path" && -d "$rootfs_path" ]]; then - chown root:root "$rootfs_path" 2>/dev/null || true + # This must be done from the host as unprivileged containers cannot chown / + local rootfs + rootfs=$(pct config "$CTID" | grep -E '^rootfs:' | sed 's/rootfs: //' | cut -d',' -f1) + if [[ -n "$rootfs" ]]; then + local mount_point="/var/lib/lxc/${CTID}/rootfs" + if [[ -d "$mount_point" ]] && [[ "$(stat -c '%U' "$mount_point")" != "root" ]]; then + chown root:root "$mount_point" 2>/dev/null || true fi - pct unmount "$CTID" >/dev/null 2>&1 || true - pct start "$CTID" >/dev/null 2>&1 - sleep 3 fi + # Continue with standard container setup msg_info "Customizing LXC Container" + + # # Install GPU userland if configured + # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then + # install_gpu_userland "VAAPI" + # fi + + # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then + # install_gpu_userland "NVIDIA" + # fi + + # Disable error trap for entire customization & install phase. + # All errors are handled explicitly — recovery menu shown on failure. + # Without this, customization errors (e.g. container stopped during base package + # install) would trigger error_handler() with a simple "Remove broken container?" + # prompt instead of the full recovery menu with retry/repair options. + set +Eeuo pipefail + trap - ERR + + local install_exit_code=0 + # Continue with standard container setup if [ "$var_os" == "alpine" ]; then sleep 3 @@ -4562,24 +4039,18 @@ EOF http://dl-cdn.alpinelinux.org/alpine/latest-stable/main http://dl-cdn.alpinelinux.org/alpine/latest-stable/community EOF' - pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null" + pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq" >>"$BUILD_LOG" 2>&1 || { + msg_error "Failed to install base packages in Alpine container" + install_exit_code=1 + } else sleep 3 LANG=${LANG:-en_US.UTF-8} - - # Devuan templates don't include locales package by default - install it first - if [ "$var_os" == "devuan" ]; then - pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y locales >/dev/null" || true - fi - - # Only configure locale if locale.gen exists (some minimal templates don't have it) - if pct exec "$CTID" -- test -f /etc/locale.gen 2>/dev/null; then - pct exec "$CTID" -- bash -c "sed -i \"/$LANG/ s/^# //\" /etc/locale.gen" - pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ - echo LANG=\$locale_line >/etc/default/locale && \ - locale-gen >/dev/null && \ - export LANG=\$locale_line" - fi + pct exec "$CTID" -- bash -c "sed -i \"/$LANG/ s/^# //\" /etc/locale.gen" + pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ + echo LANG=\$locale_line >/etc/default/locale && \ + locale-gen >/dev/null && \ + export LANG=\$locale_line" if [[ -z "${tz:-}" ]]; then tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "UTC") @@ -4594,56 +4065,75 @@ EOF' msg_warn "Skipping timezone setup – zone '$tz' not found in container" fi - pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || { + pct exec "$CTID" -- bash -c "apt-get update 2>&1 && apt-get install -y sudo curl mc gnupg2 jq 2>&1" >>"$BUILD_LOG" 2>&1 || { msg_error "apt-get base packages installation failed" - exit 1 + install_exit_code=1 } fi - msg_ok "Customized LXC Container" + # Only continue with installation if customization succeeded + if [[ $install_exit_code -eq 0 ]]; then + msg_ok "Customized LXC Container" - # Install SSH keys - install_ssh_keys_into_ct - - # Run application installer - # Start timer for duration tracking - start_install_timer - - # Disable error trap - container errors are handled internally via flag file - set +Eeuo pipefail # Disable ALL error handling temporarily - trap - ERR # Remove ERR trap completely - - lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/install/${var_install}.sh")" - local lxc_exit=$? - - set -Eeuo pipefail # Re-enable error handling - trap 'error_handler' ERR # Restore ERR trap - - # Check for error flag file in container (more reliable than lxc-attach exit code) - local install_exit_code=0 - if [[ -n "${SESSION_ID:-}" ]]; then - local error_flag="/root/.install-${SESSION_ID}.failed" - if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then - install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1") - pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true + # Optional DNS override for retry scenarios (inside LXC, never on host) + if [[ "${DNS_RETRY_OVERRIDE:-false}" == "true" ]]; then + msg_info "Applying DNS retry override in LXC (8.8.8.8, 1.1.1.1)" + pct exec "$CTID" -- bash -c "printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' >/etc/resolv.conf" >/dev/null 2>&1 || true + msg_ok "DNS override applied in LXC" fi - fi - # Fallback to lxc-attach exit code if no flag file - if [[ $install_exit_code -eq 0 && $lxc_exit -ne 0 ]]; then - install_exit_code=$lxc_exit - fi + # Install SSH keys + install_ssh_keys_into_ct - # Installation failed? + # Start timer for duration tracking + start_install_timer + + # Run application installer + # Error handling already disabled above (before customization phase) + + # Signal handlers use this flag to stop the container on abort (SIGHUP/SIGINT/SIGTERM) + # Without this, SSH disconnects leave the container running as an orphan process + # that sends "configuring" status AFTER the host already reported "failed" + export CONTAINER_INSTALLING=true + + lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/install/${var_install}.sh)" + local lxc_exit=$? + + unset CONTAINER_INSTALLING + + # Keep error handling DISABLED during failure detection and recovery + # Re-enabling it here would cause any pct exec/pull failure to trigger + # error_handler() on the host, bypassing the recovery menu entirely + + # Check for error flag file in container (more reliable than lxc-attach exit code) + if [[ -n "${SESSION_ID:-}" ]]; then + local error_flag="/root/.install-${SESSION_ID}.failed" + if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then + install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1") + pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true + fi + fi + + # Fallback to lxc-attach exit code if no flag file + if [[ $install_exit_code -eq 0 && ${lxc_exit:-0} -ne 0 ]]; then + install_exit_code=${lxc_exit:-0} + fi + fi # end: if [[ $install_exit_code -eq 0 ]] (customization succeeded) + + # Installation or customization failed? if [[ $install_exit_code -ne 0 ]]; then + # Prevent job-control signals from suspending the script during recovery. + # In non-interactive shells (bash -c), background processes (spinner) can + # trigger terminal-related signals that stop the entire process group. + # TSTP = Ctrl+Z, TTIN = bg read from tty, TTOU = bg write to tty (tostop) + trap '' TSTP TTIN TTOU + msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})" - # Report failure to telemetry API - post_update_to_api "failed" "$install_exit_code" - - # Copy both logs from container before potential deletion + # Copy install log from container BEFORE API call so get_error_text() can read it local build_log_copied=false local install_log_copied=false + local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log" if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then # Create combined log with header @@ -4681,11 +4171,29 @@ EOF' } >>"$combined_log" rm -f "$temp_install_log" install_log_copied=true + # Point INSTALL_LOG to combined log so get_full_log() finds it + INSTALL_LOG="$combined_log" fi + fi - # Show combined log - echo "" - echo -e "${GN}✔${CL} Installation log: ${BL}${combined_log}${CL}" + # Report failure to telemetry API (now with log available on host) + # NOTE: Do NOT use msg_info/spinner here — the background spinner process + # causes SIGTSTP in non-interactive shells (bash -c "$(curl ...)"), which + # stops the entire process group and prevents the recovery dialog from appearing. + $STD echo -e "${TAB}⏳ Reporting failure to telemetry..." + post_update_to_api "failed" "$install_exit_code" + $STD echo -e "${TAB}${CM:-✔} Failure reported" + + # Defense-in-depth: Ensure error handling stays disabled during recovery. + # Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail + # and trap 'error_handler' ERR. If any code path above called such a function, + # the grep/sed pipelines below would trigger error_handler on non-match (exit 1). + set +Eeuo pipefail + trap - ERR + + # Show combined log location + if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then + msg_custom "📋" "${YW}" "Installation log: ${combined_log}" fi # Dev mode: Keep container or open breakpoint shell @@ -4698,7 +4206,7 @@ EOF' pct enter "$CTID" echo "" echo -en "${YW}Container ${CTID} still running. Remove now? (y/N): ${CL}" - if read -r response && [[ "$response" =~ ^[Yy]$ ]]; then + if read -r response /dev/null || true pct destroy "$CTID" &>/dev/null || true msg_ok "Container ${CTID} removed" @@ -4716,6 +4224,7 @@ EOF' local is_network_issue=false local is_apt_issue=false local is_cmd_not_found=false + local is_disk_full=false local error_explanation="" if declare -f explain_exit_code >/dev/null 2>&1; then error_explanation="$(explain_exit_code "$install_exit_code")" @@ -4736,6 +4245,14 @@ EOF' ;; esac + # Disk full / ENOSPC detection: errno -28 (ENOSPC), exit 228 (custom handler), exit 23 (curl write error) + if [[ $install_exit_code -eq 228 || $install_exit_code -eq 23 ]]; then + is_disk_full=true + fi + if [[ -f "$combined_log" ]] && grep -qiE 'ENOSPC|no space left on device|No space left on device|Disk quota exceeded|errno -28' "$combined_log"; then + is_disk_full=true + fi + # Command not found detection if [[ $install_exit_code -eq 127 ]]; then is_cmd_not_found=true @@ -4772,6 +4289,9 @@ EOF' if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then is_cmd_not_found=true fi + if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then + is_disk_full=true + fi fi # Show error explanation if available @@ -4793,10 +4313,16 @@ EOF' echo "" fi + if [[ "$is_disk_full" == true ]]; then + echo -e "${TAB}${INFO} The container ran out of disk space during installation (${GN}ENOSPC${CL})." + echo -e "${TAB}${INFO} Current disk size: ${GN}${DISK_SIZE} GB${CL}. A rebuild with doubled disk may resolve this." + echo "" + fi + if [[ "$is_cmd_not_found" == true ]]; then local missing_cmd="" if [[ -f "$combined_log" ]]; then - missing_cmd=$(grep -oiE '[a-zA-Z0-9_.-]+: command not found' "$combined_log" | tail -1 | sed 's/: command not found//') + missing_cmd=$(grep -oiE '[a-zA-Z0-9_.-]+: command not found' "$combined_log" 2>/dev/null | tail -1 | sed 's/: command not found//') || true fi if [[ -n "$missing_cmd" ]]; then echo -e "${TAB}${INFO} Missing command: ${GN}${missing_cmd}${CL}" @@ -4812,7 +4338,7 @@ EOF' echo -e " ${GN}3)${CL} Retry with verbose mode (full rebuild)" local next_option=4 - local APT_OPTION="" OOM_OPTION="" DNS_OPTION="" + local APT_OPTION="" OOM_OPTION="" DNS_OPTION="" DISK_OPTION="" if [[ "$is_apt_issue" == true ]]; then if [[ "$var_os" == "alpine" ]]; then @@ -4837,6 +4363,18 @@ EOF' fi fi + if [[ "$is_disk_full" == true ]]; then + local disk_recovery_attempt="${DISK_RECOVERY_ATTEMPT:-0}" + if [[ $disk_recovery_attempt -lt 2 ]]; then + local new_disk=$((DISK_SIZE * 2)) + echo -e " ${GN}${next_option})${CL} Retry with more disk space (Disk: ${DISK_SIZE}→${new_disk} GB)" + DISK_OPTION=$next_option + next_option=$((next_option + 1)) + else + echo -e " ${DGN}-)${CL} ${DGN}Disk resize retry exhausted (already retried ${disk_recovery_attempt}x)${CL}" + fi + fi + if [[ "$is_network_issue" == true ]]; then echo -e " ${GN}${next_option})${CL} Retry with DNS override in LXC (8.8.8.8 / 1.1.1.1)" DNS_OPTION=$next_option @@ -4847,6 +4385,8 @@ EOF' echo "" echo -en "${YW}Select option [1-${max_option}] (default: 1, auto-remove in 60s): ${CL}" + + local response="" if read -t 60 -r response; then case "${response:-1}" in 1) @@ -4862,7 +4402,7 @@ EOF' if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}" if pct exec "$CTID" -- bash -c " - source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/install.func") + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/install.func) declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true " >/dev/null 2>&1; then local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) @@ -4872,7 +4412,7 @@ EOF' exit $install_exit_code ;; 3) - # Retry with verbose mode + # Retry with verbose mode (full rebuild) echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild...${CL}" pct stop "$CTID" &>/dev/null || true pct destroy "$CTID" &>/dev/null || true @@ -4934,9 +4474,8 @@ EOF' # Re-run install script in existing container (don't destroy/recreate) set +Eeuo pipefail trap - ERR - local _LXC_CAPTURE_LOG="/tmp/.install-capture-${SESSION_ID}.log" - lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)" 2>&1 | tee "$_LXC_CAPTURE_LOG" - local apt_retry_exit=${PIPESTATUS[0]} + lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/install/${var_install}.sh)" + local apt_retry_exit=$? set -Eeuo pipefail trap 'error_handler' ERR @@ -4972,7 +4511,6 @@ EOF' pct destroy "$CTID" &>/dev/null || true echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" echo "" - local old_ctid="$CTID" local old_ram="$RAM_SIZE" local old_cpu="$CORE_COUNT" @@ -4993,7 +4531,35 @@ EOF' echo -e " Verbose: ${GN}enabled${CL}" echo "" msg_info "Restarting installation..." + build_container + return $? + fi + if [[ -n "${DISK_OPTION}" && "${response}" == "${DISK_OPTION}" ]]; then + # Retry with doubled disk size + handled=true + echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with more disk space...${CL}" + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" + echo "" + local old_ctid="$CTID" + local old_disk="$DISK_SIZE" + export CTID=$(get_valid_container_id "$CTID") + export DISK_SIZE=$((DISK_SIZE * 2)) + export var_disk="$DISK_SIZE" + export VERBOSE="yes" + export var_verbose="yes" + export DISK_RECOVERY_ATTEMPT=$((${DISK_RECOVERY_ATTEMPT:-0} + 1)) + + echo -e "${YW}Rebuilding with increased disk space (attempt ${DISK_RECOVERY_ATTEMPT}/2):${CL}" + echo -e " Container ID: ${old_ctid} → ${CTID}" + echo -e " Disk: ${old_disk} → ${GN}${DISK_SIZE}${CL} GB (x2)" + echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores" + echo -e " Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}" + echo -e " Verbose: ${GN}enabled${CL}" + echo "" + msg_info "Restarting installation..." build_container return $? fi @@ -5027,13 +4593,11 @@ EOF' exit $install_exit_code fi ;; - esac else # Timeout - auto-remove echo "" msg_info "No response - removing container ${CTID}" - pct stop "$CTID" &>/dev/null || true pct destroy "$CTID" &>/dev/null || true msg_ok "Container ${CTID} removed" @@ -5041,14 +4605,15 @@ EOF' # Force one final status update attempt after cleanup # This ensures status is updated even if the first attempt failed (e.g., HTTP 400) + $STD echo -e "${TAB}⏳ Finalizing telemetry report..." post_update_to_api "failed" "$install_exit_code" "force" + $STD echo -e "${TAB}${CM:-✔} Telemetry finalized" + # Restore default job-control signal handling before exit + trap - TSTP TTIN TTOU exit $install_exit_code fi - # Clean up host-side capture log (not needed on success, already in combined_log on failure) - rm -f "/tmp/.install-capture-${SESSION_ID}.log" 2>/dev/null - # Re-enable error handling after successful install or recovery menu completion set -Eeuo pipefail trap 'error_handler' ERR @@ -5064,7 +4629,7 @@ destroy_lxc() { trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT local prompt - if ! read -rp "Remove this Container? " prompt; then + if ! read -rp "Remove this Container? " prompt >"${BUILD_LOG:-/dev/null}" 2>&1 || { + msg_error "Failed to download template '$TEMPLATE' to storage '$TEMPLATE_STORAGE'" + exit 222 + } fi } @@ -5448,7 +5073,6 @@ create_lxc_container() { exit 205 } if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - echo -e "ID '$CTID' is already in use." unset CTID msg_error "Cannot use ID that is already in use." exit 206 @@ -5459,14 +5083,15 @@ create_lxc_container() { # Transition to 'validation' — Proxmox-internal checks (storage, template, cluster) post_progress_to_api "validation" + # Storage capability check check_storage_support "rootdir" || { msg_error "No valid storage found for 'rootdir' [Container]" - exit 1 + exit 119 } check_storage_support "vztmpl" || { msg_error "No valid storage found for 'vztmpl' [Template]" - exit 1 + exit 120 } # Template storage selection @@ -5505,17 +5130,40 @@ create_lxc_container() { msg_info "Validating storage '$CONTAINER_STORAGE'" STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 | head -1) + if [[ -z "$STORAGE_TYPE" ]]; then + msg_error "Storage '$CONTAINER_STORAGE' not found in /etc/pve/storage.cfg" + exit 213 + fi + case "$STORAGE_TYPE" in - iscsidirect) exit 212 ;; - iscsi | zfs) exit 213 ;; - cephfs) exit 219 ;; - pbs) exit 224 ;; + iscsidirect) + msg_error "Storage '$CONTAINER_STORAGE' uses iSCSI-direct which does not support container rootfs." + exit 212 + ;; + iscsi | zfs) + msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support container rootdir content." + exit 213 + ;; + cephfs) + msg_error "Storage '$CONTAINER_STORAGE' uses CephFS which is not supported for LXC rootfs." + exit 219 + ;; + pbs) + msg_error "Storage '$CONTAINER_STORAGE' is a Proxmox Backup Server — cannot be used for containers." + exit 224 + ;; linstor | rbd | nfs | cifs) - pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null || exit 217 + if ! pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null; then + msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) is not accessible or inactive." + exit 217 + fi ;; esac - pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE" || exit 213 + if ! pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE"; then + msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support 'rootdir' content." + exit 213 + fi msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated" msg_info "Validating template storage '$TEMPLATE_STORAGE'" @@ -5539,256 +5187,298 @@ create_lxc_container() { # ------------------------------------------------------------------------------ # Template discovery & validation # ------------------------------------------------------------------------------ - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - case "$PCT_OSTYPE" in - debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;; - alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;; - *) TEMPLATE_PATTERN="" ;; - esac + CUSTOM_TEMPLATE_VARIANT="" - msg_info "Searching for template '$TEMPLATE_SEARCH'" + if [[ "$ARCH" == "arm64" ]]; then + # ARM64: use custom template download from linuxcontainers.org / GitHub + msg_info "Preparing ARM64 template" - # Initialize variables - ONLINE_TEMPLATE="" - ONLINE_TEMPLATES=() + CUSTOM_TEMPLATE_VARIANT=$(arm64_template_variant "$PCT_OSTYPE" "${PCT_OSVERSION:-}") || { + msg_error "No ARM64 template mapping for ${PCT_OSTYPE} ${PCT_OSVERSION:-latest}" + exit 207 + } - # Step 1: Check local templates first (instant) - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${TEMPLATE_SEARCH}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) + TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-rootfs.tar.xz" + TEMPLATE_SOURCE="custom-arm64" - # Step 2: If local template found, use it immediately (skip pveam update) - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - msg_ok "Template search completed" - else - # Step 3: No local template - need to check online (this may be slow) - msg_info "No local template found, checking online catalog..." - - # Update catalog with timeout to prevent long hangs - if command -v timeout &>/dev/null; then - if ! timeout 30 pveam update >/dev/null 2>&1; then - msg_warn "Template catalog update timed out (possible network/DNS issue). Run 'pveam update' manually to diagnose." - fi - else - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)" + # Resolve template path: pvesm → storage.cfg fallback → default + TEMPLATE_PATH="$(pvesm path "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" 2>/dev/null || true)" + if [[ -z "$TEMPLATE_PATH" ]]; then + local _tpl_base + _tpl_base=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) + TEMPLATE_PATH="${_tpl_base:-/var/lib/vz}/template/cache/$TEMPLATE" fi + # Download if missing, too small, or corrupt (single pass) + if [[ ! -f "$TEMPLATE_PATH" ]]; then + download_arm64_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" + else + msg_ok "Template ${BL}$TEMPLATE${CL} found locally." + fi + + else + TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" + case "$PCT_OSTYPE" in + debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;; + alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;; + *) TEMPLATE_PATTERN="" ;; + esac + + msg_info "Searching for template '$TEMPLATE_SEARCH'" + + # Initialize variables + ONLINE_TEMPLATE="" ONLINE_TEMPLATES=() - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "^${TEMPLATE_SEARCH}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - msg_ok "Template search completed" - fi - - # If still no template, try to find alternatives - if [[ -z "$TEMPLATE" ]]; then - echo "" - echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." - - # Get all available versions for this OS type - AVAILABLE_VERSIONS=() - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - sort -u -V 2>/dev/null + # Step 1: Check local templates first (instant) + mapfile -t LOCAL_TEMPLATES < <( + pveam list "$TEMPLATE_STORAGE" 2>/dev/null | + awk -v search="${TEMPLATE_SEARCH}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | + sed 's|.*/||' | sort -t - -k 2 -V ) - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo "" - echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice + # Step 2: If local template found, use it immediately (skip pveam update) + if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then + TEMPLATE="${LOCAL_TEMPLATES[-1]}" + TEMPLATE_SOURCE="local" + msg_ok "Template search completed" + else + # Step 3: No local template - need to check online (this may be slow) + msg_info "No local template found, checking online catalog..." - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - - ONLINE_TEMPLATES=() - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk '{print $2}' | - grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE_SOURCE="online" - else - msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" - exit 225 + # Update catalog with timeout to prevent long hangs + if command -v timeout &>/dev/null; then + if ! timeout 30 pveam update >/dev/null 2>&1; then + msg_warn "Template catalog update timed out (possible network/DNS issue). Run 'pveam update' manually to diagnose." fi else - msg_custom "🚫" "${YW}" "Installation cancelled" - exit 0 + pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)" fi - else - msg_error "No ${PCT_OSTYPE} templates available at all" - exit 225 + + ONLINE_TEMPLATES=() + mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "^${TEMPLATE_SEARCH}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) + [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" + + TEMPLATE="$ONLINE_TEMPLATE" + TEMPLATE_SOURCE="online" + msg_ok "Template search completed" fi - fi - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { + # If still no template, try to find alternatives if [[ -z "$TEMPLATE" ]]; then - msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" + msg_warn "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." - # Get available versions + # Get all available versions for this OS type + AVAILABLE_VERSIONS=() mapfile -t AVAILABLE_VERSIONS < <( pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk -F'\t' '{print $1}' | grep "^${PCT_OSTYPE}-" | - sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | - grep -E '^[0-9]+\.[0-9]+$' | - sort -u -V 2>/dev/null || sort -u + sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | + sort -u -V 2>/dev/null ) if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - # Use prompt_select for version selection (supports unattended mode) - local selected_version - selected_version=$(prompt_select "Select ${PCT_OSTYPE} version:" 1 60 "${AVAILABLE_VERSIONS[@]}") + echo "" + echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" + for i in "${!AVAILABLE_VERSIONS[@]}"; do + echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" + done + echo "" + read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice /dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk '{print $2}' | + grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" | + sort -t - -k 2 -V 2>/dev/null || true + ) - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${TEMPLATE_SEARCH}-" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk '{print $2}' | - grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" + if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then + TEMPLATE="${ONLINE_TEMPLATES[-1]}" + TEMPLATE_SOURCE="online" + else + msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" + exit 225 + fi else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" + msg_custom "🚫" "${YW}" "Installation cancelled" + exit 0 fi - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - msg_error "Template still not found after version change" - exit 220 - } else - msg_error "No ${PCT_OSTYPE} templates available" - exit 220 + msg_error "No ${PCT_OSTYPE} templates available at all" + exit 225 fi fi - } - # Validate that we found a template - if [[ -z "$TEMPLATE" ]]; then - msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" - msg_custom "ℹ️" "${YW}" "Please check:" - msg_custom " •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)" - msg_custom " •" "${YW}" "Does the template exist for your OS version?" - exit 225 - fi - - msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" - msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" - - 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." - exit 221 - elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template file too small (<1MB) – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template looks too small, but no online version exists. Keeping local file." + TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" + if [[ -z "$TEMPLATE_PATH" ]]; then + TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) + [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" fi - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template appears corrupted, but no online version exists. Keeping local file." - fi - else - $STD msg_ok "Template $TEMPLATE is present and valid." - fi - if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then - msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" - if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then - TEMPLATE="$ONLINE_TEMPLATE" - NEED_DOWNLOAD=1 - else - msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE" + # If we still don't have a path but have a valid template name, construct it + if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then + TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" fi - fi - if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then - [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH" - for attempt in {1..3}; do - msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE" - if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then - msg_ok "Template download successful." - break + [[ -n "$TEMPLATE_PATH" ]] || { + if [[ -z "$TEMPLATE" ]]; then + msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" + + # Get available versions + mapfile -t AVAILABLE_VERSIONS < <( + pveam available -section system 2>/dev/null | + grep "^${PCT_OSTYPE}-" | + sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | + grep -E '^[0-9]+\.[0-9]+$' | + sort -u -V 2>/dev/null || sort -u + ) + + if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then + echo -e "\n${BL}Available versions:${CL}" + for i in "${!AVAILABLE_VERSIONS[@]}"; do + echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" + done + + echo "" + read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice /dev/null | + awk -v search="${TEMPLATE_SEARCH}-" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | + sed 's|.*/||' | sort -t - -k 2 -V + ) + mapfile -t ONLINE_TEMPLATES < <( + pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk '{print $2}' | + grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" | + sort -t - -k 2 -V 2>/dev/null || true + ) + ONLINE_TEMPLATE="" + [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" + + if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then + TEMPLATE="${LOCAL_TEMPLATES[-1]}" + TEMPLATE_SOURCE="local" + else + TEMPLATE="$ONLINE_TEMPLATE" + TEMPLATE_SOURCE="online" + fi + + TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" + if [[ -z "$TEMPLATE_PATH" ]]; then + TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) + [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" + fi + + # If we still don't have a path but have a valid template name, construct it + if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then + TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" + fi + + [[ -n "$TEMPLATE_PATH" ]] || { + msg_error "Template still not found after version change" + exit 220 + } + else + msg_custom "🚫" "${YW}" "Installation cancelled" + exit 0 + fi + else + msg_error "No ${PCT_OSTYPE} templates available" + exit 220 + fi fi - if [[ $attempt -eq 3 ]]; then - msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE" - exit 222 - fi - sleep $((attempt * 5)) - done - fi + } - if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then - msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download." - exit 223 + # Validate that we found a template + if [[ -z "$TEMPLATE" ]]; then + msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" + msg_custom "ℹ️" "${YW}" "Please check:" + msg_custom " •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)" + msg_custom " •" "${YW}" "Does the template exist for your OS version?" + exit 225 + fi + + msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" + msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" + + 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." + exit 221 + elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then + if [[ -n "$ONLINE_TEMPLATE" ]]; then + msg_warn "Template file too small (<1MB) – re-downloading." + NEED_DOWNLOAD=1 + else + msg_warn "Template looks too small, but no online version exists. Keeping local file." + fi + elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then + if [[ -n "$ONLINE_TEMPLATE" ]]; then + msg_warn "Template appears corrupted – re-downloading." + NEED_DOWNLOAD=1 + else + msg_warn "Template appears corrupted, but no online version exists. Keeping local file." + fi + else + $STD msg_ok "Template $TEMPLATE is present and valid." + fi + + if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then + msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" + if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then + TEMPLATE="$ONLINE_TEMPLATE" + NEED_DOWNLOAD=1 + else + msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE" + fi + fi + + if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then + [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH" + for attempt in {1..3}; do + msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE" + if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1; then + msg_ok "Template download successful." + break + fi + if [[ $attempt -eq 3 ]]; then + msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE" + exit 222 + fi + sleep $((attempt * 5)) + done + fi + + if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then + msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download." + exit 223 + fi fi # ------------------------------------------------------------------------------ @@ -5851,17 +5541,29 @@ create_lxc_container() { LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" + # Helper: append pct_create log to BUILD_LOG before exit so combined log has full context + _flush_pct_log() { + if [[ -s "${LOGFILE:-}" && -n "${BUILD_LOG:-}" ]]; then + { + echo "" + echo "--- pct create output (${LOGFILE}) ---" + cat "$LOGFILE" + echo "--- end pct create output ---" + } >>"$BUILD_LOG" 2>/dev/null || true + fi + } + # Validate template before pct create (while holding lock) if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH" 2>/dev/null || echo 0)" -lt 1000000 ]]; then msg_info "Template file missing or too small – downloading" rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + download_template msg_ok "Template downloaded" elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then + if [[ "$ARCH" == "arm64" || -n "$ONLINE_TEMPLATE" ]]; then msg_info "Template appears corrupted – re-downloading" rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + download_template msg_ok "Template re-downloaded" else msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." @@ -5882,7 +5584,7 @@ create_lxc_container() { if grep -qiE 'unable to open|corrupt|invalid' "$LOGFILE"; then msg_info "Template may be corrupted – re-downloading" rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + download_template msg_ok "Template re-downloaded" fi @@ -5895,7 +5597,11 @@ create_lxc_container() { if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then msg_ok "Trying local storage fallback" msg_info "Downloading template to local" - pveam download local "$TEMPLATE" >/dev/null 2>&1 + if [[ "$ARCH" == "arm64" ]]; then + download_arm64_template "$LOCAL_TEMPLATE_PATH" + else + pveam download local "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 + fi msg_ok "Template downloaded to local" else msg_ok "Trying local storage fallback" @@ -5903,20 +5609,19 @@ create_lxc_container() { if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then # Local fallback also failed - check for LXC stack version issue if grep -qiE 'unsupported .* version' "$LOGFILE"; then - echo - echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template." - echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically." + msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template" offer_lxc_stack_upgrade_and_maybe_retry "yes" rc=$? case $rc in 0) : ;; # success - container created, continue 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" + msg_error "Upgrade declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve" + _flush_pct_log exit 231 ;; 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" + msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE" + _flush_pct_log exit 231 ;; esac @@ -5927,6 +5632,7 @@ create_lxc_container() { pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" set +x fi + _flush_pct_log exit 209 fi else @@ -5935,20 +5641,19 @@ create_lxc_container() { else # Already on local storage and still failed - check LXC stack version if grep -qiE 'unsupported .* version' "$LOGFILE"; then - echo - echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template." - echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically." + msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template" offer_lxc_stack_upgrade_and_maybe_retry "yes" rc=$? case $rc in 0) : ;; # success - container created, continue 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" + msg_error "Upgrade declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve" + _flush_pct_log exit 231 ;; 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" + msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE" + _flush_pct_log exit 231 ;; esac @@ -5959,6 +5664,7 @@ create_lxc_container() { pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" set +x fi + _flush_pct_log exit 209 fi fi @@ -5970,16 +5676,28 @@ create_lxc_container() { # Verify container exists pct list | awk '{print $1}' | grep -qx "$CTID" || { msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE" + _flush_pct_log exit 215 } # Verify config rootfs grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || { msg_error "RootFS entry missing in container config. See $LOGFILE" + _flush_pct_log exit 216 } msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created." + + # Append pct create log to BUILD_LOG for combined log visibility + if [[ -s "$LOGFILE" && -n "${BUILD_LOG:-}" ]]; then + { + echo "" + echo "--- pct create output ---" + cat "$LOGFILE" + echo "--- end pct create output ---" + } >>"$BUILD_LOG" 2>/dev/null || true + fi } # ============================================================================== @@ -6006,7 +5724,7 @@ description() { cat < - Logo + Logo

${APP} LXC

@@ -6019,15 +5737,15 @@ description() { - Git + GitHub - Discussions + Discussions - Issues + Issues EOF @@ -6101,44 +5819,21 @@ ensure_log_on_host() { fi } -# ------------------------------------------------------------------------------ -# api_exit_script() +# ============================================================================== +# TRAP MANAGEMENT +# ============================================================================== +# All traps (ERR, EXIT, INT, TERM, HUP) are set by catch_errors() in +# error_handler.func — called at the top of this file after sourcing. # -# - Exit trap handler for reporting to API telemetry -# - Captures exit code and reports to PocketBase using centralized error descriptions -# - Uses explain_exit_code() from api.func for consistent error messages -# - ALWAYS sends telemetry FIRST before log collection to prevent pct pull -# hangs from blocking status updates (container may be dead/unresponsive) -# - For non-zero exit codes: posts "failed" status -# - For zero exit codes where post_update_to_api was never called: -# catches orphaned "installing" records (e.g., script exited cleanly -# but description() was never reached) -# ------------------------------------------------------------------------------ -api_exit_script() { - local exit_code=$? - if [ $exit_code -ne 0 ]; then - # ALWAYS send telemetry FIRST - ensure status is reported even if - # ensure_log_on_host hangs (e.g. pct pull on dead container) - post_update_to_api "failed" "$exit_code" 2>/dev/null || true - # Best-effort log collection (non-critical after telemetry is sent) - if declare -f ensure_log_on_host >/dev/null 2>&1; then - ensure_log_on_host 2>/dev/null || true - fi - # Stop orphaned container if we're in the install phase - if [[ "${CONTAINER_INSTALLING:-}" == "true" && -n "${CTID:-}" ]] && command -v pct &>/dev/null; then - pct stop "$CTID" 2>/dev/null || true - fi - elif [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then - # Script exited with 0 but never sent a completion status - # exit_code=0 is never an error — report as success - post_update_to_api "done" "0" - fi -} - -if command -v pveversion >/dev/null 2>&1; then - trap 'api_exit_script' EXIT -fi -trap 'local _ec=$?; if [[ $_ec -ne 0 ]]; then post_update_to_api "failed" "$_ec" 2>/dev/null || true; if declare -f ensure_log_on_host &>/dev/null; then ensure_log_on_host 2>/dev/null || true; fi; fi' ERR -trap 'post_update_to_api "failed" "129" 2>/dev/null || true; if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null; then pct stop "$CTID" 2>/dev/null || true; fi; exit 129' SIGHUP -trap 'post_update_to_api "failed" "130" 2>/dev/null || true; if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null; then pct stop "$CTID" 2>/dev/null || true; fi; exit 130' SIGINT -trap 'post_update_to_api "failed" "143" 2>/dev/null || true; if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null; then pct stop "$CTID" 2>/dev/null || true; fi; exit 143' SIGTERM +# Do NOT set duplicate traps here. The handlers in error_handler.func +# (on_exit, on_interrupt, on_terminate, on_hangup, error_handler) already: +# - Send telemetry via post_update_to_api / _send_abort_telemetry +# - Stop orphaned containers via _stop_container_if_installing +# - Collect logs via ensure_log_on_host +# - Clean up lock files and spinner processes +# +# Previously, inline traps here overwrote catch_errors() traps, causing: +# - error_handler() never fired (no error output, no cleanup dialog) +# - on_hangup() never fired (SSH disconnect → stuck records) +# - Duplicated logic in two places (hard to debug) +# ============================================================================== diff --git a/misc/cloud-init.func b/misc/cloud-init.func index 0c8597f9b..054941f07 100644 --- a/misc/cloud-init.func +++ b/misc/cloud-init.func @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG # Author: community-scripts ORG -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/arm64-dev-build/LICENSE # Revision: 1 # ============================================================================== @@ -17,7 +17,7 @@ # - Cloud-Init status monitoring and waiting # # Usage: -# source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/cloud-init.func) +# source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/cloud-init.func) # setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes" # # Compatible with: Debian, Ubuntu, and all Cloud-Init enabled distributions diff --git a/misc/core.func b/misc/core.func index 39589c0c7..882e42661 100644 --- a/misc/core.func +++ b/misc/core.func @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG -# License: MIT | https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/LICENSE +# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/LICENSE # ============================================================================== # CORE FUNCTIONS - LXC CONTAINER UTILITIES @@ -276,7 +276,7 @@ shell_check() { msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell." echo -e "\nExiting..." sleep 2 - exit + exit 103 fi } @@ -293,7 +293,7 @@ root_check() { msg_error "Please run this script as root." echo -e "\nExiting..." sleep 2 - exit + exit 104 fi } @@ -314,7 +314,7 @@ pve_check() { if ((MINOR < 0 || MINOR > 9)); then msg_error "This version of Proxmox VE is not supported." msg_error "Supported: Proxmox VE version 8.0 – 8.9" - exit 1 + exit 105 fi return 0 fi @@ -325,7 +325,7 @@ pve_check() { if ((MINOR < 0 || MINOR > 1)); then msg_error "This version of Proxmox VE is not yet supported." msg_error "Supported: Proxmox VE version 9.0 – 9.1" - exit 1 + exit 105 fi return 0 fi @@ -333,7 +333,7 @@ pve_check() { # All other unsupported versions msg_error "This version of Proxmox VE is not supported." msg_error "Supported versions: Proxmox VE 8.0 – 8.9 or 9.0 – 9.1" - exit 1 + exit 105 } # ------------------------------------------------------------------------------ @@ -344,12 +344,12 @@ pve_check() { # - Provides link to ARM64-compatible scripts # ------------------------------------------------------------------------------ arch_check() { - if [ "$(dpkg --print-architecture)" != "amd64" ]; then - echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n" - echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n" - echo -e "Exiting..." + local arch + arch="$(dpkg --print-architecture)" + if [[ "$arch" != "amd64" && "$arch" != "arm64" ]]; then + msg_error "This script requires amd64 or arm64 (detected: $arch)." sleep 2 - exit + exit 106 fi } @@ -399,7 +399,6 @@ ssh_check() { # even after INSTALL_LOG is exported for the container) # - INSTALL_LOG: Container operations (application installation) # - BUILD_LOG: Host operations (container creation) - # - Fallback to BUILD_LOG if neither is set # ------------------------------------------------------------------------------ get_active_logfile() { @@ -490,9 +489,9 @@ log_section() { # # - Executes command with output redirected to active log file # - On error: displays last 20 lines of log and exits with original exit code - - # - Temporarily disables error trap to capture exit code correctly +# - Saves and restores previous error handling state (so callers that +# intentionally disabled error handling aren't silently re-enabled) # - Sources explain_exit_code() for detailed error messages # ------------------------------------------------------------------------------ silent() { @@ -510,19 +509,30 @@ silent() { return 0 fi + # Save current error handling state before disabling + # This prevents re-enabling error handling when the caller intentionally + # disabled it (e.g. build_container recovery section) + local _restore_errexit=false + [[ "$-" == *e* ]] && _restore_errexit=true + set +Eeuo pipefail trap - ERR "$@" >>"$logfile" 2>&1 local rc=$? - set -Eeuo pipefail - trap 'error_handler' ERR - + # Restore error handling ONLY if it was active before this call + if $_restore_errexit; then + set -Eeuo pipefail + trap 'error_handler' ERR + fi + if [[ $rc -ne 0 ]]; then # Source explain_exit_code if needed if ! declare -f explain_exit_code >/dev/null 2>&1; then - source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) + if ! source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/error_handler.func); then + explain_exit_code() { echo "unknown (error_handler.func download failed)"; } + fi fi local explanation @@ -543,6 +553,53 @@ silent() { fi } +# ------------------------------------------------------------------------------ +# apt_update_safe() +# +# - Runs apt-get update with graceful error handling +# - On failure: shows warning with common causes instead of aborting +# - Logs full output to active log file +# - Returns 0 even on failure so the caller can continue +# - Typical cause: enterprise repos returning 401 Unauthorized +# +# Usage: +# apt_update_safe # Warn on failure, continue without aborting +# ------------------------------------------------------------------------------ +apt_update_safe() { + local logfile + logfile="$(get_active_logfile)" + + local _restore_errexit=false + [[ "$-" == *e* ]] && _restore_errexit=true + + set +Eeuo pipefail + trap - ERR + + apt-get update >>"$logfile" 2>&1 + local rc=$? + + if $_restore_errexit; then + set -Eeuo pipefail + trap 'error_handler' ERR + fi + + if [[ $rc -ne 0 ]]; then + msg_warn "apt-get update exited with code ${rc} — some repositories may have failed." + + # Check log for common 401/403 enterprise repo issues + if grep -qiE '401\s*Unauthorized|403\s*Forbidden|enterprise\.proxmox\.com' "$logfile" 2>/dev/null; then + echo -e "${TAB}${INFO} ${YWB}Hint: Proxmox enterprise repository returned an auth error.${CL}" + echo -e "${TAB} If you don't have a subscription, you can disable the enterprise" + echo -e "${TAB} repo and use the no-subscription repo instead." + fi + + echo -e "${TAB}${INFO} ${YWB}Continuing despite partial update failure — packages may still be installable.${CL}" + echo "" + fi + + return 0 +} + # ------------------------------------------------------------------------------ # spinner() # @@ -599,6 +656,7 @@ stop_spinner() { unset SPINNER_PID SPINNER_MSG stty sane 2>/dev/null || true + stty -tostop 2>/dev/null || true } # ============================================================================== @@ -776,8 +834,8 @@ fatal() { # ------------------------------------------------------------------------------ exit_script() { clear - echo -e "\n${CROSS}${RD}User exited script${CL}\n" - exit + msg_error "User exited script" + exit 0 } # ------------------------------------------------------------------------------ @@ -791,13 +849,16 @@ exit_script() { get_header() { local app_name=$(echo "${APP,,}" | tr -d ' ') local app_type=${APP_TYPE:-ct} # Default to 'ct' if not set - local header_url="https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/${app_type}/headers/${app_name}" + local header_dir="${app_type}" + [[ "$app_type" == "addon" ]] && header_dir="tools" + local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/${header_dir}/headers/${app_name}" local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}" mkdir -p "$(dirname "$local_header_path")" if [ ! -s "$local_header_path" ]; then if ! curl -fsSL "$header_url" -o "$local_header_path"; then + msg_warn "Failed to download header: $header_url" return 1 fi fi @@ -838,10 +899,10 @@ header_info() { ensure_tput() { if ! command -v tput >/dev/null 2>&1; then if grep -qi 'alpine' /etc/os-release; then - apk add --no-cache ncurses >/dev/null 2>&1 + apk add --no-cache ncurses >/dev/null 2>&1 || msg_warn "Failed to install ncurses (tput may be unavailable)" elif command -v apt-get >/dev/null 2>&1; then apt-get update -qq >/dev/null - apt-get install -y -qq ncurses-bin >/dev/null 2>&1 + apt-get install -y -qq ncurses-bin >/dev/null 2>&1 || msg_warn "Failed to install ncurses-bin (tput may be unavailable)" fi fi } @@ -872,18 +933,13 @@ is_alpine() { # # - Determines if script should run in verbose mode # - Checks VERBOSE and var_verbose variables -# - Also returns true if not running in TTY (pipe/redirect scenario) # - Used by msg_info() to decide between spinner and static output +# - Note: Non-TTY (pipe) scenarios are handled separately in msg_info() +# to allow spinner output to pass through pipes (e.g. lxc-attach | tee) # ------------------------------------------------------------------------------ is_verbose_mode() { local verbose="${VERBOSE:-${var_verbose:-no}}" - local tty_status - if [[ -t 2 ]]; then - tty_status="interactive" - else - tty_status="not-a-tty" - fi - [[ "$verbose" != "no" || ! -t 2 ]] + [[ "$verbose" != "no" ]] } # ------------------------------------------------------------------------------ @@ -1301,6 +1357,7 @@ prompt_select() { # Validate options if [[ $num_options -eq 0 ]]; then + msg_warn "prompt_select called with no options" echo "" >&2 return 1 fi @@ -1514,6 +1571,7 @@ cleanup_lxc() { post_progress_to_api fi } + # ------------------------------------------------------------------------------ # check_or_create_swap() # @@ -1542,22 +1600,30 @@ check_or_create_swap() { local swap_size_mb swap_size_mb=$(prompt_input "Enter swap size in MB (e.g., 2048 for 2GB):" "2048" 60) if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then - msg_error "Invalid size input. Aborting." + msg_error "Invalid swap size: '${swap_size_mb}' (must be a number in MB)" return 1 fi local swap_file="/swapfile" msg_info "Creating ${swap_size_mb}MB swap file at $swap_file" - if dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=progress && - chmod 600 "$swap_file" && - mkswap "$swap_file" && - swapon "$swap_file"; then - msg_ok "Swap file created and activated successfully" - else - msg_error "Failed to create or activate swap" + if ! dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=progress; then + msg_error "Failed to allocate swap file (dd failed)" return 1 fi + if ! chmod 600 "$swap_file"; then + msg_error "Failed to set permissions on $swap_file" + return 1 + fi + if ! mkswap "$swap_file"; then + msg_error "Failed to format swap file (mkswap failed)" + return 1 + fi + if ! swapon "$swap_file"; then + msg_error "Failed to activate swap (swapon failed)" + return 1 + fi + msg_ok "Swap file created and activated successfully" } # ------------------------------------------------------------------------------ @@ -1639,7 +1705,7 @@ function get_lxc_ip() { LOCAL_IP="$(get_current_ip || true)" if [[ -z "$LOCAL_IP" ]]; then - msg_error "Could not determine LOCAL_IP" + msg_error "Could not determine LOCAL_IP (checked: eth0, hostname -I, ip route, IPv6 targets)" return 1 fi fi @@ -1651,17 +1717,4 @@ function get_lxc_ip() { # SIGNAL TRAPS # ============================================================================== -# ------------------------------------------------------------------------------ -# on_hup_keepalive() -# -# - SIGHUP (terminal hangup) trap handler -# - Keeps long-running scripts alive if terminal/SSH session disconnects -# - Stops spinner safely and writes warning to active log -# ------------------------------------------------------------------------------ -on_hup_keepalive() { - stop_spinner - log_msg "[WARN] Received SIGHUP (terminal hangup). Continuing execution in background." -} - -trap 'on_hup_keepalive' HUP trap 'stop_spinner' EXIT INT TERM diff --git a/misc/error_handler.func b/misc/error_handler.func index cea4639a1..07c77c746 100644 --- a/misc/error_handler.func +++ b/misc/error_handler.func @@ -4,7 +4,7 @@ # ------------------------------------------------------------------------------ # Copyright (c) 2021-2026 community-scripts ORG # Author: MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/arm64-dev-build/LICENSE # ------------------------------------------------------------------------------ # # Provides comprehensive error handling and signal management for all scripts. @@ -94,6 +94,29 @@ if ! declare -f explain_exit_code &>/dev/null; then 100) echo "APT: Package manager error (broken packages / dependency problems)" ;; 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;; 102) echo "APT: Lock held by another process (dpkg/apt still running)" ;; + + # --- Script Validation & Setup (103-123) --- + 103) echo "Validation: Shell is not Bash" ;; + 104) echo "Validation: Not running as root (or invoked via sudo)" ;; + 105) echo "Validation: Proxmox VE version not supported" ;; + 106) echo "Validation: Architecture not supported (ARM / PiMox)" ;; + 107) echo "Validation: Kernel key parameters unreadable" ;; + 108) echo "Validation: Kernel key limits exceeded" ;; + 109) echo "Proxmox: No available container ID after max attempts" ;; + 110) echo "Proxmox: Failed to apply default.vars" ;; + 111) echo "Proxmox: App defaults file not available" ;; + 112) echo "Proxmox: Invalid install menu option" ;; + 113) echo "LXC: Under-provisioned — user aborted update" ;; + 114) echo "LXC: Storage too low — user aborted update" ;; + 115) echo "Download: install.func download failed or incomplete" ;; + 116) echo "Proxmox: Default bridge vmbr0 not found" ;; + 117) echo "LXC: Container did not reach running state" ;; + 118) echo "LXC: No IP assigned to container after timeout" ;; + 119) echo "Proxmox: No valid storage for rootdir content" ;; + 120) echo "Proxmox: No valid storage for vztmpl content" ;; + 121) echo "LXC: Container network not ready (no IP after retries)" ;; + 122) echo "LXC: No internet connectivity — user declined to continue" ;; + 123) echo "LXC: Local IP detection failed" ;; 124) echo "Command timed out (timeout command)" ;; 125) echo "Command failed to start (Docker daemon or execution error)" ;; 126) echo "Command invoked cannot execute (permission problem?)" ;; @@ -155,6 +178,16 @@ if ! declare -f explain_exit_code &>/dev/null; then 224) echo "Proxmox: PBS storage is for backups only" ;; 225) echo "Proxmox: No template available for OS/Version" ;; 231) echo "Proxmox: LXC stack upgrade failed" ;; + + # --- Tools & Addon Scripts (232-238) --- + 232) echo "Tools: Wrong execution environment (run on PVE host, not inside LXC)" ;; + 233) echo "Tools: Application not installed (update prerequisite missing)" ;; + 234) echo "Tools: No LXC containers found or available" ;; + 235) echo "Tools: Backup or restore operation failed" ;; + 236) echo "Tools: Required hardware not detected" ;; + 237) echo "Tools: Dependency package installation failed" ;; + 238) echo "Tools: OS or distribution not supported for this addon" ;; + 239) echo "npm/Node.js: Unexpected runtime error or dependency failure" ;; 243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;; 245) echo "Node.js: Invalid command-line option" ;; @@ -162,6 +195,14 @@ if ! declare -f explain_exit_code &>/dev/null; then 247) echo "Node.js: Fatal internal error" ;; 248) echo "Node.js: Invalid C++ addon / N-API failure" ;; 249) echo "npm/pnpm/yarn: Unknown fatal error" ;; + + # --- Application Install/Update Errors (250-254) --- + 250) echo "App: Download failed or version not determined" ;; + 251) echo "App: File extraction failed (corrupt or incomplete archive)" ;; + 252) echo "App: Required file or resource not found" ;; + 253) echo "App: Data migration required — update aborted" ;; + 254) echo "App: User declined prompt or input timed out" ;; + 255) echo "DPKG: Fatal internal error" ;; *) echo "Unknown error" ;; esac @@ -199,11 +240,16 @@ error_handler() { return 0 fi + # Stop spinner and restore cursor FIRST — before any output + # This prevents spinner text overlapping with error messages + if declare -f stop_spinner >/dev/null 2>&1; then + stop_spinner 2>/dev/null || true + fi + printf "\e[?25h" + local explanation explanation="$(explain_exit_code "$exit_code")" - printf "\e[?25h" - # ALWAYS report failure to API immediately - don't wait for container checks # This ensures we capture failures that occur before/after container exists if declare -f post_update_to_api &>/dev/null; then @@ -281,6 +327,8 @@ error_handler() { echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}" fi + # Read user response + local response="" if read -t 60 -r response; then if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then echo "" @@ -359,9 +407,65 @@ _send_abort_telemetry() { command -v curl &>/dev/null || return 0 [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 [[ -z "${RANDOM_UUID:-}" ]] && return 0 - curl -fsS -m 5 -X POST "${TELEMETRY_URL:-https://telemetry.community-scripts.org/telemetry}" \ - -H "Content-Type: application/json" \ - -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-${app:-unknown}}\",\"status\":\"failed\",\"exit_code\":${exit_code}}" &>/dev/null || true + + # Collect last 200 log lines for error diagnosis (best-effort) + # Container context has no get_full_log(), so we gather as much as possible + local error_text="" + local logfile="" + if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then + logfile="${INSTALL_LOG}" + elif [[ -n "${SILENT_LOGFILE:-}" && -s "${SILENT_LOGFILE}" ]]; then + logfile="${SILENT_LOGFILE}" + fi + + if [[ -n "$logfile" ]]; then + error_text=$(tail -n 200 "$logfile" 2>/dev/null | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g; s/\\/\\\\/g; s/"/\\"/g; s/\r//g' | tr '\n' '|' | sed 's/|$//' | head -c 16384 | tr -d '\000-\010\013\014\016-\037\177') || true + fi + + # Prepend exit code explanation header (like build_error_string does on host) + local explanation="" + if declare -f explain_exit_code &>/dev/null; then + explanation=$(explain_exit_code "$exit_code" 2>/dev/null) || true + fi + if [[ -n "$explanation" && -n "$error_text" ]]; then + error_text="exit_code=${exit_code} | ${explanation}|---|${error_text}" + elif [[ -n "$explanation" && -z "$error_text" ]]; then + error_text="exit_code=${exit_code} | ${explanation}" + fi + + # Calculate duration if start time is available + local duration="" + if [[ -n "${DIAGNOSTICS_START_TIME:-}" ]]; then + duration=$(($(date +%s) - DIAGNOSTICS_START_TIME)) + fi + + # Categorize error if function is available (may not be in minimal container context) + local error_category="" + if declare -f categorize_error &>/dev/null; then + error_category=$(categorize_error "$exit_code" 2>/dev/null) || true + fi + + # Build JSON payload with error context + local payload + payload="{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-${app:-unknown}}\",\"status\":\"failed\",\"exit_code\":${exit_code}" + [[ -n "$error_text" ]] && payload="${payload},\"error\":\"${error_text}\"" + [[ -n "$error_category" ]] && payload="${payload},\"error_category\":\"${error_category}\"" + [[ -n "$duration" ]] && payload="${payload},\"duration\":${duration}" + payload="${payload}}" + + local api_url="${TELEMETRY_URL:-https://telemetry.community-scripts.org/telemetry}" + + # 2 attempts (retry once on failure) — original had no retry + local attempt + for attempt in 1 2; do + if curl -fsS -m 5 -X POST "$api_url" \ + -H "Content-Type: application/json" \ + -d "$payload" &>/dev/null; then + return 0 + fi + [[ $attempt -eq 1 ]] && sleep 1 + done + return 0 } # ------------------------------------------------------------------------------ @@ -437,6 +541,12 @@ on_exit() { # - Exits with code 130 (128 + SIGINT=2) # ------------------------------------------------------------------------------ on_interrupt() { + # Stop spinner and restore cursor before any output + if declare -f stop_spinner >/dev/null 2>&1; then + stop_spinner 2>/dev/null || true + fi + printf "\e[?25h" 2>/dev/null || true + _send_abort_telemetry "130" _stop_container_if_installing if declare -f msg_error >/dev/null 2>&1; then @@ -456,6 +566,12 @@ on_interrupt() { # - Exits with code 143 (128 + SIGTERM=15) # ------------------------------------------------------------------------------ on_terminate() { + # Stop spinner and restore cursor before any output + if declare -f stop_spinner >/dev/null 2>&1; then + stop_spinner 2>/dev/null || true + fi + printf "\e[?25h" 2>/dev/null || true + _send_abort_telemetry "143" _stop_container_if_installing if declare -f msg_error >/dev/null 2>&1; then @@ -478,6 +594,11 @@ on_terminate() { # - Exits with code 129 (128 + SIGHUP=1) # ------------------------------------------------------------------------------ on_hangup() { + # Stop spinner (no cursor restore needed — terminal is already gone) + if declare -f stop_spinner >/dev/null 2>&1; then + stop_spinner 2>/dev/null || true + fi + _send_abort_telemetry "129" _stop_container_if_installing exit 129 diff --git a/misc/install.func b/misc/install.func index 393118f5c..d98b4bce6 100644 --- a/misc/install.func +++ b/misc/install.func @@ -1,601 +1,99 @@ # Copyright (c) 2021-2026 community-scripts ORG # Author: tteck (tteckster) # Co-Author: MickLesk -# Co-Author: michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/arm64-dev-build/LICENSE # ============================================================================== -# INSTALL.FUNC - UNIFIED CONTAINER INSTALLATION & SETUP +# INSTALL.FUNC - CONTAINER INSTALLATION & SETUP # ============================================================================== # -# All-in-One install.func supporting official Proxmox LXC templates: -# - Debian, Ubuntu, Devuan (apt, systemd/sysvinit) -# - Alpine (apk, OpenRC) -# - Fedora, Rocky, AlmaLinux, CentOS Stream (dnf, systemd) -# - openSUSE (zypper, systemd) -# - Gentoo (emerge, OpenRC) -# - openEuler (dnf, systemd) +# This file provides installation functions executed inside LXC containers +# after creation. Handles: # -# Supported templates (pveam available): -# almalinux-9/10, alpine-3.x, centos-9-stream, debian-12/13, -# devuan-5, fedora-42, gentoo-current, openeuler-25, -# opensuse-15.x, rockylinux-9/10, ubuntu-22.04/24.04/25.04 -# -# Features: -# - Automatic OS detection -# - Unified package manager abstraction -# - Init system abstraction (systemd/OpenRC/sysvinit) -# - Network connectivity verification +# - Network connectivity verification (IPv4/IPv6) +# - OS updates and package installation +# - DNS resolution checks # - MOTD and SSH configuration -# - Container customization +# - Container customization and auto-login +# +# Usage: +# - Sourced by -install.sh scripts +# - Executes via pct exec inside container +# - Requires internet connectivity # # ============================================================================== # ============================================================================== -# SECTION 1: INITIALIZATION & OS DETECTION +# SECTION 1: INITIALIZATION # ============================================================================== -# Global variables for OS detection -OS_TYPE="" # debian, ubuntu, devuan, alpine, fedora, rocky, alma, centos, opensuse, gentoo, openeuler -OS_FAMILY="" # debian, alpine, rhel, suse, gentoo -OS_VERSION="" # Version number -PKG_MANAGER="" # apt, apk, dnf, yum, zypper, emerge -INIT_SYSTEM="" # systemd, openrc, sysvinit +if ! command -v curl >/dev/null 2>&1; then + printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 + apt update >/dev/null 2>&1 + apt install -y curl >/dev/null 2>&1 +fi +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/core.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/error_handler.func) +load_functions +catch_errors + +# Persist diagnostics setting inside container (exported from build.func) +# so addon scripts running later can find the user's choice +if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then + mkdir -p /usr/local/community-scripts + echo "DIAGNOSTICS=${DIAGNOSTICS:-no}" >/usr/local/community-scripts/diagnostics +fi + +# Get LXC IP address (must be called INSIDE container, after network is up) +get_lxc_ip # ------------------------------------------------------------------------------ -# detect_os() +# post_progress_to_api() # -# Detects the operating system and sets global variables: -# OS_TYPE, OS_FAMILY, OS_VERSION, PKG_MANAGER, INIT_SYSTEM +# - Lightweight progress ping from inside the container +# - Updates the existing telemetry record status +# - Arguments: +# * $1: status (optional, default: "configuring") +# - Signals that the installation is actively progressing (not stuck) +# - Fire-and-forget: never blocks or fails the script +# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set # ------------------------------------------------------------------------------ -detect_os() { - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . /etc/os-release - OS_TYPE="${ID:-unknown}" - OS_VERSION="${VERSION_ID:-unknown}" - elif [[ -f /etc/alpine-release ]]; then - OS_TYPE="alpine" - OS_VERSION=$(cat /etc/alpine-release) - elif [[ -f /etc/debian_version ]]; then - OS_TYPE="debian" - OS_VERSION=$(cat /etc/debian_version) - elif [[ -f /etc/redhat-release ]]; then - OS_TYPE="centos" - OS_VERSION=$(grep -oE '[0-9]+\.[0-9]+' /etc/redhat-release | head -1) - elif [[ -f /etc/arch-release ]]; then - OS_TYPE="arch" - OS_VERSION="rolling" - elif [[ -f /etc/gentoo-release ]]; then - OS_TYPE="gentoo" - OS_VERSION=$(cat /etc/gentoo-release | grep -oE '[0-9.]+') - else - OS_TYPE="unknown" - OS_VERSION="unknown" - fi +post_progress_to_api() { + command -v curl &>/dev/null || return 0 + [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 + [[ -z "${RANDOM_UUID:-}" ]] && return 0 - # Normalize OS type and determine family - case "$OS_TYPE" in - debian) - OS_FAMILY="debian" - PKG_MANAGER="apt" - ;; - ubuntu) - OS_FAMILY="debian" - PKG_MANAGER="apt" - ;; - devuan) - OS_FAMILY="debian" - PKG_MANAGER="apt" - ;; - alpine) - OS_FAMILY="alpine" - PKG_MANAGER="apk" - ;; - fedora) - OS_FAMILY="rhel" - PKG_MANAGER="dnf" - ;; - rocky | rockylinux) - OS_TYPE="rocky" - OS_FAMILY="rhel" - PKG_MANAGER="dnf" - ;; - alma | almalinux) - OS_TYPE="alma" - OS_FAMILY="rhel" - PKG_MANAGER="dnf" - ;; - centos) - OS_FAMILY="rhel" - # CentOS 7 uses yum, 8+ uses dnf - if [[ "${OS_VERSION%%.*}" -ge 8 ]]; then - PKG_MANAGER="dnf" - else - PKG_MANAGER="yum" - fi - ;; - rhel) - OS_FAMILY="rhel" - PKG_MANAGER="dnf" - ;; - openeuler) - OS_FAMILY="rhel" - PKG_MANAGER="dnf" - ;; - opensuse* | sles) - OS_TYPE="opensuse" - OS_FAMILY="suse" - PKG_MANAGER="zypper" - ;; - gentoo) - OS_FAMILY="gentoo" - PKG_MANAGER="emerge" - ;; - *) - OS_FAMILY="unknown" - PKG_MANAGER="unknown" - ;; - esac + local progress_status="${1:-configuring}" - # Detect init system - if command -v systemctl &>/dev/null && [[ -d /run/systemd/system ]]; then - INIT_SYSTEM="systemd" - elif command -v rc-service &>/dev/null || [[ -d /etc/init.d && -f /sbin/openrc ]]; then - INIT_SYSTEM="openrc" - elif [[ -f /etc/inittab ]]; then - INIT_SYSTEM="sysvinit" - else - INIT_SYSTEM="unknown" - fi -} - -# ------------------------------------------------------------------------------ -# Bootstrap: Ensure curl is available and source core functions -# ------------------------------------------------------------------------------ -_bootstrap() { - # Minimal bootstrap to get curl installed - if ! command -v curl &>/dev/null; then - printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 - if command -v apt-get &>/dev/null; then - apt-get update &>/dev/null && apt-get install -y curl &>/dev/null - elif command -v apk &>/dev/null; then - apk update &>/dev/null && apk add curl &>/dev/null - elif command -v dnf &>/dev/null; then - dnf install -y curl &>/dev/null - elif command -v yum &>/dev/null; then - yum install -y curl &>/dev/null - elif command -v zypper &>/dev/null; then - zypper install -y curl &>/dev/null - elif command -v emerge &>/dev/null; then - emerge --quiet net-misc/curl &>/dev/null - fi - fi - - # Configurable base URL for development — override with COMMUNITY_SCRIPTS_URL - COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main}" - - # Source core functions - source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/core.func") - source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/error_handler.func") - load_functions - catch_errors - - get_lxc_ip -} - -# Run bootstrap and OS detection -_bootstrap -detect_os - -# ============================================================================== -# SECTION 2: PACKAGE MANAGER ABSTRACTION -# ============================================================================== - -# ------------------------------------------------------------------------------ -# pkg_update() -# -# Updates package manager cache/database -# ------------------------------------------------------------------------------ -pkg_update() { - case "$PKG_MANAGER" in - apt) - $STD apt-get update - ;; - apk) - $STD apk update - ;; - dnf) - $STD dnf makecache - ;; - yum) - $STD yum makecache - ;; - zypper) - $STD zypper refresh - ;; - emerge) - $STD emerge --sync - ;; - *) - msg_error "Unknown package manager: $PKG_MANAGER" - return 1 - ;; - esac -} - -# ------------------------------------------------------------------------------ -# pkg_upgrade() -# -# Upgrades all installed packages -# ------------------------------------------------------------------------------ -pkg_upgrade() { - case "$PKG_MANAGER" in - apt) - $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade - ;; - apk) - $STD apk -U upgrade - ;; - dnf) - $STD dnf -y upgrade - ;; - yum) - $STD yum -y update - ;; - zypper) - $STD zypper -n update - ;; - emerge) - $STD emerge --quiet --update --deep @world - ;; - *) - msg_error "Unknown package manager: $PKG_MANAGER" - return 1 - ;; - esac -} - -# ------------------------------------------------------------------------------ -# pkg_install(packages...) -# -# Installs one or more packages -# Arguments: -# packages - List of packages to install -# ------------------------------------------------------------------------------ -pkg_install() { - local packages=("$@") - [[ ${#packages[@]} -eq 0 ]] && return 0 - - case "$PKG_MANAGER" in - apt) - $STD apt-get install -y "${packages[@]}" - ;; - apk) - $STD apk add --no-cache "${packages[@]}" - ;; - dnf) - $STD dnf install -y "${packages[@]}" - ;; - yum) - $STD yum install -y "${packages[@]}" - ;; - zypper) - $STD zypper install -y "${packages[@]}" - ;; - emerge) - $STD emerge --quiet "${packages[@]}" - ;; - *) - msg_error "Unknown package manager: $PKG_MANAGER" - return 1 - ;; - esac -} - -# ------------------------------------------------------------------------------ -# pkg_remove(packages...) -# -# Removes one or more packages -# ------------------------------------------------------------------------------ -pkg_remove() { - local packages=("$@") - [[ ${#packages[@]} -eq 0 ]] && return 0 - - case "$PKG_MANAGER" in - apt) - $STD apt-get remove -y "${packages[@]}" - ;; - apk) - $STD apk del "${packages[@]}" - ;; - dnf) - $STD dnf remove -y "${packages[@]}" - ;; - yum) - $STD yum remove -y "${packages[@]}" - ;; - zypper) - $STD zypper remove -y "${packages[@]}" - ;; - emerge) - $STD emerge --quiet --unmerge "${packages[@]}" - ;; - *) - msg_error "Unknown package manager: $PKG_MANAGER" - return 1 - ;; - esac -} - -# ------------------------------------------------------------------------------ -# pkg_clean() -# -# Cleans package manager cache to free space -# ------------------------------------------------------------------------------ -pkg_clean() { - case "$PKG_MANAGER" in - apt) - $STD apt-get autoremove -y - $STD apt-get autoclean - ;; - apk) - $STD apk cache clean - ;; - dnf) - $STD dnf clean all - $STD dnf autoremove -y - ;; - yum) - $STD yum clean all - ;; - zypper) - $STD zypper clean - ;; - emerge) - $STD emerge --quiet --depclean - ;; - *) - return 0 - ;; - esac + curl -fsS -m 5 -X POST "https://telemetry.community-scripts.org/telemetry" \ + -H "Content-Type: application/json" \ + -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"lxc\",\"nsapp\":\"${app:-unknown}\",\"status\":\"${progress_status}\"}" &>/dev/null || true } # ============================================================================== -# SECTION 3: SERVICE/INIT SYSTEM ABSTRACTION +# SECTION 2: NETWORK & CONNECTIVITY # ============================================================================== -# ------------------------------------------------------------------------------ -# svc_enable(service) -# -# Enables a service to start at boot -# ------------------------------------------------------------------------------ -svc_enable() { - local service="$1" - [[ -z "$service" ]] && return 1 - - case "$INIT_SYSTEM" in - systemd) - $STD systemctl enable "$service" - ;; - openrc) - $STD rc-update add "$service" default - ;; - sysvinit) - if command -v update-rc.d &>/dev/null; then - $STD update-rc.d "$service" defaults - elif command -v chkconfig &>/dev/null; then - $STD chkconfig "$service" on - fi - ;; - *) - msg_warn "Unknown init system, cannot enable $service" - return 1 - ;; - esac -} - -# ------------------------------------------------------------------------------ -# svc_disable(service) -# -# Disables a service from starting at boot -# ------------------------------------------------------------------------------ -svc_disable() { - local service="$1" - [[ -z "$service" ]] && return 1 - - case "$INIT_SYSTEM" in - systemd) - $STD systemctl disable "$service" - ;; - openrc) - $STD rc-update del "$service" default 2>/dev/null || true - ;; - sysvinit) - if command -v update-rc.d &>/dev/null; then - $STD update-rc.d "$service" remove - elif command -v chkconfig &>/dev/null; then - $STD chkconfig "$service" off - fi - ;; - *) - return 1 - ;; - esac -} - -# ------------------------------------------------------------------------------ -# svc_start(service) -# -# Starts a service immediately -# ------------------------------------------------------------------------------ -svc_start() { - local service="$1" - [[ -z "$service" ]] && return 1 - - case "$INIT_SYSTEM" in - systemd) - $STD systemctl start "$service" - ;; - openrc) - $STD rc-service "$service" start - ;; - sysvinit) - $STD /etc/init.d/"$service" start - ;; - *) - return 1 - ;; - esac -} - -# ------------------------------------------------------------------------------ -# svc_stop(service) -# -# Stops a running service -# ------------------------------------------------------------------------------ -svc_stop() { - local service="$1" - [[ -z "$service" ]] && return 1 - - case "$INIT_SYSTEM" in - systemd) - $STD systemctl stop "$service" - ;; - openrc) - $STD rc-service "$service" stop - ;; - sysvinit) - $STD /etc/init.d/"$service" stop - ;; - *) - return 1 - ;; - esac -} - -# ------------------------------------------------------------------------------ -# svc_restart(service) -# -# Restarts a service -# ------------------------------------------------------------------------------ -svc_restart() { - local service="$1" - [[ -z "$service" ]] && return 1 - - case "$INIT_SYSTEM" in - systemd) - $STD systemctl restart "$service" - ;; - openrc) - $STD rc-service "$service" restart - ;; - sysvinit) - $STD /etc/init.d/"$service" restart - ;; - *) - return 1 - ;; - esac -} - -# ------------------------------------------------------------------------------ -# svc_status(service) -# -# Gets service status (returns 0 if running) -# ------------------------------------------------------------------------------ -svc_status() { - local service="$1" - [[ -z "$service" ]] && return 1 - - case "$INIT_SYSTEM" in - systemd) - systemctl is-active --quiet "$service" - ;; - openrc) - rc-service "$service" status &>/dev/null - ;; - sysvinit) - /etc/init.d/"$service" status &>/dev/null - ;; - *) - return 1 - ;; - esac -} - -# ------------------------------------------------------------------------------ -# svc_reload_daemon() -# -# Reloads init system daemon configuration (for systemd) -# ------------------------------------------------------------------------------ -svc_reload_daemon() { - case "$INIT_SYSTEM" in - systemd) - $STD systemctl daemon-reload - ;; - *) - # Other init systems don't need this - return 0 - ;; - esac -} - -# ============================================================================== -# SECTION 4: NETWORK & CONNECTIVITY -# ============================================================================== - -# ------------------------------------------------------------------------------ -# get_ip() -# -# Gets the primary IPv4 address of the container -# Returns: IP address string -# ------------------------------------------------------------------------------ -get_ip() { - local ip="" - - # Try hostname -I first (most common) - if command -v hostname &>/dev/null; then - ip=$(hostname -I 2>/dev/null | awk '{print $1}' || true) - fi - - # Fallback to ip command - if [[ -z "$ip" ]] && command -v ip &>/dev/null; then - ip=$(ip -4 addr show scope global | awk '/inet /{print $2}' | cut -d/ -f1 | head -1) - fi - - # Fallback to ifconfig - if [[ -z "$ip" ]] && command -v ifconfig &>/dev/null; then - ip=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -1) - fi - - echo "$ip" -} - # ------------------------------------------------------------------------------ # verb_ip6() # -# Configures IPv6 based on IPV6_METHOD variable -# If IPV6_METHOD=disable: disables IPv6 via sysctl +# - Configures IPv6 based on DISABLEIPV6 variable +# - If DISABLEIPV6=yes: disables IPv6 via sysctl +# - Sets verbose mode via set_std_mode() # ------------------------------------------------------------------------------ verb_ip6() { set_std_mode # Set STD mode based on VERBOSE - if [[ "${IPV6_METHOD:-}" == "disable" ]]; then + if [ "${IPV6_METHOD:-}" = "disable" ]; then msg_info "Disabling IPv6 (this may affect some services)" mkdir -p /etc/sysctl.d - cat >/etc/sysctl.d/99-disable-ipv6.conf </dev/null </dev/null || true - fi msg_ok "Disabled IPv6" fi } @@ -603,58 +101,60 @@ EOF # ------------------------------------------------------------------------------ # setting_up_container() # -# Initial container setup: -# - Verifies network connectivity -# - Removes Python EXTERNALLY-MANAGED restrictions -# - Disables network wait services +# - Verifies network connectivity via hostname -I +# - Retries up to RETRY_NUM times with RETRY_EVERY seconds delay +# - Removes Python EXTERNALLY-MANAGED restrictions +# - Disables systemd-networkd-wait-online.service for faster boot +# - Exits with error if network unavailable after retries # ------------------------------------------------------------------------------ setting_up_container() { msg_info "Setting up Container OS" - # Wait for network - local i + # Fix Debian 13 LXC template bug where / is owned by nobody + # Only attempt in privileged containers (unprivileged cannot chown /) + if [[ "$(stat -c '%U' /)" != "root" ]]; then + (chown root:root / 2>/dev/null) || true + fi + for ((i = RETRY_NUM; i > 0; i--)); do - if [[ -n "$(get_ip)" ]]; then + if [ "$(hostname -I)" != "" ]; then break fi echo 1>&2 -en "${CROSS}${RD} No Network! " - sleep "$RETRY_EVERY" + sleep $RETRY_EVERY done - - if [[ -z "$(get_ip)" ]]; then + if [ "$(hostname -I)" = "" ]; then echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}" echo -e "${NETWORK}Check Network Settings" - exit 1 + exit 121 fi - - # Remove Python EXTERNALLY-MANAGED restriction (Debian 12+, Ubuntu 23.04+) - rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED 2>/dev/null || true - - # Disable network wait services for faster boot - case "$INIT_SYSTEM" in - systemd) - systemctl disable -q --now systemd-networkd-wait-online.service 2>/dev/null || true - ;; - esac - + rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED + systemctl disable -q --now systemd-networkd-wait-online.service msg_ok "Set up Container OS" - msg_ok "Network Connected: ${BL}$(get_ip)" + #msg_custom "${CM}" "${GN}" "Network Connected: ${BL}$(hostname -I)" + msg_ok "Network Connected: ${BL}$(hostname -I)" + post_progress_to_api } # ------------------------------------------------------------------------------ # network_check() # -# Comprehensive network connectivity check for IPv4 and IPv6 -# Tests connectivity to DNS servers and verifies DNS resolution +# - Comprehensive network connectivity check for IPv4 and IPv6 +# - Tests connectivity to multiple DNS servers: +# * IPv4: 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), 9.9.9.9 (Quad9) +# * IPv6: 2606:4700:4700::1111, 2001:4860:4860::8888, 2620:fe::fe +# - Verifies DNS resolution for GitHub and Community-Scripts domains +# - Prompts user to continue if no internet detected +# - Uses fatal() on DNS resolution failure for critical hosts # ------------------------------------------------------------------------------ network_check() { set +e trap - ERR - local ipv4_connected=false - local ipv6_connected=false + ipv4_connected=false + ipv6_connected=false sleep 1 - # Check IPv4 connectivity + # Check IPv4 connectivity to Google, Cloudflare & Quad9 DNS servers. if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then msg_ok "IPv4 Internet Connected" ipv4_connected=true @@ -662,40 +162,37 @@ network_check() { msg_error "IPv4 Internet Not Connected" fi - # Check IPv6 connectivity (if ping6 exists) - if command -v ping6 &>/dev/null; then - if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null; then - msg_ok "IPv6 Internet Connected" - ipv6_connected=true - else - msg_error "IPv6 Internet Not Connected" - fi + # Check IPv6 connectivity to Google, Cloudflare & Quad9 DNS servers. + if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null || ping6 -c 1 -W 1 2620:fe::fe &>/dev/null; then + msg_ok "IPv6 Internet Connected" + ipv6_connected=true + else + msg_error "IPv6 Internet Not Connected" fi - # Prompt if both fail + # If both IPv4 and IPv6 checks fail, prompt the user if [[ $ipv4_connected == false && $ipv6_connected == false ]]; then read -r -p "No Internet detected, would you like to continue anyway? " prompt if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then echo -e "${INFO}${RD}Expect Issues Without Internet${CL}" else echo -e "${NETWORK}Check Network Settings" - exit 1 + exit 122 fi fi - # DNS resolution checks - local GIT_HOSTS=("github.com" "raw.githubusercontent.com" "git.community-scripts.org") - local GIT_STATUS="Git DNS:" - local DNS_FAILED=false + # DNS resolution checks for GitHub-related domains (IPv4 and/or IPv6) + GIT_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com" "git.community-scripts.org") + GIT_STATUS="Git DNS:" + DNS_FAILED=false for HOST in "${GIT_HOSTS[@]}"; do - local RESOLVEDIP - RESOLVEDIP=$(getent hosts "$HOST" 2>/dev/null | awk '{ print $1 }' | head -n1) + RESOLVEDIP=$(getent hosts "$HOST" | awk '{ print $1 }' | grep -E '(^([0-9]{1,3}\.){3}[0-9]{1,3}$)|(^[a-fA-F0-9:]+$)' | head -n1) if [[ -z "$RESOLVEDIP" ]]; then - GIT_STATUS+=" $HOST:(${DNSFAIL:-FAIL})" + GIT_STATUS+="$HOST:($DNSFAIL)" DNS_FAILED=true else - GIT_STATUS+=" $HOST:(${DNSOK:-OK})" + GIT_STATUS+=" $HOST:($DNSOK)" fi done @@ -706,23 +203,25 @@ network_check() { fi set -e - trap 'error_handler $LINENO "$BASH_COMMAND"' ERR + trap 'error_handler' ERR } # ============================================================================== -# SECTION 5: OS UPDATE & PACKAGE MANAGEMENT +# SECTION 3: OS UPDATE & PACKAGE MANAGEMENT # ============================================================================== # ------------------------------------------------------------------------------ # update_os() # -# Updates container OS and sources appropriate tools.func +# - Updates container OS via apt-get update and dist-upgrade +# - Configures APT cacher proxy if CACHER=yes (accelerates package downloads) +# - Removes Python EXTERNALLY-MANAGED restrictions for pip +# - Sources tools.func for additional setup functions after update +# - Uses $STD wrapper to suppress output unless VERBOSE=yes # ------------------------------------------------------------------------------ update_os() { msg_info "Updating Container OS" - - # Configure APT cacher proxy if enabled (Debian/Ubuntu only) - if [[ "$PKG_MANAGER" == "apt" && "${CACHER:-}" == "yes" ]]; then + if [[ "$CACHER" == "yes" ]]; then echo 'Acquire::http::Proxy-Auto-Detect "/usr/local/bin/apt-proxy-detect.sh";' >/etc/apt/apt.conf.d/00aptproxy cat </usr/local/bin/apt-proxy-detect.sh #!/bin/bash @@ -734,275 +233,101 @@ fi EOF chmod +x /usr/local/bin/apt-proxy-detect.sh fi - - # Update and upgrade - pkg_update - pkg_upgrade - - # Remove Python EXTERNALLY-MANAGED restriction - rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED 2>/dev/null || true - + apt_update_safe + $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade + $STD apt-get install -y sudo curl mc gnupg2 openssh-server wget gcc + rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED msg_ok "Updated Container OS" + post_progress_to_api - # Source appropriate tools.func based on OS - case "$OS_FAMILY" in - alpine) - source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/alpine-tools.func") - ;; - *) - source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/tools.func") - ;; - esac + local tools_content + tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/tools.func) || { + msg_error "Failed to download tools.func" + exit 115 + } + source /dev/stdin <<<"$tools_content" + if ! declare -f fetch_and_deploy_gh_release >/dev/null 2>&1; then + msg_error "tools.func loaded but incomplete — missing expected functions" + exit 115 + fi } # ============================================================================== -# SECTION 6: MOTD & SSH CONFIGURATION +# SECTION 4: MOTD & SSH CONFIGURATION # ============================================================================== # ------------------------------------------------------------------------------ # motd_ssh() # -# Configures Message of the Day and SSH settings +# - Configures Message of the Day (MOTD) with container information +# - Creates /etc/profile.d/00_lxc-details.sh with: +# * Application name +# * Warning banner (DEV repository) +# * OS name and version +# * Hostname and IP address +# * GitHub repository link +# - Disables executable flag on /etc/update-motd.d/* scripts +# - Enables root SSH access if SSH_ROOT=yes +# - Configures TERM environment variable for better terminal support # ------------------------------------------------------------------------------ motd_ssh() { # Set terminal to 256-color mode - grep -qxF "export TERM='xterm-256color'" /root/.bashrc 2>/dev/null || echo "export TERM='xterm-256color'" >>/root/.bashrc + grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc - # Get OS information - local os_name="$OS_TYPE" - local os_version="$OS_VERSION" + PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" + echo "echo -e \"\"" >"$PROFILE_FILE" + echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVED${CL}\"" >>"$PROFILE_FILE" + echo "echo \"\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}\$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE" - if [[ -f /etc/os-release ]]; then - os_name=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - os_version=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"') - fi - - # Create MOTD profile script - local PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" - cat >"$PROFILE_FILE" </dev/null | awk '{print \$1}' || ip -4 addr show scope global | awk '/inet /{print \$2}' | cut -d/ -f1 | head -1)${CL:-}" -echo -e "${YW:-} Repository: ${GN:-}https://github.com/community-scripts/ProxmoxVED${CL:-}" -echo "" -EOF - - # Disable default MOTD scripts (Debian/Ubuntu) - [[ -d /etc/update-motd.d ]] && chmod -x /etc/update-motd.d/* 2>/dev/null || true - - # Configure SSH root access if requested - if [[ "${SSH_ROOT:-}" == "yes" ]]; then - # Ensure SSH server is installed - if [[ ! -f /etc/ssh/sshd_config ]]; then - msg_info "Installing SSH server" - case "$PKG_MANAGER" in - apt) - pkg_install openssh-server - ;; - apk) - pkg_install openssh - rc-update add sshd default 2>/dev/null || true - ;; - dnf | yum) - pkg_install openssh-server - ;; - zypper) - pkg_install openssh - ;; - emerge) - pkg_install net-misc/openssh - ;; - esac - msg_ok "Installed SSH server" - fi - - local sshd_config="/etc/ssh/sshd_config" - if [[ -f "$sshd_config" ]]; then - sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" "$sshd_config" - sed -i "s/PermitRootLogin prohibit-password/PermitRootLogin yes/g" "$sshd_config" - - case "$INIT_SYSTEM" in - systemd) - svc_restart sshd 2>/dev/null || svc_restart ssh 2>/dev/null || true - ;; - openrc) - svc_enable sshd 2>/dev/null || true - svc_start sshd 2>/dev/null || true - ;; - *) - svc_restart sshd 2>/dev/null || true - ;; - esac - fi + # Disable default MOTD scripts + chmod -x /etc/update-motd.d/* + + if [[ "${SSH_ROOT}" == "yes" ]]; then + sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config + systemctl restart sshd fi + post_progress_to_api } # ============================================================================== -# SECTION 7: CONTAINER CUSTOMIZATION +# SECTION 5: CONTAINER CUSTOMIZATION # ============================================================================== # ------------------------------------------------------------------------------ # customize() # -# Customizes container for passwordless login and creates update script +# - Customizes container for passwordless root login if PASSWORD is empty +# - Configures getty for auto-login via /etc/systemd/system/container-getty@1.service.d/override.conf +# - Creates /usr/bin/update script for easy application updates +# - Injects SSH authorized keys if SSH_AUTHORIZED_KEY variable is set +# - Sets proper permissions on SSH directories and key files # ------------------------------------------------------------------------------ customize() { - if [[ "${PASSWORD:-}" == "" ]]; then + if [[ "$PASSWORD" == "" ]]; then msg_info "Customizing Container" - - # Remove root password for auto-login - passwd -d root &>/dev/null || true - - case "$INIT_SYSTEM" in - systemd) - # Mask services that block boot in LXC containers - # systemd-homed-firstboot.service hangs waiting for user input on Fedora - systemctl mask systemd-homed-firstboot.service &>/dev/null || true - systemctl mask systemd-homed.service &>/dev/null || true - - # Configure console-getty for auto-login in LXC containers - # console-getty.service is THE service that handles /dev/console in LXC - # It's present on all systemd distros but not enabled by default on Fedora/RHEL - - if [[ -f /usr/lib/systemd/system/console-getty.service ]]; then - mkdir -p /etc/systemd/system/console-getty.service.d - cat >/etc/systemd/system/console-getty.service.d/override.conf <<'EOF' -[Service] -ExecStart= -ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud 115200,38400,9600 - $TERM + GETTY_OVERRIDE="/etc/systemd/system/container-getty@1.service.d/override.conf" + mkdir -p $(dirname $GETTY_OVERRIDE) + cat <$GETTY_OVERRIDE + [Service] + ExecStart= + ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM EOF - # Enable console-getty for LXC web console (required on Fedora/RHEL) - systemctl enable console-getty.service &>/dev/null || true - fi - - # Also configure container-getty@1 (Debian/Ubuntu default in LXC) - 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 <<'EOF' -[Service] -ExecStart= -ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 - $TERM -EOF - fi - - # Reload systemd and restart getty services to apply auto-login - systemctl daemon-reload - systemctl restart console-getty.service &>/dev/null || true - systemctl restart container-getty@1.service &>/dev/null || true - ;; - - openrc) - # Alpine/Gentoo: modify inittab for auto-login - if [[ -f /etc/inittab ]]; then - sed -i 's|^tty1::respawn:.*|tty1::respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab - fi - touch /root/.hushlogin - ;; - - sysvinit) - # Devuan/older systems - modify inittab for auto-login - # Devuan 5 (daedalus) uses SysVinit with various inittab formats - # LXC can use /dev/console OR /dev/tty1 depending on how pct console connects - if [[ -f /etc/inittab ]]; then - # Backup original inittab - cp /etc/inittab /etc/inittab.bak 2>/dev/null || true - - # Enable autologin on tty1 (for direct access) - handle various formats - # Devuan uses format: 1:2345:respawn:/sbin/getty 38400 tty1 - sed -i 's|^\(1:[0-9]*:respawn:\).*getty.*tty1.*|1:2345:respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab - - # CRITICAL: Add/replace console entry for LXC - this is what pct console uses! - # Remove any existing console entries first (commented or not) - sed -i '/^[^#]*:.*:respawn:.*getty.*console/d' /etc/inittab - sed -i '/^# LXC console autologin/d' /etc/inittab - - # Add new console entry for LXC at the end - echo "" >>/etc/inittab - echo "# LXC console autologin (added by community-scripts)" >>/etc/inittab - echo "co:2345:respawn:/sbin/agetty --autologin root --noclear console 115200 linux" >>/etc/inittab - - # Force a reload of inittab and respawn ALL getty processes - # Kill ALL getty processes to force respawn with new autologin settings - pkill -9 -f '[ag]etty' &>/dev/null || true - - # Small delay to let init notice the dead processes - sleep 1 - - # Reload inittab - try multiple methods - telinit q &>/dev/null || init q &>/dev/null || kill -HUP 1 &>/dev/null || true - fi - touch /root/.hushlogin - ;; - esac - + systemctl daemon-reload + systemctl restart $(basename $(dirname $GETTY_OVERRIDE) | sed 's/\.d//') msg_ok "Customized Container" fi - - # Create update script - # Use var_os for OS-based containers, otherwise use app name - local update_script_name="${var_os:-$app}" - echo "bash -c \"\$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/ct/${update_script_name}.sh)\"" >/usr/bin/update + echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/ct/${app}.sh)\"" >/usr/bin/update chmod +x /usr/bin/update - # Inject SSH authorized keys if provided - if [[ -n "${SSH_AUTHORIZED_KEY:-}" ]]; then + if [[ -n "${SSH_AUTHORIZED_KEY}" ]]; then mkdir -p /root/.ssh echo "${SSH_AUTHORIZED_KEY}" >/root/.ssh/authorized_keys chmod 700 /root/.ssh chmod 600 /root/.ssh/authorized_keys fi -} - -# ============================================================================== -# SECTION 8: UTILITY FUNCTIONS -# ============================================================================== - -# ------------------------------------------------------------------------------ -# validate_tz(timezone) -# -# Validates if a timezone is valid -# Returns: 0 if valid, 1 if invalid -# ------------------------------------------------------------------------------ -validate_tz() { - local tz="$1" - [[ -f "/usr/share/zoneinfo/$tz" ]] -} - -# ------------------------------------------------------------------------------ -# set_timezone(timezone) -# -# Sets container timezone -# ------------------------------------------------------------------------------ -set_timezone() { - local tz="$1" - if validate_tz "$tz"; then - ln -sf "/usr/share/zoneinfo/$tz" /etc/localtime - echo "$tz" >/etc/timezone 2>/dev/null || true - - # Update tzdata if available - case "$PKG_MANAGER" in - apt) - dpkg-reconfigure -f noninteractive tzdata 2>/dev/null || true - ;; - esac - msg_ok "Timezone set to $tz" - else - msg_warn "Invalid timezone: $tz" - fi -} - -# ------------------------------------------------------------------------------ -# os_info() -# -# Prints detected OS information (for debugging) -# ------------------------------------------------------------------------------ -os_info() { - echo "OS Type: $OS_TYPE" - echo "OS Family: $OS_FAMILY" - echo "OS Version: $OS_VERSION" - echo "Pkg Manager: $PKG_MANAGER" - echo "Init System: $INIT_SYSTEM" + post_progress_to_api } diff --git a/misc/tools.func b/misc/tools.func index e2486f636..c4de1036c 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -934,7 +934,11 @@ upgrade_package() { # ------------------------------------------------------------------------------ # Repository availability check with caching # ------------------------------------------------------------------------------ -declare -A _REPO_CACHE 2>/dev/null || true +# Note: Must use -gA (global) because tools.func is sourced inside update_os() +# function scope. Plain 'declare -A' would create a local variable that gets +# destroyed when update_os() returns, causing "unbound variable" errors later +# when setup_postgresql/verify_repo_available tries to access the cache key. +declare -gA _REPO_CACHE 2>/dev/null || declare -A _REPO_CACHE 2>/dev/null || true verify_repo_available() { local repo_url="$1" @@ -1732,6 +1736,13 @@ setup_deb822_repo() { rm -f "$tmp_gpg" return 1 } + else + # Already binary — copy directly + cp -f "$tmp_gpg" "/etc/apt/keyrings/${name}.gpg" || { + msg_error "Failed to install GPG key for ${name}" + rm -f "$tmp_gpg" + return 1 + } fi rm -f "$tmp_gpg" chmod 644 "/etc/apt/keyrings/${name}.gpg" @@ -3256,7 +3267,7 @@ function fetch_and_deploy_gh_release() { fi if [[ -z "$url_match" ]]; then for u in $assets; do - if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then + if [[ "$u" =~ ($arch|aarch64|arm64).*\.deb$ ]]; then url_match="$u" break fi @@ -5142,7 +5153,7 @@ current_ip="$(get_current_ip)" if [[ -z "$current_ip" ]]; then echo "[ERROR] Could not detect local IP" >&2 - exit 1 + exit 123 fi if [[ -f "$IP_FILE" ]]; then @@ -5643,20 +5654,20 @@ function setup_mongodb() { # - Handles Debian Trixie libaio1t64 transition # # Variables: -# USE_MYSQL_REPO - Set to "true" to use official MySQL repository -# (default: false, uses distro packages) +# USE_MYSQL_REPO - Use official MySQL repository (default: true) +# Set to "false" to use distro packages instead # MYSQL_VERSION - MySQL version to install when using official repo # (e.g. 8.0, 8.4) (default: 8.0) # # Examples: -# setup_mysql # Uses distro package (recommended) -# USE_MYSQL_REPO=true setup_mysql # Uses official MySQL repo -# USE_MYSQL_REPO=true MYSQL_VERSION="8.4" setup_mysql # Specific version +# setup_mysql # Uses official MySQL repo, 8.0 +# MYSQL_VERSION="8.4" setup_mysql # Specific version from MySQL repo +# USE_MYSQL_REPO=false setup_mysql # Uses distro package instead # ------------------------------------------------------------------------------ function setup_mysql() { local MYSQL_VERSION="${MYSQL_VERSION:-8.0}" - local USE_MYSQL_REPO="${USE_MYSQL_REPO:-false}" + local USE_MYSQL_REPO="${USE_MYSQL_REPO:-true}" local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) @@ -6357,21 +6368,21 @@ EOF # - Restores dumped data post-upgrade # # Variables: -# USE_PGDG_REPO - Set to "true" to use official PGDG repository -# (default: false, uses distro packages) +# USE_PGDG_REPO - Use official PGDG repository (default: true) +# Set to "false" to use distro packages instead # PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16) # PG_MODULES - Comma-separated list of modules (e.g. "postgis,contrib") # # Examples: -# setup_postgresql # Uses distro package (recommended) -# USE_PGDG_REPO=true setup_postgresql # Uses official PGDG repo -# USE_PGDG_REPO=true PG_VERSION="17" setup_postgresql # Specific version from PGDG +# setup_postgresql # Uses PGDG repo, PG 16 +# PG_VERSION="17" setup_postgresql # Specific version from PGDG +# USE_PGDG_REPO=false setup_postgresql # Uses distro package instead # ------------------------------------------------------------------------------ function setup_postgresql() { local PG_VERSION="${PG_VERSION:-16}" local PG_MODULES="${PG_MODULES:-}" - local USE_PGDG_REPO="${USE_PGDG_REPO:-false}" + local USE_PGDG_REPO="${USE_PGDG_REPO:-true}" local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) @@ -6489,14 +6500,13 @@ function setup_postgresql() { local SUITE case "$DISTRO_CODENAME" in trixie | forky | sid) - if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then SUITE="trixie-pgdg" - else - SUITE="bookworm-pgdg" + msg_warn "PGDG repo not available for ${DISTRO_CODENAME}, falling back to distro packages" + USE_PGDG_REPO=false setup_postgresql + return $? fi - ;; *) SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt") @@ -7989,7 +7999,7 @@ EOF # ------------------------------------------------------------------------------ function fetch_and_deploy_from_url() { local url="$1" - local directory="$2" + local directory="${2:-}" if [[ -z "$url" ]]; then msg_error "URL parameter is required" @@ -8121,422 +8131,3 @@ function fetch_and_deploy_from_url() { msg_ok "Successfully deployed archive to $directory" return 0 } - -function fetch_and_deploy_gl_release() { - local app="$1" - local repo="$2" - local mode="${3:-tarball}" - local version="${var_appversion:-${4:-latest}}" - local target="${5:-/opt/$app}" - local asset_pattern="${6:-}" - - if [[ -z "$app" ]]; then - app="${repo##*/}" - if [[ -z "$app" ]]; then - msg_error "fetch_and_deploy_gl_release requires app name or valid repo" - return 1 - fi - fi - - local app_lc=$(echo "${app,,}" | tr -d ' ') - local version_file="$HOME/.${app_lc}" - - local api_timeout="--connect-timeout 10 --max-time 60" - local download_timeout="--connect-timeout 15 --max-time 900" - - local current_version="" - [[ -f "$version_file" ]] && current_version=$(<"$version_file") - - ensure_dependencies jq - - local repo_encoded - repo_encoded=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$repo" 2>/dev/null \ - || echo "$repo" | sed 's|/|%2F|g') - - local api_base="https://gitlab.com/api/v4/projects/$repo_encoded/releases" - local api_url - if [[ "$version" != "latest" ]]; then - api_url="$api_base/$version" - else - api_url="$api_base?per_page=1&order_by=released_at&sort=desc" - fi - - local header=() - [[ -n "${GITLAB_TOKEN:-}" ]] && header=(-H "PRIVATE-TOKEN: $GITLAB_TOKEN") - - local max_retries=3 retry_delay=2 attempt=1 success=false http_code - - while ((attempt <= max_retries)); do - http_code=$(curl $api_timeout -sSL -w "%{http_code}" -o /tmp/gl_rel.json "${header[@]}" "$api_url" 2>/dev/null) || true - if [[ "$http_code" == "200" ]]; then - success=true - break - elif [[ "$http_code" == "429" ]]; then - if ((attempt < max_retries)); then - msg_warn "GitLab API rate limit hit, retrying in ${retry_delay}s... (attempt $attempt/$max_retries)" - sleep "$retry_delay" - retry_delay=$((retry_delay * 2)) - fi - else - sleep "$retry_delay" - fi - ((attempt++)) - done - - if ! $success; then - if [[ "$http_code" == "401" ]]; then - msg_error "GitLab API authentication failed (HTTP 401)." - if [[ -n "${GITLAB_TOKEN:-}" ]]; then - msg_error "Your GITLAB_TOKEN appears to be invalid or expired." - else - msg_error "The repository may require authentication. Try: export GITLAB_TOKEN=\"glpat-your_token\"" - fi - elif [[ "$http_code" == "404" ]]; then - msg_error "GitLab project or release not found (HTTP 404)." - msg_error "Ensure '$repo' is correct and the project is accessible." - elif [[ "$http_code" == "429" ]]; then - msg_error "GitLab API rate limit exceeded (HTTP 429)." - msg_error "To increase the limit, export a GitLab token before running the script:" - msg_error " export GITLAB_TOKEN=\"glpat-your_token_here\"" - elif [[ "$http_code" == "000" || -z "$http_code" ]]; then - msg_error "GitLab API connection failed (no response)." - msg_error "Check your network/DNS: curl -sSL https://gitlab.com/api/v4/version" - else - msg_error "Failed to fetch release metadata (HTTP $http_code)" - fi - return 1 - fi - - local json tag_name - json=$(/dev/null || uname -m) - [[ "$arch" == "x86_64" ]] && arch="amd64" - [[ "$arch" == "aarch64" ]] && arch="arm64" - - local assets url_match="" - assets=$(_gl_asset_urls "$json") - - if [[ -n "$asset_pattern" ]]; then - for u in $assets; do - case "${u##*/}" in - $asset_pattern) - url_match="$u" - break - ;; - esac - done - fi - - if [[ -z "$url_match" ]]; then - for u in $assets; do - if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then - url_match="$u" - break - fi - done - fi - - if [[ -z "$url_match" ]]; then - for u in $assets; do - [[ "$u" =~ \.deb$ ]] && url_match="$u" && break - done - fi - - if [[ -z "$url_match" ]]; then - local fallback_json - if fallback_json=$(_gl_scan_older_releases "$repo" "$repo_encoded" "https://gitlab.com" "binary" "$asset_pattern" "$tag_name"); then - json="$fallback_json" - tag_name=$(echo "$json" | jq -r '.tag_name // empty') - [[ "$tag_name" =~ ^v[0-9] ]] && version="${tag_name:1}" || version="$tag_name" - msg_info "Fetching GitLab release: $app ($version)" - assets=$(_gl_asset_urls "$json") - if [[ -n "$asset_pattern" ]]; then - for u in $assets; do - case "${u##*/}" in $asset_pattern) - url_match="$u"; break ;; - esac - done - fi - if [[ -z "$url_match" ]]; then - for u in $assets; do - [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]] && url_match="$u" && break - done - fi - if [[ -z "$url_match" ]]; then - for u in $assets; do - [[ "$u" =~ \.deb$ ]] && url_match="$u" && break - done - fi - fi - fi - - if [[ -z "$url_match" ]]; then - msg_error "No suitable .deb asset found for $app" - rm -rf "$tmpdir" - return 1 - fi - - filename="${url_match##*/}" - curl $download_timeout -fsSL "${header[@]}" -o "$tmpdir/$filename" "$url_match" || { - msg_error "Download failed: $url_match" - rm -rf "$tmpdir" - return 1 - } - - chmod 644 "$tmpdir/$filename" - local dpkg_opts="" - [[ "${DPKG_FORCE_CONFOLD:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confold" - [[ "${DPKG_FORCE_CONFNEW:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confnew" - DEBIAN_FRONTEND=noninteractive SYSTEMD_OFFLINE=1 $STD apt install -y $dpkg_opts "$tmpdir/$filename" || { - SYSTEMD_OFFLINE=1 $STD dpkg -i "$tmpdir/$filename" || { - msg_error "Both apt and dpkg installation failed" - rm -rf "$tmpdir" - return 1 - } - } - - ### Prebuild Mode ### - elif [[ "$mode" == "prebuild" ]]; then - local pattern="${6%\"}" - pattern="${pattern#\"}" - [[ -z "$pattern" ]] && { - msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)" - rm -rf "$tmpdir" - return 1 - } - - local asset_url="" - for u in $(_gl_asset_urls "$json"); do - filename_candidate="${u##*/}" - case "$filename_candidate" in - $pattern) - asset_url="$u" - break - ;; - esac - done - - if [[ -z "$asset_url" ]]; then - local fallback_json - if fallback_json=$(_gl_scan_older_releases "$repo" "$repo_encoded" "https://gitlab.com" "prebuild" "$pattern" "$tag_name"); then - json="$fallback_json" - tag_name=$(echo "$json" | jq -r '.tag_name // empty') - [[ "$tag_name" =~ ^v[0-9] ]] && version="${tag_name:1}" || version="$tag_name" - msg_info "Fetching GitLab release: $app ($version)" - for u in $(_gl_asset_urls "$json"); do - filename_candidate="${u##*/}" - case "$filename_candidate" in $pattern) - asset_url="$u"; break ;; - esac - done - fi - fi - - [[ -z "$asset_url" ]] && { - msg_error "No asset matching '$pattern' found" - rm -rf "$tmpdir" - return 1 - } - - filename="${asset_url##*/}" - curl $download_timeout -fsSL "${header[@]}" -o "$tmpdir/$filename" "$asset_url" || { - msg_error "Download failed: $asset_url" - rm -rf "$tmpdir" - return 1 - } - - local unpack_tmp - unpack_tmp=$(mktemp -d) - mkdir -p "$target" - if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then - rm -rf "${target:?}/"* - fi - - if [[ "$filename" == *.zip ]]; then - ensure_dependencies unzip - unzip -q "$tmpdir/$filename" -d "$unpack_tmp" || { - msg_error "Failed to extract ZIP archive" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - } - elif [[ "$filename" == *.tar.* || "$filename" == *.tgz || "$filename" == *.txz ]]; then - tar --no-same-owner -xf "$tmpdir/$filename" -C "$unpack_tmp" || { - msg_error "Failed to extract TAR archive" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - } - else - msg_error "Unsupported archive format: $filename" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - fi - - local top_entries inner_dir - top_entries=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1) - if [[ "$(echo "$top_entries" | wc -l)" -eq 1 && -d "$top_entries" ]]; then - inner_dir="$top_entries" - shopt -s dotglob nullglob - if compgen -G "$inner_dir/*" >/dev/null; then - cp -r "$inner_dir"/* "$target/" || { - msg_error "Failed to copy contents from $inner_dir to $target" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - } - else - msg_error "Inner directory is empty: $inner_dir" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - fi - shopt -u dotglob nullglob - else - shopt -s dotglob nullglob - if compgen -G "$unpack_tmp/*" >/dev/null; then - cp -r "$unpack_tmp"/* "$target/" || { - msg_error "Failed to copy contents to $target" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - } - else - msg_error "Unpacked archive is empty" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - fi - shopt -u dotglob nullglob - fi - - ### Singlefile Mode ### - elif [[ "$mode" == "singlefile" ]]; then - local pattern="${6%\"}" - pattern="${pattern#\"}" - [[ -z "$pattern" ]] && { - msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)" - rm -rf "$tmpdir" - return 1 - } - - local asset_url="" - for u in $(_gl_asset_urls "$json"); do - filename_candidate="${u##*/}" - case "$filename_candidate" in - $pattern) - asset_url="$u" - break - ;; - esac - done - - if [[ -z "$asset_url" ]]; then - local fallback_json - if fallback_json=$(_gl_scan_older_releases "$repo" "$repo_encoded" "https://gitlab.com" "singlefile" "$pattern" "$tag_name"); then - json="$fallback_json" - tag_name=$(echo "$json" | jq -r '.tag_name // empty') - [[ "$tag_name" =~ ^v[0-9] ]] && version="${tag_name:1}" || version="$tag_name" - msg_info "Fetching GitLab release: $app ($version)" - for u in $(_gl_asset_urls "$json"); do - filename_candidate="${u##*/}" - case "$filename_candidate" in $pattern) - asset_url="$u"; break ;; - esac - done - fi - fi - - [[ -z "$asset_url" ]] && { - msg_error "No asset matching '$pattern' found" - rm -rf "$tmpdir" - return 1 - } - - filename="${asset_url##*/}" - mkdir -p "$target" - - local use_filename="${USE_ORIGINAL_FILENAME:-false}" - local target_file="$app" - [[ "$use_filename" == "true" ]] && target_file="$filename" - - curl $download_timeout -fsSL "${header[@]}" -o "$target/$target_file" "$asset_url" || { - msg_error "Download failed: $asset_url" - rm -rf "$tmpdir" - return 1 - } - - if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then - chmod +x "$target/$target_file" - fi - - else - msg_error "Unknown mode: $mode" - rm -rf "$tmpdir" - return 1 - fi - - echo "$version" >"$version_file" - msg_ok "Deployed: $app ($version)" - rm -rf "$tmpdir" -} \ No newline at end of file diff --git a/misc/vm-core.func b/misc/vm-core.func index f40576de4..2867f648d 100644 --- a/misc/vm-core.func +++ b/misc/vm-core.func @@ -1,5 +1,5 @@ # Copyright (c) 2021-2026 community-scripts ORG -# License: MIT | https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/LICENSE +# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/LICENSE set -euo pipefail SPINNER_PID="" @@ -35,7 +35,7 @@ load_functions() { get_header() { local app_name=$(echo "${APP,,}" | tr ' ' '-') local app_type=${APP_TYPE:-vm} - local header_url="https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/${app_type}/headers/${app_name}" + local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/${app_type}/headers/${app_name}" local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}" mkdir -p "$(dirname "$local_header_path")" @@ -190,7 +190,7 @@ silent() { if [[ $rc -ne 0 ]]; then # Source explain_exit_code if needed if ! declare -f explain_exit_code >/dev/null 2>&1; then - source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/error_handler.func") 2>/dev/null || true + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/arm64-dev-build/misc/error_handler.func) 2>/dev/null || true fi local explanation="" @@ -244,7 +244,7 @@ curl_handler() { if [[ -z "$url" ]]; then msg_error "no valid url or option entered for curl_handler" - exit 1 + exit 64 fi $STD msg_info "Fetching: $url" @@ -273,7 +273,7 @@ curl_handler() { rm -f /tmp/curl_error.log fi __curl_err_handler "$exit_code" "$url" "$curl_stderr" - exit 1 # hard exit if exit_code is not 0 + exit "$exit_code" fi $STD printf "\r\033[K${INFO}${YW}Retry $attempt/$max_retries in ${delay}s...${CL}" >&2 @@ -316,7 +316,7 @@ __curl_err_handler() { esac [[ -n "$curl_msg" ]] && printf "%s\n" "$curl_msg" >&2 - exit 1 + exit "$exit_code" } # ------------------------------------------------------------------------------ @@ -331,7 +331,7 @@ shell_check() { msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell." echo -e "\nExiting..." sleep 2 - exit + exit 103 fi } @@ -352,11 +352,11 @@ clear_line() { # # - Determines if script should run in verbose mode # - Checks VERBOSE and var_verbose variables -# - Also returns true if not running in TTY (pipe/redirect scenario) +# - Note: Non-TTY (pipe) scenarios are handled separately in msg_info() # ------------------------------------------------------------------------------ is_verbose_mode() { local verbose="${VERBOSE:-${var_verbose:-no}}" - [[ "$verbose" != "no" || ! -t 2 ]] + [[ "$verbose" != "no" ]] } ### dev spinner ### @@ -552,7 +552,7 @@ check_root() { msg_error "Please run this script as root." echo -e "\nExiting..." sleep 2 - exit + exit 104 fi } @@ -562,7 +562,7 @@ pve_check() { echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1." echo -e "Exiting..." sleep 2 - exit + exit 105 fi } @@ -572,21 +572,21 @@ arch_check() { echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n" echo -e "Exiting..." sleep 2 - exit + exit 106 fi } exit_script() { clear echo -e "\n${CROSS}${RD}User exited script${CL}\n" - exit + exit 0 } check_hostname_conflict() { local hostname="$1" if qm list | awk '{print $2}' | grep -qx "$hostname"; then msg_error "Hostname $hostname already in use by another VM." - exit 1 + exit 206 fi } @@ -595,7 +595,7 @@ set_description() { cat < - Logo + Logo

${NSAPP} VM