diff --git a/misc/images/community-scripts.png b/.github/assets/community-scripts.png
similarity index 100%
rename from misc/images/community-scripts.png
rename to .github/assets/community-scripts.png
diff --git a/misc/images/community-scripts.svg b/.github/assets/community-scripts.svg
similarity index 100%
rename from misc/images/community-scripts.svg
rename to .github/assets/community-scripts.svg
diff --git a/misc/images/logo-81x112.png b/.github/assets/logo-81x112.png
similarity index 100%
rename from misc/images/logo-81x112.png
rename to .github/assets/logo-81x112.png
diff --git a/misc/images/logo-proxmoxve-name.svg b/.github/assets/logo-proxmoxve-name.svg
similarity index 100%
rename from misc/images/logo-proxmoxve-name.svg
rename to .github/assets/logo-proxmoxve-name.svg
diff --git a/misc/images/logo-proxmoxve.svg b/.github/assets/logo-proxmoxve.svg
similarity index 100%
rename from misc/images/logo-proxmoxve.svg
rename to .github/assets/logo-proxmoxve.svg
diff --git a/misc/images/logo.png b/.github/assets/logo.png
similarity index 100%
rename from misc/images/logo.png
rename to .github/assets/logo.png
diff --git a/misc/images/old_logo-81x112.png b/.github/assets/old_logo-81x112.png
similarity index 100%
rename from misc/images/old_logo-81x112.png
rename to .github/assets/old_logo-81x112.png
diff --git a/misc/images/old_logo.png b/.github/assets/old_logo.png
similarity index 100%
rename from misc/images/old_logo.png
rename to .github/assets/old_logo.png
diff --git a/.github/autolabeler-config.json b/.github/autolabeler-config.json
index d668595e6..39c7b919c 100644
--- a/.github/autolabeler-config.json
+++ b/.github/autolabeler-config.json
@@ -77,17 +77,6 @@
"excludeGlobs": []
}
],
- "core": [
- {
- "fileStatus": null,
- "includeGlobs": [
- "misc/*.func"
- ],
- "excludeGlobs": [
- "misc/api.func"
- ]
- }
- ],
"documentation": [
{
"fileStatus": null,
diff --git a/.github/workflows/delete-pocketbase-entry-on-removal.yml b/.github/workflows/delete-pocketbase-entry-on-removal.yml
index f3d42d104..07036fe14 100644
--- a/.github/workflows/delete-pocketbase-entry-on-removal.yml
+++ b/.github/workflows/delete-pocketbase-entry-on-removal.yml
@@ -267,15 +267,14 @@ jobs:
const stubPath = path.join('ct', slug + '.sh');
const content = [
"#!/usr/bin/env bash",
- "source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)",
+ "source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/core/main/core/core.func)",
"# Copyright (c) 2021-2026 community-scripts ORG",
"# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE",
"",
`APP="${appName.replace(/"/g, '\\"')}"`,
"",
- 'header_info "$APP"',
- 'variables',
'color',
+ 'header_info',
"",
'msg_error "This script is no longer available in community-scripts."',
`msg_error "${deletedMessage.replace(/"/g, '\\"')}"`,
diff --git a/.github/workflows/pr-test-command.yml b/.github/workflows/pr-test-command.yml
index 5cbeeb4c5..570ffecca 100644
--- a/.github/workflows/pr-test-command.yml
+++ b/.github/workflows/pr-test-command.yml
@@ -184,8 +184,8 @@ jobs:
);
}
lines.push(
- 'Only the script itself comes from this branch. Whatever it loads, the engine',
- 'or the `misc/` helpers, still comes from `main`.',
+ 'Only the script itself comes from this branch. The engine it loads still',
+ 'comes from community-scripts/core on `main`.',
);
}
diff --git a/.github/workflows/sync-to-incus.yml b/.github/workflows/sync-to-incus.yml
index 3565b34fb..d6c363bcf 100644
--- a/.github/workflows/sync-to-incus.yml
+++ b/.github/workflows/sync-to-incus.yml
@@ -5,8 +5,8 @@ name: Sync ct/install to Incus
#
# The actual mirroring + transformation lives in community-scripts/Incus
# (.github/workflows/sync-scripts.yml): it rewrites each script's bootstrap line
-# to load the engine from community-scripts/core (ProxmoxVE's misc/build.func is
-# a monolith with no Incus backend), so a plain 1:1 copy would break every Incus
+# to load the engine from community-scripts/core (ct scripts here load its PVE
+# backend; Incus needs the Incus one), so a plain 1:1 copy would break every Incus
# script. We therefore only fire a repository_dispatch and let that single
# source of truth do the work (and auto-approve + merge its own PR).
#
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 15b77c825..810a127cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
-

-
+

