ProxmoxVED/misc/build.func
2025-09-02 11:07:31 +02:00

1659 lines
57 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Copyright (c) 2021-2025 tteck
# Author: tteck (tteckster)
# Co-Author: MickLesk
# Co-Author: michelroegl-brunner
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
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="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
METHOD="default" # sets the METHOD variable to "default", used for the API call.
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
#CT_TYPE=${var_unprivileged:-$CT_TYPE}
}
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func)
if command -v curl >/dev/null 2>&1; then
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
load_functions
#echo "(build.func) Loaded core.func via curl"
elif command -v wget >/dev/null 2>&1; then
source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
load_functions
#echo "(build.func) Loaded core.func via wget"
fi
# This function enables error handling in the script by setting options and defining a trap for the ERR signal.
catch_errors() {
set -Eeo pipefail
# if [ -n "$BASH_VERSION" ] && command -v shopt >/dev/null 2>&1; then
# shopt -s errtrace
# fi
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
}
# This function is called when an error occurs. It receives the exit code, line number, and command that caused the error, and displays an error message.
error_handler() {
local exit_code="$?"
local line_number="$1"
local command="$2"
printf "\e[?25h"
local error_message="[ERROR] in line $line_number: exit code $exit_code: while executing command $command"
post_update_to_api "failed" "$command"
echo -e "\n$error_message\n"
exit "$exit_code"
if [[ -n "$CT_ID" ]]; then
read -p "Remove this Container? <y/N> " prompt
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
pct stop "$CT_ID" &>/dev/null
pct destroy "$CT_ID" &>/dev/null
msg_ok "Removed this Container"
fi
fi
}
# Check if the shell is using bash
shell_check() {
if [[ "$(basename "$SHELL")" != "bash" ]]; then
clear
msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell."
echo -e "\nExiting..."
sleep 2
exit
fi
}
# Run as root only
root_check() {
if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
clear
msg_error "Please run this script as root."
echo -e "\nExiting..."
sleep 2
exit
fi
}
# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported.
# Supported: Proxmox VE 8.0.x 8.9.x and 9.0 (NOT 9.1+)
pve_check() {
local PVE_VER
PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
# Check for Proxmox VE 8.x: allow 8.08.9
if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
local MINOR="${BASH_REMATCH[1]}"
if ((MINOR < 0 || MINOR > 9)); then
msg_error "This version of Proxmox VE is not supported."
msg_error "Supported: Proxmox VE version 8.0 8.9"
exit 1
fi
return 0
fi
# Check for Proxmox VE 9.x: allow ONLY 9.0
if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
local MINOR="${BASH_REMATCH[1]}"
if ((MINOR != 0)); then
msg_error "This version of Proxmox VE is not yet supported."
msg_error "Supported: Proxmox VE version 9.0"
exit 1
fi
return 0
fi
# All other unsupported versions
msg_error "This version of Proxmox VE is not supported."
msg_error "Supported versions: Proxmox VE 8.0 8.x or 9.0"
exit 1
}
# When a node is running tens of containers, it's possible to exceed the kernel's cryptographic key storage allocations.
# These are tuneable, so verify if the currently deployment is approaching the limits, advise the user on how to tune the limits, and exit the script.
# https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
maxkeys_check() {
# Read kernel parameters
per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
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
echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}"
exit 1
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
echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}"
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
echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}"
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
echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}"
exit 1
fi
echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}"
}
# This function checks the system architecture and exits if it's not "amd64".
arch_check() {
if [ "$(dpkg --print-architecture)" != "amd64" ]; then
echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
echo -e "Exiting..."
sleep 2
exit
fi
}
# Function to get the current IP address based on the distribution
get_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
CURRENT_IP=$(hostname -I | awk '{print $1}')
# Check for Alpine (uses ip command)
elif grep -q 'ID=alpine' /etc/os-release; then
CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
else
CURRENT_IP="Unknown"
fi
fi
echo "$CURRENT_IP"
}
# Function to update the IP address in the MOTD file
update_motd_ip() {
MOTD_FILE="/etc/motd"
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
}
# This function checks if the script is running through SSH and prompts the user to confirm if they want to proceed or exit.
ssh_check() {
if [ -n "${SSH_CLIENT:+x}" ]; then
if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's advisable to utilize the Proxmox shell rather than SSH, as there may be potential complications with variable retrieval. Proceed using SSH?" 10 72; then
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Proceed using SSH" "You've chosen to proceed using SSH. If any issues arise, please run the script in the Proxmox shell before creating a repository issue." 10 72
else
clear
echo "Exiting due to SSH usage. Please consider using the Proxmox shell."
exit
fi
fi
}
base_settings() {
# Default Settings
CT_TYPE=${var_unprivileged:-"1"}
DISK_SIZE=${var_disk:-"4"}
CORE_COUNT=${var_cpu:-"1"}
RAM_SIZE=${var_ram:-"1024"}
VERBOSE=${var_verbose:-"${1:-no}"}
PW=${var_pw:-""}
CT_ID=${var_ctid:-$NEXTID}
HN=${var_hostname:-$NSAPP}
BRG=${var_brg:-"vmbr0"}
NET=${var_net:-"dhcp"}
IPV6_METHOD=${var_ipv6_method:-"none"}
IPV6_STATIC=${var_ipv6_static:-""}
GATE=${var_gateway:-""}
APT_CACHER=${var_apt_cacher:-""}
APT_CACHER_IP=${var_apt_cacher_ip:-""}
MTU=${var_mtu:-""}
SD=${var_storage:-""}
NS=${var_ns:-""}
MAC=${var_mac:-""}
VLAN=${var_vlan:-""}
SSH=${var_ssh:-"no"}
SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
UDHCPC_FIX=${var_udhcpc_fix:-""}
TAGS="community-script;${var_tags:-}"
ENABLE_FUSE=${var_fuse:-"${1:-no}"}
ENABLE_TUN=${var_tun:-"${1:-no}"}
# 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="12"
fi
}
# This function displays the default values for various settings.
echo_default() {
# Convert CT_TYPE to description
CT_TYPE_DESC="Unprivileged"
if [ "$CT_TYPE" -eq 0 ]; then
CT_TYPE_DESC="Privileged"
fi
# Output the selected values with icons
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
if [ "$VERB" == "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 " "
}
# This function is called when the user decides to exit the script. It clears the screen and displays an exit message.
exit_script() {
clear
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
exit
}
# This function allows the user to configure advanced settings for the script.
advanced_settings() {
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58
# Setting Default Tag for Advanced Settings
TAGS="community-script;${var_tags:-}"
CT_DEFAULT_TYPE="${CT_TYPE}"
CT_TYPE=""
while [ -z "$CT_TYPE" ]; do
if [ "$CT_DEFAULT_TYPE" == "1" ]; then
if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
"1" "Unprivileged" ON \
"0" "Privileged" OFF \
3>&1 1>&2 2>&3); then
if [ -n "$CT_TYPE" ]; then
CT_TYPE_DESC="Unprivileged"
if [ "$CT_TYPE" -eq 0 ]; then
CT_TYPE_DESC="Privileged"
fi
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os | ${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
fi
else
exit_script
fi
fi
if [ "$CT_DEFAULT_TYPE" == "0" ]; then
if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
"1" "Unprivileged" OFF \
"0" "Privileged" ON \
3>&1 1>&2 2>&3); then
if [ -n "$CT_TYPE" ]; then
CT_TYPE_DESC="Unprivileged"
if [ "$CT_TYPE" -eq 0 ]; then
CT_TYPE_DESC="Privileged"
fi
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
fi
else
exit_script
fi
fi
done
while true; do
if PW1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then
# Empty = Autologin
if [[ -z "$PW1" ]]; then
PW=""
PW1="Automatic Login"
echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}"
break
fi
# Invalid: contains spaces
if [[ "$PW1" == *" "* ]]; then
whiptail --msgbox "Password cannot contain spaces." 8 58
continue
fi
# Invalid: too short
if ((${#PW1} < 5)); then
whiptail --msgbox "Password must be at least 5 characters." 8 58
continue
fi
# Confirm password
if PW2=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then
if [[ "$PW1" == "$PW2" ]]; then
PW="-password $PW1"
echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}"
break
else
whiptail --msgbox "Passwords do not match. Please try again." 8 58
fi
else
exit_script
fi
else
exit_script
fi
done
if CT_ID=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
if [ -z "$CT_ID" ]; then
CT_ID="$NEXTID"
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
else
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
fi
else
exit_script
fi
while true; do
if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
if [ -z "$CT_NAME" ]; then
HN="$NSAPP"
else
HN=$(echo "${CT_NAME,,}" | tr -d ' ')
fi
# Hostname validate (RFC 1123)
if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
break
else
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70
fi
else
exit_script
fi
done
while true; do
DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script
if [ -z "$DISK_SIZE" ]; then
DISK_SIZE="$var_disk"
fi
if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
break
else
whiptail --msgbox "Disk size must be a positive integer!" 8 58
fi
done
while true; do
CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
--inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script
if [ -z "$CORE_COUNT" ]; then
CORE_COUNT="$var_cpu"
fi
if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
break
else
whiptail --msgbox "CPU core count must be a positive integer!" 8 58
fi
done
while true; do
RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
--inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script
if [ -z "$RAM_SIZE" ]; then
RAM_SIZE="$var_ram"
fi
if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
break
else
whiptail --msgbox "RAM size must be a positive integer!" 8 58
fi
done
IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f)
BRIDGES=""
OLD_IFS=$IFS
IFS=$'\n'
for iface_filepath in ${IFACE_FILEPATH_LIST}; do
iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
(grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true
if [ -f "${iface_indexes_tmpfile}" ]; then
while read -r pair; do
start=$(echo "${pair}" | cut -d':' -f1)
end=$(echo "${pair}" | cut -d':' -f2)
if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
fi
done <"${iface_indexes_tmpfile}"
rm -f "${iface_indexes_tmpfile}"
fi
done
IFS=$OLD_IFS
BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
if [[ -z "$BRIDGES" ]]; then
BRG="vmbr0"
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
else
BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge:" 15 40 6 $(echo "$BRIDGES" | awk '{print $0, "Bridge"}') 3>&1 1>&2 2>&3)
if [[ -z "$BRG" ]]; then
exit_script
else
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
fi
fi
# IPv4 methods: dhcp, static, none
while true; do
IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--title "IPv4 Address Management" \
--menu "Select IPv4 Address Assignment Method:" 12 60 2 \
"dhcp" "Automatic (DHCP, recommended)" \
"static" "Static (manual entry)" \
3>&1 1>&2 2>&3)
exit_status=$?
if [ $exit_status -ne 0 ]; then
exit_script
fi
case "$IPV4_METHOD" in
dhcp)
NET="dhcp"
GATE=""
echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}"
break
;;
static)
# Static: call and validate CIDR address
while true; do
NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \
--title "IPv4 ADDRESS" 3>&1 1>&2 2>&3)
if [ -z "$NET" ]; then
whiptail --msgbox "IPv4 address must not be empty." 8 58
continue
elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}"
break
else
whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58
fi
done
# call and validate Gateway
while true; do
GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \
--title "Gateway IP" 3>&1 1>&2 2>&3)
if [ -z "$GATE1" ]; then
whiptail --msgbox "Gateway IP address cannot be empty." 8 58
elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
whiptail --msgbox "Invalid Gateway IP address format." 8 58
else
GATE=",gw=$GATE1"
echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
break
fi
done
break
;;
esac
done
# IPv6 Address Management selection
while true; do
IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \
"Select IPv6 Address Management Type:" 15 58 4 \
"auto" "SLAAC/AUTO (recommended, default)" \
"dhcp" "DHCPv6" \
"static" "Static (manual entry)" \
"none" "Disabled" \
--default-item "auto" 3>&1 1>&2 2>&3)
[ $? -ne 0 ] && exit_script
case "$IPV6_METHOD" in
auto)
echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}"
IPV6_ADDR=""
IPV6_GATE=""
break
;;
dhcp)
echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}"
IPV6_ADDR="dhcp"
IPV6_GATE=""
break
;;
static)
# Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64)
while true; do
IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
"Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \
--title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script
if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then
echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}"
break
else
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
"$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58
fi
done
# Optional: ask for IPv6 gateway for static config
while true; do
IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
"Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3)
if [ -z "$IPV6_GATE" ]; then
IPV6_GATE=""
break
elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then
break
else
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
"Invalid IPv6 gateway format." 8 58
fi
done
break
;;
none)
echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}"
IPV6_ADDR="none"
IPV6_GATE=""
break
;;
*)
exit_script
;;
esac
done
if [ "$var_os" == "alpine" ]; then
APT_CACHER=""
APT_CACHER_IP=""
else
if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then
APT_CACHER="${APT_CACHER_IP:+yes}"
echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}"
else
exit_script
fi
fi
# if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then
# DISABLEIP6="yes"
# else
# DISABLEIP6="no"
# fi
# echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}"
if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then
if [ -z "$MTU1" ]; then
MTU1="Default"
MTU=""
else
MTU=",mtu=$MTU1"
fi
echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
else
exit_script
fi
if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then
if [ -z "$SD" ]; then
SX=Host
SD=""
else
SX=$SD
SD="-searchdomain=$SD"
fi
echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}"
else
exit_script
fi
if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then
if [ -z "$NX" ]; then
NX=Host
NS=""
else
NS="-nameserver=$NX"
fi
echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}"
else
exit_script
fi
if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then
UDHCPC_FIX="yes"
else
UDHCPC_FIX="no"
fi
export UDHCPC_FIX
if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then
if [ -z "$MAC1" ]; then
MAC1="Default"
MAC=""
else
MAC=",hwaddr=$MAC1"
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
fi
else
exit_script
fi
if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then
if [ -z "$VLAN1" ]; then
VLAN1="Default"
VLAN=""
else
VLAN=",tag=$VLAN1"
fi
echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}"
else
exit_script
fi
if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then
if [ -n "${ADV_TAGS}" ]; then
ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]')
TAGS="${ADV_TAGS}"
else
TAGS=";"
fi
echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}"
else
exit_script
fi
SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "SSH Authorized key for root (leave empty for none)" 8 58 --title "SSH Key" 3>&1 1>&2 2>&3)"
if [[ -z "${SSH_AUTHORIZED_KEY}" ]]; then
SSH_AUTHORIZED_KEY=""
fi
if [[ "$PW" == -password* || -n "$SSH_AUTHORIZED_KEY" ]]; then
if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable Root SSH Access?" 10 58); then
SSH="yes"
else
SSH="no"
fi
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
else
SSH="no"
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
fi
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then
ENABLE_FUSE="yes"
else
ENABLE_FUSE="no"
fi
echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}"
if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then
VERBOSE="yes"
else
VERBOSE="no"
fi
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then
echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}"
else
clear
header_info
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
advanced_settings
fi
}
diagnostics_check() {
if ! [ -d "/usr/local/community-scripts" ]; then
mkdir -p /usr/local/community-scripts
fi
if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then
if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then
cat <<EOF >/usr/local/community-scripts/diagnostics
DIAGNOSTICS=yes
#This file is used to store the diagnostics settings for the Community-Scripts API.
#https://github.com/community-scripts/ProxmoxVED/discussions/1836
#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
#This will disable the diagnostics feature.
#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
#This will enable the diagnostics feature.
#The following information will be sent:
#"ct_type"
#"disk_size"
#"core_count"
#"ram_size"
#"os_type"
#"os_version"
#"disableip6"
#"nsapp"
#"method"
#"pve_version"
#"status"
#If you have any concerns, please review the source code at /misc/build.func
EOF
DIAGNOSTICS="yes"
else
cat <<EOF >/usr/local/community-scripts/diagnostics
DIAGNOSTICS=no
#This file is used to store the diagnostics settings for the Community-Scripts API.
#https://github.com/community-scripts/ProxmoxVED/discussions/1836
#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
#This will disable the diagnostics feature.
#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
#This will enable the diagnostics feature.
#The following information will be sent:
#"ct_type"
#"disk_size"
#"core_count"
#"ram_size"
#"os_type"
#"os_version"
#"disableip6"
#"nsapp"
#"method"
#"pve_version"
#"status"
#If you have any concerns, please review the source code at /misc/build.func
EOF
DIAGNOSTICS="no"
fi
else
DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics)
fi
}
# ------------------------------------------------------------------------------
# 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)
local VAR_WHITELIST=(
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
)
# 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 (first valid path wins)
local _find_default_vars
_find_default_vars() {
local f
for f in \
/usr/local/community-scripts/default.vars \
"$HOME/.config/community-scripts/default.vars" \
"./default.vars"; do
[ -f "$f" ] && {
echo "$f"
return 0
}
done
return 1
}
# Ensure default.vars exists, create with sane defaults if missing
local _ensure_default_vars
_ensure_default_vars() {
_find_default_vars >/dev/null 2>&1 && return 0
local canonical="/usr/local/community-scripts/default.vars"
msg_info "No default.vars found. Creating ${canonical}"
mkdir -p /usr/local/community-scripts
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
# Storage
# Example: "local", "docker", ...
# var_template_storage=local
# var_container_storage=docker
# Resources
var_cpu=1
var_disk=4
var_ram=1024
# Network
var_brg=vmbr0
var_net=dhcp
var_ipv6_method=none
# var_gateway=
# var_ipv6_static=
# var_vlan=
# var_mtu=
# var_mac=
# var_ns=
# SSH
var_ssh=no
# var_ssh_authorized_key=
# APT cacher (optional)
# var_apt_cacher=yes
# var_apt_cacher_ip=192.168.1.10
# Features/Tags/verbosity
var_fuse=no
var_tun=no
var_tags=community-script
var_verbose=no
# Security (root PW) empty => autologin
# var_pw=
# Optional fixed CTID/hostname empty => auto
# var_ctid=
# var_hostname=
EOF
chmod 0644 "$canonical"
msg_ok "Created ${canonical}"
}
# 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
}
# Safe parser for KEY=VALUE lines
local _load_vars_file
_load_vars_file() {
local file="$1"
[ -f "$file" ] || return 0
msg_info "Loading defaults from ${file}"
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_key "$var_key" || {
msg_debug "Ignore non-whitelisted ${var_key}"
continue
}
# Strip quotes
if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
var_val="${BASH_REMATCH[1]}"
elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
var_val="${BASH_REMATCH[1]}"
fi
# Unsafe check without regex (formatter-friendly)
local _unsafe=""
case "$var_val" in
*'$('*) _unsafe=1 ;;
*'`'*) _unsafe=1 ;;
*';'*) _unsafe=1 ;;
*'&'*) _unsafe=1 ;;
*'<('*) _unsafe=1 ;;
esac
if [[ -n "$_unsafe" ]]; then
msg_warn "Ignoring ${var_key} from ${file}: unsafe characters"
continue
fi
# Hard env wins
if [[ -n "${_HARD_ENV[$var_key]:-}" ]]; then
continue
fi
# Set only if not already exported
if [[ -z "${!var_key+x}" ]]; then
export "${var_key}=${var_val}"
fi
else
msg_warn "Malformed line in ${file}: ${line}"
fi
done <"$file"
msg_ok "Loaded ${file}"
}
# 1) Ensure file exists
_ensure_default_vars
# 2) Load file
local dv
dv="$(_find_default_vars)" || {
msg_error "default.vars not found after ensure step"
return 1
}
_load_vars_file "$dv"
# 3) Map var_verbose → VERBOSE
if [[ -n "${var_verbose:-}" ]]; then
case "${var_verbose,,}" in
1 | yes | true | on) VERBOSE="yes" ;;
0 | no | false | off) VERBOSE="no" ;;
*) VERBOSE="${var_verbose}" ;;
esac
else
VERBOSE="no"
fi
# 4) Apply base settings and show summary
METHOD="mydefaults-global"
base_settings "$VERBOSE"
header_info
echo -e "${DEFAULT}${BOLD}${BL}Using My Defaults (default.vars) on node $PVEHOST_NAME${CL}"
echo_default
}
install_script() {
pve_check
shell_check
root_check
arch_check
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)
timezone=$(cat /etc/timezone)
header_info
# --- PRESET support ---
if [ -n "${PRESET:-}" ]; then
case "$PRESET" in
DEFAULT | default | 1)
CHOICE="1"
;;
VERBOSE | verbose | 2)
CHOICE="2"
;;
ADVANCED | advanced | 3)
CHOICE="3"
;;
DEFAULT_VARS | default_vars | 4)
CHOICE="4"
;;
*)
echo -e "\n${CROSS}${RD}Invalid PRESET value: ${PRESET}${CL}\n"
exit 1
;;
esac
else
#"4" "Use Config File" \
#"5" "Manage Default Storage" \
while true; do
TMP_CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--title "SETTINGS" \
--menu "Choose an option:" 20 60 6 \
"1" "Default Settings" \
"2" "Default Settings (with verbose)" \
"3" "Advanced Settings" \
"4" "My Default Vars" \
"5" "Diagnostic Settings" \
"6" "Exit" \
--default-item "1" 3>&1 1>&2 2>&3) || true
if [ -z "$TMP_CHOICE" ]; then
echo -e "\n${CROSS}${RD}Menu canceled. Exiting script.${CL}\n"
exit 0
fi
CHOICE="$TMP_CHOICE"
break
done
fi
case $CHOICE in
1)
header_info
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
VERBOSE="no"
METHOD="default"
base_settings "$VERBOSE"
echo_default
;;
2)
header_info
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME (${VERBOSE_CROPPED}Verbose)${CL}"
VERBOSE="yes"
METHOD="default"
base_settings "$VERBOSE"
echo_default
;;
3)
header_info
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
METHOD="advanced"
base_settings
advanced_settings
;;
# 4)
# header_info
# echo -e "${INFO}${HOLD} ${GN}Using Config File on node $PVEHOST_NAME${CL}"
# METHOD="advanced"
# source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/config-file.func)
# config_file
# ;;
4)
# My Defaults (default.vars)
default_var_settings || {
msg_error "Failed to apply default.vars"
exit 1
}
;;
5)
if [[ $DIAGNOSTICS == "yes" ]]; then
if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --yesno "Send Diagnostics of LXC Installation?\n\nCurrent setting: ${DIAGNOSTICS}" 10 58 \
--yes-button "No" --no-button "Back"; then
DIAGNOSTICS="no"
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --msgbox "Diagnostics settings changed to ${DIAGNOSTICS}." 8 58
fi
else
if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --yesno "Send Diagnostics of LXC Installation?\n\nCurrent setting: ${DIAGNOSTICS}" 10 58 \
--yes-button "Yes" --no-button "Back"; then
DIAGNOSTICS="yes"
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --msgbox "Diagnostics settings changed to ${DIAGNOSTICS}." 8 58
fi
fi
;;
6)
echo -e "\n${CROSS}${RD}Script terminated. Have a great day!${CL}\n"
exit 0
;;
*)
echo -e "${CROSS}${RD}Invalid option, please try again.${CL}"
;;
esac
}
check_container_resources() {
# Check actual RAM & Cores
current_ram=$(free -m | awk 'NR==2{print $2}')
current_cpu=$(nproc)
# Check whether the current RAM is less than the required RAM or the CPU cores are less than required
if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? <yes/No> "
read -r prompt
# Check if the input is 'yes', otherwise exit with status 1
if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}"
exit 1
fi
else
echo -e ""
fi
}
check_container_storage() {
# Check if the /boot partition is more than 80% full
total_size=$(df /boot --output=size | tail -n 1)
local used_size=$(df /boot --output=used | tail -n 1)
usage=$((100 * used_size / total_size))
if ((usage > 80)); then
# Prompt the user for confirmation to continue
echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
echo -ne "Continue anyway? <y/N> "
read -r prompt
# Check if the input is 'y' or 'yes', otherwise exit with status 1
if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
exit 1
fi
fi
}
start() {
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func)
if command -v pveversion >/dev/null 2>&1; then
install_script
elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
VERBOSE="no"
set_std_mode
update_script
else
CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
"Support/Update functions for ${APP} LXC. Choose an option:" \
12 60 3 \
"1" "YES (Silent Mode)" \
"2" "YES (Verbose Mode)" \
"3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
case "$CHOICE" in
1)
VERBOSE="no"
set_std_mode
;;
2)
VERBOSE="yes"
set_std_mode
;;
3)
clear
exit_script
exit
;;
esac
update_script
fi
}
# This function collects user settings and integrates all the collected information.
build_container() {
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
NET_STRING="-net0 name=eth0,bridge=$BRG$MAC,ip=$NET$GATE$VLAN$MTU"
case "$IPV6_METHOD" in
auto) NET_STRING="$NET_STRING,ip6=auto" ;;
dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
static)
NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
[ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
;;
none) ;;
esac
if [ "$CT_TYPE" == "1" ]; then
FEATURES="keyctl=1,nesting=1"
else
FEATURES="nesting=1"
fi
if [ "$ENABLE_FUSE" == "yes" ]; then
FEATURES="$FEATURES,fuse=1"
fi
#if [[ $DIAGNOSTICS == "yes" ]]; then
# post_to_api
#fi
TEMP_DIR=$(mktemp -d)
pushd "$TEMP_DIR" >/dev/null
if [ "$var_os" == "alpine" ]; then
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)"
else
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)"
fi
export DIAGNOSTICS="$DIAGNOSTICS"
export RANDOM_UUID="$RANDOM_UUID"
export CACHER="$APT_CACHER"
export CACHER_IP="$APT_CACHER_IP"
export tz="$timezone"
#export DISABLEIPV6="$DISABLEIP6"
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_DISK_SIZE="$DISK_SIZE"
export PCT_OPTIONS="
-features $FEATURES
-hostname $HN
-tags $TAGS
$SD
$NS
$NET_STRING
-onboot 1
-cores $CORE_COUNT
-memory $RAM_SIZE
-unprivileged $CT_TYPE
$PW
"
bash -c "$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/create_lxc.sh)" || exit
if [ $? -ne 0 ]; then
exit 200
fi
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
# USB passthrough for privileged LXC (CT_TYPE=0)
if [ "$CT_TYPE" == "0" ]; then
cat <<EOF >>"$LXC_CONFIG"
# USB passthrough
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
fi
# VAAPI passthrough for privileged containers or known apps
VAAPI_APPS=(
"immich" "Channels" "Emby" "ErsatzTV" "Frigate" "Jellyfin"
"Plex" "Scrypted" "Tdarr" "Unmanic" "Ollama" "FileFlows"
"Open WebUI" "Debian"
)
is_vaapi_app=false
for vaapi_app in "${VAAPI_APPS[@]}"; do
if [[ "$APP" == "$vaapi_app" ]]; then
is_vaapi_app=true
break
fi
done
if [[ "$CT_TYPE" == "0" || "$is_vaapi_app" == "true" ]]; then
VAAPI_DEVICES=()
SINGLE_VAAPI_DEVICE=""
seen_ids=()
for bypath in /dev/dri/by-path/*-render /dev/dri/renderD*; do
[[ -e "$bypath" ]] || continue
dev_render=$(readlink -f "$bypath") || continue
id=$(basename "$dev_render")
[[ " ${seen_ids[*]} " == *" $id "* ]] && continue
seen_ids+=("$id")
card_index="${id#renderD}"
card="/dev/dri/card${card_index}"
combo_devices=("$dev_render")
if [[ "${#combo_devices[@]}" -eq 1 && -e /dev/dri/card0 ]]; then
combo_devices+=("/dev/dri/card0")
fi
#echo "combo_devices=${combo_devices[*]}"
pci_addr=$(basename "$bypath" | cut -d- -f1 --complement | sed 's/-render//' || true)
pci_info=$(lspci -nn | grep "${pci_addr#0000:}" || true)
name="${pci_info#*: }"
[[ -z "$name" ]] && name="Unknown GPU ($pci_addr)"
label="$(basename "$dev_render")"
[[ -e "$card" ]] && label+=" + $(basename "$card")"
label+=" $name"
msg_debug "[VAAPI DEBUG] Adding VAAPI combo: ${combo_devices[*]} ($label)"
VAAPI_DEVICES+=("$(
IFS=:
echo "${combo_devices[*]}"
)" "$label" "OFF")
done
[[ -e /dev/fb0 ]] && VAAPI_DEVICES+=("/dev/fb0" "fb0 - Framebuffer" "OFF")
GID_VIDEO=$(getent group video | cut -d: -f3)
GID_RENDER=$(getent group render | cut -d: -f3)
[[ -z "$GID_VIDEO" ]] && GID_VIDEO=44 && msg_warn "'video' group not found, falling back to GID 44"
[[ -z "$GID_RENDER" ]] && GID_RENDER=104 && msg_warn "'render' group not found, falling back to GID 104"
if [[ "${#VAAPI_DEVICES[@]}" -eq 0 ]]; then
msg_warn "No VAAPI-compatible devices found."
elif [[ "${#VAAPI_DEVICES[@]}" -eq 3 ]]; then
#msg_info "Only one VAAPI-compatible device found enabling passthrough."
IFS=":" read -ra devices <<<"${VAAPI_DEVICES[0]//\"/}"
IDX=0
DID_MOUNT_DRI=0
for d in "${devices[@]}"; do
if [[ "$CT_TYPE" == "0" ]]; then
if [[ "$DID_MOUNT_DRI" -eq 0 && -d /dev/dri ]]; then
echo "lxc.mount.entry: /dev/dri /dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG"
DID_MOUNT_DRI=1
fi
if ! major_minor=$(stat -c '%t:%T' "$d" 2>/dev/null | awk -F: '{ printf "%d:%d", "0x"$1, "0x"$2 }'); then
msg_warn "Could not stat $d skipping."
continue
fi
echo "lxc.cgroup2.devices.allow: c $major_minor rwm" >>"$LXC_CONFIG"
echo "lxc.mount.entry: $d $d none bind,optional,create=file" >>"$LXC_CONFIG"
else
GID=$([[ "$d" =~ render ]] && echo "$GID_RENDER" || echo "$GID_VIDEO")
echo "dev${IDX}: $d,gid=${GID}" >>"$LXC_CONFIG"
IDX=$((IDX + 1))
fi
done
else
if [[ "$CT_TYPE" == "0" ]]; then
whiptail --title "VAAPI passthrough" --msgbox "\
✅ VAAPI passthrough has been enabled
GPU hardware acceleration will be available inside the container
(e.g., for Jellyfin, Plex, Frigate, etc.)
Note: You may need to install drivers manually inside the container,
such as 'intel-media-driver', 'libva2', or 'vainfo'." 15 74
else
whiptail --title "VAAPI passthrough (limited)" --msgbox "\
⚠️ VAAPI passthrough in unprivileged containers may be limited
Some drivers (e.g., iHD) require privileged access to DRM subsystems.
If VAAPI fails, consider switching to a privileged container.
Note: You may need to install drivers manually inside the container,
such as 'intel-media-driver', 'libva2', or 'vainfo'." 15 74
fi
SELECTED_DEVICES=$(whiptail --title "VAAPI Device Selection" \
--checklist "Select VAAPI device(s) / GPU(s) to passthrough:" 20 100 6 \
"${VAAPI_DEVICES[@]}" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
exit_script
msg_error "VAAPI passthrough selection cancelled by user."
fi
if [[ -n "$SELECTED_DEVICES" ]]; then
IDX=0
DID_MOUNT_DRI=0
for dev in $SELECTED_DEVICES; do
dev="${dev%\"}"
dev="${dev#\"}" # strip quotes
IFS=":" read -ra devices <<<"$dev"
msg_debug "[VAAPI DEBUG] Autopassthrough for devices: ${devices[*]}"
for d in "${devices[@]}"; do
if [[ "$CT_TYPE" == "0" ]]; then
if [[ "$DID_MOUNT_DRI" -eq 0 && -d /dev/dri ]]; then
echo "lxc.mount.entry: /dev/dri /dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG"
DID_MOUNT_DRI=1
fi
if ! major_minor=$(stat -c '%t:%T' "$d" 2>/dev/null | awk -F: '{ printf "%d:%d", "0x"$1, "0x"$2 }'); then
msg_warn "Could not stat $d skipping."
continue
fi
echo "lxc.cgroup2.devices.allow: c $major_minor rwm" >>"$LXC_CONFIG"
echo "lxc.mount.entry: $d $d none bind,optional,create=file" >>"$LXC_CONFIG"
else
GID=$([[ "$d" =~ render ]] && echo "$GID_RENDER" || echo "$GID_VIDEO")
echo "dev${IDX}: $d,gid=${GID}" >>"$LXC_CONFIG"
IDX=$((IDX + 1))
fi
done
done
else
msg_warn "No VAAPI devices selected passthrough skipped."
fi
fi
fi
# TUN device passthrough
if [ "$ENABLE_TUN" == "yes" ]; then
cat <<EOF >>"$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
# This starts the container and executes <app>-install.sh
msg_info "Starting LXC Container"
pct start "$CTID"
# wait for status '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
msg_error "LXC Container did not reach running state"
exit 1
fi
done
if [ "$var_os" != "alpine" ]; then
msg_info "Waiting for network in LXC container"
sleep 2
for i in {1..10}; do
# 1. Primary check: ICMP ping (fastest, but may be blocked by ISP/firewall)
if pct exec "$CTID" -- ping -c1 -W1 deb.debian.org >/dev/null 2>&1; then
msg_ok "Network in LXC is reachable (ping)"
break
fi
# Wait and retry if not reachable yet
if [ "$i" -lt 10 ]; then
if [ "$i" -le 3 ]; then
sleep 2
else
msg_warn "No network in LXC yet (try $i/10) waiting..."
sleep 3
fi
else
# After 10 unsuccessful ping attempts, try HTTP connectivity via wget as fallback
msg_warn "Ping failed 10 times. Trying HTTP connectivity check (wget) as fallback..."
if pct exec "$CTID" -- wget -q --spider http://deb.debian.org; then
msg_ok "Network in LXC is reachable (wget fallback)"
else
msg_error "No network in LXC after all checks."
read -r -p "Set fallback DNS (1.1.1.1/8.8.8.8)? [y/N]: " choice
case "$choice" in
[yY]*)
pct set "$CTID" --nameserver 1.1.1.1
pct set "$CTID" --nameserver 8.8.8.8
# Final attempt with wget after DNS change
if pct exec "$CTID" -- wget -q --spider http://deb.debian.org; then
msg_ok "Network reachable after DNS fallback"
else
msg_error "Still no network/DNS in LXC! Aborting customization."
exit_script
fi
;;
*)
msg_error "Aborted by user no DNS fallback set."
exit_script
;;
esac
fi
break
fi
done
fi
msg_info "Customizing LXC Container"
if [ "$var_os" == "alpine" ]; then
sleep 3
pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
EOF'
pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
else
sleep 3
pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen"
pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
echo LANG=\$locale_line >/etc/default/locale && \
locale-gen >/dev/null && \
export LANG=\$locale_line"
if [[ -z "${tz:-}" ]]; then
tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
fi
if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime"
else
msg_warn "Skipping timezone setup zone '$tz' not found in container"
fi
pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || {
msg_error "apt-get base packages installation failed"
exit 1
}
fi
msg_ok "Customized LXC Container"
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"
}
destroy_lxc() {
if [[ -n "$CT_ID" ]]; then
read -p "Remove this Container? <y/N> " prompt
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
pct stop "$CT_ID" &>/dev/null
pct destroy "$CT_ID" &>/dev/null
msg_ok "Removed this Container"
else
msg_info "Container was not removed."
fi
else
msg_error "No CT_ID found. Nothing to remove."
fi
}
# This function sets the description of the container.
description() {
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
# Generate LXC Description
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<img src='https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>
<h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</h2>
<p style='margin: 16px 0;'>
<a href='https://ko-fi.com/community_scripts' target='_blank' rel='noopener noreferrer'>
<img src='https://img.shields.io/badge/&#x2615;-Buy us a coffee-blue' alt='spend Coffee' />
</a>
</p>
<span style='margin: 0 10px;'>
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVED' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
</span>
<span style='margin: 0 10px;'>
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVED/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
</span>
<span style='margin: 0 10px;'>
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVED/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
</span>
</div>
EOF
)
# Set Description in LXC
pct set "$CTID" -description "$DESCRIPTION"
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
systemctl start ping-instances.service
fi
post_update_to_api "done" "none"
}
api_exit_script() {
exit_code=$?
if [ $exit_code -ne 0 ]; then
case $exit_code in
100) post_update_to_api "failed" "100: Unexpected error in create_lxc.sh" ;;
101) post_update_to_api "failed" "101: No network connection detected in create_lxc.sh" ;;
200) post_update_to_api "failed" "200: LXC creation failed in create_lxc.sh" ;;
201) post_update_to_api "failed" "201: Invalid Storage class in create_lxc.sh" ;;
202) post_update_to_api "failed" "202: User aborted menu in create_lxc.sh" ;;
203) post_update_to_api "failed" "203: CTID not set in create_lxc.sh" ;;
204) post_update_to_api "failed" "204: PCT_OSTYPE not set in create_lxc.sh" ;;
205) post_update_to_api "failed" "205: CTID cannot be less than 100 in create_lxc.sh" ;;
206) post_update_to_api "failed" "206: CTID already in use in create_lxc.sh" ;;
207) post_update_to_api "failed" "207: Template not found in create_lxc.sh" ;;
208) post_update_to_api "failed" "208: Error downloading template in create_lxc.sh" ;;
209) post_update_to_api "failed" "209: Container creation failed, but template is intact in create_lxc.sh" ;;
*) post_update_to_api "failed" "Unknown error, exit code: $exit_code in create_lxc.sh" ;;
esac
fi
}
if command -v pveversion >/dev/null 2>&1; then
trap 'api_exit_script' EXIT
fi
trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM