2352 lines
80 KiB
Bash
2352 lines
80 KiB
Bash
#!/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()
|
||
#
|
||
# - Normalize application name (NSAPP = lowercase, no spaces)
|
||
# - Build installer filename (var_install)
|
||
# - Define regex for integer validation
|
||
# - Fetch hostname of Proxmox node
|
||
# - Set default values for diagnostics/method
|
||
# - Generate random UUID for tracking
|
||
# ------------------------------------------------------------------------------
|
||
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}
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Load core + error handler functions from community-scripts repo
|
||
#
|
||
# - Prefer curl if available, fallback to wget
|
||
# - Load: core.func, error_handler.func, api.func
|
||
# - Initialize error traps after loading
|
||
# ------------------------------------------------------------------------------
|
||
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)
|
||
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
|
||
load_functions
|
||
init_error_traps
|
||
#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)
|
||
source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
|
||
load_functions
|
||
init_error_traps
|
||
#echo "(build.func) Loaded core.func via wget"
|
||
fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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
|
||
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}"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# get_current_ip()
|
||
#
|
||
# - Returns current container IP depending on OS type
|
||
# - Debian/Ubuntu: uses `hostname -I`
|
||
# - Alpine: parses eth0 via `ip -4 addr`
|
||
# ------------------------------------------------------------------------------
|
||
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"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# update_motd_ip()
|
||
#
|
||
# - Updates /etc/motd with current container IP
|
||
# - Removes old IP entries to avoid duplicates
|
||
# ------------------------------------------------------------------------------
|
||
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
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ssh_check()
|
||
#
|
||
# - Detects if script is running over SSH
|
||
# - Warns user and recommends using Proxmox shell
|
||
# - User can choose to continue or abort
|
||
# ------------------------------------------------------------------------------
|
||
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
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# install_ssh_keys_into_ct()
|
||
#
|
||
# - Installs SSH keys into container root account if SSH is enabled
|
||
# - Uses pct push or direct input to authorized_keys
|
||
# - Falls back to warning if no keys provided
|
||
# ------------------------------------------------------------------------------
|
||
install_ssh_keys_into_ct() {
|
||
[[ "$SSH" != "yes" ]] && return 0
|
||
|
||
if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then
|
||
msg_info "Installing selected SSH keys into CT ${CTID}"
|
||
pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || {
|
||
msg_error "prepare /root/.ssh failed"
|
||
return 1
|
||
}
|
||
pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 ||
|
||
pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || {
|
||
msg_error "write authorized_keys failed"
|
||
return 1
|
||
}
|
||
pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
|
||
msg_ok "Installed SSH keys into CT ${CTID}"
|
||
return 0
|
||
fi
|
||
|
||
# Fallback: nichts ausgewählt
|
||
msg_warn "No SSH keys to install (skipping)."
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# base_settings()
|
||
#
|
||
# - Defines all base/default variables for container creation
|
||
# - Reads from environment variables (var_*)
|
||
# - Provides fallback defaults for OS type/version
|
||
# ------------------------------------------------------------------------------
|
||
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
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# echo_default()
|
||
#
|
||
# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
|
||
# - Uses icons and formatting for readability
|
||
# - Convert CT_TYPE to description
|
||
# ------------------------------------------------------------------------------
|
||
echo_default() {
|
||
CT_TYPE_DESC="Unprivileged"
|
||
if [ "$CT_TYPE" -eq 0 ]; then
|
||
CT_TYPE_DESC="Privileged"
|
||
fi
|
||
|
||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
|
||
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
|
||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
|
||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
|
||
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
|
||
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
|
||
if [ "$VERBOSE" == "yes" ]; then
|
||
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
|
||
fi
|
||
echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
|
||
echo -e " "
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# exit_script()
|
||
#
|
||
# - Called when user cancels an action
|
||
# - Clears screen and exits gracefully
|
||
# ------------------------------------------------------------------------------
|
||
exit_script() {
|
||
clear
|
||
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
|
||
exit
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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[*]}"
|
||
)
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# advanced_settings()
|
||
#
|
||
# - Interactive whiptail menu for advanced configuration
|
||
# - Lets user set container type, password, CT ID, hostname, disk, CPU, RAM
|
||
# - Supports IPv4/IPv6, DNS, MAC, VLAN, tags, SSH keys, FUSE, verbose mode
|
||
# - Ends with confirmation or re-entry if cancelled
|
||
# ------------------------------------------------------------------------------
|
||
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 key provisioning (one dialog) ---
|
||
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[@]}"
|
||
DEF_KEYS_COUNT="$COUNT"
|
||
|
||
if [[ "$DEF_KEYS_COUNT" -gt 0 ]]; then
|
||
SSH_KEY_MODE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
|
||
"Provision SSH keys for root:" 14 72 4 \
|
||
"found" "Select from detected keys (${DEF_KEYS_COUNT})" \
|
||
"manual" "Paste a single public key" \
|
||
"folder" "Scan another folder (path or glob)" \
|
||
"none" "No keys" 3>&1 1>&2 2>&3) || exit_script
|
||
else
|
||
SSH_KEY_MODE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
|
||
"No host keys detected; choose manual/none:" 12 72 2 \
|
||
"manual" "Paste a single public key" \
|
||
"none" "No keys" 3>&1 1>&2 2>&3) || exit_script
|
||
fi
|
||
|
||
case "$SSH_KEY_MODE" in
|
||
found)
|
||
SEL=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \
|
||
--checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
|
||
for tag in $SEL; do
|
||
tag="${tag%\"}"
|
||
tag="${tag#\"}"
|
||
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
|
||
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
|
||
done
|
||
;;
|
||
manual)
|
||
SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
|
||
--inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
|
||
[[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
|
||
;;
|
||
folder)
|
||
GLOB_PATH="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
|
||
--inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)"
|
||
if [[ -n "$GLOB_PATH" ]]; then
|
||
shopt -s nullglob
|
||
read -r -a _scan_files <<<"$GLOB_PATH"
|
||
shopt -u nullglob
|
||
if [[ "${#_scan_files[@]}" -gt 0 ]]; then
|
||
ssh_build_choices_from_files "${_scan_files[@]}"
|
||
if [[ "$COUNT" -gt 0 ]]; then
|
||
SEL=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \
|
||
--checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
|
||
for tag in $SEL; do
|
||
tag="${tag%\"}"
|
||
tag="${tag#\"}"
|
||
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
|
||
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
|
||
done
|
||
else
|
||
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $GLOB_PATH" 8 60
|
||
fi
|
||
else
|
||
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60
|
||
fi
|
||
fi
|
||
;;
|
||
none) : ;;
|
||
esac
|
||
|
||
# Dedupe + clean EOF
|
||
if [[ -s "$SSH_KEYS_FILE" ]]; then
|
||
sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE"
|
||
printf '\n' >>"$SSH_KEYS_FILE"
|
||
fi
|
||
|
||
# SSH activate, if keys found or password set
|
||
if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then
|
||
if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
|
||
SSH="yes"
|
||
else
|
||
SSH="no"
|
||
fi
|
||
else
|
||
SSH="no"
|
||
fi
|
||
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
|
||
|
||
export SSH_KEYS_FILE
|
||
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()
|
||
#
|
||
# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
|
||
# - Asks user whether to send anonymous diagnostic data
|
||
# - Saves DIAGNOSTICS=yes/no in the config file
|
||
# ------------------------------------------------------------------------------
|
||
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=local
|
||
|
||
# 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
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# get_app_defaults_path()
|
||
#
|
||
# - Returns full path for app-specific defaults file
|
||
# - Example: /usr/local/community-scripts/defaults/<app>.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 <nsapp>.vars exists, offers to persist current advanced settings
|
||
# into /usr/local/community-scripts/defaults/<nsapp>.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
|
||
declare -ag 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
|
||
)
|
||
fi
|
||
|
||
_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
|
||
declare -A _VARS_IN
|
||
_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" ] && 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
|
||
[ -z "${!key+x}" ] && export "$key=$val"
|
||
fi
|
||
;;
|
||
esac
|
||
done <"$file"
|
||
msg_ok "Loaded ${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 <app>.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:-}"
|
||
_fuse="${ENABLE_FUSE:-no}"
|
||
_tun="${ENABLE_TUN:-no}"
|
||
_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:-}"
|
||
_ct_storage="${CONTAINER_STORAGE:-}"
|
||
|
||
{
|
||
echo "# App-specific defaults for ${APP} (${NSAPP})"
|
||
echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||
echo
|
||
|
||
echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
|
||
echo "var_cpu=$(_sanitize_value "$_cpu")"
|
||
echo "var_ram=$(_sanitize_value "$_ram")"
|
||
echo "var_disk=$(_sanitize_value "$_disk")"
|
||
|
||
[ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
|
||
[ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
|
||
[ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
|
||
[ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
|
||
[ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
|
||
[ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
|
||
[ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
|
||
|
||
[ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
|
||
[ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")"
|
||
|
||
[ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
|
||
[ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
|
||
|
||
[ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
|
||
[ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
|
||
|
||
[ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
|
||
[ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
|
||
[ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
|
||
[ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
|
||
|
||
[ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
|
||
[ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
|
||
|
||
[ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
|
||
[ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
|
||
} >"$tmpf"
|
||
|
||
echo "$tmpf"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# maybe_offer_save_app_defaults()
|
||
#
|
||
# - Called after advanced_settings()
|
||
# - Offers to save current values as app defaults if not existing
|
||
# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
|
||
# ------------------------------------------------------------------------------
|
||
maybe_offer_save_app_defaults() {
|
||
local app_vars_path
|
||
app_vars_path="$(get_app_defaults_path)"
|
||
|
||
# always build from current settings
|
||
local new_tmp diff_tmp
|
||
new_tmp="$(_build_current_app_vars_tmp)"
|
||
diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
|
||
|
||
# 1) if no file → offer to create
|
||
if [[ ! -f "$app_vars_path" ]]; then
|
||
if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
|
||
--yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then
|
||
mkdir -p "$(dirname "$app_vars_path")"
|
||
install -m 0644 "$new_tmp" "$app_vars_path"
|
||
msg_ok "Saved app defaults: ${app_vars_path}"
|
||
fi
|
||
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 "[dev] Proxmox VE Helper Scripts" \
|
||
--title "APP DEFAULTS – ${APP}" \
|
||
--menu "Differences detected. What do you want to do?" 20 78 10 \
|
||
"Update Defaults" "Write new values to ${app_vars_file}" \
|
||
"Keep Current" "Keep existing defaults (no changes)" \
|
||
"View Diff" "Show a detailed diff" \
|
||
"Cancel" "Abort without changes" \
|
||
--default-item "Update Defaults" \
|
||
3>&1 1>&2 2>&3)" || { sel="Cancel"; }
|
||
|
||
case "$sel" in
|
||
"Update Defaults")
|
||
install -m 0644 "$new_tmp" "$app_vars_path"
|
||
msg_ok "Updated app defaults: ${app_vars_path}"
|
||
break
|
||
;;
|
||
"Keep Current")
|
||
msg_info "Keeping current app defaults: ${app_vars_path}"
|
||
break
|
||
;;
|
||
"View Diff")
|
||
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
|
||
--title "Diff – ${APP}" \
|
||
--scrolltext --textbox "$diff_tmp" 25 100
|
||
;;
|
||
"Cancel" | *)
|
||
msg_info "Canceled. No changes to app defaults."
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
|
||
rm -f "$new_tmp" "$diff_tmp"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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
|
||
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"
|
||
;;
|
||
MYDEFAULTS | mydefaults | 4)
|
||
CHOICE="4"
|
||
;;
|
||
APPDEFAULTS | appdefaults | 5)
|
||
CHOICE="5"
|
||
;;
|
||
DIAGNOSTICS | diagnostics | 6)
|
||
CHOICE="6"
|
||
;;
|
||
STORAGE | storage | 7)
|
||
CHOICE="7"
|
||
;;
|
||
*)
|
||
echo -e "\n${CROSS}${RD}Invalid PRESET value: ${PRESET}${CL}\n"
|
||
exit 1
|
||
;;
|
||
esac
|
||
else
|
||
# Build dynamic menu
|
||
local menu_items=(
|
||
"1" "Default Settings"
|
||
"2" "Default Settings (Verbose)"
|
||
"3" "Advanced Install"
|
||
"4" "My Defaults"
|
||
)
|
||
if [ -f "$(get_app_defaults_path)" ]; then
|
||
menu_items+=("5" "App Defaults for ${APP}")
|
||
fi
|
||
menu_items+=(
|
||
"6" "Diagnostic Settings"
|
||
"7" "Storage Settings"
|
||
"8" "Exit"
|
||
)
|
||
|
||
TMP_CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
|
||
--title "SETTINGS" \
|
||
--menu "Choose an option:" 20 60 9 \
|
||
"${menu_items[@]}" \
|
||
--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"
|
||
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)${CL}"
|
||
VERBOSE="yes"
|
||
METHOD="default"
|
||
base_settings "$VERBOSE"
|
||
echo_default
|
||
;;
|
||
3)
|
||
header_info
|
||
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
|
||
METHOD="advanced"
|
||
base_settings
|
||
advanced_settings
|
||
maybe_offer_save_app_defaults
|
||
;;
|
||
4)
|
||
default_var_settings || {
|
||
msg_error "Failed to apply default.vars"
|
||
exit 1
|
||
}
|
||
check_storage_or_prompt "/usr/local/community-scripts/default.vars"
|
||
;;
|
||
5)
|
||
# App Defaults
|
||
if [ -f "$(get_app_defaults_path)" ]; then
|
||
header_info
|
||
echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}"
|
||
METHOD="appdefaults"
|
||
base_settings
|
||
_load_vars_file "$(get_app_defaults_path)"
|
||
echo_default
|
||
check_storage_or_prompt "$(get_app_defaults_path)"
|
||
else
|
||
msg_error "No App Defaults available for ${APP}"
|
||
exit 1
|
||
fi
|
||
;;
|
||
6)
|
||
if [[ $DIAGNOSTICS == "yes" ]]; then
|
||
if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTIC SETTINGS" --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
|
||
--yes-button "No" --no-button "Back"; then
|
||
DIAGNOSTICS="no"
|
||
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
|
||
whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
|
||
fi
|
||
else
|
||
if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTIC SETTINGS" --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
|
||
--yes-button "Yes" --no-button "Back"; then
|
||
DIAGNOSTICS="yes"
|
||
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
|
||
whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
|
||
fi
|
||
fi
|
||
;;
|
||
7)
|
||
storage_settings_menu
|
||
;;
|
||
8)
|
||
echo -e "\n${CROSS}${RD}Script terminated.${CL}\n"
|
||
exit 0
|
||
;;
|
||
*)
|
||
echo -e "${CROSS}${RD}Invalid option, please try again.${CL}"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# check_storage_or_prompt()
|
||
#
|
||
# - Validates container/template storage
|
||
# - If invalid or missing, prompts user to select new storage
|
||
# - Updates vars file accordingly
|
||
# ------------------------------------------------------------------------------
|
||
check_storage_or_prompt() {
|
||
local vars_file="$1"
|
||
local changed=0
|
||
|
||
if [[ ! -f "$vars_file" ]]; then
|
||
msg_warn "No vars file found at $vars_file"
|
||
return 1
|
||
fi
|
||
|
||
_validate_storage() {
|
||
local s="$1"
|
||
[[ -n "$s" ]] || return 1
|
||
pvesm status -content images | awk 'NR>1 {print $1}' | grep -qx "$s"
|
||
}
|
||
|
||
# Load current values
|
||
local ct_store tpl_store
|
||
ct_store="$(awk -F= '/^var_container_storage=/ {print $2}' "$vars_file" | head -n1)"
|
||
tpl_store="$(awk -F= '/^var_template_storage=/ {print $2}' "$vars_file" | head -n1)"
|
||
|
||
# Container storage
|
||
if ! _validate_storage "$ct_store"; then
|
||
local new_ct
|
||
new_ct=$(pvesm status -content images | awk 'NR>1 {print $1" "$2" "$6}')
|
||
new_ct=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
|
||
--title "Select Container Storage" \
|
||
--menu "Choose container storage:" 20 60 10 $new_ct 3>&1 1>&2 2>&3) || return 1
|
||
if [[ -n "$new_ct" ]]; then
|
||
sed -i "/^var_container_storage=/d" "$vars_file"
|
||
echo "var_container_storage=$new_ct" >>"$vars_file"
|
||
changed=1
|
||
msg_ok "Updated container storage in $vars_file → $new_ct"
|
||
fi
|
||
fi
|
||
|
||
# Template storage
|
||
if ! _validate_storage "$tpl_store"; then
|
||
local new_tpl
|
||
new_tpl=$(pvesm status -content vztmpl | awk 'NR>1 {print $1" "$2" "$6}')
|
||
new_tpl=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
|
||
--title "Select Template Storage" \
|
||
--menu "Choose template storage:" 20 60 10 $new_tpl 3>&1 1>&2 2>&3) || return 1
|
||
if [[ -n "$new_tpl" ]]; then
|
||
sed -i "/^var_template_storage=/d" "$vars_file"
|
||
echo "var_template_storage=$new_tpl" >>"$vars_file"
|
||
changed=1
|
||
msg_ok "Updated template storage in $vars_file → $new_tpl"
|
||
fi
|
||
fi
|
||
|
||
return $changed
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# storage_settings_menu()
|
||
#
|
||
# - Menu for managing storage defaults
|
||
# - Options: update My Defaults or App Defaults storage
|
||
# ------------------------------------------------------------------------------
|
||
storage_settings_menu() {
|
||
local menu_items=(
|
||
"1" "Check & update My Defaults (default.vars)"
|
||
)
|
||
if [ -f "$(get_app_defaults_path)" ]; then
|
||
menu_items+=("2" "Check & update App Defaults for ${APP}")
|
||
fi
|
||
menu_items+=("3" "Back")
|
||
|
||
local choice
|
||
choice=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
|
||
--title "STORAGE SETTINGS" \
|
||
--menu "Select what to update:" 15 60 5 \
|
||
"${menu_items[@]}" 3>&1 1>&2 2>&3) || return 0
|
||
|
||
case "$choice" in
|
||
1)
|
||
check_storage_or_prompt "/usr/local/community-scripts/default.vars"
|
||
;;
|
||
2)
|
||
if [ -f "$(get_app_defaults_path)" ]; then
|
||
check_storage_or_prompt "$(get_app_defaults_path)"
|
||
fi
|
||
;;
|
||
3)
|
||
return 0
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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
|
||
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
|
||
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()
|
||
#
|
||
# - Checks /boot partition usage
|
||
# - Warns if usage >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
|
||
echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
|
||
echo -ne "Continue anyway? <y/N> "
|
||
read -r prompt
|
||
if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
|
||
echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
|
||
exit 1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ssh_extract_keys_from_file()
|
||
#
|
||
# - Extracts valid SSH public keys from given file
|
||
# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines
|
||
# ------------------------------------------------------------------------------
|
||
ssh_extract_keys_from_file() {
|
||
local f="$1"
|
||
[[ -r "$f" ]] || return 0
|
||
tr -d '\r' <"$f" | awk '
|
||
/^[[:space:]]*#/ {next}
|
||
/^[[:space:]]*$/ {next}
|
||
# nackt: typ base64 [comment]
|
||
/^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
|
||
# mit Optionen: finde ab erstem Key-Typ
|
||
{
|
||
match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/)
|
||
if (RSTART>0) { print substr($0, RSTART) }
|
||
}
|
||
'
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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[@]}"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# start()
|
||
#
|
||
# - Entry point of script
|
||
# - On Proxmox host: calls install_script
|
||
# - In silent mode: runs update_script
|
||
# - Otherwise: shows update/setting menu
|
||
# ------------------------------------------------------------------------------
|
||
start() {
|
||
source <(curl -fsSL https://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
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# build_container()
|
||
#
|
||
# - Creates and configures the LXC container
|
||
# - Builds network string and applies features (FUSE, TUN, VAAPI passthrough)
|
||
# - Starts container and waits for network connectivity
|
||
# - Installs base packages, SSH keys, and runs <app>-install.sh
|
||
# ------------------------------------------------------------------------------
|
||
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
|
||
"
|
||
export TEMPLATE_STORAGE="${var_template_storage:-}"
|
||
export CONTAINER_STORAGE="${var_container_storage:-}"
|
||
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" "Tunarr"
|
||
)
|
||
|
||
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"
|
||
install_ssh_keys_into_ct
|
||
if ! lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"; then
|
||
exit $?
|
||
fi
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# description()
|
||
#
|
||
# - Sets container description with HTML content (logo, links, badges)
|
||
# - Restarts ping-instances.service if present
|
||
# - Posts status "done" to API
|
||
# ------------------------------------------------------------------------------
|
||
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/☕-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
|
||
)
|
||
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 trap handler
|
||
# - Reports exit codes to API with detailed reason
|
||
# - Handles known codes (100–209) and maps them to errors
|
||
# ------------------------------------------------------------------------------
|
||
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
|