From f90e28101675523c51474cf1c70db7cfba8d3fd8 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:28:11 +0200 Subject: [PATCH] Normalize indentation in core scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reformat scripts/core/alpine-install.func and scripts/core/api.func for consistent indentation and whitespace. Changes are formatting-only (no logic changes): converted leading tabs to spaces, aligned multiline blocks, and adjusted a comment reference (error-handler → error_handler) for consistency. Improves readability and maintainability without affecting behavior. --- scripts/core/alpine-install.func | 300 +++--- scripts/core/api.func | 1668 +++++++++++++++--------------- scripts/core/build.func | 234 +++-- scripts/core/core.func | 38 +- scripts/core/error-handler.func | 18 +- scripts/core/install.func | 587 ++++++----- scripts/core/tools.func | 92 +- 7 files changed, 1561 insertions(+), 1376 deletions(-) diff --git a/scripts/core/alpine-install.func b/scripts/core/alpine-install.func index 0e0c798..f342120 100644 --- a/scripts/core/alpine-install.func +++ b/scripts/core/alpine-install.func @@ -4,7 +4,7 @@ # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE if ! command -v curl >/dev/null 2>&1; then - apk update && apk add curl >/dev/null 2>&1 + apk update && apk add curl >/dev/null 2>&1 fi source "$(dirname "${BASH_SOURCE[0]}")/core.func" source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func" @@ -14,8 +14,8 @@ 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 + 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) @@ -33,208 +33,208 @@ get_lxc_ip # - 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 + command -v curl &>/dev/null || return 0 + [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 + [[ -z "${RANDOM_UUID:-}" ]] && return 0 - local progress_status="${1:-configuring}" + 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 + 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 + set_std_mode # Set STD mode based on VERBOSE - 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 - $STD sysctl -w net.ipv6.conf.lo.disable_ipv6=1 - mkdir -p /etc/sysctl.d - $STD tee /etc/sysctl.d/99-disable-ipv6.conf >/dev/null </dev/null <&2 -en "${CROSS}${RD} No Network! " - sleep $RETRY_EVERY - i=$((i - 1)) - done + msg_info "Setting up Container OS" + while [ $i -gt 0 ]; do + if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" != "" ]; then + break + fi + echo 1>&2 -en "${CROSS}${RD} No Network! " + sleep $RETRY_EVERY + i=$((i - 1)) + done - 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 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 + 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 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 network_check() { - set +e - trap - ERR - ipv4_connected=false + set +e + trap - ERR + ipv4_connected=false - # Check IPv4 connectivity to Cloudflare, Google & 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 - else - msg_error "IPv4 Internet Not Connected" - fi + # Check IPv4 connectivity to Cloudflare, Google & 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 + else + msg_error "IPv4 Internet Not Connected" + fi - if [[ $ipv4_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 122 - fi - fi + if [[ $ipv4_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 122 + fi + fi - # DNS resolution checks for GitHub-related domains - GIT_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com" "git.community-scripts.org") - GIT_STATUS="Git DNS:" - DNS_FAILED=false + # DNS resolution checks for GitHub-related domains + 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 - 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)" - DNS_FAILED=true - else - GIT_STATUS+=" $HOST:($DNSOK)" - fi - done + for HOST in "${GIT_HOSTS[@]}"; do + 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)" + DNS_FAILED=true + else + GIT_STATUS+=" $HOST:($DNSOK)" + fi + done - if [[ "$DNS_FAILED" == true ]]; then - fatal "$GIT_STATUS" - else - msg_ok "$GIT_STATUS" - fi + if [[ "$DNS_FAILED" == true ]]; then + fatal "$GIT_STATUS" + else + msg_ok "$GIT_STATUS" + fi - set -e - trap 'error_handler $LINENO "$BASH_COMMAND"' ERR + set -e + trap 'error_handler $LINENO "$BASH_COMMAND"' ERR } # This function updates the Container OS by running apk upgrade with mirror fallback update_os() { - msg_info "Updating Container OS" - if ! $STD apk -U upgrade; then - msg_warn "apk update failed (dl-cdn.alpinelinux.org), trying alternate mirrors..." - local alpine_mirrors="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org" - local apk_ok=false - for m in $(printf '%s\n' $alpine_mirrors | shuf); do - if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then - msg_custom "${INFO}" "${YW}" "Attempting mirror: ${m}" - cat </etc/apk/repositories + msg_info "Updating Container OS" + if ! $STD apk -U upgrade; then + msg_warn "apk update failed (dl-cdn.alpinelinux.org), trying alternate mirrors..." + local alpine_mirrors="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org" + local apk_ok=false + for m in $(printf '%s\n' $alpine_mirrors | shuf); do + if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then + msg_custom "${INFO}" "${YW}" "Attempting mirror: ${m}" + cat </etc/apk/repositories http://$m/alpine/latest-stable/main http://$m/alpine/latest-stable/community EOF - if $STD apk -U upgrade; then - msg_ok "CDN set to ${m}: tests passed" - apk_ok=true - break - else - msg_warn "Mirror ${m} failed" - fi - fi - done - if [[ "$apk_ok" != true ]]; then - msg_error "All Alpine mirrors failed. Check network or try again later." - exit 1 - fi - fi - local tools_content - tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/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 + if $STD apk -U upgrade; then + msg_ok "CDN set to ${m}: tests passed" + apk_ok=true + break + else + msg_warn "Mirror ${m} failed" + fi + fi + done + if [[ "$apk_ok" != true ]]; then + msg_error "All Alpine mirrors failed. Check network or try again later." + exit 1 + fi + fi + local tools_content + tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/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 + echo "export TERM='xterm-256color'" >>/root/.bashrc - 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/ProxmoxVE${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" + 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/ProxmoxVE${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 + # 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 validate_tz() { - [[ -f "/usr/share/zoneinfo/$1" ]] + [[ -f "/usr/share/zoneinfo/$1" ]] } # This function customizes the container and enables passwordless login for the root user customize() { - if [[ "$PASSWORD" == "" ]]; then - msg_info "Customizing Container" - passwd -d root >/dev/null 2>&1 + if [[ "$PASSWORD" == "" ]]; then + msg_info "Customizing Container" + passwd -d root >/dev/null 2>&1 - # Ensure agetty is available - apk add --no-cache --force-broken-world util-linux >/dev/null 2>&1 + # Ensure agetty is available + apk add --no-cache --force-broken-world util-linux >/dev/null 2>&1 - # Create persistent autologin boot script - mkdir -p /etc/local.d - cat <<'EOF' >/etc/local.d/autologin.start + # Create persistent autologin boot script + mkdir -p /etc/local.d + cat <<'EOF' >/etc/local.d/autologin.start #!/bin/sh sed -i 's|^tty1::respawn:.*|tty1::respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab kill -HUP 1 EOF - touch /root/.hushlogin + touch /root/.hushlogin - chmod +x /etc/local.d/autologin.start - rc-update add local >/dev/null 2>&1 + chmod +x /etc/local.d/autologin.start + rc-update add local >/dev/null 2>&1 - # Apply autologin immediately for current session - /etc/local.d/autologin.start + # Apply autologin immediately for current session + /etc/local.d/autologin.start - msg_ok "Customized Container" - fi + msg_ok "Customized Container" + fi - echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update - chmod +x /usr/bin/update - post_progress_to_api + echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update + chmod +x /usr/bin/update + post_progress_to_api } diff --git a/scripts/core/api.func b/scripts/core/api.func index 6e820e5..e3d9978 100644 --- a/scripts/core/api.func +++ b/scripts/core/api.func @@ -59,52 +59,52 @@ STATUS_TIMEOUT=10 # - Skips detection if REPO_SOURCE is already set (e.g., by environment) # ------------------------------------------------------------------------------ detect_repo_source() { - # Allow explicit override via environment - [[ -n "${REPO_SOURCE:-}" ]] && return 0 + # Allow explicit override via environment + [[ -n "${REPO_SOURCE:-}" ]] && return 0 - local content="" owner_repo="" + local content="" owner_repo="" - # Method 1: Read from /proc/$$/cmdline - # When invoked via: bash -c "$(curl -fsSL https://.../ct/app.sh)" - # the full CT/VM script content is in /proc/$$/cmdline (same PID through source chain) - if [[ -r /proc/$$/cmdline ]]; then - content=$(tr '\0' ' ' /dev/null) || true - fi + # Method 1: Read from /proc/$$/cmdline + # When invoked via: bash -c "$(curl -fsSL https://.../ct/app.sh)" + # the full CT/VM script content is in /proc/$$/cmdline (same PID through source chain) + if [[ -r /proc/$$/cmdline ]]; then + content=$(tr '\0' ' ' /dev/null) || true + fi - # Method 2: Read from the original script file (bash ct/app.sh / bash vm/app.sh) - if [[ -z "$content" ]] || ! echo "$content" | grep -qE 'githubusercontent\.com|community-scripts\.org' 2>/dev/null; then - if [[ -f "$0" ]] && [[ "$0" != *bash* ]]; then - content=$(head -10 "$0" 2>/dev/null) || true - fi - fi + # Method 2: Read from the original script file (bash ct/app.sh / bash vm/app.sh) + if [[ -z "$content" ]] || ! echo "$content" | grep -qE 'githubusercontent\.com|community-scripts\.org' 2>/dev/null; then + if [[ -f "$0" ]] && [[ "$0" != *bash* ]]; then + content=$(head -10 "$0" 2>/dev/null) || true + fi + fi - # Extract owner/repo from URL patterns found in the script content - if [[ -n "$content" ]]; then - # GitHub raw URL: raw.githubusercontent.com/OWNER/REPO/... - owner_repo=$(echo "$content" | grep -oE 'raw\.githubusercontent\.com/[^/]+/[^/]+' | head -1 | sed 's|raw\.githubusercontent\.com/||') || true + # Extract owner/repo from URL patterns found in the script content + if [[ -n "$content" ]]; then + # GitHub raw URL: raw.githubusercontent.com/OWNER/REPO/... + owner_repo=$(echo "$content" | grep -oE 'raw\.githubusercontent\.com/[^/]+/[^/]+' | head -1 | sed 's|raw\.githubusercontent\.com/||') || true - # Gitea URL: git.community-scripts.org/OWNER/REPO/... - if [[ -z "$owner_repo" ]]; then - owner_repo=$(echo "$content" | grep -oE 'git\.community-scripts\.org/[^/]+/[^/]+' | head -1 | sed 's|git\.community-scripts\.org/||') || true - fi - fi + # Gitea URL: git.community-scripts.org/OWNER/REPO/... + if [[ -z "$owner_repo" ]]; then + owner_repo=$(echo "$content" | grep -oE 'git\.community-scripts\.org/[^/]+/[^/]+' | head -1 | sed 's|git\.community-scripts\.org/||') || true + fi + fi - # Map detected owner/repo to canonical repo_source value - case "$owner_repo" in - community-scripts/ProxmoxVE) REPO_SOURCE="ProxmoxVE" ;; - community-scripts/ProxmoxVED) REPO_SOURCE="ProxmoxVED" ;; - "") - # No URL detected — use hardcoded fallback - # This value must match the repo: ProxmoxVE for production, ProxmoxVED for dev - REPO_SOURCE="ProxmoxVE" - ;; - *) - # Fork or unknown repo - REPO_SOURCE="external" - ;; - esac + # Map detected owner/repo to canonical repo_source value + case "$owner_repo" in + community-scripts/ProxmoxVE) REPO_SOURCE="ProxmoxVE" ;; + community-scripts/ProxmoxVED) REPO_SOURCE="ProxmoxVED" ;; + "") + # No URL detected — use hardcoded fallback + # This value must match the repo: ProxmoxVE for production, ProxmoxVED for dev + REPO_SOURCE="ProxmoxVE" + ;; + *) + # Fork or unknown repo + REPO_SOURCE="external" + ;; + esac - export REPO_SOURCE + export REPO_SOURCE } # Run detection immediately when api.func is sourced @@ -119,7 +119,7 @@ detect_repo_source # # - Maps numeric exit codes to human-readable error descriptions # - Canonical source of truth for ALL exit code mappings -# - Used by both api.func (telemetry) and error-handler.func (error display) +# - Used by both api.func (telemetry) and error_handler.func (error display) # - Supports: # * 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) @@ -138,204 +138,204 @@ detect_repo_source # - Returns description string for given exit code # ------------------------------------------------------------------------------ explain_exit_code() { - local code="$1" - case "$code" in - # --- Generic / Shell --- - 1) echo "General error / Operation not permitted" ;; - 2) echo "Misuse of shell builtins (e.g. syntax error)" ;; - 3) echo "General syntax or argument error" ;; - 10) echo "Docker / privileged mode required (unsupported environment)" ;; + local code="$1" + case "$code" in + # --- Generic / Shell --- + 1) echo "General error / Operation not permitted" ;; + 2) echo "Misuse of shell builtins (e.g. syntax error)" ;; + 3) echo "General syntax or argument error" ;; + 10) echo "Docker / privileged mode required (unsupported environment)" ;; - # --- curl / wget errors (commonly seen in downloads) --- - 4) echo "curl: Feature not supported or protocol error" ;; - 5) echo "curl: Could not resolve proxy" ;; - 6) echo "curl: DNS resolution failed (could not resolve host)" ;; - 7) echo "curl: Failed to connect (network unreachable / host down)" ;; - 8) echo "curl: Server reply error (FTP/SFTP or apk untrusted key)" ;; - 16) echo "curl: HTTP/2 framing layer error" ;; - 18) echo "curl: Partial file (transfer not completed)" ;; - 22) echo "curl: HTTP error returned (404, 429, 500+)" ;; - 23) echo "curl: Write error (disk full or permissions)" ;; - 24) echo "curl: Write to local file failed" ;; - 25) echo "curl: Upload failed" ;; - 26) echo "curl: Read error on local file (I/O)" ;; - 27) echo "curl: Out of memory (memory allocation failed)" ;; - 28) echo "curl: Operation timeout (network slow or server not responding)" ;; - 30) echo "curl: FTP port command failed" ;; - 32) echo "curl: FTP SIZE command failed" ;; - 33) echo "curl: HTTP range error" ;; - 34) echo "curl: HTTP post error" ;; - 35) echo "curl: SSL/TLS handshake failed (certificate error)" ;; - 36) echo "curl: FTP bad download resume" ;; - 39) echo "curl: LDAP search failed" ;; - 44) echo "curl: Internal error (bad function call order)" ;; - 45) echo "curl: Interface error (failed to bind to specified interface)" ;; - 46) echo "curl: Bad password entered" ;; - 47) echo "curl: Too many redirects" ;; - 48) echo "curl: Unknown command line option specified" ;; - 51) echo "curl: SSL peer certificate or SSH host key verification failed" ;; - 52) echo "curl: Empty reply from server (got nothing)" ;; - 55) echo "curl: Failed sending network data" ;; - 56) echo "curl: Receive error (connection reset by peer)" ;; - 57) echo "curl: Unrecoverable poll/select error (system I/O failure)" ;; - 59) echo "curl: Couldn't use specified SSL cipher" ;; - 61) echo "curl: Bad/unrecognized transfer encoding" ;; - 63) echo "curl: Maximum file size exceeded" ;; - 75) echo "Temporary failure (retry later)" ;; - 78) echo "curl: Remote file not found (404 on FTP/file)" ;; - 79) echo "curl: SSH session error (key exchange/auth failed)" ;; - 92) echo "curl: HTTP/2 stream error (protocol violation)" ;; - 95) echo "curl: HTTP/3 layer error" ;; + # --- curl / wget errors (commonly seen in downloads) --- + 4) echo "curl: Feature not supported or protocol error" ;; + 5) echo "curl: Could not resolve proxy" ;; + 6) echo "curl: DNS resolution failed (could not resolve host)" ;; + 7) echo "curl: Failed to connect (network unreachable / host down)" ;; + 8) echo "curl: Server reply error (FTP/SFTP or apk untrusted key)" ;; + 16) echo "curl: HTTP/2 framing layer error" ;; + 18) echo "curl: Partial file (transfer not completed)" ;; + 22) echo "curl: HTTP error returned (404, 429, 500+)" ;; + 23) echo "curl: Write error (disk full or permissions)" ;; + 24) echo "curl: Write to local file failed" ;; + 25) echo "curl: Upload failed" ;; + 26) echo "curl: Read error on local file (I/O)" ;; + 27) echo "curl: Out of memory (memory allocation failed)" ;; + 28) echo "curl: Operation timeout (network slow or server not responding)" ;; + 30) echo "curl: FTP port command failed" ;; + 32) echo "curl: FTP SIZE command failed" ;; + 33) echo "curl: HTTP range error" ;; + 34) echo "curl: HTTP post error" ;; + 35) echo "curl: SSL/TLS handshake failed (certificate error)" ;; + 36) echo "curl: FTP bad download resume" ;; + 39) echo "curl: LDAP search failed" ;; + 44) echo "curl: Internal error (bad function call order)" ;; + 45) echo "curl: Interface error (failed to bind to specified interface)" ;; + 46) echo "curl: Bad password entered" ;; + 47) echo "curl: Too many redirects" ;; + 48) echo "curl: Unknown command line option specified" ;; + 51) echo "curl: SSL peer certificate or SSH host key verification failed" ;; + 52) echo "curl: Empty reply from server (got nothing)" ;; + 55) echo "curl: Failed sending network data" ;; + 56) echo "curl: Receive error (connection reset by peer)" ;; + 57) echo "curl: Unrecoverable poll/select error (system I/O failure)" ;; + 59) echo "curl: Couldn't use specified SSL cipher" ;; + 61) echo "curl: Bad/unrecognized transfer encoding" ;; + 63) echo "curl: Maximum file size exceeded" ;; + 75) echo "Temporary failure (retry later)" ;; + 78) echo "curl: Remote file not found (404 on FTP/file)" ;; + 79) echo "curl: SSH session error (key exchange/auth failed)" ;; + 92) echo "curl: HTTP/2 stream error (protocol violation)" ;; + 95) echo "curl: HTTP/3 layer error" ;; - # --- Package manager / APT / DPKG --- - 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)" ;; + # --- Package manager / APT / DPKG --- + 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" ;; + # --- 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" ;; - # --- BSD sysexits.h (64-78) --- - 64) echo "Usage error (wrong arguments)" ;; - 65) echo "Data format error (bad input data)" ;; - 66) echo "Input file not found (cannot open input)" ;; - 67) echo "User not found (addressee unknown)" ;; - 68) echo "Host not found (hostname unknown)" ;; - 69) echo "Service unavailable" ;; - 70) echo "Internal software error" ;; - 71) echo "System error (OS-level failure)" ;; - 72) echo "Critical OS file missing" ;; - 73) echo "Cannot create output file" ;; - 74) echo "I/O error" ;; - 76) echo "Remote protocol error" ;; - 77) echo "Permission denied" ;; + # --- BSD sysexits.h (64-78) --- + 64) echo "Usage error (wrong arguments)" ;; + 65) echo "Data format error (bad input data)" ;; + 66) echo "Input file not found (cannot open input)" ;; + 67) echo "User not found (addressee unknown)" ;; + 68) echo "Host not found (hostname unknown)" ;; + 69) echo "Service unavailable" ;; + 70) echo "Internal software error" ;; + 71) echo "System error (OS-level failure)" ;; + 72) echo "Critical OS file missing" ;; + 73) echo "Cannot create output file" ;; + 74) echo "I/O error" ;; + 76) echo "Remote protocol error" ;; + 77) echo "Permission denied" ;; - # --- Common shell/system errors --- - 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?)" ;; - 127) echo "Command not found" ;; - 128) echo "Invalid argument to exit" ;; - 129) echo "Killed by SIGHUP (terminal closed / hangup)" ;; - 130) echo "Aborted by user (SIGINT)" ;; - 131) echo "Killed by SIGQUIT (core dumped)" ;; - 132) echo "Killed by SIGILL (illegal CPU instruction)" ;; - 134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;; - 137) echo "Killed (SIGKILL / Out of memory?)" ;; - 139) echo "Segmentation fault (core dumped)" ;; - 141) echo "Broken pipe (SIGPIPE - output closed prematurely)" ;; - 143) echo "Terminated (SIGTERM)" ;; - 144) echo "Killed by signal 16 (SIGUSR1 / SIGSTKFLT)" ;; - 146) echo "Killed by signal 18 (SIGTSTP)" ;; + # --- Common shell/system errors --- + 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?)" ;; + 127) echo "Command not found" ;; + 128) echo "Invalid argument to exit" ;; + 129) echo "Killed by SIGHUP (terminal closed / hangup)" ;; + 130) echo "Aborted by user (SIGINT)" ;; + 131) echo "Killed by SIGQUIT (core dumped)" ;; + 132) echo "Killed by SIGILL (illegal CPU instruction)" ;; + 134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;; + 137) echo "Killed (SIGKILL / Out of memory?)" ;; + 139) echo "Segmentation fault (core dumped)" ;; + 141) echo "Broken pipe (SIGPIPE - output closed prematurely)" ;; + 143) echo "Terminated (SIGTERM)" ;; + 144) echo "Killed by signal 16 (SIGUSR1 / SIGSTKFLT)" ;; + 146) echo "Killed by signal 18 (SIGTSTP)" ;; - # --- Systemd / Service errors (150-154) --- - 150) echo "Systemd: Service failed to start" ;; - 151) echo "Systemd: Service unit not found" ;; - 152) echo "Permission denied (EACCES)" ;; - 153) echo "Build/compile failed (make/gcc/cmake)" ;; - 154) echo "Node.js: Native addon build failed (node-gyp)" ;; - # --- Python / pip / uv (160-162) --- - 160) echo "Python: Virtualenv / uv environment missing or broken" ;; - 161) echo "Python: Dependency resolution failed" ;; - 162) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;; + # --- Systemd / Service errors (150-154) --- + 150) echo "Systemd: Service failed to start" ;; + 151) echo "Systemd: Service unit not found" ;; + 152) echo "Permission denied (EACCES)" ;; + 153) echo "Build/compile failed (make/gcc/cmake)" ;; + 154) echo "Node.js: Native addon build failed (node-gyp)" ;; + # --- Python / pip / uv (160-162) --- + 160) echo "Python: Virtualenv / uv environment missing or broken" ;; + 161) echo "Python: Dependency resolution failed" ;; + 162) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;; - # --- PostgreSQL (170-173) --- - 170) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;; - 171) echo "PostgreSQL: Authentication failed (bad user/password)" ;; - 172) echo "PostgreSQL: Database does not exist" ;; - 173) echo "PostgreSQL: Fatal error in query / syntax" ;; + # --- PostgreSQL (170-173) --- + 170) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;; + 171) echo "PostgreSQL: Authentication failed (bad user/password)" ;; + 172) echo "PostgreSQL: Database does not exist" ;; + 173) echo "PostgreSQL: Fatal error in query / syntax" ;; - # --- MySQL / MariaDB (180-183) --- - 180) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;; - 181) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;; - 182) echo "MySQL/MariaDB: Database does not exist" ;; - 183) echo "MySQL/MariaDB: Fatal error in query / syntax" ;; + # --- MySQL / MariaDB (180-183) --- + 180) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;; + 181) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;; + 182) echo "MySQL/MariaDB: Database does not exist" ;; + 183) echo "MySQL/MariaDB: Fatal error in query / syntax" ;; - # --- MongoDB (190-193) --- - 190) echo "MongoDB: Connection failed (server not running)" ;; - 191) echo "MongoDB: Authentication failed (bad user/password)" ;; - 192) echo "MongoDB: Database not found" ;; - 193) echo "MongoDB: Fatal query error" ;; + # --- MongoDB (190-193) --- + 190) echo "MongoDB: Connection failed (server not running)" ;; + 191) echo "MongoDB: Authentication failed (bad user/password)" ;; + 192) echo "MongoDB: Database not found" ;; + 193) echo "MongoDB: Fatal query error" ;; - # --- Proxmox Custom Codes (200-231) --- - 200) echo "Proxmox: Failed to create lock file" ;; - 203) echo "Proxmox: Missing CTID variable" ;; - 204) echo "Proxmox: Missing PCT_OSTYPE variable" ;; - 205) echo "Proxmox: Invalid CTID (<100)" ;; - 206) echo "Proxmox: CTID already in use" ;; - 207) echo "Proxmox: Password contains unescaped special characters" ;; - 208) echo "Proxmox: Invalid configuration (DNS/MAC/Network format)" ;; - 209) echo "Proxmox: Container creation failed" ;; - 210) echo "Proxmox: Cluster not quorate" ;; - 211) echo "Proxmox: Timeout waiting for template lock" ;; - 212) echo "Proxmox: Storage type 'iscsidirect' does not support containers (VMs only)" ;; - 213) echo "Proxmox: Storage type does not support 'rootdir' content" ;; - 214) echo "Proxmox: Not enough storage space" ;; - 215) echo "Proxmox: Container created but not listed (ghost state)" ;; - 216) echo "Proxmox: RootFS entry missing in config" ;; - 217) echo "Proxmox: Storage not accessible" ;; - 218) echo "Proxmox: Template file corrupted or incomplete" ;; - 219) echo "Proxmox: CephFS does not support containers - use RBD" ;; - 220) echo "Proxmox: Unable to resolve template path" ;; - 221) echo "Proxmox: Template file not readable" ;; - 222) echo "Proxmox: Template download failed" ;; - 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" ;; + # --- Proxmox Custom Codes (200-231) --- + 200) echo "Proxmox: Failed to create lock file" ;; + 203) echo "Proxmox: Missing CTID variable" ;; + 204) echo "Proxmox: Missing PCT_OSTYPE variable" ;; + 205) echo "Proxmox: Invalid CTID (<100)" ;; + 206) echo "Proxmox: CTID already in use" ;; + 207) echo "Proxmox: Password contains unescaped special characters" ;; + 208) echo "Proxmox: Invalid configuration (DNS/MAC/Network format)" ;; + 209) echo "Proxmox: Container creation failed" ;; + 210) echo "Proxmox: Cluster not quorate" ;; + 211) echo "Proxmox: Timeout waiting for template lock" ;; + 212) echo "Proxmox: Storage type 'iscsidirect' does not support containers (VMs only)" ;; + 213) echo "Proxmox: Storage type does not support 'rootdir' content" ;; + 214) echo "Proxmox: Not enough storage space" ;; + 215) echo "Proxmox: Container created but not listed (ghost state)" ;; + 216) echo "Proxmox: RootFS entry missing in config" ;; + 217) echo "Proxmox: Storage not accessible" ;; + 218) echo "Proxmox: Template file corrupted or incomplete" ;; + 219) echo "Proxmox: CephFS does not support containers - use RBD" ;; + 220) echo "Proxmox: Unable to resolve template path" ;; + 221) echo "Proxmox: Template file not readable" ;; + 222) echo "Proxmox: Template download failed" ;; + 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" ;; + # --- 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)" ;; - 245) echo "Node.js: Invalid command-line option" ;; - 246) echo "Node.js: Internal JavaScript Parse Error" ;; - 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" ;; + # --- 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)" ;; + 245) echo "Node.js: Invalid command-line option" ;; + 246) echo "Node.js: Internal JavaScript Parse Error" ;; + 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" ;; + # --- 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" ;; + # --- DPKG --- + 255) echo "DPKG: Fatal internal error" ;; - # --- Default --- - *) echo "Unknown error" ;; - esac + # --- Default --- + *) echo "Unknown error" ;; + esac } # ------------------------------------------------------------------------------ @@ -346,12 +346,12 @@ explain_exit_code() { # - Handles backslashes, quotes, newlines, tabs, and carriage returns # ------------------------------------------------------------------------------ json_escape() { - # 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 ' + # 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 → \\ @@ -370,39 +370,39 @@ json_escape() { # - Handles container paths that don't exist on the host # ------------------------------------------------------------------------------ get_error_text() { - local logfile="" - if declare -f get_active_logfile >/dev/null 2>&1; then - logfile=$(get_active_logfile) - elif [[ -n "${INSTALL_LOG:-}" ]]; then - logfile="$INSTALL_LOG" - elif [[ -n "${BUILD_LOG:-}" ]]; then - logfile="$BUILD_LOG" - fi + local logfile="" + if declare -f get_active_logfile >/dev/null 2>&1; then + logfile=$(get_active_logfile) + elif [[ -n "${INSTALL_LOG:-}" ]]; then + logfile="$INSTALL_LOG" + elif [[ -n "${BUILD_LOG:-}" ]]; then + logfile="$BUILD_LOG" + fi - # If logfile is inside container (e.g. /root/.install-*), try the host copy - if [[ -n "$logfile" && ! -s "$logfile" ]]; then - # Try combined log: /tmp/--.log - if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then - local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log" - if [[ -s "$combined_log" ]]; then - logfile="$combined_log" - fi - fi - fi + # If logfile is inside container (e.g. /root/.install-*), try the host copy + if [[ -n "$logfile" && ! -s "$logfile" ]]; then + # Try combined log: /tmp/--.log + if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then + local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log" + if [[ -s "$combined_log" ]]; then + logfile="$combined_log" + fi + fi + fi - # Also try BUILD_LOG as fallback if primary log is empty/missing - if [[ -z "$logfile" || ! -s "$logfile" ]] && [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then - logfile="$BUILD_LOG" - fi + # Also try BUILD_LOG as fallback if primary log is empty/missing + if [[ -z "$logfile" || ! -s "$logfile" ]] && [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then + 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 + # 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 + 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 } # ------------------------------------------------------------------------------ @@ -415,50 +415,50 @@ get_error_text() { # - Used for the error telemetry field (full trace instead of 20 lines) # ------------------------------------------------------------------------------ get_full_log() { - local max_bytes="${1:-122880}" # 120KB default - local logfile="" + local max_bytes="${1:-122880}" # 120KB default + local logfile="" - # Ensure logs are available on host (pulls from container if needed) - if declare -f ensure_log_on_host >/dev/null 2>&1; then - ensure_log_on_host - fi + # Ensure logs are available on host (pulls from container if needed) + if declare -f ensure_log_on_host >/dev/null 2>&1; then + ensure_log_on_host + fi - # Try combined log first (most complete) - if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then - local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log" - if [[ -s "$combined_log" ]]; then - logfile="$combined_log" - fi - fi + # Try combined log first (most complete) + if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then + local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log" + if [[ -s "$combined_log" ]]; then + logfile="$combined_log" + fi + fi - # Fall back to INSTALL_LOG - if [[ -z "$logfile" || ! -s "$logfile" ]]; then - if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then - logfile="$INSTALL_LOG" - fi - fi + # Fall back to INSTALL_LOG + if [[ -z "$logfile" || ! -s "$logfile" ]]; then + if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then + logfile="$INSTALL_LOG" + fi + fi - # Fall back to BUILD_LOG - if [[ -z "$logfile" || ! -s "$logfile" ]]; then - if [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then - logfile="$BUILD_LOG" - fi - fi + # Fall back to BUILD_LOG + if [[ -z "$logfile" || ! -s "$logfile" ]]; then + if [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then + logfile="$BUILD_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 + # 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 | - sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | - sed -E 's/([0-9]{1,3}\.)[0-9]{1,3}\.[0-9]{1,3}/\1x.x/g' | - head -c "$max_bytes" - fi + if [[ -n "$logfile" && -s "$logfile" ]]; then + # Strip ANSI codes, carriage returns, and anonymize IP addresses (GDPR) + sed 's/\r$//' "$logfile" 2>/dev/null | + sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | + sed -E 's/([0-9]{1,3}\.)[0-9]{1,3}\.[0-9]{1,3}/\1x.x/g' | + head -c "$max_bytes" + fi } # ------------------------------------------------------------------------------ @@ -473,18 +473,18 @@ get_full_log() { # - Returns structured error string via stdout # ------------------------------------------------------------------------------ build_error_string() { - local exit_code="${1:-1}" - local log_text="${2:-}" - local explanation - explanation=$(explain_exit_code "$exit_code") + local exit_code="${1:-1}" + local log_text="${2:-}" + local explanation + explanation=$(explain_exit_code "$exit_code") - if [[ -n "$log_text" ]]; then - # Structured format: header + separator + log lines - printf 'exit_code=%s | %s\n---\n%s' "$exit_code" "$explanation" "$log_text" - else - # No log available - just the explanation with exit code - printf 'exit_code=%s | %s' "$exit_code" "$explanation" - fi + if [[ -n "$log_text" ]]; then + # Structured format: header + separator + log lines + printf 'exit_code=%s | %s\n---\n%s' "$exit_code" "$explanation" "$log_text" + else + # No log available - just the explanation with exit code + printf 'exit_code=%s | %s' "$exit_code" "$explanation" + fi } # ============================================================================== @@ -499,35 +499,35 @@ build_error_string() { # - Used for GPU analytics # ------------------------------------------------------------------------------ detect_gpu() { - GPU_VENDOR="unknown" - GPU_MODEL="" - GPU_PASSTHROUGH="unknown" + GPU_VENDOR="unknown" + GPU_MODEL="" + GPU_PASSTHROUGH="unknown" - local gpu_line - gpu_line=$(lspci 2>/dev/null | grep -iE "VGA|3D|Display" | head -1 || true) + local gpu_line + gpu_line=$(lspci 2>/dev/null | grep -iE "VGA|3D|Display" | head -1 || true) - if [[ -n "$gpu_line" ]]; then - # Extract model: everything after the colon, clean up - GPU_MODEL=$(echo "$gpu_line" | sed 's/.*: //' | sed 's/ (rev .*)$//' | cut -c1-64) + if [[ -n "$gpu_line" ]]; then + # Extract model: everything after the colon, clean up + GPU_MODEL=$(echo "$gpu_line" | sed 's/.*: //' | sed 's/ (rev .*)$//' | cut -c1-64) - # Detect vendor and passthrough type - if echo "$gpu_line" | grep -qi "Intel"; then - GPU_VENDOR="intel" - GPU_PASSTHROUGH="igpu" - elif echo "$gpu_line" | grep -qi "AMD\|ATI"; then - GPU_VENDOR="amd" - if echo "$gpu_line" | grep -qi "Radeon RX\|Radeon Pro"; then - GPU_PASSTHROUGH="dgpu" - else - GPU_PASSTHROUGH="igpu" - fi - elif echo "$gpu_line" | grep -qi "NVIDIA"; then - GPU_VENDOR="nvidia" - GPU_PASSTHROUGH="dgpu" - fi - fi + # Detect vendor and passthrough type + if echo "$gpu_line" | grep -qi "Intel"; then + GPU_VENDOR="intel" + GPU_PASSTHROUGH="igpu" + elif echo "$gpu_line" | grep -qi "AMD\|ATI"; then + GPU_VENDOR="amd" + if echo "$gpu_line" | grep -qi "Radeon RX\|Radeon Pro"; then + GPU_PASSTHROUGH="dgpu" + else + GPU_PASSTHROUGH="igpu" + fi + elif echo "$gpu_line" | grep -qi "NVIDIA"; then + GPU_VENDOR="nvidia" + GPU_PASSTHROUGH="dgpu" + fi + fi - export GPU_VENDOR GPU_MODEL GPU_PASSTHROUGH + export GPU_VENDOR GPU_MODEL GPU_PASSTHROUGH } # ------------------------------------------------------------------------------ @@ -538,29 +538,29 @@ detect_gpu() { # - Used for CPU analytics # ------------------------------------------------------------------------------ detect_cpu() { - CPU_VENDOR="unknown" - CPU_MODEL="" + CPU_VENDOR="unknown" + CPU_MODEL="" - if [[ -f /proc/cpuinfo ]]; then - local vendor_id - vendor_id=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | tr -d ' ' || true) + if [[ -f /proc/cpuinfo ]]; then + local vendor_id + vendor_id=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | tr -d ' ' || true) - case "$vendor_id" in - GenuineIntel) CPU_VENDOR="intel" ;; - AuthenticAMD) CPU_VENDOR="amd" ;; - *) - # ARM doesn't have vendor_id, check for CPU implementer - if grep -qi "CPU implementer" /proc/cpuinfo 2>/dev/null; then - CPU_VENDOR="arm" - fi - ;; - esac + case "$vendor_id" in + GenuineIntel) CPU_VENDOR="intel" ;; + AuthenticAMD) CPU_VENDOR="amd" ;; + *) + # ARM doesn't have vendor_id, check for CPU implementer + if grep -qi "CPU implementer" /proc/cpuinfo 2>/dev/null; then + CPU_VENDOR="arm" + fi + ;; + esac - # Extract model name and clean it up - CPU_MODEL=$(grep -m1 "model name" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | sed 's/^ *//' | sed 's/(R)//g' | sed 's/(TM)//g' | sed 's/ */ /g' | cut -c1-64 || true) - fi + # Extract model name and clean it up + CPU_MODEL=$(grep -m1 "model name" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | sed 's/^ *//' | sed 's/(R)//g' | sed 's/(TM)//g' | sed 's/ */ /g' | cut -c1-64 || true) + fi - export CPU_VENDOR CPU_MODEL + export CPU_VENDOR CPU_MODEL } # ------------------------------------------------------------------------------ @@ -572,20 +572,20 @@ detect_cpu() { # - Returns empty if not available or if speed is "Unknown" (nested VMs) # ------------------------------------------------------------------------------ detect_ram() { - RAM_SPEED="" + RAM_SPEED="" - if command -v dmidecode &>/dev/null; then - # Get configured memory speed (actual running speed) - # Use || true to handle "Unknown" values in nested VMs (no numeric match) - RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1) || true + if command -v dmidecode &>/dev/null; then + # Get configured memory speed (actual running speed) + # Use || true to handle "Unknown" values in nested VMs (no numeric match) + RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1) || true - # Fallback to Speed: if Configured not available - if [[ -z "$RAM_SPEED" ]]; then - RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Speed:" | grep -oE "[0-9]+" | head -1) || true - fi - fi + # Fallback to Speed: if Configured not available + if [[ -z "$RAM_SPEED" ]]; then + RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Speed:" | grep -oE "[0-9]+" | head -1) || true + fi + fi - export RAM_SPEED + export RAM_SPEED } # ------------------------------------------------------------------------------ @@ -608,59 +608,59 @@ detect_ram() { # - Never blocks or fails script execution # ------------------------------------------------------------------------------ post_to_api() { - # Prevent duplicate submissions (post_to_api is called from multiple places) - [[ "${POST_TO_API_DONE:-}" == "true" ]] && return 0 + # Prevent duplicate submissions (post_to_api is called from multiple places) + [[ "${POST_TO_API_DONE:-}" == "true" ]] && return 0 - # Silent fail - telemetry should never break scripts - command -v curl &>/dev/null || { - [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] curl not found, skipping" >&2 - return 0 - } - [[ "${DIAGNOSTICS:-no}" == "no" ]] && { - [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] DIAGNOSTICS=no, skipping" >&2 - return 0 - } - [[ -z "${RANDOM_UUID:-}" ]] && { - [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] RANDOM_UUID empty, skipping" >&2 - return 0 - } + # Silent fail - telemetry should never break scripts + command -v curl &>/dev/null || { + [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] curl not found, skipping" >&2 + return 0 + } + [[ "${DIAGNOSTICS:-no}" == "no" ]] && { + [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] DIAGNOSTICS=no, skipping" >&2 + return 0 + } + [[ -z "${RANDOM_UUID:-}" ]] && { + [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] RANDOM_UUID empty, skipping" >&2 + return 0 + } - [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] post_to_api() DIAGNOSTICS=$DIAGNOSTICS RANDOM_UUID=$RANDOM_UUID NSAPP=$NSAPP" >&2 + [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] post_to_api() DIAGNOSTICS=$DIAGNOSTICS RANDOM_UUID=$RANDOM_UUID NSAPP=$NSAPP" >&2 - # Set type for later status updates (preserve if already set, e.g. turnkey) - TELEMETRY_TYPE="${TELEMETRY_TYPE:-lxc}" + # Set type for later status updates (preserve if already set, e.g. turnkey) + TELEMETRY_TYPE="${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 + local pve_version="" + if command -v pveversion &>/dev/null; then + pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true + fi - # Detect GPU if not already set - if [[ -z "${GPU_VENDOR:-}" ]]; then - detect_gpu - fi - local gpu_vendor="${GPU_VENDOR:-unknown}" - local gpu_model - gpu_model=$(json_escape "${GPU_MODEL:-}") - local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}" + # Detect GPU if not already set + if [[ -z "${GPU_VENDOR:-}" ]]; then + detect_gpu + fi + local gpu_vendor="${GPU_VENDOR:-unknown}" + local gpu_model + gpu_model=$(json_escape "${GPU_MODEL:-}") + local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}" - # Detect CPU if not already set - if [[ -z "${CPU_VENDOR:-}" ]]; then - detect_cpu - fi - local cpu_vendor="${CPU_VENDOR:-unknown}" - local cpu_model - cpu_model=$(json_escape "${CPU_MODEL:-}") + # Detect CPU if not already set + if [[ -z "${CPU_VENDOR:-}" ]]; then + detect_cpu + fi + local cpu_vendor="${CPU_VENDOR:-unknown}" + local cpu_model + cpu_model=$(json_escape "${CPU_MODEL:-}") - # Detect RAM if not already set - if [[ -z "${RAM_SPEED:-}" ]]; then - detect_ram - fi - local ram_speed="${RAM_SPEED:-}" + # Detect RAM if not already set + if [[ -z "${RAM_SPEED:-}" ]]; then + detect_ram + fi + local ram_speed="${RAM_SPEED:-}" - local JSON_PAYLOAD - JSON_PAYLOAD=$( - cat <&2 - [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Payload: $JSON_PAYLOAD" >&2 + [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Sending to: $TELEMETRY_URL" >&2 + [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Payload: $JSON_PAYLOAD" >&2 - # Send initial "installing" record with retry. - # This record MUST exist for all subsequent updates to succeed. - local http_code="" attempt - local _post_success=false - 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 - if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then - _post_success=true - break - fi - [[ "$attempt" -lt 3 ]] && sleep 1 - done + # Send initial "installing" record with retry. + # This record MUST exist for all subsequent updates to succeed. + local http_code="" attempt + local _post_success=false + 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 + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + _post_success=true + break + fi + [[ "$attempt" -lt 3 ]] && sleep 1 + done - # Only mark done if at least one attempt succeeded. - # If all 3 failed, POST_TO_API_DONE stays false so post_update_to_api - # and on_exit() know the initial record was never created. - # The server has fallback logic to create a new record on status updates, - # so subsequent calls can still succeed even without the initial record. - POST_TO_API_DONE=${_post_success} + # Only mark done if at least one attempt succeeded. + # If all 3 failed, POST_TO_API_DONE stays false so post_update_to_api + # and on_exit() know the initial record was never created. + # The server has fallback logic to create a new record on status updates, + # so subsequent calls can still succeed even without the initial record. + POST_TO_API_DONE=${_post_success} } # ------------------------------------------------------------------------------ @@ -733,53 +733,53 @@ EOF # - Never blocks or fails script execution # ------------------------------------------------------------------------------ post_to_api_vm() { - # Read diagnostics setting from file - if [[ -f /usr/local/community-scripts/diagnostics ]]; then - DIAGNOSTICS=$(grep -i "^DIAGNOSTICS=" /usr/local/community-scripts/diagnostics 2>/dev/null | awk -F'=' '{print $2}') || true - fi + # Read diagnostics setting from file + if [[ -f /usr/local/community-scripts/diagnostics ]]; then + DIAGNOSTICS=$(grep -i "^DIAGNOSTICS=" /usr/local/community-scripts/diagnostics 2>/dev/null | awk -F'=' '{print $2}') || true + fi - # Silent fail - telemetry should never break scripts - command -v curl &>/dev/null || return 0 - [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 - [[ -z "${RANDOM_UUID:-}" ]] && return 0 + # 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 later status updates - TELEMETRY_TYPE="vm" + # Set type for later status updates + TELEMETRY_TYPE="vm" - local pve_version="" - if command -v pveversion &>/dev/null; then - pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true - fi + local pve_version="" + if command -v pveversion &>/dev/null; then + pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true + fi - # Detect GPU if not already set - if [[ -z "${GPU_VENDOR:-}" ]]; then - detect_gpu - fi - local gpu_vendor="${GPU_VENDOR:-unknown}" - local gpu_model - gpu_model=$(json_escape "${GPU_MODEL:-}") - local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}" + # Detect GPU if not already set + if [[ -z "${GPU_VENDOR:-}" ]]; then + detect_gpu + fi + local gpu_vendor="${GPU_VENDOR:-unknown}" + local gpu_model + gpu_model=$(json_escape "${GPU_MODEL:-}") + local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}" - # Detect CPU if not already set - if [[ -z "${CPU_VENDOR:-}" ]]; then - detect_cpu - fi - local cpu_vendor="${CPU_VENDOR:-unknown}" - local cpu_model - cpu_model=$(json_escape "${CPU_MODEL:-}") + # Detect CPU if not already set + if [[ -z "${CPU_VENDOR:-}" ]]; then + detect_cpu + fi + local cpu_vendor="${CPU_VENDOR:-unknown}" + local cpu_model + cpu_model=$(json_escape "${CPU_MODEL:-}") - # Detect RAM if not already set - if [[ -z "${RAM_SPEED:-}" ]]; then - detect_ram - fi - local ram_speed="${RAM_SPEED:-}" + # Detect RAM if not already set + if [[ -z "${RAM_SPEED:-}" ]]; then + detect_ram + fi + local ram_speed="${RAM_SPEED:-}" - # Remove 'G' suffix from disk size - local DISK_SIZE_API="${DISK_SIZE%G}" + # Remove 'G' suffix from disk size + local DISK_SIZE_API="${DISK_SIZE%G}" - local JSON_PAYLOAD - JSON_PAYLOAD=$( - cat </dev/null) || http_code="000" - if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then - _post_success=true - break - fi - [[ "$attempt" -lt 3 ]] && sleep 1 - done + # Send initial "installing" record with retry (must succeed for updates to work) + local http_code="" attempt + local _post_success=false + 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" + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + _post_success=true + break + fi + [[ "$attempt" -lt 3 ]] && sleep 1 + done - POST_TO_API_DONE=${_post_success} + POST_TO_API_DONE=${_post_success} } # ------------------------------------------------------------------------------ @@ -836,17 +836,17 @@ EOF # - Can be called multiple times safely # ------------------------------------------------------------------------------ post_progress_to_api() { - command -v curl &>/dev/null || return 0 - [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 - [[ -z "${RANDOM_UUID:-}" ]] && return 0 + command -v curl &>/dev/null || return 0 + [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 + [[ -z "${RANDOM_UUID:-}" ]] && return 0 - local progress_status="${1:-configuring}" - local app_name="${NSAPP:-${app:-unknown}}" - local telemetry_type="${TELEMETRY_TYPE:-lxc}" + local progress_status="${1:-configuring}" + local app_name="${NSAPP:-${app:-unknown}}" + local telemetry_type="${TELEMETRY_TYPE:-lxc}" - 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}\",\"nsapp\":\"${app_name}\",\"status\":\"${progress_status}\"}" &>/dev/null || true + 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}\",\"nsapp\":\"${app_name}\",\"status\":\"${progress_status}\"}" &>/dev/null || true } # ------------------------------------------------------------------------------ @@ -865,111 +865,111 @@ post_progress_to_api() { # - Never blocks or fails script execution # ------------------------------------------------------------------------------ post_update_to_api() { - # Silent fail - telemetry should never break scripts - command -v curl &>/dev/null || return 0 + # Silent fail - telemetry should never break scripts + command -v curl &>/dev/null || return 0 - # Support "force" mode (3rd arg) to bypass duplicate check for retries after cleanup - local force="${3:-}" - POST_UPDATE_DONE=${POST_UPDATE_DONE:-false} - if [[ "$POST_UPDATE_DONE" == "true" && "$force" != "force" ]]; then - return 0 - fi + # Support "force" mode (3rd arg) to bypass duplicate check for retries after cleanup + local force="${3:-}" + POST_UPDATE_DONE=${POST_UPDATE_DONE:-false} + if [[ "$POST_UPDATE_DONE" == "true" && "$force" != "force" ]]; then + return 0 + fi - [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 - [[ -z "${RANDOM_UUID:-}" ]] && return 0 + [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 + [[ -z "${RANDOM_UUID:-}" ]] && return 0 - local status="${1:-failed}" - local raw_exit_code="${2:-1}" - local exit_code=0 error="" pb_status error_category="" + local status="${1:-failed}" + local raw_exit_code="${2:-1}" + local exit_code=0 error="" pb_status error_category="" - # Get GPU info (if detected) - local gpu_vendor="${GPU_VENDOR:-unknown}" - local gpu_model - gpu_model=$(json_escape "${GPU_MODEL:-}") - local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}" + # Get GPU info (if detected) + local gpu_vendor="${GPU_VENDOR:-unknown}" + local gpu_model + gpu_model=$(json_escape "${GPU_MODEL:-}") + local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}" - # Get CPU info (if detected) - local cpu_vendor="${CPU_VENDOR:-unknown}" - local cpu_model - cpu_model=$(json_escape "${CPU_MODEL:-}") + # Get CPU info (if detected) + local cpu_vendor="${CPU_VENDOR:-unknown}" + local cpu_model + cpu_model=$(json_escape "${CPU_MODEL:-}") - # Get RAM info (if detected) - local ram_speed="${RAM_SPEED:-}" + # Get RAM info (if detected) + local ram_speed="${RAM_SPEED:-}" - # Map status to telemetry values: installing, success, failed, unknown - case "$status" in - done | success) - pb_status="success" - exit_code=0 - error="" - error_category="" - ;; - failed) - pb_status="failed" - ;; - *) - pb_status="unknown" - ;; - esac + # Map status to telemetry values: installing, success, failed, unknown + case "$status" in + done | success) + pb_status="success" + exit_code=0 + error="" + error_category="" + ;; + failed) + pb_status="failed" + ;; + *) + pb_status="unknown" + ;; + esac - # 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 - exit_code=1 - fi - # Get full installation log for error field - local log_text="" - log_text=$(get_full_log 122880) || true # 120KB max - if [[ -z "$log_text" ]]; then - # Fallback to last 20 lines - log_text=$(get_error_text) - fi - local full_error - full_error=$(build_error_string "$exit_code" "$log_text") - error=$(json_escape "$full_error") - short_error=$(json_escape "$(explain_exit_code "$exit_code")") - error_category=$(categorize_error "$exit_code") - [[ -z "$error" ]] && error="Unknown error" + # 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 + exit_code=1 + fi + # Get full installation log for error field + local log_text="" + log_text=$(get_full_log 122880) || true # 120KB max + if [[ -z "$log_text" ]]; then + # Fallback to last 20 lines + log_text=$(get_error_text) + fi + local full_error + full_error=$(build_error_string "$exit_code" "$log_text") + error=$(json_escape "$full_error") + 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 + # 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 - local duration=0 - if [[ -n "${INSTALL_START_TIME:-}" ]]; then - duration=$(($(date +%s) - INSTALL_START_TIME)) - fi + # Calculate duration if timer was started + local duration=0 + if [[ -n "${INSTALL_START_TIME:-}" ]]; then + duration=$(($(date +%s) - INSTALL_START_TIME)) + fi - # Get PVE version - local pve_version="" - if command -v pveversion &>/dev/null; then - pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true - fi + # Get PVE version + local pve_version="" + if command -v pveversion &>/dev/null; then + pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true + fi - local http_code="" + 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 + # 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=$( - cat </dev/null) || http_code="000" + http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000" - if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then - POST_UPDATE_DONE=true - return 0 - fi + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + POST_UPDATE_DONE=true + return 0 + fi - # ── Attempt 2: Medium error text (truncated log ≤16KB instead of full 120KB) ── - sleep 1 - local RETRY_PAYLOAD - RETRY_PAYLOAD=$( - cat </dev/null) || http_code="000" + http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "$RETRY_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000" - if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then - POST_UPDATE_DONE=true - return 0 - fi + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + POST_UPDATE_DONE=true + return 0 + fi - # ── Attempt 3: Minimal payload with medium error (bare minimum to set status) ── - sleep 2 - local MINIMAL_PAYLOAD - MINIMAL_PAYLOAD=$( - cat </dev/null) || http_code="000" + http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "$MINIMAL_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000" - if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then - POST_UPDATE_DONE=true - return 0 - fi + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + POST_UPDATE_DONE=true + return 0 + fi - # All 3 attempts failed — do NOT set POST_UPDATE_DONE=true. - # This allows the EXIT trap (on_exit in error-handler.func) to retry. - # No infinite loop risk: EXIT trap fires exactly once. + # All 3 attempts failed — do NOT set POST_UPDATE_DONE=true. + # This allows the EXIT trap (on_exit in error_handler.func) to retry. + # No infinite loop risk: EXIT trap fires exactly once. } # ============================================================================== @@ -1096,65 +1096,65 @@ EOF # - Used to group errors in dashboard # ------------------------------------------------------------------------------ categorize_error() { - # Allow build.func to override category based on log analysis (exit code 1 subclassification) - if [[ -n "${ERROR_CATEGORY_OVERRIDE:-}" ]]; then - echo "$ERROR_CATEGORY_OVERRIDE" - return - fi + # Allow build.func to override category based on log analysis (exit code 1 subclassification) + if [[ -n "${ERROR_CATEGORY_OVERRIDE:-}" ]]; then + echo "$ERROR_CATEGORY_OVERRIDE" + return + fi - local code="$1" - case "$code" in - # Network errors (curl/wget) - 6 | 7 | 22 | 35) echo "network" ;; + local code="$1" + case "$code" in + # Network errors (curl/wget) + 6 | 7 | 22 | 35) echo "network" ;; - # Docker / Privileged mode required - 10) echo "config" ;; + # Docker / Privileged mode required + 10) echo "config" ;; - # Timeout errors - 28 | 124 | 211) echo "timeout" ;; + # Timeout errors + 28 | 124 | 211) echo "timeout" ;; - # Storage errors (Proxmox storage) - 214 | 217 | 219 | 224) echo "storage" ;; + # Storage errors (Proxmox storage) + 214 | 217 | 219 | 224) echo "storage" ;; - # Dependency/Package errors (APT, DPKG, pip, commands) - 100 | 101 | 102 | 127 | 160 | 161 | 162 | 255) echo "dependency" ;; + # Dependency/Package errors (APT, DPKG, pip, commands) + 100 | 101 | 102 | 127 | 160 | 161 | 162 | 255) echo "dependency" ;; - # Permission errors - 126 | 152) echo "permission" ;; + # Permission errors + 126 | 152) echo "permission" ;; - # Configuration errors (Proxmox config, invalid args) - 128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; + # Configuration errors (Proxmox config, invalid args) + 128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; - # Proxmox container/template errors - 200 | 209 | 210 | 212 | 213 | 215 | 216 | 218 | 220 | 221 | 222 | 223 | 225 | 231) echo "proxmox" ;; + # Proxmox container/template errors + 200 | 209 | 210 | 212 | 213 | 215 | 216 | 218 | 220 | 221 | 222 | 223 | 225 | 231) echo "proxmox" ;; - # Service/Systemd errors - 150 | 151 | 153 | 154) echo "service" ;; + # Service/Systemd errors + 150 | 151 | 153 | 154) echo "service" ;; - # Database errors (PostgreSQL, MySQL, MongoDB) - 170 | 171 | 172 | 173 | 180 | 181 | 182 | 183 | 190 | 191 | 192 | 193) echo "database" ;; + # Database errors (PostgreSQL, MySQL, MongoDB) + 170 | 171 | 172 | 173 | 180 | 181 | 182 | 183 | 190 | 191 | 192 | 193) echo "database" ;; - # Node.js / JavaScript runtime errors - 243 | 245 | 246 | 247 | 248 | 249) echo "runtime" ;; + # Node.js / JavaScript runtime errors + 243 | 245 | 246 | 247 | 248 | 249) echo "runtime" ;; - # Python environment errors - # (already covered: 160-162 under dependency) + # Python environment errors + # (already covered: 160-162 under dependency) - # Aborted by user (SIGHUP=terminal closed, SIGINT=Ctrl+C, SIGTERM=killed) - 129 | 130 | 143) echo "user_aborted" ;; + # Aborted by user (SIGHUP=terminal closed, SIGINT=Ctrl+C, SIGTERM=killed) + 129 | 130 | 143) echo "user_aborted" ;; - # Resource errors (OOM, SIGKILL, SIGABRT) - 134 | 137) echo "resource" ;; + # Resource errors (OOM, SIGKILL, SIGABRT) + 134 | 137) echo "resource" ;; - # Signal/Process errors (SIGPIPE, SIGSEGV) - 139 | 141) echo "signal" ;; + # Signal/Process errors (SIGPIPE, SIGSEGV) + 139 | 141) echo "signal" ;; - # Shell errors (general error, syntax error) - 1 | 2) echo "shell" ;; + # Shell errors (general error, syntax error) + 1 | 2) echo "shell" ;; - # Default - truly unknown - *) echo "unknown" ;; - esac + # Default - truly unknown + *) echo "unknown" ;; + esac } # ------------------------------------------------------------------------------ @@ -1165,8 +1165,8 @@ categorize_error() { # - Sets INSTALL_START_TIME global variable # ------------------------------------------------------------------------------ start_install_timer() { - INSTALL_START_TIME=$(date +%s) - export INSTALL_START_TIME + INSTALL_START_TIME=$(date +%s) + export INSTALL_START_TIME } # ------------------------------------------------------------------------------ @@ -1176,12 +1176,12 @@ start_install_timer() { # - Returns 0 if timer was not started # ------------------------------------------------------------------------------ get_install_duration() { - if [[ -z "${INSTALL_START_TIME:-}" ]]; then - echo "0" - return - fi - local now=$(date +%s) - echo $((now - INSTALL_START_TIME)) + if [[ -z "${INSTALL_START_TIME:-}" ]]; then + echo "0" + return + fi + local now=$(date +%s) + echo $((now - INSTALL_START_TIME)) } # ------------------------------------------------------------------------------ @@ -1193,18 +1193,18 @@ get_install_duration() { # * $1: exit_code from the script # ------------------------------------------------------------------------------ _telemetry_report_exit() { - local ec="${1:-0}" - local status="success" - [[ "$ec" -ne 0 ]] && status="failed" + local ec="${1:-0}" + local status="success" + [[ "$ec" -ne 0 ]] && status="failed" - # Lazy name resolution: use explicit name, fall back to $APP, then "unknown" - local name="${TELEMETRY_TOOL_NAME:-${APP:-unknown}}" + # Lazy name resolution: use explicit name, fall back to $APP, then "unknown" + local name="${TELEMETRY_TOOL_NAME:-${APP:-unknown}}" - if [[ "${TELEMETRY_TOOL_TYPE:-pve}" == "addon" ]]; then - post_addon_to_api "$name" "$status" "$ec" - else - post_tool_to_api "$name" "$status" "$ec" - fi + if [[ "${TELEMETRY_TOOL_TYPE:-pve}" == "addon" ]]; then + post_addon_to_api "$name" "$status" "$ec" + else + post_tool_to_api "$name" "$status" "$ec" + fi } # ------------------------------------------------------------------------------ @@ -1224,21 +1224,21 @@ _telemetry_report_exit() { # init_tool_telemetry "" "addon" # uses $APP at exit time # ------------------------------------------------------------------------------ init_tool_telemetry() { - local name="${1:-}" - local type="${2:-pve}" + local name="${1:-}" + local type="${2:-pve}" - [[ -n "$name" ]] && TELEMETRY_TOOL_NAME="$name" - TELEMETRY_TOOL_TYPE="$type" + [[ -n "$name" ]] && TELEMETRY_TOOL_NAME="$name" + TELEMETRY_TOOL_TYPE="$type" - # Read diagnostics opt-in/opt-out - if [[ -f /usr/local/community-scripts/diagnostics ]]; then - DIAGNOSTICS=$(grep -i "^DIAGNOSTICS=" /usr/local/community-scripts/diagnostics 2>/dev/null | awk -F'=' '{print $2}') || true - fi + # Read diagnostics opt-in/opt-out + if [[ -f /usr/local/community-scripts/diagnostics ]]; then + DIAGNOSTICS=$(grep -i "^DIAGNOSTICS=" /usr/local/community-scripts/diagnostics 2>/dev/null | awk -F'=' '{print $2}') || true + fi - start_install_timer + start_install_timer - # EXIT trap: automatically report telemetry when script ends - trap '_telemetry_report_exit "$?"' EXIT + # EXIT trap: automatically report telemetry when script ends + trap '_telemetry_report_exit "$?"' EXIT } # ------------------------------------------------------------------------------ @@ -1252,40 +1252,40 @@ init_tool_telemetry() { # - For PVE host tools, not container installations # ------------------------------------------------------------------------------ post_tool_to_api() { - command -v curl &>/dev/null || return 0 - [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 + command -v curl &>/dev/null || return 0 + [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 - local tool_name="${1:-unknown}" - local status="${2:-success}" - local exit_code="${3:-0}" - local error="" error_category="" - local uuid duration + local tool_name="${1:-unknown}" + local status="${2:-success}" + local exit_code="${3:-0}" + local error="" error_category="" + local uuid duration - # Generate UUID for this tool execution - uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "tool-$(date +%s)") - duration=$(get_install_duration) + # Generate UUID for this tool execution + uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "tool-$(date +%s)") + duration=$(get_install_duration) - # Map status - [[ "$status" == "done" ]] && status="success" + # Map status + [[ "$status" == "done" ]] && status="success" - if [[ "$status" == "failed" ]]; then - [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 - local error_text="" - error_text=$(get_error_text) - local full_error - full_error=$(build_error_string "$exit_code" "$error_text") - error=$(json_escape "$full_error") - error_category=$(categorize_error "$exit_code") - fi + if [[ "$status" == "failed" ]]; then + [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 + local error_text="" + error_text=$(get_error_text) + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") + error_category=$(categorize_error "$exit_code") + fi - local pve_version="" - if command -v pveversion &>/dev/null; then - pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true - fi + local pve_version="" + if command -v pveversion &>/dev/null; then + pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true + fi - local JSON_PAYLOAD - JSON_PAYLOAD=$( - cat </dev/null || true + curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "$JSON_PAYLOAD" &>/dev/null || true } # ------------------------------------------------------------------------------ @@ -1318,42 +1318,42 @@ EOF # - For addons installed inside containers # ------------------------------------------------------------------------------ post_addon_to_api() { - command -v curl &>/dev/null || return 0 - [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 + command -v curl &>/dev/null || return 0 + [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 - local addon_name="${1:-unknown}" - local status="${2:-success}" - local exit_code="${3:-0}" - local error="" error_category="" - local uuid duration + local addon_name="${1:-unknown}" + local status="${2:-success}" + local exit_code="${3:-0}" + local error="" error_category="" + local uuid duration - # Generate UUID for this addon installation - uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "addon-$(date +%s)") - duration=$(get_install_duration) + # Generate UUID for this addon installation + uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "addon-$(date +%s)") + duration=$(get_install_duration) - # Map status - [[ "$status" == "done" ]] && status="success" + # Map status + [[ "$status" == "done" ]] && status="success" - if [[ "$status" == "failed" ]]; then - [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 - local error_text="" - error_text=$(get_error_text) - local full_error - full_error=$(build_error_string "$exit_code" "$error_text") - error=$(json_escape "$full_error") - error_category=$(categorize_error "$exit_code") - fi + if [[ "$status" == "failed" ]]; then + [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 + local error_text="" + error_text=$(get_error_text) + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") + error_category=$(categorize_error "$exit_code") + fi - # Detect OS info - local os_type="" os_version="" - if [[ -f /etc/os-release ]]; then - os_type=$(grep "^ID=" /etc/os-release | cut -d= -f2 | tr -d '"' || true) - os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d= -f2 | tr -d '"' || true) - fi + # Detect OS info + local os_type="" os_version="" + if [[ -f /etc/os-release ]]; then + os_type=$(grep "^ID=" /etc/os-release | cut -d= -f2 | tr -d '"' || true) + os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d= -f2 | tr -d '"' || true) + fi - local JSON_PAYLOAD - JSON_PAYLOAD=$( - cat </dev/null || true + curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "$JSON_PAYLOAD" &>/dev/null || true } # ------------------------------------------------------------------------------ @@ -1389,63 +1389,63 @@ EOF # * GPU info (if detect_gpu was called) # ------------------------------------------------------------------------------ post_update_to_api_extended() { - # Silent fail - telemetry should never break scripts - command -v curl &>/dev/null || return 0 + # Silent fail - telemetry should never break scripts + command -v curl &>/dev/null || return 0 - # Prevent duplicate submissions - POST_UPDATE_DONE=${POST_UPDATE_DONE:-false} - [[ "$POST_UPDATE_DONE" == "true" ]] && return 0 + # Prevent duplicate submissions + POST_UPDATE_DONE=${POST_UPDATE_DONE:-false} + [[ "$POST_UPDATE_DONE" == "true" ]] && return 0 - [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 - [[ -z "${RANDOM_UUID:-}" ]] && return 0 + [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 + [[ -z "${RANDOM_UUID:-}" ]] && return 0 - local status="${1:-failed}" - local raw_exit_code="${2:-1}" - local exit_code=0 error="" pb_status error_category="" - local duration gpu_vendor gpu_passthrough + local status="${1:-failed}" + local raw_exit_code="${2:-1}" + local exit_code=0 error="" pb_status error_category="" + local duration gpu_vendor gpu_passthrough - # Get duration - duration=$(get_install_duration) + # Get duration + duration=$(get_install_duration) - # Get GPU info (if detected) - gpu_vendor="${GPU_VENDOR:-}" - gpu_passthrough="${GPU_PASSTHROUGH:-}" + # Get GPU info (if detected) + gpu_vendor="${GPU_VENDOR:-}" + gpu_passthrough="${GPU_PASSTHROUGH:-}" - # Map status to telemetry values - case "$status" in - done | success) - pb_status="success" - exit_code=0 - error="" - error_category="" - ;; - failed) - pb_status="failed" - ;; - *) - pb_status="unknown" - ;; - esac + # Map status to telemetry values + case "$status" in + done | success) + pb_status="success" + exit_code=0 + error="" + error_category="" + ;; + failed) + pb_status="failed" + ;; + *) + pb_status="unknown" + ;; + esac - # 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 - exit_code=1 - fi - local error_text="" - error_text=$(get_error_text) - local full_error - full_error=$(build_error_string "$exit_code" "$error_text") - error=$(json_escape "$full_error") - error_category=$(categorize_error "$exit_code") - [[ -z "$error" ]] && error="Unknown error" - fi + # 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 + exit_code=1 + fi + local error_text="" + error_text=$(get_error_text) + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") + error_category=$(categorize_error "$exit_code") + [[ -z "$error" ]] && error="Unknown error" + fi - local JSON_PAYLOAD - JSON_PAYLOAD=$( - cat </dev/null) || http_code="000" + local http_code + http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000" - if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then - POST_UPDATE_DONE=true - return 0 - fi + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + POST_UPDATE_DONE=true + return 0 + fi - # Retry with minimal payload - sleep 1 - http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ - -H "Content-Type: application/json" \ - -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-unknown}\",\"status\":\"${pb_status}\",\"exit_code\":${exit_code},\"install_duration\":${duration:-0}}" \ - -o /dev/null 2>/dev/null) || http_code="000" + # Retry with minimal payload + sleep 1 + http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ + -H "Content-Type: application/json" \ + -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-unknown}\",\"status\":\"${pb_status}\",\"exit_code\":${exit_code},\"install_duration\":${duration:-0}}" \ + -o /dev/null 2>/dev/null) || http_code="000" - if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then - POST_UPDATE_DONE=true - return 0 - fi + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + POST_UPDATE_DONE=true + return 0 + fi - # Do NOT set POST_UPDATE_DONE=true — let EXIT trap retry + # Do NOT set POST_UPDATE_DONE=true — let EXIT trap retry } diff --git a/scripts/core/build.func b/scripts/core/build.func index 9c82bef..1bf37e4 100755 --- a/scripts/core/build.func +++ b/scripts/core/build.func @@ -18,7 +18,7 @@ # # Usage: # - Sourced automatically by CT creation scripts -# - Requires core.func and error-handler.func to be loaded first +# - Requires core.func and error_handler.func to be loaded first # # ============================================================================== @@ -986,13 +986,23 @@ base_settings() { # Runtime check: Verify APT cacher is reachable if configured if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then - if ! curl -s --connect-timeout 2 "http://${APT_CACHER_IP}:3142" >/dev/null 2>&1; then - msg_warn "APT Cacher configured but not reachable at ${APT_CACHER_IP}:3142" + local _check_host _check_port _check_url + _check_host=$(echo "$APT_CACHER_IP" | sed -e 's|https\?://||' -e 's|/.*||' | cut -d: -f1) + _check_port=$(echo "$APT_CACHER_IP" | sed -e 's|https\?://||' -e 's|/.*||' | cut -s -d: -f2) + if [[ "$APT_CACHER_IP" =~ ^https?:// ]]; then + _check_url="$APT_CACHER_IP" + _check_port="${_check_port:-80}" + else + _check_port="${_check_port:-3142}" + _check_url="http://${APT_CACHER_IP}:${_check_port}" + fi + if ! curl -s --connect-timeout 2 "${_check_url}" >/dev/null 2>&1; then + msg_warn "APT Cacher configured but not reachable at ${_check_url}" msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation" APT_CACHER="" APT_CACHER_IP="" else - msg_ok "APT Cacher verified at ${APT_CACHER_IP}:3142" + msg_ok "APT Cacher verified at ${_check_url}" fi fi @@ -1044,7 +1054,7 @@ load_vars_file() { # Allowed var_* keys local VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_keyctl + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token 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_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 @@ -1199,6 +1209,13 @@ load_vars_file() { continue fi ;; + var_apt_cacher_ip) + # Allow: plain IP/hostname, http://host, https://host:port + if [[ -n "$var_val" ]] && ! [[ "$var_val" =~ ^(https?://)?[a-zA-Z0-9._-]+(:[0-9]+)?(/.*)?$ ]]; then + msg_warn "Invalid APT Cacher address '$var_val' in $file, ignoring" + continue + fi + ;; var_container_storage | var_template_storage) # Validate that the storage exists and is active on the current node local _storage_status @@ -1238,7 +1255,7 @@ default_var_settings() { # Allowed var_* keys (alphabetically sorted) # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) local VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_keyctl + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token 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_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 @@ -1311,9 +1328,11 @@ var_ipv6_method=none var_ssh=no # var_ssh_authorized_key= -# APT cacher (optional - with example) +# APT cacher (optional - IP or URL) # var_apt_cacher=yes # var_apt_cacher_ip=192.168.1.10 +# var_apt_cacher_ip=http://proxy.local +# var_apt_cacher_ip=https://proxy.local:443 # Features/Tags/verbosity var_fuse=no @@ -1331,6 +1350,10 @@ var_verbose=no # Security (root PW) – empty => autologin # var_pw= + +# GitHub Personal Access Token (optional – avoids API rate limits during installs) +# Create at https://github.com/settings/tokens – read-only public access is sufficient +# var_github_token=ghp_your_token_here EOF # Now choose storages (always prompt unless just one exists) @@ -1368,6 +1391,11 @@ EOF VERBOSE="no" fi + # 4) Map var_github_token → GITHUB_TOKEN (only if not already set in environment) + if [[ -z "${GITHUB_TOKEN:-}" && -n "${var_github_token:-}" ]]; then + export GITHUB_TOKEN="${var_github_token}" + fi + # 4) Apply base settings and show summary METHOD="mydefaults-global" base_settings "$VERBOSE" @@ -1400,7 +1428,7 @@ get_app_defaults_path() { if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) declare -ag VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token var_gpu var_gateway var_hostname var_ipv6_method var_mac var_mtu 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 @@ -2526,7 +2554,7 @@ advanced_settings() { # Ask for IP if enabled if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "APT CACHER IP" \ - --inputbox "\nEnter APT Cacher-NG server IP address:" 10 58 "$_apt_cacher_ip" \ + --inputbox "\nEnter APT Cacher-NG IP or URL:\n(e.g. 192.168.1.10, http://host, https://host:443)" 12 62 "$_apt_cacher_ip" \ 3>&1 1>&2 2>&3); then _apt_cacher_ip="$result" fi @@ -3530,6 +3558,7 @@ build_container() { # Gateway if [[ -n "$GATE" ]]; then case "$GATE" in + ,gw=) ;; ,gw=*) NET_STRING+="$GATE" ;; *) NET_STRING+=",gw=$GATE" ;; esac @@ -3584,8 +3613,6 @@ build_container() { fi # Build PCT_OPTIONS as string for export - TEMP_DIR=$(mktemp -d) - pushd "$TEMP_DIR" >/dev/null local _func_url if [ "$var_os" == "alpine" ]; then _func_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func" @@ -3998,7 +4025,7 @@ EOF # Wait for IP assignment (IPv4 or IPv6) local ip_in_lxc="" - for i in {1..20}; do + for i in {1..60}; do # Try IPv4 first ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) # Fallback to IPv6 if IPv4 not available @@ -4006,11 +4033,18 @@ EOF ip_in_lxc=$(pct exec "$CTID" -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1) fi [ -n "$ip_in_lxc" ] && break - sleep 1 + # Progressive backoff: 1s for first 20, 2s for next 20, 3s for last 20 + if [ "$i" -le 20 ]; then + sleep 1 + elif [ "$i" -le 40 ]; then + sleep 2 + else + sleep 3 + fi done if [ -z "$ip_in_lxc" ]; then - msg_error "No IP assigned to CT $CTID after 20s" + msg_error "No IP assigned to CT $CTID after 60 attempts" msg_custom "🔧" "${YW}" "Troubleshooting:" echo " • Verify bridge ${BRG} exists and has connectivity" echo " • Check if DHCP server is reachable (if using DHCP)" @@ -4623,7 +4657,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 "$(dirname \"\${BASH_SOURCE[0]}\")/install.func" + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/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) @@ -5241,9 +5275,10 @@ create_lxc_container() { exit 205 } if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - unset CTID - msg_error "Cannot use ID that is already in use." - exit 206 + msg_warn "Container/VM ID $CTID is already in use (detected late). Reassigning..." + CTID=$(get_valid_container_id "$((CTID + 1))") + export CTID + msg_ok "Reassigned to container ID $CTID" fi # Report installation start to API early - captures failures in storage/template/create @@ -5719,30 +5754,77 @@ create_lxc_container() { if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..." - # Check if template issue - retry with fresh download - 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" >>"${BUILD_LOG:-/dev/null}" 2>&1 - msg_ok "Template re-downloaded" - fi + # Check if CTID collision (race condition: ID claimed between validation and creation) + if grep -qiE 'already exists|already in use' "$LOGFILE"; then + local old_ctid="$CTID" + CTID=$(get_valid_container_id "$((CTID + 1))") + export CTID + msg_warn "Container ID $old_ctid was claimed by another process. Retrying with ID $CTID" + LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" + if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then + msg_ok "Container successfully created with new ID $CTID" + else + msg_error "Container creation failed even with new ID $CTID. See $LOGFILE" + _flush_pct_log + exit 209 + fi + else + # Not a CTID collision - check if template issue and retry with fresh download + 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" >>"${BUILD_LOG:-/dev/null}" 2>&1 + msg_ok "Template re-downloaded" + fi - # Retry after repair - if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then - # Fallback to local storage if not already on local - if [[ "$TEMPLATE_STORAGE" != "local" ]]; then - msg_info "Retrying container creation with fallback to local storage" - LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then - msg_ok "Trying local storage fallback" - msg_info "Downloading template to local" - pveam download local "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 - msg_ok "Template downloaded to local" + # Retry after repair + if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then + # Fallback to local storage if not already on local + if [[ "$TEMPLATE_STORAGE" != "local" ]]; then + msg_info "Retrying container creation with fallback to local storage" + LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" + if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then + msg_ok "Trying local storage fallback" + msg_info "Downloading template to local" + pveam download local "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 + msg_ok "Template downloaded to local" + else + msg_ok "Trying local storage fallback" + fi + 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 + 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) + 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) + msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE" + _flush_pct_log + exit 231 + ;; + esac + else + msg_error "Container creation failed. See $LOGFILE" + if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then + set -x + pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" + set +x + fi + _flush_pct_log + exit 209 + fi + else + msg_ok "Container successfully created using local fallback." + fi else - msg_ok "Trying local storage fallback" - fi - if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then - # Local fallback also failed - check for LXC stack version issue + # Already on local storage and still failed - check LXC stack version if grep -qiE 'unsupported .* version' "$LOGFILE"; then msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template" offer_lxc_stack_upgrade_and_maybe_retry "yes" @@ -5770,50 +5852,28 @@ create_lxc_container() { _flush_pct_log exit 209 fi - else - msg_ok "Container successfully created using local fallback." fi else - # Already on local storage and still failed - check LXC stack version - if grep -qiE 'unsupported .* version' "$LOGFILE"; then - 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) - 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) - msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE" - _flush_pct_log - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" - set +x - fi - _flush_pct_log - exit 209 - fi + msg_ok "Container successfully created after template repair." fi - else - msg_ok "Container successfully created after template repair." - fi + fi # close CTID collision else-branch fi - # Verify container exists - pct list | awk '{print $1}' | grep -qx "$CTID" || { - msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE" + # Verify container exists (allow up to 10s for pmxcfs sync in clusters) + local _pct_visible=false + for _pct_check in {1..10}; do + if pct list | awk '{print $1}' | grep -qx "$CTID"; then + _pct_visible=true + break + fi + sleep 1 + done + if [[ "$_pct_visible" != true ]]; then + msg_error "Container ID $CTID not listed in 'pct list' after 10s. See $LOGFILE" + msg_custom "🔧" "${YW}" "This can happen in clusters with pmxcfs sync delays." _flush_pct_log exit 215 - } + fi # Verify config rootfs grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || { @@ -5853,6 +5913,12 @@ create_lxc_container() { # ------------------------------------------------------------------------------ description() { IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) + local script_slug script_url donate_url + + script_slug="${SCRIPT_SLUG:-${NSAPP}}" + script_slug="$(echo "$script_slug" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')" + script_url="https://community-scripts.org/scripts/${script_slug}" + donate_url="https://community-scripts.org/donate" # Generate LXC Description DESCRIPTION=$( @@ -5865,8 +5931,14 @@ description() {

