From 771c947a06215e846fad3baa0d8505b445285c85 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:05:15 +0100 Subject: [PATCH 01/19] Refactor Core Refactored misc/alpine-install.func to improve error handling, network checks, and MOTD setup. Added misc/alpine-tools.func and misc/error_handler.func for modular tool installation and error management. Enhanced misc/api.func with detailed exit code explanations and telemetry functions. Updated misc/core.func for better initialization, validation, and execution helpers. Removed misc/create_lxc.sh as part of cleanup. --- misc/alpine-install.func | 91 ++--- misc/alpine-tools.func | 507 ++++++++++++++++++++++++++ misc/api.func | 202 ++++++++++- misc/core.func | 756 +++++++++++++++++++++++++++++++-------- misc/create_lxc.sh | 385 -------------------- misc/error_handler.func | 342 ++++++++++++++++++ misc/install.func | 181 ++++++---- 7 files changed, 1813 insertions(+), 651 deletions(-) create mode 100644 misc/alpine-tools.func delete mode 100644 misc/create_lxc.sh create mode 100644 misc/error_handler.func diff --git a/misc/alpine-install.func b/misc/alpine-install.func index ddea81eccd3..9a4dba3f1dc 100644 --- a/misc/alpine-install.func +++ b/misc/alpine-install.func @@ -7,41 +7,56 @@ if ! command -v curl >/dev/null 2>&1; then apk update && apk add curl >/dev/null 2>&1 fi source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) load_functions +catch_errors # This function enables IPv6 if it's not disabled and sets verbose mode verb_ip6() { set_std_mode # Set STD mode based on VERBOSE - if [ "$IPV6_METHOD" == "disable" ]; then - msg_info "Disabling IPv6 (this may affect some services)" + if [ "$DISABLEIPV6" == "yes" ]; then $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 <>/etc/sysctl.conf $STD rc-update add sysctl default - msg_ok "Disabled IPv6" fi } -# This function catches errors and handles them with the error handler function -catch_errors() { - set -Eeuo pipefail - trap 'error_handler $LINENO "$BASH_COMMAND"' ERR -} +set -Eeuo pipefail +trap 'error_handler $? $LINENO "$BASH_COMMAND"' ERR +trap on_exit EXIT +trap on_interrupt INT +trap on_terminate TERM -# This function handles errors error_handler() { + local exit_code="$1" + local line_number="$2" + local command="$3" + + # Exitcode 0 = kein Fehler → ignorieren + if [[ "$exit_code" -eq 0 ]]; then + return 0 + fi + + printf "\e[?25h" + echo -e "\n${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}\n" + exit "$exit_code" +} + +on_exit() { local exit_code="$?" - local line_number="$1" - local command="$2" - local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" - echo -e "\n$error_message\n" + [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile" + exit "$exit_code" +} + +on_interrupt() { + echo -e "\n${RD}Interrupted by user (SIGINT)${CL}" + exit 130 +} + +on_terminate() { + echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}" + exit 143 } # This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection @@ -70,10 +85,10 @@ network_check() { set +e trap - ERR 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 "Internet Connected" + ipv4_status="${GN}✔${CL} IPv4" else - msg_error "Internet NOT Connected" - read -r -p "Would you like to continue anyway? " prompt + ipv4_status="${RD}✖${CL} IPv4" + read -r -p "Internet NOT connected. Continue anyway? " prompt if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then echo -e "${INFO}${RD}Expect Issues Without Internet${CL}" else @@ -82,7 +97,11 @@ network_check() { fi fi RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }') - if [[ -z "$RESOLVEDIP" ]]; then msg_error "DNS Lookup Failure"; else msg_ok "DNS Resolved github.com to ${BL}$RESOLVEDIP${CL}"; fi + if [[ -z "$RESOLVEDIP" ]]; then + msg_error "Internet: ${ipv4_status} DNS Failed" + else + msg_ok "Internet: ${ipv4_status} DNS: ${BL}${RESOLVEDIP}${CL}" + fi set -e trap 'error_handler $LINENO "$BASH_COMMAND"' ERR } @@ -90,8 +109,8 @@ network_check() { # This function updates the Container OS by running apt-get update and upgrade update_os() { msg_info "Updating Container OS" - $STD apk -U upgrade - source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) + $STD apk update && $STD apk upgrade + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-tools.func) msg_ok "Updated Container OS" } @@ -112,18 +131,15 @@ motd_ssh() { 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 -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} IP Address: ${GN}${IP}${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} Repository: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE" echo "echo \"\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${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 } @@ -162,11 +178,4 @@ EOF 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 } diff --git a/misc/alpine-tools.func b/misc/alpine-tools.func new file mode 100644 index 00000000000..663b5f1d31b --- /dev/null +++ b/misc/alpine-tools.func @@ -0,0 +1,507 @@ +#!/bin/ash +# shellcheck shell=ash + +# Erwartet vorhandene msg_* und optional $STD aus deinem Framework. + +# ------------------------------ +# helpers +# ------------------------------ +lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; } +has() { command -v "$1" >/dev/null 2>&1; } + +need_tool() { + # usage: need_tool curl jq unzip ... + # setup missing tools via apk + local missing=0 t + for t in "$@"; do + if ! has "$t"; then missing=1; fi + done + if [ "$missing" -eq 1 ]; then + msg_info "Installing tools: $*" + apk add --no-cache "$@" >/dev/null 2>&1 || { + msg_error "apk add failed for: $*" + return 1 + } + msg_ok "Tools ready: $*" + fi +} + +net_resolves() { + # better handling for missing getent on Alpine + # usage: net_resolves api.github.com + local host="$1" + ping -c1 -W1 "$host" >/dev/null 2>&1 || nslookup "$host" >/dev/null 2>&1 +} + +ensure_usr_local_bin_persist() { + local PROFILE_FILE="/etc/profile.d/10-localbin.sh" + if [ ! -f "$PROFILE_FILE" ]; then + echo 'case ":$PATH:" in *:/usr/local/bin:*) ;; *) export PATH="/usr/local/bin:$PATH";; esac' >"$PROFILE_FILE" + chmod +x "$PROFILE_FILE" + fi +} + +download_with_progress() { + # $1 url, $2 dest + local url="$1" out="$2" cl + need_tool curl pv || return 1 + cl=$(curl -fsSLI "$url" 2>/dev/null | awk 'tolower($0) ~ /^content-length:/ {print $2}' | tr -d '\r') + if [ -n "$cl" ]; then + curl -fsSL "$url" | pv -s "$cl" >"$out" || { + msg_error "Download failed: $url" + return 1 + } + else + curl -fL# -o "$out" "$url" || { + msg_error "Download failed: $url" + return 1 + } + fi +} + +# ------------------------------ +# GitHub: check Release +# ------------------------------ +check_for_gh_release() { + # app, repo, [pinned] + local app="$1" source="$2" pinned="${3:-}" + local app_lc + app_lc="$(lower "$app" | tr -d ' ')" + local current_file="$HOME/.${app_lc}" + local current="" release tag + + msg_info "Check for update: $app" + + net_resolves api.github.com || { + msg_error "DNS/network error: api.github.com" + return 1 + } + need_tool curl jq || return 1 + + tag=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | jq -r '.tag_name // empty') + [ -z "$tag" ] && { + msg_error "Unable to fetch latest tag for $app" + return 1 + } + release="${tag#v}" + + [ -f "$current_file" ] && current="$(cat "$current_file")" + + if [ -n "$pinned" ]; then + if [ "$pinned" = "$release" ]; then + msg_ok "$app pinned to v$pinned (no update)" + return 1 + fi + if [ "$current" = "$pinned" ]; then + msg_ok "$app pinned v$pinned installed (upstream v$release)" + return 1 + fi + msg_info "$app pinned v$pinned (upstream v$release) → update/downgrade" + CHECK_UPDATE_RELEASE="$pinned" + return 0 + fi + + if [ "$release" != "$current" ] || [ ! -f "$current_file" ]; then + CHECK_UPDATE_RELEASE="$release" + msg_info "New release available: v$release (current: v${current:-none})" + return 0 + fi + + msg_ok "$app is up to date (v$release)" + return 1 +} + +# ------------------------------ +# GitHub: get Release & deployen (Alpine) +# modes: tarball | prebuild | singlefile +# ------------------------------ +fetch_and_deploy_gh() { + # $1 app, $2 repo, [$3 mode], [$4 version], [$5 target], [$6 asset_pattern + local app="$1" repo="$2" mode="${3:-tarball}" version="${4:-latest}" target="${5:-/opt/$1}" pattern="${6:-}" + local app_lc + app_lc="$(lower "$app" | tr -d ' ')" + local vfile="$HOME/.${app_lc}" + local json url filename tmpd unpack + + net_resolves api.github.com || { + msg_error "DNS/network error" + return 1 + } + need_tool curl jq tar || return 1 + [ "$mode" = "prebuild" ] || [ "$mode" = "singlefile" ] && need_tool unzip >/dev/null 2>&1 || true + + tmpd="$(mktemp -d)" || return 1 + mkdir -p "$target" + + # Release JSON + if [ "$version" = "latest" ]; then + json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest")" || { + msg_error "GitHub API failed" + rm -rf "$tmpd" + return 1 + } + else + json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/tags/$version")" || { + msg_error "GitHub API failed" + rm -rf "$tmpd" + return 1 + } + fi + + # correct Version + version="$(printf '%s' "$json" | jq -r '.tag_name // empty')" + version="${version#v}" + + [ -z "$version" ] && { + msg_error "No tag in release json" + rm -rf "$tmpd" + return 1 + } + + case "$mode" in + tarball | source) + url="$(printf '%s' "$json" | jq -r '.tarball_url // empty')" + [ -z "$url" ] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz" + filename="${app_lc}-${version}.tar.gz" + download_with_progress "$url" "$tmpd/$filename" || { + rm -rf "$tmpd" + return 1 + } + tar -xzf "$tmpd/$filename" -C "$tmpd" || { + msg_error "tar extract failed" + rm -rf "$tmpd" + return 1 + } + unpack="$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1)" + # copy content of unpack to target + (cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || { + msg_error "copy failed" + rm -rf "$tmpd" + return 1 + } + ;; + prebuild) + [ -n "$pattern" ] || { + msg_error "prebuild requires asset pattern" + rm -rf "$tmpd" + return 1 + } + url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" ' + BEGIN{IGNORECASE=1} + $0 ~ p {print; exit} + ')" + [ -z "$url" ] && { + msg_error "asset not found for pattern: $pattern" + rm -rf "$tmpd" + return 1 + } + filename="${url##*/}" + download_with_progress "$url" "$tmpd/$filename" || { + rm -rf "$tmpd" + return 1 + } + # unpack archive (Zip or tarball) + case "$filename" in + *.zip) + need_tool unzip || { + rm -rf "$tmpd" + return 1 + } + mkdir -p "$tmpd/unp" + unzip -q "$tmpd/$filename" -d "$tmpd/unp" + ;; + *.tar.gz | *.tgz | *.tar.xz | *.tar.zst | *.tar.bz2) + mkdir -p "$tmpd/unp" + tar -xf "$tmpd/$filename" -C "$tmpd/unp" + ;; + *) + msg_error "unsupported archive: $filename" + rm -rf "$tmpd" + return 1 + ;; + esac + # top-level folder strippen + if [ "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq 1 ] && [ -z "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type f | head -n1)" ]; then + unpack="$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d)" + (cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || { + msg_error "copy failed" + rm -rf "$tmpd" + return 1 + } + else + (cd "$tmpd/unp" && tar -cf - .) | (cd "$target" && tar -xf -) || { + msg_error "copy failed" + rm -rf "$tmpd" + return 1 + } + fi + ;; + singlefile) + [ -n "$pattern" ] || { + msg_error "singlefile requires asset pattern" + rm -rf "$tmpd" + return 1 + } + url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" ' + BEGIN{IGNORECASE=1} + $0 ~ p {print; exit} + ')" + [ -z "$url" ] && { + msg_error "asset not found for pattern: $pattern" + rm -rf "$tmpd" + return 1 + } + filename="${url##*/}" + download_with_progress "$url" "$target/$app" || { + rm -rf "$tmpd" + return 1 + } + chmod +x "$target/$app" + ;; + *) + msg_error "Unknown mode: $mode" + rm -rf "$tmpd" + return 1 + ;; + esac + + echo "$version" >"$vfile" + ensure_usr_local_bin_persist + rm -rf "$tmpd" + msg_ok "Deployed $app ($version) → $target" +} + +# ------------------------------ +# yq (mikefarah) – Alpine +# ------------------------------ +setup_yq() { + # prefer apk, unless FORCE_GH=1 + if [ "${FORCE_GH:-0}" != "1" ] && apk info -e yq >/dev/null 2>&1; then + msg_info "Updating yq via apk" + apk add --no-cache --upgrade yq >/dev/null 2>&1 || true + msg_ok "yq ready ($(yq --version 2>/dev/null))" + return 0 + fi + + need_tool curl || return 1 + local arch bin url tmp + case "$(uname -m)" in + x86_64) arch="amd64" ;; + aarch64) arch="arm64" ;; + *) + msg_error "Unsupported arch for yq: $(uname -m)" + return 1 + ;; + esac + url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}" + tmp="$(mktemp)" + download_with_progress "$url" "$tmp" || return 1 + install -m 0755 "$tmp" /usr/local/bin/yq + rm -f "$tmp" + msg_ok "Setup yq ($(yq --version 2>/dev/null))" +} + +# ------------------------------ +# Adminer – Alpine +# ------------------------------ +setup_adminer() { + need_tool curl || return 1 + msg_info "Setup Adminer (Alpine)" + mkdir -p /var/www/localhost/htdocs/adminer + curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \ + -o /var/www/localhost/htdocs/adminer/index.php || { + msg_error "Adminer download failed" + return 1 + } + msg_ok "Adminer at /adminer (served by your webserver)" +} + +# ------------------------------ +# uv – Alpine (musl tarball) +# optional: PYTHON_VERSION="3.12" +# ------------------------------ +setup_uv() { + need_tool curl tar || return 1 + local UV_BIN="/usr/local/bin/uv" + local arch tarball url tmpd ver installed + + case "$(uname -m)" in + x86_64) arch="x86_64-unknown-linux-musl" ;; + aarch64) arch="aarch64-unknown-linux-musl" ;; + *) + msg_error "Unsupported arch for uv: $(uname -m)" + return 1 + ;; + esac + + ver="$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | jq -r '.tag_name' 2>/dev/null)" + ver="${ver#v}" + [ -z "$ver" ] && { + msg_error "uv: cannot determine latest version" + return 1 + } + + if has "$UV_BIN"; then + installed="$($UV_BIN -V 2>/dev/null | awk '{print $2}')" + [ "$installed" = "$ver" ] && { + msg_ok "uv $ver already installed" + return 0 + } + msg_info "Updating uv $installed → $ver" + else + msg_info "Setup uv $ver" + fi + + tmpd="$(mktemp -d)" || return 1 + tarball="uv-${arch}.tar.gz" + url="https://github.com/astral-sh/uv/releases/download/v${ver}/${tarball}" + + download_with_progress "$url" "$tmpd/uv.tar.gz" || { + rm -rf "$tmpd" + return 1 + } + tar -xzf "$tmpd/uv.tar.gz" -C "$tmpd" || { + msg_error "uv: extract failed" + rm -rf "$tmpd" + return 1 + } + + # tar contains ./uv + if [ -x "$tmpd/uv" ]; then + install -m 0755 "$tmpd/uv" "$UV_BIN" + else + # fallback: in subfolder + install -m 0755 "$tmpd"/*/uv "$UV_BIN" 2>/dev/null || { + msg_error "uv binary not found in tar" + rm -rf "$tmpd" + return 1 + } + fi + rm -rf "$tmpd" + ensure_usr_local_bin_persist + msg_ok "Setup uv $ver" + + if [ -n "${PYTHON_VERSION:-}" ]; then + local match + match="$(uv python list --only-downloads 2>/dev/null | awk -v maj="$PYTHON_VERSION" ' + $0 ~ "^cpython-"maj"\\." { print $0 }' | awk -F- '{print $2}' | sort -V | tail -n1)" + [ -z "$match" ] && { + msg_error "No matching Python for $PYTHON_VERSION" + return 1 + } + if ! uv python list | grep -q "cpython-${match}-linux"; then + msg_info "Installing Python $match via uv" + uv python install "$match" || { + msg_error "uv python install failed" + return 1 + } + msg_ok "Python $match installed (uv)" + fi + fi +} + +# ------------------------------ +# Java – Alpine (OpenJDK) +# JAVA_VERSION: 17|21 (Default 21) +# ------------------------------ +setup_java() { + local JAVA_VERSION="${JAVA_VERSION:-21}" pkg + case "$JAVA_VERSION" in + 17) pkg="openjdk17-jdk" ;; + 21 | *) pkg="openjdk21-jdk" ;; + esac + msg_info "Setup Java (OpenJDK $JAVA_VERSION)" + apk add --no-cache "$pkg" >/dev/null 2>&1 || { + msg_error "apk add $pkg failed" + return 1 + } + # set JAVA_HOME + local prof="/etc/profile.d/20-java.sh" + if [ ! -f "$prof" ]; then + echo 'export JAVA_HOME=$(dirname $(dirname $(readlink -f $(command -v java))))' >"$prof" + echo 'case ":$PATH:" in *:$JAVA_HOME/bin:*) ;; *) export PATH="$JAVA_HOME/bin:$PATH";; esac' >>"$prof" + chmod +x "$prof" + fi + msg_ok "Java ready: $(java -version 2>&1 | head -n1)" +} + +# ------------------------------ +# Go – Alpine (apk prefers, else tarball) +# ------------------------------ +setup_go() { + if [ -z "${GO_VERSION:-}" ]; then + msg_info "Setup Go (apk)" + apk add --no-cache go >/dev/null 2>&1 || { + msg_error "apk add go failed" + return 1 + } + msg_ok "Go ready: $(go version 2>/dev/null)" + return 0 + fi + + need_tool curl tar || return 1 + local ARCH TARBALL URL TMP + case "$(uname -m)" in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + *) + msg_error "Unsupported arch for Go: $(uname -m)" + return 1 + ;; + esac + TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz" + URL="https://go.dev/dl/${TARBALL}" + msg_info "Setup Go $GO_VERSION (tarball)" + TMP="$(mktemp)" + download_with_progress "$URL" "$TMP" || return 1 + rm -rf /usr/local/go + tar -C /usr/local -xzf "$TMP" || { + msg_error "extract go failed" + rm -f "$TMP" + return 1 + } + rm -f "$TMP" + ln -sf /usr/local/go/bin/go /usr/local/bin/go + ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt + ensure_usr_local_bin_persist + msg_ok "Go ready: $(go version 2>/dev/null)" +} + +# ------------------------------ +# Composer – Alpine +# uses php83-cli + openssl + phar +# ------------------------------ +setup_composer() { + local COMPOSER_BIN="/usr/local/bin/composer" + if ! has php; then + # prefers php83 + msg_info "Installing PHP CLI for Composer" + apk add --no-cache php83-cli php83-openssl php83-phar php83-iconv >/dev/null 2>&1 || { + # Fallback to generic php if 83 not available + apk add --no-cache php-cli php-openssl php-phar php-iconv >/dev/null 2>&1 || { + msg_error "Failed to install php-cli for composer" + return 1 + } + } + msg_ok "PHP CLI ready: $(php -v | head -n1)" + fi + + if [ -x "$COMPOSER_BIN" ]; then + msg_info "Updating Composer" + else + msg_info "Setup Composer" + fi + + need_tool curl || return 1 + curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || { + msg_error "composer installer download failed" + return 1 + } + php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 || { + msg_error "composer install failed" + return 1 + } + rm -f /tmp/composer-setup.php + ensure_usr_local_bin_persist + msg_ok "Composer ready: $(composer --version 2>/dev/null)" +} diff --git a/misc/api.func b/misc/api.func index d42f919fc5c..05952900089 100644 --- a/misc/api.func +++ b/misc/api.func @@ -2,6 +2,153 @@ # Author: michelroegl-brunner # License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE +# ============================================================================== +# API.FUNC - TELEMETRY & DIAGNOSTICS API +# ============================================================================== +# +# Provides functions for sending anonymous telemetry data to Community-Scripts +# API for analytics and diagnostics purposes. +# +# Features: +# - Container/VM creation statistics +# - Installation success/failure tracking +# - Error code mapping and reporting +# - Privacy-respecting anonymous telemetry +# +# Usage: +# source <(curl -fsSL .../api.func) +# post_to_api # Report container creation +# post_update_to_api # Report installation status +# +# Privacy: +# - Only anonymous statistics (no personal data) +# - User can opt-out via diagnostics settings +# - Random UUID for session tracking only +# +# ============================================================================== + +# ============================================================================== +# SECTION 1: ERROR CODE DESCRIPTIONS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# explain_exit_code() +# +# - Maps numeric exit codes to human-readable error descriptions +# - Supports: +# * Generic/Shell errors (1, 2, 126, 127, 128, 130, 137, 139, 143) +# * Package manager errors (APT, DPKG: 100, 101, 255) +# * Node.js/npm errors (243-249, 254) +# * Python/pip/uv errors (210-212) +# * PostgreSQL errors (231-234) +# * MySQL/MariaDB errors (241-244) +# * MongoDB errors (251-254) +# * Proxmox custom codes (200-231) +# - Returns description string for given exit code +# - Shared function with error_handler.func for consistency +# ------------------------------------------------------------------------------ +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)" ;; + 126) echo "Command invoked cannot execute (permission problem?)" ;; + 127) echo "Command not found" ;; + 128) echo "Invalid argument to exit" ;; + 130) echo "Terminated by Ctrl+C (SIGINT)" ;; + 137) echo "Killed (SIGKILL / Out of memory?)" ;; + 139) echo "Segmentation fault (core dumped)" ;; + 143) echo "Terminated (SIGTERM)" ;; + + # --- Package manager / APT / DPKG --- + 100) echo "APT: Package manager error (broken packages / dependency problems)" ;; + 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;; + 255) echo "DPKG: Fatal internal error" ;; + + # --- Node.js / npm / pnpm / yarn --- + 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 "Node.js: Inspector error" ;; + 254) echo "npm/pnpm/yarn: Unknown fatal error" ;; + + # --- Python / pip / uv --- + 210) echo "Python: Virtualenv / uv environment missing or broken" ;; + 211) echo "Python: Dependency resolution failed" ;; + 212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;; + + # --- PostgreSQL --- + 231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;; + 232) echo "PostgreSQL: Authentication failed (bad user/password)" ;; + 233) echo "PostgreSQL: Database does not exist" ;; + 234) echo "PostgreSQL: Fatal error in query / syntax" ;; + + # --- MySQL / MariaDB --- + 241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;; + 242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;; + 243) echo "MySQL/MariaDB: Database does not exist" ;; + 244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;; + + # --- MongoDB --- + 251) echo "MongoDB: Connection failed (server not running)" ;; + 252) echo "MongoDB: Authentication failed (bad user/password)" ;; + 253) echo "MongoDB: Database not found" ;; + 254) echo "MongoDB: Fatal query error" ;; + + # --- Proxmox Custom Codes --- + 200) echo "Custom: Failed to create lock file" ;; + 203) echo "Custom: Missing CTID variable" ;; + 204) echo "Custom: Missing PCT_OSTYPE variable" ;; + 205) echo "Custom: Invalid CTID (<100)" ;; + 206) echo "Custom: CTID already in use (check 'pct list' and /etc/pve/lxc/)" ;; + 207) echo "Custom: Password contains unescaped special characters (-, /, \\, *, etc.)" ;; + 208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;; + 209) echo "Custom: Container creation failed (check logs for pct create output)" ;; + 210) echo "Custom: Cluster not quorate" ;; + 211) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;; + 214) echo "Custom: Not enough storage space" ;; + 215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;; + 216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;; + 217) echo "Custom: Storage does not support rootdir (check storage capabilities)" ;; + 218) echo "Custom: Template file corrupted or incomplete download (size <1MB or invalid archive)" ;; + 220) echo "Custom: Unable to resolve template path" ;; + 221) echo "Custom: Template file exists but not readable (check file permissions)" ;; + 222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;; + 223) echo "Custom: Template not available after download (storage sync issue)" ;; + 225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;; + 231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;; + + # --- Default --- + *) echo "Unknown error" ;; + esac +} + +# ============================================================================== +# SECTION 2: TELEMETRY FUNCTIONS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# post_to_api() +# +# - Sends LXC container creation statistics to Community-Scripts API +# - Only executes if: +# * curl is available +# * DIAGNOSTICS=yes +# * RANDOM_UUID is set +# - Payload includes: +# * Container type, disk size, CPU cores, RAM +# * OS type and version +# * IPv6 disable status +# * Application name (NSAPP) +# * Installation method +# * PVE version +# * Status: "installing" +# * Random UUID for session tracking +# - Anonymous telemetry (no personal data) +# ------------------------------------------------------------------------------ post_to_api() { if ! command -v curl &>/dev/null; then @@ -38,14 +185,26 @@ post_to_api() { } EOF ) - if [[ "$DIAGNOSTICS" == "yes" ]]; then RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \ -H "Content-Type: application/json" \ -d "$JSON_PAYLOAD") || true fi + } +# ------------------------------------------------------------------------------ +# post_to_api_vm() +# +# - Sends VM creation statistics to Community-Scripts API +# - Similar to post_to_api() but for virtual machines (not containers) +# - Reads DIAGNOSTICS from /usr/local/community-scripts/diagnostics file +# - Payload differences: +# * ct_type=2 (VM instead of LXC) +# * type="vm" +# * Disk size without 'G' suffix (parsed from DISK_SIZE variable) +# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set +# ------------------------------------------------------------------------------ post_to_api_vm() { if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then @@ -88,7 +247,6 @@ post_to_api_vm() { } EOF ) - if [[ "$DIAGNOSTICS" == "yes" ]]; then RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \ -H "Content-Type: application/json" \ @@ -96,19 +254,54 @@ EOF fi } -POST_UPDATE_DONE=false +# ------------------------------------------------------------------------------ +# post_update_to_api() +# +# - Reports installation completion status to API +# - Prevents duplicate submissions via POST_UPDATE_DONE flag +# - Arguments: +# * $1: status ("success" or "failed") +# * $2: exit_code (default: 1 for failed, 0 for success) +# - Payload includes: +# * Final status (success/failed) +# * Error description via get_error_description() +# * Random UUID for session correlation +# - Only executes once per session +# - Silently returns if: +# * curl not available +# * Already reported (POST_UPDATE_DONE=true) +# * DIAGNOSTICS=no +# ------------------------------------------------------------------------------ post_update_to_api() { if ! command -v curl &>/dev/null; then return fi + # Initialize flag if not set (prevents 'unbound variable' error with set -u) + POST_UPDATE_DONE=${POST_UPDATE_DONE:-false} + if [ "$POST_UPDATE_DONE" = true ]; then return 0 fi + exit_code=${2:-1} local API_URL="http://api.community-scripts.org/upload/updatestatus" local status="${1:-failed}" - local error="${2:-No error message}" + if [[ "$status" == "failed" ]]; then + local exit_code="${2:-1}" + elif [[ "$status" == "success" ]]; then + local exit_code="${2:-0}" + fi + + if [[ -z "$exit_code" ]]; then + exit_code=1 + fi + + error=$(explain_exit_code "$exit_code") + + if [ -z "$error" ]; then + error="Unknown error" + fi JSON_PAYLOAD=$( cat </dev/null 2>&1 + # Enable bash tracing if trace mode active + if [[ "${DEV_MODE_TRACE:-false}" == "true" ]]; then + set -x + export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' + fi } -# Function to download & save header files -get_header() { - local app_name=$(echo "${APP,,}" | tr -d ' ') - local app_type=${APP_TYPE:-ct} - local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/${app_type}/headers/${app_name}" - local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}" +# ------------------------------------------------------------------------------ +# parse_dev_mode() +# +# - Parses comma-separated dev_mode variable (e.g., "motd,keep,trace") +# - Sets global flags for each mode: +# * DEV_MODE_MOTD: Setup SSH/MOTD before installation +# * DEV_MODE_KEEP: Never delete container on failure +# * DEV_MODE_TRACE: Enable bash set -x tracing +# * DEV_MODE_PAUSE: Pause after each msg_info step +# * DEV_MODE_BREAKPOINT: Open shell on error instead of cleanup +# * DEV_MODE_LOGS: Persist all logs to /var/log/community-scripts/ +# * DEV_MODE_DRYRUN: Show commands without executing +# - Call this early in script execution +# ------------------------------------------------------------------------------ +parse_dev_mode() { + local mode + # Initialize all flags to false + export DEV_MODE_MOTD=false + export DEV_MODE_KEEP=false + export DEV_MODE_TRACE=false + export DEV_MODE_PAUSE=false + export DEV_MODE_BREAKPOINT=false + export DEV_MODE_LOGS=false + export DEV_MODE_DRYRUN=false + + # Parse comma-separated modes + if [[ -n "${dev_mode:-}" ]]; then + IFS=',' read -ra MODES <<<"$dev_mode" + for mode in "${MODES[@]}"; do + mode="$(echo "$mode" | xargs)" # Trim whitespace + case "$mode" in + motd) export DEV_MODE_MOTD=true ;; + keep) export DEV_MODE_KEEP=true ;; + trace) export DEV_MODE_TRACE=true ;; + pause) export DEV_MODE_PAUSE=true ;; + breakpoint) export DEV_MODE_BREAKPOINT=true ;; + logs) export DEV_MODE_LOGS=true ;; + dryrun) export DEV_MODE_DRYRUN=true ;; + *) + if declare -f msg_warn >/dev/null 2>&1; then + msg_warn "Unknown dev_mode: '$mode' (ignored)" + else + echo "[WARN] Unknown dev_mode: '$mode' (ignored)" >&2 + fi + ;; + esac + done + + # Show active dev modes + local active_modes=() + [[ $DEV_MODE_MOTD == true ]] && active_modes+=("motd") + [[ $DEV_MODE_KEEP == true ]] && active_modes+=("keep") + [[ $DEV_MODE_TRACE == true ]] && active_modes+=("trace") + [[ $DEV_MODE_PAUSE == true ]] && active_modes+=("pause") + [[ $DEV_MODE_BREAKPOINT == true ]] && active_modes+=("breakpoint") + [[ $DEV_MODE_LOGS == true ]] && active_modes+=("logs") + [[ $DEV_MODE_DRYRUN == true ]] && active_modes+=("dryrun") + + if [[ ${#active_modes[@]} -gt 0 ]]; then + if declare -f msg_custom >/dev/null 2>&1; then + msg_custom "🔧" "${YWB}" "Dev modes active: ${active_modes[*]}" + else + echo "[DEV] Active modes: ${active_modes[*]}" >&2 + fi + fi + fi +} - mkdir -p "$(dirname "$local_header_path")" +# ============================================================================== +# SECTION 2: VALIDATION CHECKS +# ============================================================================== - if [ ! -s "$local_header_path" ]; then - if ! curl -fsSL "$header_url" -o "$local_header_path"; then - return 1 - fi +# ------------------------------------------------------------------------------ +# shell_check() +# +# - Verifies that the script is running under Bash shell +# - Exits with error message if different shell is detected +# - Required because scripts use Bash-specific features +# ------------------------------------------------------------------------------ +shell_check() { + if [[ "$(ps -p $$ -o comm=)" != "bash" ]]; then + clear + msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell." + echo -e "\nExiting..." + sleep 2 + exit fi +} - cat "$local_header_path" 2>/dev/null || true +# ------------------------------------------------------------------------------ +# root_check() +# +# - Verifies script is running with root privileges +# - Detects if executed via sudo (which can cause issues) +# - Exits with error if not running as root directly +# ------------------------------------------------------------------------------ +root_check() { + if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then + clear + msg_error "Please run this script as root." + echo -e "\nExiting..." + sleep 2 + exit + fi } -header_info() { - local app_name=$(echo "${APP,,}" | tr -d ' ') - local header_content +# ------------------------------------------------------------------------------ +# pve_check() +# +# - Validates Proxmox VE version compatibility +# - Supported: PVE 8.0-8.9 and PVE 9.0-9.1 +# - Exits with error message if unsupported version detected +# ------------------------------------------------------------------------------ +pve_check() { + local PVE_VER + PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" + + # Check for Proxmox VE 8.x: allow 8.0–8.9 + if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then + local MINOR="${BASH_REMATCH[1]}" + if ((MINOR < 0 || MINOR > 9)); then + msg_error "This version of Proxmox VE is not supported." + msg_error "Supported: Proxmox VE version 8.0 – 8.9" + exit 1 + fi + return 0 + fi - header_content=$(get_header "$app_name") || header_content="" + # Check for Proxmox VE 9.x: allow 9.0–9.1 + if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then + local MINOR="${BASH_REMATCH[1]}" + if ((MINOR < 0 || MINOR > 1)); then + msg_error "This version of Proxmox VE is not yet supported." + msg_error "Supported: Proxmox VE version 9.0 – 9.1" + exit 1 + fi + return 0 + fi - clear - local term_width - term_width=$(tput cols 2>/dev/null || echo 120) + # All other unsupported versions + msg_error "This version of Proxmox VE is not supported." + msg_error "Supported versions: Proxmox VE 8.0 – 8.9 or 9.0 – 9.1" + exit 1 +} - if [ -n "$header_content" ]; then - echo "$header_content" +# ------------------------------------------------------------------------------ +# arch_check() +# +# - Validates system architecture is amd64/x86_64 +# - Exits with error message for unsupported architectures (e.g., ARM/PiMox) +# - Provides link to ARM64-compatible scripts +# ------------------------------------------------------------------------------ +arch_check() { + if [ "$(dpkg --print-architecture)" != "amd64" ]; then + echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n" + echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n" + echo -e "Exiting..." + sleep 2 + exit fi } -ensure_tput() { - if ! command -v tput >/dev/null 2>&1; then - if grep -qi 'alpine' /etc/os-release; then - apk add --no-cache ncurses >/dev/null 2>&1 - elif command -v apt-get >/dev/null 2>&1; then - apt-get update -qq >/dev/null - apt-get install -y -qq ncurses-bin >/dev/null 2>&1 +# ------------------------------------------------------------------------------ +# ssh_check() +# +# - Detects if script is running over SSH connection +# - Warns user for external SSH connections (recommends Proxmox shell) +# - Skips warning for local/same-subnet connections +# - Does not abort execution, only warns +# ------------------------------------------------------------------------------ +ssh_check() { + if [ -n "$SSH_CLIENT" ]; then + local client_ip=$(awk '{print $1}' <<<"$SSH_CLIENT") + local host_ip=$(hostname -I | awk '{print $1}') + + # Check if connection is local (Proxmox WebUI or same machine) + # - localhost (127.0.0.1, ::1) + # - same IP as host + # - local network range (10.x, 172.16-31.x, 192.168.x) + if [[ "$client_ip" == "127.0.0.1" || "$client_ip" == "::1" || "$client_ip" == "$host_ip" ]]; then + return fi - fi -} -is_alpine() { - local os_id="${var_os:-${PCT_OSTYPE:-}}" + # Check if client is in same local network (optional, safer approach) + local host_subnet=$(echo "$host_ip" | cut -d. -f1-3) + local client_subnet=$(echo "$client_ip" | cut -d. -f1-3) + if [[ "$host_subnet" == "$client_subnet" ]]; then + return + fi - if [[ -z "$os_id" && -f /etc/os-release ]]; then - os_id="$( - . /etc/os-release 2>/dev/null - echo "${ID:-}" - )" + # Only warn for truly external connections + msg_warn "Running via external SSH (client: $client_ip)." + msg_warn "For better stability, consider using the Proxmox Shell (Console) instead." fi - - [[ "$os_id" == "alpine" ]] } -is_verbose_mode() { - local verbose="${VERBOSE:-${var_verbose:-no}}" - local tty_status - if [[ -t 2 ]]; then - tty_status="interactive" +# ============================================================================== +# SECTION 3: EXECUTION HELPERS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# get_active_logfile() +# +# - Returns the appropriate log file based on execution context +# - BUILD_LOG: Host operations (container creation) +# - INSTALL_LOG: Container operations (application installation) +# - Fallback to BUILD_LOG if neither is set +# ------------------------------------------------------------------------------ +get_active_logfile() { + if [[ -n "${INSTALL_LOG:-}" ]]; then + echo "$INSTALL_LOG" + elif [[ -n "${BUILD_LOG:-}" ]]; then + echo "$BUILD_LOG" else - tty_status="not-a-tty" + # Fallback for legacy scripts + echo "/tmp/build-$(date +%Y%m%d_%H%M%S).log" fi - [[ "$verbose" != "no" || ! -t 2 ]] } +# Legacy compatibility: SILENT_LOGFILE points to active log +SILENT_LOGFILE="$(get_active_logfile)" + # ------------------------------------------------------------------------------ -# Handles specific curl error codes and displays descriptive messages. -# ------------------------------------------------------------------------------ -__curl_err_handler() { - local exit_code="$1" - local target="$2" - local curl_msg="$3" - - case $exit_code in - 1) msg_error "Unsupported protocol: $target" ;; - 2) msg_error "Curl init failed: $target" ;; - 3) msg_error "Malformed URL: $target" ;; - 5) msg_error "Proxy resolution failed: $target" ;; - 6) msg_error "Host resolution failed: $target" ;; - 7) msg_error "Connection failed: $target" ;; - 9) msg_error "Access denied: $target" ;; - 18) msg_error "Partial file transfer: $target" ;; - 22) msg_error "HTTP error (e.g. 400/404): $target" ;; - 23) msg_error "Write error on local system: $target" ;; - 26) msg_error "Read error from local file: $target" ;; - 28) msg_error "Timeout: $target" ;; - 35) msg_error "SSL connect error: $target" ;; - 47) msg_error "Too many redirects: $target" ;; - 51) msg_error "SSL cert verify failed: $target" ;; - 52) msg_error "Empty server response: $target" ;; - 55) msg_error "Send error: $target" ;; - 56) msg_error "Receive error: $target" ;; - 60) msg_error "SSL CA not trusted: $target" ;; - 67) msg_error "Login denied by server: $target" ;; - 78) msg_error "Remote file not found (404): $target" ;; - *) msg_error "Curl failed with code $exit_code: $target" ;; - esac - - [[ -n "$curl_msg" ]] && printf "%s\n" "$curl_msg" >&2 - exit 1 -} +# silent() +# +# - Executes command with output redirected to active log file +# - On error: displays last 10 lines of log and exits with original exit code +# - Temporarily disables error trap to capture exit code correctly +# - Sources explain_exit_code() for detailed error messages +# ------------------------------------------------------------------------------ +silent() { + local cmd="$*" + local caller_line="${BASH_LINENO[0]:-unknown}" + local logfile="$(get_active_logfile)" + + # Dryrun mode: Show command without executing + if [[ "${DEV_MODE_DRYRUN:-false}" == "true" ]]; then + if declare -f msg_custom >/dev/null 2>&1; then + msg_custom "🔍" "${BL}" "[DRYRUN] $cmd" + else + echo "[DRYRUN] $cmd" >&2 + fi + return 0 + fi -fatal() { - msg_error "$1" - kill -INT $$ + set +Eeuo pipefail + trap - ERR + + "$@" >>"$logfile" 2>&1 + local rc=$? + + set -Eeuo pipefail + trap 'error_handler' ERR + + if [[ $rc -ne 0 ]]; then + # Source explain_exit_code if needed + if ! declare -f explain_exit_code >/dev/null 2>&1; then + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) + fi + + 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 + local log_lines=$(wc -l <"$logfile") + echo "--- Last 10 lines of silent log ---" + tail -n 10 "$logfile" + echo "-----------------------------------" + + # Show how to view full log if there are more lines + if [[ $log_lines -gt 10 ]]; then + msg_custom "📋" "${YW}" "View full log (${log_lines} lines): ${logfile}" + fi + fi + + exit "$rc" + fi } +# ------------------------------------------------------------------------------ +# spinner() +# +# - Displays animated spinner with rotating characters (⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏) +# - Shows SPINNER_MSG alongside animation +# - Runs in infinite loop until killed by stop_spinner() +# - Uses color_spinner() colors for output +# ------------------------------------------------------------------------------ spinner() { local chars=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏) + local msg="${SPINNER_MSG:-Processing...}" local i=0 while true; do local index=$((i++ % ${#chars[@]})) - printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${SPINNER_MSG:-}${CS_CL}" + printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${msg}${CS_CL}" sleep 0.1 done } +# ------------------------------------------------------------------------------ +# clear_line() +# +# - Clears current terminal line using tput or ANSI escape codes +# - Moves cursor to beginning of line (carriage return) +# - Erases from cursor to end of line +# - Fallback to ANSI codes if tput not available +# ------------------------------------------------------------------------------ clear_line() { tput cr 2>/dev/null || echo -en "\r" tput el 2>/dev/null || echo -en "\033[K" } +# ------------------------------------------------------------------------------ +# stop_spinner() +# +# - Stops running spinner process by PID +# - Reads PID from SPINNER_PID variable or /tmp/.spinner.pid file +# - Attempts graceful kill, then forced kill if needed +# - Cleans up temp file and resets terminal state +# - Unsets SPINNER_PID and SPINNER_MSG variables +# ------------------------------------------------------------------------------ stop_spinner() { local pid="${SPINNER_PID:-}" [[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(/dev/null || true } +# ============================================================================== +# SECTION 4: MESSAGE OUTPUT +# ============================================================================== + +# ------------------------------------------------------------------------------ +# msg_info() +# +# - Displays informational message with spinner animation +# - Shows each unique message only once (tracked via MSG_INFO_SHOWN) +# - In verbose/Alpine mode: shows hourglass icon instead of spinner +# - Stops any existing spinner before starting new one +# - Backgrounds spinner process and stores PID for later cleanup +# ------------------------------------------------------------------------------ msg_info() { local msg="$1" [[ -z "$msg" ]] && return @@ -317,6 +532,12 @@ msg_info() { if is_verbose_mode || is_alpine; then local HOURGLASS="${TAB}⏳${TAB}" printf "\r\e[2K%s %b" "$HOURGLASS" "${YW}${msg}${CL}" >&2 + + # Pause mode: Wait for Enter after each step + if [[ "${DEV_MODE_PAUSE:-false}" == "true" ]]; then + echo -en "\n${YWB}[PAUSE]${CL} Press Enter to continue..." >&2 + read -r + fi return fi @@ -325,29 +546,68 @@ msg_info() { SPINNER_PID=$! echo "$SPINNER_PID" >/tmp/.spinner.pid disown "$SPINNER_PID" 2>/dev/null || true + + # Pause mode: Stop spinner and wait + if [[ "${DEV_MODE_PAUSE:-false}" == "true" ]]; then + stop_spinner + echo -en "\n${YWB}[PAUSE]${CL} Press Enter to continue..." >&2 + read -r + fi } +# ------------------------------------------------------------------------------ +# msg_ok() +# +# - Displays success message with checkmark icon +# - Stops spinner and clears line before output +# - Removes message from MSG_INFO_SHOWN to allow re-display +# - Uses green color for success indication +# ------------------------------------------------------------------------------ msg_ok() { local msg="$1" [[ -z "$msg" ]] && return stop_spinner clear_line - printf "%s %b\n" "$CM" "${GN}${msg}${CL}" >&2 + echo -e "$CM ${GN}${msg}${CL}" unset MSG_INFO_SHOWN["$msg"] } +# ------------------------------------------------------------------------------ +# msg_error() +# +# - Displays error message with cross/X icon +# - Stops spinner before output +# - Uses red color for error indication +# - Outputs to stderr +# ------------------------------------------------------------------------------ msg_error() { stop_spinner local msg="$1" - echo -e "${BFR:-} ${CROSS:-✖️} ${RD}${msg}${CL}" + echo -e "${BFR:-}${CROSS:-✖️} ${RD}${msg}${CL}" >&2 } +# ------------------------------------------------------------------------------ +# msg_warn() +# +# - Displays warning message with info/lightbulb icon +# - Stops spinner before output +# - Uses bright yellow color for warning indication +# - Outputs to stderr +# ------------------------------------------------------------------------------ msg_warn() { stop_spinner local msg="$1" - echo -e "${BFR:-} ${INFO:-ℹ️} ${YWB}${msg}${CL}" + echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2 } +# ------------------------------------------------------------------------------ +# msg_custom() +# +# - Displays custom message with user-defined symbol and color +# - Arguments: symbol, color code, message text +# - Stops spinner before output +# - Useful for specialized status messages +# ------------------------------------------------------------------------------ msg_custom() { local symbol="${1:-"[*]"}" local color="${2:-"\e[36m"}" @@ -357,17 +617,181 @@ msg_custom() { echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}" } -run_container_safe() { - local ct="$1" - shift - local cmd="$*" +# ------------------------------------------------------------------------------ +# msg_debug() +# +# - Displays debug message with timestamp when var_full_verbose=1 +# - Automatically enables var_verbose if not already set +# - Shows date/time prefix for log correlation +# - Uses bright yellow color for debug output +# ------------------------------------------------------------------------------ +msg_debug() { + if [[ "${var_full_verbose:-0}" == "1" ]]; then + [[ "${var_verbose:-0}" != "1" ]] && var_verbose=1 + echo -e "${YWB}[$(date '+%F %T')] [DEBUG]${CL} $*" + fi +} + +# ------------------------------------------------------------------------------ +# msg_dev() +# +# - Display development mode messages with 🔧 icon +# - Only shown when dev_mode is active +# - Useful for debugging and development-specific output +# - Format: [DEV] message with distinct formatting +# - Usage: msg_dev "Container ready for debugging" +# ------------------------------------------------------------------------------ +msg_dev() { + if [[ -n "${dev_mode:-}" ]]; then + echo -e "${SEARCH}${BOLD}${DGN}🔧 [DEV]${CL} $*" + fi +} +# +# - Displays error message and immediately terminates script +# - Sends SIGINT to current process to trigger error handler +# - Use for unrecoverable errors that require immediate exit +# ------------------------------------------------------------------------------ +fatal() { + msg_error "$1" + kill -INT $$ +} - lxc-attach -n "$ct" -- bash -euo pipefail -c " - trap 'echo Aborted in container; exit 130' SIGINT SIGTERM - $cmd - " || __handle_general_error "lxc-attach to CT $ct" +# ============================================================================== +# SECTION 5: UTILITY FUNCTIONS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# exit_script() +# +# - Called when user cancels an action +# - Clears screen and displays exit message +# - Exits with default exit code +# ------------------------------------------------------------------------------ +exit_script() { + clear + echo -e "\n${CROSS}${RD}User exited script${CL}\n" + exit } +# ------------------------------------------------------------------------------ +# get_header() +# +# - Downloads and caches application header ASCII art +# - Falls back to local cache if already downloaded +# - Determines app type (ct/vm) from APP_TYPE variable +# - Returns header content or empty string on failure +# ------------------------------------------------------------------------------ +get_header() { + local app_name=$(echo "${APP,,}" | tr -d ' ') + local app_type=${APP_TYPE:-ct} # Default zu 'ct' falls nicht gesetzt + local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/${app_type}/headers/${app_name}" + local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}" + + mkdir -p "$(dirname "$local_header_path")" + + if [ ! -s "$local_header_path" ]; then + if ! curl -fsSL "$header_url" -o "$local_header_path"; then + return 1 + fi + fi + + cat "$local_header_path" 2>/dev/null || true +} + +# ------------------------------------------------------------------------------ +# header_info() +# +# - Displays application header ASCII art at top of screen +# - Clears screen before displaying header +# - Detects terminal width for formatting +# - Returns silently if header not available +# ------------------------------------------------------------------------------ +header_info() { + local app_name=$(echo "${APP,,}" | tr -d ' ') + local header_content + + header_content=$(get_header "$app_name") || header_content="" + + clear + local term_width + term_width=$(tput cols 2>/dev/null || echo 120) + + if [ -n "$header_content" ]; then + echo "$header_content" + fi +} + +# ------------------------------------------------------------------------------ +# ensure_tput() +# +# - Ensures tput command is available for terminal control +# - Installs ncurses-bin on Debian/Ubuntu or ncurses on Alpine +# - Required for clear_line() and terminal width detection +# ------------------------------------------------------------------------------ +ensure_tput() { + if ! command -v tput >/dev/null 2>&1; then + if grep -qi 'alpine' /etc/os-release; then + apk add --no-cache ncurses >/dev/null 2>&1 + elif command -v apt-get >/dev/null 2>&1; then + apt-get update -qq >/dev/null + apt-get install -y -qq ncurses-bin >/dev/null 2>&1 + fi + fi +} + +# ------------------------------------------------------------------------------ +# is_alpine() +# +# - Detects if running on Alpine Linux +# - Checks var_os, PCT_OSTYPE, or /etc/os-release +# - Returns 0 if Alpine, 1 otherwise +# - Used to adjust behavior for Alpine-specific commands +# ------------------------------------------------------------------------------ +is_alpine() { + local os_id="${var_os:-${PCT_OSTYPE:-}}" + + if [[ -z "$os_id" && -f /etc/os-release ]]; then + os_id="$( + . /etc/os-release 2>/dev/null + echo "${ID:-}" + )" + fi + + [[ "$os_id" == "alpine" ]] +} + +# ------------------------------------------------------------------------------ +# is_verbose_mode() +# +# - Determines if script should run in verbose mode +# - Checks VERBOSE and var_verbose variables +# - Also returns true if not running in TTY (pipe/redirect scenario) +# - Used by msg_info() to decide between spinner and static output +# ------------------------------------------------------------------------------ +is_verbose_mode() { + local verbose="${VERBOSE:-${var_verbose:-no}}" + local tty_status + if [[ -t 2 ]]; then + tty_status="interactive" + else + tty_status="not-a-tty" + fi + [[ "$verbose" != "no" || ! -t 2 ]] +} + +# ============================================================================== +# SECTION 6: CLEANUP & MAINTENANCE +# ============================================================================== + +# ------------------------------------------------------------------------------ +# cleanup_lxc() +# +# - Comprehensive cleanup of package managers, caches, and logs +# - Supports Alpine (apk), Debian/Ubuntu (apt), and language package managers +# - Cleans: Python (pip/uv), Node.js (npm/yarn/pnpm), Go, Rust, Ruby, PHP +# - Truncates log files and vacuums systemd journal +# - Run at end of container creation to minimize disk usage +# ------------------------------------------------------------------------------ cleanup_lxc() { msg_info "Cleaning up" @@ -411,6 +835,16 @@ cleanup_lxc() { msg_ok "Cleaned" } +# ------------------------------------------------------------------------------ +# check_or_create_swap() +# +# - Checks if swap is active on system +# - Offers to create swap file if none exists +# - Prompts user for swap size in MB +# - Creates /swapfile with specified size +# - Activates swap immediately +# - Returns 0 if swap active or successfully created, 1 if declined/failed +# ------------------------------------------------------------------------------ check_or_create_swap() { msg_info "Checking for active swap" @@ -449,4 +883,8 @@ check_or_create_swap() { fi } +# ============================================================================== +# SIGNAL TRAPS +# ============================================================================== + trap 'stop_spinner' EXIT INT TERM diff --git a/misc/create_lxc.sh b/misc/create_lxc.sh deleted file mode 100644 index 975311f6a5a..00000000000 --- a/misc/create_lxc.sh +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2025 tteck -# Author: tteck (tteckster) -# Co-Author: MickLesk -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE - -# This sets verbose mode if the global variable is set to "yes" -# if [ "$VERBOSE" == "yes" ]; then set -x; fi - -if command -v curl >/dev/null 2>&1; then - source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) - load_functions - #echo "(create-lxc.sh) Loaded core.func via curl" -elif command -v wget >/dev/null 2>&1; then - source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) - load_functions - #echo "(create-lxc.sh) Loaded core.func via wget" -fi - -# This sets error handling options and defines the error_handler function to handle errors -set -Eeuo pipefail -trap 'error_handler $LINENO "$BASH_COMMAND"' ERR -trap on_exit EXIT -trap on_interrupt INT -trap on_terminate TERM - -function on_exit() { - local exit_code="$?" - [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile" - exit "$exit_code" -} - -function error_handler() { - local exit_code="$?" - local line_number="$1" - local command="$2" - printf "\e[?25h" - echo -e "\n${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}\n" - exit "$exit_code" -} - -function on_interrupt() { - echo -e "\n${RD}Interrupted by user (SIGINT)${CL}" - exit 130 -} - -function on_terminate() { - echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}" - exit 143 -} - -function exit_script() { - clear - printf "\e[?25h" - echo -e "\n${CROSS}${RD}User exited script${CL}\n" - kill 0 - exit 1 -} - -function check_storage_support() { - local CONTENT="$1" - local -a VALID_STORAGES=() - while IFS= read -r line; do - local STORAGE_NAME - STORAGE_NAME=$(awk '{print $1}' <<<"$line") - [[ -z "$STORAGE_NAME" ]] && continue - VALID_STORAGES+=("$STORAGE_NAME") - done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1') - - [[ ${#VALID_STORAGES[@]} -gt 0 ]] -} - -# This function selects a storage pool for a given content type (e.g., rootdir, vztmpl). -function select_storage() { - local CLASS=$1 CONTENT CONTENT_LABEL - - case $CLASS in - container) - CONTENT='rootdir' - CONTENT_LABEL='Container' - ;; - template) - CONTENT='vztmpl' - CONTENT_LABEL='Container template' - ;; - iso) - CONTENT='iso' - CONTENT_LABEL='ISO image' - ;; - images) - CONTENT='images' - CONTENT_LABEL='VM Disk image' - ;; - backup) - CONTENT='backup' - CONTENT_LABEL='Backup' - ;; - snippets) - CONTENT='snippets' - CONTENT_LABEL='Snippets' - ;; - *) - msg_error "Invalid storage class '$CLASS'" - return 1 - ;; - esac - - # Check for preset STORAGE variable - if [ "$CONTENT" = "rootdir" ] && [ -n "${STORAGE:-}" ]; then - if pvesm status -content "$CONTENT" | awk 'NR>1 {print $1}' | grep -qx "$STORAGE"; then - STORAGE_RESULT="$STORAGE" - msg_info "Using preset storage: $STORAGE_RESULT for $CONTENT_LABEL" - return 0 - else - msg_error "Preset storage '$STORAGE' is not valid for content type '$CONTENT'." - return 2 - fi - fi - - local -A STORAGE_MAP - local -a MENU - local COL_WIDTH=0 - - while read -r TAG TYPE _ TOTAL USED FREE _; do - [[ -n "$TAG" && -n "$TYPE" ]] || continue - local STORAGE_NAME="$TAG" - local DISPLAY="${STORAGE_NAME} (${TYPE})" - local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED") - local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE") - local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B" - STORAGE_MAP["$DISPLAY"]="$STORAGE_NAME" - MENU+=("$DISPLAY" "$INFO" "OFF") - ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY} - done < <(pvesm status -content "$CONTENT" | awk 'NR>1') - - if [ ${#MENU[@]} -eq 0 ]; then - msg_error "No storage found for content type '$CONTENT'." - return 2 - fi - - if [ $((${#MENU[@]} / 3)) -eq 1 ]; then - STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}" - STORAGE_INFO="${MENU[1]}" - return 0 - fi - - local WIDTH=$((COL_WIDTH + 42)) - while true; do - local DISPLAY_SELECTED - DISPLAY_SELECTED=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "Storage Pools" \ - --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \ - 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) - - # Cancel or ESC - [[ $? -ne 0 ]] && exit_script - - # Strip trailing whitespace or newline (important for storages like "storage (dir)") - DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED") - - if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then - whiptail --msgbox "No valid storage selected. Please try again." 8 58 - continue - fi - - STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}" - for ((i = 0; i < ${#MENU[@]}; i += 3)); do - if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then - STORAGE_INFO="${MENU[$i + 1]}" - break - fi - done - return 0 - done -} - -# Test if required variables are set -[[ "${CTID:-}" ]] || { - msg_error "You need to set 'CTID' variable." - exit 203 -} -[[ "${PCT_OSTYPE:-}" ]] || { - msg_error "You need to set 'PCT_OSTYPE' variable." - exit 204 -} - -# Test if ID is valid -[ "$CTID" -ge "100" ] || { - msg_error "ID cannot be less than 100." - exit 205 -} - -# Test if ID is in use -if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - echo -e "ID '$CTID' is already in use." - unset CTID - msg_error "Cannot use ID that is already in use." - exit 206 -fi - -# This checks for the presence of valid Container Storage and Template Storage locations -if ! check_storage_support "rootdir"; then - msg_error "No valid storage found for 'rootdir' [Container]" - exit 1 -fi -if ! check_storage_support "vztmpl"; then - msg_error "No valid storage found for 'vztmpl' [Template]" - exit 1 -fi - -while true; do - if select_storage template; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}$TEMPLATE_STORAGE${CL} ($TEMPLATE_STORAGE_INFO) [Template]" - break - fi -done - -while true; do - if select_storage container; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}$CONTAINER_STORAGE${CL} ($CONTAINER_STORAGE_INFO) [Container]" - break - fi -done - -# Check free space on selected container storage -STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') -REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024)) -if [ "$STORAGE_FREE" -lt "$REQUIRED_KB" ]; then - msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G." - exit 214 -fi - -# Check Cluster Quorum if in Cluster -if [ -f /etc/pve/corosync.conf ]; then - msg_info "Checking cluster quorum" - if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then - - msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)." - exit 210 - fi - msg_ok "Cluster is quorate" -fi - -# Update LXC template list -TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" -case "$PCT_OSTYPE" in -debian | ubuntu) - TEMPLATE_PATTERN="-standard_" - ;; -alpine | fedora | rocky | centos) - TEMPLATE_PATTERN="-default_" - ;; -*) - TEMPLATE_PATTERN="" - ;; -esac - -# 1. Check local templates first -msg_info "Searching for template '$TEMPLATE_SEARCH'" -mapfile -t TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" | - awk -v s="$TEMPLATE_SEARCH" -v p="$TEMPLATE_PATTERN" '$1 ~ s && $1 ~ p {print $1}' | - sed 's/.*\///' | sort -t - -k 2 -V -) - -if [ ${#TEMPLATES[@]} -gt 0 ]; then - TEMPLATE_SOURCE="local" -else - msg_info "No local template found, checking online repository" - pveam update >/dev/null 2>&1 - mapfile -t TEMPLATES < <( - pveam update >/dev/null 2>&1 && - pveam available -section system | - sed -n "s/.*\($TEMPLATE_SEARCH.*$TEMPLATE_PATTERN.*\)/\1/p" | - sort -t - -k 2 -V - ) - TEMPLATE_SOURCE="online" -fi - -TEMPLATE="${TEMPLATES[-1]}" -TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || - echo "/var/lib/vz/template/cache/$TEMPLATE")" -msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" - -# 4. Validate template (exists & not corrupted) -TEMPLATE_VALID=1 - -if [ ! -s "$TEMPLATE_PATH" ]; then - TEMPLATE_VALID=0 -elif ! tar --use-compress-program=zstdcat -tf "$TEMPLATE_PATH" >/dev/null 2>&1; then - TEMPLATE_VALID=0 -fi - -if [ "$TEMPLATE_VALID" -eq 0 ]; then - msg_warn "Template $TEMPLATE is missing or corrupted. Re-downloading." - [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH" - for attempt in {1..3}; do - msg_info "Attempt $attempt: Downloading LXC template..." - if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then - msg_ok "Template download successful." - break - fi - if [ $attempt -eq 3 ]; then - msg_error "Failed after 3 attempts. Please check network access or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE" - exit 208 - fi - sleep $((attempt * 5)) - done -fi - -msg_info "Creating LXC Container" -# Check and fix subuid/subgid -grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid -grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid - -# Combine all options -PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}}) -[[ " ${PCT_OPTIONS[@]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}") - -# Secure creation of the LXC container with lock and template check -lockfile="/tmp/template.${TEMPLATE}.lock" -exec 9>"$lockfile" || { - msg_error "Failed to create lock file '$lockfile'." - exit 200 -} -flock -w 60 9 || { - msg_error "Timeout while waiting for template lock" - exit 211 -} - -if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" &>/dev/null; then - msg_error "Container creation failed. Checking if template is corrupted or incomplete." - - if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - msg_error "Template file too small or missing – re-downloading." - rm -f "$TEMPLATE_PATH" - elif ! zstdcat "$TEMPLATE_PATH" | tar -tf - &>/dev/null; then - msg_error "Template appears to be corrupted – re-downloading." - rm -f "$TEMPLATE_PATH" - else - msg_error "Template is valid, but container creation failed. Update your whole Proxmox System (pve-container) first or check https://github.com/community-scripts/ProxmoxVE/discussions/8126" - exit 209 - fi - - # Retry download - for attempt in {1..3}; do - msg_info "Attempt $attempt: Re-downloading template..." - if timeout 120 pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null; then - msg_ok "Template re-download successful." - break - fi - if [ "$attempt" -eq 3 ]; then - msg_error "Three failed attempts. Aborting." - exit 208 - fi - sleep $((attempt * 5)) - done - - sleep 1 # I/O-Sync-Delay - msg_ok "Re-downloaded LXC Template" -fi - -if ! pct list | awk '{print $1}' | grep -qx "$CTID"; then - msg_error "Container ID $CTID not listed in 'pct list' – unexpected failure." - exit 215 -fi - -if ! grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf"; then - msg_error "RootFS entry missing in container config – storage not correctly assigned." - exit 216 -fi - -if grep -q '^hostname:' "/etc/pve/lxc/$CTID.conf"; then - CT_HOSTNAME=$(grep '^hostname:' "/etc/pve/lxc/$CTID.conf" | awk '{print $2}') - if [[ ! "$CT_HOSTNAME" =~ ^[a-z0-9-]+$ ]]; then - msg_warn "Hostname '$CT_HOSTNAME' contains invalid characters – may cause issues with networking or DNS." - fi -fi - -msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created." diff --git a/misc/error_handler.func b/misc/error_handler.func new file mode 100644 index 00000000000..5418a490eec --- /dev/null +++ b/misc/error_handler.func @@ -0,0 +1,342 @@ +#!/usr/bin/env bash +# ------------------------------------------------------------------------------ +# ERROR HANDLER - ERROR & SIGNAL MANAGEMENT +# ------------------------------------------------------------------------------ +# Copyright (c) 2021-2025 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# ------------------------------------------------------------------------------ +# +# Provides comprehensive error handling and signal management for all scripts. +# Includes: +# - Exit code explanations (shell, package managers, databases, custom codes) +# - Error handler with detailed logging +# - Signal handlers (EXIT, INT, TERM) +# - Initialization function for trap setup +# +# Usage: +# source <(curl -fsSL .../error_handler.func) +# catch_errors +# +# ------------------------------------------------------------------------------ + +# ============================================================================== +# SECTION 1: EXIT CODE EXPLANATIONS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# explain_exit_code() +# +# - Maps numeric exit codes to human-readable error descriptions +# - Supports: +# * Generic/Shell errors (1, 2, 126, 127, 128, 130, 137, 139, 143) +# * Package manager errors (APT, DPKG: 100, 101, 255) +# * Node.js/npm errors (243-249, 254) +# * Python/pip/uv errors (210-212) +# * PostgreSQL errors (231-234) +# * MySQL/MariaDB errors (241-244) +# * MongoDB errors (251-254) +# * Proxmox custom codes (200-231) +# - 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)" ;; + 126) echo "Command invoked cannot execute (permission problem?)" ;; + 127) echo "Command not found" ;; + 128) echo "Invalid argument to exit" ;; + 130) echo "Terminated by Ctrl+C (SIGINT)" ;; + 137) echo "Killed (SIGKILL / Out of memory?)" ;; + 139) echo "Segmentation fault (core dumped)" ;; + 143) echo "Terminated (SIGTERM)" ;; + + # --- Package manager / APT / DPKG --- + 100) echo "APT: Package manager error (broken packages / dependency problems)" ;; + 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;; + 255) echo "DPKG: Fatal internal error" ;; + + # --- Node.js / npm / pnpm / yarn --- + 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 "Node.js: Inspector error" ;; + 254) echo "npm/pnpm/yarn: Unknown fatal error" ;; + + # --- Python / pip / uv --- + 210) echo "Python: Virtualenv / uv environment missing or broken" ;; + 211) echo "Python: Dependency resolution failed" ;; + 212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;; + + # --- PostgreSQL --- + 231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;; + 232) echo "PostgreSQL: Authentication failed (bad user/password)" ;; + 233) echo "PostgreSQL: Database does not exist" ;; + 234) echo "PostgreSQL: Fatal error in query / syntax" ;; + + # --- MySQL / MariaDB --- + 241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;; + 242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;; + 243) echo "MySQL/MariaDB: Database does not exist" ;; + 244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;; + + # --- MongoDB --- + 251) echo "MongoDB: Connection failed (server not running)" ;; + 252) echo "MongoDB: Authentication failed (bad user/password)" ;; + 253) echo "MongoDB: Database not found" ;; + 254) echo "MongoDB: Fatal query error" ;; + + # --- Proxmox Custom Codes --- + 200) echo "Custom: Failed to create lock file" ;; + 203) echo "Custom: Missing CTID variable" ;; + 204) echo "Custom: Missing PCT_OSTYPE variable" ;; + 205) echo "Custom: Invalid CTID (<100)" ;; + 206) echo "Custom: CTID already in use (check 'pct list' and /etc/pve/lxc/)" ;; + # --- Proxmox Custom Codes --- + 200) echo "Custom: Failed to create lock file" ;; + 203) echo "Custom: Missing CTID variable" ;; + 204) echo "Custom: Missing PCT_OSTYPE variable" ;; + 205) echo "Custom: Invalid CTID (<100)" ;; + 206) echo "Custom: CTID already in use (check 'pct list' and /etc/pve/lxc/)" ;; + 207) echo "Custom: Password contains unescaped special characters (-, /, \\, *, etc.)" ;; + 208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;; + 209) echo "Custom: Container creation failed (check logs for pct create output)" ;; + 210) echo "Custom: Cluster not quorate" ;; + 211) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;; + 214) echo "Custom: Not enough storage space" ;; + 215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;; + 216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;; + 217) echo "Custom: Storage does not support rootdir (check storage capabilities)" ;; + 218) echo "Custom: Template file corrupted or incomplete download (size <1MB or invalid archive)" ;; + 220) echo "Custom: Unable to resolve template path" ;; + 221) echo "Custom: Template file exists but not readable (check file permissions)" ;; + 222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;; + 223) echo "Custom: Template not available after download (storage sync issue)" ;; + 225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;; + 231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;; + + # --- Default --- + *) echo "Unknown error" ;; + 208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;; + 209) echo "Custom: Container creation failed (check logs for pct create output)" ;; + 210) echo "Custom: Cluster not quorate" ;; + 211) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;; + 214) echo "Custom: Not enough storage space" ;; + 215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;; + 216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;; + 217) echo "Custom: Storage does not support rootdir (check storage capabilities)" ;; + 218) echo "Custom: Template file corrupted or incomplete download (size <1MB or invalid archive)" ;; + 220) echo "Custom: Unable to resolve template path" ;; + 221) echo "Custom: Template file exists but not readable (check file permissions)" ;; + 222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;; + 223) echo "Custom: Template not available after download (storage sync issue)" ;; + 225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;; + 231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;; + + # --- Default --- + *) echo "Unknown error" ;; + esac +} + +# ============================================================================== +# SECTION 2: ERROR HANDLERS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# error_handler() +# +# - Main error handler triggered by ERR trap +# - Arguments: exit_code, command, line_number +# - Behavior: +# * Returns silently if exit_code is 0 (success) +# * Sources explain_exit_code() for detailed error description +# * Displays error message with: +# - Line number where error occurred +# - Exit code with explanation +# - Command that failed +# * Shows last 20 lines of SILENT_LOGFILE if available +# * Copies log to container /root for later inspection +# * Exits with original exit code +# ------------------------------------------------------------------------------ +error_handler() { + local exit_code=${1:-$?} + local command=${2:-${BASH_COMMAND:-unknown}} + local line_number=${BASH_LINENO[0]:-unknown} + + command="${command//\$STD/}" + + if [[ "$exit_code" -eq 0 ]]; then + return 0 + fi + + local explanation + explanation="$(explain_exit_code "$exit_code")" + + printf "\e[?25h" + + # Use msg_error if available, fallback to echo + if declare -f msg_error >/dev/null 2>&1; then + msg_error "in line ${line_number}: exit code ${exit_code} (${explanation}): while executing command ${command}" + else + echo -e "\n${RD}[ERROR]${CL} in line ${RD}${line_number}${CL}: exit code ${RD}${exit_code}${CL} (${explanation}): while executing command ${YWB}${command}${CL}\n" + fi + + if [[ -n "${DEBUG_LOGFILE:-}" ]]; then + { + echo "------ ERROR ------" + echo "Timestamp : $(date '+%Y-%m-%d %H:%M:%S')" + echo "Exit Code : $exit_code ($explanation)" + echo "Line : $line_number" + echo "Command : $command" + echo "-------------------" + } >>"$DEBUG_LOGFILE" + fi + + # Get active log file (BUILD_LOG or INSTALL_LOG) + local active_log="" + if declare -f get_active_logfile >/dev/null 2>&1; then + active_log="$(get_active_logfile)" + elif [[ -n "${SILENT_LOGFILE:-}" ]]; then + active_log="$SILENT_LOGFILE" + fi + + if [[ -n "$active_log" && -s "$active_log" ]]; then + echo "--- Last 20 lines of silent log ---" + tail -n 20 "$active_log" + echo "-----------------------------------" + + # Detect context: Container (INSTALL_LOG set + /root exists) vs Host (BUILD_LOG) + if [[ -n "${INSTALL_LOG:-}" && -d /root ]]; then + # CONTAINER CONTEXT: Copy log and create flag file for host + local container_log="/root/.install-${SESSION_ID:-error}.log" + cp "$active_log" "$container_log" 2>/dev/null || true + + # Create error flag file with exit code for host detection + echo "$exit_code" >"/root/.install-${SESSION_ID:-error}.failed" 2>/dev/null || true + + if declare -f msg_custom >/dev/null 2>&1; then + msg_custom "📋" "${YW}" "Log saved to: ${container_log}" + else + echo -e "${YW}Log saved to:${CL} ${BL}${container_log}${CL}" + fi + else + # HOST CONTEXT: Show local log path and offer container cleanup + if declare -f msg_custom >/dev/null 2>&1; then + msg_custom "📋" "${YW}" "Full log: ${active_log}" + else + echo -e "${YW}Full log:${CL} ${BL}${active_log}${CL}" + fi + + # Offer to remove container if it exists (build errors after container creation) + if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null && pct status "$CTID" &>/dev/null; then + echo "" + echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}" + + if read -t 60 -r response; then + if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then + echo -e "\n${YW}Removing container ${CTID}${CL}" + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + echo -e "${GN}✔${CL} Container ${CTID} removed" + elif [[ "$response" =~ ^[Nn]$ ]]; then + echo -e "\n${YW}Container ${CTID} kept for debugging${CL}" + fi + else + # Timeout - auto-remove + echo -e "\n${YW}No response - auto-removing container${CL}" + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + echo -e "${GN}✔${CL} Container ${CTID} removed" + fi + fi + fi + fi + + exit "$exit_code" +} + +# ============================================================================== +# SECTION 3: SIGNAL HANDLERS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# on_exit() +# +# - EXIT trap handler +# - Cleans up lock files if lockfile variable is set +# - Exits with captured exit code +# - Always runs on script termination (success or failure) +# ------------------------------------------------------------------------------ +on_exit() { + local exit_code=$? + [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile" + exit "$exit_code" +} + +# ------------------------------------------------------------------------------ +# on_interrupt() +# +# - SIGINT (Ctrl+C) trap handler +# - Displays "Interrupted by user" message +# - Exits with code 130 (128 + SIGINT=2) +# ------------------------------------------------------------------------------ +on_interrupt() { + if declare -f msg_error >/dev/null 2>&1; then + msg_error "Interrupted by user (SIGINT)" + else + echo -e "\n${RD}Interrupted by user (SIGINT)${CL}" + fi + exit 130 +} + +# ------------------------------------------------------------------------------ +# on_terminate() +# +# - SIGTERM trap handler +# - Displays "Terminated by signal" message +# - Exits with code 143 (128 + SIGTERM=15) +# - Triggered by external process termination +# ------------------------------------------------------------------------------ +on_terminate() { + if declare -f msg_error >/dev/null 2>&1; then + msg_error "Terminated by signal (SIGTERM)" + else + echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}" + fi + exit 143 +} + +# ============================================================================== +# SECTION 4: INITIALIZATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# catch_errors() +# +# - Initializes error handling and signal traps +# - Enables strict error handling: +# * set -Ee: Exit on error, inherit ERR trap in functions +# * set -o pipefail: Pipeline fails if any command fails +# * set -u: (optional) Exit on undefined variable (if STRICT_UNSET=1) +# - Sets up traps: +# * ERR → error_handler +# * EXIT → on_exit +# * INT → on_interrupt +# * TERM → on_terminate +# - Call this function early in every script +# ------------------------------------------------------------------------------ +catch_errors() { + set -Ee -o pipefail + if [ "${STRICT_UNSET:-0}" = "1" ]; then + set -u + fi + + trap 'error_handler' ERR + trap on_exit EXIT + trap on_interrupt INT + trap on_terminate TERM +} diff --git a/misc/install.func b/misc/install.func index bd51cde15c1..b5571668ac1 100644 --- a/misc/install.func +++ b/misc/install.func @@ -1,8 +1,31 @@ -# Copyright (c) 2021-2025 tteck +# Copyright (c) 2021-2025 community-scripts ORG # Author: tteck (tteckster) -# Co-Author: MickLesk -# License: MIT -# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Co-Author: MickLesk | michelroegl-brunner +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE + +# ============================================================================== +# INSTALL.FUNC - CONTAINER INSTALLATION & SETUP +# ============================================================================== +# +# This file provides installation functions executed inside LXC containers +# after creation. Handles: +# +# - Network connectivity verification (IPv4/IPv6) +# - OS updates and package installation +# - DNS resolution checks +# - MOTD and SSH configuration +# - Container customization and auto-login +# +# Usage: +# - Sourced by -install.sh scripts +# - Executes via pct exec inside container +# - Requires internet connectivity +# +# ============================================================================== + +# ============================================================================== +# SECTION 1: INITIALIZATION +# ============================================================================== if ! command -v curl >/dev/null 2>&1; then printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 @@ -10,49 +33,39 @@ if ! command -v curl >/dev/null 2>&1; then apt-get install -y curl >/dev/null 2>&1 fi source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) load_functions -# This function enables IPv6 if it's not disabled and sets verbose mode -verb_ip6() { - set_std_mode # Set STD mode based on VERBOSE +catch_errors - 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 <>/etc/sysctl.conf + $STD sysctl -p fi } -# This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection +# ------------------------------------------------------------------------------ +# setting_up_container() +# +# - Verifies network connectivity via hostname -I +# - Retries up to RETRY_NUM times with RETRY_EVERY seconds delay +# - Removes Python EXTERNALLY-MANAGED restrictions +# - Disables systemd-networkd-wait-online.service for faster boot +# - Exits with error if network unavailable after retries +# ------------------------------------------------------------------------------ setting_up_container() { msg_info "Setting up Container OS" for ((i = RETRY_NUM; i > 0; i--)); do @@ -74,8 +87,17 @@ setting_up_container() { msg_ok "Network Connected: ${BL}$(hostname -I)" } -# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected -# 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() +# +# - Comprehensive network connectivity check for IPv4 and IPv6 +# - Tests connectivity to multiple DNS servers: +# * IPv4: 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), 9.9.9.9 (Quad9) +# * IPv6: 2606:4700:4700::1111, 2001:4860:4860::8888, 2620:fe::fe +# - Verifies DNS resolution for GitHub and Community-Scripts domains +# - Prompts user to continue if no internet detected +# - Uses fatal() on DNS resolution failure for critical hosts +# ------------------------------------------------------------------------------ network_check() { set +e trap - ERR @@ -135,12 +157,24 @@ network_check() { trap 'error_handler $LINENO "$BASH_COMMAND"' ERR } -# This function updates the Container OS by running apt-get update and upgrade +# ============================================================================== +# SECTION 3: OS UPDATE & PACKAGE MANAGEMENT +# ============================================================================== + +# ------------------------------------------------------------------------------ +# update_os() +# +# - Updates container OS via apt-get update and dist-upgrade +# - Configures APT cacher proxy if CACHER=yes (accelerates package downloads) +# - Removes Python EXTERNALLY-MANAGED restrictions for pip +# - Sources tools.func for additional setup functions after update +# - Uses $STD wrapper to suppress output unless VERBOSE=yes +# ------------------------------------------------------------------------------ update_os() { msg_info "Updating Container OS" if [[ "$CACHER" == "yes" ]]; then - echo 'Acquire::http::Proxy-Auto-Detect "/usr/local/bin/apt-proxy-detect.sh";' >/etc/apt/apt.conf.d/00aptproxy - cat </usr/local/bin/apt-proxy-detect.sh + echo "Acquire::http::Proxy-Auto-Detect \"/usr/local/bin/apt-proxy-detect.sh\";" >/etc/apt/apt.conf.d/00aptproxy + cat <<'EOF' >/usr/local/bin/apt-proxy-detect.sh #!/bin/bash if nc -w1 -z "${CACHER_IP}" 3142; then echo -n "http://${CACHER_IP}:3142" @@ -154,16 +188,30 @@ EOF $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED msg_ok "Updated Container OS" - source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) } -# This function modifies the message of the day (motd) and SSH settings +# ============================================================================== +# SECTION 4: MOTD & SSH CONFIGURATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# motd_ssh() +# +# - Configures Message of the Day (MOTD) with container information +# - Creates /etc/profile.d/00_lxc-details.sh with: +# * Application name +# * Warning banner (DEV repository) +# * OS name and version +# * Hostname and IP address +# * GitHub repository link +# - Disables executable flag on /etc/update-motd.d/* scripts +# - Enables root SSH access if SSH_ROOT=yes +# - Configures TERM environment variable for better terminal support +# ------------------------------------------------------------------------------ motd_ssh() { - # Set terminal to 256-color mode grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc - # Get OS information (Debian / Ubuntu) if [ -f "/etc/os-release" ]; then OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"') @@ -174,14 +222,14 @@ motd_ssh() { 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 -e "echo -e \"${BOLD}${YW}${APPLICATION} LXC Container - DEV Repository${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${RD}WARNING: This is a DEVELOPMENT version (ProxmoxVED). Do NOT use in production!${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} Repository: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE" echo "echo \"\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${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/* if [[ "${SSH_ROOT}" == "yes" ]]; then @@ -190,16 +238,28 @@ motd_ssh() { fi } -# This function customizes the container by modifying the getty service and enabling auto-login for the root user +# ============================================================================== +# SECTION 5: CONTAINER CUSTOMIZATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# customize() +# +# - Customizes container for passwordless root login if PASSWORD is empty +# - Configures getty for auto-login via /etc/systemd/system/container-getty@1.service.d/override.conf +# - Creates /usr/bin/update script for easy application updates +# - Injects SSH authorized keys if SSH_AUTHORIZED_KEY variable is set +# - Sets proper permissions on SSH directories and key files +# ------------------------------------------------------------------------------ customize() { if [[ "$PASSWORD" == "" ]]; then 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 + cat <<'EOF' >$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//') @@ -207,7 +267,6 @@ EOF 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 From 46aa306c947e8ebfc57ec2738d4964abcea163b7 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:06:07 +0100 Subject: [PATCH 02/19] Delete config-file.func --- misc/config-file.func | 699 ------------------------------------------ 1 file changed, 699 deletions(-) delete mode 100644 misc/config-file.func diff --git a/misc/config-file.func b/misc/config-file.func deleted file mode 100644 index 9799b4a4bdd..00000000000 --- a/misc/config-file.func +++ /dev/null @@ -1,699 +0,0 @@ -config_file() { - CONFIG_FILE="/opt/community-scripts/.settings" - - if [[ -f "/opt/community-scripts/${NSAPP}.conf" ]]; then - CONFIG_FILE="/opt/community-scripts/${NSAPP}.conf" - fi - - if CONFIG_FILE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set absolute path to config file" 8 58 "$CONFIG_FILE" --title "CONFIG FILE" 3>&1 1>&2 2>&3); then - if [[ ! -f "$CONFIG_FILE" ]]; then - echo -e "${CROSS}${RD}Config file not found, exiting script!.${CL}" - exit - else - echo -e "${INFO}${BOLD}${DGN}Using config File: ${BGN}$CONFIG_FILE${CL}" - source "$CONFIG_FILE" - fi - fi - if [[ -n "${CT_ID-}" ]]; then - if [[ "$CT_ID" =~ ^([0-9]{3,4})-([0-9]{3,4})$ ]]; then - MIN_ID=${BASH_REMATCH[1]} - MAX_ID=${BASH_REMATCH[2]} - if ((MIN_ID >= MAX_ID)); then - msg_error "Invalid Container ID range. The first number must be smaller than the second number, was ${CT_ID}" - exit - fi - - LIST_OF_IDS=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | grep -oP '"vmid":\s*\K\d+') || true - if [[ -n "$LIST_OF_IDS" ]]; then - for ((ID = MIN_ID; ID <= MAX_ID; ID++)); do - if ! grep -q "^$ID$" <<<"$LIST_OF_IDS"; then - CT_ID=$ID - break - fi - done - fi - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - - elif [[ "$CT_ID" =~ ^[0-9]+$ ]]; then - LIST_OF_IDS=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | grep -oP '"vmid":\s*\K\d+') || true - if [[ -n "$LIST_OF_IDS" ]]; then - - if ! grep -q "^$CT_ID$" <<<"$LIST_OF_IDS"; then - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - else - msg_error "Container ID $CT_ID already exists" - exit - fi - else - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - fi - else - msg_error "Invalid Container ID format. Needs to be 0000-9999 or 0-9999, was ${CT_ID}" - exit - fi - else - if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then - if [ -z "$CT_ID" ]; then - CT_ID="$NEXTID" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - else - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - fi - else - exit_script - fi - - fi - if [[ -n "${CT_TYPE-}" ]]; then - if [[ "$CT_TYPE" -eq 0 ]]; then - CT_TYPE_DESC="Privileged" - elif [[ "$CT_TYPE" -eq 1 ]]; then - CT_TYPE_DESC="Unprivileged" - else - msg_error "Unknown setting for CT_TYPE, should be 1 or 0, was ${CT_TYPE}" - exit - fi - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - else - if CT_TYPE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \ - "1" "Unprivileged" ON \ - "0" "Privileged" OFF \ - 3>&1 1>&2 2>&3); then - if [ -n "$CT_TYPE" ]; then - CT_TYPE_DESC="Unprivileged" - if [ "$CT_TYPE" -eq 0 ]; then - CT_TYPE_DESC="Privileged" - fi - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - fi - else - exit_script - fi - fi - - if [[ -n "${PW-}" ]]; then - if [[ "$PW" == "none" ]]; then - PW="" - else - if [[ "$PW" == *" "* ]]; then - msg_error "Password cannot be empty" - exit - elif [[ ${#PW} -lt 5 ]]; then - msg_error "Password must be at least 5 characters long" - exit - else - echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}" - fi - PW="-password $PW" - fi - else - while true; do - if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then - if [[ -n "$PW1" ]]; then - if [[ "$PW1" == *" "* ]]; then - whiptail --msgbox "Password cannot contain spaces. Please try again." 8 58 - elif [ ${#PW1} -lt 5 ]; then - whiptail --msgbox "Password must be at least 5 characters long. Please try again." 8 58 - else - if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then - if [[ "$PW1" == "$PW2" ]]; then - PW="-password $PW1" - echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}" - break - else - whiptail --msgbox "Passwords do not match. Please try again." 8 58 - fi - else - exit_script - fi - fi - else - PW1="Automatic Login" - PW="" - echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}" - break - fi - else - exit_script - fi - done - fi - - if [[ -n "${HN-}" ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - else - if CT_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo "${CT_NAME,,}" | tr -d ' ') - fi - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - else - exit_script - fi - fi - - if [[ -n "${DISK_SIZE-}" ]]; then - if [[ "$DISK_SIZE" =~ ^-?[0-9]+$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - else - msg_error "DISK_SIZE must be an integer, was ${DISK_SIZE}" - exit - fi - else - if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - else - if ! [[ $DISK_SIZE =~ $INTEGER ]]; then - echo -e "{INFO}${HOLD}${RD} DISK SIZE MUST BE AN INTEGER NUMBER!${CL}" - advanced_settings - fi - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - fi - else - exit_script - fi - fi - - if [[ -n "${CORE_COUNT-}" ]]; then - if [[ "$CORE_COUNT" =~ ^-?[0-9]+$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - else - msg_error "CORE_COUNT must be an integer, was ${CORE_COUNT}" - exit - fi - else - if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3); then - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - else - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - fi - else - exit_script - fi - fi - - if [[ -n "${RAM_SIZE-}" ]]; then - if [[ "$RAM_SIZE" =~ ^-?[0-9]+$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - else - msg_error "RAM_SIZE must be an integer, was ${RAM_SIZE}" - exit - fi - else - if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3); then - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - else - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - fi - else - exit_script - fi - fi - - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) - BRIDGES="" - OLD_IFS=$IFS - IFS=$'\n' - - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - - iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') - (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true - - if [ -f "${iface_indexes_tmpfile}" ]; then - - while read -r pair; do - start=$(echo "${pair}" | cut -d':' -f1) - end=$(echo "${pair}" | cut -d':' -f2) - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') - BRIDGES="${iface_name}"$'\n'"${BRIDGES}" - fi - - done <"${iface_indexes_tmpfile}" - rm -f "${iface_indexes_tmpfile}" - fi - - done - IFS=$OLD_IFS - BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) - - if [[ -n "${BRG-}" ]]; then - if echo "$BRIDGES" | grep -q "${BRG}"; then - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - msg_error "Bridge '${BRG}' does not exist in /etc/network/interfaces or /etc/network/interfaces.d/sdn" - exit - fi - else - BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge:" 15 40 6 $(echo "$BRIDGES" | awk '{print $0, "Bridge"}') 3>&1 1>&2 2>&3) - if [ -z "$BRG" ]; then - exit_script - else - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - fi - fi - - local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$' - local ip_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$' - - if [[ -n ${NET-} ]]; then - if [ "$NET" == "dhcp" ]; then - echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}DHCP${CL}" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}Default${CL}" - GATE="" - elif [[ "$NET" =~ $ip_cidr_regex ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}" - if [[ -n "$GATE" ]]; then - [[ "$GATE" =~ ",gw=" ]] && GATE="${GATE##,gw=}" - if [[ "$GATE" =~ $ip_regex ]]; then - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE${CL}" - GATE=",gw=$GATE" - else - msg_error "Invalid IP Address format for Gateway. Needs to be 0.0.0.0, was ${GATE}" - exit - fi - - else - while true; do - GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - fi - elif [[ "$NET" == *-* ]]; then - IFS="-" read -r ip_start ip_end <<<"$NET" - - if [[ ! "$ip_start" =~ $ip_cidr_regex ]] || [[ ! "$ip_end" =~ $ip_cidr_regex ]]; then - msg_error "Invalid IP range format, was $NET should be 0.0.0.0/0-0.0.0.0/0" - exit 1 - fi - - ip1="${ip_start%%/*}" - ip2="${ip_end%%/*}" - cidr="${ip_start##*/}" - - ip_to_int() { - local IFS=. - read -r i1 i2 i3 i4 <<<"$1" - echo $(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4)) - } - - int_to_ip() { - local ip=$1 - echo "$(((ip >> 24) & 0xFF)).$(((ip >> 16) & 0xFF)).$(((ip >> 8) & 0xFF)).$((ip & 0xFF))" - } - - start_int=$(ip_to_int "$ip1") - end_int=$(ip_to_int "$ip2") - - for ((ip_int = start_int; ip_int <= end_int; ip_int++)); do - ip=$(int_to_ip $ip_int) - msg_info "Checking IP: $ip" - if ! ping -c 2 -W 1 "$ip" >/dev/null 2>&1; then - NET="$ip/$cidr" - msg_ok "Using free IP Address: ${BGN}$NET${CL}" - sleep 3 - break - fi - done - if [[ "$NET" == *-* ]]; then - msg_error "No free IP found in range" - exit 1 - fi - if [ -n "$GATE" ]; then - if [[ "$GATE" =~ $ip_regex ]]; then - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE${CL}" - GATE=",gw=$GATE" - else - msg_error "Invalid IP Address format for Gateway. Needs to be 0.0.0.0, was ${GATE}" - exit - fi - else - while true; do - GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - fi - else - msg_error "Invalid IP Address format. Needs to be 0.0.0.0/0 or a range like 10.0.0.1/24-10.0.0.10/24, was ${NET}" - exit - fi - else - while true; do - NET=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Static IPv4 CIDR Address (/24)" 8 58 dhcp --title "IP ADDRESS" 3>&1 1>&2 2>&3) - exit_status=$? - if [ $exit_status -eq 0 ]; then - if [ "$NET" = "dhcp" ]; then - echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}" - break - else - if [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}" - break - else - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "$NET is an invalid IPv4 CIDR address. Please enter a valid IPv4 CIDR address or 'dhcp'" 8 58 - fi - fi - else - exit_script - fi - done - if [ "$NET" != "dhcp" ]; then - while true; do - GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - else - GATE="" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}Default${CL}" - fi - fi - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if [[ -n "${APT_CACHER_IP-}" ]]; then - if [[ ! $APT_CACHER_IP == "none" ]]; then - APT_CACHER="yes" - echo -e "${NETWORK}${BOLD}${DGN}APT-CACHER IP Address: ${BGN}$APT_CACHER_IP${CL}" - else - APT_CACHER="" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}No${CL}" - fi - else - if APT_CACHER_IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - if [[ -n $APT_CACHER_IP ]]; then - APT_CACHER_IP="none" - fi - else - exit_script - fi - fi - fi - - if [[ -n "${MTU-}" ]]; then - if [[ "$MTU" =~ ^-?[0-9]+$ ]]; then - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU${CL}" - MTU=",mtu=$MTU" - else - msg_error "MTU must be an integer, was ${MTU}" - exit - fi - else - if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$MTU1" ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit_script - fi - fi - - if [[ "$IPV6_METHOD" == "static" ]]; then - if [[ -n "$IPV6STATIC" ]]; then - IP6=",ip6=${IPV6STATIC}" - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}${IPV6STATIC}${CL}" - else - msg_error "IPV6_METHOD is set to static but IPV6STATIC is empty" - exit - fi - elif [[ "$IPV6_METHOD" == "auto" ]]; then - IP6=",ip6=auto" - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}auto${CL}" - else - IP6="" - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}none${CL}" - fi - - if [[ -n "${SD-}" ]]; then - if [[ "$SD" == "none" ]]; then - SD="" - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}Host${CL}" - else - # Strip prefix if present for config file storage - local SD_VALUE="$SD" - [[ "$SD" =~ ^-searchdomain= ]] && SD_VALUE="${SD#-searchdomain=}" - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SD_VALUE${CL}" - SD="-searchdomain=$SD_VALUE" - fi - else - if SD=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z "$SD" ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" - else - exit_script - fi - fi - - if [[ -n "${NS-}" ]]; then - if [[ $NS == "none" ]]; then - NS="" - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}Host${CL}" - else - # Strip prefix if present for config file storage - local NS_VALUE="$NS" - [[ "$NS" =~ ^-nameserver= ]] && NS_VALUE="${NS#-nameserver=}" - if [[ "$NS_VALUE" =~ $ip_regex ]]; then - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NS_VALUE${CL}" - NS="-nameserver=$NS_VALUE" - else - msg_error "Invalid IP Address format for DNS Server. Needs to be 0.0.0.0, was ${NS_VALUE}" - exit - fi - fi - else - if NX=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z "$NX" ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" - else - exit_script - fi - fi - - if [[ -n "${MAC-}" ]]; then - if [[ "$MAC" == "none" ]]; then - MAC="" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}Host${CL}" - else - # Strip prefix if present for config file storage - local MAC_VALUE="$MAC" - [[ "$MAC" =~ ^,hwaddr= ]] && MAC_VALUE="${MAC#,hwaddr=}" - if [[ "$MAC_VALUE" =~ ^([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}$ ]]; then - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC_VALUE${CL}" - MAC=",hwaddr=$MAC_VALUE" - else - msg_error "MAC Address must be in the format xx:xx:xx:xx:xx:xx, was ${MAC_VALUE}" - exit - fi - fi - else - if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z "$MAC1" ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit_script - fi - fi - - if [[ -n "${VLAN-}" ]]; then - if [[ "$VLAN" == "none" ]]; then - VLAN="" - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}Host${CL}" - else - # Strip prefix if present for config file storage - local VLAN_VALUE="$VLAN" - [[ "$VLAN" =~ ^,tag= ]] && VLAN_VALUE="${VLAN#,tag=}" - if [[ "$VLAN_VALUE" =~ ^-?[0-9]+$ ]]; then - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN_VALUE${CL}" - VLAN=",tag=$VLAN_VALUE" - else - msg_error "VLAN must be an integer, was ${VLAN_VALUE}" - exit - fi - fi - else - if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z "$VLAN1" ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" - else - exit_script - fi - fi - - if [[ -n "${TAGS-}" ]]; then - if [[ "$TAGS" == *"DEFAULT"* ]]; then - TAGS="${TAGS//DEFAULT/}" - TAGS="${TAGS//;/}" - TAGS="$TAGS;${var_tags:-}" - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - fi - else - TAGS="community-scripts;" - if ADV_TAGS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then - if [ -n "${ADV_TAGS}" ]; then - ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') - TAGS="${ADV_TAGS}" - else - TAGS=";" - fi - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - else - exit_script - fi - fi - - if [[ -n "${SSH-}" ]]; then - if [[ "$SSH" == "yes" ]]; then - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - if [[ ! -z "$SSH_AUTHORIZED_KEY" ]]; then - echo -e "${ROOTSSH}${BOLD}${DGN}SSH Authorized Key: ${BGN}********************${CL}" - else - echo -e "${ROOTSSH}${BOLD}${DGN}SSH Authorized Key: ${BGN}None${CL}" - fi - elif [[ "$SSH" == "no" ]]; then - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - else - msg_error "SSH needs to be 'yes' or 'no', was ${SSH}" - exit - fi - else - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "SSH Authorized key for root (leave empty for none)" 8 58 --title "SSH Key" 3>&1 1>&2 2>&3)" - if [[ -z "${SSH_AUTHORIZED_KEY}" ]]; then - SSH_AUTHORIZED_KEY="" - fi - if [[ "$PW" == -password* || -n "$SSH_AUTHORIZED_KEY" ]]; then - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable Root SSH Access?" 10 58); then - SSH="yes" - else - SSH="no" - fi - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - else - SSH="no" - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - fi - fi - - if [[ -n "$ENABLE_FUSE" ]]; then - if [[ "$ENABLE_FUSE" == "yes" ]]; then - echo -e "${FUSE}${BOLD}${DGN}Enable FUSE: ${BGN}Yes${CL}" - elif [[ "$ENABLE_FUSE" == "no" ]]; then - echo -e "${FUSE}${BOLD}${DGN}Enable FUSE: ${BGN}No${CL}" - else - msg_error "Enable FUSE needs to be 'yes' or 'no', was ${ENABLE_FUSE}" - exit - fi - else - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE" --yesno "Enable FUSE?" 10 58); then - ENABLE_FUSE="yes" - else - ENABLE_FUSE="no" - fi - echo -e "${FUSE}${BOLD}${DGN}Enable FUSE: ${BGN}$ENABLE_FUSE${CL}" - fi - - if [[ -n "$ENABLE_TUN" ]]; then - if [[ "$ENABLE_TUN" == "yes" ]]; then - echo -e "${FUSE}${BOLD}${DGN}Enable TUN: ${BGN}Yes${CL}" - elif [[ "$ENABLE_TUN" == "no" ]]; then - echo -e "${FUSE}${BOLD}${DGN}Enable TUN: ${BGN}No${CL}" - else - msg_error "Enable TUN needs to be 'yes' or 'no', was ${ENABLE_TUN}" - exit - fi - else - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "TUN" --yesno "Enable TUN?" 10 58); then - ENABLE_TUN="yes" - else - ENABLE_TUN="no" - fi - echo -e "${FUSE}${BOLD}${DGN}Enable TUN: ${BGN}$ENABLE_TUN${CL}" - fi - - if [[ -n "${VERBOSE-}" ]]; then - if [[ "$VERBOSE" == "yes" ]]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" - elif [[ "$VERBOSE" == "no" ]]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}No${CL}" - else - msg_error "Verbose Mode needs to be 'yes' or 'no', was ${VERBOSE}" - exit - fi - else - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then - VERBOSE="yes" - else - VERBOSE="no" - fi - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" - fi - - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS WITH CONFIG FILE COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then - echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above settings${CL}" - else - clear - header_info - echo -e "${INFO}${HOLD} ${GN}Using Config File on node $PVEHOST_NAME${CL}" - config_file - fi -} From bdf578f2a2fed3b71d49fb71daf356671e7e8bfc Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:06:46 +0100 Subject: [PATCH 03/19] Update install.func --- misc/install.func | 2 -- 1 file changed, 2 deletions(-) diff --git a/misc/install.func b/misc/install.func index b5571668ac1..d156d7afb4a 100644 --- a/misc/install.func +++ b/misc/install.func @@ -222,8 +222,6 @@ motd_ssh() { PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" echo "echo -e \"\"" >"$PROFILE_FILE" - echo -e "echo -e \"${BOLD}${YW}${APPLICATION} LXC Container - DEV Repository${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${RD}WARNING: This is a DEVELOPMENT version (ProxmoxVED). Do NOT use in production!${CL}\"" >>"$PROFILE_FILE" echo -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" echo -e "echo -e \"${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE" From 640eddd0941c32eabdfc6633a6aaab69a78fbd1a Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:01:53 +0100 Subject: [PATCH 04/19] Refactor stop_all_services function and variable names Refactor service stopping logic and improve variable handling --- misc/tools.func | 399 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 312 insertions(+), 87 deletions(-) diff --git a/misc/tools.func b/misc/tools.func index 5233c07c088..4bf173cd5a4 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -72,23 +72,23 @@ stop_all_services() { local service_patterns=("$@") for pattern in "${service_patterns[@]}"; do - # Find all matching services (use || true to avoid pipeline failures) - local services - services=$(systemctl list-units --type=service --all 2>/dev/null | - grep -oE "${pattern}[^ ]*\.service" 2>/dev/null | - sort -u 2>/dev/null || true) - - # Only process if we found any services - if [[ -n "$services" ]]; then - while IFS= read -r service; do - [[ -z "$service" ]] && continue + # Find all matching services + + systemctl list-units --type=service --all 2>/dev/null | + grep -oE "${pattern}[^ ]*\.service" | + sort -u | + + + + while read -r service; do + $STD systemctl stop "$service" 2>/dev/null || true $STD systemctl disable "$service" 2>/dev/null || true - done <<<"$services" - fi + done + done - return 0 + } # ------------------------------------------------------------------------------ @@ -1214,7 +1214,7 @@ setup_deb822_repo() { local gpg_url="$2" local repo_url="$3" local suite="$4" - local component="${5-main}" + local component="${5:-main}" local architectures="${6-}" # optional # Validate required parameters @@ -1535,9 +1535,10 @@ check_for_gh_release() { # ------------------------------------------------------------------------------ create_self_signed_cert() { local APP_NAME="${1:-${APPLICATION}}" - local CERT_DIR="/etc/ssl/${APP_NAME}" - local CERT_KEY="${CERT_DIR}/${APP_NAME}.key" - local CERT_CRT="${CERT_DIR}/${APP_NAME}.crt" + local APP_NAME_LC=$(echo "${APP_NAME,,}" | tr -d ' ') + local CERT_DIR="/etc/ssl/${APP_NAME_LC}" + local CERT_KEY="${CERT_DIR}/${APP_NAME_LC}.key" + local CERT_CRT="${CERT_DIR}/${APP_NAME_LC}.crt" if [[ -f "$CERT_CRT" && -f "$CERT_KEY" ]]; then return 0 @@ -2940,9 +2941,16 @@ setup_mariadb() { # Resolve "latest" to actual version if [[ "$MARIADB_VERSION" == "latest" ]]; then if ! curl -fsI --max-time 10 http://mirror.mariadb.org/repo/ >/dev/null 2>&1; then - msg_warn "MariaDB mirror not reachable - trying cached package list fallback" - # Fallback: try to use a known stable version - MARIADB_VERSION="12.0" + msg_warn "MariaDB mirror not reachable - trying mariadb_repo_setup fallback" + # Try using official mariadb_repo_setup script as fallback + if curl -fsSL --max-time 15 https://r.mariadb.com/downloads/mariadb_repo_setup 2>/dev/null | bash -s -- --skip-verify >/dev/null 2>&1; then + msg_ok "MariaDB repository configured via mariadb_repo_setup" + # Extract version from configured repo + MARIADB_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.list 2>/dev/null | head -n1 || echo "12.2") + else + msg_warn "mariadb_repo_setup failed - using hardcoded fallback version" + MARIADB_VERSION="12.2" + fi else MARIADB_VERSION=$(curl -fsSL --max-time 15 http://mirror.mariadb.org/repo/ 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' | @@ -2952,8 +2960,14 @@ setup_mariadb() { head -n1 || echo "") if [[ -z "$MARIADB_VERSION" ]]; then - msg_warn "Could not parse latest GA MariaDB version from mirror - using fallback" - MARIADB_VERSION="12.0" + msg_warn "Could not parse latest GA MariaDB version from mirror - trying mariadb_repo_setup" + if curl -fsSL --max-time 15 https://r.mariadb.com/downloads/mariadb_repo_setup 2>/dev/null | bash -s -- --skip-verify >/dev/null 2>&1; then + msg_ok "MariaDB repository configured via mariadb_repo_setup" + MARIADB_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.list 2>/dev/null | head -n1 || echo "12.2") + else + msg_warn "mariadb_repo_setup failed - using hardcoded fallback version" + MARIADB_VERSION="12.2" + fi fi fi fi @@ -3633,8 +3647,8 @@ function setup_php() { if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then msg_info "Removing conflicting PHP ${CURRENT_PHP} (need ${PHP_VERSION})" stop_all_services "php.*-fpm" - $STD apt-get purge -y "php*" 2>/dev/null || true - $STD apt-get autoremove -y 2>/dev/null || true + $STD apt purge -y "php*" 2>/dev/null || true + $STD apt autoremove -y 2>/dev/null || true fi # NOW create pinning for the desired version @@ -3661,7 +3675,7 @@ EOF } ensure_apt_working || return 1 - $STD apt-get update + $STD apt update # Get available PHP version from repository local AVAILABLE_PHP_VERSION="" @@ -3856,74 +3870,74 @@ function setup_postgresql() { local SUITE case "$DISTRO_CODENAME" in trixie | forky | sid) - # For Debian Testing/Unstable, try PostgreSQL repo first, fallback to native packages + if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then SUITE="trixie-pgdg" - setup_deb822_repo \ - "pgdg" \ - "https://www.postgresql.org/media/keys/ACCC4CF8.asc" \ - "https://apt.postgresql.org/pub/repos/apt" \ - "$SUITE" \ - "main" - - if ! $STD apt update; then - msg_warn "Failed to update PostgreSQL repository, falling back to native packages" - SUITE="" - fi + + + + + + + + + + + else - SUITE="" + SUITE="bookworm-pgdg" fi - # If no repo or packages not installable, use native Debian packages - if [[ -z "$SUITE" ]] || ! apt-cache show "postgresql-${PG_VERSION}" 2>/dev/null | grep -q "Version:"; then - msg_info "Using native Debian packages for $DISTRO_CODENAME" - - # Install ssl-cert dependency if available - if apt-cache search "^ssl-cert$" 2>/dev/null | grep -q .; then - $STD apt install -y ssl-cert 2>/dev/null || true - fi - - if ! $STD apt install -y postgresql postgresql-client 2>/dev/null; then - msg_error "Failed to install native PostgreSQL packages" - return 1 - fi - - if ! command -v psql >/dev/null 2>&1; then - msg_error "PostgreSQL installed but psql command not found" - return 1 - fi - - # Restore database backup if we upgraded from previous version - if [[ -n "$CURRENT_PG_VERSION" ]]; then - msg_info "Restoring PostgreSQL databases from backup..." - $STD runuser -u postgres -- psql /dev/null || { - msg_warn "Failed to restore database backup - this may be expected for major version upgrades" - } - fi - - $STD systemctl enable --now postgresql 2>/dev/null || true - - # Get actual installed version - INSTALLED_VERSION="$(psql -V 2>/dev/null | awk '{print $3}' | cut -d. -f1)" - - # Add PostgreSQL binaries to PATH - if ! grep -q '/usr/lib/postgresql' /etc/environment 2>/dev/null; then - echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/'"${INSTALLED_VERSION}"'/bin"' >/etc/environment - fi - - cache_installed_version "postgresql" "$INSTALLED_VERSION" - msg_ok "Setup PostgreSQL $INSTALLED_VERSION (native)" - - # Install optional modules if specified - if [[ -n "$PG_MODULES" ]]; then - IFS=',' read -ra MODULES <<<"$PG_MODULES" - for module in "${MODULES[@]}"; do - $STD apt install -y "postgresql-${INSTALLED_VERSION}-${module}" 2>/dev/null || true - done - fi - return 0 - fi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ;; *) SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt") @@ -4817,3 +4831,214 @@ function setup_yq() { cache_installed_version "yq" "$FINAL_VERSION" msg_ok "Setup yq $FINAL_VERSION" } + +# ------------------------------------------------------------------------------ +# Docker Engine Installation and Management (All-In-One) +# +# Description: +# - Detects and migrates old Docker installations +# - Installs/Updates Docker Engine via official repository +# - Optional: Installs/Updates Portainer CE +# - Updates running containers interactively +# - Cleans up legacy repository files +# +# Usage: +# setup_docker +# DOCKER_PORTAINER="true" setup_docker +# DOCKER_LOG_DRIVER="json-file" setup_docker +# +# Variables: +# DOCKER_PORTAINER - Install Portainer CE (optional, "true" to enable) +# DOCKER_LOG_DRIVER - Log driver (optional, default: "journald") +# DOCKER_SKIP_UPDATES - Skip container update check (optional, "true" to skip) +# +# Features: +# - Migrates from get.docker.com to repository-based installation +# - Updates Docker Engine if newer version available +# - Interactive container update with multi-select +# - Portainer installation and update support +# ------------------------------------------------------------------------------ +function setup_docker() { + local docker_installed=false + local portainer_installed=false + + # Check if Docker is already installed + if command -v docker &>/dev/null; then + docker_installed=true + DOCKER_CURRENT_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1) + msg_info "Docker $DOCKER_CURRENT_VERSION detected" + fi + + # Check if Portainer is running + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^portainer$'; then + portainer_installed=true + msg_info "Portainer container detected" + fi + + # Cleanup old repository configurations + if [ -f /etc/apt/sources.list.d/docker.list ]; then + msg_info "Migrating from old Docker repository format" + rm -f /etc/apt/sources.list.d/docker.list + rm -f /etc/apt/keyrings/docker.asc + fi + + # Setup/Update Docker repository + msg_info "Setting up Docker Repository" + setup_deb822_repo \ + "docker" \ + "https://download.docker.com/linux/$(get_os_info id)/gpg" \ + "https://download.docker.com/linux/$(get_os_info id)" \ + "$(get_os_info codename)" \ + "stable" \ + "$(dpkg --print-architecture)" + + # Install or upgrade Docker + if [ "$docker_installed" = true ]; then + msg_info "Checking for Docker updates" + DOCKER_LATEST_VERSION=$(apt-cache policy docker-ce | grep Candidate | awk '{print $2}' | cut -d':' -f2 | cut -d'-' -f1) + + if [ "$DOCKER_CURRENT_VERSION" != "$DOCKER_LATEST_VERSION" ]; then + msg_info "Updating Docker $DOCKER_CURRENT_VERSION → $DOCKER_LATEST_VERSION" + $STD apt install -y --only-upgrade \ + docker-ce \ + docker-ce-cli \ + containerd.io \ + docker-buildx-plugin \ + docker-compose-plugin + msg_ok "Updated Docker to $DOCKER_LATEST_VERSION" + else + msg_ok "Docker is up-to-date ($DOCKER_CURRENT_VERSION)" + fi + else + msg_info "Installing Docker" + $STD apt install -y \ + docker-ce \ + docker-ce-cli \ + containerd.io \ + docker-buildx-plugin \ + docker-compose-plugin + + DOCKER_CURRENT_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1) + msg_ok "Installed Docker $DOCKER_CURRENT_VERSION" + fi + + # Configure daemon.json + local log_driver="${DOCKER_LOG_DRIVER:-journald}" + mkdir -p /etc/docker + if [ ! -f /etc/docker/daemon.json ]; then + cat </etc/docker/daemon.json +{ + "log-driver": "$log_driver" +} +EOF + fi + + # Enable and start Docker + systemctl enable -q --now docker + + # Portainer Management + if [[ "${DOCKER_PORTAINER:-}" == "true" ]]; then + if [ "$portainer_installed" = true ]; then + msg_info "Checking for Portainer updates" + PORTAINER_CURRENT=$(docker inspect portainer --format='{{.Config.Image}}' 2>/dev/null | cut -d':' -f2) + PORTAINER_LATEST=$(curl -fsSL https://registry.hub.docker.com/v2/repositories/portainer/portainer-ce/tags?page_size=100 | grep -oP '"name":"\K[0-9]+\.[0-9]+\.[0-9]+"' | head -1 | tr -d '"') + + if [ "$PORTAINER_CURRENT" != "$PORTAINER_LATEST" ]; then + read -r -p "${TAB3}Update Portainer $PORTAINER_CURRENT → $PORTAINER_LATEST? " prompt + if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then + msg_info "Updating Portainer" + docker stop portainer + docker rm portainer + docker pull portainer/portainer-ce:latest + docker run -d \ + -p 9000:9000 \ + -p 9443:9443 \ + --name=portainer \ + --restart=always \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v portainer_data:/data \ + portainer/portainer-ce:latest + msg_ok "Updated Portainer to $PORTAINER_LATEST" + fi + else + msg_ok "Portainer is up-to-date ($PORTAINER_CURRENT)" + fi + else + msg_info "Installing Portainer" + docker volume create portainer_data + docker run -d \ + -p 9000:9000 \ + -p 9443:9443 \ + --name=portainer \ + --restart=always \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v portainer_data:/data \ + portainer/portainer-ce:latest + + LOCAL_IP=$(hostname -I | awk '{print $1}') + msg_ok "Installed Portainer (http://${LOCAL_IP}:9000)" + fi + fi + + # Interactive Container Update Check + if [[ "${DOCKER_SKIP_UPDATES:-}" != "true" ]] && [ "$docker_installed" = true ]; then + msg_info "Checking for container updates" + + # Get list of running containers with update status + local containers_with_updates=() + local container_info=() + local index=1 + + while IFS= read -r container; do + local name=$(echo "$container" | awk '{print $1}') + local image=$(echo "$container" | awk '{print $2}') + local current_digest=$(docker inspect "$name" --format='{{.Image}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12) + + # Pull latest image digest + docker pull "$image" >/dev/null 2>&1 + local latest_digest=$(docker inspect "$image" --format='{{.Id}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12) + + if [ "$current_digest" != "$latest_digest" ]; then + containers_with_updates+=("$name") + container_info+=("${index}) ${name} (${image})") + ((index++)) + fi + done < <(docker ps --format '{{.Names}} {{.Image}}') + + if [ ${#containers_with_updates[@]} -gt 0 ]; then + echo "" + echo "${TAB3}Container updates available:" + for info in "${container_info[@]}"; do + echo "${TAB3} $info" + done + echo "" + read -r -p "${TAB3}Select containers to update (e.g., 1,3,5 or 'all' or 'none'): " selection + + if [[ ${selection,,} == "all" ]]; then + for container in "${containers_with_updates[@]}"; do + msg_info "Updating container: $container" + docker stop "$container" + docker rm "$container" + # Note: This requires the original docker run command - best to recreate via compose + msg_ok "Stopped and removed $container (please recreate with updated image)" + done + elif [[ ${selection,,} != "none" ]]; then + IFS=',' read -ra SELECTED <<<"$selection" + for num in "${SELECTED[@]}"; do + num=$(echo "$num" | xargs) # trim whitespace + if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le "${#containers_with_updates[@]}" ]; then + container="${containers_with_updates[$((num - 1))]}" + msg_info "Updating container: $container" + docker stop "$container" + docker rm "$container" + msg_ok "Stopped and removed $container (please recreate with updated image)" + fi + done + fi + else + msg_ok "All containers are up-to-date" + fi + fi + + msg_ok "Docker setup completed" +} From c9abf01e7fadc5262c0f31533705d0dc0237ddec Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:09:53 +0100 Subject: [PATCH 05/19] Refactor installation script and update copyright Updated copyright information and adjusted package installation commands. Enhanced IPv6 disabling logic and improved container customization process. --- misc/install.func | 51 ++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/misc/install.func b/misc/install.func index d156d7afb4a..128df72643d 100644 --- a/misc/install.func +++ b/misc/install.func @@ -1,7 +1,8 @@ -# Copyright (c) 2021-2025 community-scripts ORG +# Copyright (c) 2021-2025 tteck # Author: tteck (tteckster) -# Co-Author: MickLesk | michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Co-Author: MickLesk +# License: MIT +# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE # ============================================================================== # INSTALL.FUNC - CONTAINER INSTALLATION & SETUP @@ -29,8 +30,8 @@ if ! command -v curl >/dev/null 2>&1; then printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 - apt-get update >/dev/null 2>&1 - apt-get install -y curl >/dev/null 2>&1 + apt update >/dev/null 2>&1 + apt install -y curl >/dev/null 2>&1 fi source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) @@ -51,9 +52,17 @@ catch_errors verb_ip6() { set_std_mode # Set STD mode based on VERBOSE - if [ "$DISABLEIPV6" == "yes" ]; then - echo "net.ipv6.conf.all.disable_ipv6 = 1" >>/etc/sysctl.conf - $STD sysctl -p + 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 </etc/apt/apt.conf.d/00aptproxy - cat <<'EOF' >/usr/local/bin/apt-proxy-detect.sh + echo 'Acquire::http::Proxy-Auto-Detect "/usr/local/bin/apt-proxy-detect.sh";' >/etc/apt/apt.conf.d/00aptproxy + cat </usr/local/bin/apt-proxy-detect.sh #!/bin/bash if nc -w1 -z "${CACHER_IP}" 3142; then echo -n "http://${CACHER_IP}:3142" @@ -188,6 +197,7 @@ EOF $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED msg_ok "Updated Container OS" + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) } @@ -210,8 +220,10 @@ 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 + # Get OS information (Debian / Ubuntu) if [ -f "/etc/os-release" ]; then OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"') @@ -222,12 +234,14 @@ motd_ssh() { PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" echo "echo -e \"\"" >"$PROFILE_FILE" - echo -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} Repository: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE" echo "echo \"\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${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/* if [[ "${SSH_ROOT}" == "yes" ]]; then @@ -254,10 +268,10 @@ customize() { msg_info "Customizing Container" GETTY_OVERRIDE="/etc/systemd/system/container-getty@1.service.d/override.conf" mkdir -p $(dirname $GETTY_OVERRIDE) - cat <<'EOF' >$GETTY_OVERRIDE -[Service] -ExecStart= -ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 $TERM + 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//') @@ -265,6 +279,7 @@ EOF 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 From e949db0492b98c50d0e47323a7e6ef1237ddca23 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:10:15 +0100 Subject: [PATCH 06/19] Update install.func --- misc/install.func | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/install.func b/misc/install.func index 128df72643d..c90704522a2 100644 --- a/misc/install.func +++ b/misc/install.func @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025 tteck +# Copyright (c) 2021-2025 community-scripts ORG # Author: tteck (tteckster) # Co-Author: MickLesk # License: MIT From 840347cab3c26e9d848ad2a01019e9f5ab44429a Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:10:32 +0100 Subject: [PATCH 07/19] Update license comment format in install.func --- misc/install.func | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/misc/install.func b/misc/install.func index c90704522a2..ea4abd7d06f 100644 --- a/misc/install.func +++ b/misc/install.func @@ -1,8 +1,7 @@ # Copyright (c) 2021-2025 community-scripts ORG # Author: tteck (tteckster) # Co-Author: MickLesk -# License: MIT -# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE # ============================================================================== # INSTALL.FUNC - CONTAINER INSTALLATION & SETUP From fd8d4b742931d3fd5cc22c1343454c3c4f7505cd Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:15:18 +0100 Subject: [PATCH 08/19] Refactor IPv6 handling and enhance MOTD and SSH Refactor IPv6 handling and update OS function. Enhance MOTD with additional details and configure SSH settings. --- misc/alpine-install.func | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/misc/alpine-install.func b/misc/alpine-install.func index 9a4dba3f1dc..4a8282e9e08 100644 --- a/misc/alpine-install.func +++ b/misc/alpine-install.func @@ -15,10 +15,19 @@ catch_errors verb_ip6() { set_std_mode # Set STD mode based on VERBOSE - if [ "$DISABLEIPV6" == "yes" ]; then + if [ "$IPV6_METHOD" == "disable" ]; then + msg_info "Disabling IPv6 (this may affect some services)" $STD sysctl -w net.ipv6.conf.all.disable_ipv6=1 - echo "net.ipv6.conf.all.disable_ipv6 = 1" >>/etc/sysctl.conf + $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 <"$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 -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} IP Address: ${GN}${IP}${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} Repository: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE" echo "echo \"\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${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 } @@ -178,4 +190,5 @@ EOF echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update chmod +x /usr/bin/update + } From 13acde2a30b6fc0f196d7580b653e74c213e7f6b Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:19:53 +0100 Subject: [PATCH 09/19] big core refactor --- misc/build.func | 4125 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 3227 insertions(+), 898 deletions(-) diff --git a/misc/build.func b/misc/build.func index d895d3c8d81..9a76779d670 100644 --- a/misc/build.func +++ b/misc/build.func @@ -1,9 +1,42 @@ -# Copyright (c) 2021-2025 tteck -# Author: tteck (tteckster) -# Co-Author: MickLesk -# Co-Author: michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE - +#!/usr/bin/env bash +# Copyright (c) 2021-2025 community-scripts ORG +# Author: tteck (tteckster) | MickLesk | michelroegl-brunner +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE + +# ============================================================================== +# BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION +# ============================================================================== +# +# This file provides the main build functions for creating and configuring +# LXC containers in Proxmox VE. It handles: +# +# - Variable initialization and defaults +# - Container creation and resource allocation +# - Storage selection and management +# - Advanced configuration and customization +# - User interaction menus and prompts +# +# Usage: +# - Sourced automatically by CT creation scripts +# - Requires core.func and error_handler.func to be loaded first +# +# ============================================================================== + +# ============================================================================== +# SECTION 1: INITIALIZATION & CORE VARIABLES +# ============================================================================== + +# ------------------------------------------------------------------------------ +# variables() +# +# - Initializes core variables for container creation +# - Normalizes application name (NSAPP = lowercase, no spaces) +# - Builds installer filename (var_install) +# - Defines regex patterns for validation +# - Fetches Proxmox hostname and version +# - Generates unique session ID for tracking and logging +# - Captures app-declared resource defaults (CPU, RAM, Disk) +# ------------------------------------------------------------------------------ variables() { NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces. var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP. @@ -12,95 +45,72 @@ variables() { DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call. METHOD="default" # sets the METHOD variable to "default", used for the API call. RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable. - CT_TYPE=${var_unprivileged:-$CT_TYPE} + SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files + BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log + CTTYPE="${CTTYPE:-${CT_TYPE:-1}}" + + # Parse dev_mode early + parse_dev_mode + + # Setup persistent log directory if logs mode active + if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then + mkdir -p /var/log/community-scripts + BUILD_LOG="/var/log/community-scripts/create-lxc-${SESSION_ID}-$(date +%Y%m%d_%H%M%S).log" + fi + + # Get Proxmox VE version and kernel version + if command -v pveversion >/dev/null 2>&1; then + PVEVERSION="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" + else + PVEVERSION="N/A" + fi + KERNEL_VERSION=$(uname -r) + + # Capture app-declared defaults (for precedence logic) + # These values are set by the app script BEFORE default.vars is loaded + # If app declares higher values than default.vars, app values take precedence + if [[ -n "${var_cpu:-}" && "${var_cpu}" =~ ^[0-9]+$ ]]; then + export APP_DEFAULT_CPU="${var_cpu}" + fi + if [[ -n "${var_ram:-}" && "${var_ram}" =~ ^[0-9]+$ ]]; then + export APP_DEFAULT_RAM="${var_ram}" + fi + if [[ -n "${var_disk:-}" && "${var_disk}" =~ ^[0-9]+$ ]]; then + export APP_DEFAULT_DISK="${var_disk}" + fi } + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) if command -v curl >/dev/null 2>&1; then source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) load_functions + catch_errors + #echo "(build.func) Loaded core.func via curl" elif command -v wget >/dev/null 2>&1; then source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) + source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) load_functions + catch_errors + #echo "(build.func) Loaded core.func via wget" fi -# This function enables error handling in the script by setting options and defining a trap for the ERR signal. -catch_errors() { - set -Eeo pipefail - trap 'error_handler $LINENO "$BASH_COMMAND"' ERR -} - -# This function is called when an error occurs. It receives the exit code, line number, and command that caused the error, and displays an error message. -error_handler() { - source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) - printf "\e[?25h" - local exit_code="$?" - local line_number="$1" - local command="$2" - local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" - post_update_to_api "failed" "${command}" - echo -e "\n$error_message\n" -} -# Check if the current shell is using bash -shell_check() { - if [[ "$(ps -p $$ -o comm=)" != "bash" ]]; then - clear - msg_error "Your default shell is not bash. Please report this to our github issues or discord." - echo -e "\nExiting..." - sleep 2 - exit - fi -} - -# Run as root only -root_check() { - if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then - clear - msg_error "Please run this script as root." - echo -e "\nExiting..." - sleep 2 - exit - fi -} - -# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported. -# Supported: Proxmox VE 8.0.x – 8.9.x, 9.0 and 9.1 -pve_check() { - local PVE_VER - PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" - - # Check for Proxmox VE 8.x: allow 8.0–8.9 - if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then - local MINOR="${BASH_REMATCH[1]}" - if ((MINOR < 0 || MINOR > 9)); then - msg_error "This version of Proxmox VE is not supported." - msg_error "Supported: Proxmox VE version 8.0 – 8.9" - exit 1 - fi - return 0 - fi - - # Check for Proxmox VE 9.x: allow 9.0 and 9.1 - if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then - local MINOR="${BASH_REMATCH[1]}" - if ((MINOR < 0 || MINOR > 1)); then - msg_error "This version of Proxmox VE is not supported." - msg_error "Supported: Proxmox VE version 9.0 – 9.1" - exit 1 - fi - return 0 - fi +# ============================================================================== +# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION +# ============================================================================== - # All other unsupported versions - msg_error "This version of Proxmox VE is not supported." - msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0 – 9.1" - exit 1 -} +# ------------------------------------------------------------------------------ +# maxkeys_check() +# +# - Reads kernel keyring limits (maxkeys, maxbytes) +# - Checks current usage for LXC user (UID 100000) +# - Warns if usage is close to limits and suggests sysctl tuning +# - Exits if thresholds are exceeded +# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html +# ------------------------------------------------------------------------------ -# When a node is running tens of containers, it's possible to exceed the kernel's cryptographic key storage allocations. -# These are tuneable, so verify if the currently deployment is approaching the limits, advise the user on how to tune the limits, and exit the script. -# https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html maxkeys_check() { # Read kernel parameters per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0) @@ -141,21 +151,21 @@ maxkeys_check() { exit 1 fi - echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}" + # Silent success - only show errors if they exist } -# This function checks the system architecture and exits if it's not "amd64". -arch_check() { - if [ "$(dpkg --print-architecture)" != "amd64" ]; then - echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n" - echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n" - echo -e "Exiting..." - sleep 2 - exit - fi -} - -# Function to get the current IP address based on the distribution +# ============================================================================== +# SECTION 3: CONTAINER SETUP UTILITIES +# ============================================================================== + +# ------------------------------------------------------------------------------ +# get_current_ip() +# +# - Returns current container IP depending on OS type +# - Debian/Ubuntu: uses `hostname -I` +# - Alpine: parses eth0 via `ip -4 addr` +# - Returns "Unknown" if OS type cannot be determined +# ------------------------------------------------------------------------------ get_current_ip() { if [ -f /etc/os-release ]; then # Check for Debian/Ubuntu (uses hostname -I) @@ -171,7 +181,12 @@ get_current_ip() { echo "$CURRENT_IP" } -# Function to update the IP address in the MOTD file +# ------------------------------------------------------------------------------ +# update_motd_ip() +# +# - Updates /etc/motd with current container IP +# - Removes old IP entries to avoid duplicates +# ------------------------------------------------------------------------------ update_motd_ip() { MOTD_FILE="/etc/motd" @@ -185,25 +200,203 @@ update_motd_ip() { fi } -# This function checks if the script is running through SSH and prompts the user to confirm if they want to proceed or exit. -ssh_check() { - if [ -n "${SSH_CLIENT:+x}" ]; then - if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's advisable to utilize the Proxmox shell rather than SSH, as there may be potential complications with variable retrieval. Proceed using SSH?" 10 72; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Proceed using SSH" "You've chosen to proceed using SSH. If any issues arise, please run the script in the Proxmox shell before creating a repository issue." 10 72 - else - clear - echo "Exiting due to SSH usage. Please consider using the Proxmox shell." - exit +# ------------------------------------------------------------------------------ +# install_ssh_keys_into_ct() +# +# - Installs SSH keys into container root account if SSH is enabled +# - Uses pct push or direct input to authorized_keys +# - Falls back to warning if no keys provided +# ------------------------------------------------------------------------------ +install_ssh_keys_into_ct() { + [[ "$SSH" != "yes" ]] && return 0 + + if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then + msg_info "Installing selected SSH keys into CT ${CTID}" + pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || { + msg_error "prepare /root/.ssh failed" + return 1 + } + pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 || + pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || { + msg_error "write authorized_keys failed" + return 1 + } + pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true + msg_ok "Installed SSH keys into CT ${CTID}" + return 0 + fi + + # Fallback + msg_warn "No SSH keys to install (skipping)." + return 0 +} + +# ------------------------------------------------------------------------------ +# find_host_ssh_keys() +# +# - Scans system for available SSH keys +# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys) +# - Returns list of files containing valid SSH public keys +# - Sets FOUND_HOST_KEY_COUNT to number of keys found +# ------------------------------------------------------------------------------ +find_host_ssh_keys() { + local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))' + local -a files=() cand=() + local g="${var_ssh_import_glob:-}" + local total=0 f base c + + shopt -s nullglob + if [[ -n "$g" ]]; then + for pat in $g; do cand+=($pat); done + else + cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2) + cand+=(/root/.ssh/*.pub) + cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*) + fi + shopt -u nullglob + + for f in "${cand[@]}"; do + [[ -f "$f" && -r "$f" ]] || continue + base="$(basename -- "$f")" + case "$base" in + known_hosts | known_hosts.* | config) continue ;; + id_*) [[ "$f" != *.pub ]] && continue ;; + esac + + # CRLF safe check for host keys + c=$(tr -d '\r' <"$f" | awk ' + /^[[:space:]]*#/ {next} + /^[[:space:]]*$/ {next} + {print} + ' | grep -E -c '"$re"' || true) + + if ((c > 0)); then + files+=("$f") + total=$((total + c)) fi + done + + # Fallback to /root/.ssh/authorized_keys + if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then + if grep -E -q "$re" /root/.ssh/authorized_keys; then + files+=(/root/.ssh/authorized_keys) + total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0))) + fi + fi + + FOUND_HOST_KEY_COUNT="$total" + ( + IFS=: + echo "${files[*]}" + ) +} + +# ============================================================================== +# SECTION 4: STORAGE & RESOURCE MANAGEMENT +# ============================================================================== + +# ------------------------------------------------------------------------------ +# _write_storage_to_vars() +# +# - Writes storage selection to vars file +# - Removes old entries (commented and uncommented) to avoid duplicates +# - Arguments: vars_file, key (var_container_storage/var_template_storage), value +# ------------------------------------------------------------------------------ +_write_storage_to_vars() { + # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value + local vf="$1" key="$2" val="$3" + # remove uncommented and commented versions to avoid duplicates + sed -i "/^[#[:space:]]*${key}=/d" "$vf" + echo "${key}=${val}" >>"$vf" +} + +choose_and_set_storage_for_file() { + # $1 = vars_file, $2 = class ('container'|'template') + local vf="$1" class="$2" key="" current="" + case "$class" in + container) key="var_container_storage" ;; + template) key="var_template_storage" ;; + *) + msg_error "Unknown storage class: $class" + return 1 + ;; + esac + + current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf") + + # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4). + local content="rootdir" + [[ "$class" == "template" ]] && content="vztmpl" + local count + count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l) + + if [[ "$count" -eq 1 ]]; then + STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}') + STORAGE_INFO="" + else + # If the current value is preselectable, we could show it, but per your requirement we always offer selection + select_storage "$class" || return 1 fi + + _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT" + + # Keep environment in sync for later steps (e.g. app-default save) + if [[ "$class" == "container" ]]; then + export var_container_storage="$STORAGE_RESULT" + export CONTAINER_STORAGE="$STORAGE_RESULT" + else + export var_template_storage="$STORAGE_RESULT" + export TEMPLATE_STORAGE="$STORAGE_RESULT" + fi + + # Silent operation - no output message } +# ============================================================================== +# SECTION 5: CONFIGURATION & DEFAULTS MANAGEMENT +# ============================================================================== + +# ------------------------------------------------------------------------------ +# base_settings() +# +# - Defines all base/default variables for container creation +# - Reads from environment variables (var_*) +# - Provides fallback defaults for OS type/version +# - App-specific values take precedence when they are HIGHER (for CPU, RAM, DISK) +# - Sets up container type, resources, network, SSH, features, and tags +# ------------------------------------------------------------------------------ base_settings() { # Default Settings CT_TYPE=${var_unprivileged:-"1"} - DISK_SIZE=${var_disk:-"4"} - CORE_COUNT=${var_cpu:-"1"} - RAM_SIZE=${var_ram:-"1024"} + + # Resource allocation: App defaults take precedence if HIGHER + # Compare app-declared values (saved in APP_DEFAULT_*) with current var_* values + local final_disk="${var_disk:-4}" + local final_cpu="${var_cpu:-1}" + local final_ram="${var_ram:-1024}" + + # If app declared higher values, use those instead + if [[ -n "${APP_DEFAULT_DISK:-}" && "${APP_DEFAULT_DISK}" =~ ^[0-9]+$ ]]; then + if [[ "${APP_DEFAULT_DISK}" -gt "${final_disk}" ]]; then + final_disk="${APP_DEFAULT_DISK}" + fi + fi + + if [[ -n "${APP_DEFAULT_CPU:-}" && "${APP_DEFAULT_CPU}" =~ ^[0-9]+$ ]]; then + if [[ "${APP_DEFAULT_CPU}" -gt "${final_cpu}" ]]; then + final_cpu="${APP_DEFAULT_CPU}" + fi + fi + + if [[ -n "${APP_DEFAULT_RAM:-}" && "${APP_DEFAULT_RAM}" =~ ^[0-9]+$ ]]; then + if [[ "${APP_DEFAULT_RAM}" -gt "${final_ram}" ]]; then + final_ram="${APP_DEFAULT_RAM}" + fi + fi + + DISK_SIZE="${final_disk}" + CORE_COUNT="${final_cpu}" + RAM_SIZE="${final_ram}" VERBOSE=${var_verbose:-"${1:-no}"} PW=${var_pw:-""} CT_ID=${var_ctid:-$NEXTID} @@ -215,6 +408,19 @@ base_settings() { GATE=${var_gateway:-""} APT_CACHER=${var_apt_cacher:-""} APT_CACHER_IP=${var_apt_cacher_ip:-""} + + # 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" + 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" + fi + fi + MTU=${var_mtu:-""} SD=${var_storage:-""} NS=${var_ns:-""} @@ -223,7 +429,7 @@ base_settings() { SSH=${var_ssh:-"no"} SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""} UDHCPC_FIX=${var_udhcpc_fix:-""} - TAGS="community-script;${var_tags:-}" + TAGS="community-script,${var_tags:-}" ENABLE_FUSE=${var_fuse:-"${1:-no}"} ENABLE_TUN=${var_tun:-"${1:-no}"} @@ -236,634 +442,1204 @@ base_settings() { fi } -write_config() { - mkdir -p /opt/community-scripts - # This function writes the configuration to a file. - if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Write configfile" --yesno "Do you want to write the selections to a config file?" 10 60; then - FILEPATH="/opt/community-scripts/${NSAPP}.conf" - [[ "$GATE" =~ ",gw=" ]] && local GATE="${GATE##,gw=}" - - # Strip prefixes from parameters for config file storage - local SD_VALUE="${SD}" - local NS_VALUE="${NS}" - local MAC_VALUE="${MAC}" - local VLAN_VALUE="${VLAN}" - [[ "$SD" =~ ^-searchdomain= ]] && SD_VALUE="${SD#-searchdomain=}" - [[ "$NS" =~ ^-nameserver= ]] && NS_VALUE="${NS#-nameserver=}" - [[ "$MAC" =~ ^,hwaddr= ]] && MAC_VALUE="${MAC#,hwaddr=}" - [[ "$VLAN" =~ ^,tag= ]] && VLAN_VALUE="${VLAN#,tag=}" - - if [[ ! -f $FILEPATH ]]; then - cat <"$FILEPATH" -# ${NSAPP} Configuration File -# Generated on $(date) - -CT_TYPE="${CT_TYPE}" -DISK_SIZE="${DISK_SIZE}" -CORE_COUNT="${CORE_COUNT}" -RAM_SIZE="${RAM_SIZE}" -VERBOSE="${VERBOSE}" -PW="${PW##-password }" -#CT_ID=$NEXTID -HN="${HN}" -BRG="${BRG}" -NET="${NET}" -IPV6_METHOD="${IPV6_METHOD:-none}" -# Set this only if using "IPV6_METHOD=static" -#IPV6STATIC="fd00::1234/64" - -GATE="${GATE:-none}" -APT_CACHER_IP="${APT_CACHER_IP:-none}" -MTU="${MTU:-1500}" -SD="${SD_VALUE:-none}" -NS="${NS_VALUE:-none}" -MAC="${MAC_VALUE:-none}" -VLAN="${VLAN_VALUE:-none}" -SSH="${SSH}" -SSH_AUTHORIZED_KEY="${SSH_AUTHORIZED_KEY}" -TAGS="${TAGS:-none}" -ENABLE_FUSE="$ENABLE_FUSE" -ENABLE_TUN="$ENABLE_TUN" -EOF +# ------------------------------------------------------------------------------ +# load_vars_file() +# +# - Safe parser for KEY=VALUE lines from vars files +# - Used by default_var_settings and app defaults loading +# - Only loads whitelisted var_* keys +# ------------------------------------------------------------------------------ +load_vars_file() { + local file="$1" + [ -f "$file" ] || return 0 + msg_info "Loading defaults from ${file}" + + # Allowed var_* keys + local VAR_WHITELIST=( + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_keyctl + var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu + var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged + var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage + ) - echo -e "${INFO}${BOLD}${GN}Writing configuration to ${FILEPATH}${CL}" - else - echo -e "${INFO}${BOLD}${RD}Configuration file already exists at ${FILEPATH}${CL}" - if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Overwrite configfile" --yesno "Do you want to overwrite the existing config file?" 10 60; then - rm -f "$FILEPATH" - cat <"$FILEPATH" -# ${NSAPP} Configuration File -# Generated on $(date) - -CT_TYPE="${CT_TYPE}" -DISK_SIZE="${DISK_SIZE}" -CORE_COUNT="${CORE_COUNT}" -RAM_SIZE="${RAM_SIZE}" -VERBOSE="${VERBOSE}" -PW="${PW##-password }" -#CT_ID=$NEXTID -HN="${HN}" -BRG="${BRG}" -NET="${NET}" -IPV6_METHOD="${IPV6_METHOD:-none}" - -# Set this only if using "IPV6_METHOD=static" -#IPV6STATIC="fd00::1234/64" - -GATE="${GATE:-none}" -APT_CACHER_IP="${APT_CACHER_IP:-none}" -MTU="${MTU:-1500}" -SD="${SD_VALUE:-none}" -NS="${NS_VALUE:-none}" -MAC="${MAC_VALUE:-none}" -VLAN="${VLAN_VALUE:-none}" -SSH="${SSH}" -SSH_AUTHORIZED_KEY="${SSH_AUTHORIZED_KEY}" -TAGS="${TAGS:-none}" -ENABLE_FUSE="$ENABLE_FUSE" -ENABLE_TUN="$ENABLE_TUN" -EOF - echo -e "${INFO}${BOLD}${GN}Writing configuration to ${FILEPATH}${CL}" - else - echo -e "${INFO}${BOLD}${RD}Configuration file not overwritten${CL}" + # Whitelist check helper + _is_whitelisted() { + local k="$1" w + for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done + return 1 + } + + local line key val + while IFS= read -r line || [ -n "$line" ]; do + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z "$line" || "$line" == \#* ]] && continue + if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then + local var_key="${BASH_REMATCH[1]}" + local var_val="${BASH_REMATCH[2]}" + + [[ "$var_key" != var_* ]] && continue + _is_whitelisted "$var_key" || continue + + # Strip quotes + if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then + var_val="${BASH_REMATCH[1]}" + elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then + var_val="${BASH_REMATCH[1]}" fi + + # Set only if not already exported + [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}" fi - fi + done <"$file" + msg_ok "Loaded ${file}" } -# This function displays the default values for various settings. -echo_default() { - # Convert CT_TYPE to description - CT_TYPE_DESC="Unprivileged" - if [ "$CT_TYPE" -eq 0 ]; then - CT_TYPE_DESC="Privileged" - fi +# ------------------------------------------------------------------------------ +# default_var_settings +# +# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing) +# - Loads var_* values from default.vars (safe parser, no source/eval) +# - Precedence: ENV var_* > default.vars > built-in defaults +# - Maps var_verbose → VERBOSE +# - Calls base_settings "$VERBOSE" and echo_default +# ------------------------------------------------------------------------------ +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_keyctl + var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu + var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged + var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage + ) - # Output the selected values with icons - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [ "$VERBOSE" == "yes" ]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" + # Snapshot: environment variables (highest precedence) + declare -A _HARD_ENV=() + local _k + for _k in "${VAR_WHITELIST[@]}"; do + if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi + done + + # Find default.vars location + local _find_default_vars + _find_default_vars() { + local f + for f in \ + /usr/local/community-scripts/default.vars \ + "$HOME/.config/community-scripts/default.vars" \ + "./default.vars"; do + [ -f "$f" ] && { + echo "$f" + return 0 + } + done + return 1 + } + # Allow override of storages via env (for non-interactive use cases) + [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage" + [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage" + + # Create once, with storages already selected, no var_ctid/var_hostname lines + local _ensure_default_vars + _ensure_default_vars() { + _find_default_vars >/dev/null 2>&1 && return 0 + + local canonical="/usr/local/community-scripts/default.vars" + # Silent creation - no msg_info output + mkdir -p /usr/local/community-scripts + + # Pick storages before writing the file (always ask unless only one) + # Create a minimal temp file to write into + : >"$canonical" + + # Base content (no var_ctid / var_hostname here) + cat >"$canonical" <<'EOF' +# Community-Scripts defaults (var_* only). Lines starting with # are comments. +# Precedence: ENV var_* > default.vars > built-ins. +# Keep keys alphabetically sorted. + +# Container type +var_unprivileged=1 + +# Resources +var_cpu=1 +var_disk=4 +var_ram=1024 + +# Network +var_brg=vmbr0 +var_net=dhcp +var_ipv6_method=none +# var_gateway= +# var_vlan= +# var_mtu= +# var_mac= +# var_ns= + +# SSH +var_ssh=no +# var_ssh_authorized_key= + +# APT cacher (optional - with example) +# var_apt_cacher=yes +# var_apt_cacher_ip=192.168.1.10 + +# Features/Tags/verbosity +var_fuse=no +var_tun=no + +# Advanced Settings (Proxmox-official features) +var_nesting=1 # Allow nesting (required for Docker/LXC in CT) +var_keyctl=0 # Allow keyctl() - needed for Docker (systemd-networkd workaround) +var_mknod=0 # Allow device node creation (requires kernel 5.3+, experimental) +var_mount_fs= # Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults) +var_protection=no # Prevent accidental deletion of container +var_timezone= # Container timezone (e.g. Europe/Berlin, leave empty for host timezone) +var_tags=community-script +var_verbose=no + +# Security (root PW) – empty => autologin +# var_pw= +EOF + + # Now choose storages (always prompt unless just one exists) + choose_and_set_storage_for_file "$canonical" template + choose_and_set_storage_for_file "$canonical" container + + chmod 0644 "$canonical" + # Silent creation - no output message + } + + # Whitelist check + local _is_whitelisted_key + _is_whitelisted_key() { + local k="$1" + local w + for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done + return 1 + } + + # 1) Ensure file exists + _ensure_default_vars + + # 2) Load file + local dv + dv="$(_find_default_vars)" || { + msg_error "default.vars not found after ensure step" + return 1 + } + load_vars_file "$dv" + + # 3) Map var_verbose → VERBOSE + if [[ -n "${var_verbose:-}" ]]; then + case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac + else + VERBOSE="no" fi - echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" - echo -e " " + + # 4) Apply base settings and show summary + METHOD="mydefaults-global" + base_settings "$VERBOSE" + header_info + echo -e "${DEFAULT}${BOLD}${BL}Using User Defaults (default.vars) on node $PVEHOST_NAME${CL}" + echo_default } -# This function is called when the user decides to exit the script. It clears the screen and displays an exit message. -exit_script() { - clear - echo -e "\n${CROSS}${RD}User exited script${CL}\n" - exit +# ------------------------------------------------------------------------------ +# get_app_defaults_path() +# +# - Returns full path for app-specific defaults file +# - Example: /usr/local/community-scripts/defaults/.vars +# ------------------------------------------------------------------------------ + +get_app_defaults_path() { + local n="${NSAPP:-${APP,,}}" + echo "/usr/local/community-scripts/defaults/${n}.vars" } -# This function allows the user to configure advanced settings for the script. -advanced_settings() { - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58 - # Setting Default Tag for Advanced Settings - TAGS="community-script;${var_tags:-}" - CT_DEFAULT_TYPE="${CT_TYPE}" - CT_TYPE="" - while [ -z "$CT_TYPE" ]; do - if [ "$CT_DEFAULT_TYPE" == "1" ]; then - if CT_TYPE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \ - "1" "Unprivileged" ON \ - "0" "Privileged" OFF \ - 3>&1 1>&2 2>&3); then - if [ -n "$CT_TYPE" ]; then - CT_TYPE_DESC="Unprivileged" - if [ "$CT_TYPE" -eq 0 ]; then - CT_TYPE_DESC="Privileged" - fi - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - fi - else - exit_script - fi - fi - if [ "$CT_DEFAULT_TYPE" == "0" ]; then - if CT_TYPE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \ - "1" "Unprivileged" OFF \ - "0" "Privileged" ON \ - 3>&1 1>&2 2>&3); then - if [ -n "$CT_TYPE" ]; then - CT_TYPE_DESC="Unprivileged" - if [ "$CT_TYPE" -eq 0 ]; then - CT_TYPE_DESC="Privileged" - fi - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - fi - else - exit_script - fi - fi - done +# ------------------------------------------------------------------------------ +# maybe_offer_save_app_defaults +# +# - Called after advanced_settings returned with fully chosen values. +# - If no .vars exists, offers to persist current advanced settings +# into /usr/local/community-scripts/defaults/.vars +# - Only writes whitelisted var_* keys. +# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc. +# ------------------------------------------------------------------------------ +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_gateway var_hostname var_ipv6_method var_mac var_mtu + var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged + var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage + ) +fi - while true; do - if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then - # Empty = Autologin - if [[ -z "$PW1" ]]; then - PW="" - PW1="Automatic Login" - echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}" - break - fi +# Global whitelist check function (used by _load_vars_file_to_map and others) +_is_whitelisted_key() { + local k="$1" + local w + for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done + return 1 +} - # Invalid: contains spaces - if [[ "$PW1" == *" "* ]]; then - whiptail --msgbox "Password cannot contain spaces." 8 58 - continue - fi +_sanitize_value() { + # Disallow Command-Substitution / Shell-Meta + case "$1" in + *'$('* | *'`'* | *';'* | *'&'* | *'<('*) + echo "" + return 0 + ;; + esac + echo "$1" +} - # Invalid: too short - if ((${#PW1} < 5)); then - whiptail --msgbox "Password must be at least 5 characters." 8 58 - continue +# Map-Parser: read var_* from file into _VARS_IN associative array +# Note: Main _load_vars_file() with full validation is defined in default_var_settings section +# This simplified version is used specifically for diff operations via _VARS_IN array +declare -A _VARS_IN +_load_vars_file_to_map() { + local file="$1" + [ -f "$file" ] || return 0 + _VARS_IN=() # Clear array + local line key val + while IFS= read -r line || [ -n "$line" ]; do + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [ -z "$line" ] && continue + case "$line" in + \#*) continue ;; + esac + key=$(printf "%s" "$line" | cut -d= -f1) + val=$(printf "%s" "$line" | cut -d= -f2-) + case "$key" in + var_*) + if _is_whitelisted_key "$key"; then + _VARS_IN["$key"]="$val" fi + ;; + esac + done <"$file" +} - # Confirm password - if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then - if [[ "$PW1" == "$PW2" ]]; then - PW="-password $PW1" - echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}" - break - else - whiptail --msgbox "Passwords do not match. Please try again." 8 58 - fi - else - exit_script +# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new) +_build_vars_diff() { + local oldf="$1" newf="$2" + local k + local -A OLD=() NEW=() + _load_vars_file_to_map "$oldf" + for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done + _load_vars_file_to_map "$newf" + for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done + + local out + out+="# Diff for ${APP} (${NSAPP})\n" + out+="# Old: ${oldf}\n# New: ${newf}\n\n" + + local found_change=0 + + # Changed & Removed + for k in "${!OLD[@]}"; do + if [[ -v NEW["$k"] ]]; then + if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then + out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n" + found_change=1 fi else - exit_script + out+="- ${k}\n - old: ${OLD[$k]}\n" + found_change=1 fi done - if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then - if [ -z "$CT_ID" ]; then - CT_ID="$NEXTID" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - else - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" + # Added + for k in "${!NEW[@]}"; do + if [[ ! -v OLD["$k"] ]]; then + out+="+ ${k}\n + new: ${NEW[$k]}\n" + found_change=1 fi - else - exit_script + done + + if [[ $found_change -eq 0 ]]; then + out+="(No differences)\n" fi - while true; do - if CT_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo "${CT_NAME,,}" | tr -d ' ') - fi - # Hostname validate (RFC 1123) - if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - break - else - whiptail --backtitle "Proxmox VE Helper Scripts" \ - --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 - fi - else - exit_script - fi - done + printf "%b" "$out" +} - while true; do - DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script +# Build a temporary .vars file from current advanced settings +_build_current_app_vars_tmp() { + tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)" - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - fi + # NET/GW + _net="${NET:-}" + _gate="" + case "${GATE:-}" in + ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;; + esac - if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - break - else - whiptail --msgbox "Disk size must be a positive integer!" 8 58 - fi - done + # IPv6 + _ipv6_method="${IPV6_METHOD:-auto}" + _ipv6_static="" + _ipv6_gateway="" + if [ "$_ipv6_method" = "static" ]; then + _ipv6_static="${IPV6_ADDR:-}" + _ipv6_gateway="${IPV6_GATE:-}" + fi - while true; do - CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script + # MTU/VLAN/MAC + _mtu="" + _vlan="" + _mac="" + case "${MTU:-}" in + ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;; + esac + case "${VLAN:-}" in + ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;; + esac + case "${MAC:-}" in + ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;; + esac - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - fi + # DNS / Searchdomain + _ns="" + _searchdomain="" + case "${NS:-}" in + -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;; + esac + case "${SD:-}" in + -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;; + esac - if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - break - else - whiptail --msgbox "CPU core count must be a positive integer!" 8 58 + # SSH / APT / Features + _ssh="${SSH:-no}" + _ssh_auth="${SSH_AUTHORIZED_KEY:-}" + _apt_cacher="${APT_CACHER:-}" + _apt_cacher_ip="${APT_CACHER_IP:-}" + _fuse="${ENABLE_FUSE:-no}" + _tun="${ENABLE_TUN:-no}" + _nesting="${ENABLE_NESTING:-1}" + _keyctl="${ENABLE_KEYCTL:-0}" + _mknod="${ENABLE_MKNOD:-0}" + _mount_fs="${ALLOW_MOUNT_FS:-}" + _protect="${PROTECT_CT:-no}" + _timezone="${CT_TIMEZONE:-}" + _tags="${TAGS:-}" + _verbose="${VERBOSE:-no}" + + # Type / Resources / Identity + _unpriv="${CT_TYPE:-1}" + _cpu="${CORE_COUNT:-1}" + _ram="${RAM_SIZE:-1024}" + _disk="${DISK_SIZE:-4}" + _hostname="${HN:-$NSAPP}" + + # Storage + _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}" + _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}" + + { + echo "# App-specific defaults for ${APP} (${NSAPP})" + echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" + echo + + echo "var_unprivileged=$(_sanitize_value "$_unpriv")" + echo "var_cpu=$(_sanitize_value "$_cpu")" + echo "var_ram=$(_sanitize_value "$_ram")" + echo "var_disk=$(_sanitize_value "$_disk")" + + [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")" + [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")" + [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")" + [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")" + [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")" + [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")" + [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")" + + [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")" + # var_ipv6_static removed - static IPs are unique, can't be default + + [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")" + [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")" + + [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")" + [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")" + + [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" + [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" + [ -n "$_nesting" ] && echo "var_nesting=$(_sanitize_value "$_nesting")" + [ -n "$_keyctl" ] && echo "var_keyctl=$(_sanitize_value "$_keyctl")" + [ -n "$_mknod" ] && echo "var_mknod=$(_sanitize_value "$_mknod")" + [ -n "$_mount_fs" ] && echo "var_mount_fs=$(_sanitize_value "$_mount_fs")" + [ -n "$_protect" ] && echo "var_protection=$(_sanitize_value "$_protect")" + [ -n "$_timezone" ] && echo "var_timezone=$(_sanitize_value "$_timezone")" + [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")" + [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")" + + [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")" + [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")" + + [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")" + [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")" + } >"$tmpf" + + echo "$tmpf" +} + +# ------------------------------------------------------------------------------ +# maybe_offer_save_app_defaults() +# +# - Called after advanced_settings() +# - Offers to save current values as app defaults if not existing +# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel +# ------------------------------------------------------------------------------ +maybe_offer_save_app_defaults() { + local app_vars_path + app_vars_path="$(get_app_defaults_path)" + + # always build from current settings + local new_tmp diff_tmp + new_tmp="$(_build_current_app_vars_tmp)" + diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")" + + # 1) if no file → offer to create + if [[ ! -f "$app_vars_path" ]]; then + if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then + mkdir -p "$(dirname "$app_vars_path")" + install -m 0644 "$new_tmp" "$app_vars_path" + msg_ok "Saved app defaults: ${app_vars_path}" fi - done + rm -f "$new_tmp" "$diff_tmp" + return 0 + fi - while true; do - RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script + # 2) if file exists → build diff + _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp" - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - fi + # if no differences → do nothing + if grep -q "^(No differences)$" "$diff_tmp"; then + rm -f "$new_tmp" "$diff_tmp" + return 0 + fi + + # 3) if file exists → show menu with default selection "Update Defaults" + local app_vars_file + app_vars_file="$(basename "$app_vars_path")" - if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" + while true; do + local sel + sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "APP DEFAULTS – ${APP}" \ + --menu "Differences detected. What do you want to do?" 20 78 10 \ + "Update Defaults" "Write new values to ${app_vars_file}" \ + "Keep Current" "Keep existing defaults (no changes)" \ + "View Diff" "Show a detailed diff" \ + "Cancel" "Abort without changes" \ + --default-item "Update Defaults" \ + 3>&1 1>&2 2>&3)" || { sel="Cancel"; } + + case "$sel" in + "Update Defaults") + install -m 0644 "$new_tmp" "$app_vars_path" + msg_ok "Updated app defaults: ${app_vars_path}" break - else - whiptail --msgbox "RAM size must be a positive integer!" 8 58 - fi + ;; + "Keep Current") + msg_custom "ℹ️" "${BL}" "Keeping current app defaults: ${app_vars_path}" + break + ;; + "View Diff") + whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "Diff – ${APP}" \ + --scrolltext --textbox "$diff_tmp" 25 100 + ;; + "Cancel" | *) + msg_custom "🚫" "${YW}" "Canceled. No changes to app defaults." + break + ;; + esac done - BRIDGES="" - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) - OLD_IFS=$IFS - IFS=$'\n' - - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') + rm -f "$new_tmp" "$diff_tmp" +} - (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | - awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true +ensure_storage_selection_for_vars_file() { + local vf="$1" - if [ -f "${iface_indexes_tmpfile}" ]; then - while read -r pair; do - start=$(echo "${pair}" | cut -d':' -f1) - end=$(echo "${pair}" | cut -d':' -f2) + # Read stored values (if any) + local tpl ct + tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) + ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') - BRIDGES="${iface_name}"$'\n'"${BRIDGES}" - fi + if [[ -n "$tpl" && -n "$ct" ]]; then + TEMPLATE_STORAGE="$tpl" + CONTAINER_STORAGE="$ct" + return 0 + fi - done <"${iface_indexes_tmpfile}" - rm -f "${iface_indexes_tmpfile}" - fi + choose_and_set_storage_for_file "$vf" template + choose_and_set_storage_for_file "$vf" container - done + # Silent operation - no output message +} - IFS=$OLD_IFS +ensure_global_default_vars_file() { + local vars_path="/usr/local/community-scripts/default.vars" + if [[ ! -f "$vars_path" ]]; then + mkdir -p "$(dirname "$vars_path")" + touch "$vars_path" + fi + echo "$vars_path" +} - BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) +# ============================================================================== +# SECTION 6: ADVANCED INTERACTIVE CONFIGURATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# advanced_settings() +# +# - Interactive wizard-style configuration with BACK navigation +# - State-machine approach: each step can go forward or backward +# - Cancel at Step 1 = Exit Script, Cancel at other steps = Go Back +# - Allows user to customize all container settings +# ------------------------------------------------------------------------------ +advanced_settings() { + # Enter alternate screen buffer to prevent flicker between dialogs + tput smcup 2>/dev/null || true + trap 'tput rmcup 2>/dev/null || true' RETURN - if [[ -z "$BRIDGES" ]]; then - BRG="vmbr0" - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge:" 15 40 6 $(echo "$BRIDGES" | awk '{print $0, "Bridge"}') 3>&1 1>&2 2>&3) - if [ -z "$BRG" ]; then - exit_script - else - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + # Initialize defaults + TAGS="community-script;${var_tags:-}" + local STEP=1 + local MAX_STEP=19 + + # Store values for back navigation + local _ct_type="${CT_TYPE:-1}" + local _pw="" + local _pw_display="Automatic Login" + local _ct_id="$NEXTID" + local _hostname="$NSAPP" + local _disk_size="$var_disk" + local _core_count="$var_cpu" + local _ram_size="$var_ram" + local _bridge="vmbr0" + local _net="dhcp" + local _gate="" + local _ipv6_method="auto" + local _ipv6_addr="" + local _ipv6_gate="" + local _apt_cacher_ip="" + local _mtu="" + local _sd="" + local _ns="" + local _mac="" + local _vlan="" + local _tags="$TAGS" + local _enable_fuse="no" + local _verbose="no" + local _enable_keyctl="0" + local _enable_mknod="0" + local _mount_fs="" + local _protect_ct="no" + local _ct_timezone="" + + # Helper to show current progress + show_progress() { + local current=$1 + local total=$MAX_STEP + echo -e "\n${INFO}${BOLD}${DGN}Step $current of $total${CL}" + } + + # Detect available bridges (do this once) + local BRIDGES="" + local BRIDGE_MENU_OPTIONS=() + _detect_bridges() { + IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f 2>/dev/null) + BRIDGES="" + local OLD_IFS=$IFS + IFS=$'\n' + for iface_filepath in ${IFACE_FILEPATH_LIST}; do + local iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') + (grep -Pn '^\s*iface' "${iface_filepath}" 2>/dev/null | cut -d':' -f1 && wc -l "${iface_filepath}" 2>/dev/null | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" 2>/dev/null || true + if [ -f "${iface_indexes_tmpfile}" ]; then + while read -r pair; do + local start=$(echo "${pair}" | cut -d':' -f1) + local end=$(echo "${pair}" | cut -d':' -f2) + if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" 2>/dev/null | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then + local iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') + BRIDGES="${iface_name}"$'\n'"${BRIDGES}" + fi + done <"${iface_indexes_tmpfile}" + rm -f "${iface_indexes_tmpfile}" + fi + done + IFS=$OLD_IFS + BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) + + # Build bridge menu + BRIDGE_MENU_OPTIONS=() + if [[ -n "$BRIDGES" ]]; then + while IFS= read -r bridge; do + if [[ -n "$bridge" ]]; then + local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//') + BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }") + fi + done <<<"$BRIDGES" fi - fi + } + _detect_bridges - # IPv4 methods: dhcp, static, none - while true; do - IPV4_METHOD=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "IPv4 Address Management" \ - --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ - "dhcp" "Automatic (DHCP, recommended)" \ - "static" "Static (manual entry)" \ - 3>&1 1>&2 2>&3) - - exit_status=$? - if [ $exit_status -ne 0 ]; then - exit_script - fi + # Main wizard loop + while [ $STEP -le $MAX_STEP ]; do + case $STEP in - case "$IPV4_METHOD" in - dhcp) - NET="dhcp" - GATE="" - echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" - break + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 1: Container Type + # ═══════════════════════════════════════════════════════════════════════════ + 1) + local default_on="ON" + local default_off="OFF" + [[ "$_ct_type" == "0" ]] && { + default_on="OFF" + default_off="ON" + } + + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONTAINER TYPE" \ + --ok-button "Next" --cancel-button "Exit" \ + --radiolist "\nChoose container type:\n\nUse SPACE to select, ENTER to confirm." 14 58 2 \ + "1" "Unprivileged (recommended)" $default_on \ + "0" "Privileged" $default_off \ + 3>&1 1>&2 2>&3); then + [[ -n "$result" ]] && _ct_type="$result" + ((STEP++)) + else + exit_script + fi ;; - static) - # Static: call and validate CIDR address - while true; do - NET=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ - --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) - if [ -z "$NET" ]; then - whiptail --msgbox "IPv4 address must not be empty." 8 58 - continue - elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" - break - else - whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 - fi - done - # call and validate Gateway - while true; do - GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ - --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --msgbox "Gateway IP address cannot be empty." 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --msgbox "Invalid Gateway IP address format." 8 58 + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 2: Root Password + # ═══════════════════════════════════════════════════════════════════════════ + 2) + if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "ROOT PASSWORD" \ + --ok-button "Next" --cancel-button "Back" \ + --passwordbox "\nSet Root Password (needed for root ssh access)\n\nLeave blank for automatic login (no password)" 12 58 \ + 3>&1 1>&2 2>&3); then + + if [[ -z "$PW1" ]]; then + _pw="" + _pw_display="Automatic Login" + ((STEP++)) + elif [[ "$PW1" == *" "* ]]; then + whiptail --msgbox "Password cannot contain spaces." 8 58 + elif ((${#PW1} < 5)); then + whiptail --msgbox "Password must be at least 5 characters." 8 58 else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break + # Verify password + if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "PASSWORD VERIFICATION" \ + --ok-button "Confirm" --cancel-button "Back" \ + --passwordbox "\nVerify Root Password" 10 58 \ + 3>&1 1>&2 2>&3); then + if [[ "$PW1" == "$PW2" ]]; then + _pw="-password $PW1" + _pw_display="********" + ((STEP++)) + else + whiptail --msgbox "Passwords do not match. Please try again." 8 58 + fi + else + ((STEP--)) + fi fi - done - break - ;; - esac - done - - # IPv6 Address Management selection - while true; do - IPV6_METHOD=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu \ - "Select IPv6 Address Management Type:" 16 70 5 \ - "auto" "SLAAC/AUTO (recommended) - Dynamic IPv6 from network" \ - "dhcp" "DHCPv6 - DHCP-assigned IPv6 address" \ - "static" "Static - Manual IPv6 address configuration" \ - "none" "None - No IPv6 assignment (most containers)" \ - "disable" "Fully Disabled - (breaks some services)" \ - --default-item "auto" 3>&1 1>&2 2>&3) - [ $? -ne 0 ] && exit_script - - case "$IPV6_METHOD" in - auto) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" - IPV6_ADDR="" - IPV6_GATE="" - break + else + ((STEP--)) + fi ;; - dhcp) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" - IPV6_ADDR="dhcp" - IPV6_GATE="" - break + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 3: Container ID + # ═══════════════════════════════════════════════════════════════════════════ + 3) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONTAINER ID" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet Container ID" 10 58 "$_ct_id" \ + 3>&1 1>&2 2>&3); then + _ct_id="${result:-$NEXTID}" + ((STEP++)) + else + ((STEP--)) + fi ;; - static) - # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) - while true; do - IPV6_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \ - "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ - --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script - if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" - break + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 4: Hostname + # ═══════════════════════════════════════════════════════════════════════════ + 4) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "HOSTNAME" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet Hostname (lowercase, alphanumeric, hyphens only)" 10 58 "$_hostname" \ + 3>&1 1>&2 2>&3); then + local hn_test="${result:-$NSAPP}" + hn_test=$(echo "${hn_test,,}" | tr -d ' ') + if [[ "$hn_test" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then + _hostname="$hn_test" + ((STEP++)) else - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox \ - "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 + whiptail --msgbox "Invalid hostname: '$hn_test'\n\nOnly lowercase letters, digits and hyphens are allowed." 10 58 fi - done - # Optional: ask for IPv6 gateway for static config - while true; do - IPV6_GATE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \ - "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) - if [ -z "$IPV6_GATE" ]; then - IPV6_GATE="" - break - elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then - break + else + ((STEP--)) + fi + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 5: Disk Size + # ═══════════════════════════════════════════════════════════════════════════ + 5) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "DISK SIZE" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet Disk Size in GB" 10 58 "$_disk_size" \ + 3>&1 1>&2 2>&3); then + local disk_test="${result:-$var_disk}" + if [[ "$disk_test" =~ ^[1-9][0-9]*$ ]]; then + _disk_size="$disk_test" + ((STEP++)) else - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox \ - "Invalid IPv6 gateway format." 8 58 + whiptail --msgbox "Disk size must be a positive integer!" 8 58 + fi + else + ((STEP--)) + fi + ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 6: CPU Cores + # ═══════════════════════════════════════════════════════════════════════════ + 6) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CPU CORES" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nAllocate CPU Cores" 10 58 "$_core_count" \ + 3>&1 1>&2 2>&3); then + local cpu_test="${result:-$var_cpu}" + if [[ "$cpu_test" =~ ^[1-9][0-9]*$ ]]; then + _core_count="$cpu_test" + ((STEP++)) + else + whiptail --msgbox "CPU core count must be a positive integer!" 8 58 fi - done - break + else + ((STEP--)) + fi ;; - none) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}None${CL}" - IPV6_ADDR="none" - IPV6_GATE="" - break + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 7: RAM Size + # ═══════════════════════════════════════════════════════════════════════════ + 7) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "RAM SIZE" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nAllocate RAM in MiB" 10 58 "$_ram_size" \ + 3>&1 1>&2 2>&3); then + local ram_test="${result:-$var_ram}" + if [[ "$ram_test" =~ ^[1-9][0-9]*$ ]]; then + _ram_size="$ram_test" + ((STEP++)) + else + whiptail --msgbox "RAM size must be a positive integer!" 8 58 + fi + else + ((STEP--)) + fi ;; - disable) - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox \ - "⚠️ WARNING - FULLY DISABLE IPv6:\n\nThis will completely disable IPv6 inside the container via sysctl.\n\nSide Effects:\n • Services requiring IPv6 will fail\n • Localhost IPv6 (::1) will not work\n • Some applications may not start\n\nOnly use if you have a specific reason to completely disable IPv6.\n\nFor most use cases, select 'None' instead." 14 70 - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Fully Disabled (IPv6 disabled via sysctl)${CL}" - IPV6_ADDR="none" - IPV6_GATE="" - break + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 8: Network Bridge + # ═══════════════════════════════════════════════════════════════════════════ + 8) + if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then + _bridge="vmbr0" + ((STEP++)) + else + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "NETWORK BRIDGE" \ + --ok-button "Next" --cancel-button "Back" \ + --menu "\nSelect network bridge:" 16 58 6 \ + "${BRIDGE_MENU_OPTIONS[@]}" \ + 3>&1 1>&2 2>&3); then + _bridge="${result:-vmbr0}" + ((STEP++)) + else + ((STEP--)) + fi + fi ;; - *) - exit_script + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 9: IPv4 Configuration + # ═══════════════════════════════════════════════════════════════════════════ + 9) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "IPv4 CONFIGURATION" \ + --ok-button "Next" --cancel-button "Back" \ + --menu "\nSelect IPv4 Address Assignment:" 14 60 2 \ + "dhcp" "Automatic (DHCP, recommended)" \ + "static" "Static (manual entry)" \ + 3>&1 1>&2 2>&3); then + + if [[ "$result" == "static" ]]; then + # Get static IP + local static_ip + if static_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "STATIC IPv4 ADDRESS" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nEnter Static IPv4 CIDR Address\n(e.g. 192.168.1.100/24)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + if [[ "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then + # Get gateway + local gateway_ip + if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "GATEWAY IP" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nEnter Gateway IP address" 10 58 "" \ + 3>&1 1>&2 2>&3); then + if [[ "$gateway_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + _net="$static_ip" + _gate=",gw=$gateway_ip" + ((STEP++)) + else + whiptail --msgbox "Invalid Gateway IP format." 8 58 + fi + fi + else + whiptail --msgbox "Invalid IPv4 CIDR format.\nExample: 192.168.1.100/24" 8 58 + fi + fi + else + _net="dhcp" + _gate="" + ((STEP++)) + fi + else + ((STEP--)) + fi ;; - esac - done - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if APT_CACHER_IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - else - exit_script - fi - fi + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 10: IPv6 Configuration + # ═══════════════════════════════════════════════════════════════════════════ + 10) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "IPv6 CONFIGURATION" \ + --ok-button "Next" --cancel-button "Back" \ + --menu "\nSelect IPv6 Address Management:" 16 58 4 \ + "auto" "SLAAC/AUTO (recommended)" \ + "dhcp" "DHCPv6" \ + "static" "Static (manual entry)" \ + "none" "Disabled" \ + 3>&1 1>&2 2>&3); then - if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$MTU1" ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit_script - fi + _ipv6_method="$result" + case "$result" in + static) + local ipv6_addr + if ipv6_addr=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "STATIC IPv6 ADDRESS" \ + --inputbox "\nEnter IPv6 CIDR address\n(e.g. 2001:db8::1/64)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + if [[ "$ipv6_addr" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then + _ipv6_addr="$ipv6_addr" + # Optional gateway + _ipv6_gate=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "IPv6 GATEWAY" \ + --inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \ + 3>&1 1>&2 2>&3) || true + ((STEP++)) + else + whiptail --msgbox "Invalid IPv6 CIDR format." 8 58 + fi + fi + ;; + dhcp) + _ipv6_addr="dhcp" + _ipv6_gate="" + ((STEP++)) + ;; + none) + _ipv6_addr="none" + _ipv6_gate="" + ((STEP++)) + ;; + *) + _ipv6_addr="" + _ipv6_gate="" + ((STEP++)) + ;; + esac + else + ((STEP--)) + fi + ;; - if SD=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z "$SD" ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" - else - exit_script - fi + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 11: MTU Size + # ═══════════════════════════════════════════════════════════════════════════ + 11) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "MTU SIZE" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet Interface MTU Size\n(leave blank for default 1500)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + _mtu="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - if NX=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z "$NX" ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" - else - exit_script - fi + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 12: DNS Search Domain + # ═══════════════════════════════════════════════════════════════════════════ + 12) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "DNS SEARCH DOMAIN" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet DNS Search Domain\n(leave blank to use host setting)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + _sd="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z "$MAC1" ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit_script - fi + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 13: DNS Server + # ═══════════════════════════════════════════════════════════════════════════ + 13) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "DNS SERVER" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet DNS Server IP\n(leave blank to use host setting)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + _ns="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z "$VLAN1" ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" - else - exit_script - fi + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 14: MAC Address + # ═══════════════════════════════════════════════════════════════════════════ + 14) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "MAC ADDRESS" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet MAC Address\n(leave blank for auto-generated)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + _mac="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - if ADV_TAGS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then - if [ -n "${ADV_TAGS}" ]; then - ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') - TAGS="${ADV_TAGS}" - else - TAGS=";" - fi - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - else - exit_script - fi + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 15: VLAN Tag + # ═══════════════════════════════════════════════════════════════════════════ + 15) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "VLAN TAG" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet VLAN Tag\n(leave blank for no VLAN)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + _vlan="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "SSH Authorized key for root (leave empty for none)" 8 58 --title "SSH Key" 3>&1 1>&2 2>&3)" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 16: Tags + # ═══════════════════════════════════════════════════════════════════════════ + 16) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONTAINER TAGS" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet Custom Tags (semicolon-separated)\n(remove all for no tags)" 12 58 "$_tags" \ + 3>&1 1>&2 2>&3); then + _tags="${result:-;}" + _tags=$(echo "$_tags" | tr -d '[:space:]') + ((STEP++)) + else + ((STEP--)) + fi + ;; - if [[ -z "${SSH_AUTHORIZED_KEY}" ]]; then - SSH_AUTHORIZED_KEY="" - fi + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 17: SSH Settings + # ═══════════════════════════════════════════════════════════════════════════ + 17) + configure_ssh_settings + # configure_ssh_settings handles its own flow, always advance + ((STEP++)) + ;; - if [[ "$PW" == -password* || -n "$SSH_AUTHORIZED_KEY" ]]; then - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable Root SSH Access?" 10 58); then - SSH="yes" - else - SSH="no" - fi - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - else - SSH="no" - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - fi + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 18: FUSE & Verbose Mode + # ═══════════════════════════════════════════════════════════════════════════ + 18) + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "FUSE SUPPORT" \ + --ok-button "Next" --cancel-button "Back" \ + --defaultno \ + --yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc." 12 58; then + _enable_fuse="yes" + else + if [ $? -eq 1 ]; then + _enable_fuse="no" + else + ((STEP--)) + continue + fi + fi - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then - ENABLE_FUSE="yes" - else - ENABLE_FUSE="no" - fi - echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}" + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "VERBOSE MODE" \ + --defaultno \ + --yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then + _verbose="yes" + else + _verbose="no" + fi + ((STEP++)) + ;; - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then - VERBOSE="yes" - else - VERBOSE="no" - fi - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 19: Confirmation + # ═══════════════════════════════════════════════════════════════════════════ + 19) + # Build summary + local ct_type_desc="Unprivileged" + [[ "$_ct_type" == "0" ]] && ct_type_desc="Privileged" + + local summary="Container Type: $ct_type_desc +Container ID: $_ct_id +Hostname: $_hostname + +Resources: + Disk: ${_disk_size} GB + CPU: $_core_count cores + RAM: $_ram_size MiB + +Network: + Bridge: $_bridge + IPv4: $_net + IPv6: $_ipv6_method + +Options: + FUSE: $_enable_fuse + Verbose: $_verbose" + + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONFIRM SETTINGS" \ + --ok-button "Create LXC" --cancel-button "Back" \ + --yesno "$summary\n\nCreate ${APP} LXC with these settings?" 26 58; then + ((STEP++)) + else + ((STEP--)) + fi + ;; + esac + done - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then - echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}" - - # Strip prefixes from DNS parameters for config file storage - local SD_VALUE="$SD" - local NS_VALUE="$NS" - local MAC_VALUE="$MAC" - local VLAN_VALUE="$VLAN" - [[ "$SD" =~ ^-searchdomain= ]] && SD_VALUE="${SD#-searchdomain=}" - [[ "$NS" =~ ^-nameserver= ]] && NS_VALUE="${NS#-nameserver=}" - [[ "$MAC" =~ ^,hwaddr= ]] && MAC_VALUE="${MAC#,hwaddr=}" - [[ "$VLAN" =~ ^,tag= ]] && VLAN_VALUE="${VLAN#,tag=}" - - # Temporarily store original values - local SD_ORIG="$SD" - local NS_ORIG="$NS" - local MAC_ORIG="$MAC" - local VLAN_ORIG="$VLAN" - - # Set clean values for config file writing - SD="$SD_VALUE" - NS="$NS_VALUE" - MAC="$MAC_VALUE" - VLAN="$VLAN_VALUE" - - write_config - - # Restore original formatted values for container creation - SD="$SD_ORIG" - NS="$NS_ORIG" - MAC="$MAC_ORIG" - VLAN="$VLAN_ORIG" + # ═══════════════════════════════════════════════════════════════════════════ + # Apply all collected values to global variables + # ═══════════════════════════════════════════════════════════════════════════ + CT_TYPE="$_ct_type" + PW="$_pw" + CT_ID="$_ct_id" + HN="$_hostname" + DISK_SIZE="$_disk_size" + CORE_COUNT="$_core_count" + RAM_SIZE="$_ram_size" + BRG="$_bridge" + NET="$_net" + GATE="$_gate" + IPV6_METHOD="$_ipv6_method" + IPV6_ADDR="$_ipv6_addr" + IPV6_GATE="$_ipv6_gate" + TAGS="$_tags" + ENABLE_FUSE="$_enable_fuse" + VERBOSE="$_verbose" + + # Format optional values + [[ -n "$_mtu" ]] && MTU=",mtu=$_mtu" || MTU="" + [[ -n "$_sd" ]] && SD="-searchdomain=$_sd" || SD="" + [[ -n "$_ns" ]] && NS="-nameserver=$_ns" || NS="" + [[ -n "$_mac" ]] && MAC=",hwaddr=$_mac" || MAC="" + [[ -n "$_vlan" ]] && VLAN=",tag=$_vlan" || VLAN="" + + # Alpine UDHCPC fix + if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ -n "$_ns" ]; then + UDHCPC_FIX="yes" else - clear - header_info - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}" - advanced_settings + UDHCPC_FIX="no" fi + export UDHCPC_FIX + export SSH_KEYS_FILE + + # Display final summary + echo -e "\n${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$([ "$CT_TYPE" == "1" ] && echo "Unprivileged" || echo "Privileged")${CL}" + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" + echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + echo -e "${NETWORK}${BOLD}${DGN}IPv4: ${BGN}$NET${CL}" + echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}$IPV6_METHOD${CL}" + echo -e "${FUSE}${BOLD}${DGN}FUSE Support: ${BGN}$ENABLE_FUSE${CL}" + echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" + echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}" } +# ============================================================================== +# SECTION 7: USER INTERFACE & DIAGNOSTICS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# diagnostics_check() +# +# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics +# - Asks user whether to send anonymous diagnostic data +# - Saves DIAGNOSTICS=yes/no in the config file +# - Creates file if missing with default DIAGNOSTICS=yes +# - Reads current diagnostics setting from file +# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out +# ------------------------------------------------------------------------------ diagnostics_check() { if ! [ -d "/usr/local/community-scripts" ]; then mkdir -p /usr/local/community-scripts fi if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then + if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then cat </usr/local/community-scripts/diagnostics DIAGNOSTICS=yes @@ -876,7 +1652,6 @@ DIAGNOSTICS=yes #To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue. #This will enable the diagnostics feature. #The following information will be sent: -#"ct_type" #"disk_size" #"core_count" #"ram_size" @@ -902,7 +1677,6 @@ DIAGNOSTICS=no #To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue. #This will enable the diagnostics feature. #The following information will be sent: -#"ct_type" #"disk_size" #"core_count" #"ram_size" @@ -920,9 +1694,64 @@ EOF DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics) fi +} + +diagnostics_menu() { + if [ "${DIAGNOSTICS:-no}" = "yes" ]; then + if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "DIAGNOSTIC SETTINGS" \ + --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ + --yes-button "No" --no-button "Back"; then + DIAGNOSTICS="no" + sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics + whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 + fi + else + if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "DIAGNOSTIC SETTINGS" \ + --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ + --yes-button "Yes" --no-button "Back"; then + DIAGNOSTICS="yes" + sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics + whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 + fi + fi +} +# ------------------------------------------------------------------------------ +# echo_default() +# +# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.) +# - Uses icons and formatting for readability +# - Convert CT_TYPE to description +# ------------------------------------------------------------------------------ +echo_default() { + CT_TYPE_DESC="Unprivileged" + if [ "$CT_TYPE" -eq 0 ]; then + CT_TYPE_DESC="Privileged" + fi + echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" + if [ "$VERBOSE" == "yes" ]; then + echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" + fi + echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" + echo -e " " } +# ------------------------------------------------------------------------------ +# install_script() +# +# - Main entrypoint for installation mode +# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check) +# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit) +# - Applies chosen settings and triggers container build +# ------------------------------------------------------------------------------ install_script() { pve_check shell_check @@ -935,110 +1764,203 @@ install_script() { if systemctl is-active -q ping-instances.service; then systemctl -q stop ping-instances.service fi + NEXTID=$(pvesh get /cluster/nextid) - # Read timezone - fallback for Debian 13/Proxmox 9+ where /etc/timezone doesn't exist - if [[ -f /etc/timezone ]]; then + + # Get timezone using timedatectl (Debian 13+ compatible) + # Fallback to /etc/timezone for older systems + if command -v timedatectl >/dev/null 2>&1; then + timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "UTC") + elif [ -f /etc/timezone ]; then timezone=$(cat /etc/timezone) else - timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "UTC") + timezone="UTC" fi + + # Show APP Header header_info - while true; do - TMP_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "SETTINGS" \ - --menu "Choose an option:" 20 60 6 \ - "1" "Default Settings" \ - "2" "Default Settings (with verbose)" \ - "3" "Advanced Settings" \ - "4" "Use Config File" \ - "5" "Diagnostic Settings" \ - "6" "Exit" \ - --default-item "1" 3>&1 1>&2 2>&3) || true - - if [ -z "$TMP_CHOICE" ]; then - echo -e "\n${CROSS}${RD}Menu canceled. Exiting script.${CL}\n" - exit 0 - fi + # --- Support CLI argument as direct preset (default, advanced, …) --- + CHOICE="${mode:-${1:-}}" + + # If no CLI argument → show whiptail menu + # Build menu dynamically based on available options + local appdefaults_option="" + local settings_option="" + local menu_items=( + "1" "Default Install" + "2" "Advanced Install" + "3" "User Defaults" + ) - CHOICE="$TMP_CHOICE" + if [ -f "$(get_app_defaults_path)" ]; then + appdefaults_option="4" + menu_items+=("4" "App Defaults for ${APP}") + settings_option="5" + menu_items+=("5" "Settings") + else + settings_option="4" + menu_items+=("4" "Settings") + fi - case $CHOICE in - 1) + APPDEFAULTS_OPTION="$appdefaults_option" + SETTINGS_OPTION="$settings_option" + + # Main menu loop - allows returning from Settings + while true; do + if [ -z "$CHOICE" ]; then + TMP_CHOICE=$(whiptail \ + --backtitle "Proxmox VE Helper Scripts" \ + --title "Community-Scripts Options" \ + --ok-button "Select" --cancel-button "Exit Script" \ + --notags \ + --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ + 20 60 9 \ + "${menu_items[@]}" \ + --default-item "1" \ + 3>&1 1>&2 2>&3) || exit_script + CHOICE="$TMP_CHOICE" + fi + + # --- Main case --- + local defaults_target="" + local run_maybe_offer="no" + case "$CHOICE" in + 1 | default | DEFAULT) header_info echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" VERBOSE="no" METHOD="default" base_settings "$VERBOSE" echo_default + defaults_target="$(ensure_global_default_vars_file)" break ;; - 2) - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME (${VERBOSE_CROPPED}Verbose)${CL}" - VERBOSE="yes" - METHOD="default" - base_settings "$VERBOSE" - echo_default - break - ;; - 3) + 2 | advanced | ADVANCED) header_info - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}" + echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" + echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" METHOD="advanced" base_settings advanced_settings + defaults_target="$(ensure_global_default_vars_file)" + run_maybe_offer="yes" break ;; - 4) - header_info - echo -e "${INFO}${HOLD} ${GN}Using Config File on node $PVEHOST_NAME${CL}" - METHOD="config_file" - source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/config-file.func) - config_file + 3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS) + default_var_settings || { + msg_error "Failed to apply default.vars" + exit 1 + } + defaults_target="/usr/local/community-scripts/default.vars" break ;; - 5) - if [[ $DIAGNOSTICS == "yes" ]]; then - if whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --yesno "Send Diagnostics of LXC Installation?\n\nCurrent setting: ${DIAGNOSTICS}" 10 58 \ - --yes-button "No" --no-button "Back"; then - DIAGNOSTICS="no" - sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics - whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --msgbox "Diagnostics settings changed to ${DIAGNOSTICS}." 8 58 - fi + "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS) + if [ -f "$(get_app_defaults_path)" ]; then + header_info + echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" + METHOD="appdefaults" + base_settings + load_vars_file "$(get_app_defaults_path)" + echo_default + defaults_target="$(get_app_defaults_path)" + break else - if whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --yesno "Send Diagnostics of LXC Installation?\n\nCurrent setting: ${DIAGNOSTICS}" 10 58 \ - --yes-button "Yes" --no-button "Back"; then - DIAGNOSTICS="yes" - sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics - whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --msgbox "Diagnostics settings changed to ${DIAGNOSTICS}." 8 58 - fi + msg_error "No App Defaults available for ${APP}" + exit 1 fi - ;; - 6) - echo -e "\n${CROSS}${RD}Script terminated. Have a great day!${CL}\n" - exit 0 + "$SETTINGS_OPTION" | settings | SETTINGS) + settings_menu + # After settings menu, show main menu again + header_info + CHOICE="" ;; *) - echo -e "\n${CROSS}${RD}Invalid option, please try again.${CL}\n" + echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" + exit 1 + ;; + esac + done + + if [[ -n "$defaults_target" ]]; then + ensure_storage_selection_for_vars_file "$defaults_target" + fi + + if [[ "$run_maybe_offer" == "yes" ]]; then + maybe_offer_save_app_defaults + fi +} + +edit_default_storage() { + local vf="/usr/local/community-scripts/default.vars" + + # Ensure file exists + if [[ ! -f "$vf" ]]; then + mkdir -p "$(dirname "$vf")" + touch "$vf" + fi + + # Let ensure_storage_selection_for_vars_file handle everything + ensure_storage_selection_for_vars_file "$vf" +} + +settings_menu() { + while true; do + local settings_items=( + "1" "Manage API-Diagnostic Setting" + "2" "Edit Default.vars" + ) + if [ -f "$(get_app_defaults_path)" ]; then + settings_items+=("3" "Edit App.vars for ${APP}") + settings_items+=("4" "Back to Main Menu") + else + settings_items+=("3" "Back to Main Menu") + fi + + local choice + choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "Community-Scripts SETTINGS Menu" \ + --ok-button "Select" --cancel-button "Exit Script" \ + --menu "\n\nChoose a settings option:\n\nUse Arrow keys to navigate, ENTER to select, TAB for buttons." 20 60 9 \ + "${settings_items[@]}" \ + 3>&1 1>&2 2>&3) || exit_script + + case "$choice" in + 1) diagnostics_menu ;; + 2) nano /usr/local/community-scripts/default.vars ;; + 3) + if [ -f "$(get_app_defaults_path)" ]; then + nano "$(get_app_defaults_path)" + else + # Back was selected (no app.vars available) + return + fi + ;; + 4) + # Back to main menu + return ;; esac done } +# ------------------------------------------------------------------------------ +# check_container_resources() +# +# - Compares host RAM/CPU with required values +# - Warns if under-provisioned and asks user to continue or abort +# ------------------------------------------------------------------------------ check_container_resources() { - # Check actual RAM & Cores current_ram=$(free -m | awk 'NR==2{print $2}') current_cpu=$(nproc) - # Check whether the current RAM is less than the required RAM or the CPU cores are less than required if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}" echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n" - echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? [y/N] " + echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? " read -r prompt - if [[ ! "${prompt,,}" =~ ^(y|yes)$ ]]; then + if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}" exit 1 fi @@ -1047,42 +1969,236 @@ check_container_resources() { fi } +# ------------------------------------------------------------------------------ +# check_container_storage() +# +# - Checks /boot partition usage +# - Warns if usage >80% and asks user confirmation before proceeding +# ------------------------------------------------------------------------------ check_container_storage() { - # Check if the /boot partition is more than 80% full total_size=$(df /boot --output=size | tail -n 1) local used_size=$(df /boot --output=used | tail -n 1) usage=$((100 * used_size / total_size)) if ((usage > 80)); then - # Prompt the user for confirmation to continue echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}" - echo -ne "Continue anyway? [y/N] " + echo -ne "Continue anyway? " read -r prompt - if [[ ! "${prompt,,}" =~ ^(y|yes)$ ]]; then + if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}" exit 1 fi fi } -start() { - source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) - if command -v pveversion >/dev/null 2>&1; then - install_script - else - CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ - "Support/Update functions for ${APP} LXC. Choose an option:" \ - 12 60 3 \ - "1" "YES (Silent Mode)" \ - "2" "YES (Verbose Mode)" \ - "3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3) +# ------------------------------------------------------------------------------ +# ssh_extract_keys_from_file() +# +# - Extracts valid SSH public keys from given file +# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines +# ------------------------------------------------------------------------------ +ssh_extract_keys_from_file() { + local f="$1" + [[ -r "$f" ]] || return 0 + tr -d '\r' <"$f" | awk ' + /^[[:space:]]*#/ {next} + /^[[:space:]]*$/ {next} + # nackt: typ base64 [comment] + /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next} + # mit Optionen: finde ab erstem Key-Typ + { + match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/) + if (RSTART>0) { print substr($0, RSTART) } + } + ' +} - case "$CHOICE" in - 1) - VERBOSE="no" - set_std_mode - ;; - 2) - VERBOSE="yes" +# ------------------------------------------------------------------------------ +# ssh_build_choices_from_files() +# +# - Builds interactive whiptail checklist of available SSH keys +# - Generates fingerprint, type and comment for each key +# ------------------------------------------------------------------------------ +ssh_build_choices_from_files() { + local -a files=("$@") + CHOICES=() + COUNT=0 + MAPFILE="$(mktemp)" + local id key typ fp cmt base ln=0 + + for f in "${files[@]}"; do + [[ -f "$f" && -r "$f" ]] || continue + base="$(basename -- "$f")" + case "$base" in + known_hosts | known_hosts.* | config) continue ;; + id_*) [[ "$f" != *.pub ]] && continue ;; + esac + + # map every key in file + while IFS= read -r key; do + [[ -n "$key" ]] || continue + + typ="" + fp="" + cmt="" + # Only the pure key part (without options) is already included in ‘key’. + read -r _typ _b64 _cmt <<<"$key" + typ="${_typ:-key}" + cmt="${_cmt:-}" + # Fingerprint via ssh-keygen (if available) + if command -v ssh-keygen >/dev/null 2>&1; then + fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')" + fi + # Label shorten + [[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..." + + ln=$((ln + 1)) + COUNT=$((COUNT + 1)) + id="K${COUNT}" + echo "${id}|${key}" >>"$MAPFILE" + CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF") + done < <(ssh_extract_keys_from_file "$f") + done +} + +# ------------------------------------------------------------------------------ +# ssh_discover_default_files() +# +# - Scans standard paths for SSH keys +# - Includes ~/.ssh/*.pub, /etc/ssh/authorized_keys, etc. +# ------------------------------------------------------------------------------ +ssh_discover_default_files() { + local -a cand=() + shopt -s nullglob + cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2) + cand+=(/root/.ssh/*.pub) + cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*) + shopt -u nullglob + printf '%s\0' "${cand[@]}" +} + +configure_ssh_settings() { + SSH_KEYS_FILE="$(mktemp)" + : >"$SSH_KEYS_FILE" + + IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') + ssh_build_choices_from_files "${_def_files[@]}" + local default_key_count="$COUNT" + + local ssh_key_mode + if [[ "$default_key_count" -gt 0 ]]; then + ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ + "Provision SSH keys for root:" 14 72 4 \ + "found" "Select from detected keys (${default_key_count})" \ + "manual" "Paste a single public key" \ + "folder" "Scan another folder (path or glob)" \ + "none" "No keys" 3>&1 1>&2 2>&3) || exit_script + else + ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ + "No host keys detected; choose manual/none:" 12 72 2 \ + "manual" "Paste a single public key" \ + "none" "No keys" 3>&1 1>&2 2>&3) || exit_script + fi + + case "$ssh_key_mode" in + found) + local selection + selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \ + --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script + for tag in $selection; do + tag="${tag%\"}" + tag="${tag#\"}" + local line + line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) + [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" + done + ;; + manual) + SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)" + [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE" + ;; + folder) + local glob_path + glob_path=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3) + if [[ -n "$glob_path" ]]; then + shopt -s nullglob + read -r -a _scan_files <<<"$glob_path" + shopt -u nullglob + if [[ "${#_scan_files[@]}" -gt 0 ]]; then + ssh_build_choices_from_files "${_scan_files[@]}" + if [[ "$COUNT" -gt 0 ]]; then + local folder_selection + folder_selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \ + --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script + for tag in $folder_selection; do + tag="${tag%\"}" + tag="${tag#\"}" + local line + line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) + [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" + done + else + whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60 + fi + else + whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60 + fi + fi + ;; + none) + : + ;; + esac + + if [[ -s "$SSH_KEYS_FILE" ]]; then + sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" + printf '\n' >>"$SSH_KEYS_FILE" + fi + + if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then + if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then + SSH="yes" + else + SSH="no" + fi + else + SSH="no" + fi +} + +# ------------------------------------------------------------------------------ +# start() +# +# - Entry point of script +# - On Proxmox host: calls install_script +# - In silent mode: runs update_script +# - Otherwise: shows update/setting menu +# ------------------------------------------------------------------------------ +start() { + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) + if command -v pveversion >/dev/null 2>&1; then + install_script || return 0 + return 0 + elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then + VERBOSE="no" + set_std_mode + update_script + else + CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ + "Support/Update functions for ${APP} LXC. Choose an option:" \ + 12 60 3 \ + "1" "YES (Silent Mode)" \ + "2" "YES (Verbose Mode)" \ + "3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3) + + case "$CHOICE" in + 1) + VERBOSE="no" + set_std_mode + ;; + 2) + VERBOSE="yes" set_std_mode ;; 3) @@ -1092,15 +2208,67 @@ start() { ;; esac update_script - cleanup_lxc fi } -# This function collects user settings and integrates all the collected information. +# ============================================================================== +# SECTION 8: CONTAINER CREATION & DEPLOYMENT +# ============================================================================== + +# ------------------------------------------------------------------------------ +# build_container() +# +# - Main function for creating and configuring LXC container +# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6) +# - Creates container via pct create with all specified settings +# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough +# - Starts container and waits for network connectivity +# - Installs base packages (curl, sudo, etc.) +# - Injects SSH keys if configured +# - Executes -install.sh inside container +# - Posts installation telemetry to API if diagnostics enabled +# ------------------------------------------------------------------------------ build_container() { # if [ "$VERBOSE" == "yes" ]; then set -x; fi - NET_STRING="-net0 name=eth0,bridge=$BRG$MAC,ip=$NET$GATE$VLAN$MTU" + NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}" + + # MAC + if [[ -n "$MAC" ]]; then + case "$MAC" in + ,hwaddr=*) NET_STRING+="$MAC" ;; + *) NET_STRING+=",hwaddr=$MAC" ;; + esac + fi + + # IP (immer zwingend, Standard dhcp) + NET_STRING+=",ip=${NET:-dhcp}" + + # Gateway + if [[ -n "$GATE" ]]; then + case "$GATE" in + ,gw=*) NET_STRING+="$GATE" ;; + *) NET_STRING+=",gw=$GATE" ;; + esac + fi + + # VLAN + if [[ -n "$VLAN" ]]; then + case "$VLAN" in + ,tag=*) NET_STRING+="$VLAN" ;; + *) NET_STRING+=",tag=$VLAN" ;; + esac + fi + + # MTU + if [[ -n "$MTU" ]]; then + case "$MTU" in + ,mtu=*) NET_STRING+="$MTU" ;; + *) NET_STRING+=",mtu=$MTU" ;; + esac + fi + + # IPv6 Handling case "$IPV6_METHOD" in auto) NET_STRING="$NET_STRING,ip6=auto" ;; dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;; @@ -1110,6 +2278,8 @@ build_container() { ;; none) ;; esac + + # Build FEATURES string (simple working version) if [ "$CT_TYPE" == "1" ]; then FEATURES="keyctl=1,nesting=1" else @@ -1120,9 +2290,8 @@ build_container() { FEATURES="$FEATURES,fuse=1" fi - if [[ $DIAGNOSTICS == "yes" ]]; then - post_to_api - fi + # NEW IMPLEMENTATION (Fixed): Build PCT_OPTIONS properly + # Key insight: Bash cannot export arrays, so we build the options as a string TEMP_DIR=$(mktemp -d) pushd "$TEMP_DIR" >/dev/null @@ -1132,8 +2301,10 @@ build_container() { export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)" fi + # Core exports for install.func export DIAGNOSTICS="$DIAGNOSTICS" export RANDOM_UUID="$RANDOM_UUID" + export SESSION_ID="$SESSION_ID" export CACHER="$APT_CACHER" export CACHER_IP="$APT_CACHER_IP" export tz="$timezone" @@ -1150,28 +2321,162 @@ build_container() { export PCT_OSTYPE="$var_os" export PCT_OSVERSION="$var_version" export PCT_DISK_SIZE="$DISK_SIZE" - export PCT_OPTIONS=" - -features $FEATURES - -hostname $HN - -tags $TAGS - $SD - $NS - $NET_STRING - -onboot 1 - -cores $CORE_COUNT - -memory $RAM_SIZE - -unprivileged $CT_TYPE - $PW - " - # This executes create_lxc.sh and creates the container and .conf file - bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/create_lxc.sh)" $? + + # DEV_MODE exports (optional, for debugging) + export BUILD_LOG="$BUILD_LOG" + export INSTALL_LOG="/root/.install-${SESSION_ID}.log" + export dev_mode="${dev_mode:-}" + export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}" + export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}" + export DEV_MODE_TRACE="${DEV_MODE_TRACE:-false}" + export DEV_MODE_PAUSE="${DEV_MODE_PAUSE:-false}" + export DEV_MODE_BREAKPOINT="${DEV_MODE_BREAKPOINT:-false}" + export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}" + export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}" + + # Build PCT_OPTIONS as multi-line string + PCT_OPTIONS_STRING=" -features $FEATURES + -hostname $HN + -tags $TAGS" + + # Add storage if specified + if [ -n "$SD" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + $SD" + fi + + # Add nameserver if specified + if [ -n "$NS" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + $NS" + fi + + # Network configuration + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + $NET_STRING + -onboot 1 + -cores $CORE_COUNT + -memory $RAM_SIZE + -unprivileged $CT_TYPE" + + # Protection flag (if var_protection was set) + if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + -protection 1" + fi + + # Timezone flag (if var_timezone was set) + if [ -n "${CT_TIMEZONE:-}" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + -timezone $CT_TIMEZONE" + fi + + # Password (already formatted) + if [ -n "$PW" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + $PW" + fi + + # Export as string (this works, unlike arrays!) + export PCT_OPTIONS="$PCT_OPTIONS_STRING" + export TEMPLATE_STORAGE="${var_template_storage:-}" + export CONTAINER_STORAGE="${var_container_storage:-}" + + # # DEBUG: Show final PCT_OPTIONS being exported + # echo "[DEBUG] PCT_OPTIONS to be exported:" + # echo "$PCT_OPTIONS" | sed 's/^/ /' + # echo "[DEBUG] Calling create_lxc_container..." + + create_lxc_container || exit $? LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" - # USB passthrough for privileged LXC (CT_TYPE=0) - if [ "$CT_TYPE" == "0" ]; then + # ============================================================================ + # GPU/USB PASSTHROUGH CONFIGURATION + # ============================================================================ + + # List of applications that benefit from GPU acceleration + GPU_APPS=( + "immich" "channels" "emby" "ersatztv" "frigate" + "jellyfin" "plex" "scrypted" "tdarr" "unmanic" + "ollama" "fileflows" "open-webui" "tunarr" "debian" + "handbrake" "sunshine" "moonlight" "kodi" "stremio" + "viseron" + ) + + # Check if app needs GPU + is_gpu_app() { + local app="${1,,}" + for gpu_app in "${GPU_APPS[@]}"; do + [[ "$app" == "${gpu_app,,}" ]] && return 0 + done + return 1 + } + + # Detect all available GPU devices + detect_gpu_devices() { + INTEL_DEVICES=() + AMD_DEVICES=() + NVIDIA_DEVICES=() + + # Store PCI info to avoid multiple calls + local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") + + # Check for Intel GPU - look for Intel vendor ID [8086] + if echo "$pci_vga_info" | grep -q "\[8086:"; then + msg_custom "🎮" "${BL}" "Detected Intel GPU" + if [[ -d /dev/dri ]]; then + for d in /dev/dri/renderD* /dev/dri/card*; do + [[ -e "$d" ]] && INTEL_DEVICES+=("$d") + done + fi + fi + + # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD) + if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then + msg_custom "🎮" "${RD}" "Detected AMD GPU" + if [[ -d /dev/dri ]]; then + # Only add if not already claimed by Intel + if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then + for d in /dev/dri/renderD* /dev/dri/card*; do + [[ -e "$d" ]] && AMD_DEVICES+=("$d") + done + fi + fi + fi + + # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] + if echo "$pci_vga_info" | grep -q "\[10de:"; then + msg_custom "🎮" "${GN}" "Detected NVIDIA GPU" + + # Simple passthrough - just bind /dev/nvidia* devices if they exist + for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset /dev/nvidia-uvm /dev/nvidia-uvm-tools; do + [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d") + done + + if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then + msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough" + else + msg_warn "NVIDIA GPU detected via PCI but no /dev/nvidia* devices found" + msg_custom "ℹ️" "${YW}" "Skipping NVIDIA passthrough (host drivers may not be loaded)" + fi + fi + + # Debug output + msg_debug "Intel devices: ${INTEL_DEVICES[*]}" + msg_debug "AMD devices: ${AMD_DEVICES[*]}" + msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" + } + + # Configure USB passthrough for privileged containers + configure_usb_passthrough() { + if [[ "$CT_TYPE" != "0" ]]; then + return 0 + fi + + msg_info "Configuring automatic USB passthrough (privileged container)" cat <>"$LXC_CONFIG" -# USB passthrough +# Automatic USB passthrough (privileged container) lxc.cgroup2.devices.allow: a lxc.cap.drop: lxc.cgroup2.devices.allow: c 188:* rwm @@ -1182,96 +2487,140 @@ lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create= lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file EOF - fi + msg_ok "USB passthrough configured" + } + + # Configure GPU passthrough + configure_gpu_passthrough() { + # Skip if not a GPU app and not privileged + if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then + return 0 + fi - # VAAPI passthrough for privileged containers or known apps - VAAPI_APPS=( - "immich" - "Channels" - "Emby" - "ErsatzTV" - "Frigate" - "Jellyfin" - "Plex" - "Scrypted" - "Tdarr" - "Unmanic" - "Ollama" - "FileFlows" - "Open WebUI" - ) + detect_gpu_devices - is_vaapi_app=false - for vaapi_app in "${VAAPI_APPS[@]}"; do - if [[ "$APP" == "$vaapi_app" ]]; then - is_vaapi_app=true - break - fi - done + # Count available GPU types + local gpu_count=0 + local available_gpus=() - if ([ "$CT_TYPE" == "0" ] || [ "$is_vaapi_app" == "true" ]) && - ([[ -e /dev/dri/renderD128 ]] || [[ -e /dev/dri/card0 ]] || [[ -e /dev/fb0 ]]); then + if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then + available_gpus+=("INTEL") + gpu_count=$((gpu_count + 1)) + fi - echo "" - msg_custom "⚙️ " "\e[96m" "Configuring VAAPI passthrough for LXC container" - if [ "$CT_TYPE" != "0" ]; then - msg_custom "⚠️ " "\e[33m" "Container is unprivileged – VAAPI passthrough may not work without additional host configuration (e.g., idmap)." + if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then + available_gpus+=("AMD") + gpu_count=$((gpu_count + 1)) fi - msg_custom "ℹ️ " "\e[96m" "VAAPI enables GPU hardware acceleration (e.g., for video transcoding in Jellyfin or Plex)." - echo "" - read -rp "➤ Automatically mount all available VAAPI devices? [Y/n]: " VAAPI_ALL - - if [[ "$VAAPI_ALL" =~ ^[Yy]$|^$ ]]; then - if [ "$CT_TYPE" == "0" ]; then - # PRV Container → alles zulässig - [[ -e /dev/dri/renderD128 ]] && { - echo "lxc.cgroup2.devices.allow: c 226:128 rwm" >>"$LXC_CONFIG" - echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG" - } - [[ -e /dev/dri/card0 ]] && { - echo "lxc.cgroup2.devices.allow: c 226:0 rwm" >>"$LXC_CONFIG" - echo "lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none bind,optional,create=file" >>"$LXC_CONFIG" - } - [[ -e /dev/fb0 ]] && { - echo "lxc.cgroup2.devices.allow: c 29:0 rwm" >>"$LXC_CONFIG" - echo "lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file" >>"$LXC_CONFIG" - } - [[ -d /dev/dri ]] && { - echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG" - } - else - # UNPRV Container → nur devX für UI - [[ -e /dev/dri/card0 ]] && echo "dev0: /dev/dri/card0,gid=44" >>"$LXC_CONFIG" - [[ -e /dev/dri/card1 ]] && echo "dev0: /dev/dri/card1,gid=44" >>"$LXC_CONFIG" - [[ -e /dev/dri/renderD128 ]] && echo "dev1: /dev/dri/renderD128,gid=104" >>"$LXC_CONFIG" - fi + + if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then + available_gpus+=("NVIDIA") + gpu_count=$((gpu_count + 1)) fi - fi - if [ "$CT_TYPE" == "1" ] && [ "$is_vaapi_app" == "true" ]; then - if [[ -e /dev/dri/card0 ]]; then - echo "dev0: /dev/dri/card0,gid=44" >>"$LXC_CONFIG" - elif [[ -e /dev/dri/card1 ]]; then - echo "dev0: /dev/dri/card1,gid=44" >>"$LXC_CONFIG" + if [[ $gpu_count -eq 0 ]]; then + msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough" + return 0 fi - if [[ -e /dev/dri/renderD128 ]]; then - echo "dev1: /dev/dri/renderD128,gid=104" >>"$LXC_CONFIG" + + local selected_gpu="" + + if [[ $gpu_count -eq 1 ]]; then + # Automatic selection for single GPU + selected_gpu="${available_gpus[0]}" + msg_ok "Automatically configuring ${selected_gpu} GPU passthrough" + else + # Multiple GPUs - ask user + echo -e "\n${INFO} Multiple GPU types detected:" + for gpu in "${available_gpus[@]}"; do + echo " - $gpu" + done + read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu + selected_gpu="${selected_gpu^^}" + + # Validate selection + local valid=0 + for gpu in "${available_gpus[@]}"; do + [[ "$selected_gpu" == "$gpu" ]] && valid=1 + done + + if [[ $valid -eq 0 ]]; then + msg_warn "Invalid selection. Skipping GPU passthrough." + return 0 + fi fi - fi - # TUN device passthrough - if [ "$ENABLE_TUN" == "yes" ]; then - cat <>"$LXC_CONFIG" + # Apply passthrough configuration based on selection + local dev_idx=0 + + case "$selected_gpu" in + INTEL | AMD) + local devices=() + [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") + [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") + + # Use pct set to add devices with proper dev0/dev1 format + # GIDs will be detected and set after container starts + local dev_index=0 + for dev in "${devices[@]}"; do + # Add to config using pct set (will be visible in GUI) + echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG" + dev_index=$((dev_index + 1)) + done + + export GPU_TYPE="$selected_gpu" + msg_ok "${selected_gpu} GPU passthrough configured (${#devices[@]} devices)" + ;; + + NVIDIA) + if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then + msg_warn "No NVIDIA devices available for passthrough" + return 0 + fi + + # Use pct set for NVIDIA devices + local dev_index=0 + for dev in "${NVIDIA_DEVICES[@]}"; do + echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG" + dev_index=$((dev_index + 1)) + done + + export GPU_TYPE="NVIDIA" + msg_ok "NVIDIA GPU passthrough configured (${#NVIDIA_DEVICES[@]} devices) - install drivers in container if needed" + ;; + esac + } + + # Additional device passthrough + configure_additional_devices() { + # TUN device passthrough + if [ "$ENABLE_TUN" == "yes" ]; then + cat <>"$LXC_CONFIG" lxc.cgroup2.devices.allow: c 10:200 rwm lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file EOF - fi + fi + + # Coral TPU passthrough + if [[ -e /dev/apex_0 ]]; then + msg_custom "🔌" "${BL}" "Detected Coral TPU - configuring passthrough" + echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG" + fi + } + + # Execute pre-start configurations + configure_usb_passthrough + configure_gpu_passthrough + configure_additional_devices + + # ============================================================================ + # START CONTAINER AND INSTALL USERLAND + # ============================================================================ - # This starts the container and executes -install.sh msg_info "Starting LXC Container" pct start "$CTID" - # wait for status 'running' + # Wait for container to be running for i in {1..10}; do if pct status "$CTID" | grep -q "status: running"; then msg_ok "Started LXC Container" @@ -1284,51 +2633,47 @@ EOF fi done + # Wait for network (skip for Alpine initially) if [ "$var_os" != "alpine" ]; then msg_info "Waiting for network in LXC container" - for i in {1..10}; do - # 1. Primary check: ICMP ping (fastest, but may be blocked by ISP/firewall) - if pct exec "$CTID" -- ping -c1 -W1 deb.debian.org >/dev/null 2>&1; then - msg_ok "Network in LXC is reachable (ping)" - break - fi - # Wait and retry if not reachable yet - if [ "$i" -lt 10 ]; then - msg_warn "No network in LXC yet (try $i/10) – waiting..." - sleep 3 - else - # After 10 unsuccessful ping attempts, try HTTP connectivity via wget as fallback - msg_warn "Ping failed 10 times. Trying HTTP connectivity check (wget) as fallback..." - if pct exec "$CTID" -- wget -q --spider http://deb.debian.org; then - msg_ok "Network in LXC is reachable (wget fallback)" - else - msg_error "No network in LXC after all checks." - read -r -p "Set fallback DNS (1.1.1.1/8.8.8.8)? [y/N]: " choice - case "$choice" in - [yY]*) - pct set "$CTID" --nameserver 1.1.1.1 - pct set "$CTID" --nameserver 8.8.8.8 - # Final attempt with wget after DNS change - if pct exec "$CTID" -- wget -q --spider http://deb.debian.org; then - msg_ok "Network reachable after DNS fallback" - else - msg_error "Still no network/DNS in LXC! Aborting customization." - exit_script - fi - ;; - *) - msg_error "Aborted by user – no DNS fallback set." - exit_script - ;; - esac - fi - break - fi + + # Wait for IP + for i in {1..20}; do + ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) + [ -n "$ip_in_lxc" ] && break + sleep 1 done + + if [ -z "$ip_in_lxc" ]; then + msg_error "No IP assigned to CT $CTID after 20s" + exit 1 + fi + + # Simple connectivity check - just verify IP is assigned + msg_ok "Network configured (IP: $ip_in_lxc)" fi + # Function to get correct GID inside container + get_container_gid() { + local group="$1" + local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3) + echo "${gid:-44}" # Default to 44 if not found + } + + fix_gpu_gids + # Continue with standard container setup msg_info "Customizing LXC Container" - : "${tz:=Etc/UTC}" + + # # Install GPU userland if configured + # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then + # install_gpu_userland "VAAPI" + # fi + + # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then + # install_gpu_userland "NVIDIA" + # fi + + # Continue with standard container setup if [ "$var_os" == "alpine" ]; then sleep 3 pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories @@ -1338,8 +2683,7 @@ EOF' pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null" else sleep 3 - LANG=${LANG:-en_US.UTF-8} - pct exec "$CTID" -- bash -c "sed -i \"/$LANG/ s/^# //\" /etc/locale.gen" + pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen" pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ echo LANG=\$locale_line >/etc/default/locale && \ locale-gen >/dev/null && \ @@ -1348,20 +2692,1011 @@ EOF' if [[ -z "${tz:-}" ]]; then tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC") fi + if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then - pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime" + # Set timezone using symlink (Debian 13+ compatible) + # Create /etc/timezone for backwards compatibility with older scripts + pct exec "$CTID" -- bash -c "tz='$tz'; ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime && echo \"\$tz\" >/etc/timezone || true" else msg_warn "Skipping timezone setup – zone '$tz' not found in container" fi - pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" + pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || { + msg_error "apt-get base packages installation failed" + exit 1 + } fi + msg_ok "Customized LXC Container" + # Install SSH keys + install_ssh_keys_into_ct + + # Run application installer + # NOTE: We disable error handling here because: + # 1. Container errors are caught by error_handler INSIDE container + # 2. Container creates flag file with exit code + # 3. We read flag file and handle cleanup manually below + # 4. We DON'T want host error_handler to fire for lxc-attach command itself + + set +Eeuo pipefail # Disable ALL error handling temporarily + trap - ERR # Remove ERR trap completely + lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)" + local lxc_exit=$? + + set -Eeuo pipefail # Re-enable error handling + trap 'error_handler' ERR # Restore ERR trap + + # Check for error flag file in container (more reliable than lxc-attach exit code) + local install_exit_code=0 + if [[ -n "${SESSION_ID:-}" ]]; then + local error_flag="/root/.install-${SESSION_ID}.failed" + if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then + install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1") + pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true + fi + fi + + # Fallback to lxc-attach exit code if no flag file + if [[ $install_exit_code -eq 0 && $lxc_exit -ne 0 ]]; then + install_exit_code=$lxc_exit + fi + + # Installation failed? + if [[ $install_exit_code -ne 0 ]]; then + msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})" + + # Copy both logs from container before potential deletion + local build_log_copied=false + local install_log_copied=false + + if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then + # Copy BUILD_LOG (creation log) if it exists + if [[ -f "${BUILD_LOG}" ]]; then + cp "${BUILD_LOG}" "/tmp/create-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null && build_log_copied=true + fi + + # Copy INSTALL_LOG from container + if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "/tmp/install-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null; then + install_log_copied=true + fi + + # Show available logs + echo "" + [[ "$build_log_copied" == true ]] && echo -e "${GN}✔${CL} Container creation log: ${BL}/tmp/create-lxc-${CTID}-${SESSION_ID}.log${CL}" + [[ "$install_log_copied" == true ]] && echo -e "${GN}✔${CL} Installation log: ${BL}/tmp/install-lxc-${CTID}-${SESSION_ID}.log${CL}" + fi + + # Dev mode: Keep container or open breakpoint shell + if [[ "${DEV_MODE_KEEP:-false}" == "true" ]]; then + msg_dev "Keep mode active - container ${CTID} preserved" + return 0 + elif [[ "${DEV_MODE_BREAKPOINT:-false}" == "true" ]]; then + msg_dev "Breakpoint mode - opening shell in container ${CTID}" + echo -e "${YW}Type 'exit' to return to host${CL}" + pct enter "$CTID" + echo "" + echo -en "${YW}Container ${CTID} still running. Remove now? (y/N): ${CL}" + if read -r response && [[ "$response" =~ ^[Yy]$ ]]; then + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + msg_ok "Container ${CTID} removed" + else + msg_dev "Container ${CTID} kept for debugging" + fi + exit $install_exit_code + fi + + # Prompt user for cleanup with 60s timeout (plain echo - no msg_info to avoid spinner) + echo "" + echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}" + + if read -t 60 -r response; then + if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then + # Remove container + echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}" + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" + elif [[ "$response" =~ ^[Nn]$ ]]; then + echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}" + + # Dev mode: Setup MOTD/SSH for debugging access to broken container + if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then + echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}" + if pct exec "$CTID" -- bash -c " + source <(curl -fsSL 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) + echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}" + fi + fi + fi + else + # Timeout - auto-remove + echo -e "\n${YW}No response - auto-removing container${CL}" + echo -e "${TAB}${HOLD}${YW}Removing container ${CTID}${CL}" + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" + fi + + exit $install_exit_code + fi +} + +destroy_lxc() { + if [[ -z "$CT_ID" ]]; then + msg_error "No CT_ID found. Nothing to remove." + return 1 + fi + + # Abbruch bei Ctrl-C / Ctrl-D / ESC + trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT + + local prompt + if ! read -rp "Remove this Container? " prompt; then + # read gibt != 0 zurück bei Ctrl-D/ESC + msg_error "Aborted input (Ctrl-D/ESC)" + return 130 + fi + + case "${prompt,,}" in + y | yes) + if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then + msg_ok "Removed Container $CT_ID" + else + msg_error "Failed to remove Container $CT_ID" + return 1 + fi + ;; + "" | n | no) + msg_custom "ℹ️" "${BL}" "Container was not removed." + ;; + *) + msg_warn "Invalid response. Container was not removed." + ;; + esac +} + +# ------------------------------------------------------------------------------ +# Storage discovery / selection helpers +# ------------------------------------------------------------------------------ +# ===== Storage discovery / selection helpers (ported from create_lxc.sh) ===== +resolve_storage_preselect() { + local class="$1" preselect="$2" required_content="" + case "$class" in + template) required_content="vztmpl" ;; + container) required_content="rootdir" ;; + *) return 1 ;; + esac + [[ -z "$preselect" ]] && return 1 + if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then + msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)" + return 1 + fi + + local line total used free + line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')" + if [[ -z "$line" ]]; then + STORAGE_INFO="n/a" + else + total="$(awk '{print $4}' <<<"$line")" + used="$(awk '{print $5}' <<<"$line")" + free="$(awk '{print $6}' <<<"$line")" + local total_h used_h free_h + if command -v numfmt >/dev/null 2>&1; then + total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")" + used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")" + free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")" + STORAGE_INFO="Free: ${free_h} Used: ${used_h}" + else + STORAGE_INFO="Free: ${free} Used: ${used}" + fi + fi + STORAGE_RESULT="$preselect" + return 0 +} + +fix_gpu_gids() { + if [[ -z "${GPU_TYPE:-}" ]]; then + return 0 + fi + + msg_info "Detecting and setting correct GPU group IDs" + + # Get actual GIDs from container + local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") + local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") + + # Create groups if they don't exist + if [[ -z "$video_gid" ]]; then + pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" >/dev/null 2>&1 + video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") + [[ -z "$video_gid" ]] && video_gid="44" + fi + + if [[ -z "$render_gid" ]]; then + pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" >/dev/null 2>&1 + render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") + [[ -z "$render_gid" ]] && render_gid="104" + fi + + # Stop container to update config + pct stop "$CTID" >/dev/null 2>&1 + sleep 1 + + # Update dev entries with correct GIDs + sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid=${render_gid}|g" "$LXC_CONFIG" + sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid=${video_gid}|g" "$LXC_CONFIG" + + # Restart container + pct start "$CTID" >/dev/null 2>&1 + sleep 2 + + msg_ok "GPU passthrough configured (video:${video_gid}, render:${render_gid})" + + # For privileged containers: also fix permissions inside container + if [[ "$CT_TYPE" == "0" ]]; then + pct exec "$CTID" -- bash -c " + if [ -d /dev/dri ]; then + for dev in /dev/dri/*; do + if [ -e \"\$dev\" ]; then + if [[ \"\$dev\" =~ renderD ]]; then + chgrp ${render_gid} \"\$dev\" 2>/dev/null || true + else + chgrp ${video_gid} \"\$dev\" 2>/dev/null || true + fi + chmod 660 \"\$dev\" 2>/dev/null || true + fi + done + fi + " >/dev/null 2>&1 + fi +} + +check_storage_support() { + local CONTENT="$1" VALID=0 + while IFS= read -r line; do + local STORAGE_NAME + STORAGE_NAME=$(awk '{print $1}' <<<"$line") + [[ -n "$STORAGE_NAME" ]] && VALID=1 + done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1') + [[ $VALID -eq 1 ]] +} + +select_storage() { + local CLASS=$1 CONTENT CONTENT_LABEL + case $CLASS in + container) + CONTENT='rootdir' + CONTENT_LABEL='Container' + ;; + template) + CONTENT='vztmpl' + CONTENT_LABEL='Container template' + ;; + iso) + CONTENT='iso' + CONTENT_LABEL='ISO image' + ;; + images) + CONTENT='images' + CONTENT_LABEL='VM Disk image' + ;; + backup) + CONTENT='backup' + CONTENT_LABEL='Backup' + ;; + snippets) + CONTENT='snippets' + CONTENT_LABEL='Snippets' + ;; + *) + msg_error "Invalid storage class '$CLASS'" + return 1 + ;; + esac + + declare -A STORAGE_MAP + local -a MENU=() + local COL_WIDTH=0 + + while read -r TAG TYPE _ TOTAL USED FREE _; do + [[ -n "$TAG" && -n "$TYPE" ]] || continue + local DISPLAY="${TAG} (${TYPE})" + local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED") + local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE") + local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B" + STORAGE_MAP["$DISPLAY"]="$TAG" + MENU+=("$DISPLAY" "$INFO" "OFF") + ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY} + done < <(pvesm status -content "$CONTENT" | awk 'NR>1') + + if [[ ${#MENU[@]} -eq 0 ]]; then + msg_error "No storage found for content type '$CONTENT'." + return 2 + fi + + if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then + STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}" + STORAGE_INFO="${MENU[1]}" + return 0 + fi + + local WIDTH=$((COL_WIDTH + 42)) + while true; do + local DISPLAY_SELECTED + DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "Storage Pools" \ + --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \ + 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; } + + DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED") + if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then + whiptail --msgbox "No valid storage selected. Please try again." 8 58 + continue + fi + STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}" + for ((i = 0; i < ${#MENU[@]}; i += 3)); do + if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then + STORAGE_INFO="${MENU[$i + 1]}" + break + fi + done + return 0 + done +} + +create_lxc_container() { + # ------------------------------------------------------------------------------ + # Optional verbose mode (debug tracing) + # ------------------------------------------------------------------------------ + if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi + + # ------------------------------------------------------------------------------ + # Helpers (dynamic versioning / template parsing) + # ------------------------------------------------------------------------------ + pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; } + pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; } + + ver_ge() { dpkg --compare-versions "$1" ge "$2"; } + ver_gt() { dpkg --compare-versions "$1" gt "$2"; } + ver_lt() { dpkg --compare-versions "$1" lt "$2"; } + + # Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1" + parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; } + + # Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create + # Returns: + # 0 = no upgrade needed + # 1 = upgraded (and if do_retry=yes and retry succeeded, creation done) + # 2 = user declined + # 3 = upgrade attempted but failed OR retry failed + offer_lxc_stack_upgrade_and_maybe_retry() { + local do_retry="${1:-no}" # yes|no + local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0 + + _pvec_i="$(pkg_ver pve-container)" + _lxcp_i="$(pkg_ver lxc-pve)" + _pvec_c="$(pkg_cand pve-container)" + _lxcp_c="$(pkg_cand lxc-pve)" + + if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then + ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1 + fi + if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then + ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1 + fi + if [[ $need -eq 0 ]]; then + msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)" + return 0 + fi + + echo + echo "An update for the Proxmox LXC stack is available:" + echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}" + echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}" + echo + read -rp "Do you want to upgrade now? [y/N] " _ans + case "${_ans,,}" in + y | yes) + msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)" + if $STD apt-get update && $STD apt-get install -y --only-upgrade pve-container lxc-pve; then + msg_ok "LXC stack upgraded." + if [[ "$do_retry" == "yes" ]]; then + msg_info "Retrying container creation after upgrade" + if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then + msg_ok "Container created successfully after upgrade." + return 0 + else + msg_error "pct create still failed after upgrade. See $LOGFILE" + return 3 + fi + fi + return 1 + else + msg_error "Upgrade failed. Please check APT output." + return 3 + fi + ;; + *) return 2 ;; + esac + } + + # ------------------------------------------------------------------------------ + # Required input variables + # ------------------------------------------------------------------------------ + [[ "${CTID:-}" ]] || { + msg_error "You need to set 'CTID' variable." + exit 203 + } + [[ "${PCT_OSTYPE:-}" ]] || { + msg_error "You need to set 'PCT_OSTYPE' variable." + exit 204 + } + + msg_debug "CTID=$CTID" + msg_debug "PCT_OSTYPE=$PCT_OSTYPE" + msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" + + # ID checks + [[ "$CTID" -ge 100 ]] || { + msg_error "ID cannot be less than 100." + exit 205 + } + if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then + echo -e "ID '$CTID' is already in use." + unset CTID + msg_error "Cannot use ID that is already in use." + exit 206 + fi + + # Storage capability check + check_storage_support "rootdir" || { + msg_error "No valid storage found for 'rootdir' [Container]" + exit 1 + } + check_storage_support "vztmpl" || { + msg_error "No valid storage found for 'vztmpl' [Template]" + exit 1 + } + + # Template storage selection + if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then + TEMPLATE_STORAGE="$STORAGE_RESULT" + TEMPLATE_STORAGE_INFO="$STORAGE_INFO" + msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" + else + while true; do + if [[ -z "${var_template_storage:-}" ]]; then + if select_storage template; then + TEMPLATE_STORAGE="$STORAGE_RESULT" + TEMPLATE_STORAGE_INFO="$STORAGE_INFO" + msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" + break + fi + fi + done + fi + + # Container storage selection + if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then + CONTAINER_STORAGE="$STORAGE_RESULT" + CONTAINER_STORAGE_INFO="$STORAGE_INFO" + msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" + else + if [[ -z "${var_container_storage:-}" ]]; then + if select_storage container; then + CONTAINER_STORAGE="$STORAGE_RESULT" + CONTAINER_STORAGE_INFO="$STORAGE_INFO" + msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" + fi + fi + fi + + # Validate content types + msg_info "Validating content types of storage '$CONTAINER_STORAGE'" + STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs) + msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT" + grep -qw "rootdir" <<<"$STORAGE_CONTENT" || { + msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC." + exit 217 + } + $STD msg_ok "Storage '$CONTAINER_STORAGE' supports 'rootdir'" + + msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'" + TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs) + msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT" + if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then + msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail." + else + $STD msg_ok "Template storage '$TEMPLATE_STORAGE' supports 'vztmpl'" + fi + + # Free space check + STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') + REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024)) + [[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || { + msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G." + exit 214 + } + + # Cluster quorum (if cluster) + if [[ -f /etc/pve/corosync.conf ]]; then + msg_info "Checking cluster quorum" + if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then + msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)." + exit 210 + fi + msg_ok "Cluster is quorate" + fi + + # ------------------------------------------------------------------------------ + # Template discovery & validation + # ------------------------------------------------------------------------------ + TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" + case "$PCT_OSTYPE" in + debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;; + alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;; + *) TEMPLATE_PATTERN="" ;; + esac + + msg_info "Searching for template '$TEMPLATE_SEARCH'" + + # Build regex patterns outside awk/grep for clarity + SEARCH_PATTERN="^${TEMPLATE_SEARCH}" + + #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" + #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" + #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" + + mapfile -t LOCAL_TEMPLATES < <( + pveam list "$TEMPLATE_STORAGE" 2>/dev/null | + awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | + sed 's|.*/||' | sort -t - -k 2 -V + ) + + pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." + + msg_ok "Template search completed" + + #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" + #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' + + set +u + mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) + #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" + set -u + + ONLINE_TEMPLATE="" + [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" + + #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" + #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" + if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then + #msg_debug "First 3 online templates:" + count=0 + for idx in "${!ONLINE_TEMPLATES[@]}"; do + #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" + ((count++)) + [[ $count -ge 3 ]] && break + done + fi + #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" + + if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then + TEMPLATE="${LOCAL_TEMPLATES[-1]}" + TEMPLATE_SOURCE="local" + else + TEMPLATE="$ONLINE_TEMPLATE" + TEMPLATE_SOURCE="online" + fi + + # If still no template, try to find alternatives + if [[ -z "$TEMPLATE" ]]; then + echo "" + echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." + + # Get all available versions for this OS type + mapfile -t AVAILABLE_VERSIONS < <( + pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk -F'\t' '{print $1}' | + grep "^${PCT_OSTYPE}-" | + sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | + sort -u -V 2>/dev/null + ) + + if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then + echo "" + echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" + for i in "${!AVAILABLE_VERSIONS[@]}"; do + echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" + done + echo "" + read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice + + if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then + PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" + TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" + SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" + + #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" + + mapfile -t ONLINE_TEMPLATES < <( + pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk -F'\t' '{print $1}' | + grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | + sort -t - -k 2 -V 2>/dev/null || true + ) + + if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then + TEMPLATE="${ONLINE_TEMPLATES[-1]}" + TEMPLATE_SOURCE="online" + #echo "[DEBUG] Found alternative: $TEMPLATE" + else + msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" + exit 225 + fi + else + msg_custom "🚫" "${YW}" "Installation cancelled" + exit 0 + fi + else + msg_error "No ${PCT_OSTYPE} templates available at all" + exit 225 + fi + fi + + #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" + #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" + + TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" + if [[ -z "$TEMPLATE_PATH" ]]; then + TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) + [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" + fi + + # If we still don't have a path but have a valid template name, construct it + if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then + TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" + fi + + [[ -n "$TEMPLATE_PATH" ]] || { + if [[ -z "$TEMPLATE" ]]; then + msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" + + # Get available versions + mapfile -t AVAILABLE_VERSIONS < <( + pveam available -section system 2>/dev/null | + grep "^${PCT_OSTYPE}-" | + sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | + grep -E '^[0-9]+\.[0-9]+$' | + sort -u -V 2>/dev/null || sort -u + ) + + if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then + echo -e "\n${BL}Available versions:${CL}" + for i in "${!AVAILABLE_VERSIONS[@]}"; do + echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" + done + + echo "" + read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice + + if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then + export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" + export PCT_OSVERSION="$var_version" + msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" + + # Retry template search with new version + TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" + SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" + + mapfile -t LOCAL_TEMPLATES < <( + pveam list "$TEMPLATE_STORAGE" 2>/dev/null | + awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | + sed 's|.*/||' | sort -t - -k 2 -V + ) + mapfile -t ONLINE_TEMPLATES < <( + pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk -F'\t' '{print $1}' | + grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | + sort -t - -k 2 -V 2>/dev/null || true + ) + ONLINE_TEMPLATE="" + [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" + + if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then + TEMPLATE="${LOCAL_TEMPLATES[-1]}" + TEMPLATE_SOURCE="local" + else + TEMPLATE="$ONLINE_TEMPLATE" + TEMPLATE_SOURCE="online" + fi + + TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" + if [[ -z "$TEMPLATE_PATH" ]]; then + TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) + [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" + fi + + # If we still don't have a path but have a valid template name, construct it + if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then + TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" + fi + + [[ -n "$TEMPLATE_PATH" ]] || { + msg_error "Template still not found after version change" + exit 220 + } + else + msg_custom "🚫" "${YW}" "Installation cancelled" + exit 1 + fi + else + msg_error "No ${PCT_OSTYPE} templates available" + exit 220 + fi + fi + } + + # Validate that we found a template + if [[ -z "$TEMPLATE" ]]; then + msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" + msg_custom "ℹ️" "${YW}" "Please check:" + msg_custom " •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)" + msg_custom " •" "${YW}" "Does the template exist for your OS version?" + exit 225 + fi + + msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" + msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" + + NEED_DOWNLOAD=0 + if [[ ! -f "$TEMPLATE_PATH" ]]; then + msg_info "Template not present locally – will download." + NEED_DOWNLOAD=1 + elif [[ ! -r "$TEMPLATE_PATH" ]]; then + msg_error "Template file exists but is not readable – check permissions." + exit 221 + elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then + if [[ -n "$ONLINE_TEMPLATE" ]]; then + msg_warn "Template file too small (<1MB) – re-downloading." + NEED_DOWNLOAD=1 + else + msg_warn "Template looks too small, but no online version exists. Keeping local file." + fi + elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then + if [[ -n "$ONLINE_TEMPLATE" ]]; then + msg_warn "Template appears corrupted – re-downloading." + NEED_DOWNLOAD=1 + else + msg_warn "Template appears corrupted, but no online version exists. Keeping local file." + fi + else + $STD msg_ok "Template $TEMPLATE is present and valid." + fi + + if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then + msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" + if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then + TEMPLATE="$ONLINE_TEMPLATE" + NEED_DOWNLOAD=1 + else + msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE" + fi + fi + + if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then + [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH" + for attempt in {1..3}; do + msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE" + if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then + msg_ok "Template download successful." + break + fi + if [[ $attempt -eq 3 ]]; then + msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE" + exit 222 + fi + sleep $((attempt * 5)) + done + fi + + if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then + msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download." + exit 223 + fi + + # ------------------------------------------------------------------------------ + # Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins) + # ------------------------------------------------------------------------------ + if [[ "$PCT_OSTYPE" == "debian" ]]; then + OSVER="$(parse_template_osver "$TEMPLATE")" + if [[ -n "$OSVER" ]]; then + # Proactive, aber ohne Abbruch – nur Angebot + offer_lxc_stack_upgrade_and_maybe_retry "no" || true + fi + fi + + # ------------------------------------------------------------------------------ + # Create LXC Container + # ------------------------------------------------------------------------------ + msg_info "Creating LXC container" + + # Ensure subuid/subgid entries exist + grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid + grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid + + # PCT_OPTIONS is now a string (exported from build_container) + # Add rootfs if not already specified + if [[ ! "$PCT_OPTIONS" =~ "-rootfs" ]]; then + PCT_OPTIONS="$PCT_OPTIONS + -rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}" + fi + + # Lock by template file (avoid concurrent downloads/creates) + lockfile="/tmp/template.${TEMPLATE}.lock" + exec 9>"$lockfile" || { + msg_error "Failed to create lock file '$lockfile'." + exit 200 + } + flock -w 60 9 || { + msg_error "Timeout while waiting for template lock." + exit 211 + } + + LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" + + # # DEBUG: Show the actual command that will be executed + # echo "[DEBUG] ===== PCT CREATE COMMAND DETAILS =====" + # echo "[DEBUG] CTID: $CTID" + # echo "[DEBUG] Template: ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" + # echo "[DEBUG] PCT_OPTIONS (will be word-split):" + # echo "$PCT_OPTIONS" | sed 's/^/ /' + # echo "[DEBUG] Full command line:" + # echo " pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" + # echo "[DEBUG] ========================================" + + msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" + msg_debug "Logfile: $LOGFILE" + + # First attempt (PCT_OPTIONS is a multi-line string, use it directly) + if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then + msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Validating template..." + + # Validate template file + if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then + msg_warn "Template file too small or missing – re-downloading." + rm -f "$TEMPLATE_PATH" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" + elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then + if [[ -n "$ONLINE_TEMPLATE" ]]; then + msg_warn "Template appears corrupted – re-downloading." + rm -f "$TEMPLATE_PATH" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" + else + msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." + fi + 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_info "Downloading template to local..." + pveam download local "$TEMPLATE" >/dev/null 2>&1 + 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 + echo + echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template." + echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically." + offer_lxc_stack_upgrade_and_maybe_retry "yes" + rc=$? + case $rc in + 0) : ;; # success - container created, continue + 2) + echo "Upgrade was declined. Please update and re-run: + apt update && apt install --only-upgrade pve-container lxc-pve" + exit 231 + ;; + 3) + echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" + 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 + 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 + echo + echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template." + echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically." + offer_lxc_stack_upgrade_and_maybe_retry "yes" + rc=$? + case $rc in + 0) : ;; # success - container created, continue + 2) + echo "Upgrade was declined. Please update and re-run: + apt update && apt install --only-upgrade pve-container lxc-pve" + exit 231 + ;; + 3) + echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" + 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 + exit 209 + fi + fi + else + msg_ok "Container successfully created after template repair." + fi + fi + + # Verify container exists + pct list | awk '{print $1}' | grep -qx "$CTID" || { + msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE" + exit 215 + } + + # Verify config rootfs + grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || { + msg_error "RootFS entry missing in container config. See $LOGFILE" + exit 216 + } + + msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created." + + # Report container creation to API + post_to_api } -# This function sets the description of the container. +# ============================================================================== +# SECTION 9: POST-INSTALLATION & FINALIZATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# description() +# +# - Sets container description with formatted HTML content +# - Includes: +# * Community-Scripts logo +# * Application name +# * Links to GitHub, Discussions, Issues +# * Ko-fi donation badge +# - Restarts ping-instances.service if present (monitoring) +# - Posts final "done" status to API telemetry +# ------------------------------------------------------------------------------ description() { IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) @@ -1396,8 +3731,6 @@ description() { EOF ) - - # Set Description in LXC pct set "$CTID" -description "$DESCRIPTION" if [[ -f /etc/systemd/system/ping-instances.service ]]; then @@ -1407,27 +3740,23 @@ EOF post_update_to_api "done" "none" } +# ============================================================================== +# SECTION 10: ERROR HANDLING & EXIT TRAPS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# api_exit_script() +# +# - Exit trap handler for reporting to API telemetry +# - Captures exit code and reports to API using centralized error descriptions +# - Uses explain_exit_code() from error_handler.func for consistent error messages +# - Posts failure status with exit code to API (error description added automatically) +# - Only executes on non-zero exit codes +# ------------------------------------------------------------------------------ api_exit_script() { - exit_code=$? # Capture the exit status of the last executed command - #200 exit codes indicate error in create_lxc.sh - #100 exit codes indicate error in install.func - + exit_code=$? if [ $exit_code -ne 0 ]; then - case $exit_code in - 100) post_update_to_api "failed" "100: Unexpected error in create_lxc.sh" ;; - 101) post_update_to_api "failed" "101: No network connection detected in create_lxc.sh" ;; - 200) post_update_to_api "failed" "200: LXC creation failed in create_lxc.sh" ;; - 201) post_update_to_api "failed" "201: Invalid Storage class in create_lxc.sh" ;; - 202) post_update_to_api "failed" "202: User aborted menu in create_lxc.sh" ;; - 203) post_update_to_api "failed" "203: CTID not set in create_lxc.sh" ;; - 204) post_update_to_api "failed" "204: PCT_OSTYPE not set in create_lxc.sh" ;; - 205) post_update_to_api "failed" "205: CTID cannot be less than 100 in create_lxc.sh" ;; - 206) post_update_to_api "failed" "206: CTID already in use in create_lxc.sh" ;; - 207) post_update_to_api "failed" "207: Template not found in create_lxc.sh" ;; - 208) post_update_to_api "failed" "208: Error downloading template in create_lxc.sh" ;; - 209) post_update_to_api "failed" "209: Container creation failed, but template is intact in create_lxc.sh" ;; - *) post_update_to_api "failed" "Unknown error, exit code: $exit_code in create_lxc.sh" ;; - esac + post_update_to_api "failed" "$exit_code" fi } From e049d5943e6308bddae59fda9e8109f14d016cc3 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:33:37 +0100 Subject: [PATCH 10/19] Enhance IPv6 configuration menu options Updated IPv6 Address Management menu options for clarity and added a new option for fully disabling IPv6. --- misc/build.func | 122 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 6 deletions(-) diff --git a/misc/build.func b/misc/build.func index 9a76779d670..23f09781c75 100644 --- a/misc/build.func +++ b/misc/build.func @@ -1,7 +1,9 @@ + #!/usr/bin/env bash # Copyright (c) 2021-2025 community-scripts ORG # Author: tteck (tteckster) | MickLesk | michelroegl-brunner # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE + # ============================================================================== # BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION @@ -80,8 +82,110 @@ variables() { fi } - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + if command -v curl >/dev/null 2>&1; then source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) @@ -1342,11 +1446,12 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "IPv6 CONFIGURATION" \ --ok-button "Next" --cancel-button "Back" \ - --menu "\nSelect IPv6 Address Management:" 16 58 4 \ - "auto" "SLAAC/AUTO (recommended)" \ - "dhcp" "DHCPv6" \ - "static" "Static (manual entry)" \ - "none" "Disabled" \ + --menu "\nSelect IPv6 Address Management:" 16 70 5 \ + "auto" "SLAAC/AUTO (recommended) - Dynamic IPv6 from network" \ + "dhcp" "DHCPv6 - DHCP-assigned IPv6 address" \ + "static" "Static - Manual IPv6 address configuration" \ + "none" "None - No IPv6 assignment (most containers)" \ + "disable" "Fully Disabled - (breaks some services)" \ 3>&1 1>&2 2>&3); then _ipv6_method="$result" @@ -1375,6 +1480,11 @@ advanced_settings() { _ipv6_gate="" ((STEP++)) ;; + + + + + none) _ipv6_addr="none" _ipv6_gate="" From 3d1aa7e8f355712515817d94dcc0d009c1cd8309 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:07:42 +0100 Subject: [PATCH 11/19] Update default Node.js version to 24 LTS --- misc/tools.func | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misc/tools.func b/misc/tools.func index 4bf173cd5a4..b615bb8a961 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -3418,12 +3418,12 @@ EOF # - Optionally installs or updates global npm modules # # Variables: -# NODE_VERSION - Node.js version to install (default: 22) +# NODE_VERSION - Node.js version to install (default: 24 LTS) # NODE_MODULE - Comma-separated list of global modules (e.g. "yarn,@vue/cli@5.0.0") # ------------------------------------------------------------------------------ function setup_nodejs() { - local NODE_VERSION="${NODE_VERSION:-22}" + local NODE_VERSION="${NODE_VERSION:-24}" local NODE_MODULE="${NODE_MODULE:-}" # ALWAYS clean up legacy installations first (nvm, etc.) to prevent conflicts From bc1eb41a004490108028db87b182ad3e0df8a543 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:57:11 +0100 Subject: [PATCH 12/19] Update misc/alpine-tools.func Co-authored-by: Michel Roegl-Brunner <73236783+michelroegl-brunner@users.noreply.github.com> --- misc/alpine-tools.func | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/alpine-tools.func b/misc/alpine-tools.func index 663b5f1d31b..76fa7b6cba6 100644 --- a/misc/alpine-tools.func +++ b/misc/alpine-tools.func @@ -112,7 +112,7 @@ check_for_gh_release() { } # ------------------------------ -# GitHub: get Release & deployen (Alpine) +# GitHub: get Release & deploy (Alpine) # modes: tarball | prebuild | singlefile # ------------------------------ fetch_and_deploy_gh() { From f468ee848202b8fcca748086c84f7bc3fd96951f Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:35:44 +0100 Subject: [PATCH 13/19] indention --- misc/alpine-install.func | 2 +- misc/build.func | 111 +-------------------------------------- misc/tools.func | 72 ++----------------------- 3 files changed, 6 insertions(+), 179 deletions(-) diff --git a/misc/alpine-install.func b/misc/alpine-install.func index 4a8282e9e08..75391136fff 100644 --- a/misc/alpine-install.func +++ b/misc/alpine-install.func @@ -190,5 +190,5 @@ EOF echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update chmod +x /usr/bin/update - + } diff --git a/misc/build.func b/misc/build.func index 23f09781c75..bdce254e455 100644 --- a/misc/build.func +++ b/misc/build.func @@ -1,9 +1,7 @@ - #!/usr/bin/env bash # Copyright (c) 2021-2025 community-scripts ORG # Author: tteck (tteckster) | MickLesk | michelroegl-brunner # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE - # ============================================================================== # BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION @@ -82,110 +80,7 @@ variables() { fi } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if command -v curl >/dev/null 2>&1; then source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) @@ -1480,11 +1375,7 @@ advanced_settings() { _ipv6_gate="" ((STEP++)) ;; - - - - - + none) _ipv6_addr="none" _ipv6_gate="" diff --git a/misc/tools.func b/misc/tools.func index b615bb8a961..6fcccdca307 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -73,22 +73,18 @@ stop_all_services() { for pattern in "${service_patterns[@]}"; do # Find all matching services - + systemctl list-units --type=service --all 2>/dev/null | grep -oE "${pattern}[^ ]*\.service" | sort -u | - - - while read -r service; do - + $STD systemctl stop "$service" 2>/dev/null || true $STD systemctl disable "$service" 2>/dev/null || true done - + done - } # ------------------------------------------------------------------------------ @@ -3870,74 +3866,14 @@ function setup_postgresql() { local SUITE case "$DISTRO_CODENAME" in trixie | forky | sid) - + if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then SUITE="trixie-pgdg" - - - - - - - - - - - else SUITE="bookworm-pgdg" fi - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; *) SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt") From b670426cab3747c0321fb16de501ab3f780cce8f Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:39:48 +0100 Subject: [PATCH 14/19] remove debugf and duplicate codes --- misc/alpine-tools.func | 2 +- misc/build.func | 21 +++++---------------- misc/error_handler.func | 24 ------------------------ 3 files changed, 6 insertions(+), 41 deletions(-) diff --git a/misc/alpine-tools.func b/misc/alpine-tools.func index 76fa7b6cba6..d2efb8cdb84 100644 --- a/misc/alpine-tools.func +++ b/misc/alpine-tools.func @@ -1,7 +1,7 @@ #!/bin/ash # shellcheck shell=ash -# Erwartet vorhandene msg_* und optional $STD aus deinem Framework. +# Expects existing msg_* functions and optional $STD from the framework. # ------------------------------ # helpers diff --git a/misc/build.func b/misc/build.func index bdce254e455..dad3889d52c 100644 --- a/misc/build.func +++ b/misc/build.func @@ -87,7 +87,6 @@ if command -v curl >/dev/null 2>&1; then source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) load_functions catch_errors - #echo "(build.func) Loaded core.func via curl" elif command -v wget >/dev/null 2>&1; then source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) @@ -2242,7 +2241,7 @@ build_container() { esac fi - # IP (immer zwingend, Standard dhcp) + # IP (always required, default dhcp) NET_STRING+=",ip=${NET:-dhcp}" # Gateway @@ -2834,12 +2833,12 @@ destroy_lxc() { return 1 fi - # Abbruch bei Ctrl-C / Ctrl-D / ESC + # Abort on Ctrl-C / Ctrl-D / ESC trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT local prompt if ! read -rp "Remove this Container? " prompt; then - # read gibt != 0 zurück bei Ctrl-D/ESC + # read returns non-zero on Ctrl-D/ESC msg_error "Aborted input (Ctrl-D/ESC)" return 130 fi @@ -3555,16 +3554,6 @@ create_lxc_container() { LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" - # # DEBUG: Show the actual command that will be executed - # echo "[DEBUG] ===== PCT CREATE COMMAND DETAILS =====" - # echo "[DEBUG] CTID: $CTID" - # echo "[DEBUG] Template: ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" - # echo "[DEBUG] PCT_OPTIONS (will be word-split):" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Full command line:" - # echo " pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" - # echo "[DEBUG] ========================================" - msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" msg_debug "Logfile: $LOGFILE" @@ -3609,7 +3598,7 @@ create_lxc_container() { 0) : ;; # success - container created, continue 2) echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" + apt update && apt install --only-upgrade pve-container lxc-pve" exit 231 ;; 3) @@ -3641,7 +3630,7 @@ create_lxc_container() { 0) : ;; # success - container created, continue 2) echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" + apt update && apt install --only-upgrade pve-container lxc-pve" exit 231 ;; 3) diff --git a/misc/error_handler.func b/misc/error_handler.func index 5418a490eec..fb34b8e0650 100644 --- a/misc/error_handler.func +++ b/misc/error_handler.func @@ -90,12 +90,6 @@ explain_exit_code() { 253) echo "MongoDB: Database not found" ;; 254) echo "MongoDB: Fatal query error" ;; - # --- Proxmox Custom Codes --- - 200) echo "Custom: Failed to create lock file" ;; - 203) echo "Custom: Missing CTID variable" ;; - 204) echo "Custom: Missing PCT_OSTYPE variable" ;; - 205) echo "Custom: Invalid CTID (<100)" ;; - 206) echo "Custom: CTID already in use (check 'pct list' and /etc/pve/lxc/)" ;; # --- Proxmox Custom Codes --- 200) echo "Custom: Failed to create lock file" ;; 203) echo "Custom: Missing CTID variable" ;; @@ -119,24 +113,6 @@ explain_exit_code() { 225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;; 231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;; - # --- Default --- - *) echo "Unknown error" ;; - 208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;; - 209) echo "Custom: Container creation failed (check logs for pct create output)" ;; - 210) echo "Custom: Cluster not quorate" ;; - 211) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;; - 214) echo "Custom: Not enough storage space" ;; - 215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;; - 216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;; - 217) echo "Custom: Storage does not support rootdir (check storage capabilities)" ;; - 218) echo "Custom: Template file corrupted or incomplete download (size <1MB or invalid archive)" ;; - 220) echo "Custom: Unable to resolve template path" ;; - 221) echo "Custom: Template file exists but not readable (check file permissions)" ;; - 222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;; - 223) echo "Custom: Template not available after download (storage sync issue)" ;; - 225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;; - 231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;; - # --- Default --- *) echo "Unknown error" ;; esac From ab65711e765f55f3d9adf5a20fb18f544ad36066 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:44:01 +0100 Subject: [PATCH 15/19] Update whiptail backtitles and error codes Removed '[dev]' from whiptail --backtitle strings for consistency. Refactored custom exit codes in build.func and error_handler.func: updated Proxmox error codes, shifted MySQL/MariaDB codes to 260-263, and removed unused MongoDB code. Updated error descriptions to match new codes. --- misc/build.func | 46 ++++++++++++++++++++--------------------- misc/error_handler.func | 21 +++++++++---------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/misc/build.func b/misc/build.func index dad3889d52c..c0728e7d883 100644 --- a/misc/build.func +++ b/misc/build.func @@ -903,7 +903,7 @@ maybe_offer_save_app_defaults() { # 1) if no file → offer to create if [[ ! -f "$app_vars_path" ]]; then - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + if whiptail --backtitle "Proxmox VE Helper Scripts" \ --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then mkdir -p "$(dirname "$app_vars_path")" install -m 0644 "$new_tmp" "$app_vars_path" @@ -928,7 +928,7 @@ maybe_offer_save_app_defaults() { while true; do local sel - sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + sel="$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "APP DEFAULTS – ${APP}" \ --menu "Differences detected. What do you want to do?" 20 78 10 \ "Update Defaults" "Write new values to ${app_vars_file}" \ @@ -949,7 +949,7 @@ maybe_offer_save_app_defaults() { break ;; "View Diff") - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "Diff – ${APP}" \ --scrolltext --textbox "$diff_tmp" 25 100 ;; @@ -1639,7 +1639,7 @@ diagnostics_check() { fi if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then - if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then cat </usr/local/community-scripts/diagnostics DIAGNOSTICS=yes @@ -1698,7 +1698,7 @@ EOF diagnostics_menu() { if [ "${DIAGNOSTICS:-no}" = "yes" ]; then - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + if whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "DIAGNOSTIC SETTINGS" \ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ --yes-button "No" --no-button "Back"; then @@ -1707,7 +1707,7 @@ diagnostics_menu() { whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 fi else - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + if whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "DIAGNOSTIC SETTINGS" \ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ --yes-button "Yes" --no-button "Back"; then @@ -2086,14 +2086,14 @@ configure_ssh_settings() { local ssh_key_mode if [[ "$default_key_count" -gt 0 ]]; then - ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ + ssh_key_mode=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ "Provision SSH keys for root:" 14 72 4 \ "found" "Select from detected keys (${default_key_count})" \ "manual" "Paste a single public key" \ "folder" "Scan another folder (path or glob)" \ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script else - ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ + ssh_key_mode=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ "No host keys detected; choose manual/none:" 12 72 2 \ "manual" "Paste a single public key" \ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script @@ -2102,7 +2102,7 @@ configure_ssh_settings() { case "$ssh_key_mode" in found) local selection - selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \ + selection=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \ --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script for tag in $selection; do tag="${tag%\"}" @@ -2113,13 +2113,13 @@ configure_ssh_settings() { done ;; manual) - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + SSH_AUTHORIZED_KEY="$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)" [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE" ;; folder) local glob_path - glob_path=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + glob_path=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3) if [[ -n "$glob_path" ]]; then shopt -s nullglob @@ -2129,7 +2129,7 @@ configure_ssh_settings() { ssh_build_choices_from_files "${_scan_files[@]}" if [[ "$COUNT" -gt 0 ]]; then local folder_selection - folder_selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \ + folder_selection=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \ --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script for tag in $folder_selection; do tag="${tag%\"}" @@ -2139,10 +2139,10 @@ configure_ssh_settings() { [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" done else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60 + whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60 fi else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60 + whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60 fi fi ;; @@ -2157,7 +2157,7 @@ configure_ssh_settings() { fi if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then - if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then + if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then SSH="yes" else SSH="no" @@ -2185,7 +2185,7 @@ start() { set_std_mode update_script else - CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ + CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ "Support/Update functions for ${APP} LXC. Choose an option:" \ 12 60 3 \ "1" "YES (Silent Mode)" \ @@ -3029,7 +3029,7 @@ select_storage() { local WIDTH=$((COL_WIDTH + 42)) while true; do local DISPLAY_SELECTED - DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + DISPLAY_SELECTED=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "Storage Pools" \ --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \ 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; } @@ -3229,7 +3229,7 @@ create_lxc_container() { msg_info "Checking cluster quorum" if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)." - exit 210 + exit 201 fi msg_ok "Cluster is quorate" fi @@ -3549,7 +3549,7 @@ create_lxc_container() { } flock -w 60 9 || { msg_error "Timeout while waiting for template lock." - exit 211 + exit 202 } LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" @@ -3599,11 +3599,11 @@ create_lxc_container() { 2) echo "Upgrade was declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 + exit 213 ;; 3) echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 + exit 213 ;; esac else @@ -3631,11 +3631,11 @@ create_lxc_container() { 2) echo "Upgrade was declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 + exit 213 ;; 3) echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 + exit 213 ;; esac else diff --git a/misc/error_handler.func b/misc/error_handler.func index fb34b8e0650..78e178fb072 100644 --- a/misc/error_handler.func +++ b/misc/error_handler.func @@ -34,9 +34,9 @@ # * Node.js/npm errors (243-249, 254) # * Python/pip/uv errors (210-212) # * PostgreSQL errors (231-234) -# * MySQL/MariaDB errors (241-244) -# * MongoDB errors (251-254) -# * Proxmox custom codes (200-231) +# * MySQL/MariaDB errors (260-263) +# * MongoDB errors (251-253) +# * Proxmox custom codes (200-209, 213-223, 225) # - Returns description string for given exit code # ------------------------------------------------------------------------------ explain_exit_code() { @@ -79,19 +79,20 @@ explain_exit_code() { 234) echo "PostgreSQL: Fatal error in query / syntax" ;; # --- MySQL / MariaDB --- - 241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;; - 242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;; - 243) echo "MySQL/MariaDB: Database does not exist" ;; - 244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;; + 260) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;; + 261) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;; + 262) echo "MySQL/MariaDB: Database does not exist" ;; + 263) echo "MySQL/MariaDB: Fatal error in query / syntax" ;; # --- MongoDB --- 251) echo "MongoDB: Connection failed (server not running)" ;; 252) echo "MongoDB: Authentication failed (bad user/password)" ;; 253) echo "MongoDB: Database not found" ;; - 254) echo "MongoDB: Fatal query error" ;; # --- Proxmox Custom Codes --- 200) echo "Custom: Failed to create lock file" ;; + 201) echo "Custom: Cluster not quorate" ;; + 202) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;; 203) echo "Custom: Missing CTID variable" ;; 204) echo "Custom: Missing PCT_OSTYPE variable" ;; 205) echo "Custom: Invalid CTID (<100)" ;; @@ -99,8 +100,7 @@ explain_exit_code() { 207) echo "Custom: Password contains unescaped special characters (-, /, \\, *, etc.)" ;; 208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;; 209) echo "Custom: Container creation failed (check logs for pct create output)" ;; - 210) echo "Custom: Cluster not quorate" ;; - 211) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;; + 213) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;; 214) echo "Custom: Not enough storage space" ;; 215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;; 216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;; @@ -111,7 +111,6 @@ explain_exit_code() { 222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;; 223) echo "Custom: Template not available after download (storage sync issue)" ;; 225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;; - 231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;; # --- Default --- *) echo "Unknown error" ;; From 7bc0722c6ea2fdb65bea20b54cfbf5e11a41c05b Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:45:52 +0100 Subject: [PATCH 16/19] comments --- misc/build.func | 6 +++--- misc/core.func | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/misc/build.func b/misc/build.func index c0728e7d883..9f675344948 100644 --- a/misc/build.func +++ b/misc/build.func @@ -2002,9 +2002,9 @@ ssh_extract_keys_from_file() { tr -d '\r' <"$f" | awk ' /^[[:space:]]*#/ {next} /^[[:space:]]*$/ {next} - # nackt: typ base64 [comment] + # bare format: type base64 [comment] /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next} - # mit Optionen: finde ab erstem Key-Typ + # with options: find from first key-type onward { match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/) if (RSTART>0) { print substr($0, RSTART) } @@ -3520,7 +3520,7 @@ create_lxc_container() { if [[ "$PCT_OSTYPE" == "debian" ]]; then OSVER="$(parse_template_osver "$TEMPLATE")" if [[ -n "$OSVER" ]]; then - # Proactive, aber ohne Abbruch – nur Angebot + # Proactive, but without abort – only offer offer_lxc_stack_upgrade_and_maybe_retry "no" || true fi fi diff --git a/misc/core.func b/misc/core.func index fc679d38dfe..fb5c0885645 100644 --- a/misc/core.func +++ b/misc/core.func @@ -683,7 +683,7 @@ exit_script() { # ------------------------------------------------------------------------------ get_header() { local app_name=$(echo "${APP,,}" | tr -d ' ') - local app_type=${APP_TYPE:-ct} # Default zu 'ct' falls nicht gesetzt + local app_type=${APP_TYPE:-ct} # Default to 'ct' if not set local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/${app_type}/headers/${app_name}" local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}" From 4371d6456738c071f8e239d2b8a30636de08fda5 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:51:46 +0100 Subject: [PATCH 17/19] Refactor error handling and clean up debug comments Standardized bash variable checks, removed unnecessary debug and commented code, and clarified error handling logic in container build and setup scripts. These changes improve code readability and maintainability without altering functional behavior. --- misc/alpine-install.func | 3 +-- misc/build.func | 45 +++------------------------------------- misc/install.func | 2 +- misc/tools.func | 9 ++------ 4 files changed, 7 insertions(+), 52 deletions(-) diff --git a/misc/alpine-install.func b/misc/alpine-install.func index 75391136fff..0791defccb8 100644 --- a/misc/alpine-install.func +++ b/misc/alpine-install.func @@ -15,7 +15,7 @@ catch_errors verb_ip6() { set_std_mode # Set STD mode based on VERBOSE - if [ "$IPV6_METHOD" == "disable" ]; then + if [ "${IPV6_METHOD:-}" = "disable" ]; then msg_info "Disabling IPv6 (this may affect some services)" $STD sysctl -w net.ipv6.conf.all.disable_ipv6=1 $STD sysctl -w net.ipv6.conf.default.disable_ipv6=1 @@ -42,7 +42,6 @@ error_handler() { local line_number="$2" local command="$3" - # Exitcode 0 = kein Fehler → ignorieren if [[ "$exit_code" -eq 0 ]]; then return 0 fi diff --git a/misc/build.func b/misc/build.func index 9f675344948..132e882849e 100644 --- a/misc/build.func +++ b/misc/build.func @@ -92,7 +92,6 @@ elif command -v wget >/dev/null 2>&1; then source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) load_functions catch_errors - #echo "(build.func) Loaded core.func via wget" fi # ============================================================================== @@ -2279,7 +2278,7 @@ build_container() { none) ;; esac - # Build FEATURES string (simple working version) + # Build FEATURES string if [ "$CT_TYPE" == "1" ]; then FEATURES="keyctl=1,nesting=1" else @@ -2290,9 +2289,7 @@ build_container() { FEATURES="$FEATURES,fuse=1" fi - # NEW IMPLEMENTATION (Fixed): Build PCT_OPTIONS properly - # Key insight: Bash cannot export arrays, so we build the options as a string - + # Build PCT_OPTIONS as string for export TEMP_DIR=$(mktemp -d) pushd "$TEMP_DIR" >/dev/null if [ "$var_os" == "alpine" ]; then @@ -2382,11 +2379,6 @@ build_container() { export TEMPLATE_STORAGE="${var_template_storage:-}" export CONTAINER_STORAGE="${var_container_storage:-}" - # # DEBUG: Show final PCT_OPTIONS being exported - # echo "[DEBUG] PCT_OPTIONS to be exported:" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Calling create_lxc_container..." - create_lxc_container || exit $? LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" @@ -2713,12 +2705,7 @@ EOF' install_ssh_keys_into_ct # Run application installer - # NOTE: We disable error handling here because: - # 1. Container errors are caught by error_handler INSIDE container - # 2. Container creates flag file with exit code - # 3. We read flag file and handle cleanup manually below - # 4. We DON'T want host error_handler to fire for lxc-attach command itself - + # Disable error trap - container errors are handled internally via flag file set +Eeuo pipefail # Disable ALL error handling temporarily trap - ERR # Remove ERR trap completely @@ -2864,7 +2851,6 @@ destroy_lxc() { # ------------------------------------------------------------------------------ # Storage discovery / selection helpers # ------------------------------------------------------------------------------ -# ===== Storage discovery / selection helpers (ported from create_lxc.sh) ===== resolve_storage_preselect() { local class="$1" preselect="$2" required_content="" case "$class" in @@ -3249,10 +3235,6 @@ create_lxc_container() { # Build regex patterns outside awk/grep for clarity SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" - mapfile -t LOCAL_TEMPLATES < <( pveam list "$TEMPLATE_STORAGE" 2>/dev/null | awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | @@ -3263,30 +3245,13 @@ create_lxc_container() { msg_ok "Template search completed" - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - set +u mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" set -u ONLINE_TEMPLATE="" [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then TEMPLATE="${LOCAL_TEMPLATES[-1]}" TEMPLATE_SOURCE="local" @@ -3337,7 +3302,6 @@ create_lxc_container() { if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then TEMPLATE="${ONLINE_TEMPLATES[-1]}" TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" else msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" exit 225 @@ -3352,9 +3316,6 @@ create_lxc_container() { fi fi - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" if [[ -z "$TEMPLATE_PATH" ]]; then TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) diff --git a/misc/install.func b/misc/install.func index ea4abd7d06f..1bdec5e7a69 100644 --- a/misc/install.func +++ b/misc/install.func @@ -51,7 +51,7 @@ catch_errors verb_ip6() { set_std_mode # Set STD mode based on VERBOSE - if [ "$IPV6_METHOD" == "disable" ]; then + if [ "${IPV6_METHOD:-}" = "disable" ]; then msg_info "Disabling IPv6 (this may affect some services)" mkdir -p /etc/sysctl.d $STD tee /etc/sysctl.d/99-disable-ipv6.conf >/dev/null </dev/null 2>&1; then msg_error "MongoDB binary not found after installation" return 1 @@ -3481,14 +3480,11 @@ function setup_nodejs() { return 1 } - # CRITICAL: Force APT cache refresh AFTER repository setup - # This ensures NodeSource is the only nodejs source in APT cache + # Force APT cache refresh after repository setup $STD apt update - # Install dependencies (NodeSource is now the only nodejs source) ensure_dependencies curl ca-certificates gnupg - # Install Node.js from NodeSource install_packages_with_retry "nodejs" || { msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" return 1 @@ -3639,7 +3635,7 @@ function setup_php() { local CURRENT_PHP="" CURRENT_PHP=$(is_tool_installed "php" 2>/dev/null) || true - # CRITICAL: If wrong version is installed, remove it FIRST before any pinning + # Remove conflicting PHP version before pinning if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then msg_info "Removing conflicting PHP ${CURRENT_PHP} (need ${PHP_VERSION})" stop_all_services "php.*-fpm" @@ -3786,7 +3782,6 @@ EOF local INSTALLED_VERSION=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2) - # Critical: if major.minor doesn't match, fail and cleanup if [[ "$INSTALLED_VERSION" != "$PHP_VERSION" ]]; then msg_error "PHP version mismatch: requested ${PHP_VERSION} but got ${INSTALLED_VERSION}" msg_error "This indicates a critical package installation issue" From 5dfb24876d58c2c68c853be910007e87472adf02 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:54:49 +0100 Subject: [PATCH 18/19] Update build.func --- misc/build.func | 2 -- 1 file changed, 2 deletions(-) diff --git a/misc/build.func b/misc/build.func index 132e882849e..ad7e52037f6 100644 --- a/misc/build.func +++ b/misc/build.func @@ -3289,8 +3289,6 @@ create_lxc_container() { TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" - mapfile -t ONLINE_TEMPLATES < <( pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | From c35102b0baa97b05b117f666d34ed922cfac8f17 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:12:43 +0100 Subject: [PATCH 19/19] feat: Improve LXC network checks and LINSTOR storage handling Enhanced LXC container network setup to check for both IPv4 and IPv6 addresses, added connectivity (ping) tests, and provided troubleshooting tips on failure. Updated storage validation to support LINSTOR, including cluster connectivity checks and special handling for LINSTOR template storage. --- misc/build.func | 56 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/misc/build.func b/misc/build.func index ad7e52037f6..c7748d046a0 100644 --- a/misc/build.func +++ b/misc/build.func @@ -2629,20 +2629,48 @@ EOF if [ "$var_os" != "alpine" ]; then msg_info "Waiting for network in LXC container" - # Wait for IP + # Wait for IP assignment (IPv4 or IPv6) + local ip_in_lxc="" for i in {1..20}; do - ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) + # 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 + if [ -z "$ip_in_lxc" ]; then + 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 done if [ -z "$ip_in_lxc" ]; then msg_error "No IP assigned to CT $CTID after 20s" + echo -e "${YW}Troubleshooting:${CL}" + echo " • Verify bridge ${BRG} exists and has connectivity" + echo " • Check if DHCP server is reachable (if using DHCP)" + echo " • Verify static IP configuration (if using static IP)" + echo " • Check Proxmox firewall rules" + echo " • If using Tailscale: Disable MagicDNS temporarily" exit 1 fi - # Simple connectivity check - just verify IP is assigned - msg_ok "Network configured (IP: $ip_in_lxc)" + # Verify basic connectivity (ping test) + local ping_success=false + for retry in {1..3}; do + if pct exec "$CTID" -- ping -c 1 -W 2 1.1.1.1 &>/dev/null || + pct exec "$CTID" -- ping -c 1 -W 2 8.8.8.8 &>/dev/null || + pct exec "$CTID" -- ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null; then + ping_success=true + break + fi + sleep 2 + done + + if [ "$ping_success" = false ]; then + msg_warn "Network configured (IP: $ip_in_lxc) but connectivity test failed" + echo -e "${YW}Container may have limited internet access. Installation will continue...${CL}" + else + msg_ok "Network in LXC is reachable (ping)" + fi fi # Function to get correct GID inside container get_container_gid() { @@ -3185,8 +3213,19 @@ create_lxc_container() { # Validate content types msg_info "Validating content types of storage '$CONTAINER_STORAGE'" - STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs) + STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm|linstor): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs) msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT" + + # Check storage type for special handling + STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1) + if [[ "$STORAGE_TYPE" == "linstor" ]]; then + msg_info "Detected LINSTOR storage - verifying cluster connectivity" + if ! pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null; then + msg_error "LINSTOR storage '$CONTAINER_STORAGE' not accessible. Check LINSTOR cluster health." + exit 217 + fi + fi + grep -qw "rootdir" <<<"$STORAGE_CONTENT" || { msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC." exit 217 @@ -3196,6 +3235,13 @@ create_lxc_container() { msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'" TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs) msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT" + + # Check if template storage is LINSTOR (may need special handling) + TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1) + if [[ "$TEMPLATE_TYPE" == "linstor" ]]; then + msg_info "Template storage uses LINSTOR - ensuring resource availability" + fi + if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail." else