+
Community Scripts — Proxmox VE
One-command installations for services, containers and VMs on Proxmox VE
@@ -104,7 +104,7 @@ Virtual machine scripts need the same versions as containers.
| `vm/` | Virtual machine scripts |
| `tools/pve/` | Host tooling: post-install, hardware passthrough, maintenance |
| `turnkey/` | TurnKey Linux appliance helper |
-| `misc/` | Legacy engine copies — current scripts load [core](https://github.com/community-scripts/core) at runtime, so changes here never reach a running install |
+| `misc/` | One legacy engine file left, loaded only by `vm/owncloud-vm.sh` — everything else loads [core](https://github.com/community-scripts/core) at runtime |
Application metadata — name, description, logo, tags — is managed on the website,
not in this repository. Use the **Report Issue** link on any script page to
diff --git a/install/argus-install.sh b/install/argus-install.sh
index 06ab10a78..3d8ee4780 100644
--- a/install/argus-install.sh
+++ b/install/argus-install.sh
@@ -57,7 +57,7 @@ service:
url: community-scripts/ProxmoxVE
use_prerelease: false
dashboard:
- icon: https://raw.githubusercontent.com/community-scripts/ProxmoxVE/refs/heads/main/misc/images/logo.png
+ icon: https://raw.githubusercontent.com/community-scripts/ProxmoxVE/refs/heads/main/.github/assets/logo.png
icon_link_to: https://community-scripts.org/
web_url: https://github.com/community-scripts/ProxmoxVE/releases
EOF
diff --git a/misc/alpine-install.func b/misc/alpine-install.func
deleted file mode 100644
index 36de2315b..000000000
--- a/misc/alpine-install.func
+++ /dev/null
@@ -1,275 +0,0 @@
-# Copyright (c) 2021-2026 community-scripts ORG
-# Author: tteck (tteckster)
-# Co-Author: MickLesk
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-
-if ! command -v curl >/dev/null 2>&1; then
- apk update && apk add curl >/dev/null 2>&1
-fi
-# Mark container context BEFORE error handling starts: error_handler/on_exit
-# must write local failure artifacts instead of talking to the telemetry API
-# (the host is the single telemetry reporter).
-export TELEMETRY_CONTEXT="container"
-# A freshly booted container may not have DNS up yet. `source <(curl ...)` hides
-# curl's exit code, so a failed download would silently source an empty stream.
-_bootstrap_die() {
- if declare -f msg_error >/dev/null 2>&1; then
- msg_error "$1"
- else
- echo "FATAL: $1" >&2
- fi
- exit 115
-}
-
-_bootstrap_source() {
- local url="$1" probe="$2" content
- content=$(curl -fsSL --connect-timeout 10 --retry 5 --retry-connrefused --retry-delay 2 "$url") ||
- _bootstrap_die "Failed to download ${url##*/}"
- source /dev/stdin <<<"$content"
- declare -f "$probe" >/dev/null 2>&1 ||
- _bootstrap_die "${url##*/} loaded but incomplete (missing ${probe})"
-}
-
-_FUNC_BASE="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc"
-_bootstrap_source "$_FUNC_BASE/core.func" load_functions
-_bootstrap_source "$_FUNC_BASE/error_handler.func" catch_errors
-load_functions
-catch_errors
-
-# Persist diagnostics setting inside container (exported from build.func)
-# so addon scripts running later can find the user's choice
-if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then
- mkdir -p /usr/local/community-scripts
- echo "DIAGNOSTICS=${DIAGNOSTICS:-no}" >/usr/local/community-scripts/diagnostics
-fi
-
-# Get LXC IP address (must be called INSIDE container, after network is up)
-get_lxc_ip
-
-# ------------------------------------------------------------------------------
-# post_progress_to_api()
-#
-# - Lightweight progress ping from inside the container
-# - Updates the existing telemetry record status
-# - Arguments:
-# * $1: status (optional, default: "configuring")
-# - Signals that the installation is actively progressing (not stuck)
-# - Fire-and-forget: never blocks or fails the script
-# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
-# ------------------------------------------------------------------------------
-post_progress_to_api() {
- command -v curl &>/dev/null || return 0
- [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
- [[ -z "${RANDOM_UUID:-}" ]] && return 0
-
- local progress_status="${1:-configuring}"
-
- curl -fsS -m 5 -X POST "https://telemetry.community-scripts.org/telemetry" \
- -H "Content-Type: application/json" \
- -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"lxc\",\"nsapp\":\"${app:-unknown}\",\"status\":\"${progress_status}\",\"platform\":\"${TELEMETRY_PLATFORM:-}\",\"repo_source\":\"${REPO_SOURCE:-}\",\"repo_slug\":\"${REPO_SLUG:-}\"}" &>/dev/null || true
-}
-
-# This function enables IPv6 if it's not disabled and sets verbose mode
-verb_ip6() {
- set_std_mode # Set STD mode based on VERBOSE
-
- if [ "${IPV6_METHOD:-}" = "disable" ]; then
- msg_info "Disabling IPv6 (this may affect some services)"
- $STD sysctl -w net.ipv6.conf.all.disable_ipv6=1
- $STD sysctl -w net.ipv6.conf.default.disable_ipv6=1
- $STD sysctl -w net.ipv6.conf.lo.disable_ipv6=1
- mkdir -p /etc/sysctl.d
- $STD tee /etc/sysctl.d/99-disable-ipv6.conf >/dev/null <&2 -en "${CROSS}${RD} No Network! "
- sleep $RETRY_EVERY
- i=$((i - 1))
- done
-
- if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" = "" ]; then
- echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}"
- echo -e "${NETWORK}Check Network Settings"
- exit 121
- fi
- msg_ok "Set up Container OS"
- msg_ok "Network Connected: ${BL}$(ip addr show | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | tail -n1)${CL}"
- post_progress_to_api
-}
-
-# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected
-network_check() {
- set +e
- trap - ERR
- ipv4_connected=false
-
- # Check IPv4 connectivity to Cloudflare, Google & Quad9 DNS servers
- if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then
- msg_ok "IPv4 Internet Connected"
- ipv4_connected=true
- else
- msg_error "IPv4 Internet Not Connected"
- fi
-
- if [[ $ipv4_connected == false ]]; then
- read -r -p "No Internet detected, would you like to continue anyway? " prompt /dev/tcp/$m/80" 2>/dev/null; then
- msg_custom "${INFO}" "${YW}" "Attempting mirror: ${m}"
- cat </etc/apk/repositories
-http://$m/alpine/latest-stable/main
-http://$m/alpine/latest-stable/community
-EOF
- if $STD apk -U upgrade; then
- msg_ok "CDN set to ${m}: tests passed"
- apk_ok=true
- break
- else
- msg_warn "Mirror ${m} failed"
- fi
- fi
- done
- if [[ "$apk_ok" != true ]]; then
- msg_error "All Alpine mirrors failed. Check network or try again later."
- exit 1
- fi
- fi
- local tools_content
- tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) || {
- msg_error "Failed to download tools.func"
- exit 115
- }
- source /dev/stdin <<<"$tools_content"
- if ! declare -f fetch_and_deploy_gh_release >/dev/null 2>&1; then
- msg_error "tools.func loaded but incomplete — missing expected functions"
- exit 115
- fi
- msg_ok "Updated Container OS"
- post_progress_to_api
-}
-
-# This function modifies the message of the day (motd) and SSH settings
-motd_ssh() {
- echo "export TERM='xterm-256color'" >>/root/.bashrc
-
- PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
-
- echo "[ -t 1 ] || return 0" >"$PROFILE_FILE"
- echo "echo -e \"\"" >>"$PROFILE_FILE"
- echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE"
- echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE"
- echo "echo \"\"" >>"$PROFILE_FILE"
- cat <>"$PROFILE_FILE"
-os_display="Unknown OS"
-if [ -r /etc/os-release ]; then
- . /etc/os-release
- os_display="\${PRETTY_NAME:-\${NAME:-Unknown OS}}"
-fi
-echo -e "${TAB}${OS}${YW} OS: ${GN}\${os_display}${CL}"
-EOF
- echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
- echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(ip -4 addr show eth0 | awk '/inet / {print \$2}' | cut -d/ -f1 | head -n 1)${CL}\"" >>"$PROFILE_FILE"
-
- # Configure SSH if enabled
- if [[ "${SSH_ROOT}" == "yes" ]]; then
- # Enable sshd service
- $STD rc-update add sshd
- # Allow root login via SSH
- sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
- # Start the sshd service
- $STD /etc/init.d/sshd start
- fi
- post_progress_to_api
-}
-
-# Validate Timezone for some LXC's
-validate_tz() {
- [[ -f "/usr/share/zoneinfo/$1" ]]
-}
-
-# This function customizes the container and enables passwordless login for the root user
-customize() {
- if [[ "$PASSWORD" == "" ]]; then
- msg_info "Customizing Container"
- passwd -d root >/dev/null 2>&1
-
- # Ensure agetty is available
- apk add --no-cache --force-broken-world util-linux >/dev/null 2>&1
-
- # Create persistent autologin boot script
- mkdir -p /etc/local.d
- cat <<'EOF' >/etc/local.d/autologin.start
-#!/bin/sh
-sed -i 's|^tty1::respawn:.*|tty1::respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab
-kill -HUP 1
-EOF
- touch /root/.hushlogin
-
- chmod +x /etc/local.d/autologin.start
- rc-update add local >/dev/null 2>&1
-
- # Apply autologin immediately for current session
- /etc/local.d/autologin.start
-
- msg_ok "Customized Container"
- fi
-
- echo 'set -a; [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh; set +a; bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/'"${app}"'.sh)"' >/usr/bin/update
- chmod +x /usr/bin/update
- post_progress_to_api
-}
diff --git a/misc/alpine-tools.func b/misc/alpine-tools.func
deleted file mode 100644
index f5ba60368..000000000
--- a/misc/alpine-tools.func
+++ /dev/null
@@ -1,515 +0,0 @@
-#!/bin/ash
-# shellcheck shell=ash
-
-# Expects existing msg_* functions and optional $STD from the 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 100
- }
- 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() {
- # Login shells: /etc/profile.d/
- local PROFILE_FILE="/etc/profile.d/10-localbin.sh"
- if [ ! -f "$PROFILE_FILE" ]; then
- echo 'case ":$PATH:" in *:/usr/local/bin:*) ;; *) export PATH="/usr/local/bin:$PATH";; esac' >"$PROFILE_FILE"
- chmod +x "$PROFILE_FILE"
- fi
-
- # Non-login shells (pct enter): /root/.profile and /root/.bashrc
- for rc_file in /root/.profile /root/.bashrc; do
- if [ -f "$rc_file" ] && ! grep -q '/usr/local/bin' "$rc_file"; then
- echo 'export PATH="/usr/local/bin:$PATH"' >>"$rc_file"
- fi
- done
-}
-
-download_with_progress() {
- # $1 url, $2 dest
- local url="$1" out="$2" cl
- need_tool curl pv || return 127
- 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 250
- }
- else
- curl -fL# -o "$out" "$url" || {
- msg_error "Download failed: $url"
- return 250
- }
- 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 6
- }
- need_tool curl jq || return 127
-
- 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 22
- }
- 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 & deploy (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 6
- }
- need_tool curl jq tar || return 127
- [ "$mode" = "prebuild" ] || [ "$mode" = "singlefile" ] && need_tool unzip >/dev/null 2>&1 || true
-
- tmpd="$(mktemp -d)" || return 252
- 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 22
- }
- else
- json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/tags/$version")" || {
- msg_error "GitHub API failed"
- rm -rf "$tmpd"
- return 22
- }
- 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 65
- }
-
- 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 250
- }
- tar -xzf "$tmpd/$filename" -C "$tmpd" || {
- msg_error "tar extract failed"
- rm -rf "$tmpd"
- return 251
- }
- 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 252
- }
- ;;
- prebuild)
- [ -n "$pattern" ] || {
- msg_error "prebuild requires asset pattern"
- rm -rf "$tmpd"
- return 65
- }
- 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 250
- }
- filename="${url##*/}"
- download_with_progress "$url" "$tmpd/$filename" || {
- rm -rf "$tmpd"
- return 250
- }
- # unpack archive (Zip or tarball)
- case "$filename" in
- *.zip)
- need_tool unzip || {
- rm -rf "$tmpd"
- return 127
- }
- 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 251
- ;;
- 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 252
- }
- else
- (cd "$tmpd/unp" && tar -cf - .) | (cd "$target" && tar -xf -) || {
- msg_error "copy failed"
- rm -rf "$tmpd"
- return 252
- }
- fi
- ;;
- singlefile)
- [ -n "$pattern" ] || {
- msg_error "singlefile requires asset pattern"
- rm -rf "$tmpd"
- return 65
- }
- 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 250
- }
- filename="${url##*/}"
- download_with_progress "$url" "$target/$app" || {
- rm -rf "$tmpd"
- return 250
- }
- chmod +x "$target/$app"
- ;;
- *)
- msg_error "Unknown mode: $mode"
- rm -rf "$tmpd"
- return 65
- ;;
- 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 127
- 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 238
- ;;
- esac
- url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}"
- tmp="$(mktemp)"
- download_with_progress "$url" "$tmp" || return 250
- /usr/bin/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 127
- 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 250
- }
- 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 127
- 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 238
- ;;
- 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 250
- }
-
- 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 252
- 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 250
- }
- tar -xzf "$tmpd/uv.tar.gz" -C "$tmpd" || {
- msg_error "uv: extract failed"
- rm -rf "$tmpd"
- return 251
- }
-
- # tar contains ./uv
- if [ -x "$tmpd/uv" ]; then
- /usr/bin/install -m 0755 "$tmpd/uv" "$UV_BIN"
- else
- # fallback: in subfolder
- /usr/bin/install -m 0755 "$tmpd"/*/uv "$UV_BIN" 2>/dev/null || {
- msg_error "uv binary not found in tar"
- rm -rf "$tmpd"
- return 252
- }
- 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 250
- }
- 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 150
- }
- 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 100
- }
- # 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 100
- }
- msg_ok "Go ready: $(go version 2>/dev/null)"
- return 0
- fi
-
- need_tool curl tar || return 127
- 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 238
- ;;
- 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 250
- rm -rf /usr/local/go
- tar -C /usr/local -xzf "$TMP" || {
- msg_error "extract go failed"
- rm -f "$TMP"
- return 251
- }
- 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 100
- }
- }
- 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 127
- curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || {
- msg_error "composer installer download failed"
- return 250
- }
- php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 || {
- msg_error "composer install failed"
- return 150
- }
- rm -f /tmp/composer-setup.php
- ensure_usr_local_bin_persist
- msg_ok "Composer ready: $(composer --version 2>/dev/null)"
-}
diff --git a/misc/build.func b/misc/build.func
deleted file mode 100644
index 8aeed099c..000000000
--- a/misc/build.func
+++ /dev/null
@@ -1,7331 +0,0 @@
-#!/usr/bin/env bash
-# Copyright (c) 2021-2026 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.
- INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
- PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
- DIAGNOSTICS="no" # Safe default: no telemetry until user consents via diagnostics_check()
- METHOD="default" # sets the METHOD variable to "default", used for the API call.
- RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
- EXECUTION_ID="${RANDOM_UUID}" # Unique execution ID for telemetry record identification (unique-indexed in PocketBase)
- SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files
- BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log
- # NOTE: combined_log is constructed locally in build_container() and ensure_log_on_host()
- # as "/tmp/${NSAPP}-${CTID}-${SESSION_ID}.log" (requires CTID, not available here)
- CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
-
- # ARM64 Template default variables
- DEBIAN_DEFAULT_CODENAME="trixie"
- UBUNTU_DEFAULT_CODENAME="noble"
- ALPINE_DEFAULT_VERSION="3.23"
-
- # 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 ...)` hides the download exit code, so a failed fetch would
-# silently source an empty stream and leave the helpers undefined.
-_bootstrap_die() {
- if declare -f msg_error >/dev/null 2>&1; then
- msg_error "$1"
- else
- echo "FATAL: $1" >&2
- fi
- exit 115
-}
-
-_bootstrap_source() {
- local url="$1" probe="$2" content=""
- if command -v curl >/dev/null 2>&1; then
- content=$(curl -fsSL --connect-timeout 10 --retry 5 --retry-connrefused --retry-delay 2 "$url") || content=""
- elif command -v wget >/dev/null 2>&1; then
- content=$(wget -qO- --tries=5 --timeout=10 "$url") || content=""
- else
- _bootstrap_die "Neither curl nor wget available"
- fi
- [[ -n "$content" ]] || _bootstrap_die "Failed to download ${url##*/}"
- source /dev/stdin <<<"$content"
- declare -f "$probe" >/dev/null 2>&1 ||
- _bootstrap_die "${url##*/} loaded but incomplete (missing ${probe})"
-}
-
-_FUNC_BASE="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc"
-_bootstrap_source "$_FUNC_BASE/api.func" post_to_api
-_bootstrap_source "$_FUNC_BASE/core.func" load_functions
-_bootstrap_source "$_FUNC_BASE/error_handler.func" catch_errors
-load_functions
-catch_errors
-
-# ==============================================================================
-# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION
-# ==============================================================================
-
-# ------------------------------------------------------------------------------
-# 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
-# ------------------------------------------------------------------------------
-
-maxkeys_check() {
- # Read kernel parameters
- per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
- per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
-
- # Exit if kernel parameters are unavailable
- if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
- msg_error "Unable to read kernel key parameters. Ensure proper permissions."
- exit 107
- fi
-
- # Fetch key usage for user ID 100000 (typical for containers)
- used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
- used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
-
- # Calculate thresholds and suggested new limits
- threshold_keys=$((per_user_maxkeys - 100))
- threshold_bytes=$((per_user_maxbytes - 1000))
- new_limit_keys=$((per_user_maxkeys * 2))
- new_limit_bytes=$((per_user_maxbytes * 2))
-
- # Check if key or byte usage is near limits
- failure=0
- if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
- msg_warn "Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys})"
- echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
- failure=1
- fi
- if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
- msg_warn "Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes})"
- echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
- failure=1
- fi
-
- # Provide next steps if issues are detected
- if [[ "$failure" -eq 1 ]]; then
- msg_error "Kernel key limits exceeded - see suggestions above"
- exit 108
- fi
-
- # Silent success - only show errors if they exist
-}
-
-# ==============================================================================
-# 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` or `ip -6 addr`
-# - Supports IPv6-only environments as fallback
-# - Returns "Unknown" if OS type cannot be determined
-# ------------------------------------------------------------------------------
-get_current_ip() {
- CURRENT_IP=""
- if [ -f /etc/os-release ]; then
- # Check for Debian/Ubuntu (uses hostname -I)
- if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
- # Try IPv4 first
- CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)
- # Fallback to IPv6 if no IPv4
- if [[ -z "$CURRENT_IP" ]]; then
- CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1 || true)
- fi
- # Check for Alpine (uses ip command)
- elif grep -q 'ID=alpine' /etc/os-release; then
- # Try IPv4 first
- CURRENT_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
- # Fallback to IPv6 if no IPv4
- if [[ -z "$CURRENT_IP" ]]; then
- CURRENT_IP=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n 1)
- fi
- else
- CURRENT_IP="Unknown"
- fi
- fi
- echo "$CURRENT_IP"
-}
-
-# ------------------------------------------------------------------------------
-# update_motd_ip()
-#
-# - Updates /etc/motd with current container IP
-# - Removes old IP entries to avoid duplicates
-# - Regenerates /etc/profile.d/00_lxc-details.sh with dynamic OS/IP info
-# ------------------------------------------------------------------------------
-update_motd_ip() {
- MOTD_FILE="/etc/motd"
- PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
-
- if [ -f "$MOTD_FILE" ]; then
- # Remove existing IP Address lines to prevent duplication
- sed -i '/IP Address:/d' "$MOTD_FILE"
-
- IP=$(get_current_ip)
- # Add the new IP address
- echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
- fi
-
- # Update dynamic LXC details profile if values changed (e.g., after OS upgrade)
- # Only update if file exists and is from community-scripts
- if [ -f "$PROFILE_FILE" ] && grep -q "community-scripts" "$PROFILE_FILE" 2>/dev/null; then
- # Get current values
- local current_hostname="$(hostname)"
- local current_ip="$(hostname -I | awk '{print $1}')"
-
- # Escape sed special chars in replacement strings (& \ |)
- current_hostname="${current_hostname//\\/\\\\}"
- current_hostname="${current_hostname//&/\\&}"
- current_ip="${current_ip//\\/\\\\}"
- current_ip="${current_ip//&/\\&}"
-
- # Update only if values actually changed
- if ! grep -q "Hostname:.*$current_hostname" "$PROFILE_FILE" 2>/dev/null; then
- sed -i "s|Hostname:.*|Hostname: \${GN}$current_hostname\${CL}\\\"|" "$PROFILE_FILE"
- fi
- if ! grep -q "IP Address:.*$current_ip" "$PROFILE_FILE" 2>/dev/null; then
- sed -i "s|IP Address:.*|IP Address: \${GN}$current_ip\${CL}\\\"|" "$PROFILE_FILE"
- fi
- fi
-}
-
-# ------------------------------------------------------------------------------
-# 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
-# - Supports both SSH_KEYS_FILE (from advanced settings) and SSH_AUTHORIZED_KEY (from user defaults)
-# - Falls back to warning if no keys provided
-# ------------------------------------------------------------------------------
-install_ssh_keys_into_ct() {
- [[ "${SSH:-no}" != "yes" ]] && return 0
-
- # Ensure SSH_KEYS_FILE is defined (may not be set if advanced_settings was skipped)
- : "${SSH_KEYS_FILE:=}"
-
- # If SSH_KEYS_FILE doesn't exist but SSH_AUTHORIZED_KEY is set (from user defaults),
- # create a temporary SSH_KEYS_FILE with the key
- if [[ -z "$SSH_KEYS_FILE" || ! -s "$SSH_KEYS_FILE" ]] && [[ -n "${SSH_AUTHORIZED_KEY:-}" ]]; then
- SSH_KEYS_FILE="$(mktemp)"
- printf '%s\n' "$SSH_AUTHORIZED_KEY" >"$SSH_KEYS_FILE"
- fi
-
- 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 252
- }
- 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 252
- }
- 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
-}
-
-# ------------------------------------------------------------------------------
-# validate_container_id()
-#
-# - Validates if a container ID is available for use (CLUSTER-WIDE)
-# - Checks cluster resources via pvesh for VMs/CTs on ALL nodes
-# - Falls back to local config file check if pvesh unavailable
-# - Checks if ID is used in LVM logical volumes
-# - Returns 0 if ID is available, 1 if already in use
-# ------------------------------------------------------------------------------
-validate_container_id() {
- local ctid="$1"
-
- # Check if ID is numeric
- if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then
- return 1
- fi
-
- # CLUSTER-WIDE CHECK: Query all VMs/CTs across all nodes
- # This catches IDs used on other nodes in the cluster
- # NOTE: Works on single-node too - Proxmox always has internal cluster structure
- # Falls back gracefully if pvesh unavailable or returns empty
- if command -v pvesh &>/dev/null; then
- local cluster_ids
- cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
- grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true)
- if [[ -n "$cluster_ids" ]] && echo "$cluster_ids" | grep -qw "$ctid"; then
- return 1
- fi
- fi
-
- # LOCAL FALLBACK: Check if config file exists for VM or LXC
- # This handles edge cases where pvesh might not return all info
- if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then
- return 1
- fi
-
- # Check ALL nodes in cluster for config files (handles pmxcfs sync delays)
- # NOTE: On single-node, /etc/pve/nodes/ contains just the one node - still works
- if [[ -d "/etc/pve/nodes" ]]; then
- for node_dir in /etc/pve/nodes/*/; do
- if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then
- return 1
- fi
- done
- fi
-
- # Check if ID is used in LVM logical volumes
- if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then
- return 1
- fi
-
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# get_valid_container_id()
-#
-# - Returns a valid, unused container ID (CLUSTER-AWARE)
-# - Uses pvesh /cluster/nextid as starting point (already cluster-aware)
-# - If provided ID is valid, returns it
-# - Otherwise increments until a free one is found across entire cluster
-# - Calls validate_container_id() to check availability
-# ------------------------------------------------------------------------------
-get_valid_container_id() {
- local suggested_id="${1:-$(pvesh get /cluster/nextid 2>/dev/null || echo 100)}"
-
- # Ensure we have a valid starting ID
- if ! [[ "$suggested_id" =~ ^[0-9]+$ ]]; then
- suggested_id=$(pvesh get /cluster/nextid 2>/dev/null || echo 100)
- fi
-
- local max_attempts=1000
- local attempts=0
-
- while ! validate_container_id "$suggested_id"; do
- suggested_id=$((suggested_id + 1))
- attempts=$((attempts + 1))
- if [[ $attempts -ge $max_attempts ]]; then
- msg_error "Could not find available container ID after $max_attempts attempts"
- exit 109
- fi
- done
-
- echo "$suggested_id"
-}
-
-# ------------------------------------------------------------------------------
-# validate_hostname()
-#
-# - Validates hostname/FQDN according to RFC 1123/952
-# - Checks total length (max 253 characters for FQDN)
-# - Validates each label (max 63 chars, alphanumeric + hyphens)
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_hostname() {
- local hostname="$1"
-
- # Check total length (max 253 for FQDN)
- if [[ ${#hostname} -gt 253 ]] || [[ -z "$hostname" ]]; then
- return 1
- fi
-
- # Split by dots and validate each label
- local IFS='.'
- read -ra labels <<<"$hostname"
- for label in "${labels[@]}"; do
- # Each label: 1-63 chars, alphanumeric, hyphens allowed (not at start/end)
- if [[ -z "$label" ]] || [[ ${#label} -gt 63 ]]; then
- return 1
- fi
- if [[ ! "$label" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] && [[ ! "$label" =~ ^[a-z0-9]$ ]]; then
- return 1
- fi
- done
-
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# validate_mac_address()
-#
-# - Validates MAC address format (XX:XX:XX:XX:XX:XX)
-# - Empty value is allowed (auto-generated)
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_mac_address() {
- local mac="$1"
- [[ -z "$mac" ]] && return 0
- if [[ ! "$mac" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]]; then
- return 1
- fi
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# validate_vlan_tag()
-#
-# - Validates VLAN tag (1-4094)
-# - Empty value is allowed (no VLAN)
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_vlan_tag() {
- local vlan="$1"
- [[ -z "$vlan" ]] && return 0
- if ! [[ "$vlan" =~ ^[0-9]+$ ]] || ((vlan < 1 || vlan > 4094)); then
- return 1
- fi
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# validate_mtu()
-#
-# - Validates MTU size (576-65535, common values: 1500, 9000)
-# - Empty value is allowed (default 1500)
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_mtu() {
- local mtu="$1"
- [[ -z "$mtu" ]] && return 0
- if ! [[ "$mtu" =~ ^[0-9]+$ ]] || ((mtu < 576 || mtu > 65535)); then
- return 1
- fi
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# validate_ipv6_address()
-#
-# - Validates IPv6 address with optional CIDR notation
-# - Supports compressed (::) and full notation
-# - Empty value is allowed
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_ipv6_address() {
- local ipv6="$1"
- [[ -z "$ipv6" ]] && return 0
-
- # Extract address and CIDR
- local addr="${ipv6%%/*}"
- local cidr="${ipv6##*/}"
-
- # Validate CIDR if present (1-128)
- if [[ "$ipv6" == */* ]]; then
- if ! [[ "$cidr" =~ ^[0-9]+$ ]] || ((cidr < 1 || cidr > 128)); then
- return 1
- fi
- fi
-
- # Basic IPv6 validation - check for valid characters and structure
- # Must contain only hex digits and colons
- if [[ ! "$addr" =~ ^[0-9a-fA-F:]+$ ]]; then
- return 1
- fi
-
- # Must contain at least one colon
- if [[ ! "$addr" == *:* ]]; then
- return 1
- fi
-
- # Check for valid double-colon usage (only one :: allowed)
- if [[ "$addr" == *::*::* ]]; then
- return 1
- fi
-
- # Check that no segment exceeds 4 hex chars
- local IFS=':'
- local -a segments
- read -ra segments <<<"$addr"
- for seg in "${segments[@]}"; do
- if [[ ${#seg} -gt 4 ]]; then
- return 1
- fi
- done
-
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# validate_bridge()
-#
-# - Validates that network bridge exists and is active
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_bridge() {
- local bridge="$1"
- [[ -z "$bridge" ]] && return 1
-
- # Check if bridge interface exists
- if ! ip link show dev "$bridge" &>/dev/null; then
- return 1
- fi
-
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# validate_sdn_vnet()
-#
-# - Validates that an SDN vnet exists in the cluster config
-# ------------------------------------------------------------------------------
-validate_sdn_vnet() {
- local vnet="$1"
- [[ -z "$vnet" ]] && return 1
- [[ -f /etc/pve/sdn/vnets.cfg ]] && grep -qE "^vnet:[[:space:]]*${vnet}([[:space:]]|$)" /etc/pve/sdn/vnets.cfg && return 0
- command -v pvesh &>/dev/null && pvesh get "/cluster/sdn/vnets/${vnet}" &>/dev/null && return 0
- return 1
-}
-
-# ------------------------------------------------------------------------------
-# validate_gateway_in_subnet()
-#
-# - Validates that gateway IP is in the same subnet as static IP
-# - Arguments: static_ip (with CIDR), gateway_ip
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_gateway_in_subnet() {
- local static_ip="$1"
- local gateway="$2"
-
- [[ -z "$static_ip" || -z "$gateway" ]] && return 0
-
- # Extract IP and CIDR
- local ip="${static_ip%%/*}"
- local cidr="${static_ip##*/}"
-
- # /31 and /32 are valid point-to-point / zero-trust DMZ configurations
- # where the gateway is technically outside the subnet — skip validation
- ((cidr >= 31)) && return 0
-
- # Convert CIDR to netmask bits
- local mask=$((0xFFFFFFFF << (32 - cidr) & 0xFFFFFFFF))
-
- # Convert IPs to integers
- local IFS='.'
- read -r i1 i2 i3 i4 <<<"$ip"
- read -r g1 g2 g3 g4 <<<"$gateway"
-
- local ip_int=$(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
- local gw_int=$(((g1 << 24) + (g2 << 16) + (g3 << 8) + g4))
-
- # Check if both are in same network
- if (((ip_int & mask) != (gw_int & mask))); then
- return 1
- fi
-
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# validate_ip_address()
-#
-# - Validates IPv4 address with CIDR notation
-# - Checks each octet is 0-255
-# - Checks CIDR is 1-32
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_ip_address() {
- local ip="$1"
- [[ -z "$ip" ]] && return 1
-
- # Check format with CIDR
- if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$ ]]; then
- return 1
- fi
-
- local o1="${BASH_REMATCH[1]}"
- local o2="${BASH_REMATCH[2]}"
- local o3="${BASH_REMATCH[3]}"
- local o4="${BASH_REMATCH[4]}"
- local cidr="${BASH_REMATCH[5]}"
-
- # Validate octets (0-255)
- for octet in "$o1" "$o2" "$o3" "$o4"; do
- if ((octet > 255)); then
- return 1
- fi
- done
-
- # Validate CIDR (1-32)
- if ((cidr < 1 || cidr > 32)); then
- return 1
- fi
-
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# validate_gateway_ip()
-#
-# - Validates gateway IPv4 address (without CIDR)
-# - Checks each octet is 0-255
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_gateway_ip() {
- local ip="$1"
- [[ -z "$ip" ]] && return 0
-
- # Check format without CIDR
- if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
- return 1
- fi
-
- local o1="${BASH_REMATCH[1]}"
- local o2="${BASH_REMATCH[2]}"
- local o3="${BASH_REMATCH[3]}"
- local o4="${BASH_REMATCH[4]}"
-
- # Validate octets (0-255)
- for octet in "$o1" "$o2" "$o3" "$o4"; do
- if ((octet > 255)); then
- return 1
- fi
- done
-
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# validate_timezone()
-#
-# - Validates timezone string against system zoneinfo
-# - Empty value or "host" is allowed
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_timezone() {
- local tz="$1"
- [[ -z "$tz" || "$tz" == "host" ]] && return 0
-
- # Check if timezone file exists
- if [[ ! -f "/usr/share/zoneinfo/$tz" ]]; then
- return 1
- fi
-
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# validate_tags()
-#
-# - Validates Proxmox tags format
-# - Only alphanumeric, hyphens, underscores, and semicolons allowed
-# - Empty value is allowed
-# - Returns 0 if valid, 1 if invalid
-# ------------------------------------------------------------------------------
-validate_tags() {
- local tags="$1"
- [[ -z "$tags" ]] && return 0
-
- # Tags can only contain alphanumeric, -, _, and ; (separator)
- if [[ ! "$tags" =~ ^[a-zA-Z0-9_\;-]+$ ]]; then
- return 1
- fi
-
- 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 3B: IP RANGE SCANNING
-# ==============================================================================
-
-# ------------------------------------------------------------------------------
-# ip_to_int() / int_to_ip()
-#
-# - Converts IP address to integer and vice versa for range iteration
-# ------------------------------------------------------------------------------
-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))"
-}
-
-# ------------------------------------------------------------------------------
-# resolve_ip_from_range()
-#
-# - Takes an IP range in format "10.0.0.1/24-10.0.0.10/24"
-# - Pings each IP in the range to find the first available one
-# - Returns the first free IP with CIDR notation
-# - Sets NET_RESOLVED to the resolved IP or empty on failure
-# ------------------------------------------------------------------------------
-resolve_ip_from_range() {
- local range="$1"
- 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_start ip_end
-
- # Parse range: "10.0.0.1/24-10.0.0.10/24"
- ip_start="${range%%-*}"
- ip_end="${range##*-}"
-
- if [[ ! "$ip_start" =~ $ip_cidr_regex ]] || [[ ! "$ip_end" =~ $ip_cidr_regex ]]; then
- NET_RESOLVED=""
- return 1
- fi
-
- local ip1="${ip_start%%/*}"
- local ip2="${ip_end%%/*}"
- local cidr="${ip_start##*/}"
-
- local start_int=$(ip_to_int "$ip1")
- local end_int=$(ip_to_int "$ip2")
-
- for ((ip_int = start_int; ip_int <= end_int; ip_int++)); do
- local ip=$(int_to_ip $ip_int)
- msg_info "Checking IP: $ip"
- if ! ping -c 1 -W 1 "$ip" >/dev/null 2>&1; then
- NET_RESOLVED="$ip/$cidr"
- msg_ok "Found free IP: ${BGN}$NET_RESOLVED${CL}"
- return 0
- fi
- done
-
- NET_RESOLVED=""
- msg_error "No free IP found in range $range"
- return 1
-}
-
-# ------------------------------------------------------------------------------
-# is_ip_range()
-#
-# - Checks if a string is an IP range (contains - and looks like IP/CIDR)
-# - Returns 0 if it's a range, 1 otherwise
-# ------------------------------------------------------------------------------
-is_ip_range() {
- local value="$1"
- local ip_start ip_end
- if [[ "$value" == *-* ]] && [[ "$value" != "dhcp" ]]; then
- local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$'
- ip_start="${value%%-*}"
- ip_end="${value##*-}"
- if [[ "$ip_start" =~ $ip_cidr_regex ]] && [[ "$ip_end" =~ $ip_cidr_regex ]]; then
- return 0
- fi
- fi
- return 1
-}
-
-# ==============================================================================
-# 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 65
- ;;
- 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=""
-
- # Validate storage space for auto-picked container storage
- if [[ "$class" == "container" && -n "${DISK_SIZE:-}" ]]; then
- validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes"
- # Continue even if validation fails - user was warned
- fi
- else
- # If the current value is preselectable, we could show it, but per your requirement we always offer selection
- select_storage "$class" || return 150
- 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
-# ==============================================================================
-
-# ------------------------------------------------------------------------------
-# Distinct versions of OS type $1 in the cached pveam catalog (no network).
-# `pveam available` prints "\t" - the name is field 2.
-# Must never fail: callers capture this via mapfile < <(...).
-# ------------------------------------------------------------------------------
-_list_os_versions() {
- local ostype="${1:-}"
- [[ -n "$ostype" ]] || return 0
- local arch
- arch="$(dpkg --print-architecture 2>/dev/null || echo amd64)"
- pveam available -section system 2>/dev/null |
- grep -E "_${arch}\.(tar\.zst|tar\.xz|tar\.gz)([[:space:]]|$)" |
- awk '{print $2}' |
- sed -nE "s/^${ostype}-([0-9]+(\.[0-9]+)?).*/\1/p" |
- sort -u -V || true
-}
-
-# ------------------------------------------------------------------------------
-# Catalog templates matching prefix $1 and variant $2 ("debian-13" +
-# "-standard_"), oldest first. Same no-fail contract as _list_os_versions.
-# ------------------------------------------------------------------------------
-_list_templates() {
- local search="${1:-}" pattern="${2:-}"
- [[ -n "$search" ]] || return 0
- local arch
- arch="$(dpkg --print-architecture 2>/dev/null || echo amd64)"
- pveam available -section system 2>/dev/null |
- grep -E "_${arch}\.(tar\.zst|tar\.xz|tar\.gz)([[:space:]]|$)" |
- awk '{print $2}' |
- grep -E "^${search}.*${pattern}" |
- sort -t - -k 2 -V || true
-}
-
-# ------------------------------------------------------------------------------
-# Maps var_version onto a version this host's catalog actually has, before the
-# settings summary is printed. Availability comes from `pveam available` only -
-# never inferred from the PVE release (an updated 8.4 may carry templates an
-# older 8.x never saw, and entries drop out again at EOL).
-# Picks the nearest version, newer wins on a tie. Interactive: menu with that
-# candidate preselected; unattended: takes it with a warning.
-# ------------------------------------------------------------------------------
-resolve_os_version() {
- local ostype="${var_os:-}" want="${var_version:-}"
- [[ -n "$ostype" && -n "$want" ]] || return 0
-
- # ARM64 pulls from linuxcontainers.org, the pveam catalog says nothing there
- [[ "$(dpkg --print-architecture 2>/dev/null)" == "arm64" ]] && return 0
-
- local -a versions=()
- mapfile -t versions < <(_list_os_versions "$ostype")
- [[ ${#versions[@]} -gt 0 ]] || return 0
-
- local v
- for v in "${versions[@]}"; do
- [[ "$v" == "$want" ]] && return 0
- done
-
- # Already downloaded counts as available even if the catalog dropped it
- if [[ -n "${TEMPLATE_STORAGE:-}" ]] &&
- pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "/${ostype}-${want}-"; then
- return 0
- fi
-
- local candidate
- candidate=$(printf '%s\n' "${versions[@]}" | awk -v want="$want" '
- function num(v, a) { split(v, a, "."); return a[1] * 10000 + (a[2] + 0) }
- BEGIN { w = num(want); best = ""; bestd = -1 }
- {
- d = num($0) - w
- ad = (d < 0) ? -d : d
- if (bestd < 0 || ad < bestd || (ad == bestd && d > 0)) { bestd = ad; best = $0 }
- }
- END { print best }
- ')
- [[ -n "$candidate" ]] || return 0
-
- msg_warn "${ostype^} ${want} is not in this host's template catalog (PVE ${PVEVERSION:-unknown})"
-
- if [[ "${PHS_SILENT:-0}" != "1" ]] && command -v whiptail &>/dev/null && [ -t 0 ] && [[ "$TERM" != "dumb" ]]; then
- local -a menu=()
- while read -r v; do
- if [[ "$v" == "$candidate" ]]; then
- menu+=("$v" "closest to ${want} (recommended)")
- else
- menu+=("$v" " ")
- fi
- done < <(printf '%s\n' "${versions[@]}" | sort -rV)
-
- local choice
- choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "OS VERSION NOT AVAILABLE" \
- --ok-button "Use" --cancel-button "Exit" \
- --menu "\n${ostype^} ${want} is not available on this Proxmox host.\n\nSelect the version to use instead:" \
- 20 70 8 "${menu[@]}" --default-item "$candidate" \
- 3>&1 1>&2 2>&3) || exit_script
- candidate="$choice"
- else
- msg_warn "Unattended run - continuing with ${ostype} ${candidate}"
- fi
-
- var_version="$candidate"
- export var_version
- msg_ok "Using ${ostype} ${var_version} instead of ${want}"
- return 0 # last call in base_settings(), which runs under errexit
-}
-
-# ------------------------------------------------------------------------------
-# 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"}
-
- # 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=""
- if [[ -n "${var_pw:-}" ]]; then
- local _pw_raw="${var_pw}"
- case "$_pw_raw" in
- --password\ *) _pw_raw="${_pw_raw#--password }" ;;
- -password\ *) _pw_raw="${_pw_raw#-password }" ;;
- esac
- while [[ "$_pw_raw" == -* ]]; do
- _pw_raw="${_pw_raw#-}"
- done
- if [[ -z "$_pw_raw" ]]; then
- msg_warn "Password was only dashes after cleanup; leaving empty."
- else
- PW="--password $_pw_raw"
- fi
- fi
-
- # Validate and set Container ID
- local requested_id="${var_ctid:-$NEXTID}"
- if ! validate_container_id "$requested_id"; then
- # Only show warning if user manually specified an ID (not auto-assigned)
- if [[ -n "${var_ctid:-}" ]]; then
- msg_warn "Container ID $requested_id is already in use. Using next available ID: $(get_valid_container_id "$requested_id")"
- fi
- requested_id=$(get_valid_container_id "$requested_id")
- fi
- CT_ID="$requested_id"
-
- # Validate and set Hostname/FQDN
- local requested_hostname="${var_hostname:-$NSAPP}"
- requested_hostname=$(echo "${requested_hostname,,}" | tr -d ' ')
- if ! validate_hostname "$requested_hostname"; then
- if [[ -n "${var_hostname:-}" ]]; then
- msg_warn "Invalid hostname '$requested_hostname'. Using default: $NSAPP"
- fi
- requested_hostname="$NSAPP"
- fi
- HN="$requested_hostname"
-
- BRG=${var_brg:-"vmbr0"}
- SDN_VNET=${var_sdn_vnet:-""}
- NET=${var_net:-"dhcp"}
-
- # Resolve IP range if NET contains a range (e.g., 192.168.1.100/24-192.168.1.200/24)
- if is_ip_range "$NET"; then
- msg_info "Scanning IP range: $NET"
- if resolve_ip_from_range "$NET"; then
- NET="$NET_RESOLVED"
- else
- msg_error "Could not find free IP in range. Falling back to DHCP."
- NET="dhcp"
- fi
- fi
-
- IPV6_METHOD=${var_ipv6_method:-"none"}
- GATE=${var_gateway:-""}
-
- # Guard against invalid gateway combinations from defaults/app vars:
- # - DHCP must not force a static gateway
- # - Static IPv4 must use a gateway in the same subnet
- if [[ "$NET" == "dhcp" && -n "$GATE" ]]; then
- msg_warn "Ignoring var_gateway '$GATE' because var_net is 'dhcp'"
- GATE=""
- elif [[ "$NET" != "dhcp" && -n "$GATE" ]]; then
- if ! validate_gateway_in_subnet "$NET" "$GATE"; then
- msg_warn "Ignoring var_gateway '$GATE' because it is not in subnet of var_net '$NET'"
- GATE=""
- fi
- fi
-
- APT_CACHER=${var_apt_cacher:-""}
- APT_CACHER_IP=${var_apt_cacher_ip:-""}
- INHERIT_HOST_CA="${var_inherit_host_ca:-no}"
-
- # Runtime check: Verify APT cacher is reachable if configured
- if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then
- local _check_host _check_port _check_url
- _check_host=$(echo "$APT_CACHER_IP" | sed -e 's|https\?://||' -e 's|/.*||' | cut -d: -f1)
- _check_port=$(echo "$APT_CACHER_IP" | sed -e 's|https\?://||' -e 's|/.*||' | cut -s -d: -f2)
- if [[ "$APT_CACHER_IP" =~ ^https?:// ]]; then
- _check_url="$APT_CACHER_IP"
- _check_port="${_check_port:-80}"
- else
- _check_port="${_check_port:-3142}"
- _check_url="http://${APT_CACHER_IP}:${_check_port}"
- fi
- if ! curl -s --connect-timeout 2 "${_check_url}" >/dev/null 2>&1; then
- msg_warn "APT Cacher configured but not reachable at ${_check_url}"
- msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation"
- APT_CACHER=""
- APT_CACHER_IP=""
- else
- msg_ok "APT Cacher verified at ${_check_url}"
- fi
- fi
-
- HTTP_PROXY="${var_http_proxy:-}"
- HTTP_NO_PROXY="${var_http_no_proxy:-}"
-
- MTU=${var_mtu:-""}
- _sd_val="${var_searchdomain:-""}"
- [[ -n "$_sd_val" ]] && SD="-searchdomain=$_sd_val" || SD=""
- _ns_val="${var_ns:-""}"
- [[ -n "$_ns_val" ]] && NS="-nameserver=$_ns_val" || NS=""
- MAC=${var_mac:-""}
- VLAN=${var_vlan:-""}
- SSH=${var_ssh:-"no"}
- SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
- # Build TAGS: ensure community-script prefix, use semicolons (pct format), no duplicates
- if [[ "${var_tags:-}" == *community-script* ]]; then
- TAGS="${var_tags:-community-script}"
- else
- TAGS="community-script${var_tags:+;${var_tags}}"
- fi
- ENABLE_FUSE=${var_fuse:-"${1:-no}"}
- ENABLE_TUN=${var_tun:-"${1:-no}"}
-
- # Additional settings that may be skipped if advanced_settings is not run (e.g., App Defaults)
- ENABLE_GPU=${var_gpu:-"no"}
- ENABLE_NESTING=${var_nesting:-"1"}
- ENABLE_KEYCTL=${var_keyctl:-"0"}
- ENABLE_MKNOD=${var_mknod:-"0"}
- ALLOW_MOUNT_FS=${var_mount_fs:-""}
- PROTECT_CT=${var_protection:-"no"}
- CT_TIMEZONE=${var_timezone:-"$timezone"}
- [[ "${CT_TIMEZONE:-}" == Etc/* ]] && CT_TIMEZONE="host" # pct doesn't accept Etc/* zones
-
- # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
- if [ -z "$var_os" ]; then
- var_os="debian"
- fi
- if [ -z "$var_version" ]; then
- var_version="13"
- fi
-
- # every install path passes here, and still before any summary is printed
- resolve_os_version
-}
-
-# ------------------------------------------------------------------------------
-# 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
-# - Optional force parameter to override existing values (for app defaults)
-# - Optional protected list preserves genuinely user-exported var_* values
-# ------------------------------------------------------------------------------
-load_vars_file() {
- local file="$1"
- local force="${2:-no}" # If "yes", override existing variables
- local protected="${3:-}" # space-separated var_* keys the user genuinely exported before this file loaded; never overwritten
- [ -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_github_token var_gpu var_http_no_proxy var_http_proxy var_inherit_host_ca var_keyctl
- var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
- var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
- var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain
- var_post_install var_sdn_vnet
- )
-
- # Whitelist check helper
- _is_whitelisted() {
- local k="$1" w
- for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
- return 1
- }
-
- # Protected check helper (genuinely user-exported vars, see $protected above)
- _is_protected() {
- local k="$1" p
- for p in $protected; do [ "$k" = "$p" ] && 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 inline comments (anything after unquoted #)
- # Only strip if not inside quotes
- if [[ ! "$var_val" =~ ^[\"\'] ]]; then
- var_val="${var_val%%#*}"
- fi
-
- # Strip quotes
- if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
- var_val="${BASH_REMATCH[1]}"
- elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
- var_val="${BASH_REMATCH[1]}"
- fi
-
- # Trim trailing whitespace
- var_val="${var_val%"${var_val##*[![:space:]]}"}"
-
- # Validate values before setting (skip empty values - they use defaults)
- if [[ -n "$var_val" ]]; then
- case "$var_key" in
- var_mac)
- if ! validate_mac_address "$var_val"; then
- msg_warn "Invalid MAC address '$var_val' in $file, ignoring"
- continue
- fi
- ;;
- var_vlan)
- if ! validate_vlan_tag "$var_val"; then
- msg_warn "Invalid VLAN tag '$var_val' in $file (must be 1-4094), ignoring"
- continue
- fi
- ;;
- var_mtu)
- if ! validate_mtu "$var_val"; then
- msg_warn "Invalid MTU '$var_val' in $file (must be 576-65535), ignoring"
- continue
- fi
- ;;
- var_tags)
- if ! validate_tags "$var_val"; then
- msg_warn "Invalid tags '$var_val' in $file (alphanumeric, -, _, ; only), ignoring"
- continue
- fi
- ;;
- var_timezone)
- if ! validate_timezone "$var_val"; then
- msg_warn "Invalid timezone '$var_val' in $file, ignoring"
- continue
- fi
- ;;
- var_brg)
- if ! validate_bridge "$var_val"; then
- msg_warn "Bridge '$var_val' not found in $file, ignoring"
- continue
- fi
- ;;
- var_gateway)
- if ! validate_gateway_ip "$var_val"; then
- msg_warn "Invalid gateway IP '$var_val' in $file, ignoring"
- continue
- fi
- ;;
- var_hostname)
- if ! validate_hostname "$var_val"; then
- msg_warn "Invalid hostname '$var_val' in $file, ignoring"
- continue
- fi
- ;;
- var_cpu)
- if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1 || var_val > 128)); then
- msg_warn "Invalid CPU count '$var_val' in $file (must be 1-128), ignoring"
- continue
- fi
- ;;
- var_ram)
- if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 256)); then
- msg_warn "Invalid RAM '$var_val' in $file (must be >= 256 MiB), ignoring"
- continue
- fi
- ;;
- var_disk)
- if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1)); then
- msg_warn "Invalid disk size '$var_val' in $file (must be >= 1 GB), ignoring"
- continue
- fi
- ;;
- var_unprivileged)
- if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
- msg_warn "Invalid unprivileged value '$var_val' in $file (must be 0 or 1), ignoring"
- continue
- fi
- ;;
- var_nesting)
- if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
- msg_warn "Invalid nesting value '$var_val' in $file (must be 0 or 1), ignoring"
- continue
- fi
- # Warn about potential issues with systemd-based OS when nesting is disabled via vars file
- if [[ "$var_val" == "0" && "${var_os:-debian}" != "alpine" ]]; then
- msg_warn "Nesting disabled in $file - modern systemd-based distributions may require nesting for proper operation"
- fi
- ;;
- var_keyctl)
- if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
- msg_warn "Invalid keyctl value '$var_val' in $file (must be 0 or 1), ignoring"
- continue
- fi
- ;;
- var_net)
- # var_net can be: dhcp, static IP/CIDR, or IP range
- if [[ "$var_val" != "dhcp" ]]; then
- if is_ip_range "$var_val"; then
- : # IP range is valid, will be resolved at runtime
- elif ! validate_ip_address "$var_val"; then
- msg_warn "Invalid network '$var_val' in $file (must be dhcp or IP/CIDR), ignoring"
- continue
- fi
- fi
- ;;
- var_fuse | var_tun | var_gpu | var_ssh | var_verbose | var_protection)
- if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then
- msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring"
- continue
- fi
- ;;
- var_mknod)
- if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
- msg_warn "Invalid mknod value '$var_val' in $file (must be 0 or 1), ignoring"
- continue
- fi
- ;;
- var_mount_fs)
- # Normalize: strip spaces, trailing commas
- var_val="${var_val// /}"
- var_val="${var_val%%,}"
- var_val="${var_val##,}"
- if [[ -n "$var_val" ]] && [[ ! "$var_val" =~ ^[a-zA-Z0-9]+(,[a-zA-Z0-9]+)*$ ]]; then
- msg_warn "Invalid mount_fs value '$var_val' in $file (comma-separated fs names only, e.g. nfs,cifs), ignoring"
- continue
- fi
- ;;
- var_ipv6_method)
- if [[ "$var_val" != "auto" && "$var_val" != "dhcp" && "$var_val" != "static" && "$var_val" != "none" ]]; then
- msg_warn "Invalid IPv6 method '$var_val' in $file (must be auto/dhcp/static/none), ignoring"
- continue
- fi
- ;;
- var_apt_cacher_ip)
- # Allow: plain IP/hostname, http://host, https://host:port
- if [[ -n "$var_val" ]] && ! [[ "$var_val" =~ ^(https?://)?[a-zA-Z0-9._-]+(:[0-9]+)?(/.*)?$ ]]; then
- msg_warn "Invalid APT Cacher address '$var_val' in $file, ignoring"
- continue
- fi
- ;;
- var_sdn_vnet)
- if [[ -n "$var_val" ]] && ! validate_sdn_vnet "$var_val"; then
- msg_warn "SDN vnet '$var_val' from $file not found, ignoring"
- continue
- fi
- ;;
- var_http_proxy)
- if [[ -n "$var_val" ]] && ! [[ "$var_val" =~ ^https?://[^[:space:]]+(:[0-9]+)?/?$ ]]; then
- msg_warn "Invalid HTTP proxy URL '$var_val' in $file, ignoring"
- continue
- fi
- ;;
- var_http_no_proxy)
- if [[ -n "$var_val" ]] && [[ ! "$var_val" =~ ^[a-zA-Z0-9.*_:/-]+(,[a-zA-Z0-9.*_:/-]+)*$ ]]; then
- msg_warn "Invalid no_proxy value '$var_val' in $file, ignoring"
- continue
- fi
- ;;
- var_inherit_host_ca)
- if [[ "$var_val" != "yes" && "$var_val" != "no" && "$var_val" != "auto" ]]; then
- msg_warn "Invalid host CA inheritance value '$var_val' in $file (must be yes/no/auto), ignoring"
- continue
- fi
- ;;
- var_container_storage | var_template_storage)
- # Validate that the storage exists and is active on the current node
- local _storage_status
- _storage_status=$(pvesm status 2>/dev/null | awk -v s="$var_val" '$1 == s { print $3 }')
- if [[ -z "$_storage_status" ]]; then
- msg_warn "Storage '$var_val' from $file not found on this node, ignoring"
- continue
- elif [[ "$_storage_status" == "disabled" ]]; then
- msg_warn "Storage '$var_val' from $file is disabled on this node, ignoring"
- continue
- fi
- ;;
- esac
- fi
-
- # Set variable: force mode overrides existing, otherwise only set if empty.
- # Exception: var_cpu/var_ram/var_disk are always applied here (unless the
- # user genuinely exported them beforehand, per $protected) even though the
- # app script already declared its own baseline for them - base_settings()
- # reconciles the final floor against APP_DEFAULT_* afterward, so this file
- # must be allowed to raise them instead of being silently blocked by the
- # app's own pre-set value.
- if [[ "$force" == "yes" ]]; then
- export "${var_key}=${var_val}"
- elif _is_protected "$var_key"; then
- :
- elif [[ "$var_key" == "var_cpu" || "$var_key" == "var_ram" || "$var_key" == "var_disk" ]]; then
- export "${var_key}=${var_val}"
- else
- [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
- fi
- fi
- done <"$file"
- msg_ok "Loaded ${file}"
-}
-
-# ------------------------------------------------------------------------------
-# 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_github_token var_gpu var_http_no_proxy var_http_proxy var_inherit_host_ca var_keyctl
- var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
- var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
- var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
- var_post_install var_sdn_vnet
- )
-
- # 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 252
- }
- # 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 - IP or URL)
-# var_apt_cacher=yes
-# var_apt_cacher_ip=192.168.1.10
-# var_apt_cacher_ip=http://proxy.local
-# var_apt_cacher_ip=https://proxy.local:443
-
-# HTTP/HTTPS proxy (optional - for networks requiring a proxy)
-# var_http_proxy=http://proxy.local:8080
-# var_http_no_proxy=localhost,127.0.0.1,.local
-# var_inherit_host_ca=no
-
-# 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=
-
-# GitHub Personal Access Token (optional – avoids API rate limits during installs)
-# Create at https://github.com/settings/tokens – read-only public access is sufficient
-# var_github_token=ghp_your_token_here
-
-# Optional post-install script (host-side path to a *.sh on the Proxmox host)
-# Runs ON THE HOST after the container is fully provisioned.
-# Available env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG
-# var_post_install=/opt/post-install/myhook.sh
-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 252
- }
- load_vars_file "$dv" "no" "${!_HARD_ENV[*]}"
-
- # 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
-
- # 4) Map var_github_token → GITHUB_TOKEN (only if not already set in environment)
- if [[ -z "${GITHUB_TOKEN:-}" && -n "${var_github_token:-}" ]]; then
- export GITHUB_TOKEN="${var_github_token}"
- fi
-
- # 4) Apply base settings and show summary
- METHOD="mydefaults-global"
- base_settings "$VERBOSE"
- header_info
- echo -e "${DEFAULT}${BOLD}${BL}Using User Defaults (default.vars) on node $PVEHOST_NAME${CL}"
- echo_default
-}
-
-# ------------------------------------------------------------------------------
-# 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"
-}
-
-# ------------------------------------------------------------------------------
-# 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_github_token var_gpu var_http_no_proxy var_http_proxy var_inherit_host_ca var_keyctl
- var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
- var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
- var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain
- var_post_install var_sdn_vnet
- )
-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
-}
-
-_sanitize_value() {
- # Disallow Command-Substitution / Shell-Meta
- case "$1" in
- *'$('* | *'`'* | *';'* | *'&'* | *'<('*)
- echo ""
- return 0
- ;;
- esac
- echo "$1"
-}
-
-# 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"
-}
-
-# 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
- out+="- ${k}\n - old: ${OLD[$k]}\n"
- found_change=1
- fi
- done
-
- # Added
- for k in "${!NEW[@]}"; do
- if [[ ! -v OLD["$k"] ]]; then
- out+="+ ${k}\n + new: ${NEW[$k]}\n"
- found_change=1
- fi
- done
-
- if [[ $found_change -eq 0 ]]; then
- out+="(No differences)\n"
- fi
-
- printf "%b" "$out"
-}
-
-# Build a temporary .vars file from current advanced settings
-_build_current_app_vars_tmp() {
- tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
-
- # NET/GW
- _net="${NET:-}"
- _gate=""
- case "${GATE:-}" in
- ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
- esac
-
- # IPv6
- _ipv6_method="${IPV6_METHOD:-auto}"
- _ipv6_static=""
- _ipv6_gateway=""
- if [ "$_ipv6_method" = "static" ]; then
- _ipv6_static="${IPV6_ADDR:-}"
- _ipv6_gateway="${IPV6_GATE:-}"
- fi
-
- # 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
-
- # 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
-
- # SSH / APT / Features
- _ssh="${SSH:-no}"
- _ssh_auth="${SSH_AUTHORIZED_KEY:-}"
- _apt_cacher="${APT_CACHER:-}"
- _apt_cacher_ip="${APT_CACHER_IP:-}"
- _http_proxy="${HTTP_PROXY:-${var_http_proxy:-}}"
- _http_no_proxy="${HTTP_NO_PROXY:-${var_http_no_proxy:-}}"
- _inherit_host_ca="${INHERIT_HOST_CA:-${var_inherit_host_ca:-no}}"
- _fuse="${ENABLE_FUSE:-no}"
- _tun="${ENABLE_TUN:-no}"
- _gpu="${ENABLE_GPU:-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_os=$(_sanitize_value "${var_os:-}")"
- echo "var_version=$(_sanitize_value "${var_version:-}")"
- echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
- echo "var_cpu=$(_sanitize_value "$_cpu")"
- echo "var_ram=$(_sanitize_value "$_ram")"
- 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 "$_http_proxy" ] && echo "var_http_proxy=$(_sanitize_value "$_http_proxy")"
- [ -n "$_http_no_proxy" ] && echo "var_http_no_proxy=$(_sanitize_value "$_http_no_proxy")"
- [ -n "$_inherit_host_ca" ] && echo "var_inherit_host_ca=$(_sanitize_value "$_inherit_host_ca")"
-
- [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
- [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
- [ -n "$_gpu" ] && echo "var_gpu=$(_sanitize_value "$_gpu")"
- [ -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 "${var_sdn_vnet:-}" ] && echo "var_sdn_vnet=$(_sanitize_value "${var_sdn_vnet}")"
-
- [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
- [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
- [ -n "${var_post_install:-}" ] && echo "var_post_install=$(_sanitize_value "${var_post_install}")"
- } >"$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 "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")"
- /usr/bin/install -m 0644 "$new_tmp" "$app_vars_path"
- msg_ok "Saved app defaults: ${app_vars_path}"
- fi
- rm -f "$new_tmp" "$diff_tmp"
- return 0
- fi
-
- # 2) if file exists → build diff
- _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
-
- # 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")"
-
- while true; do
- local sel
- 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}" \
- "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")
- /usr/bin/install -m 0644 "$new_tmp" "$app_vars_path"
- msg_ok "Updated app defaults: ${app_vars_path}"
- break
- ;;
- "Keep Current")
- msg_custom "ℹ️" "${BL}" "Keeping current app defaults: ${app_vars_path}"
- break
- ;;
- "View Diff")
- whiptail --backtitle "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
-
- rm -f "$new_tmp" "$diff_tmp"
-}
-
-ensure_storage_selection_for_vars_file() {
- local vf="$1"
-
- # Read stored values (if any)
- local tpl ct
- tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2- || true)
- ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2- || true)
-
- if [[ -n "$tpl" && -n "$ct" ]]; then
- TEMPLATE_STORAGE="$tpl"
- CONTAINER_STORAGE="$ct"
-
- # Validate storage space for loaded container storage
- if [[ -n "${DISK_SIZE:-}" ]]; then
- validate_storage_space "$ct" "$DISK_SIZE" "yes"
- # Continue even if validation fails - user was warned
- fi
-
- return 0
- fi
-
- choose_and_set_storage_for_file "$vf" template
- choose_and_set_storage_for_file "$vf" container
-
- # Silent operation - no output message
-}
-
-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"
-}
-
-# ==============================================================================
-# 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
-
- # Initialize defaults
- # Build TAGS: ensure community-script prefix, use semicolons (pct format), no duplicates
- if [[ "${var_tags:-}" == *community-script* ]]; then
- TAGS="${var_tags:-community-script}"
- else
- TAGS="community-script${var_tags:+;${var_tags}}"
- fi
- local STEP=1
- local MAX_STEP=31
-
- # Store values for back navigation - inherit from var_* app defaults
- local _ct_type="${var_unprivileged:-1}"
- local _pw=""
- local _pw_display="Automatic Login"
- local _ct_id="$NEXTID"
- local _hostname="$NSAPP"
- local _disk_size="${var_disk:-4}"
- local _core_count="${var_cpu:-1}"
- local _ram_size="${var_ram:-1024}"
- local _bridge="${var_brg:-vmbr0}"
- local _sdn_vnet="${var_sdn_vnet:-}"
- local _net="${var_net:-dhcp}"
- local _gate="${var_gateway:-}"
- local _ipv6_method="${var_ipv6_method:-auto}"
- local _ipv6_addr=""
- local _ipv6_gate=""
- local _apt_cacher="${var_apt_cacher:-no}"
- local _apt_cacher_ip="${var_apt_cacher_ip:-}"
- local _http_proxy="${var_http_proxy:-}"
- local _http_no_proxy="${var_http_no_proxy:-}"
- local _inherit_host_ca="${var_inherit_host_ca:-no}"
- local _mtu="${var_mtu:-}"
- local _sd="${var_searchdomain:-}"
- local _ns="${var_ns:-}"
- local _mac="${var_mac:-}"
- local _vlan="${var_vlan:-}"
- local _tags="$TAGS"
- local _enable_fuse="${var_fuse:-no}"
- local _enable_tun="${var_tun:-no}"
- local _enable_gpu="${var_gpu:-no}"
- local _enable_nesting="${var_nesting:-1}"
- local _verbose="${var_verbose:-no}"
- local _enable_keyctl="${var_keyctl:-0}"
- local _enable_mknod="${var_mknod:-0}"
- local _mount_fs="${var_mount_fs:-}"
- local _protect_ct="${var_protection:-no}"
- local _post_install="${var_post_install:-}"
-
- # Detect host timezone for default (if not set via var_timezone)
- local _host_timezone=""
- if command -v timedatectl >/dev/null 2>&1; then
- _host_timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "")
- elif [ -f /etc/timezone ]; then
- _host_timezone=$(cat /etc/timezone 2>/dev/null || echo "")
- fi
- # Map Etc/* timezones to "host" (pct doesn't accept Etc/* zones)
- [[ "${_host_timezone:-}" == Etc/* ]] && _host_timezone="host"
- local _ct_timezone="${var_timezone:-$_host_timezone}"
- [[ "${_ct_timezone:-}" == Etc/* ]] && _ct_timezone="host"
-
- # 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*//;s/^[- ]*//' || true)
- BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }")
- fi
- done <<<"$BRIDGES"
- fi
- if [[ -f /etc/pve/sdn/vnets.cfg ]]; then
- while IFS= read -r vnet; do
- [[ -n "$vnet" ]] && BRIDGE_MENU_OPTIONS+=("sdn:${vnet}" "[SDN] ${vnet}")
- done < <(awk '/^vnet:/{print $2}' /etc/pve/sdn/vnets.cfg 2>/dev/null)
- fi
- }
- _detect_bridges
-
- # Main wizard loop
- while [ $STEP -le $MAX_STEP ]; do
- case $STEP in
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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
- else
- local _pw1_clean="$PW1"
- while [[ "$_pw1_clean" == -* ]]; do
- _pw1_clean="${_pw1_clean#-}"
- done
- if [[ -z "$_pw1_clean" ]]; then
- whiptail --msgbox "Password cannot be only '-' characters." 8 58
- continue
- elif ((${#_pw1_clean} < 5)); then
- whiptail --msgbox "Password must be at least 5 characters (after removing leading '-')." 8 70
- continue
- fi
- # 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
- local _pw2_clean="$PW2"
- while [[ "$_pw2_clean" == -* ]]; do
- _pw2_clean="${_pw2_clean#-}"
- done
- if [[ "$_pw1_clean" == "$_pw2_clean" ]]; then
- _pw="--password $_pw1_clean"
- _pw_display="********"
- ((STEP++))
- else
- whiptail --msgbox "Passwords do not match. Please try again." 8 58
- fi
- else
- ((STEP--))
- fi
- fi
- else
- ((STEP--))
- fi
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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
- local input_id="${result:-$NEXTID}"
-
- # Validate that ID is numeric
- if ! [[ "$input_id" =~ ^[0-9]+$ ]]; then
- whiptail --backtitle "Proxmox VE Helper Scripts" --title "Invalid ID" --msgbox "Container ID must be numeric." 8 58
- continue
- fi
-
- # Check if ID is already in use
- if ! validate_container_id "$input_id"; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --title "ID Already In Use" \
- --yesno "Container/VM ID $input_id is already in use.\n\nWould you like to use the next available ID ($(get_valid_container_id "$input_id"))?" 10 58; then
- _ct_id=$(get_valid_container_id "$input_id")
- else
- continue
- fi
- else
- _ct_id="$input_id"
- fi
- ((STEP++))
- else
- ((STEP--))
- fi
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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 (or FQDN, e.g. host.example.com)" 10 58 "$_hostname" \
- 3>&1 1>&2 2>&3); then
- local hn_test="${result:-$NSAPP}"
- hn_test=$(echo "${hn_test,,}" | tr -d ' ')
-
- if validate_hostname "$hn_test"; then
- _hostname="$hn_test"
- ((STEP++))
- else
- whiptail --msgbox "Invalid hostname: '$hn_test'\n\nRules:\n- Only lowercase letters, digits, dots and hyphens\n- Labels separated by dots (max 63 chars each)\n- No leading/trailing hyphens or dots\n- No consecutive dots\n- Total max 253 characters" 14 60
- fi
- 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 --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
- else
- ((STEP--))
- fi
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 8: Network Bridge
- # ═══════════════════════════════════════════════════════════════════════════
- 8)
- if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then
- # Validate default bridge exists
- if validate_bridge "vmbr0"; then
- _bridge="vmbr0"
- ((STEP++))
- else
- whiptail --msgbox "Default bridge 'vmbr0' not found!\n\nPlease configure a network bridge in Proxmox first." 10 58
- msg_error "Default bridge 'vmbr0' not found"
- exit 116
- fi
- 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
- local bridge_test="${result:-vmbr0}"
- # Skip separator entries (e.g., __other__) - re-display menu
- if [[ "$bridge_test" == "__other__" || "$bridge_test" == -* ]]; then
- continue
- fi
- if [[ "$bridge_test" == sdn:* ]]; then
- local vnet_test="${bridge_test#sdn:}"
- if validate_sdn_vnet "$vnet_test"; then
- _sdn_vnet="$vnet_test"
- _bridge="${var_brg:-vmbr0}"
- ((STEP++))
- else
- whiptail --msgbox "SDN vnet '$vnet_test' is not configured on this cluster." 8 58
- fi
- elif validate_bridge "$bridge_test"; then
- _bridge="$bridge_test"
- _sdn_vnet=""
- ((STEP++))
- else
- whiptail --msgbox "Bridge '$bridge_test' is not available or not active." 8 58
- fi
- else
- ((STEP--))
- fi
- fi
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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:" 16 65 3 \
- "dhcp" "Automatic (DHCP, recommended)" \
- "static" "Static (manual entry)" \
- "range" "IP Range Scan (find first free IP)" \
- 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 validate_ip_address "$static_ip"; 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 validate_gateway_ip "$gateway_ip"; then
- # Validate gateway is in same subnet
- if validate_gateway_in_subnet "$static_ip" "$gateway_ip"; then
- _net="$static_ip"
- _gate=",gw=$gateway_ip"
- ((STEP++))
- else
- whiptail --msgbox "Gateway is not in the same subnet as the static IP.\n\nStatic IP: $static_ip\nGateway: $gateway_ip" 10 58
- fi
- else
- whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58
- fi
- fi
- else
- whiptail --msgbox "Invalid IPv4 CIDR format.\n\nEach octet must be 0-255.\nCIDR must be 1-32.\nExample: 192.168.1.100/24" 12 58
- fi
- fi
- elif [[ "$result" == "range" ]]; then
- # IP Range Scan
- local ip_range
- if ip_range=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "IP RANGE SCAN" \
- --ok-button "Scan" --cancel-button "Back" \
- --inputbox "\nEnter IP range to scan for free address\n(e.g. 192.168.1.100/24-192.168.1.200/24)" 12 65 "" \
- 3>&1 1>&2 2>&3); then
- if is_ip_range "$ip_range"; then
- # Exit whiptail screen temporarily to show scan progress
- clear
- header_info
- echo -e "${INFO}${BOLD}${DGN}Scanning IP range for free address...${CL}\n"
- if resolve_ip_from_range "$ip_range"; 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 "\nFound free IP: $NET_RESOLVED\n\nEnter Gateway IP address" 12 58 "" \
- 3>&1 1>&2 2>&3); then
- if validate_gateway_ip "$gateway_ip"; then
- # Validate gateway is in same subnet
- if validate_gateway_in_subnet "$NET_RESOLVED" "$gateway_ip"; then
- _net="$NET_RESOLVED"
- _gate=",gw=$gateway_ip"
- ((STEP++))
- else
- whiptail --msgbox "Gateway is not in the same subnet as the IP.\n\nIP: $NET_RESOLVED\nGateway: $gateway_ip" 10 58
- fi
- else
- whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58
- fi
- fi
- else
- whiptail --msgbox "No free IP found in the specified range.\nAll IPs responded to ping." 10 58
- fi
- else
- whiptail --msgbox "Invalid IP range format.\n\nExample: 192.168.1.100/24-192.168.1.200/24" 10 58
- fi
- fi
- else
- _net="dhcp"
- _gate=""
- ((STEP++))
- fi
- else
- ((STEP--))
- 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 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"
- 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 validate_ipv6_address "$ipv6_addr"; then
- _ipv6_addr="$ipv6_addr"
- # Optional gateway - loop until valid or empty
- local ipv6_gw_valid=false
- while [[ "$ipv6_gw_valid" == "false" ]]; do
- local ipv6_gw
- ipv6_gw=$(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
- # Validate gateway if provided
- if [[ -n "$ipv6_gw" ]]; then
- if validate_ipv6_address "$ipv6_gw"; then
- _ipv6_gate="$ipv6_gw"
- ipv6_gw_valid=true
- ((STEP++))
- else
- whiptail --msgbox "Invalid IPv6 gateway format.\n\nExample: 2001:db8::1" 8 58
- fi
- else
- _ipv6_gate=""
- ipv6_gw_valid=true
- ((STEP++))
- fi
- done
- else
- whiptail --msgbox "Invalid IPv6 CIDR format.\n\nExample: 2001:db8::1/64\nCIDR must be 1-128." 10 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
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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, common values: 1500, 9000)" 12 62 "" \
- 3>&1 1>&2 2>&3); then
- if validate_mtu "$result"; then
- _mtu="$result"
- ((STEP++))
- else
- whiptail --msgbox "Invalid MTU size.\n\nMTU must be between 576 and 65535.\nCommon values: 1500 (default), 9000 (jumbo frames)" 10 58
- fi
- else
- ((STEP--))
- 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
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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, format: XX:XX:XX:XX:XX:XX)" 12 62 "" \
- 3>&1 1>&2 2>&3); then
- if validate_mac_address "$result"; then
- _mac="$result"
- ((STEP++))
- else
- whiptail --msgbox "Invalid MAC address format.\n\nRequired format: XX:XX:XX:XX:XX:XX\nExample: 02:00:00:00:00:01" 10 58
- fi
- else
- ((STEP--))
- 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 (1-4094)\n(leave blank for no VLAN)" 12 58 "" \
- 3>&1 1>&2 2>&3); then
- if validate_vlan_tag "$result"; then
- _vlan="$result"
- ((STEP++))
- else
- whiptail --msgbox "Invalid VLAN tag.\n\nVLAN must be a number between 1 and 4094." 8 58
- fi
- else
- ((STEP--))
- fi
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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(alphanumeric, hyphens, underscores only)" 12 58 "$_tags" \
- 3>&1 1>&2 2>&3); then
- local tags_test="${result:-}"
- tags_test=$(echo "$tags_test" | tr -d '[:space:]')
- if validate_tags "$tags_test"; then
- _tags="$tags_test"
- ((STEP++))
- else
- whiptail --msgbox "Invalid tag format.\n\nTags can only contain:\n- Letters (a-z, A-Z)\n- Numbers (0-9)\n- Hyphens (-)\n- Underscores (_)\n- Semicolons (;) as separator" 14 58
- fi
- else
- ((STEP--))
- fi
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 17: SSH Settings
- # ═══════════════════════════════════════════════════════════════════════════
- 17)
- configure_ssh_settings "Step $STEP/$MAX_STEP"
- # configure_ssh_settings handles its own flow, always advance
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 18: FUSE Support
- # ═══════════════════════════════════════════════════════════════════════════
- 18)
- local fuse_default_flag="--defaultno"
- [[ "$_enable_fuse" == "yes" || "$_enable_fuse" == "1" ]] && fuse_default_flag=""
-
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "FUSE SUPPORT" \
- --ok-button "Next" --cancel-button "Back" \
- $fuse_default_flag \
- --yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc.\n\n(App default: ${var_fuse:-no})" 14 58; then
- _enable_fuse="yes"
- else
- if [ $? -eq 1 ]; then
- _enable_fuse="no"
- else
- ((STEP--))
- continue
- fi
- fi
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 19: TUN/TAP Support
- # ═══════════════════════════════════════════════════════════════════════════
- 19)
- local tun_default_flag="--defaultno"
- [[ "$_enable_tun" == "yes" || "$_enable_tun" == "1" ]] && tun_default_flag=""
-
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "TUN/TAP SUPPORT" \
- --ok-button "Next" --cancel-button "Back" \
- $tun_default_flag \
- --yesno "\nEnable TUN/TAP device support?\n\nRequired for: VPN apps (WireGuard, OpenVPN, Tailscale),\nnetwork tunneling, and containerized networking.\n\n(App default: ${var_tun:-no})" 14 62; then
- _enable_tun="yes"
- else
- if [ $? -eq 1 ]; then
- _enable_tun="no"
- else
- ((STEP--))
- continue
- fi
- fi
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 20: Nesting Support
- # ═══════════════════════════════════════════════════════════════════════════
- 20)
- local nesting_default_flag=""
- [[ "$_enable_nesting" == "0" || "$_enable_nesting" == "no" ]] && nesting_default_flag="--defaultno"
-
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "NESTING SUPPORT" \
- --ok-button "Next" --cancel-button "Back" \
- $nesting_default_flag \
- --yesno "\nEnable Nesting?\n\nRequired for: Docker, LXC inside LXC, Podman,\nand other containerization tools.\n\n(App default: ${var_nesting:-1})" 14 58; then
- _enable_nesting="1"
- else
- if [ $? -eq 1 ]; then
- _enable_nesting="0"
- # Warn about potential issues with systemd-based OS when nesting is disabled
- if [[ "$var_os" != "alpine" ]]; then
- whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "⚠️ NESTING WARNING" \
- --msgbox "Modern systemd-based distributions (Debian 13+, Ubuntu 24.04+, etc.) may require nesting to be enabled for proper operation.\n\nWithout nesting, the container may start in a degraded state with failing services (error 243/CREDENTIALS).\n\nIf you experience issues, enable nesting in the container options." 14 68
- fi
- else
- ((STEP--))
- continue
- fi
- fi
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 21: GPU Passthrough
- # ═══════════════════════════════════════════════════════════════════════════
- 21)
- local gpu_default_flag="--defaultno"
- [[ "$_enable_gpu" == "yes" ]] && gpu_default_flag=""
-
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "GPU PASSTHROUGH" \
- --ok-button "Next" --cancel-button "Back" \
- $gpu_default_flag \
- --yesno "\nEnable GPU Passthrough?\n\nAutomatically detects and passes through available GPUs\n(Intel/AMD/NVIDIA) for hardware acceleration.\n\nRecommended for: Media servers, AI/ML, Transcoding\n\n(App default: ${var_gpu:-no})" 16 62; then
- _enable_gpu="yes"
- else
- if [ $? -eq 1 ]; then
- _enable_gpu="no"
- else
- ((STEP--))
- continue
- fi
- fi
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 22: Keyctl Support (Docker/systemd)
- # ═══════════════════════════════════════════════════════════════════════════
- 22)
- # Keyctl is always required for unprivileged containers — skip dialog
- if [[ "$_ct_type" == "1" ]]; then
- _enable_keyctl="1"
- ((STEP++))
- continue
- fi
-
- local keyctl_default_flag="--defaultno"
- [[ "$_enable_keyctl" == "1" ]] && keyctl_default_flag=""
-
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "KEYCTL SUPPORT" \
- --ok-button "Next" --cancel-button "Back" \
- $keyctl_default_flag \
- --yesno "\nEnable Keyctl support?\n\nRequired for: Docker containers, systemd-networkd,\nand kernel keyring operations.\n\n(App default: ${var_keyctl:-0})" 14 62; then
- _enable_keyctl="1"
- else
- if [ $? -eq 1 ]; then
- _enable_keyctl="0"
- else
- ((STEP--))
- continue
- fi
- fi
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 23: APT Cacher Proxy
- # ═══════════════════════════════════════════════════════════════════════════
- 23)
- local apt_cacher_default_flag="--defaultno"
- [[ "$_apt_cacher" == "yes" ]] && apt_cacher_default_flag=""
-
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "APT CACHER PROXY" \
- --ok-button "Next" --cancel-button "Back" \
- $apt_cacher_default_flag \
- --yesno "\nUse APT Cacher-NG proxy?\n\nSpeeds up package downloads by caching them locally.\nRequires apt-cacher-ng running on your network.\n\n(App default: ${var_apt_cacher:-no})" 14 62; then
- _apt_cacher="yes"
- # Ask for IP if enabled
- if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "APT CACHER IP" \
- --inputbox "\nEnter APT Cacher-NG IP or URL:\n(e.g. 192.168.1.10, http://host, https://host:443)" 12 62 "$_apt_cacher_ip" \
- 3>&1 1>&2 2>&3); then
- _apt_cacher_ip="$result"
- fi
- else
- if [ $? -eq 1 ]; then
- _apt_cacher="no"
- _apt_cacher_ip=""
- else
- ((STEP--))
- continue
- fi
- fi
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 24: HTTP/HTTPS Proxy
- # ═══════════════════════════════════════════════════════════════════════════
- 24)
- local http_proxy_default_flag="--defaultno"
- [[ -n "$_http_proxy" ]] && http_proxy_default_flag=""
-
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "HTTP/HTTPS PROXY" \
- --ok-button "Next" --cancel-button "Back" \
- $http_proxy_default_flag \
- --yesno "\nUse HTTP/HTTPS proxy?\n\nRequired when internet access must go through a proxy server.\n(e.g. corporate networks)\n\n(App default: ${var_http_proxy:-none})" 14 62; then
- if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "HTTP PROXY URL" \
- --inputbox "\nEnter HTTP proxy URL:\n(e.g. http://proxy.local:8080)" 12 62 "$_http_proxy" \
- 3>&1 1>&2 2>&3); then
- _http_proxy="$result"
- local _no_proxy_default="${_http_no_proxy:-localhost,127.0.0.1,.local}"
- if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "NO_PROXY EXCEPTIONS" \
- --inputbox "\nEnter NO_PROXY exceptions (comma-separated):\nLeave empty for defaults." 12 62 "$_no_proxy_default" \
- 3>&1 1>&2 2>&3); then
- _http_no_proxy="$result"
- fi
- fi
- else
- if [ $? -eq 1 ]; then
- _http_proxy=""
- _http_no_proxy=""
- else
- ((STEP--))
- continue
- fi
- fi
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 25: Host CA Inheritance
- # ═══════════════════════════════════════════════════════════════════════════
- 25)
- local host_ca_count=0
- local host_ca_dir="/usr/local/share/ca-certificates"
- local cert
- shopt -s nullglob
- for cert in "$host_ca_dir"/*.crt; do
- host_ca_count=$((host_ca_count + 1))
- done
- shopt -u nullglob
-
- if [[ $host_ca_count -eq 0 ]]; then
- _inherit_host_ca="no"
- ((STEP++))
- continue
- fi
-
- local host_ca_default_flag="--defaultno"
- [[ "$_inherit_host_ca" == "yes" ]] && host_ca_default_flag=""
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "HOST CA INHERITANCE" \
- --ok-button "Next" --cancel-button "Back" \
- $host_ca_default_flag \
- --yesno "\nInherit host CA certificates into this container?\n\nDetected on host: ${host_ca_count} certificate(s) in:\n${host_ca_dir}\n\nRecommended for private PKI / TLS-inspection environments.\n\n(App default: ${var_inherit_host_ca:-no})" 16 72; then
- _inherit_host_ca="yes"
- else
- if [ $? -eq 1 ]; then
- _inherit_host_ca="no"
- else
- ((STEP--))
- continue
- fi
- fi
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 26: Container Timezone
- # ═══════════════════════════════════════════════════════════════════════════
- 26)
- local tz_hint="$_ct_timezone"
- [[ -z "$tz_hint" ]] && tz_hint="(empty - will use host timezone)"
-
- if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "CONTAINER TIMEZONE" \
- --ok-button "Next" --cancel-button "Back" \
- --inputbox "\nSet container timezone.\n\nExamples: Europe/Berlin, America/New_York, Asia/Tokyo\n\nHost timezone: ${_host_timezone:-unknown}\n\nLeave empty to inherit from host." 16 62 "$_ct_timezone" \
- 3>&1 1>&2 2>&3); then
- local tz_test="$result"
- [[ "${tz_test:-}" == Etc/* ]] && tz_test="host" # pct doesn't accept Etc/* zones
- if validate_timezone "$tz_test"; then
- _ct_timezone="$tz_test"
- ((STEP++))
- else
- whiptail --msgbox "Invalid timezone: '$result'\n\nTimezone must exist in /usr/share/zoneinfo/\n\nExamples:\n- Europe/Berlin\n- America/New_York\n- Asia/Tokyo\n- UTC" 14 58
- fi
- else
- ((STEP--))
- fi
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 27: Container Protection
- # ═══════════════════════════════════════════════════════════════════════════
- 27)
- local protect_default_flag="--defaultno"
- [[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_default_flag=""
-
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "CONTAINER PROTECTION" \
- --ok-button "Next" --cancel-button "Back" \
- $protect_default_flag \
- --yesno "\nEnable Container Protection?\n\nPrevents accidental deletion of this container.\nYou must disable protection before removing.\n\n(App default: ${var_protection:-no})" 14 62; then
- _protect_ct="yes"
- else
- if [ $? -eq 1 ]; then
- _protect_ct="no"
- else
- ((STEP--))
- continue
- fi
- fi
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 28: Device Node Creation (mknod)
- # ═══════════════════════════════════════════════════════════════════════════
- 28)
- local mknod_default_flag="--defaultno"
- [[ "$_enable_mknod" == "1" ]] && mknod_default_flag=""
-
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "DEVICE NODE CREATION" \
- --ok-button "Next" --cancel-button "Back" \
- $mknod_default_flag \
- --yesno "\nAllow device node creation (mknod)?\n\nRequired for: Creating device files inside container.\nExperimental feature (requires kernel 5.3+).\n\n(App default: ${var_mknod:-0})" 14 62; then
- _enable_mknod="1"
- else
- if [ $? -eq 1 ]; then
- _enable_mknod="0"
- else
- ((STEP--))
- continue
- fi
- fi
- ((STEP++))
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 29: Mount Filesystems
- # ═══════════════════════════════════════════════════════════════════════════
- 29)
- local mount_hint=""
- [[ -n "$_mount_fs" ]] && mount_hint="$_mount_fs" || mount_hint="(none)"
-
- if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "MOUNT FILESYSTEMS" \
- --ok-button "Next" --cancel-button "Back" \
- --inputbox "\nAllow specific filesystem mounts.\n\nComma-separated list: nfs, cifs, fuse, ext4, etc.\nLeave empty for defaults (none).\n\nCurrent: $mount_hint" 14 62 "$_mount_fs" \
- 3>&1 1>&2 2>&3); then
- # Normalize: strip spaces and trailing/leading commas
- result="${result// /}"
- result="${result%%,}"
- result="${result##,}"
- _mount_fs="$result"
- ((STEP++))
- else
- ((STEP--))
- fi
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 30: Optional host-side post-install hook (path on the Proxmox HOST)
- # ═══════════════════════════════════════════════════════════════════════════
- 30)
- local _hook_prompt="Optional: absolute path to a *.sh file ON THE PROXMOX HOST.
-
-It runs as root on the HOST (NOT in the LXC) after the container
-is fully provisioned and started.
-
-Available env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG.
-
-Leave empty to skip."
- while true; do
- if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "POST-INSTALL HOOK (HOST)" \
- --ok-button "Next" --cancel-button "Back" \
- --inputbox "$_hook_prompt" 16 70 "${_post_install}" \
- 3>&1 1>&2 2>&3); then
- # Normalize: strip surrounding whitespace
- result="$(printf '%s' "$result" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
- if [[ -z "$result" ]]; then
- _post_install=""
- ((STEP++))
- break
- fi
- # Reject obvious shell-meta sneaking through
- if [[ "$result" == *';'* || "$result" == *'$('* || "$result" == *'`'* || "$result" == *'&&'* || "$result" == *'||'* ]]; then
- whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID PATH" \
- --msgbox "Path contains shell metacharacters. Please provide a plain absolute file path." 10 70
- continue
- fi
- if [[ "$result" != /* ]]; then
- whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID PATH" \
- --msgbox "Path must be absolute (start with /).\n\nGot: $result" 10 70
- continue
- fi
- if [[ ! -f "$result" ]]; then
- if ! whiptail --backtitle "Proxmox VE Helper Scripts" --title "FILE NOT FOUND" \
- --yesno "File does not exist on host:\n\n$result\n\nKeep this path anyway?" 12 70; then
- continue
- fi
- fi
- _post_install="$result"
- ((STEP++))
- break
- else
- ((STEP--))
- break
- fi
- done
- ;;
-
- # ═══════════════════════════════════════════════════════════════════════════
- # STEP 31: Verbose Mode & Confirmation
- # ═══════════════════════════════════════════════════════════════════════════
- 31)
- local verbose_default_flag="--defaultno"
- [[ "$_verbose" == "yes" ]] && verbose_default_flag=""
-
- if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
- --title "VERBOSE MODE" \
- $verbose_default_flag \
- --yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then
- _verbose="yes"
- else
- _verbose="no"
- fi
- # Build summary
- local ct_type_desc="Unprivileged"
- [[ "$_ct_type" == "0" ]] && ct_type_desc="Privileged"
-
- local nesting_desc="Disabled"
- [[ "$_enable_nesting" == "1" ]] && nesting_desc="Enabled"
-
- local keyctl_desc="Disabled"
- [[ "$_enable_keyctl" == "1" ]] && keyctl_desc="Enabled"
-
- local protect_desc="No"
- [[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_desc="Yes"
-
- local tz_display="${_ct_timezone:-Host TZ}"
- local apt_display="${_apt_cacher:-no}"
- [[ "$_apt_cacher" == "yes" && -n "$_apt_cacher_ip" ]] && apt_display="$_apt_cacher_ip"
- local http_proxy_display="${_http_proxy:-(none)}"
- local inherit_ca_display="${_inherit_host_ca:-no}"
-
- local post_install_display="${_post_install:-(none)}"
- local post_install_warn=""
- [[ -n "$_post_install" ]] && post_install_warn="
- ⚠ Hook runs as root on Proxmox HOST (not in LXC)"
-
- 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
-
-Features:
- FUSE: $_enable_fuse | TUN: $_enable_tun
- Nesting: $nesting_desc | Keyctl: $keyctl_desc
- Mknod: $([ "$_enable_mknod" == "1" ] && echo Enabled || echo Disabled) | Mount FS: ${_mount_fs:-(none)}
- GPU: $_enable_gpu | Protection: $protect_desc
-
-Advanced:
- Timezone: $tz_display
- APT Cacher: $apt_display
- HTTP Proxy: $http_proxy_display
- Inherit Host CAs: $inherit_ca_display
- Verbose: $_verbose
- Post-Install Script: ${post_install_display}${post_install_warn}"
-
- 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?" 32 62; then
- ((STEP++))
- else
- ((STEP--))
- fi
- ;;
- esac
- done
-
- # ═══════════════════════════════════════════════════════════════════════════
- # 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"
- ENABLE_TUN="$_enable_tun"
- ENABLE_GPU="$_enable_gpu"
- ENABLE_NESTING="$_enable_nesting"
- ENABLE_KEYCTL="$_enable_keyctl"
- ENABLE_MKNOD="$_enable_mknod"
- ALLOW_MOUNT_FS="$_mount_fs"
- PROTECT_CT="$_protect_ct"
- CT_TIMEZONE="$_ct_timezone"
- APT_CACHER="$_apt_cacher"
- APT_CACHER_IP="$_apt_cacher_ip"
- HTTP_PROXY="$_http_proxy"
- HTTP_NO_PROXY="$_http_no_proxy"
- INHERIT_HOST_CA="$_inherit_host_ca"
- VERBOSE="$_verbose"
- var_post_install="$_post_install"
-
- # Update var_* based on user choice (for functions that check these)
- var_gpu="$_enable_gpu"
- var_fuse="$_enable_fuse"
- var_tun="$_enable_tun"
- var_nesting="$_enable_nesting"
- var_keyctl="$_enable_keyctl"
- var_mknod="$_enable_mknod"
- var_mount_fs="$_mount_fs"
- var_protection="$_protect_ct"
- var_timezone="$_ct_timezone"
- var_apt_cacher="$_apt_cacher"
- var_apt_cacher_ip="$_apt_cacher_ip"
- var_sdn_vnet="$_sdn_vnet"
- var_http_proxy="$_http_proxy"
- var_http_no_proxy="$_http_no_proxy"
- var_inherit_host_ca="$_inherit_host_ca"
-
- # 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=""
-
- export SSH_KEYS_FILE
-
- # Exit alternate screen buffer before showing summary (so output remains visible)
- tput rmcup 2>/dev/null || true
- trap - RETURN
-
- # 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:-no}${CL}"
- [[ "${ENABLE_TUN:-no}" == "yes" ]] && echo -e "${NETWORK}${BOLD}${DGN}TUN/TAP Support: ${BGN}$ENABLE_TUN${CL}"
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Nesting: ${BGN}$([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")${CL}"
- [[ "${ENABLE_KEYCTL:-0}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Keyctl: ${BGN}Enabled${CL}"
- echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}${ENABLE_GPU:-no}${CL}"
- [[ "${ENABLE_MKNOD:-0}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Mknod: ${BGN}Enabled${CL}"
- [[ -n "${ALLOW_MOUNT_FS:-}" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Mount FS: ${BGN}${ALLOW_MOUNT_FS}${CL}"
- [[ "${PROTECT_CT:-no}" == "yes" || "${PROTECT_CT:-no}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Protection: ${BGN}Enabled${CL}"
- [[ -n "${CT_TIMEZONE:-}" ]] && echo -e "${INFO}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}"
- [[ "$APT_CACHER" == "yes" ]] && echo -e "${INFO}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}"
- [[ -n "${HTTP_PROXY:-}" ]] && echo -e "${INFO}${BOLD}${DGN}HTTP Proxy: ${BGN}$HTTP_PROXY${CL}"
- echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
- echo -e "${CREATING}${BOLD}${RD}Creating an LXC of ${APP} using the above advanced settings${CL}"
-
- # Log settings to file
- log_section "CONTAINER SETTINGS (ADVANCED) - ${APP}"
- log_msg "Application: ${APP}"
- log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
- log_msg "Operating System: $var_os ($var_version)"
- log_msg "Container Type: $([ "$CT_TYPE" == "1" ] && echo "Unprivileged" || echo "Privileged")"
- log_msg "Container ID: $CT_ID"
- log_msg "Hostname: $HN"
- log_msg "Disk Size: ${DISK_SIZE} GB"
- log_msg "CPU Cores: $CORE_COUNT"
- log_msg "RAM Size: ${RAM_SIZE} MiB"
- log_msg "Bridge: $BRG"
- log_msg "IPv4: $NET"
- log_msg "IPv6: $IPV6_METHOD"
- log_msg "FUSE Support: ${ENABLE_FUSE:-no}"
- log_msg "Nesting: $([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")"
- log_msg "Mknod: $([ "${ENABLE_MKNOD:-0}" == "1" ] && echo "Enabled" || echo "Disabled")"
- [[ -n "${ALLOW_MOUNT_FS:-}" ]] && log_msg "Mount FS: ${ALLOW_MOUNT_FS}"
- log_msg "GPU Passthrough: ${ENABLE_GPU:-no}"
- log_msg "Verbose Mode: $VERBOSE"
- log_msg "Session ID: ${SESSION_ID}"
-}
-
-# ==============================================================================
-# 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 (first run only)
-# - Saves DIAGNOSTICS=yes/no in the config file
-# - Reads current diagnostics setting from existing file
-# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out
-# ------------------------------------------------------------------------------
-diagnostics_check() {
- local config_dir="/usr/local/community-scripts"
- local config_file="${config_dir}/diagnostics"
-
- mkdir -p "$config_dir"
-
- if [[ -f "$config_file" ]]; then
- DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' "$config_file") || true
- DIAGNOSTICS="${DIAGNOSTICS:-no}"
- return
- fi
-
- local result
- result=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "TELEMETRY & DIAGNOSTICS" \
- --ok-button "Confirm" --cancel-button "Exit" \
- --radiolist "\nHelp improve Community-Scripts by sharing anonymous data.\n\nWhat we collect:\n - Container resources (CPU, RAM, disk), OS & PVE version\n - Application name, install method and status\n\nWhat we DON'T collect:\n - No IP addresses, hostnames, or personal data\n\nYou can change this anytime in the Settings menu.\nPrivacy: https://github.com/community-scripts/telemetry-service/blob/main/docs/PRIVACY.md\n\nUse SPACE to select, ENTER to confirm." 22 76 2 \
- "yes" "Yes, share anonymous data" OFF \
- "no" "No, opt out" OFF \
- 3>&1 1>&2 2>&3) || result="no"
-
- DIAGNOSTICS="${result:-no}"
-
- cat <"$config_file"
-DIAGNOSTICS=${DIAGNOSTICS}
-
-# Community-Scripts Telemetry Configuration
-# https://telemetry.community-scripts.org
-#
-# This file stores your telemetry preference.
-# Set DIAGNOSTICS=yes to share anonymous installation data.
-# Set DIAGNOSTICS=no to disable telemetry.
-#
-# You can also change this via the Settings menu during installation.
-#
-# Data collected (when enabled):
-# disk_size, core_count, ram_size, os_type, os_version,
-# nsapp, method, pve_version, status, exit_code
-#
-# No personal data (IPs, hostnames, passwords) is ever collected.
-# Privacy: https://github.com/community-scripts/telemetry-service/blob/main/docs/PRIVACY.md
-EOF
-}
-
-diagnostics_menu() {
- local current="${DIAGNOSTICS:-no}"
- local status_text="DISABLED"
- [[ "$current" == "yes" ]] && status_text="ENABLED"
-
- local dialog_text=(
- "Telemetry is currently: ${status_text}\n\n"
- "Anonymous data helps us improve scripts and track issues.\n"
- "No personal data is ever collected.\n\n"
- "More info: https://telemetry.community-scripts.org\n\n"
- "Do you want to ${current:+change this setting}?"
- )
-
- if [[ "$current" == "yes" ]]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "TELEMETRY SETTINGS" \
- --yesno "${dialog_text[*]}" 14 64 \
- --yes-button "Disable" --no-button "Keep enabled"; then
- DIAGNOSTICS="no"
- sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
- whiptail --msgbox "Telemetry disabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58
- fi
- else
- if whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "TELEMETRY SETTINGS" \
- --yesno "${dialog_text[*]}" 14 64 \
- --yes-button "Enable" --no-button "Keep disabled"; then
- DIAGNOSTICS="yes"
- sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
- whiptail --msgbox "Telemetry enabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 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
-# - Also logs settings to log file for debugging
-# ------------------------------------------------------------------------------
-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 [[ "$(dpkg --print-architecture)" == "arm64" ]]; then
- echo -e "${INFO}${BOLD}${DGN}Architecture: ${BGN}arm64${CL}"
- fi
- if [[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]]; then
- echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}Enabled${CL}"
- fi
- 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 " "
-
- # Log settings to file
- log_section "CONTAINER SETTINGS - ${APP}"
- log_msg "Application: ${APP}"
- log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
- log_msg "Container ID: ${CT_ID}"
- log_msg "Operating System: $var_os ($var_version)"
- log_msg "Container Type: $CT_TYPE_DESC"
- log_msg "Disk Size: ${DISK_SIZE} GB"
- log_msg "CPU Cores: ${CORE_COUNT}"
- log_msg "RAM Size: ${RAM_SIZE} MiB"
- [[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]] && log_msg "GPU Passthrough: Enabled"
- [[ "$VERBOSE" == "yes" ]] && log_msg "Verbose Mode: Enabled"
- log_msg "Session ID: ${SESSION_ID}"
-}
-
-# ------------------------------------------------------------------------------
-# 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
- root_check
- ensure_whiptail
- arch_check
- arm64_notice
- ssh_check
- maxkeys_check
- diagnostics_check
-
- if systemctl is-active -q ping-instances.service; then
- systemctl -q stop ping-instances.service
- fi
-
- NEXTID=$(pvesh get /cluster/nextid)
-
- # 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="UTC"
- fi
- [[ "${timezone:-}" == Etc/* ]] && timezone="host" # pct doesn't accept Etc/* zones
-
- # Show APP Header
- header_info
-
- # --- 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"
- )
-
- 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
-
- 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 | advanced | ADVANCED)
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
- METHOD="advanced"
- base_settings
- advanced_settings
- defaults_target="$(ensure_global_default_vars_file)"
- run_maybe_offer="yes"
- break
- ;;
- 3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS)
- default_var_settings || {
- msg_error "Failed to apply default.vars"
- exit 110
- }
- defaults_target="/usr/local/community-scripts/default.vars"
- break
- ;;
- "$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"
- load_vars_file "$(get_app_defaults_path)" "yes" # Force override script defaults
- base_settings
- echo_default
- defaults_target="$(get_app_defaults_path)"
- break
- else
- msg_error "No App Defaults available for ${APP}"
- exit 111
- fi
- ;;
- "$SETTINGS_OPTION" | settings | SETTINGS)
- settings_menu
- # After settings menu, show main menu again
- header_info
- CHOICE=""
- ;;
- generated | GENERATED)
- header_info
- echo -e "${DEFAULT}${BOLD}${BL}Using Generated Settings on node $PVEHOST_NAME${CL}"
- VERBOSE="no"
- METHOD="generated"
- base_settings "$VERBOSE"
- echo_default
- break
- ;;
- *)
- msg_error "Invalid option: $CHOICE"
- exit 112
- ;;
- 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) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;;
- 3)
- if [ -f "$(get_app_defaults_path)" ]; then
- ${EDITOR:-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() {
- current_ram=$(free -m | awk 'NR==2{print $2}')
- current_cpu=$(nproc)
-
- if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
- msg_warn "Under-provisioned: Required ${var_cpu} CPU/${var_ram}MB RAM, Current ${current_cpu} CPU/${current_ram}MB RAM"
- echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
- if is_unattended; then
- msg_error "Aborted: under-provisioned LXC in unattended mode (${current_cpu} CPU/${current_ram}MB RAM < ${var_cpu} CPU/${var_ram}MB RAM)"
- exit 113
- fi
- echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? "
- read -r prompt 80% and asks user confirmation before proceeding
-# ------------------------------------------------------------------------------
-check_container_storage() {
- total_size=$(df /boot --output=size | tail -n 1)
- local used_size=$(df /boot --output=used | tail -n 1)
- usage=$((100 * used_size / total_size))
- if ((usage > 80)); then
- msg_warn "Storage is dangerously low (${usage}% used on /boot)"
- if is_unattended; then
- msg_error "Aborted: storage too low in unattended mode (${usage}% used on /boot)"
- exit 114
- fi
- echo -ne "Continue anyway? "
- read -r prompt 0) { print substr($0, RSTART) }
- }
- '
-}
-
-# ------------------------------------------------------------------------------
-# 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() {
- local step_info="${1:-}"
- local backtitle="Proxmox VE Helper Scripts"
- [[ -n "$step_info" ]] && backtitle="Proxmox VE Helper Scripts [${step_info}]"
-
- 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 "$backtitle" --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 "$backtitle" --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 "$backtitle" --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- || true)
- [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
- done
- ;;
- manual)
- SSH_AUTHORIZED_KEY="$(whiptail --backtitle "$backtitle" \
- --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 "$backtitle" \
- --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
- [[ -d "$glob_path" ]] && glob_path="${glob_path%/}/*"
- local -a _scan_files
- mapfile -t _scan_files < <(compgen -G "$glob_path")
- 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 "$backtitle" --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- || true)
- [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
- done
- else
- whiptail --backtitle "$backtitle" --msgbox "No keys found in: $glob_path" 8 60
- fi
- else
- whiptail --backtitle "$backtitle" --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
-
- # Always show SSH access dialog - user should be able to enable SSH even without keys
- if (whiptail --backtitle "$backtitle" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
- SSH="yes"
- else
- SSH="no"
- fi
-}
-
-# ------------------------------------------------------------------------------
-# msg_menu()
-#
-# - Displays a numbered menu for update_script() functions
-# - In silent mode (PHS_SILENT=1): auto-selects the default option
-# - In interactive mode: shows menu via read with 10s timeout + default fallback
-# - Usage: CHOICE=$(msg_menu "Title" "tag1" "Description 1" "tag2" "Desc 2" ...)
-# - The first item is always the default
-# - Returns the selected tag to stdout
-# - If no valid selection or timeout, returns the default (first) tag
-# ------------------------------------------------------------------------------
-msg_menu() {
- local title="$1"
- shift
-
- # Parse items into parallel arrays: tags[] and descriptions[]
- local -a tags=()
- local -a descs=()
- while [[ $# -ge 2 ]]; do
- tags+=("$1")
- descs+=("$2")
- shift 2
- done
-
- local default_tag="${tags[0]}"
- local count=${#tags[@]}
-
- # Silent mode: return default immediately
- if [[ -n "${PHS_SILENT+x}" ]] && [[ "${PHS_SILENT}" == "1" ]]; then
- echo "$default_tag"
- return 0
- fi
-
- # Display menu to /dev/tty so it doesn't get captured by command substitution
- {
- echo ""
- msg_custom "📋" "${BL}" "${title}"
- echo ""
- for i in "${!tags[@]}"; do
- local marker=" "
- [[ $i -eq 0 ]] && marker="* "
- printf "${TAB3}${marker}%s) %s\n" "${tags[$i]}" "${descs[$i]}"
- done
- echo ""
- } >/dev/tty
-
- local selection=""
- read -r -t 10 -p "${TAB3}Select [default=${default_tag}, timeout 10s]: " selection /dev/tty || true
-
- # Validate selection
- if [[ -n "$selection" ]]; then
- for tag in "${tags[@]}"; do
- if [[ "$selection" == "$tag" ]]; then
- echo "$selection"
- return 0
- fi
- done
- msg_warn "Invalid selection '${selection}' - using default: ${default_tag}"
- fi
-
- echo "$default_tag"
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# run_addon_updates()
-#
-# - Scans /usr/local/bin/update_* for addon update scripts installed alongside
-# the main application (e.g. by tools/addon/*.sh)
-# - For each found addon, prompts the user (60s timeout, default no) whether
-# it should be updated as well
-# - Skipped entirely when PHS_SILENT=1 to keep unattended updates predictable
-# ------------------------------------------------------------------------------
-run_addon_updates() {
- shopt -s nullglob
- local addons=(/usr/local/bin/update_*)
- shopt -u nullglob
-
- ((${#addons[@]} == 0)) && return 0
-
- if [[ "${PHS_SILENT:-0}" == "1" ]]; then
- msg_info "Detected ${#addons[@]} addon update script(s) - skipping (PHS_SILENT)"
- return 0
- fi
-
- echo
- echo -e "${INFO}${YW} Detected installed addon update script(s):${CL}"
- local a name
- for a in "${addons[@]}"; do
- echo -e "${TAB}- ${a##*/update_}"
- done
- echo
-
- local ans
- for a in "${addons[@]}"; do
- name="${a##*/update_}"
- printf 'Do you also want to update addon "%s"? (y/N) [60s]: ' "$name"
- ans=""
- if read -r -t 60 ans /dev/null); then
- msg_warn "Script status check is unavailable. Continuing without status verification."
- return 0
- fi
-
- local is_deleted is_disabled deleted_message disable_message pinned_version pin_reason info_url
- if printf '%s' "$response" | grep -qE '"items":[[:space:]]*\[[[:space:]]*\]'; then
- return 0
- fi
-
- # PocketBase returns a flat, fixed-field JSON blob; sed is enough here (no jq needed).
- is_deleted=$(printf '%s' "$response" | sed -n 's/.*"is_deleted"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p' | head -1)
- is_disabled=$(printf '%s' "$response" | sed -n 's/.*"is_disabled"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p' | head -1)
- deleted_message=$(printf '%s' "$response" | sed -n 's/.*"deleted_message"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
- disable_message=$(printf '%s' "$response" | sed -n 's/.*"disable_message"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
- pinned_version=$(printf '%s' "$response" | sed -n 's/.*"pinned_version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
- pin_reason=$(printf '%s' "$response" | sed -n 's/.*"pin_reason"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
- is_deleted=${is_deleted:-false}
- is_disabled=${is_disabled:-false}
- info_url="https://community-scripts.org/scripts/${script_slug}"
-
- if [[ "$is_deleted" == "true" ]]; then
- msg_error "This script is no longer available in community-scripts."
- [[ -n "$deleted_message" ]] && msg_error "$deleted_message"
- [[ -z "$deleted_message" ]] && msg_error "This script was removed and cannot be installed or updated."
- msg_warn "More info: ${info_url}"
- return 1
- fi
-
- if [[ "$is_disabled" == "true" ]]; then
- # Allow bypass via var_ignore_disable=true (still warn, but continue)
- case "${var_ignore_disable:-}" in
- 1 | yes | true | on)
- msg_warn "This script is currently disabled in community-scripts."
- [[ -n "$disable_message" ]] && msg_warn "$disable_message"
- msg_warn "Bypassing disable status via var_ignore_disable — continuing at your own risk."
- msg_warn "More info: ${info_url}"
- return 0
- ;;
- esac
-
- msg_error "This script is currently disabled in community-scripts."
- [[ -n "$disable_message" ]] && msg_error "$disable_message"
- [[ -z "$disable_message" ]] && msg_error "Updates and installs are temporarily disabled for this script."
- msg_warn "More info: ${info_url}"
- return 1
- fi
-
- if [[ "$mode" == "update" && -n "$pinned_version" && -n "${NSAPP:-}" ]]; then
- local current_file="$HOME/.${NSAPP}"
- if [[ -f "$current_file" ]]; then
- local installed pinned_clean installed_clean
- installed="$(<"$current_file")"
- pinned_clean="$pinned_version"
- installed_clean="$installed"
- [[ "$pinned_clean" =~ ^v[0-9] ]] && pinned_clean="${pinned_clean:1}"
- [[ "$installed_clean" =~ ^v[0-9] ]] && installed_clean="${installed_clean:1}"
- if [[ "$installed_clean" == "$pinned_clean" ]]; then
- if [[ -n "$pin_reason" ]]; then
- msg_info "You are already on the pinned version (${pinned_version}). ${pin_reason}"
- else
- msg_info "You are already on the pinned version (${pinned_version}). No newer update is offered intentionally — please do not open an issue unless you see an actual error."
- fi
- msg_info "More info: ${info_url}"
- fi
- fi
- fi
-
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# check_container_os_guard()
-#
-# - Compares the container OS (/etc/os-release) with the script's recommended
-# var_os/var_version before running update_script
-# - On mismatch: interactive runs ask whether to continue (default: no);
-# headless runs (PHS_SILENT=1 / no tty) abort instead of updating on an
-# unsupported base and leaving the app broken mid-update
-# - Bypass via var_ignore_os_mismatch=1|yes|true|on (still warns, but continues)
-# ------------------------------------------------------------------------------
-check_container_os_guard() {
- local rec_os="${var_os:-}" rec_ver="${var_version:-}"
- rec_os="${rec_os,,}"
- [[ -z "$rec_os" || -z "$rec_ver" ]] && return 0
- [[ -r /etc/os-release ]] || return 0
-
- local cur_os cur_ver
- cur_os="$(. /etc/os-release 2>/dev/null; echo "${ID:-}")"
- cur_ver="$(. /etc/os-release 2>/dev/null; echo "${VERSION_ID:-}")"
- cur_os="${cur_os,,}"
- [[ -z "$cur_os" || -z "$cur_ver" ]] && return 0
-
- # Exact version or prefix on a dot boundary (e.g. alpine 3.22 matches 3.22.1)
- if [[ "$cur_os" == "$rec_os" ]] && [[ "$cur_ver" == "$rec_ver" || "$cur_ver" == "$rec_ver".* ]]; then
- return 0
- fi
-
- local downgraded_target=0
- if [[ "$cur_os" == "$rec_os" ]] && [[ "$cur_ver" != "$rec_ver" ]]; then
- if [[ "$(printf '%s\n%s\n' "$cur_ver" "$rec_ver" | sort -V | tail -n1)" == "$cur_ver" ]]; then
- downgraded_target=1
- fi
- fi
-
- case "${var_ignore_os_mismatch:-}" in
- 1 | yes | true | on)
- msg_warn "Container OS is ${cur_os} ${cur_ver} but the script recommends ${rec_os} ${rec_ver} — continuing via var_ignore_os_mismatch (may break, no support)."
- return 0
- ;;
- esac
-
- # Persistent opt-out: stores the ignored target version, so the question
- # comes back once the script targets a newer OS again
- local ignore_file="/usr/local/community-scripts/ignore-os-mismatch"
- if [[ -f "$ignore_file" && "$(cat "$ignore_file" 2>/dev/null)" == "${rec_os} ${rec_ver}" ]]; then
- msg_warn "Container OS is ${cur_os} ${cur_ver} but the script recommends ${rec_os} ${rec_ver} — continuing (previously ignored via ${ignore_file}, may break, no support)."
- return 0
- fi
-
- local mismatch_detail remediation
- if [[ "$downgraded_target" -eq 1 ]]; then
- mismatch_detail="Your container runs ${cur_os} ${cur_ver}, which is newer than what this script now targets (${rec_os} ${rec_ver}).\n\nThe target was likely lowered on purpose, e.g. because ${APP:-the app} had problems on newer OS versions - check the GitHub issues for ${APP:-this app} for details.\n\nContinuing on ${cur_ver} often still works, but isn't guaranteed. If you are unsure, ask in our Discord (https://discord.gg/3AnUqsXnmK) before continuing."
- remediation="This script now targets the older ${rec_os} ${rec_ver}. Check the GitHub issues for ${APP:-this app}, or ask in Discord (https://discord.gg/3AnUqsXnmK) if you are unsure"
- else
- mismatch_detail="This container runs ${cur_os} ${cur_ver}, but this script now targets ${rec_os} ${rec_ver}.\n\nUpdating on the older base OS may fail or leave ${APP:-the application} broken (e.g. required runtime versions are not available).\n\nRecommended: upgrade the container OS to ${rec_os} ${rec_ver} first, then run this update again."
- remediation="Upgrade the container OS to ${rec_os} ${rec_ver} first, then run this update again"
- fi
-
- if [[ "${PHS_SILENT:-0}" != "1" ]] && command -v whiptail &>/dev/null && [ -t 0 ] && [[ "$TERM" != "dumb" ]]; then
- local choice
- choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "OS VERSION MISMATCH" --menu \
- "${mismatch_detail}\n\nIf you continue anyway, it may break — no support is provided in that case.\n\nContinue anyway?" \
- 20 70 3 \
- "1" "No (cancel update)" \
- "2" "Yes (continue this time)" \
- "3" "Yes (continue and don't ask again)" \
- --nocancel --default-item "1" 3>&1 1>&2 2>&3)
- case "$choice" in
- 2)
- msg_warn "Continuing update on ${cur_os} ${cur_ver} despite recommended ${rec_os} ${rec_ver} — may break, no support."
- return 0
- ;;
- 3)
- mkdir -p "${ignore_file%/*}"
- echo "${rec_os} ${rec_ver}" >"$ignore_file"
- msg_warn "Continuing update on ${cur_os} ${cur_ver}; OS check for ${rec_os} ${rec_ver} disabled via ${ignore_file} — may break, no support."
- return 0
- ;;
- esac
- msg_error "Update cancelled: container OS ${cur_os} ${cur_ver} does not match the recommended ${rec_os} ${rec_ver}."
- return 1
- fi
-
- msg_error "Container OS ${cur_os} ${cur_ver} does not match the recommended ${rec_os} ${rec_ver} — skipping update."
- msg_error "${remediation} — or bypass this check (may break, no support) with: echo \"${rec_os} ${rec_ver}\" > ${ignore_file}"
- return 1
-}
-
-# ------------------------------------------------------------------------------
-# start()
-#
-# - Entry point of script
-# - On Proxmox host: calls install_script
-# - In silent mode: runs update_script with automatic cleanup
-# - Otherwise: shows update/setting menu and runs update_script with cleanup
-# ------------------------------------------------------------------------------
-
-start() {
- source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
- if command -v pveversion >/dev/null 2>&1; then
- runtime_script_status_guard install || return 0
- install_script || return 0
- return 0
- elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
- VERBOSE="no"
- set_std_mode
- ensure_profile_loaded
- get_lxc_ip
- runtime_script_status_guard update || return 0
- check_container_os_guard || return 0
- update_script
- run_addon_updates
- update_motd_ip
- cleanup_lxc
- elif ! command -v whiptail &>/dev/null || ! [ -t 0 ] || [[ "$TERM" == "dumb" ]]; then
- msg_info "No interactive terminal detected – defaulting to silent update mode"
- VERBOSE="no"
- set_std_mode
- ensure_profile_loaded
- get_lxc_ip
- runtime_script_status_guard update || return 0
- check_container_os_guard || return 0
- update_script
- run_addon_updates
- update_motd_ip
- cleanup_lxc
- 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)
-
- case "$CHOICE" in
- 1)
- VERBOSE="no"
- set_std_mode
- ;;
- 2)
- VERBOSE="yes"
- set_std_mode
- ;;
- 3)
- clear
- exit_script
- exit 0
- ;;
- esac
- ensure_profile_loaded
- get_lxc_ip
- runtime_script_status_guard update || return 0
- check_container_os_guard || return 0
- update_script
- run_addon_updates
- update_motd_ip
- cleanup_lxc
- fi
-}
-
-# ==============================================================================
-# SECTION 8: CONTAINER CREATION & DEPLOYMENT
-# ==============================================================================
-
-# ------------------------------------------------------------------------------
-# _apply_http_proxy_in_container()
-#
-# - Writes persistent proxy settings into the LXC before base package install
-# - Ensures apt/apk/curl work behind corporate proxies during early setup
-# ------------------------------------------------------------------------------
-_apply_http_proxy_in_container() {
- local proxy="${HTTP_PROXY:-}"
- local noproxy="${HTTP_NO_PROXY:-}"
- [[ -z "$proxy" || -z "${CTID:-}" ]] && return 0
- [[ -z "$noproxy" ]] && noproxy="localhost,127.0.0.1,.local"
-
- msg_info "Applying HTTP proxy in container"
- local tmpf
- tmpf="$(mktemp)"
- cat >"$tmpf" </dev/null 2>&1 || {
- rm -f "$tmpf"
- msg_warn "Failed to push HTTP proxy profile into container"
- return 0
- }
- pct exec "$CTID" -- chmod 644 /etc/profile.d/90-http-proxy.sh >/dev/null 2>&1 || true
-
- if [[ "$var_os" != "alpine" && "${APT_CACHER:-}" != "yes" ]]; then
- cat >"$tmpf" </dev/null 2>&1 || true
- fi
- rm -f "$tmpf"
- msg_ok "Applied HTTP proxy in container"
-}
-
-# ------------------------------------------------------------------------------
-# _apply_host_ca_certs_in_container()
-#
-# - Copies administrator-provided CA certificates from the Proxmox host into the
-# container before base package bootstrap
-# - Source: /usr/local/share/ca-certificates/*.crt (Debian convention)
-# - Refreshes the container trust store when update-ca-certificates is available
-# - No-op when no host certificates are present; failures are non-fatal
-# ------------------------------------------------------------------------------
-_apply_host_ca_certs_in_container() {
- local host_ca_dir="/usr/local/share/ca-certificates"
- [[ -z "${CTID:-}" ]] && return 0
- local inherit_host_ca="${INHERIT_HOST_CA:-${var_inherit_host_ca:-no}}"
-
- local -a host_certs=()
- local cert
- shopt -s nullglob
- for cert in "$host_ca_dir"/*.crt; do
- host_certs+=("$cert")
- done
- shopt -u nullglob
-
- [[ ${#host_certs[@]} -eq 0 ]] && return 0
-
- # Opt-in only: copy host CA certs solely when explicitly enabled.
- # Any other value (no/auto/unset) is a silent no-op to preserve LXC isolation.
- case "${inherit_host_ca,,}" in
- yes | true | 1 | on) ;;
- *)
- return 0
- ;;
- esac
-
- msg_info "Inheriting host CA certificates into container"
-
- local found=${#host_certs[@]}
- local copied=0
- local skipped=0
- local cert_name
-
- pct exec "$CTID" -- mkdir -p /usr/local/share/ca-certificates >/dev/null 2>&1 || {
- msg_warn "Failed to create CA certificate directory in container"
- return 0
- }
-
- for cert in "${host_certs[@]}"; do
- cert_name="$(basename "$cert")"
- if [[ ! -r "$cert" || "$cert_name" != *.crt ]]; then
- msg_warn "Skipping invalid or unreadable host CA certificate: ${cert_name}"
- skipped=$((skipped + 1))
- continue
- fi
-
- if pct push "$CTID" "$cert" "/usr/local/share/ca-certificates/${cert_name}" >/dev/null 2>&1; then
- pct exec "$CTID" -- chmod 644 "/usr/local/share/ca-certificates/${cert_name}" >/dev/null 2>&1 || true
- copied=$((copied + 1))
- else
- msg_warn "Failed to push host CA certificate: ${cert_name}"
- skipped=$((skipped + 1))
- fi
- done
-
- if [[ $copied -eq 0 ]]; then
- msg_warn "No host CA certificates were copied (${found} found, ${skipped} skipped)"
- return 0
- fi
-
- local refresh_shell="bash"
- [[ "$var_os" == "alpine" ]] && refresh_shell="ash"
-
- if pct exec "$CTID" -- "$refresh_shell" -c 'command -v update-ca-certificates >/dev/null 2>&1 && update-ca-certificates' >/dev/null 2>&1; then
- msg_ok "Inherited ${copied} host CA certificate(s) and updated trust store (${skipped} skipped)"
- else
- msg_warn "Copied ${copied} host CA certificate(s), but trust store update failed or update-ca-certificates is unavailable (${skipped} skipped)"
- fi
-}
-
-# ------------------------------------------------------------------------------
-# 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:-vmbr0}"
- if [[ -n "${var_sdn_vnet:-${SDN_VNET:-}}" ]]; then
- NET_STRING="-net0 name=eth0,bridge=${var_sdn_vnet:-$SDN_VNET}"
- fi
-
- # MAC
- if [[ -n "$MAC" ]]; then
- case "$MAC" in
- ,hwaddr=*) NET_STRING+="$MAC" ;;
- *) NET_STRING+=",hwaddr=$MAC" ;;
- esac
- fi
-
- # IP (always required, default dhcp)
- NET_STRING+=",ip=${NET:-dhcp}"
-
- # Gateway
- if [[ -n "$GATE" ]]; then
- case "$GATE" in
- ,gw=) ;;
- ,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" ;;
- static)
- if [[ -n "$IPV6_ADDR" ]]; then
- NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
- [ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
- fi
- ;;
- none) ;;
- esac
-
- # Build FEATURES string based on container type and user choices
- FEATURES=""
-
- # Nesting support (user configurable, default enabled)
- if [ "${ENABLE_NESTING:-1}" == "1" ]; then
- FEATURES="nesting=1"
- fi
-
- # Keyctl for unprivileged containers (needed for Docker)
- if [ "$CT_TYPE" == "1" ]; then
- [ -n "$FEATURES" ] && FEATURES="$FEATURES,"
- FEATURES="${FEATURES}keyctl=1"
- fi
-
- if [ "$ENABLE_FUSE" == "yes" ]; then
- [ -n "$FEATURES" ] && FEATURES="$FEATURES,"
- FEATURES="${FEATURES}fuse=1"
- fi
-
- # Mknod support (user configurable via advanced settings)
- if [ "${ENABLE_MKNOD:-0}" == "1" ]; then
- [ -n "$FEATURES" ] && FEATURES="$FEATURES,"
- FEATURES="${FEATURES}mknod=1"
- fi
-
- # Mount filesystem types (user configurable via advanced settings)
- if [ -n "${ALLOW_MOUNT_FS:-}" ]; then
- # Sanitize: strip spaces, trailing/leading commas, then convert commas to semicolons
- local _mount_clean="${ALLOW_MOUNT_FS// /}"
- _mount_clean="${_mount_clean%%,}"
- _mount_clean="${_mount_clean##,}"
- _mount_clean="${_mount_clean%%;}"
- _mount_clean="${_mount_clean//,/;}"
- if [ -n "$_mount_clean" ]; then
- [ -n "$FEATURES" ] && FEATURES="$FEATURES,"
- FEATURES="${FEATURES}mount=${_mount_clean}"
- fi
- fi
-
- # Build PCT_OPTIONS as string for export
- local _func_url
- if [ "$var_os" == "alpine" ]; then
- _func_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func"
- else
- _func_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func"
- fi
- export FUNCTIONS_FILE_PATH="$(curl -fsSL "$_func_url")"
- if [[ -z "$FUNCTIONS_FILE_PATH" || ${#FUNCTIONS_FILE_PATH} -lt 100 ]]; then
- msg_error "Failed to download install functions from: $_func_url"
- exit 115
- fi
-
- # Core exports for install.func
- export DIAGNOSTICS="$DIAGNOSTICS"
- export RANDOM_UUID="$RANDOM_UUID"
- export EXECUTION_ID="$EXECUTION_ID"
- export SESSION_ID="$SESSION_ID"
- # Repo attribution + platform for container-side progress pings (the
- # container inherits the host's detection instead of re-detecting)
- export REPO_SOURCE="${REPO_SOURCE:-}"
- export REPO_SLUG="${REPO_SLUG:-}"
- export TELEMETRY_PLATFORM="pve"
- export CACHER="$APT_CACHER"
- export CACHER_IP="$APT_CACHER_IP"
- if [[ -n "${HTTP_PROXY:-}" ]]; then
- export HTTP_PROXY HTTPS_PROXY="$HTTP_PROXY" http_proxy="$HTTP_PROXY" https_proxy="$HTTP_PROXY"
- export NO_PROXY="${HTTP_NO_PROXY:-}" no_proxy="${HTTP_NO_PROXY:-}"
- export var_http_proxy="$HTTP_PROXY"
- export var_http_no_proxy="${HTTP_NO_PROXY:-}"
- fi
- export tz="$timezone"
- export APPLICATION="$APP"
- export app="$NSAPP"
- export PASSWORD="$PW"
- export VERBOSE="$VERBOSE"
- export SSH_ROOT="${SSH}"
- export SSH_AUTHORIZED_KEY
- export CTID="$CT_ID"
- export CTTYPE="$CT_TYPE"
- export ENABLE_FUSE="$ENABLE_FUSE"
- export ENABLE_TUN="$ENABLE_TUN"
- export PCT_OSTYPE="$var_os"
- export PCT_OSVERSION="$var_version"
- export PCT_ARCH="$(dpkg --print-architecture 2>/dev/null || uname -m)"
- export PCT_DISK_SIZE="$DISK_SIZE"
- export IPV6_METHOD="$IPV6_METHOD"
- export ENABLE_GPU="$ENABLE_GPU"
-
- # DEV_MODE exports (optional, for debugging)
- export BUILD_LOG="$BUILD_LOG"
- export INSTALL_LOG="/root/.install-${SESSION_ID}.log"
-
- # Keep host-side logging on BUILD_LOG (not exported — invisible to container)
- # Without this, get_active_logfile() would return INSTALL_LOG (a container path)
- # and all host msg_info/msg_ok/msg_error would write to /root/.install-SESSION.log
- # on the HOST instead of BUILD_LOG, causing incomplete telemetry logs.
- _HOST_LOGFILE="$BUILD_LOG"
-
- export dev_mode="${dev_mode:-}"
- export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}"
- export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}"
- 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=" -hostname $HN"
-
- # Only add -tags if TAGS is not empty
- if [ -n "$TAGS" ]; then
- PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
- -tags $TAGS"
- fi
-
- # Only add -features if FEATURES is not empty
- if [ -n "$FEATURES" ]; then
- PCT_OPTIONS_STRING=" -features $FEATURES
-$PCT_OPTIONS_STRING"
- fi
-
- # Add searchdomain 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 (map Etc/* to "host" as pct doesn't accept them)
- if [ -n "${CT_TIMEZONE:-}" ]; then
- local _pct_timezone="$CT_TIMEZONE"
- [[ "$_pct_timezone" == Etc/* ]] && _pct_timezone="host"
- PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
- -timezone $_pct_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:-}"
-
- # Validate storage space only if CONTAINER_STORAGE is already set
- # (Storage selection happens in create_lxc_container for some modes)
- if [[ -n "$CONTAINER_STORAGE" ]]; then
- msg_info "Validating storage space"
- if ! validate_storage_space "$CONTAINER_STORAGE" "$DISK_SIZE" "no"; then
- local free_space
- free_space=$(pvesm status 2>/dev/null | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
- local free_fmt
- free_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free_space" 2>/dev/null || echo "${free_space}KB")
- msg_error "Not enough space on '$CONTAINER_STORAGE'. Required: ${DISK_SIZE}GB, Available: ${free_fmt}"
- exit 214
- fi
- msg_ok "Storage space validated"
- check_storage_health "$CONTAINER_STORAGE" "$DISK_SIZE"
- fi
-
- create_lxc_container || exit $?
-
- # Transition to 'configuring' — container created, now setting up OS/userland
- post_progress_to_api "configuring"
-
- LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
-
- # ============================================================================
- # GPU/USB PASSTHROUGH CONFIGURATION
- # ============================================================================
-
- # Check if GPU passthrough is enabled
- # Returns true only if var_gpu is explicitly set to "yes"
- # Can be set via:
- # - Environment variable: var_gpu=yes bash -c "..."
- # - CT script default: var_gpu="${var_gpu:-no}"
- # - Advanced settings wizard
- # - App defaults file: /usr/local/community-scripts/defaults/.vars
- is_gpu_app() {
- [[ "${var_gpu:-no}" == "yes" ]] && return 0
- return 1
- }
-
- # Detect all available GPU devices
- detect_gpu_devices() {
- INTEL_DEVICES=()
- AMD_DEVICES=()
- NVIDIA_DEVICES=()
-
- # Store PCI info to avoid multiple calls
- # grep returns exit 1 when no match — use || true to prevent ERR trap
- local pci_vga_info
- pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D" || true)
-
- # No GPU-related PCI devices at all? Skip silently.
- if [[ -z "$pci_vga_info" ]]; then
- msg_debug "No VGA/Display/3D PCI devices found"
- return 0
- fi
-
- # Map each DRI render/card node to its owning GPU via the PCI vendor ID
- # exposed in sysfs (/sys/class/drm//device/vendor). This is required
- # on multi-GPU hosts: globbing every /dev/dri node onto the first detected
- # vendor would pass through *both* GPUs and skip the selection prompt.
- if [[ -d /dev/dri ]]; then
- for d in /dev/dri/renderD* /dev/dri/card*; do
- [[ -e "$d" ]] || continue
- local node vendor
- node=$(basename "$d")
- vendor=$(cat "/sys/class/drm/${node}/device/vendor" 2>/dev/null || true)
- case "$vendor" in
- 0x8086) INTEL_DEVICES+=("$d") ;; # Intel
- 0x1002 | 0x1022) AMD_DEVICES+=("$d") ;; # AMD/ATI
- 0x10de) ;; # NVIDIA - handled via /dev/nvidia* below
- *) msg_debug "Unmapped GPU vendor '${vendor:-unknown}' for $d" ;;
- esac
- done
- fi
-
- [[ ${#INTEL_DEVICES[@]} -gt 0 ]] && msg_custom "🎮" "${BL}" "Detected Intel GPU"
- [[ ${#AMD_DEVICES[@]} -gt 0 ]] && msg_custom "🎮" "${RD}" "Detected AMD GPU"
-
- # AMD compute (ROCm) additionally needs /dev/kfd alongside the render nodes
- if [[ ${#AMD_DEVICES[@]} -gt 0 && -e /dev/kfd ]]; then
- AMD_DEVICES+=("/dev/kfd")
- fi
-
- # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
- if grep -q "\[10de:" <<<"$pci_vga_info"; then
- msg_custom "🎮" "${GN}" "Detected NVIDIA GPU"
-
- # Simple passthrough - just bind /dev/nvidia* devices if they exist
- # Only include character devices (-c), skip directories like /dev/nvidia-caps
- for d in /dev/nvidia*; do
- [[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
- done
- # Also check for devices inside /dev/nvidia-caps/ directory
- if [[ -d /dev/nvidia-caps ]]; then
- for d in /dev/nvidia-caps/*; do
- [[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
- done
- fi
-
- 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"
-# Automatic USB passthrough (privileged container)
-lxc.cgroup2.devices.allow: a
-lxc.cap.drop:
-lxc.cgroup2.devices.allow: c 188:* rwm
-lxc.cgroup2.devices.allow: c 189:* rwm
-lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
-lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
-lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
-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
- msg_ok "USB passthrough configured"
- }
-
- # Configure GPU passthrough
- configure_gpu_passthrough() {
- # Skip if:
- # GPU passthrough is enabled when var_gpu="yes":
- # - Set via environment variable: var_gpu=yes bash -c "..."
- # - Set in CT script: var_gpu="${var_gpu:-no}"
- # - Enabled in advanced_settings wizard
- # - Configured in app defaults file
- if ! is_gpu_app "$APP"; then
- return 0
- fi
-
- detect_gpu_devices
-
- # Count available GPU types
- local gpu_count=0
- local available_gpus=()
-
- if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
- available_gpus+=("INTEL")
- gpu_count=$((gpu_count + 1))
- fi
-
- if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
- available_gpus+=("AMD")
- gpu_count=$((gpu_count + 1))
- fi
-
- if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
- available_gpus+=("NVIDIA")
- gpu_count=$((gpu_count + 1))
- fi
-
- if [[ $gpu_count -eq 0 ]]; then
- msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough"
- return 0
- fi
-
- 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 >"$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
-
- # 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
-
- # Increase disk size for AMD ROCm runtime (~4GB extra needed)
- if [[ "${GPU_TYPE:-}" == "AMD" ]]; then
- local rocm_extra=4
- local new_disk_size=$((PCT_DISK_SIZE + rocm_extra))
- if pct resize "$CTID" rootfs "${new_disk_size}G" >/dev/null 2>&1; then
- msg_ok "Disk resized ${PCT_DISK_SIZE}GB → ${new_disk_size}GB for ROCm"
- else
- msg_warn "Failed to resize disk for ROCm — installation may fail if space is insufficient"
- fi
- fi
-
- # ============================================================================
- # START CONTAINER AND INSTALL USERLAND
- # ============================================================================
-
- msg_info "Starting LXC Container"
- pct start "$CTID"
-
- # 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"
- break
- fi
- sleep 1
- if [ "$i" -eq 10 ]; then
- local ct_status
- ct_status=$(pct status "$CTID" 2>/dev/null || echo "unknown")
- msg_error "LXC Container did not reach running state (status: ${ct_status})"
- exit 117
- fi
- done
-
- # Wait for network (skip for Alpine initially)
- if [ "$var_os" != "alpine" ]; then
- msg_info "Waiting for network in LXC container"
-
- # Wait for IP assignment (IPv4 or IPv6)
- local ip_in_lxc=""
- for i in {1..60}; do
- # Try IPv4 first
- ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
- # Fallback to IPv6 if IPv4 not available
- 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
- # Progressive backoff: 1s for first 20, 2s for next 20, 3s for last 20
- if [ "$i" -le 20 ]; then
- sleep 1
- elif [ "$i" -le 40 ]; then
- sleep 2
- else
- sleep 3
- fi
- done
-
- if [ -z "$ip_in_lxc" ]; then
- msg_error "No IP assigned to CT $CTID after 60 attempts"
- msg_custom "🔧" "${YW}" "Troubleshooting:"
- echo " • Verify bridge ${BRG} exists and has connectivity"
- echo " • Check if DHCP server is reachable (if using DHCP)"
- echo " • Verify static IP configuration (if using static IP)"
- echo " • Check Proxmox firewall rules"
- echo " • If using Tailscale: Disable MagicDNS temporarily"
- exit 118
- fi
-
- # 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 - installation will continue"
- else
- msg_ok "Network in LXC is reachable (ping)"
- fi
- 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
-
- # Fix Debian 13 LXC template bug where / is owned by nobody:nogroup
- # This must be done from the host as unprivileged containers cannot chown /
- local rootfs
- rootfs=$(pct config "$CTID" | grep -E '^rootfs:' | sed 's/rootfs: //' | cut -d',' -f1 || true)
- if [[ -n "$rootfs" ]]; then
- local mount_point="/var/lib/lxc/${CTID}/rootfs"
- if [[ -d "$mount_point" ]] && [[ "$(stat -c '%U' "$mount_point")" != "root" ]]; then
- chown root:root "$mount_point" 2>/dev/null || true
- fi
- fi
-
- # Continue with standard container setup
- msg_info "Customizing LXC Container"
-
- # # Install GPU userland if configured
- # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
- # install_gpu_userland "VAAPI"
- # fi
-
- # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
- # install_gpu_userland "NVIDIA"
- # fi
-
- # Disable error trap for entire customization & install phase.
- # All errors are handled explicitly — recovery menu shown on failure.
- # Without this, customization errors (e.g. container stopped during base package
- # install) would trigger error_handler() with a simple "Remove broken container?"
- # prompt instead of the full recovery menu with retry/repair options.
- set +Eeuo pipefail
- trap - ERR
-
- local install_exit_code=0
-
- _apply_http_proxy_in_container
- _apply_host_ca_certs_in_container
-
- # Continue with standard container setup
- if [ "$var_os" == "alpine" ]; then
- sleep 3
- pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories
-https://dl-cdn.alpinelinux.org/alpine/latest-stable/main
-https://dl-cdn.alpinelinux.org/alpine/latest-stable/community
-EOF'
- pct exec "$CTID" -- ash -c 'set -a; [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh; set +a; apk add bash newt curl openssh nano mc ncurses jq' >>"$BUILD_LOG" 2>&1 || {
- msg_warn "apk install failed (dl-cdn.alpinelinux.org), trying alternate mirrors..."
- local alpine_exit=0
- pct exec "$CTID" -- ash -c '
- set -a
- [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh
- set +a
- ALPINE_MIRRORS="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org"
- for m in $(printf "%s\n" $ALPINE_MIRRORS | shuf); do
- if wget -q --spider --timeout=2 "http://$m/alpine/latest-stable/main/" 2>/dev/null; then
- echo " Attempting mirror: $m"
- cat </etc/apk/repositories
-http://$m/alpine/latest-stable/main
-http://$m/alpine/latest-stable/community
-EOF
- if apk update >/dev/null 2>&1 && apk add bash newt curl openssh nano mc ncurses jq >/dev/null 2>&1; then
- echo " CDN set to $m: tests passed"
- exit 0
- else
- echo " Mirror $m failed"
- fi
- fi
- done
- exit 2
- ' && alpine_exit=0 || alpine_exit=$?
- if [[ $alpine_exit -ne 0 ]]; then
- msg_error "Failed to install base packages in Alpine container"
- install_exit_code=1
- fi
- }
- else
- sleep 3
- LANG=${LANG:-en_US.UTF-8}
- local LANG_ESC="${LANG//./\\.}"
- LANG_ESC="${LANG_ESC//|/\\|}"
- pct exec "$CTID" -- bash -c "sed -i \"/$LANG_ESC/ s/^# //\" /etc/locale.gen"
- pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
- echo LANG=\$locale_line >/etc/default/locale && \
- locale-gen >/dev/null && \
- export LANG=\$locale_line"
-
- if [[ -z "${tz:-}" ]]; then
- tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "UTC")
- fi
- [[ "${tz:-}" == Etc/* ]] && tz="UTC" # Normalize Etc/* to UTC for container setup
-
- if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
- # 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
-
- local _base_pkgs="sudo curl mc gnupg2 jq"
- if [[ "${ARCH:-amd64}" == "arm64" ]]; then
- _base_pkgs+=" openssh-server wget gcc"
- fi
-
- # Detect broken DNS resolver (e.g. Tailscale MagicDNS) and inject public DNS
- if ! pct exec "$CTID" -- bash -c "getent hosts deb.debian.org >/dev/null 2>&1 && getent hosts archive.ubuntu.com >/dev/null 2>&1"; then
- msg_warn "APT repository DNS resolution failed in container, injecting public DNS servers"
- pct exec "$CTID" -- bash -c "echo -e 'nameserver 8.8.8.8\nnameserver 1.1.1.1' >/etc/resolv.conf"
- fi
-
- pct exec "$CTID" -- bash -c "set -a; [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh; set +a; apt-get update 2>&1 && apt-get install -y ${_base_pkgs} 2>&1" >>"$BUILD_LOG" 2>&1 || {
- local failed_mirror
- failed_mirror=$(pct exec "$CTID" -- bash -c "grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null" 2>/dev/null || echo "unknown")
- msg_warn "apt-get update failed (${failed_mirror})."
- msg_custom "ℹ️" "${YW}" "Probing alternate mirrors (this can take 1-2 minutes on network issues)"
- local mirror_exit=0
- pct exec "$CTID" -- env APT_BASE="$_base_pkgs" bash -c '
- set -a
- [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh
- set +a
- DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian")
- echo " Mirror fallback for distro: $DISTRO"
-
- if [ "$DISTRO" = "ubuntu" ]; then
- EU_MIRRORS="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de"
- US_MIRRORS="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu"
- AP_MIRRORS="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au"
- else
- EU_MIRRORS="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net"
- US_MIRRORS="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com"
- AP_MIRRORS="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in"
- fi
-
- TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC")
- case "$TZ" in
- Europe/*|Arctic/*) REGIONAL="$EU_MIRRORS"; OTHERS="$US_MIRRORS $AP_MIRRORS" ;;
- America/*) REGIONAL="$US_MIRRORS"; OTHERS="$EU_MIRRORS $AP_MIRRORS" ;;
- Asia/*|Australia/*|Pacific/*) REGIONAL="$AP_MIRRORS"; OTHERS="$EU_MIRRORS $US_MIRRORS" ;;
- *) REGIONAL=""; OTHERS="$EU_MIRRORS $US_MIRRORS $AP_MIRRORS" ;;
- esac
-
- echo "Acquire::By-Hash \"no\";" >/etc/apt/apt.conf.d/99no-by-hash
-
- try_mirrors() {
- for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
- [ -f "$src" ] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: http://${1}/|g; s|deb http[s]*://[^/]*/|deb http://${1}/|g" "$src"
- done
- rm -rf /var/lib/apt/lists/*
- APT_OUT=$(apt-get update 2>&1)
- APT_RC=$?
- if echo "$APT_OUT" | grep -qi "hashsum\|hash sum"; then
- echo " Mirror $1 failed (hash mismatch)"
- return 1
- elif echo "$APT_OUT" | grep -qi "SSL\|certificate"; then
- echo " Mirror $1 failed (SSL/certificate error)"
- return 1
- elif [ $APT_RC -ne 0 ]; then
- echo " Mirror $1 failed (apt-get update error)"
- return 1
- elif apt-get install -y $APT_BASE >/dev/null 2>&1; then
- echo " CDN set to $1: tests passed"
- return 0
- else
- echo " Mirror $1 failed (package install error)"
- return 1
- fi
- }
-
- scan_reachable() {
- local result=""
- for m in $1; do
- if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
- result="$result $m"
- fi
- done
- echo "$result" | xargs
- }
-
- # Phase 1: Scan global mirrors first (independent of local CDN issues)
- echo " Phase 1/3: Scanning global mirrors for reachability..."
- OTHERS_OK=$(scan_reachable "$OTHERS")
- OTHERS_PICK=$(printf "%s\n" $OTHERS_OK | shuf | head -3 | xargs)
-
- if [ -z "$OTHERS_PICK" ]; then
- echo " No reachable global mirrors found"
- fi
-
- for mirror in $OTHERS_PICK; do
- echo " Attempting mirror: $mirror"
- try_mirrors "$mirror" && exit 0
- done
-
- # Phase 2: Try primary mirror
- echo " Phase 2/3: Trying primary mirror..."
- if [ "$DISTRO" = "ubuntu" ]; then
- PRIMARY="archive.ubuntu.com"
- else
- PRIMARY="ftp.debian.org"
- fi
- if timeout 2 bash -c "echo >/dev/tcp/$PRIMARY/80" 2>/dev/null; then
- echo " Attempting mirror: $PRIMARY"
- try_mirrors "$PRIMARY" && exit 0
- fi
-
- # Phase 3: Fall back to regional mirrors
- echo " Phase 3/3: Scanning regional mirrors..."
- REGIONAL_OK=$(scan_reachable "$REGIONAL")
- REGIONAL_PICK=$(printf "%s\n" $REGIONAL_OK | shuf | head -3 | xargs)
-
- if [ -z "$REGIONAL_PICK" ]; then
- echo " No reachable regional mirrors found"
- fi
-
- for mirror in $REGIONAL_PICK; do
- echo " Attempting mirror: $mirror"
- try_mirrors "$mirror" && exit 0
- done
-
- exit 2
- ' && mirror_exit=0 || mirror_exit=$?
- if [[ $mirror_exit -eq 2 ]]; then
- msg_warn "Multiple mirrors failed (possible CDN synchronization issue)."
- if [[ "$var_os" == "ubuntu" ]]; then
- msg_warn "Find Ubuntu mirrors at: https://launchpad.net/ubuntu/+archivemirrors"
- else
- msg_warn "Find Debian mirrors at: https://www.debian.org/mirror/list"
- fi
- local custom_mirror=""
- while true; do
- read -rp " Enter a mirror hostname (or 'skip' to abort): " custom_mirror /dev/null 2>&1 && apt-get install -y ${_base_pkgs} >/dev/null 2>&1
- " && break
- msg_warn "Mirror '${custom_mirror}' also failed. Try another or type 'skip'."
- done
- if [[ "$custom_mirror" == "skip" ]]; then
- msg_warn "Mirror selection aborted by user ('skip')"
- msg_warn "Aborting installation before base package bootstrap"
- post_update_to_api "aborted" "130" "force"
- install_exit_code=130
- fi
- elif [[ $mirror_exit -ne 0 ]]; then
- msg_error "apt-get base packages installation failed"
- install_exit_code=100
- fi
- }
- fi
-
- # Only continue with installation if customization succeeded
- if [[ $install_exit_code -eq 0 ]]; then
- msg_ok "Customized LXC Container"
-
- # Optional DNS override for retry scenarios (inside LXC, never on host)
- if [[ "${DNS_RETRY_OVERRIDE:-false}" == "true" ]]; then
- msg_info "Applying DNS retry override in LXC (8.8.8.8, 1.1.1.1)"
- pct exec "$CTID" -- bash -c "printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' >/etc/resolv.conf" >/dev/null 2>&1 || true
- msg_ok "DNS override applied in LXC"
- fi
-
- # Install SSH keys
- install_ssh_keys_into_ct
-
- # Start timer for duration tracking
- start_install_timer
-
- # Run application installer
- # Error handling already disabled above (before customization phase)
-
- # Signal handlers use this flag to stop the container on abort (SIGHUP/SIGINT/SIGTERM)
- # Without this, SSH disconnects leave the container running as an orphan process
- # that sends "configuring" status AFTER the host already reported "failed"
- export CONTAINER_INSTALLING=true
-
- local _install_script
- _install_script="$(curl -fsSL "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh")"
- lxc-attach -n "$CTID" -- bash -c "$_install_script"
- local lxc_exit=$?
-
- unset CONTAINER_INSTALLING
-
- # Keep error handling DISABLED during failure detection and recovery
- # Re-enabling it here would cause any pct exec/pull failure to trigger
- # error_handler() on the host, bypassing the recovery menu entirely
-
- # Check for error flag file in container (more reliable than lxc-attach exit code)
- if [[ -n "${SESSION_ID:-}" ]]; then
- local error_flag="/root/.install-${SESSION_ID}.failed"
- if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then
- install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1")
- pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true
- fi
- fi
-
- # Fallback to lxc-attach exit code if no flag file
- if [[ $install_exit_code -eq 0 && ${lxc_exit:-0} -ne 0 ]]; then
- install_exit_code=${lxc_exit:-0}
- fi
- fi # end: if [[ $install_exit_code -eq 0 ]] (customization succeeded)
-
- # Installation or customization failed?
- if [[ $install_exit_code -ne 0 ]]; then
- # Prevent job-control signals from suspending the script during recovery.
- # In non-interactive shells (bash -c), background processes (spinner) can
- # trigger terminal-related signals that stop the entire process group.
- # TSTP = Ctrl+Z, TTIN = bg read from tty, TTOU = bg write to tty (tostop)
- trap '' TSTP TTIN TTOU
-
- # lxc-attach left us in a background process group - claim the terminal back
- # before we print anything. Note this does NOT hold: the log collection
- # below uses pct pull/exec, which take it away again, so every interactive
- # prompt has to reclaim it again right before its read.
- reclaim_tty
-
- msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})"
-
- # Copy install log from container BEFORE API call so get_error_text() can read it
- local build_log_copied=false
- local install_log_copied=false
- local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
-
- if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
- # Create combined log with header
- {
- echo "================================================================================"
- echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
- echo "Container ID: ${CTID}"
- echo "Session ID: ${SESSION_ID}"
- echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
- echo "================================================================================"
- echo ""
- } >"$combined_log"
-
- # Append BUILD_LOG (host-side creation log) if it exists
- if [[ -f "${BUILD_LOG}" ]]; then
- {
- echo "================================================================================"
- echo "PHASE 1: CONTAINER CREATION (Host)"
- echo "================================================================================"
- cat "${BUILD_LOG}"
- echo ""
- } >>"$combined_log"
- build_log_copied=true
- fi
-
- # Copy and append INSTALL_LOG from container
- local temp_install_log="/tmp/.install-temp-${SESSION_ID}.log"
- if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_install_log" 2>/dev/null; then
- {
- echo "================================================================================"
- echo "PHASE 2: APPLICATION INSTALLATION (Container)"
- echo "================================================================================"
- cat "$temp_install_log"
- echo ""
- } >>"$combined_log"
- rm -f "$temp_install_log"
- install_log_copied=true
- # Point INSTALL_LOG to combined log so get_full_log() finds it
- INSTALL_LOG="$combined_log"
- fi
-
- # Pull the structured error capture (.errinfo) from the container.
- # It contains EXACTLY the output of the command that failed (written by
- # silent()/error_handler inside the container) and is the primary source
- # for the telemetry error trace - instead of a generic log tail.
- local host_errinfo="/tmp/.errinfo-${SESSION_ID}"
- if timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log.errinfo" "$host_errinfo" 2>/dev/null && [[ -s "$host_errinfo" ]]; then
- TELEMETRY_ERRINFO="$host_errinfo"
- export TELEMETRY_ERRINFO
- fi
- fi
-
- # Defense-in-depth: Ensure error handling stays disabled during recovery.
- # Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail
- # and trap 'error_handler' ERR. If any code path above called such a function,
- # the grep/sed pipelines below would trigger error_handler on non-match (exit 1).
- set +Eeuo pipefail
- trap - ERR
-
- # --- Exit code 1 subclassification: analyze logs BEFORE telemetry call ---
- # Exit code 1 is generic ("General error"). Analyze logs to determine the
- # real error category so telemetry gets a useful classification instead of "shell".
- local is_oom=false
- local is_network_issue=false
- local is_apt_issue=false
- local is_cmd_not_found=false
- local is_disk_full=false
-
- if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then
- if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' "$combined_log"; then
- is_apt_issue=true
- fi
- if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then
- is_oom=true
- fi
- if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then
- is_network_issue=true
- fi
- if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then
- is_cmd_not_found=true
- fi
- if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
- is_disk_full=true
- fi
- fi
-
- # Set override for categorize_error() so telemetry gets the real category
- if [[ "$is_apt_issue" == true ]]; then
- export ERROR_CATEGORY_OVERRIDE="dependency"
- elif [[ "$is_oom" == true ]]; then
- export ERROR_CATEGORY_OVERRIDE="resource"
- elif [[ "$is_network_issue" == true ]]; then
- export ERROR_CATEGORY_OVERRIDE="network"
- elif [[ "$is_disk_full" == true ]]; then
- export ERROR_CATEGORY_OVERRIDE="storage"
- elif [[ "$is_cmd_not_found" == true ]]; then
- export ERROR_CATEGORY_OVERRIDE="dependency"
- fi
-
- # Report failure to telemetry API (now with log available on host)
- # NOTE: Do NOT use msg_info/spinner here — the background spinner process
- # causes SIGTSTP in non-interactive shells (bash -c "$(curl ...)"), which
- # stops the entire process group and prevents the recovery dialog from appearing.
- $STD echo -e "${TAB}⏳ Reporting failure to telemetry..."
- post_update_to_api "failed" "$install_exit_code"
- $STD echo -e "${TAB}${CM:-✔} Failure reported"
-
- # Show combined log location
- if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
- msg_custom "📋" "${YW}" "Installation log: ${combined_log}"
- fi
-
- # Dev mode: Keep container or open breakpoint shell
- 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}"
- reclaim_tty
- if read -r response /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
- echo ""
-
- # Extend error detection for non-exit-1 codes (exit 1 was already analyzed above)
- # The is_* flags were set above for exit code 1 log analysis; here we add
- # exit-code-specific detections for other codes.
- local error_explanation=""
- if declare -f explain_exit_code >/dev/null 2>&1; then
- error_explanation="$(explain_exit_code "$install_exit_code")"
- fi
-
- # OOM detection: exit codes 134 (SIGABRT/heap), 137 (SIGKILL/OOM), 243 (Node.js heap)
- if [[ $install_exit_code -eq 134 || $install_exit_code -eq 137 || $install_exit_code -eq 243 ]]; then
- is_oom=true
- fi
-
- # APT/DPKG detection: exit codes 100-102 (APT), 255 (DPKG with log evidence)
- case "$install_exit_code" in
- 100 | 101 | 102) is_apt_issue=true ;;
- 255)
- if [[ -f "$combined_log" ]] && grep -qiE 'dpkg|apt-get|apt\.conf|broken packages|unmet dependencies|E: Sub-process|E: Failed' "$combined_log"; then
- is_apt_issue=true
- fi
- ;;
- esac
-
- # Disk full / ENOSPC detection: errno -28 (ENOSPC), exit 228 (custom handler), exit 23 (curl write error)
- if [[ $install_exit_code -eq 228 || $install_exit_code -eq 23 ]]; then
- is_disk_full=true
- fi
- if [[ -f "$combined_log" ]] && grep -qiE 'ENOSPC|no space left on device|No space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
- is_disk_full=true
- fi
-
- # Command not found detection
- if [[ $install_exit_code -eq 127 ]]; then
- is_cmd_not_found=true
- fi
-
- # Network-related detection (curl/apt/git fetch failures and transient network issues)
- case "$install_exit_code" in
- 6 | 7 | 22 | 28 | 35 | 52 | 56 | 57 | 75 | 78) is_network_issue=true ;;
- 100)
- # APT can fail due to network (Failed to fetch)
- if [[ -f "$combined_log" ]] && grep -qiE 'Failed to fetch|Could not resolve|Connection failed|Network is unreachable|Temporary failure resolving' "$combined_log"; then
- is_network_issue=true
- fi
- ;;
- 128)
- if [[ -f "$combined_log" ]] && grep -qiE 'RPC failed|early EOF|fetch-pack|HTTP/2 stream|Could not resolve host|Temporary failure resolving|Failed to fetch|Connection reset|Network is unreachable' "$combined_log"; then
- is_network_issue=true
- fi
- ;;
- esac
-
- # Show error explanation if available
- if [[ -n "$error_explanation" ]]; then
- echo -e "${TAB}${RD}Error: ${error_explanation}${CL}"
- echo ""
- fi
-
- # Show specific hints for known error types
- if [[ $install_exit_code -eq 10 ]]; then
- echo -e "${TAB}${INFO} This error usually means the container needs ${GN}privileged${CL} mode or Docker/nesting support."
- echo -e "${TAB}${INFO} Recreate with: Advanced Install → Container Type: ${GN}Privileged${CL}"
- echo ""
- fi
-
- if [[ $install_exit_code -eq 125 || $install_exit_code -eq 126 ]]; then
- echo -e "${TAB}${INFO} The command exists but cannot be executed. This may be a ${GN}permission${CL} issue."
- echo -e "${TAB}${INFO} If using Docker, ensure the container is ${GN}privileged${CL} or has correct permissions."
- echo ""
- fi
-
- if [[ "$is_disk_full" == true ]]; then
- echo -e "${TAB}${INFO} The container ran out of disk space during installation (${GN}ENOSPC${CL})."
- echo -e "${TAB}${INFO} Current disk size: ${GN}${DISK_SIZE} GB${CL}. A rebuild with doubled disk may resolve this."
- echo ""
- fi
-
- if [[ "$is_cmd_not_found" == true ]]; then
- local missing_cmd=""
- if [[ -f "$combined_log" ]]; then
- missing_cmd=$(grep -oiE '[a-zA-Z0-9_.-]+: command not found' "$combined_log" 2>/dev/null | tail -1 | sed 's/: command not found//') || true
- fi
- if [[ -n "$missing_cmd" ]]; then
- echo -e "${TAB}${INFO} Missing command: ${GN}${missing_cmd}${CL}"
- fi
- echo ""
- fi
-
- # Build recovery menu based on error type
- echo -e "${YW}What would you like to do?${CL}"
- echo ""
- echo -e " ${GN}1)${CL} Remove container and exit"
- echo -e " ${GN}2)${CL} Keep container for debugging"
- echo -e " ${GN}3)${CL} Retry with verbose mode (full rebuild)"
-
- local next_option=4
- local APT_OPTION="" OOM_OPTION="" DNS_OPTION="" DISK_OPTION=""
-
- if [[ "$is_apt_issue" == true ]]; then
- if [[ "$var_os" == "alpine" ]]; then
- echo -e " ${GN}${next_option})${CL} Repair APK state and re-run install (in-place)"
- else
- echo -e " ${GN}${next_option})${CL} Repair APT/DPKG state and re-run install (in-place)"
- fi
- APT_OPTION=$next_option
- next_option=$((next_option + 1))
- fi
-
- if [[ "$is_oom" == true ]]; then
- local recovery_attempt="${RECOVERY_ATTEMPT:-0}"
- if [[ $recovery_attempt -lt 2 ]]; then
- local new_ram=$((RAM_SIZE * 2))
- local new_cpu=$((CORE_COUNT * 2))
- echo -e " ${GN}${next_option})${CL} Retry with more resources (RAM: ${RAM_SIZE}→${new_ram} MiB, CPU: ${CORE_COUNT}→${new_cpu} cores)"
- OOM_OPTION=$next_option
- next_option=$((next_option + 1))
- else
- echo -e " ${DGN}-)${CL} ${DGN}OOM retry exhausted (already retried ${recovery_attempt}x)${CL}"
- fi
- fi
-
- if [[ "$is_disk_full" == true ]]; then
- local disk_recovery_attempt="${DISK_RECOVERY_ATTEMPT:-0}"
- if [[ $disk_recovery_attempt -lt 2 ]]; then
- local new_disk=$((DISK_SIZE * 2))
- echo -e " ${GN}${next_option})${CL} Retry with more disk space (Disk: ${DISK_SIZE}→${new_disk} GB)"
- DISK_OPTION=$next_option
- next_option=$((next_option + 1))
- else
- echo -e " ${DGN}-)${CL} ${DGN}Disk resize retry exhausted (already retried ${disk_recovery_attempt}x)${CL}"
- fi
- fi
-
- if [[ "$is_network_issue" == true ]]; then
- echo -e " ${GN}${next_option})${CL} Retry with DNS override in LXC (8.8.8.8 / 1.1.1.1)"
- DNS_OPTION=$next_option
- next_option=$((next_option + 1))
- fi
-
- local max_option=$((next_option - 1))
-
- echo ""
- echo -en "${YW}Select option [1-${max_option}] (default: 1, auto-remove in 60s): ${CL}"
-
- local response=""
- local read_rc
- reclaim_tty
- read -t 60 -r response /dev/null || true
- pct destroy "$CTID" &>/dev/null || true
- echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
- ;;
- 2)
- 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
- exit $install_exit_code
- ;;
- 3)
- # Retry with verbose mode (full rebuild)
- echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild...${CL}"
- pct stop "$CTID" &>/dev/null || true
- pct destroy "$CTID" &>/dev/null || true
- echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
- echo ""
- # Get new container ID
- local old_ctid="$CTID"
- export CTID=$(get_valid_container_id "$CTID")
- export VERBOSE="yes"
- export var_verbose="yes"
-
- # Show rebuild summary
- echo -e "${YW}Rebuilding with preserved settings:${CL}"
- echo -e " Container ID: ${old_ctid} → ${CTID}"
- echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores | Disk: ${DISK_SIZE} GB"
- echo -e " Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
- echo -e " Verbose: ${GN}enabled${CL}"
- echo ""
- msg_info "Restarting installation..."
- # New telemetry execution for the retry (previous one keeps its "failed")
- declare -f telemetry_new_attempt &>/dev/null && telemetry_new_attempt
- # Re-run build_container
- build_container
- return $?
- ;;
- *)
- # Handle dynamic smart recovery options via named option variables
- local handled=false
-
- if [[ -n "${APT_OPTION}" && "${response}" == "${APT_OPTION}" ]]; then
- # Package manager in-place repair: fix broken state and re-run install script
- handled=true
- if [[ "$var_os" == "alpine" ]]; then
- echo -e "\n${TAB}${HOLD}${YW}Repairing APK state in container ${CTID}...${CL}"
- pct exec "$CTID" -- ash -c "
- apk fix 2>/dev/null || true
- apk cache clean 2>/dev/null || true
- apk update 2>/dev/null || true
- " >/dev/null 2>&1 || true
- echo -e "${BFR}${CM}${GN}APK state repaired in container ${CTID}${CL}"
- else
- echo -e "\n${TAB}${HOLD}${YW}Repairing APT/DPKG state in container ${CTID}...${CL}"
- pct exec "$CTID" -- bash -c "
- DEBIAN_FRONTEND=noninteractive dpkg --configure -a 2>/dev/null || true
- apt-get -f install -y 2>/dev/null || true
- apt-get clean 2>/dev/null
- apt-get update 2>/dev/null || true
- " >/dev/null 2>&1 || true
- echo -e "${BFR}${CM}${GN}APT/DPKG state repaired in container ${CTID}${CL}"
- fi
- echo ""
- export VERBOSE="yes"
- export var_verbose="yes"
-
- echo -e "${YW}Re-running installation in existing container ${CTID}:${CL}"
- echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores | Disk: ${DISK_SIZE} GB"
- echo -e " Verbose: ${GN}enabled${CL}"
- echo ""
- msg_info "Re-running installation script..."
-
- # New telemetry execution for the in-place retry
- declare -f telemetry_new_attempt &>/dev/null && telemetry_new_attempt
- declare -f post_to_api &>/dev/null && post_to_api 2>/dev/null || true
-
- # Re-run install script in existing container (don't destroy/recreate)
- set +Eeuo pipefail
- trap - ERR
- local _install_script
- _install_script="$(curl -fsSL "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh")"
- lxc-attach -n "$CTID" -- bash -c "$_install_script"
- local apt_retry_exit=$?
- set -Eeuo pipefail
- trap 'error_handler' ERR
-
- # Check for error flag from retry
- local apt_retry_code=0
- if [[ -n "${SESSION_ID:-}" ]]; then
- local retry_error_flag="/root/.install-${SESSION_ID}.failed"
- if pct exec "$CTID" -- test -f "$retry_error_flag" 2>/dev/null; then
- apt_retry_code=$(pct exec "$CTID" -- cat "$retry_error_flag" 2>/dev/null || echo "1")
- pct exec "$CTID" -- rm -f "$retry_error_flag" 2>/dev/null || true
- fi
- fi
-
- if [[ $apt_retry_code -eq 0 && $apt_retry_exit -ne 0 ]]; then
- apt_retry_code=$apt_retry_exit
- fi
-
- if [[ $apt_retry_code -eq 0 ]]; then
- msg_ok "Installation completed successfully after APT repair!"
- INSTALL_COMPLETE=true
- post_update_to_api "done" "0" "force"
- return 0
- else
- msg_error "Installation still failed after APT repair (exit code: ${apt_retry_code})"
- install_exit_code=$apt_retry_code
- fi
- fi
-
- if [[ -n "${OOM_OPTION}" && "${response}" == "${OOM_OPTION}" ]]; then
- # Retry with doubled resources
- handled=true
- echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with more resources...${CL}"
- pct stop "$CTID" &>/dev/null || true
- pct destroy "$CTID" &>/dev/null || true
- echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
- echo ""
- local old_ctid="$CTID"
- local old_ram="$RAM_SIZE"
- local old_cpu="$CORE_COUNT"
- export CTID=$(get_valid_container_id "$CTID")
- export RAM_SIZE=$((RAM_SIZE * 2))
- export CORE_COUNT=$((CORE_COUNT * 2))
- export var_ram="$RAM_SIZE"
- export var_cpu="$CORE_COUNT"
- export VERBOSE="yes"
- export var_verbose="yes"
- export RECOVERY_ATTEMPT=$((${RECOVERY_ATTEMPT:-0} + 1))
-
- echo -e "${YW}Rebuilding with increased resources (attempt ${RECOVERY_ATTEMPT}/2):${CL}"
- echo -e " Container ID: ${old_ctid} → ${CTID}"
- echo -e " RAM: ${old_ram} → ${GN}${RAM_SIZE}${CL} MiB (x2)"
- echo -e " CPU: ${old_cpu} → ${GN}${CORE_COUNT}${CL} cores (x2)"
- echo -e " Disk: ${DISK_SIZE} GB | Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
- echo -e " Verbose: ${GN}enabled${CL}"
- echo ""
- msg_info "Restarting installation..."
- declare -f telemetry_new_attempt &>/dev/null && telemetry_new_attempt
- build_container
- return $?
- fi
-
- if [[ -n "${DISK_OPTION}" && "${response}" == "${DISK_OPTION}" ]]; then
- # Retry with doubled disk size
- handled=true
- echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with more disk space...${CL}"
- pct stop "$CTID" &>/dev/null || true
- pct destroy "$CTID" &>/dev/null || true
- echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
- echo ""
- local old_ctid="$CTID"
- local old_disk="$DISK_SIZE"
- export CTID=$(get_valid_container_id "$CTID")
- export DISK_SIZE=$((DISK_SIZE * 2))
- export var_disk="$DISK_SIZE"
- export VERBOSE="yes"
- export var_verbose="yes"
- export DISK_RECOVERY_ATTEMPT=$((${DISK_RECOVERY_ATTEMPT:-0} + 1))
-
- echo -e "${YW}Rebuilding with increased disk space (attempt ${DISK_RECOVERY_ATTEMPT}/2):${CL}"
- echo -e " Container ID: ${old_ctid} → ${CTID}"
- echo -e " Disk: ${old_disk} → ${GN}${DISK_SIZE}${CL} GB (x2)"
- echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores"
- echo -e " Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
- echo -e " Verbose: ${GN}enabled${CL}"
- echo ""
- msg_info "Restarting installation..."
- declare -f telemetry_new_attempt &>/dev/null && telemetry_new_attempt
- build_container
- return $?
- fi
-
- if [[ -n "${DNS_OPTION}" && "${response}" == "${DNS_OPTION}" ]]; then
- # Retry with DNS override in LXC
- handled=true
- echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with DNS override...${CL}"
- pct stop "$CTID" &>/dev/null || true
- pct destroy "$CTID" &>/dev/null || true
- echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
- echo ""
- local old_ctid="$CTID"
- export CTID=$(get_valid_container_id "$CTID")
- export DNS_RETRY_OVERRIDE="true"
- export VERBOSE="yes"
- export var_verbose="yes"
-
- echo -e "${YW}Rebuilding with DNS override in LXC:${CL}"
- echo -e " Container ID: ${old_ctid} → ${CTID}"
- echo -e " DNS: ${GN}8.8.8.8, 1.1.1.1${CL} (inside LXC only)"
- echo -e " Verbose: ${GN}enabled${CL}"
- echo ""
- msg_info "Restarting installation..."
- declare -f telemetry_new_attempt &>/dev/null && telemetry_new_attempt
- build_container
- return $?
- fi
-
- if [[ "$handled" == false ]]; then
- echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}"
- exit $install_exit_code
- fi
- ;;
- esac
- elif [[ $read_rc -gt 128 ]]; then
- # Timeout - auto-remove
- echo ""
- msg_info "No response - removing container ${CTID}"
- pct stop "$CTID" &>/dev/null || true
- pct destroy "$CTID" &>/dev/null || true
- msg_ok "Container ${CTID} removed"
- else
- # read itself failed (e.g. broken/closed stdin, I/O error) rather than
- # timing out - don't guess and destroy the container on a read we
- # couldn't actually capture; keep it since that's the reversible choice.
- while read -r -t 0.1 -n 1 _ /dev/null; do :; done
- echo ""
- msg_error "Could not read your response (stdin error) - keeping container ${CTID} for safety."
- msg_error "Remove it manually if not needed: pct destroy ${CTID}"
- fi
-
- # Force one final status update attempt after cleanup
- # This ensures status is updated even if the first attempt failed (e.g., HTTP 400)
- $STD echo -e "${TAB}⏳ Finalizing telemetry report..."
- post_update_to_api "failed" "$install_exit_code" "force"
- $STD echo -e "${TAB}${CM:-✔} Telemetry finalized"
-
- # Restore default job-control signal handling before exit
- trap - TSTP TTIN TTOU
- exit $install_exit_code
- fi
-
- # Re-enable error handling after successful install or recovery menu completion
- set -Eeuo pipefail
- trap 'error_handler' ERR
-}
-
-destroy_lxc() {
- if [[ -z "$CT_ID" ]]; then
- msg_error "No CT_ID found. Nothing to remove."
- return 65
- fi
-
- # Abort on Ctrl-C / Ctrl-D / ESC
- trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
-
- local prompt
- reclaim_tty
- if ! read -rp "Remove this Container? " prompt /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
-# ------------------------------------------------------------------------------
-resolve_storage_preselect() {
- local class="$1" preselect="$2" required_content=""
- case "$class" in
- template) required_content="vztmpl" ;;
- container) required_content="rootdir" ;;
- *) return 65 ;;
- 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 238
- 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 --from-unit=1024 --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
- used_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
- free_h="$(numfmt --to=iec --from-unit=1024 --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
-
- # Validate GIDs are numeric before sed
- [[ "$render_gid" =~ ^[0-9]+$ ]] || render_gid="104"
- [[ "$video_gid" =~ ^[0-9]+$ ]] || video_gid="44"
-
- # 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"
- sed -i -E "s|(dev[0-9]+: /dev/kfd),gid=[0-9]+|\1,gid=${render_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" -- sh -c "
- if [ -d /dev/dri ]; then
- for dev in /dev/dri/*; do
- if [ -e \"\$dev\" ]; then
- case \"\$dev\" in
- *renderD*) chgrp ${render_gid} \"\$dev\" 2>/dev/null || true ;;
- *) chgrp ${video_gid} \"\$dev\" 2>/dev/null || true ;;
- esac
- chmod 660 \"\$dev\" 2>/dev/null || true
- fi
- done
- fi
- if [ -e /dev/kfd ]; then
- chgrp ${render_gid} /dev/kfd 2>/dev/null || true
- chmod 660 /dev/kfd 2>/dev/null || true
- 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 65
- ;;
- 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=1024 --format %.1f <<<"$USED")
- local FREE_FMT=$(numfmt --to=iec --from-unit=1024 --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 "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
-
- # Validate storage space for container storage
- if [[ "$CLASS" == "container" && -n "${DISK_SIZE:-}" ]]; then
- validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes"
- check_storage_health "$STORAGE_RESULT" "$DISK_SIZE"
- # Continue even if validation fails - user was warned
- fi
-
- return 0
- done
-}
-
-# ------------------------------------------------------------------------------
-# validate_storage_space()
-#
-# - Validates if storage has enough free space for container
-# - Takes storage name and required size in GB
-# - Returns 0 if enough space, 1 if not enough, 2 if storage unavailable
-# - Can optionally show whiptail warning
-# - Handles all storage types: dir, lvm, lvmthin, zfs, nfs, cifs, etc.
-# ------------------------------------------------------------------------------
-validate_storage_space() {
- local storage="$1"
- local required_gb="${2:-8}"
- local show_dialog="${3:-no}"
-
- # Get full storage line from pvesm status
- local storage_line
- storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}')
-
- # Check if storage exists and is active
- if [[ -z "$storage_line" ]]; then
- [[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' not found!\n\nThe storage may be unavailable or disabled." 10 60
- return 236
- fi
-
- # Check storage status (column 3)
- local status
- status=$(awk '{print $3}' <<<"$storage_line")
- if [[ "$status" == "disabled" ]]; then
- [[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' is disabled!\n\nPlease enable the storage first." 10 60
- return 236
- fi
-
- # Get storage type and free space (column 6)
- local storage_type storage_free
- storage_type=$(awk '{print $2}' <<<"$storage_line")
- storage_free=$(awk '{print $6}' <<<"$storage_line")
-
- # Some storage types (like PBS, iSCSI) don't report size info
- # In these cases, skip space validation
- if [[ -z "$storage_free" || "$storage_free" == "0" ]]; then
- # Silent pass for storages without size info
- return 0
- fi
-
- local required_kb=$((required_gb * 1024 * 1024))
- local free_gb_fmt
- free_gb_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$storage_free" 2>/dev/null || echo "${storage_free}KB")
-
- if [[ "$storage_free" -lt "$required_kb" ]]; then
- if [[ "$show_dialog" == "yes" ]]; then
- whiptail --msgbox "⚠️ Warning: Storage '$storage' may not have enough space!\n\nStorage Type: ${storage_type}\nRequired: ${required_gb}GB\nAvailable: ${free_gb_fmt}\n\nYou can continue, but creation might fail." 14 70
- fi
- return 236
- fi
-
- return 0
-}
-
-# ------------------------------------------------------------------------------
-# check_storage_health()
-#
-# - Warns or blocks when target storage is nearly full
-# - Complements validate_storage_space (free space vs. requested disk)
-# - Uses pvesm status usage when available
-# ------------------------------------------------------------------------------
-check_storage_health() {
- local storage="$1"
- local required_gb="${2:-8}"
- [[ -z "$storage" ]] && return 0
-
- local storage_line storage_type total_kb avail_kb used_pct free_gb_fmt
- storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}')
- [[ -z "$storage_line" ]] && return 0
-
- storage_type=$(awk '{print $2}' <<<"$storage_line")
- total_kb=$(awk '{print $4}' <<<"$storage_line")
- avail_kb=$(awk '{print $6}' <<<"$storage_line")
-
- [[ -z "$total_kb" || "$total_kb" == "0" || -z "$avail_kb" ]] && return 0
-
- used_pct=$(((total_kb - avail_kb) * 100 / total_kb))
- free_gb_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$avail_kb" 2>/dev/null || echo "${avail_kb}KB")
-
- if ((used_pct >= 95)); then
- msg_warn "Storage '${storage}' (${storage_type}) is ${used_pct}% full (${free_gb_fmt} free)"
- if ! is_unattended && command -v whiptail >/dev/null 2>&1 && [[ -t 0 ]]; then
- if ! whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "STORAGE ALMOST FULL" \
- --yesno "Storage '${storage}' is ${used_pct}% full.\n\nType: ${storage_type}\nAvailable: ${free_gb_fmt}\nRequired for CT: ${required_gb} GB\n\nContinue anyway?" 14 70; then
- msg_error "Installation cancelled – storage nearly full"
- exit 214
- fi
- elif ((used_pct >= 98)); then
- msg_error "Storage '${storage}' is ${used_pct}% full – refusing install in unattended mode"
- exit 214
- fi
- elif ((used_pct >= 85)); then
- msg_warn "Storage '${storage}' (${storage_type}) is ${used_pct}% full (${free_gb_fmt} free)"
- fi
-
- return 0
-}
-
-# ==============================================================================
-# SECTION 8: CONTAINER CREATION
-# ==============================================================================
-
-# ------------------------------------------------------------------------------
-# create_lxc_container()
-#
-# - Main function for creating LXC containers
-# - Handles all phases: validation, template discovery, container creation,
-# network config, storage, etc.
-# - Extensive error checking with detailed exit codes
-# ------------------------------------------------------------------------------
-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"; }
-
- # Switch to the previous OS major version template and retry pct create
- # Determines the fallback version automatically based on available templates.
- # Returns: 0 = success, 1 = failed
- fallback_to_previous_os_version() {
- local old_template="$TEMPLATE"
- local os_type="${PCT_OSTYPE:-}"
- local current_ver="${PCT_OSVERSION:-}"
-
- # Determine template search pattern based on OS type
- local tpl_pattern=""
- case "$os_type" in
- debian | ubuntu) tpl_pattern="-standard_" ;;
- alpine | fedora | rocky | centos) tpl_pattern="-default_" ;;
- *) tpl_pattern="" ;;
- esac
-
- msg_info "Searching for an older $os_type template (current: $os_type $current_ver)"
-
- # Collect all available versions for this OS type (local + online)
- local -a all_versions=()
-
- # Local templates
- mapfile -t _local_vers < <(
- pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
- awk -v os="$os_type" -v pat="$tpl_pattern" '$1 ~ ("^"os"|/"os) && $1 ~ pat {print $1}' |
- sed 's|.*/||' |
- sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" |
- sort -u -V
- )
- all_versions+=("${_local_vers[@]}")
-
- # Online templates (only if needed)
- if command -v timeout &>/dev/null; then
- timeout 30 pveam update >/dev/null 2>&1 || true
- else
- pveam update >/dev/null 2>&1 || true
- fi
- mapfile -t _online_vers < <(
- pveam available -section system 2>/dev/null |
- awk '{print $2}' |
- grep -E "^${os_type}-[0-9]" |
- { [[ -n "$tpl_pattern" ]] && grep "$tpl_pattern" || cat; } |
- sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" |
- sort -u -V 2>/dev/null || true
- )
- all_versions+=("${_online_vers[@]}")
-
- # Deduplicate and sort, find the highest version below current
- local fallback_ver=""
- fallback_ver=$(printf '%s\n' "${all_versions[@]}" | sort -u -V | awk -v cur="$current_ver" '{
- # Compare major versions: extract major part
- split($0, a, ".")
- split(cur, b, ".")
- if (a[1]+0 < b[1]+0) ver=$0
- } END { if (ver) print ver }')
-
- if [[ -z "$fallback_ver" ]]; then
- msg_error "No older $os_type template version found."
- return 1
- fi
-
- msg_ok "Fallback version: $os_type $fallback_ver"
-
- # Find the actual template file for this version
- local fallback_search="${os_type}-${fallback_ver}"
- local fallback_template=""
-
- # Check local first
- mapfile -t _fb_local < <(
- pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
- awk -v search="$fallback_search" -v pat="$tpl_pattern" '$1 ~ search && $1 ~ pat {print $1}' |
- sed 's|.*/||' | sort -t - -k 2 -V
- )
- if [[ ${#_fb_local[@]} -gt 0 ]]; then
- fallback_template="${_fb_local[-1]}"
- else
- # Check online
- mapfile -t _fb_online < <(
- pveam available -section system 2>/dev/null |
- awk '{print $2}' |
- grep -E "^${fallback_search}.*${tpl_pattern}" |
- sort -t - -k 2 -V 2>/dev/null || true
- )
- [[ ${#_fb_online[@]} -gt 0 ]] && fallback_template="${_fb_online[-1]}"
- fi
-
- if [[ -z "$fallback_template" ]]; then
- msg_error "No template found for $os_type $fallback_ver."
- return 1
- fi
-
- msg_ok "Found template: $fallback_template"
-
- # Download if needed
- local fallback_path
- fallback_path="$(pvesm path "$TEMPLATE_STORAGE:vztmpl/$fallback_template" 2>/dev/null || true)"
- [[ -z "$fallback_path" ]] && fallback_path="/var/lib/vz/template/cache/$fallback_template"
-
- if [[ ! -f "$fallback_path" ]]; then
- msg_info "Downloading $os_type $fallback_ver template"
- if ! pveam download "$TEMPLATE_STORAGE" "$fallback_template" >>"${BUILD_LOG:-/dev/null}" 2>&1; then
- msg_error "Failed to download $os_type $fallback_ver template."
- return 1
- fi
- msg_ok "Template downloaded"
- fi
-
- # Update variables
- TEMPLATE="$fallback_template"
- TEMPLATE_PATH="$fallback_path"
- PCT_OSVERSION="$fallback_ver"
- export PCT_OSVERSION
-
- # Retry pct create
- msg_info "Retrying container creation with $os_type $fallback_ver"
- if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
- msg_ok "Container created successfully with $os_type $fallback_ver (fallback from $old_template)."
- return 0
- else
- msg_error "Container creation with $os_type $fallback_ver also failed. See $LOGFILE"
- return 1
- fi
- }
-
- # Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
- # Returns:
- # 0 = no upgrade needed / container created after upgrade or explicit fallback
- # 1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
- # 2 = user chose ignore
- # 3 = upgrade attempted but failed OR retry failed
- # 4 = user cancelled
- 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
- local _ans
-
- has_previous_os_version_template() {
- local os_type="${PCT_OSTYPE:-}"
- local current_ver="${PCT_OSVERSION:-}"
- local tpl_pattern="${TEMPLATE_PATTERN:-${TEMPLATE:-}}"
- local -a all_versions=()
-
- [[ -n "$os_type" && -n "$current_ver" ]] || return 1
-
- mapfile -t _local_vers < <(
- pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
- awk '{print $1}' |
- sed 's|.*/||' |
- grep -E "^${os_type}-[0-9]" |
- { [[ -n "$tpl_pattern" ]] && grep "$tpl_pattern" || cat; } |
- sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" |
- sort -u -V
- )
- all_versions+=("${_local_vers[@]}")
-
- if command -v timeout &>/dev/null; then
- timeout 30 pveam update >/dev/null 2>&1 || true
- else
- pveam update >/dev/null 2>&1 || true
- fi
- mapfile -t _online_vers < <(
- pveam available -section system 2>/dev/null |
- awk '{print $2}' |
- grep -E "^${os_type}-[0-9]" |
- { [[ -n "$tpl_pattern" ]] && grep "$tpl_pattern" || cat; } |
- sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" |
- sort -u -V 2>/dev/null || true
- )
- all_versions+=("${_online_vers[@]}")
-
- printf '%s\n' "${all_versions[@]}" | sort -u -V | awk -v cur="$current_ver" '
- {
- split($0, a, ".")
- split(cur, b, ".")
- if (a[1]+0 < b[1]+0) found=1
- }
- END { exit found ? 0 : 1 }
- '
- }
-
- _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
- local _disable_update="${DISABLE_UPDATE:-no}"
- if [[ "${PHS_SILENT:-0}" == "1" || "${_disable_update,,}" == "yes" ]]; then
- msg_info "Skipping host LXC stack upgrade prompt (unattended mode)"
- return 2
- fi
-
- msg_warn "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 " note : option 1 runs host apt update + apt upgrade"
- echo
-
- # Offer older OS version fallback when template version might be too new for LXC stack
- local _has_fallback_option=false
- if [[ "$do_retry" == "yes" ]] && has_previous_os_version_template; then
- _has_fallback_option=true
- echo " [1] Run host upgrade now (recommended). WARNING: this runs apt upgrade and updates all Packages on your host!"
- echo " [2] Use an older ${PCT_OSTYPE} template instead (may not work with all scripts)"
- echo " [3] Ignore (continue without upgrading)"
- echo " [4] Cancel (abort the script)"
- echo
- read -rp "Select option [1/2/3/4]: " _ans /dev/null; then
- msg_error "LXC stack upgrade caused PVE tool breakage (likely Perl module incompatibility)."
- msg_custom "⚠️" "${YW}" "A partial package upgrade has left the PVE stack in an inconsistent state."
- msg_custom "🔧" "${YW}" "Please run the following on the Proxmox host, then retry:"
- echo -e "${TAB} apt update && apt upgrade -y"
- echo -e "${TAB} reboot"
- return 3
- fi
- 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
- }
-
- ARCH="$(dpkg --print-architecture)"
-
- # Maps OS type + version to the release variant name used by ARM64 template sources.
- arm64_template_variant() {
- case "$1:$2" in
- debian:12) echo "bookworm" ;;
- debian:13) echo "trixie" ;;
- debian:) echo "$DEBIAN_DEFAULT_CODENAME" ;;
-
- ubuntu:24.04) echo "noble" ;;
- ubuntu:26.04) echo "questing" ;;
- ubuntu:) echo "$UBUNTU_DEFAULT_CODENAME" ;;
-
- alpine:*) echo "${2:-$ALPINE_DEFAULT_VERSION}" ;;
-
- *) return 1 ;;
- esac
- }
-
- # Downloads an ARM64 LXC rootfs template from jenkins.linuxcontainers.org.
- download_arm64_template() {
- local dest="$1" url
-
- mkdir -p "$(dirname "$dest")" || {
- msg_error "Cannot create template dir."
- exit 207
- }
-
- url="https://jenkins.linuxcontainers.org/job/image-${PCT_OSTYPE}/architecture=arm64,release=${CUSTOM_TEMPLATE_VARIANT},variant=default/lastStableBuild/artifact/rootfs.tar.xz"
-
- msg_info "Downloading ${PCT_OSTYPE^} ${CUSTOM_TEMPLATE_VARIANT} ARM64 template"
- local patched="${dest%/*}/.${dest##*/}.patched"
- (
- flock -x 200
- if [[ -f "$patched" ]] && [[ -s "$dest" ]] && xz -t "$dest" 2>/dev/null; then
- exit 0
- fi
- if ! curl -fsSL -o "$dest" "$url"; then
- msg_error "Failed to download ARM64 template from: $url"
- exit 208
- fi
- # Some linuxcontainers.org rootfs builds ship without /etc/network,
- # which makes pve-container's post-create hook fail when it writes the
- # interfaces file. This appends an empty /etc/network/interfaces if missing.
- if ! tar -tJf "$dest" 2>/dev/null | grep -q '/etc/network/$'; then
- fixdir=$(mktemp -d)
- tmptar="${dest%.xz}"
- mkdir -p "$fixdir/etc/network"
- : >"$fixdir/etc/network/interfaces"
- if xz -d -f "$dest" && tar -C "$fixdir" --append -f "$tmptar" ./etc/network && xz -1 -f "$tmptar"; then
- rm -rf "$fixdir"
- else
- rm -rf "$fixdir" "$tmptar"
- msg_error "Failed to patch ARM64 template (missing /etc/network)"
- exit 208
- fi
- fi
- touch "$patched"
- ) 200>"${dest}.lock"
- local dl_rc=$?
- if [[ $dl_rc -ne 0 ]]; then
- exit "$dl_rc"
- fi
- msg_ok "Downloaded ARM64 LXC template"
- }
-
- download_template() {
- local dest="${1:-$TEMPLATE_PATH}"
- if [[ "$ARCH" == "arm64" ]]; then
- download_arm64_template "$dest"
- else
- pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 || {
- msg_error "Failed to download template '$TEMPLATE' to storage '$TEMPLATE_STORAGE'"
- exit 222
- }
- fi
- }
-
- # ------------------------------------------------------------------------------
- # 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
- msg_warn "Container/VM ID $CTID is already in use (detected late). Reassigning..."
- CTID=$(get_valid_container_id "$((CTID + 1))")
- export CTID
- msg_ok "Reassigned to container ID $CTID"
- fi
-
- # Report installation start to API early - captures failures in storage/template/create
- post_to_api
-
- # Transition to 'validation' — Proxmox-internal checks (storage, template, cluster)
- post_progress_to_api "validation"
-
- # Storage capability check
- check_storage_support "rootdir" || {
- msg_error "No valid storage found for 'rootdir' [Container]"
- exit 119
- }
- check_storage_support "vztmpl" || {
- msg_error "No valid storage found for 'vztmpl' [Template]"
- exit 120
- }
-
- # 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
-
- msg_info "Validating storage '$CONTAINER_STORAGE'"
- # Check if storage.cfg is accessible (pmxcfs must be mounted)
- if [[ ! -f /etc/pve/storage.cfg ]]; then
- if ! mountpoint -q /etc/pve 2>/dev/null; then
- msg_error "Proxmox cluster filesystem (pmxcfs) is not mounted at /etc/pve."
- msg_custom "🔧" "${YW}" "Try: systemctl restart pve-cluster"
- else
- msg_error "/etc/pve/storage.cfg does not exist."
- msg_custom "🔧" "${YW}" "Check Proxmox cluster filesystem integrity: pvecm status"
- fi
- exit 213
- fi
-
- STORAGE_TYPE=$(grep -E "^[^:]+:[[:space:]]*$CONTAINER_STORAGE[[:space:]]*$" /etc/pve/storage.cfg | cut -d: -f1 | head -1 || true)
-
- # Fallback: use pvesm status to determine storage type
- if [[ -z "$STORAGE_TYPE" ]]; then
- STORAGE_TYPE=$(pvesm status -storage "$CONTAINER_STORAGE" 2>/dev/null | awk 'NR>1{print $2}')
- fi
-
- if [[ -z "$STORAGE_TYPE" ]]; then
- msg_error "Storage '$CONTAINER_STORAGE' not found in /etc/pve/storage.cfg"
- msg_custom "📋" "${YW}" "Available storages: $(pvesm status 2>/dev/null | awk 'NR>1{printf "%s (%s) ", $1, $2}' || echo 'n/a')"
- if [[ -r /etc/pve/storage.cfg ]]; then
- msg_custom "📋" "${YW}" "Storage definitions found in config:"
- grep -E '^[a-z]+:' /etc/pve/storage.cfg 2>/dev/null | while IFS= read -r _line; do
- echo "${TAB} $_line"
- done
- fi
- msg_custom "📖" "${YW}" "See https://pve.proxmox.com/wiki/Storage for storage configuration details."
- exit 213
- fi
-
- case "$STORAGE_TYPE" in
- iscsidirect)
- msg_error "Storage '$CONTAINER_STORAGE' uses iSCSI-direct which does not support container rootfs."
- exit 212
- ;;
- iscsi | zfs)
- msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support container rootdir content."
- exit 213
- ;;
- cephfs)
- msg_error "Storage '$CONTAINER_STORAGE' uses CephFS which is not supported for LXC rootfs."
- exit 219
- ;;
- pbs)
- msg_error "Storage '$CONTAINER_STORAGE' is a Proxmox Backup Server — cannot be used for containers."
- exit 224
- ;;
- linstor | rbd | nfs | cifs)
- if ! pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null; then
- msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) is not accessible or inactive."
- exit 217
- fi
- ;;
- esac
-
- if ! pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE"; then
- msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support 'rootdir' content."
- msg_custom "💡" "${YW}" "Enable 'Disk image' (rootdir) for storage '${CONTAINER_STORAGE}' in:"
- msg_custom " " "${YW}" "Datacenter → Storage → ${CONTAINER_STORAGE} → Edit → Content"
- msg_custom "📖" "${YW}" "See: https://pve.proxmox.com/wiki/Storage"
- msg_custom "🔗" "${YW}" "Help: https://github.com/community-scripts/ProxmoxVE/discussions"
- exit 213
- fi
- msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated"
-
- msg_info "Validating template storage '$TEMPLATE_STORAGE'"
- TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 || true)
-
- if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then
- msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'"
- fi
- msg_ok "Template storage '$TEMPLATE_STORAGE' validated"
-
- # 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
- # ------------------------------------------------------------------------------
- CUSTOM_TEMPLATE_VARIANT=""
-
- if [[ "$ARCH" == "arm64" ]]; then
- # ARM64: use custom template download from linuxcontainers.org / GitHub
- msg_info "Preparing ARM64 template"
-
- CUSTOM_TEMPLATE_VARIANT=$(arm64_template_variant "$PCT_OSTYPE" "${PCT_OSVERSION:-}") || {
- msg_error "No ARM64 template mapping for ${PCT_OSTYPE} ${PCT_OSVERSION:-latest}"
- exit 207
- }
-
- TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-rootfs.tar.xz"
- TEMPLATE_SOURCE="custom-arm64"
-
- # Resolve template path
- TEMPLATE_PATH="$(pvesm path "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" 2>/dev/null || true)"
- if [[ -z "$TEMPLATE_PATH" ]]; then
- local _tpl_base
- _tpl_base=$(awk -v s="$TEMPLATE_STORAGE" '
- $0 ~ "^[^:]+:[[:space:]]*" s "$" {f=1; next}
- f && /^[^[:space:]]/ {f=0}
- f && $1 == "path" {print $2; exit}
- ' /etc/pve/storage.cfg)
- TEMPLATE_PATH="${_tpl_base:-/var/lib/vz}/template/cache/$TEMPLATE"
- fi
-
- # Download if missing, too small, or corrupt
- if [[ ! -f "$TEMPLATE_PATH" ]]; then
- download_arm64_template "$TEMPLATE_PATH"
- elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]] || ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
- msg_warn "Local template invalid - re-downloading."
- rm -f "$TEMPLATE_PATH"
- download_arm64_template "$TEMPLATE_PATH"
- else
- msg_ok "Template ${BL}$TEMPLATE${CL} found locally."
- fi
-
- else
- TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
- case "$PCT_OSTYPE" in
- debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
- alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
- *) TEMPLATE_PATTERN="" ;;
- esac
-
- msg_info "Searching for template '$TEMPLATE_SEARCH'"
-
- # Initialize variables
- ONLINE_TEMPLATE=""
- ONLINE_TEMPLATES=()
-
- # Step 1: Check local templates first (instant)
- mapfile -t LOCAL_TEMPLATES < <(
- pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
- awk -v search="${TEMPLATE_SEARCH}" -v pattern="${TEMPLATE_PATTERN}" -v arch="${ARCH}" \
- '$1 ~ search && $1 ~ pattern && $1 ~ ("_" arch "\\.(tar\\.zst|tar\\.xz|tar\\.gz)$") {print $1}' |
- sed 's|.*/||' | sort -t - -k 2 -V
- )
-
- # Step 2: If local template found, use it immediately (skip pveam update)
- if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
- TEMPLATE="${LOCAL_TEMPLATES[-1]}"
- TEMPLATE_SOURCE="local"
- msg_ok "Template search completed"
- else
- # Step 3: No local template - need to check online (this may be slow)
- msg_info "No local template found, checking online catalog..."
-
- # Update catalog with timeout to prevent long hangs
- if command -v timeout &>/dev/null; then
- if ! timeout 30 pveam update >/dev/null 2>&1; then
- msg_warn "Template catalog update timed out (possible network/DNS issue). Run 'pveam update' manually to diagnose."
- fi
- else
- pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)"
- fi
-
- ONLINE_TEMPLATES=()
- mapfile -t ONLINE_TEMPLATES < <(_list_templates "$TEMPLATE_SEARCH" "$TEMPLATE_PATTERN")
- [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
-
- TEMPLATE="$ONLINE_TEMPLATE"
- TEMPLATE_SOURCE="online"
- msg_ok "Template search completed"
- fi
-
- # resolve_os_version() already validated the version, so this means the
- # variant (TEMPLATE_PATTERN) is missing - a catalog issue, don't auto-repair
- if [[ -z "$TEMPLATE" ]]; then
- msg_error "No ${TEMPLATE_PATTERN#-} template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
- msg_custom "ℹ️" "${YW}" "Refresh the catalog and check what this host offers:"
- echo -e "${TAB} pveam update && pveam available -section system | grep '${PCT_OSTYPE}-'"
- exit 225
- 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 "Could not resolve a storage path for template ${TEMPLATE}"
- exit 220
- }
-
- 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 it."
- NEED_DOWNLOAD=1
- elif [[ ! -r "$TEMPLATE_PATH" ]]; then
- msg_error "Template file exists but is not readable, check permissions."
- exit 221
- elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
- if [[ -n "$ONLINE_TEMPLATE" ]]; then
- msg_warn "Template file too small (<1MB), re-downloading."
- NEED_DOWNLOAD=1
- else
- msg_warn "Template looks too small, but no online version exists. Keeping local file."
- fi
- elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
- if [[ -n "$ONLINE_TEMPLATE" ]]; then
- msg_warn "Template appears corrupted, re-downloading."
- NEED_DOWNLOAD=1
- else
- msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
- fi
- else
- $STD msg_ok "Template $TEMPLATE is present and valid."
- fi
-
- if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
- msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
- if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
- TEMPLATE="$ONLINE_TEMPLATE"
- NEED_DOWNLOAD=1
- else
- msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE"
- fi
- fi
-
- if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
- [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
- for attempt in {1..3}; do
- msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
- if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1; then
- msg_ok "Template download successful."
- break
- fi
- if [[ $attempt -eq 3 ]]; then
- msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
- exit 222
- fi
- sleep $((attempt * 5))
- done
- fi
-
- if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
- msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
- exit 223
- fi
- fi
-
- # ------------------------------------------------------------------------------
- # 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
- local _lxc_stack_rc=0
- offer_lxc_stack_upgrade_and_maybe_retry "no" || _lxc_stack_rc=$?
- # 4 = user chose "Cancel" -> honor it and abort before creating the container
- if [[ $_lxc_stack_rc -eq 4 ]]; then
- exit_script
- fi
- 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 template downloads/validation)
- lockfile="/tmp/template.${TEMPLATE}.lock"
-
- # Cleanup stale lock files (older than 1 hour - likely from crashed processes)
- if [[ -f "$lockfile" ]]; then
- local lock_age=$(($(date +%s) - $(stat -c %Y "$lockfile" 2>/dev/null || echo 0)))
- if [[ $lock_age -gt 3600 ]]; then
- msg_warn "Removing stale template lock file (age: ${lock_age}s)"
- rm -f "$lockfile"
- fi
- fi
-
- exec 9>"$lockfile" || {
- msg_error "Failed to create lock file '$lockfile'."
- exit 200
- }
-
- # Retry logic for template lock (another container creation may be running)
- local lock_attempts=0
- local max_lock_attempts=10
- local lock_wait_time=30
-
- while ! flock -w "$lock_wait_time" 9; do
- lock_attempts=$((lock_attempts + 1))
- if [[ $lock_attempts -ge $max_lock_attempts ]]; then
- msg_error "Timeout while waiting for template lock after ${max_lock_attempts} attempts."
- msg_custom "💡" "${YW}" "Another container creation may be stuck. Check running processes or remove: $lockfile"
- exit 211
- fi
- msg_custom "⏳" "${YW}" "Another container is being created with this template. Waiting... (attempt ${lock_attempts}/${max_lock_attempts})"
- done
-
- LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log"
-
- # Helper: append pct_create log to BUILD_LOG before exit so combined log has full context
- _flush_pct_log() {
- if [[ -s "${LOGFILE:-}" && -n "${BUILD_LOG:-}" ]]; then
- {
- echo ""
- echo "--- pct create output (${LOGFILE}) ---"
- cat "$LOGFILE"
- echo "--- end pct create output ---"
- } >>"$BUILD_LOG" 2>/dev/null || true
- fi
- }
-
- # Validate template before pct create (while holding lock)
- if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH" 2>/dev/null || echo 0)" -lt 1000000 ]]; then
- msg_info "Template file missing or too small - downloading"
- rm -f "$TEMPLATE_PATH"
- download_template
- msg_ok "Template downloaded"
- elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
- if [[ "$ARCH" == "arm64" || -n "$ONLINE_TEMPLATE" ]]; then
- msg_info "Template appears corrupted - re-downloading"
- rm -f "$TEMPLATE_PATH"
- download_template
- msg_ok "Template re-downloaded"
- else
- msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
- fi
- fi
-
- # Release lock after template validation - pct create has its own internal locking
- exec 9>&-
-
- 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)
- # Disable globbing: unquoted $PCT_OPTIONS needs word-splitting but must not glob-expand
- # (e.g. passwords containing * or ? would match filenames otherwise)
- set -f
- if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
- msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..."
-
- # Check for Perl module breakage (partial PVE upgrade)
- if grep -qiE 'Compilation failed|Bareword.*not allowed' "$LOGFILE"; then
- msg_error "Container creation failed due to broken Perl modules on the PVE host."
- msg_custom "⚠️" "${YW}" "This usually happens after a partial PVE package upgrade."
- msg_custom "🔧" "${YW}" "Please run the following on the Proxmox host, then retry:"
- echo -e "${TAB} apt update && apt dist-upgrade -y"
- echo -e "${TAB} reboot"
- _flush_pct_log
- exit 232
- fi
-
- # Check if CTID collision (race condition: ID claimed between validation and creation)
- if grep -qiE 'already exists|already in use' "$LOGFILE"; then
- local old_ctid="$CTID"
- CTID=$(get_valid_container_id "$((CTID + 1))")
- export CTID
- msg_warn "Container ID $old_ctid was claimed by another process. Retrying with ID $CTID"
- LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log"
- if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
- msg_ok "Container successfully created with new ID $CTID"
- else
- msg_error "Container creation failed even with new ID $CTID. See $LOGFILE"
- _flush_pct_log
- exit 209
- fi
- else
- # Not a CTID collision - check if template issue and retry with fresh download
- if grep -qiE 'unable to open|corrupt|invalid' "$LOGFILE"; then
- msg_info "Template may be corrupted - re-downloading"
- rm -f "$TEMPLATE_PATH"
- download_template
- msg_ok "Template re-downloaded"
- fi
-
- # Retry after repair
- if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
- # Fallback to local storage if not already on local
- if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
- msg_info "Retrying container creation with fallback to local storage"
- LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
- if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
- msg_ok "Trying local storage fallback"
- msg_info "Downloading template to local"
- if [[ "$ARCH" == "arm64" ]]; then
- download_arm64_template "$LOCAL_TEMPLATE_PATH"
- else
- pveam download local "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1
- fi
- msg_ok "Template downloaded to local"
- else
- msg_ok "Trying local storage fallback"
- fi
- if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
- # Local fallback also failed - check for LXC stack version issue
- if grep -qiE 'unsupported .* version' "$LOGFILE"; then
- msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
- offer_lxc_stack_upgrade_and_maybe_retry "yes"
- rc=$?
- case $rc in
- 0) : ;; # success - container created, continue
- 2)
- msg_error "LXC stack upgrade ignored. Please inspect: $LOGFILE"
- _flush_pct_log
- exit 231
- ;;
- 3)
- msg_error "LXC stack upgrade failed. Please inspect: $LOGFILE"
- _flush_pct_log
- exit 231
- ;;
- 4)
- msg_error "Cancelled by user."
- _flush_pct_log
- exit 231
- ;;
- esac
- else
- msg_error "Container creation failed. See $LOGFILE"
- if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
- set -x
- pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
- set +x
- fi
- _flush_pct_log
- exit 209
- fi
- else
- msg_ok "Container successfully created using local fallback."
- fi
- else
- # Already on local storage and still failed - check LXC stack version
- if grep -qiE 'unsupported .* version' "$LOGFILE"; then
- msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
- offer_lxc_stack_upgrade_and_maybe_retry "yes"
- rc=$?
- case $rc in
- 0) : ;; # success - container created, continue
- 2)
- msg_error "LXC stack upgrade ignored. Please inspect: $LOGFILE"
- _flush_pct_log
- exit 231
- ;;
- 3)
- msg_error "LXC stack upgrade failed. Please inspect: $LOGFILE"
- _flush_pct_log
- exit 231
- ;;
- 4)
- msg_error "Cancelled by user."
- _flush_pct_log
- exit 231
- ;;
- esac
- else
- msg_error "Container creation failed. See $LOGFILE"
- if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
- set -x
- pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
- set +x
- fi
- _flush_pct_log
- exit 209
- fi
- fi
- else
- msg_ok "Container successfully created after template repair."
- fi
- fi # close CTID collision else-branch
- fi
- set +f # re-enable globbing after pct create block
-
- # Verify container exists (allow up to 10s for pmxcfs sync in clusters)
- local _pct_visible=false
- for _pct_check in {1..10}; do
- if pct list | awk '{print $1}' | grep -qx "$CTID"; then
- _pct_visible=true
- break
- fi
- sleep 1
- done
- if [[ "$_pct_visible" != true ]]; then
- msg_error "Container ID $CTID not listed in 'pct list' after 10s. See $LOGFILE"
- msg_custom "🔧" "${YW}" "This can happen in clusters with pmxcfs sync delays."
- _flush_pct_log
- exit 215
- fi
-
- # Verify config rootfs
- grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
- msg_error "RootFS entry missing in container config. See $LOGFILE"
- _flush_pct_log
- exit 216
- }
-
- msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
-
- # Append pct create log to BUILD_LOG for combined log visibility
- if [[ -s "$LOGFILE" && -n "${BUILD_LOG:-}" ]]; then
- {
- echo ""
- echo "--- pct create output ---"
- cat "$LOGFILE"
- echo "--- end pct create output ---"
- } >>"$BUILD_LOG" 2>/dev/null || true
- fi
-}
-
-# ==============================================================================
-# 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)
- local script_slug script_url donate_url
-
- script_slug="${SCRIPT_SLUG:-${NSAPP}}"
- script_slug="$(echo "$script_slug" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')"
- script_url="https://community-scripts.org/scripts/${script_slug}"
- donate_url="https://community-scripts.org/donate"
-
- # Generate LXC Description
- DESCRIPTION=$(
- cat <
-
-
-
-
- ${APP} LXC
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- GitHub
-
-
-
- Discussions
-
-
-
- Issues
-
-
-EOF
- )
- pct set "$CTID" -description "$DESCRIPTION"
-
- if [[ -f /etc/systemd/system/ping-instances.service ]]; then
- systemctl start ping-instances.service
- fi
-
- # Optional host-side post-install hook
- # Path comes from var_post_install (default.vars / app.vars / advanced settings).
- # Runs ON THE PROXMOX HOST after the container is up and configured.
- # Exposed env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG.
- # Output (stdout/stderr) is captured to /var/log/community-scripts/post-install-