${APP} LXC

- - spend Coffee + + Sponsoring and donations + +

+ +

+ + Open script page

@@ -5959,9 +6031,9 @@ ensure_log_on_host() { # 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. +# error_handler.func — called at the top of this file after sourcing. # -# Do NOT set duplicate traps here. The handlers in error-handler.func +# 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 diff --git a/scripts/core/core.func b/scripts/core/core.func index 38d918a..bceb949 100644 --- a/scripts/core/core.func +++ b/scripts/core/core.func @@ -143,7 +143,7 @@ ensure_profile_loaded() { # Source all profile.d scripts to ensure PATH is complete if [[ -d /etc/profile.d ]]; then for script in /etc/profile.d/*.sh; do - [[ -r "$script" ]] && source "$script" + [[ -r "$script" ]] && source "$script" || true done fi @@ -527,29 +527,23 @@ silent() { fi if [[ $rc -ne 0 ]]; then - # Source explain_exit_code if needed - if ! declare -f explain_exit_code >/dev/null 2>&1; then - if ! source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func"; then - explain_exit_code() { echo "unknown (error-handler.func download failed)"; } - fi - fi + # Return instead of exit so that callers can use `$STD cmd || true` + # or `if $STD cmd; then ...` to handle errors gracefully. + # When no || / if is used, set -e + ERR trap will still catch it + # and error_handler() will display the error and exit. + # + # Set flag so error_handler knows to show log tail from silent's logfile + export _SILENT_FAILED_RC="$rc" + export _SILENT_FAILED_CMD="$cmd" + export _SILENT_FAILED_LINE="$caller_line" + export _SILENT_FAILED_LOG="$logfile" - local explanation - explanation="$(explain_exit_code "$rc")" - - printf "\e[?25h" - msg_error "in line ${caller_line}: exit code ${rc} (${explanation})" - msg_custom "→" "${YWB}" "${cmd}" - - if [[ -s "$logfile" ]]; then - echo -e "\n${TAB}--- Last 20 lines of log ---" - tail -n 20 "$logfile" - echo -e "${TAB}-----------------------------------" - echo -e "${TAB}📋 Full log: ${logfile}\n" - fi - - exit "$rc" + return "$rc" fi + + # Clear stale flags on success (prevents false positives if a previous + # $STD cmd || true failed and a later non-silent command triggers error_handler) + unset _SILENT_FAILED_RC _SILENT_FAILED_CMD _SILENT_FAILED_LINE _SILENT_FAILED_LOG 2>/dev/null || true } # ------------------------------------------------------------------------------ diff --git a/scripts/core/error-handler.func b/scripts/core/error-handler.func index b65ec49..d20ce55 100644 --- a/scripts/core/error-handler.func +++ b/scripts/core/error-handler.func @@ -15,7 +15,7 @@ # - Initialization function for trap setup # # Usage: -# source <(curl -fsSL .../error-handler.func) +# source <(curl -fsSL .../error_handler.func) # catch_errors # # ------------------------------------------------------------------------------ @@ -236,6 +236,16 @@ error_handler() { command="${command//\$STD/}" + # If error originated from silent(), use its captured metadata + # This provides the actual command and line number instead of "silent ..." + if [[ -n "${_SILENT_FAILED_RC:-}" ]]; then + exit_code="$_SILENT_FAILED_RC" + command="$_SILENT_FAILED_CMD" + line_number="$_SILENT_FAILED_LINE" + # Clear flags to prevent stale data on subsequent errors + unset _SILENT_FAILED_RC _SILENT_FAILED_CMD _SILENT_FAILED_LINE + fi + if [[ "$exit_code" -eq 0 ]]; then return 0 fi @@ -279,8 +289,12 @@ error_handler() { fi # Get active log file (BUILD_LOG or INSTALL_LOG) + # Prefer silent()'s logfile when available (contains the actual command output) local active_log="" - if declare -f get_active_logfile >/dev/null 2>&1; then + if [[ -n "${_SILENT_FAILED_LOG:-}" && -s "${_SILENT_FAILED_LOG}" ]]; then + active_log="$_SILENT_FAILED_LOG" + unset _SILENT_FAILED_LOG + elif declare -f get_active_logfile >/dev/null 2>&1; then active_log="$(get_active_logfile)" elif [[ -n "${SILENT_LOGFILE:-}" ]]; then active_log="$SILENT_LOGFILE" diff --git a/scripts/core/install.func b/scripts/core/install.func index 0870b20..d67e6e3 100755 --- a/scripts/core/install.func +++ b/scripts/core/install.func @@ -28,9 +28,9 @@ # ============================================================================== 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 + 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 "$(dirname "${BASH_SOURCE[0]}")/core.func" source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func" @@ -40,8 +40,8 @@ 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 + 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) @@ -59,15 +59,15 @@ get_lxc_ip # - 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 + command -v curl &>/dev/null || return 0 + [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 + [[ -z "${RANDOM_UUID:-}" ]] && return 0 - local progress_status="${1:-configuring}" + 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 + 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 } # ============================================================================== @@ -82,20 +82,20 @@ post_progress_to_api() { # - Sets verbose mode via set_std_mode() # ------------------------------------------------------------------------------ verb_ip6() { - set_std_mode # Set STD mode based on VERBOSE + set_std_mode # Set STD mode based on VERBOSE - if [ "${IPV6_METHOD:-}" = "disable" ]; then - msg_info "Disabling IPv6 (this may affect some services)" - mkdir -p /etc/sysctl.d - $STD tee /etc/sysctl.d/99-disable-ipv6.conf >/dev/null </dev/null </dev/null) || true - fi + # 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 [ "$(hostname -I)" != "" ]; then - break - fi - echo 1>&2 -en "${CROSS}${RD} No Network! " - sleep $RETRY_EVERY - done - 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 121 - fi - rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED - systemctl disable -q --now systemd-networkd-wait-online.service - msg_ok "Set up Container OS" - #msg_custom "${CM}" "${GN}" "Network Connected: ${BL}$(hostname -I)" - msg_ok "Network Connected: ${BL}$(hostname -I)" - post_progress_to_api + for ((i = RETRY_NUM; i > 0; i--)); do + if [ "$(hostname -I)" != "" ]; then + break + fi + echo 1>&2 -en "${CROSS}${RD} No Network! " + sleep $RETRY_EVERY + done + 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 121 + fi + rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED + systemctl disable -q --now systemd-networkd-wait-online.service + msg_ok "Set up Container OS" + #msg_custom "${CM}" "${GN}" "Network Connected: ${BL}$(hostname -I)" + msg_ok "Network Connected: ${BL}$(hostname -I)" + post_progress_to_api } # ------------------------------------------------------------------------------ @@ -148,62 +148,62 @@ setting_up_container() { # - Uses fatal() on DNS resolution failure for critical hosts # ------------------------------------------------------------------------------ network_check() { - set +e - trap - ERR - ipv4_connected=false - ipv6_connected=false - sleep 1 + set +e + trap - ERR + ipv4_connected=false + ipv6_connected=false + sleep 1 - # 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 - else - msg_error "IPv4 Internet Not Connected" - fi + # 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 + else + msg_error "IPv4 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 + # 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 - # 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 122 - fi - fi + # 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 122 + fi + fi - # 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 + # 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 - 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)" - DNS_FAILED=true - else - GIT_STATUS+=" $HOST:($DNSOK)" - fi - done + for HOST in "${GIT_HOSTS[@]}"; do + 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)" + DNS_FAILED=true + else + GIT_STATUS+=" $HOST:($DNSOK)" + fi + done - if [[ "$DNS_FAILED" == true ]]; then - fatal "$GIT_STATUS" - else - msg_ok "$GIT_STATUS" - fi + if [[ "$DNS_FAILED" == true ]]; then + fatal "$GIT_STATUS" + else + msg_ok "$GIT_STATUS" + fi - set -e - trap 'error_handler' ERR + set -e + trap 'error_handler' ERR } # ============================================================================== @@ -220,161 +220,161 @@ network_check() { # - Detects hash mismatch, SSL errors, and generic apt failures # ------------------------------------------------------------------------------ apt_update_safe() { - if $STD apt-get update; then - return 0 - fi + if $STD apt-get update; then + return 0 + fi - local failed_mirror - failed_mirror=$(grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null || echo "unknown") - msg_warn "apt-get update failed (${failed_mirror}), trying alternate mirrors..." + local failed_mirror + failed_mirror=$(grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null || echo "unknown") + msg_warn "apt-get update failed (${failed_mirror}), trying alternate mirrors..." - local distro - distro=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian") + local distro + distro=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian") - local eu_mirrors us_mirrors ap_mirrors - if [[ "$distro" == "ubuntu" ]]; then - eu_mirrors="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de" - us_mirrors="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu" - ap_mirrors="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au" - else - eu_mirrors="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net" - us_mirrors="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com" - ap_mirrors="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in" - fi + local eu_mirrors us_mirrors ap_mirrors + if [[ "$distro" == "ubuntu" ]]; then + eu_mirrors="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de" + us_mirrors="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu" + ap_mirrors="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au" + else + eu_mirrors="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net" + us_mirrors="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com" + ap_mirrors="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in" + fi - local tz regional others - tz=$(cat /etc/timezone 2>/dev/null || echo "UTC") - case "$tz" in - Europe/* | Arctic/*) - regional="$eu_mirrors" - others="$us_mirrors $ap_mirrors" - ;; - America/*) - regional="$us_mirrors" - others="$eu_mirrors $ap_mirrors" - ;; - Asia/* | Australia/* | Pacific/*) - regional="$ap_mirrors" - others="$eu_mirrors $us_mirrors" - ;; - *) - regional="" - others="$eu_mirrors $us_mirrors $ap_mirrors" - ;; - esac + local tz regional others + tz=$(cat /etc/timezone 2>/dev/null || echo "UTC") + case "$tz" in + Europe/* | Arctic/*) + regional="$eu_mirrors" + others="$us_mirrors $ap_mirrors" + ;; + America/*) + regional="$us_mirrors" + others="$eu_mirrors $ap_mirrors" + ;; + Asia/* | Australia/* | Pacific/*) + regional="$ap_mirrors" + others="$eu_mirrors $us_mirrors" + ;; + *) + regional="" + others="$eu_mirrors $us_mirrors $ap_mirrors" + ;; + esac - echo 'Acquire::By-Hash "no";' >/etc/apt/apt.conf.d/99no-by-hash + echo 'Acquire::By-Hash "no";' >/etc/apt/apt.conf.d/99no-by-hash - _try_apt_mirror() { - local m=$1 - for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do - [[ -f "$src" ]] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: http://${m}/|g; s|deb http[s]*://[^/]*/|deb http://${m}/|g" "$src" - done - rm -rf /var/lib/apt/lists/* - local out - out=$(apt-get update 2>&1) - if echo "$out" | grep -qi "hashsum\|hash sum"; then - msg_warn "Mirror ${m} failed (hash mismatch)" - return 1 - elif echo "$out" | grep -qi "SSL\|certificate"; then - msg_warn "Mirror ${m} failed (SSL/certificate error)" - return 1 - elif echo "$out" | grep -q "^E:"; then - msg_warn "Mirror ${m} failed (apt-get update error)" - return 1 - else - msg_ok "CDN set to ${m}: tests passed" - return 0 - fi - } + _try_apt_mirror() { + local m=$1 + for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do + [[ -f "$src" ]] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: http://${m}/|g; s|deb http[s]*://[^/]*/|deb http://${m}/|g" "$src" + done + rm -rf /var/lib/apt/lists/* + local out + out=$(apt-get update 2>&1) + if echo "$out" | grep -qi "hashsum\|hash sum"; then + msg_warn "Mirror ${m} failed (hash mismatch)" + return 1 + elif echo "$out" | grep -qi "SSL\|certificate"; then + msg_warn "Mirror ${m} failed (SSL/certificate error)" + return 1 + elif echo "$out" | grep -q "^E:"; then + msg_warn "Mirror ${m} failed (apt-get update error)" + return 1 + else + msg_ok "CDN set to ${m}: tests passed" + return 0 + fi + } - _scan_reachable() { - local result="" - for m in $1; do - if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then - result="$result $m" - fi - done - echo "$result" | xargs - } + _scan_reachable() { + local result="" + for m in $1; do + if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then + result="$result $m" + fi + done + echo "$result" | xargs + } - local apt_ok=false + local apt_ok=false - # Phase 1: Scan global mirrors first (independent of local CDN issues) - local others_ok - others_ok=$(_scan_reachable "$others") - local others_pick - others_pick=$(printf '%s\n' $others_ok | shuf | head -3 | xargs) + # Phase 1: Scan global mirrors first (independent of local CDN issues) + local others_ok + others_ok=$(_scan_reachable "$others") + local others_pick + others_pick=$(printf '%s\n' $others_ok | shuf | head -3 | xargs) - for mirror in $others_pick; do - msg_custom "${INFO}" "${YW}" "Attempting mirror: ${mirror}" - if _try_apt_mirror "$mirror"; then - apt_ok=true - break - fi - done + for mirror in $others_pick; do + msg_custom "${INFO}" "${YW}" "Attempting mirror: ${mirror}" + if _try_apt_mirror "$mirror"; then + apt_ok=true + break + fi + done - # Phase 2: Try primary mirror - if [[ "$apt_ok" != true ]]; then - local primary - if [[ "$distro" == "ubuntu" ]]; then - primary="archive.ubuntu.com" - else - primary="ftp.debian.org" - fi - if timeout 2 bash -c "echo >/dev/tcp/$primary/80" 2>/dev/null; then - msg_custom "${INFO}" "${YW}" "Attempting mirror: ${primary}" - if _try_apt_mirror "$primary"; then - apt_ok=true - fi - fi - fi + # Phase 2: Try primary mirror + if [[ "$apt_ok" != true ]]; then + local primary + if [[ "$distro" == "ubuntu" ]]; then + primary="archive.ubuntu.com" + else + primary="ftp.debian.org" + fi + if timeout 2 bash -c "echo >/dev/tcp/$primary/80" 2>/dev/null; then + msg_custom "${INFO}" "${YW}" "Attempting mirror: ${primary}" + if _try_apt_mirror "$primary"; then + apt_ok=true + fi + fi + fi - # Phase 3: Fall back to regional mirrors - if [[ "$apt_ok" != true ]]; then - local regional_ok - regional_ok=$(_scan_reachable "$regional") - local regional_pick - regional_pick=$(printf '%s\n' $regional_ok | shuf | head -3 | xargs) + # Phase 3: Fall back to regional mirrors + if [[ "$apt_ok" != true ]]; then + local regional_ok + regional_ok=$(_scan_reachable "$regional") + local regional_pick + regional_pick=$(printf '%s\n' $regional_ok | shuf | head -3 | xargs) - for mirror in $regional_pick; do - msg_custom "${INFO}" "${YW}" "Attempting mirror: ${mirror}" - if _try_apt_mirror "$mirror"; then - apt_ok=true - break - fi - done - fi + for mirror in $regional_pick; do + msg_custom "${INFO}" "${YW}" "Attempting mirror: ${mirror}" + if _try_apt_mirror "$mirror"; then + apt_ok=true + break + fi + done + fi - # Phase 4: All auto mirrors failed, prompt user - if [[ "$apt_ok" != true ]]; then - msg_warn "Multiple mirrors failed (possible CDN synchronization issue)." - if [[ "$distro" == "ubuntu" ]]; then - msg_warn "Find Ubuntu mirrors at: https://launchpad.net/ubuntu/+archivemirrors" - else - msg_warn "Find Debian mirrors at: https://www.debian.org/mirror/list" - fi - local custom_mirror - while true; do - read -rp " Enter a mirror hostname (or 'skip' to abort): " custom_mirror /etc/apt/apt.conf.d/00aptproxy - cat </usr/local/bin/apt-proxy-detect.sh + msg_info "Updating Container OS" + if [[ "$CACHER" == "yes" ]]; then + echo 'Acquire::http::Proxy-Auto-Detect "/usr/local/bin/apt-proxy-detect.sh";' >/etc/apt/apt.conf.d/00aptproxy + local _proxy_raw="${CACHER_IP}" + local _proxy_host _proxy_port _proxy_url + # Parse host and port from URL or plain IP/hostname + _proxy_host=$(echo "$_proxy_raw" | sed -e 's|https\?://||' -e 's|/.*||' | cut -d: -f1) + _proxy_port=$(echo "$_proxy_raw" | sed -e 's|https\?://||' -e 's|/.*||' | cut -s -d: -f2) + if [[ "$_proxy_raw" =~ ^https?:// ]]; then + # Full URL provided — use as-is for proxy output, extract port for nc check + _proxy_url="$_proxy_raw" + _proxy_port="${_proxy_port:-80}" + else + # Legacy: plain IP or hostname — default to http + port 3142 + _proxy_port="${_proxy_port:-3142}" + _proxy_url="http://${_proxy_raw}:${_proxy_port}" + fi + cat </usr/local/bin/apt-proxy-detect.sh #!/bin/bash -if nc -w1 -z "${CACHER_IP}" 3142; then - echo -n "http://${CACHER_IP}:3142" +if nc -w1 -z "${_proxy_host}" ${_proxy_port}; then + echo -n "${_proxy_url}" else echo -n "DIRECT" fi EOF - chmod +x /usr/local/bin/apt-proxy-detect.sh - fi - apt_update_safe - $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade - rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED - msg_ok "Updated Container OS" - post_progress_to_api + chmod +x /usr/local/bin/apt-proxy-detect.sh + fi + apt_update_safe + $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade + rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED + msg_ok "Updated Container OS" + post_progress_to_api - source "$(dirname "${BASH_SOURCE[0]}")/tools.func" + local tools_content + tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/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 } # ============================================================================== @@ -428,26 +451,26 @@ EOF # - Configures TERM environment variable for better terminal support # ------------------------------------------------------------------------------ motd_ssh() { - # Set terminal to 256-color mode - grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc + # Set terminal to 256-color mode + grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc - 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/ProxmoxVE${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" + 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/ProxmoxVE${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" - # Disable default MOTD scripts - chmod -x /etc/update-motd.d/* + # 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 + 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 } # ============================================================================== @@ -464,27 +487,27 @@ motd_ssh() { # - Sets proper permissions on SSH directories and key files # ------------------------------------------------------------------------------ customize() { - if [[ "$PASSWORD" == "" ]]; then - msg_info "Customizing Container" - GETTY_OVERRIDE="/etc/systemd/system/container-getty@1.service.d/override.conf" - mkdir -p "$(dirname "$GETTY_OVERRIDE")" - cat <"$GETTY_OVERRIDE" + if [[ "$PASSWORD" == "" ]]; then + msg_info "Customizing Container" + 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 - systemctl daemon-reload - systemctl restart "$(basename "$(dirname "$GETTY_OVERRIDE")" | sed 's/\.d//')" - msg_ok "Customized Container" - fi - echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update - chmod +x /usr/bin/update + systemctl daemon-reload + systemctl restart "$(basename "$(dirname "$GETTY_OVERRIDE")" | sed 's/\.d//')" + msg_ok "Customized Container" + fi + echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update + chmod +x /usr/bin/update - 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 - post_progress_to_api + 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 + post_progress_to_api } diff --git a/scripts/core/tools.func b/scripts/core/tools.func index 52c6cb3..eb64a9a 100644 --- a/scripts/core/tools.func +++ b/scripts/core/tools.func @@ -1117,15 +1117,90 @@ is_package_installed() { fi } +# ------------------------------------------------------------------------------ +# validate_github_token() +# Checks a GitHub token via the /user endpoint. +# Prints a status message and returns: +# 0 - token is valid +# 1 - token is invalid / expired (HTTP 401) +# 2 - token has no public repo scope (HTTP 200 but missing scope) +# 3 - network/API error +# Also reports expiry date if the token carries an x-oauth-expiry header. +# ------------------------------------------------------------------------------ +validate_github_token() { + local token="${1:-${GITHUB_TOKEN:-}}" + [[ -z "$token" ]] && return 3 + + local response headers http_code expiry_date scopes + headers=$(mktemp) + response=$(curl -sSL -w "%{http_code}" \ + -D "$headers" \ + -o /dev/null \ + -H "Authorization: Bearer $token" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/user" 2>/dev/null) || { + rm -f "$headers" + return 3 + } + http_code="$response" + + # Read expiry header (fine-grained PATs carry this) + expiry_date=$(grep -i '^github-authentication-token-expiration:' "$headers" | + sed 's/.*: *//' | tr -d '\r\n' || true) + # Read token scopes (classic PATs) + scopes=$(grep -i '^x-oauth-scopes:' "$headers" | + sed 's/.*: *//' | tr -d '\r\n' || true) + rm -f "$headers" + + case "$http_code" in + 200) + if [[ -n "$expiry_date" ]]; then + msg_ok "GitHub token is valid (expires: $expiry_date)." + else + msg_ok "GitHub token is valid (no expiry / fine-grained PAT)." + fi + # Warn if classic PAT has no public_repo scope + if [[ -n "$scopes" && "$scopes" != *"public_repo"* && "$scopes" != *"repo"* ]]; then + msg_warn "Token has no 'public_repo' scope - private repos and some release APIs may fail." + return 2 + fi + return 0 + ;; + 401) + msg_error "GitHub token is invalid or expired (HTTP 401)." + return 1 + ;; + *) + msg_warn "GitHub token validation returned HTTP $http_code - treating as valid." + return 0 + ;; + esac +} + # ------------------------------------------------------------------------------ # Prompt user to enter a GitHub Personal Access Token (PAT) interactively # Returns 0 if a valid token was provided, 1 otherwise # ------------------------------------------------------------------------------ prompt_for_github_token() { if [[ ! -t 0 ]]; then + # Non-interactive: pick up var_github_token if set (from default.vars / app.vars / env) + if [[ -z "${GITHUB_TOKEN:-}" && -n "${var_github_token:-}" ]]; then + export GITHUB_TOKEN="${var_github_token}" + msg_ok "GitHub token loaded from var_github_token." + return 0 + fi return 1 fi + # Prefer var_github_token when already set and no interactive override needed + if [[ -z "${GITHUB_TOKEN:-}" && -n "${var_github_token:-}" ]]; then + export GITHUB_TOKEN="${var_github_token}" + msg_ok "GitHub token loaded from var_github_token." + validate_github_token || true + return 0 + fi + local reply read -rp "${TAB}Would you like to enter a GitHub Personal Access Token (PAT)? [y/N]: " reply reply="${reply:-n}" @@ -1147,10 +1222,16 @@ prompt_for_github_token() { msg_warn "Token must not contain spaces. Please try again." continue fi - break + # Validate before accepting + export GITHUB_TOKEN="$token" + if validate_github_token "$token"; then + break + else + msg_warn "Please enter a valid token, or press Ctrl+C to abort." + unset GITHUB_TOKEN + fi done - export GITHUB_TOKEN="$token" msg_ok "GitHub token has been set." return 0 } @@ -2860,7 +2941,7 @@ function fetch_and_deploy_codeberg_release() { while ((attempt < ${#api_timeouts[@]})); do resp=$(curl --connect-timeout 10 --max-time "${api_timeouts[$attempt]}" -fsSL -w "%{http_code}" -o /tmp/codeberg_rel.json "$api_url") && success=true && break - ((attempt++)) + attempt=$((attempt + 1)) if ((attempt < ${#api_timeouts[@]})); then msg_warn "API request timed out after ${api_timeouts[$((attempt - 1))]}s, retrying... (attempt $((attempt + 1))/${#api_timeouts[@]})" fi @@ -3370,7 +3451,8 @@ function fetch_and_deploy_gh_release() { if prompt_for_github_token; then header=(-H "Authorization: token $GITHUB_TOKEN") retry_delay=2 - attempt=0 + attempt=1 + continue fi fi else @@ -4458,7 +4540,7 @@ function setup_hwaccel() { local in_ct="${CTTYPE:-0}" - # ═════════════════════════════════════════════════════�����═════════════════════ + # ═══════════════════════════════════════════════════════════════════════════ # Process Selected GPUs # ═══════════════════════════════════════════════════════════════════════════ for idx in "${SELECTED_INDICES[@]}"; do