diff --git a/scripts/core/alpine-install.func b/scripts/core/alpine-install.func index ae3a987..291708a 100644 --- a/scripts/core/alpine-install.func +++ b/scripts/core/alpine-install.func @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025 community-scripts ORG +# Copyright (c) 2021-2026 community-scripts ORG # Author: tteck (tteckster) # Co-Author: MickLesk # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE diff --git a/scripts/core/api.func b/scripts/core/api.func index e9e6c59..92c26ce 100644 --- a/scripts/core/api.func +++ b/scripts/core/api.func @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025 community-scripts ORG +# Copyright (c) 2021-2026 community-scripts ORG # Author: michelroegl-brunner # License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE diff --git a/scripts/core/build.func b/scripts/core/build.func index 1b11f66..67d02b1 100755 --- a/scripts/core/build.func +++ b/scripts/core/build.func @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright (c) 2021-2025 community-scripts ORG +# Copyright (c) 2021-2026 community-scripts ORG # Author: tteck (tteckster) | MickLesk | michelroegl-brunner # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE @@ -205,7 +205,10 @@ update_motd_ip() { # - Falls back to warning if no keys provided # ------------------------------------------------------------------------------ install_ssh_keys_into_ct() { - [[ "$SSH" != "yes" ]] && return 0 + [[ "${SSH:-no}" != "yes" ]] && return 0 + + # Ensure SSH_KEYS_FILE is defined (may not be set if advanced_settings was skipped) + : "${SSH_KEYS_FILE:=}" if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then msg_info "Installing selected SSH keys into CT ${CTID}" @@ -288,6 +291,90 @@ find_host_ssh_keys() { ) } +# ============================================================================== +# SECTION 3B: IP RANGE SCANNING +# ============================================================================== + +# ------------------------------------------------------------------------------ +# ip_to_int() / int_to_ip() +# +# - Converts IP address to integer and vice versa for range iteration +# ------------------------------------------------------------------------------ +ip_to_int() { + local IFS=. + read -r i1 i2 i3 i4 <<<"$1" + echo $(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4)) +} + +int_to_ip() { + local ip=$1 + echo "$(((ip >> 24) & 0xFF)).$(((ip >> 16) & 0xFF)).$(((ip >> 8) & 0xFF)).$((ip & 0xFF))" +} + +# ------------------------------------------------------------------------------ +# resolve_ip_from_range() +# +# - Takes an IP range in format "10.0.0.1/24-10.0.0.10/24" +# - Pings each IP in the range to find the first available one +# - Returns the first free IP with CIDR notation +# - Sets NET_RESOLVED to the resolved IP or empty on failure +# ------------------------------------------------------------------------------ +resolve_ip_from_range() { + local range="$1" + local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$' + local ip_start ip_end + + # Parse range: "10.0.0.1/24-10.0.0.10/24" + ip_start="${range%%-*}" + ip_end="${range##*-}" + + if [[ ! "$ip_start" =~ $ip_cidr_regex ]] || [[ ! "$ip_end" =~ $ip_cidr_regex ]]; then + NET_RESOLVED="" + return 1 + fi + + local ip1="${ip_start%%/*}" + local ip2="${ip_end%%/*}" + local cidr="${ip_start##*/}" + + local start_int=$(ip_to_int "$ip1") + local end_int=$(ip_to_int "$ip2") + + for ((ip_int = start_int; ip_int <= end_int; ip_int++)); do + local ip=$(int_to_ip $ip_int) + msg_info "Checking IP: $ip" + if ! ping -c 1 -W 1 "$ip" >/dev/null 2>&1; then + NET_RESOLVED="$ip/$cidr" + msg_ok "Found free IP: ${BGN}$NET_RESOLVED${CL}" + return 0 + fi + done + + NET_RESOLVED="" + msg_error "No free IP found in range $range" + return 1 +} + +# ------------------------------------------------------------------------------ +# is_ip_range() +# +# - Checks if a string is an IP range (contains - and looks like IP/CIDR) +# - Returns 0 if it's a range, 1 otherwise +# ------------------------------------------------------------------------------ +is_ip_range() { + local value="$1" + local ip_start ip_end + if [[ "$value" == *-* ]] && [[ "$value" != "dhcp" ]]; then + local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$' + ip_start="${value%%-*}" + ip_end="${value##*-}" + if [[ "$ip_start" =~ $ip_cidr_regex ]] && [[ "$ip_end" =~ $ip_cidr_regex ]]; then + return 0 + fi + fi + return 1 +} + # ============================================================================== # SECTION 4: STORAGE & RESOURCE MANAGEMENT # ============================================================================== @@ -400,6 +487,18 @@ base_settings() { HN=${var_hostname:-$NSAPP} BRG=${var_brg:-"vmbr0"} NET=${var_net:-"dhcp"} + + # Resolve IP range if NET contains a range (e.g., 192.168.1.100/24-192.168.1.200/24) + if is_ip_range "$NET"; then + msg_info "Scanning IP range: $NET" + if resolve_ip_from_range "$NET"; then + NET="$NET_RESOLVED" + else + msg_error "Could not find free IP in range. Falling back to DHCP." + NET="dhcp" + fi + fi + IPV6_METHOD=${var_ipv6_method:-"none"} IPV6_STATIC=${var_ipv6_static:-""} GATE=${var_gateway:-""} @@ -430,6 +529,15 @@ base_settings() { ENABLE_FUSE=${var_fuse:-"${1:-no}"} ENABLE_TUN=${var_tun:-"${1:-no}"} + # Additional settings that may be skipped if advanced_settings is not run (e.g., App Defaults) + ENABLE_GPU=${var_gpu:-"no"} + ENABLE_NESTING=${var_nesting:-"1"} + ENABLE_KEYCTL=${var_keyctl:-"0"} + ENABLE_MKNOD=${var_mknod:-"0"} + PROTECT_CT=${var_protection:-"no"} + CT_TIMEZONE=${var_timezone:-"$timezone"} + [[ "${CT_TIMEZONE:-}" == Etc/* ]] && CT_TIMEZONE="host" # pct doesn't accept Etc/* zones + # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts if [ -z "$var_os" ]; then var_os="debian" @@ -445,15 +553,17 @@ base_settings() { # - Safe parser for KEY=VALUE lines from vars files # - Used by default_var_settings and app defaults loading # - Only loads whitelisted var_* keys +# - Optional force parameter to override existing values (for app defaults) # ------------------------------------------------------------------------------ load_vars_file() { local file="$1" + local force="${2:-no}" # If "yes", override existing variables [ -f "$file" ] || return 0 msg_info "Loading defaults from ${file}" # Allowed var_* keys local VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_keyctl + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_keyctl var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage @@ -478,6 +588,12 @@ load_vars_file() { [[ "$var_key" != var_* ]] && continue _is_whitelisted "$var_key" || continue + # Strip inline comments (anything after unquoted #) + # Only strip if not inside quotes + if [[ ! "$var_val" =~ ^[\"\'] ]]; then + var_val="${var_val%%#*}" + fi + # Strip quotes if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then var_val="${BASH_REMATCH[1]}" @@ -485,8 +601,15 @@ load_vars_file() { var_val="${BASH_REMATCH[1]}" fi - # Set only if not already exported - [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}" + # Trim trailing whitespace + var_val="${var_val%"${var_val##*[![:space:]]}"}" + + # Set variable: force mode overrides existing, otherwise only set if empty + if [[ "$force" == "yes" ]]; then + export "${var_key}=${var_val}" + else + [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}" + fi fi done <"$file" msg_ok "Loaded ${file}" @@ -505,7 +628,7 @@ default_var_settings() { # Allowed var_* keys (alphabetically sorted) # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) local VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_keyctl + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_keyctl var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage @@ -667,7 +790,7 @@ get_app_defaults_path() { if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) declare -ag VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_gateway var_hostname var_ipv6_method var_mac var_mtu var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage @@ -816,6 +939,7 @@ _build_current_app_vars_tmp() { _apt_cacher_ip="${APT_CACHER_IP:-}" _fuse="${ENABLE_FUSE:-no}" _tun="${ENABLE_TUN:-no}" + _gpu="${ENABLE_GPU:-no}" _nesting="${ENABLE_NESTING:-1}" _keyctl="${ENABLE_KEYCTL:-0}" _mknod="${ENABLE_MKNOD:-0}" @@ -865,6 +989,7 @@ _build_current_app_vars_tmp() { [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" + [ -n "$_gpu" ] && echo "var_gpu=$(_sanitize_value "$_gpu")" [ -n "$_nesting" ] && echo "var_nesting=$(_sanitize_value "$_nesting")" [ -n "$_keyctl" ] && echo "var_keyctl=$(_sanitize_value "$_keyctl")" [ -n "$_mknod" ] && echo "var_mknod=$(_sanitize_value "$_mknod")" @@ -1011,37 +1136,52 @@ advanced_settings() { # Initialize defaults TAGS="community-script;${var_tags:-}" local STEP=1 - local MAX_STEP=19 + local MAX_STEP=28 - # Store values for back navigation - local _ct_type="${CT_TYPE:-1}" + # Store values for back navigation - inherit from var_* app defaults + local _ct_type="${var_unprivileged:-1}" local _pw="" local _pw_display="Automatic Login" local _ct_id="$NEXTID" local _hostname="$NSAPP" - local _disk_size="$var_disk" - local _core_count="$var_cpu" - local _ram_size="$var_ram" - local _bridge="vmbr0" - local _net="dhcp" - local _gate="" - local _ipv6_method="auto" + local _disk_size="${var_disk:-4}" + local _core_count="${var_cpu:-1}" + local _ram_size="${var_ram:-1024}" + local _bridge="${var_brg:-vmbr0}" + local _net="${var_net:-dhcp}" + local _gate="${var_gateway:-}" + local _ipv6_method="${var_ipv6_method:-auto}" local _ipv6_addr="" local _ipv6_gate="" - local _apt_cacher_ip="" - local _mtu="" - local _sd="" - local _ns="" - local _mac="" - local _vlan="" + local _apt_cacher="${var_apt_cacher:-no}" + local _apt_cacher_ip="${var_apt_cacher_ip:-}" + local _mtu="${var_mtu:-}" + local _sd="${var_searchdomain:-}" + local _ns="${var_ns:-}" + local _mac="${var_mac:-}" + local _vlan="${var_vlan:-}" local _tags="$TAGS" - local _enable_fuse="no" - local _verbose="no" - local _enable_keyctl="0" - local _enable_mknod="0" - local _mount_fs="" - local _protect_ct="no" - local _ct_timezone="" + local _enable_fuse="${var_fuse:-no}" + local _enable_tun="${var_tun:-no}" + local _enable_gpu="${var_gpu:-no}" + local _enable_nesting="${var_nesting:-1}" + local _verbose="${var_verbose:-no}" + local _enable_keyctl="${var_keyctl:-0}" + local _enable_mknod="${var_mknod:-0}" + local _mount_fs="${var_mount_fs:-}" + local _protect_ct="${var_protection:-no}" + + # Detect host timezone for default (if not set via var_timezone) + local _host_timezone="" + if command -v timedatectl >/dev/null 2>&1; then + _host_timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "") + elif [ -f /etc/timezone ]; then + _host_timezone=$(cat /etc/timezone 2>/dev/null || echo "") + fi + # Map Etc/* timezones to "host" (pct doesn't accept Etc/* zones) + [[ "${_host_timezone:-}" == Etc/* ]] && _host_timezone="host" + local _ct_timezone="${var_timezone:-$_host_timezone}" + [[ "${_ct_timezone:-}" == Etc/* ]] && _ct_timezone="host" # Helper to show current progress show_progress() { @@ -1289,9 +1429,10 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "IPv4 CONFIGURATION" \ --ok-button "Next" --cancel-button "Back" \ - --menu "\nSelect IPv4 Address Assignment:" 14 60 2 \ + --menu "\nSelect IPv4 Address Assignment:" 16 65 3 \ "dhcp" "Automatic (DHCP, recommended)" \ "static" "Static (manual entry)" \ + "range" "IP Range Scan (find first free IP)" \ 3>&1 1>&2 2>&3); then if [[ "$result" == "static" ]]; then @@ -1322,6 +1463,42 @@ advanced_settings() { whiptail --msgbox "Invalid IPv4 CIDR format.\nExample: 192.168.1.100/24" 8 58 fi fi + elif [[ "$result" == "range" ]]; then + # IP Range Scan + local ip_range + if ip_range=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "IP RANGE SCAN" \ + --ok-button "Scan" --cancel-button "Back" \ + --inputbox "\nEnter IP range to scan for free address\n(e.g. 192.168.1.100/24-192.168.1.200/24)" 12 65 "" \ + 3>&1 1>&2 2>&3); then + if is_ip_range "$ip_range"; then + # Exit whiptail screen temporarily to show scan progress + clear + header_info + echo -e "${INFO}${BOLD}${DGN}Scanning IP range for free address...${CL}\n" + if resolve_ip_from_range "$ip_range"; then + # Get gateway + local gateway_ip + if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "GATEWAY IP" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nFound free IP: $NET_RESOLVED\n\nEnter Gateway IP address" 12 58 "" \ + 3>&1 1>&2 2>&3); then + if [[ "$gateway_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + _net="$NET_RESOLVED" + _gate=",gw=$gateway_ip" + ((STEP++)) + else + whiptail --msgbox "Invalid Gateway IP format." 8 58 + fi + fi + else + whiptail --msgbox "No free IP found in the specified range.\nAll IPs responded to ping." 10 58 + fi + else + whiptail --msgbox "Invalid IP range format.\n\nExample: 192.168.1.100/24-192.168.1.200/24" 10 58 + fi + fi else _net="dhcp" _gate="" @@ -1491,20 +1668,23 @@ advanced_settings() { # STEP 17: SSH Settings # ═══════════════════════════════════════════════════════════════════════════ 17) - configure_ssh_settings + configure_ssh_settings "Step $STEP/$MAX_STEP" # configure_ssh_settings handles its own flow, always advance ((STEP++)) ;; # ═══════════════════════════════════════════════════════════════════════════ - # STEP 18: FUSE & Verbose Mode + # STEP 18: FUSE Support # ═══════════════════════════════════════════════════════════════════════════ 18) + local fuse_default_flag="--defaultno" + [[ "$_enable_fuse" == "yes" || "$_enable_fuse" == "1" ]] && fuse_default_flag="" + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "FUSE SUPPORT" \ --ok-button "Next" --cancel-button "Back" \ - --defaultno \ - --yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc." 12 58; then + $fuse_default_flag \ + --yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc.\n\n(App default: ${var_fuse:-no})" 14 58; then _enable_fuse="yes" else if [ $? -eq 1 ]; then @@ -1514,26 +1694,256 @@ advanced_settings() { continue fi fi + ((STEP++)) + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 19: TUN/TAP Support + # ═══════════════════════════════════════════════════════════════════════════ + 19) + local tun_default_flag="--defaultno" + [[ "$_enable_tun" == "yes" || "$_enable_tun" == "1" ]] && tun_default_flag="" if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "VERBOSE MODE" \ - --defaultno \ - --yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then - _verbose="yes" + --title "TUN/TAP SUPPORT" \ + --ok-button "Next" --cancel-button "Back" \ + $tun_default_flag \ + --yesno "\nEnable TUN/TAP device support?\n\nRequired for: VPN apps (WireGuard, OpenVPN, Tailscale),\nnetwork tunneling, and containerized networking.\n\n(App default: ${var_tun:-no})" 14 62; then + _enable_tun="yes" else - _verbose="no" + if [ $? -eq 1 ]; then + _enable_tun="no" + else + ((STEP--)) + continue + fi fi ((STEP++)) ;; # ═══════════════════════════════════════════════════════════════════════════ - # STEP 19: Confirmation + # STEP 20: Nesting Support # ═══════════════════════════════════════════════════════════════════════════ - 19) + 20) + local nesting_default_flag="" + [[ "$_enable_nesting" == "0" || "$_enable_nesting" == "no" ]] && nesting_default_flag="--defaultno" + + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "NESTING SUPPORT" \ + --ok-button "Next" --cancel-button "Back" \ + $nesting_default_flag \ + --yesno "\nEnable Nesting?\n\nRequired for: Docker, LXC inside LXC, Podman,\nand other containerization tools.\n\n(App default: ${var_nesting:-1})" 14 58; then + _enable_nesting="1" + else + if [ $? -eq 1 ]; then + _enable_nesting="0" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 21: GPU Passthrough + # ═══════════════════════════════════════════════════════════════════════════ + 21) + local gpu_default_flag="--defaultno" + [[ "$_enable_gpu" == "yes" ]] && gpu_default_flag="" + + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "GPU PASSTHROUGH" \ + --ok-button "Next" --cancel-button "Back" \ + $gpu_default_flag \ + --yesno "\nEnable GPU Passthrough?\n\nAutomatically detects and passes through available GPUs\n(Intel/AMD/NVIDIA) for hardware acceleration.\n\nRecommended for: Media servers, AI/ML, Transcoding\n\n(App default: ${var_gpu:-no})" 16 62; then + _enable_gpu="yes" + else + if [ $? -eq 1 ]; then + _enable_gpu="no" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 22: Keyctl Support (Docker/systemd) + # ═══════════════════════════════════════════════════════════════════════════ + 22) + local keyctl_default_flag="--defaultno" + [[ "$_enable_keyctl" == "1" ]] && keyctl_default_flag="" + + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "KEYCTL SUPPORT" \ + --ok-button "Next" --cancel-button "Back" \ + $keyctl_default_flag \ + --yesno "\nEnable Keyctl support?\n\nRequired for: Docker containers, systemd-networkd,\nand kernel keyring operations.\n\nNote: Automatically enabled for unprivileged containers.\n\n(App default: ${var_keyctl:-0})" 16 62; then + _enable_keyctl="1" + else + if [ $? -eq 1 ]; then + _enable_keyctl="0" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 23: APT Cacher Proxy + # ═══════════════════════════════════════════════════════════════════════════ + 23) + local apt_cacher_default_flag="--defaultno" + [[ "$_apt_cacher" == "yes" ]] && apt_cacher_default_flag="" + + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "APT CACHER PROXY" \ + --ok-button "Next" --cancel-button "Back" \ + $apt_cacher_default_flag \ + --yesno "\nUse APT Cacher-NG proxy?\n\nSpeeds up package downloads by caching them locally.\nRequires apt-cacher-ng running on your network.\n\n(App default: ${var_apt_cacher:-no})" 14 62; then + _apt_cacher="yes" + # Ask for IP if enabled + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "APT CACHER IP" \ + --inputbox "\nEnter APT Cacher-NG server IP address:" 10 58 "$_apt_cacher_ip" \ + 3>&1 1>&2 2>&3); then + _apt_cacher_ip="$result" + fi + else + if [ $? -eq 1 ]; then + _apt_cacher="no" + _apt_cacher_ip="" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 24: Container Timezone + # ═══════════════════════════════════════════════════════════════════════════ + 24) + local tz_hint="$_ct_timezone" + [[ -z "$tz_hint" ]] && tz_hint="(empty - will use host timezone)" + + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONTAINER TIMEZONE" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet container timezone.\n\nExamples: Europe/Berlin, America/New_York, Asia/Tokyo\n\nHost timezone: ${_host_timezone:-unknown}\n\nLeave empty to inherit from host." 16 62 "$_ct_timezone" \ + 3>&1 1>&2 2>&3); then + _ct_timezone="$result" + [[ "${_ct_timezone:-}" == Etc/* ]] && _ct_timezone="host" # pct doesn't accept Etc/* zones + ((STEP++)) + else + ((STEP--)) + fi + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 25: Container Protection + # ═══════════════════════════════════════════════════════════════════════════ + 25) + local protect_default_flag="--defaultno" + [[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_default_flag="" + + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONTAINER PROTECTION" \ + --ok-button "Next" --cancel-button "Back" \ + $protect_default_flag \ + --yesno "\nEnable Container Protection?\n\nPrevents accidental deletion of this container.\nYou must disable protection before removing.\n\n(App default: ${var_protection:-no})" 14 62; then + _protect_ct="yes" + else + if [ $? -eq 1 ]; then + _protect_ct="no" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 26: Device Node Creation (mknod) + # ═══════════════════════════════════════════════════════════════════════════ + 26) + local mknod_default_flag="--defaultno" + [[ "$_enable_mknod" == "1" ]] && mknod_default_flag="" + + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "DEVICE NODE CREATION" \ + --ok-button "Next" --cancel-button "Back" \ + $mknod_default_flag \ + --yesno "\nAllow device node creation (mknod)?\n\nRequired for: Creating device files inside container.\nExperimental feature (requires kernel 5.3+).\n\n(App default: ${var_mknod:-0})" 14 62; then + _enable_mknod="1" + else + if [ $? -eq 1 ]; then + _enable_mknod="0" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 27: Mount Filesystems + # ═══════════════════════════════════════════════════════════════════════════ + 27) + local mount_hint="" + [[ -n "$_mount_fs" ]] && mount_hint="$_mount_fs" || mount_hint="(none)" + + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "MOUNT FILESYSTEMS" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nAllow specific filesystem mounts.\n\nComma-separated list: nfs, cifs, fuse, ext4, etc.\nLeave empty for defaults (none).\n\nCurrent: $mount_hint" 14 62 "$_mount_fs" \ + 3>&1 1>&2 2>&3); then + _mount_fs="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 28: Verbose Mode & Confirmation + # ═══════════════════════════════════════════════════════════════════════════ + 28) + local verbose_default_flag="--defaultno" + [[ "$_verbose" == "yes" ]] && verbose_default_flag="" + + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "VERBOSE MODE" \ + $verbose_default_flag \ + --yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then + _verbose="yes" + else + _verbose="no" + fi # Build summary local ct_type_desc="Unprivileged" [[ "$_ct_type" == "0" ]] && ct_type_desc="Privileged" + local nesting_desc="Disabled" + [[ "$_enable_nesting" == "1" ]] && nesting_desc="Enabled" + + local keyctl_desc="Disabled" + [[ "$_enable_keyctl" == "1" ]] && keyctl_desc="Enabled" + + local protect_desc="No" + [[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_desc="Yes" + + local tz_display="${_ct_timezone:-Host TZ}" + local apt_display="${_apt_cacher:-no}" + [[ "$_apt_cacher" == "yes" && -n "$_apt_cacher_ip" ]] && apt_display="$_apt_cacher_ip" + local summary="Container Type: $ct_type_desc Container ID: $_ct_id Hostname: $_hostname @@ -1548,14 +1958,20 @@ Network: IPv4: $_net IPv6: $_ipv6_method -Options: - FUSE: $_enable_fuse +Features: + FUSE: $_enable_fuse | TUN: $_enable_tun + Nesting: $nesting_desc | Keyctl: $keyctl_desc + GPU: $_enable_gpu | Protection: $protect_desc + +Advanced: + Timezone: $tz_display + APT Cacher: $apt_display Verbose: $_verbose" if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "CONFIRM SETTINGS" \ --ok-button "Create LXC" --cancel-button "Back" \ - --yesno "$summary\n\nCreate ${APP} LXC with these settings?" 26 58; then + --yesno "$summary\n\nCreate ${APP} LXC with these settings?" 32 62; then ((STEP++)) else ((STEP--)) @@ -1582,8 +1998,31 @@ Options: IPV6_GATE="$_ipv6_gate" TAGS="$_tags" ENABLE_FUSE="$_enable_fuse" + ENABLE_TUN="$_enable_tun" + ENABLE_GPU="$_enable_gpu" + ENABLE_NESTING="$_enable_nesting" + ENABLE_KEYCTL="$_enable_keyctl" + ENABLE_MKNOD="$_enable_mknod" + ALLOW_MOUNT_FS="$_mount_fs" + PROTECT_CT="$_protect_ct" + CT_TIMEZONE="$_ct_timezone" + APT_CACHER="$_apt_cacher" + APT_CACHER_IP="$_apt_cacher_ip" VERBOSE="$_verbose" + # Update var_* based on user choice (for functions that check these) + var_gpu="$_enable_gpu" + var_fuse="$_enable_fuse" + var_tun="$_enable_tun" + var_nesting="$_enable_nesting" + var_keyctl="$_enable_keyctl" + var_mknod="$_enable_mknod" + var_mount_fs="$_mount_fs" + var_protection="$_protect_ct" + var_timezone="$_ct_timezone" + var_apt_cacher="$_apt_cacher" + var_apt_cacher_ip="$_apt_cacher_ip" + # Format optional values [[ -n "$_mtu" ]] && MTU=",mtu=$_mtu" || MTU="" [[ -n "$_sd" ]] && SD="-searchdomain=$_sd" || SD="" @@ -1600,6 +2039,10 @@ Options: export UDHCPC_FIX export SSH_KEYS_FILE + # Exit alternate screen buffer before showing summary (so output remains visible) + tput rmcup 2>/dev/null || true + trap - RETURN + # Display final summary echo -e "\n${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" @@ -1613,7 +2056,14 @@ Options: echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" echo -e "${NETWORK}${BOLD}${DGN}IPv4: ${BGN}$NET${CL}" echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}$IPV6_METHOD${CL}" - echo -e "${FUSE}${BOLD}${DGN}FUSE Support: ${BGN}$ENABLE_FUSE${CL}" + echo -e "${FUSE}${BOLD}${DGN}FUSE Support: ${BGN}${ENABLE_FUSE:-no}${CL}" + [[ "${ENABLE_TUN:-no}" == "yes" ]] && echo -e "${NETWORK}${BOLD}${DGN}TUN/TAP Support: ${BGN}$ENABLE_TUN${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Nesting: ${BGN}$([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")${CL}" + [[ "${ENABLE_KEYCTL:-0}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Keyctl: ${BGN}Enabled${CL}" + echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}${ENABLE_GPU:-no}${CL}" + [[ "${PROTECT_CT:-no}" == "yes" || "${PROTECT_CT:-no}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Protection: ${BGN}Enabled${CL}" + [[ -n "${CT_TIMEZONE:-}" ]] && echo -e "${INFO}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}" + [[ "$APT_CACHER" == "yes" ]] && echo -e "${INFO}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}" echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}" } @@ -1736,6 +2186,9 @@ echo_default() { 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 [[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]]; then + echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}Enabled${CL}" + fi if [ "$VERBOSE" == "yes" ]; then echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" fi @@ -1756,7 +2209,7 @@ install_script() { shell_check root_check arch_check - #ssh_check Removed in local as we always use SSH + ssh_check maxkeys_check diagnostics_check @@ -1775,6 +2228,7 @@ install_script() { else timezone="UTC" fi + [[ "${timezone:-}" == Etc/* ]] && timezone="host" # pct doesn't accept Etc/* zones # Show APP Header header_info @@ -1859,8 +2313,8 @@ install_script() { header_info echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" METHOD="appdefaults" + load_vars_file "$(get_app_defaults_path)" "yes" # Force override script defaults base_settings - load_vars_file "$(get_app_defaults_path)" echo_default defaults_target="$(get_app_defaults_path)" break @@ -2076,6 +2530,10 @@ ssh_discover_default_files() { } configure_ssh_settings() { + local step_info="${1:-}" + local backtitle="Proxmox VE Helper Scripts" + [[ -n "$step_info" ]] && backtitle="Proxmox VE Helper Scripts [${step_info}]" + SSH_KEYS_FILE="$(mktemp)" : >"$SSH_KEYS_FILE" @@ -2085,14 +2543,14 @@ configure_ssh_settings() { local ssh_key_mode if [[ "$default_key_count" -gt 0 ]]; then - ssh_key_mode=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ + ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \ "Provision SSH keys for root:" 14 72 4 \ "found" "Select from detected keys (${default_key_count})" \ "manual" "Paste a single public key" \ "folder" "Scan another folder (path or glob)" \ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script else - ssh_key_mode=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ + ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \ "No host keys detected; choose manual/none:" 12 72 2 \ "manual" "Paste a single public key" \ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script @@ -2101,7 +2559,7 @@ configure_ssh_settings() { case "$ssh_key_mode" in found) local selection - selection=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \ + selection=$(whiptail --backtitle "$backtitle" --title "SELECT HOST KEYS" \ --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script for tag in $selection; do tag="${tag%\"}" @@ -2112,13 +2570,13 @@ configure_ssh_settings() { done ;; manual) - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + SSH_AUTHORIZED_KEY="$(whiptail --backtitle "$backtitle" \ --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)" [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE" ;; folder) local glob_path - glob_path=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + glob_path=$(whiptail --backtitle "$backtitle" \ --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3) if [[ -n "$glob_path" ]]; then shopt -s nullglob @@ -2128,7 +2586,7 @@ configure_ssh_settings() { ssh_build_choices_from_files "${_scan_files[@]}" if [[ "$COUNT" -gt 0 ]]; then local folder_selection - folder_selection=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \ + folder_selection=$(whiptail --backtitle "$backtitle" --title "SELECT FOLDER KEYS" \ --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script for tag in $folder_selection; do tag="${tag%\"}" @@ -2138,10 +2596,10 @@ configure_ssh_settings() { [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" done else - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60 + whiptail --backtitle "$backtitle" --msgbox "No keys found in: $glob_path" 8 60 fi else - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60 + whiptail --backtitle "$backtitle" --msgbox "Path/glob returned no files." 8 60 fi fi ;; @@ -2155,12 +2613,9 @@ configure_ssh_settings() { printf '\n' >>"$SSH_KEYS_FILE" fi - if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then - SSH="yes" - else - SSH="no" - fi + # Always show SSH access dialog - user should be able to enable SSH even without keys + if (whiptail --backtitle "$backtitle" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then + SSH="yes" else SSH="no" fi @@ -2171,8 +2626,8 @@ configure_ssh_settings() { # # - Entry point of script # - On Proxmox host: calls install_script -# - In silent mode: runs update_script -# - Otherwise: shows update/setting menu +# - In silent mode: runs update_script with automatic cleanup +# - Otherwise: shows update/setting menu and runs update_script with cleanup # ------------------------------------------------------------------------------ start() { source "$(dirname "${BASH_SOURCE[0]}")/tools.func" @@ -2183,6 +2638,7 @@ start() { VERBOSE="no" set_std_mode update_script + cleanup_lxc else CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ "Support/Update functions for ${APP} LXC. Choose an option:" \ @@ -2207,6 +2663,7 @@ start() { ;; esac update_script + cleanup_lxc fi } @@ -2278,15 +2735,23 @@ build_container() { none) ;; esac - # Build FEATURES string - if [ "$CT_TYPE" == "1" ]; then - FEATURES="keyctl=1,nesting=1" - else + # Build FEATURES string based on container type and user choices + FEATURES="" + + # Nesting support (user configurable, default enabled) + if [ "${ENABLE_NESTING:-1}" == "1" ]; then FEATURES="nesting=1" fi + # Keyctl for unprivileged containers (needed for Docker) + if [ "$CT_TYPE" == "1" ]; then + [ -n "$FEATURES" ] && FEATURES="$FEATURES," + FEATURES="${FEATURES}keyctl=1" + fi + if [ "$ENABLE_FUSE" == "yes" ]; then - FEATURES="$FEATURES,fuse=1" + [ -n "$FEATURES" ] && FEATURES="$FEATURES," + FEATURES="${FEATURES}fuse=1" fi # Build PCT_OPTIONS as string for export @@ -2318,6 +2783,8 @@ build_container() { export PCT_OSTYPE="$var_os" export PCT_OSVERSION="$var_version" export PCT_DISK_SIZE="$DISK_SIZE" + export IPV6_METHOD="$IPV6_METHOD" + export ENABLE_GPU="$ENABLE_GPU" # DEV_MODE exports (optional, for debugging) export BUILD_LOG="$BUILD_LOG" @@ -2332,10 +2799,15 @@ build_container() { export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}" # Build PCT_OPTIONS as multi-line string - PCT_OPTIONS_STRING=" -features $FEATURES - -hostname $HN + PCT_OPTIONS_STRING=" -hostname $HN -tags $TAGS" + # Only add -features if FEATURES is not empty + if [ -n "$FEATURES" ]; then + PCT_OPTIONS_STRING=" -features $FEATURES +$PCT_OPTIONS_STRING" + fi + # Add storage if specified if [ -n "$SD" ]; then PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING @@ -2362,10 +2834,12 @@ build_container() { -protection 1" fi - # Timezone flag (if var_timezone was set) + # Timezone (map Etc/* to "host" as pct doesn't accept them) if [ -n "${CT_TIMEZONE:-}" ]; then + local _pct_timezone="$CT_TIMEZONE" + [[ "$_pct_timezone" == Etc/* ]] && _pct_timezone="host" PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - -timezone $CT_TIMEZONE" + -timezone $_pct_timezone" fi # Password (already formatted) @@ -2387,21 +2861,15 @@ build_container() { # GPU/USB PASSTHROUGH CONFIGURATION # ============================================================================ - # List of applications that benefit from GPU acceleration - GPU_APPS=( - "immich" "channels" "emby" "ersatztv" "frigate" - "jellyfin" "plex" "scrypted" "tdarr" "unmanic" - "ollama" "fileflows" "open-webui" "tunarr" "debian" - "handbrake" "sunshine" "moonlight" "kodi" "stremio" - "viseron" - ) - - # Check if app needs GPU + # Check if GPU passthrough is enabled + # Returns true only if var_gpu is explicitly set to "yes" + # Can be set via: + # - Environment variable: var_gpu=yes bash -c "..." + # - CT script default: var_gpu="${var_gpu:-no}" + # - Advanced settings wizard + # - App defaults file: /usr/local/community-scripts/defaults/.vars is_gpu_app() { - local app="${1,,}" - for gpu_app in "${GPU_APPS[@]}"; do - [[ "$app" == "${gpu_app,,}" ]] && return 0 - done + [[ "${var_gpu:-no}" == "yes" ]] && return 0 return 1 } @@ -2441,6 +2909,8 @@ build_container() { if echo "$pci_vga_info" | grep -q "\[10de:"; then msg_custom "🎮" "${GN}" "Detected NVIDIA GPU" + # Simple passthrough - just bind /dev/nvidia* devices if they exist + # Only include character devices (-c), skip directories like /dev/nvidia-caps for d in /dev/nvidia*; do [[ -c "$d" ]] && NVIDIA_DEVICES+=("$d") done @@ -2489,8 +2959,13 @@ EOF # Configure GPU passthrough configure_gpu_passthrough() { - # Skip if not a GPU app and not privileged - if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then + # Skip if: + # GPU passthrough is enabled when var_gpu="yes": + # - Set via environment variable: var_gpu=yes bash -c "..." + # - Set in CT script: var_gpu="${var_gpu:-no}" + # - Enabled in advanced_settings wizard + # - Configured in app defaults file + if ! is_gpu_app "$APP"; then return 0 fi @@ -2708,15 +3183,17 @@ 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" + LANG=${LANG:-en_US.UTF-8} + 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") + tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "UTC") fi + [[ "${tz:-}" == Etc/* ]] && tz="UTC" # Normalize Etc/* to UTC for container setup if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then # Set timezone using symlink (Debian 13+ compatible) @@ -2959,11 +3436,11 @@ fix_gpu_gids() { # For privileged containers: also fix permissions inside container if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- sh -c " + pct exec "$CTID" -- sh -c " if [ -d /dev/dri ]; then for dev in /dev/dri/*; do if [ -e \"\$dev\" ]; then - case \"\$dev\" in + case \"\$dev\" in *renderD*) chgrp ${render_gid} \"\$dev\" 2>/dev/null || true ;; *) chgrp ${video_gid} \"\$dev\" 2>/dev/null || true ;; esac @@ -3228,9 +3705,12 @@ create_lxc_container() { ;; esac - pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE" || exit 213Add a comment on line R3227Add diff commentMarkdown input: edit mode selected.WritePreviewAdd a suggestionHeadingBoldItalicQuoteCodeLinkUnordered listNumbered listTask listMentionReferenceSaved repliesAdd FilesPaste, drop, or click to add filesCancelCommentStart a reviewReturn to code + pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE" || exit 213 msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated" + msg_info "Validating template storage '$TEMPLATE_STORAGE'" + TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1) + if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'" fi @@ -3266,12 +3746,9 @@ create_lxc_container() { msg_info "Searching for template '$TEMPLATE_SEARCH'" - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - mapfile -t LOCAL_TEMPLATES < <( pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | + awk -v search="${TEMPLATE_SEARCH}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | sed 's|.*/||' | sort -t - -k 2 -V ) @@ -3280,12 +3757,20 @@ create_lxc_container() { msg_ok "Template search completed" set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) + mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "^${TEMPLATE_SEARCH}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) set -u ONLINE_TEMPLATE="" [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" + if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then + count=0 + for idx in "${!ONLINE_TEMPLATES[@]}"; do + ((count++)) + [[ $count -ge 3 ]] && break + done + fi + if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then TEMPLATE="${LOCAL_TEMPLATES[-1]}" TEMPLATE_SOURCE="local" @@ -3321,13 +3806,12 @@ create_lxc_container() { if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" mapfile -t ONLINE_TEMPLATES < <( pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | + awk '{print $2}' | + grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true ) @@ -3388,32 +3872,22 @@ create_lxc_container() { # Retry template search with new version TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" mapfile -t LOCAL_TEMPLATES < <( pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | + awk -v search="${TEMPLATE_SEARCH}-" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | sed 's|.*/||' | sort -t - -k 2 -V ) mapfile -t ONLINE_TEMPLATES < <( pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | + awk '{print $2}' | + grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true ) ONLINE_TEMPLATE="" [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - ((count++)) - [[ $count -ge 3 ]] && break - done - ONLINE_TEMPLATE="${ONLINE_TEMPLATES[$idx]}" - fi - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then TEMPLATE="${LOCAL_TEMPLATES[-1]}" TEMPLATE_SOURCE="local" @@ -3522,7 +3996,6 @@ create_lxc_container() { if [[ "$PCT_OSTYPE" == "debian" ]]; then OSVER="$(parse_template_osver "$TEMPLATE")" if [[ -n "$OSVER" ]]; then - # Proactive, but without abort – only offer offer_lxc_stack_upgrade_and_maybe_retry "no" || true fi fi @@ -3600,7 +4073,7 @@ create_lxc_container() { 0) : ;; # success - container created, continue 2) echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" + apt update && apt install --only-upgrade pve-container lxc-pve" exit 231 ;; 3) @@ -3632,12 +4105,12 @@ create_lxc_container() { 0) : ;; # success - container created, continue 2) echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 213 + apt update && apt install --only-upgrade pve-container lxc-pve" + exit 231 ;; 3) echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 213 + exit 231 ;; esac else @@ -3757,4 +4230,4 @@ if command -v pveversion >/dev/null 2>&1; then 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 \ No newline at end of file +trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM diff --git a/scripts/core/cloud-init.func b/scripts/core/cloud-init.func index ea95d9a..63bd2a2 100644 --- a/scripts/core/cloud-init.func +++ b/scripts/core/cloud-init.func @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright (c) 2021-2025 community-scripts ORG +# Copyright (c) 2021-2026 community-scripts ORG # Author: community-scripts ORG # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE # Revision: 1 @@ -502,4 +502,4 @@ if validate_ip_cidr "192.168.1.100/24"; then echo "Valid IP/CIDR" fi -EXAMPLES \ No newline at end of file +EXAMPLES diff --git a/scripts/core/core.func b/scripts/core/core.func index 4adc842..a241c1a 100644 --- a/scripts/core/core.func +++ b/scripts/core/core.func @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright (c) 2021-2025 community-scripts ORG +# Copyright (c) 2021-2026 community-scripts ORG # License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE # ============================================================================== @@ -123,6 +123,7 @@ icons() { CREATING="${TAB}🚀${TAB}${CL}" ADVANCED="${TAB}🧩${TAB}${CL}" FUSE="${TAB}🗂️${TAB}${CL}" + GPU="${TAB}🎮${TAB}${CL}" HOURGLASS="${TAB}⏳${TAB}" } @@ -808,18 +809,15 @@ cleanup_lxc() { find /tmp /var/tmp -type f -name 'tmp*' -delete 2>/dev/null || true find /tmp /var/tmp -type f -name 'tempfile*' -delete 2>/dev/null || true - # Truncate writable log files silently (permission errors ignored) - if command -v truncate >/dev/null 2>&1; then - find /var/log -type f -writable -print0 2>/dev/null | - xargs -0 -n1 truncate -s 0 2>/dev/null || true + # Node.js npm - directly remove cache directory + # npm cache clean/verify can fail with ENOTEMPTY errors, so we skip them + if command -v npm &>/dev/null; then + rm -rf /root/.npm/_cacache /root/.npm/_logs 2>/dev/null || true fi - - # Node.js npm - if command -v npm &>/dev/null; then $STD npm cache clean --force || true; fi # Node.js yarn - if command -v yarn &>/dev/null; then $STD yarn cache clean || true; fi + if command -v yarn &>/dev/null; then yarn cache clean &>/dev/null || true; fi # Node.js pnpm - if command -v pnpm &>/dev/null; then $STD pnpm store prune || true; fi + if command -v pnpm &>/dev/null; then pnpm store prune &>/dev/null || true; fi # Go if command -v go &>/dev/null; then $STD go clean -cache -modcache || true; fi # Rust cargo @@ -827,11 +825,8 @@ cleanup_lxc() { # Ruby gem if command -v gem &>/dev/null; then $STD gem cleanup || true; fi # Composer (PHP) - if command -v composer &>/dev/null; then $STD composer clear-cache || true; fi + if command -v composer &>/dev/null; then COMPOSER_ALLOW_SUPERUSER=1 $STD composer clear-cache || true; fi - if command -v journalctl &>/dev/null; then - $STD journalctl --vacuum-time=10m || true - fi msg_ok "Cleaned" } @@ -887,4 +882,4 @@ check_or_create_swap() { # SIGNAL TRAPS # ============================================================================== -trap 'stop_spinner' EXIT INT TERM \ No newline at end of file +trap 'stop_spinner' EXIT INT TERM diff --git a/scripts/core/error-handler.func b/scripts/core/error-handler.func index e227c39..8d19d4d 100644 --- a/scripts/core/error-handler.func +++ b/scripts/core/error-handler.func @@ -2,7 +2,7 @@ # ------------------------------------------------------------------------------ # ERROR HANDLER - ERROR & SIGNAL MANAGEMENT # ------------------------------------------------------------------------------ -# Copyright (c) 2021-2025 community-scripts ORG +# Copyright (c) 2021-2026 community-scripts ORG # Author: MickLesk (CanbiZ) # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE # ------------------------------------------------------------------------------ @@ -34,9 +34,9 @@ # * Node.js/npm errors (243-249, 254) # * Python/pip/uv errors (210-212) # * PostgreSQL errors (231-234) -# * MySQL/MariaDB errors (260-263) -# * MongoDB errors (251-253) -# * Proxmox custom codes (200-209, 213-223, 225) +# * MySQL/MariaDB errors (241-244) +# * MongoDB errors (251-254) +# * Proxmox custom codes (200-231) # - Returns description string for given exit code # ------------------------------------------------------------------------------ explain_exit_code() { @@ -319,4 +319,4 @@ catch_errors() { trap on_exit EXIT trap on_interrupt INT trap on_terminate TERM -} \ No newline at end of file +} diff --git a/scripts/core/install.func b/scripts/core/install.func index bc49170..5392605 100755 --- a/scripts/core/install.func +++ b/scripts/core/install.func @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025 community-scripts ORG +# Copyright (c) 2021-2026 community-scripts ORG # Author: tteck (tteckster) # Co-Author: MickLesk # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE @@ -222,21 +222,12 @@ motd_ssh() { # Set terminal to 256-color mode grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc - # Get OS information (Debian / Ubuntu) - if [ -f "/etc/os-release" ]; then - OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"') - elif [ -f "/etc/debian_version" ]; then - OS_NAME="Debian" - OS_VERSION=$(cat /etc/debian_version) - fi - PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" echo "echo -e \"\"" >"$PROFILE_FILE" echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE" echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE" echo "echo \"\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}\$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')${CL}\"" >>"$PROFILE_FILE" echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE" diff --git a/scripts/core/tools.func b/scripts/core/tools.func index aee7b40..5440eee 100644 --- a/scripts/core/tools.func +++ b/scripts/core/tools.func @@ -72,17 +72,17 @@ stop_all_services() { local service_patterns=("$@") for pattern in "${service_patterns[@]}"; do - # Find all matching services + # Find all matching services (grep || true to handle no matches) + local services + services=$(systemctl list-units --type=service --all 2>/dev/null | + grep -oE "${pattern}[^ ]*\.service" 2>/dev/null | sort -u) || true - systemctl list-units --type=service --all 2>/dev/null | - grep -oE "${pattern}[^ ]*\.service" | - sort -u | + if [[ -n "$services" ]]; then while read -r service; do - $STD systemctl stop "$service" 2>/dev/null || true $STD systemctl disable "$service" 2>/dev/null || true - done - + done <<<"$services" + fi done } @@ -219,7 +219,7 @@ upgrade_packages_with_retry() { if [[ $retry -le $max_retries ]]; then msg_warn "Package upgrade failed, retrying ($retry/$max_retries)..." sleep 2 - # Fix any interrupted dpkg operations before retry + # Fix any interrupted dpkg operations before retry $STD dpkg --configure -a 2>/dev/null || true $STD apt update 2>/dev/null || true fi @@ -334,9 +334,9 @@ remove_old_tool_version() { $STD apt purge -y nodejs npm >/dev/null 2>&1 || true # Clean up npm global modules if command -v npm >/dev/null 2>&1; then - npm list -g 2>/dev/null | grep -oE '^ \S+' | awk '{print $1}' | while read -r module; do + npm list -g 2>/dev/null | grep -oE '^ \S+' | awk '{print $1}' 2>/dev/null | while read -r module; do npm uninstall -g "$module" >/dev/null 2>&1 || true - done + done || true fi cleanup_legacy_install "nodejs" cleanup_tool_keyrings "nodesource" @@ -1167,7 +1167,7 @@ cleanup_orphaned_sources() { # Extract Signed-By path from .sources file local keyring_path - keyring_path=$(grep -E '^Signed-By:' "$sources_file" 2>/dev/null | awk '{print $2}') + keyring_path=$(grep -E '^Signed-By:' "$sources_file" 2>/dev/null | awk '{print $2}' 2>/dev/null || true) # If keyring doesn't exist, remove the .sources file if [[ -n "$keyring_path" ]] && [[ ! -f "$keyring_path" ]]; then @@ -1191,6 +1191,7 @@ ensure_apt_working() { if [[ -f /var/lib/dpkg/lock-frontend ]] || dpkg --audit 2>&1 | grep -q "interrupted"; then $STD dpkg --configure -a 2>/dev/null || true fi + # Clean up orphaned sources first cleanup_orphaned_sources @@ -1221,6 +1222,7 @@ setup_deb822_repo() { local suite="$4" local component="${5:-main}" local architectures="${6-}" # optional + local enabled="${7-}" # optional: "true" or "false" # Validate required parameters if [[ -z "$name" || -z "$gpg_url" || -z "$repo_url" || -z "$suite" ]]; then @@ -1248,9 +1250,13 @@ setup_deb822_repo() { echo "Types: deb" echo "URIs: $repo_url" echo "Suites: $suite" - echo "Components: $component" + # Flat repositories (suite="./" or absolute path) must not have Components + if [[ "$suite" != "./" && -n "$component" ]]; then + echo "Components: $component" + fi [[ -n "$architectures" ]] && echo "Architectures: $architectures" echo "Signed-By: /etc/apt/keyrings/${name}.gpg" + [[ -n "$enabled" ]] && echo "Enabled: $enabled" } >/etc/apt/sources.list.d/${name}.sources $STD apt update @@ -1452,15 +1458,32 @@ check_for_gh_release() { ensure_dependencies jq - # Fetch releases and exclude drafts/prereleases - local releases_json - releases_json=$(curl -fsSL --max-time 20 \ - -H 'Accept: application/vnd.github+json' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "https://api.github.com/repos/${source}/releases") || { - msg_error "Unable to fetch releases for ${app}" - return 1 - } + # Try /latest endpoint for non-pinned versions (most efficient) + local releases_json="" + + if [[ -z "$pinned_version_in" ]]; then + releases_json=$(curl -fsSL --max-time 20 \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "https://api.github.com/repos/${source}/releases/latest" 2>/dev/null) + + if [[ $? -eq 0 ]] && [[ -n "$releases_json" ]]; then + # Wrap single release in array for consistent processing + releases_json="[$releases_json]" + fi + fi + + # If no releases yet (pinned version OR /latest failed), fetch up to 100 + if [[ -z "$releases_json" ]]; then + # Fetch releases and exclude drafts/prereleases + releases_json=$(curl -fsSL --max-time 20 \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "https://api.github.com/repos/${source}/releases?per_page=100") || { + msg_error "Unable to fetch releases for ${app}" + return 1 + } + fi mapfile -t raw_tags < <(jq -r '.[] | select(.draft==false and .prerelease==false) | .tag_name' <<<"$releases_json") if ((${#raw_tags[@]} == 0)); then @@ -1734,12 +1757,13 @@ function fetch_and_deploy_gh_release() { ### Tarball Mode ### if [[ "$mode" == "tarball" || "$mode" == "source" ]]; then - url=$(echo "$json" | jq -r '.tarball_url // empty') - [[ -z "$url" ]] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz" + # GitHub API's tarball_url/zipball_url can return HTTP 300 Multiple Choices + # when a branch and tag share the same name. Use explicit refs/tags/ URL instead. + local direct_tarball_url="https://github.com/$repo/archive/refs/tags/$tag_name.tar.gz" filename="${app_lc}-${version}.tar.gz" - curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url" || { - msg_error "Download failed: $url" + curl $download_timeout -fsSL -o "$tmpdir/$filename" "$direct_tarball_url" || { + msg_error "Download failed: $direct_tarball_url" rm -rf "$tmpdir" return 1 } @@ -2049,7 +2073,7 @@ function setup_adminer() { return 1 } local VERSION - VERSION=$(dpkg -s adminer 2>/dev/null | grep '^Version:' | awk '{print $2}') + VERSION=$(dpkg -s adminer 2>/dev/null | grep '^Version:' | awk '{print $2}' 2>/dev/null || echo 'unknown') cache_installed_version "adminer" "${VERSION:-unknown}" msg_ok "Setup Adminer (Debian/Ubuntu)" fi @@ -2539,121 +2563,710 @@ function setup_gs() { # Sets up Hardware Acceleration on debian or ubuntu. # # Description: -# - Determites CPU/GPU/APU Vendor -# - Installs the correct libraries and packages -# - Sets up Hardware Acceleration +# - Detects all available GPUs (Intel, AMD, NVIDIA) +# - Allows user to select which GPU(s) to configure (with 60s timeout) +# - Installs the correct libraries and packages for each GPU type +# - Supports: Debian 11/12/13, Ubuntu 22.04/24.04 +# - Intel: Legacy (Gen 6-8), Modern (Gen 9+), Arc +# - AMD: Discrete GPUs, APUs, ROCm compute +# - NVIDIA: Version-matched drivers from CUDA repository # # Notes: -# - Some things are fetched from intel repositories due to not being in debian repositories. +# - Some Intel packages are fetched from GitHub due to missing Debian packages +# - NVIDIA requires matching host driver version # ------------------------------------------------------------------------------ function setup_hwaccel() { + # Check if user explicitly disabled GPU in advanced settings + # ENABLE_GPU is exported from build.func + if [[ "${ENABLE_GPU:-no}" == "no" ]]; then + return 0 + fi + + # Check if GPU passthrough is enabled (device nodes must exist) + if [[ ! -d /dev/dri && ! -e /dev/nvidia0 && ! -e /dev/kfd ]]; then + msg_warn "No GPU passthrough detected (/dev/dri, /dev/nvidia*, /dev/kfd not found) - skipping hardware acceleration setup" + return 0 + fi + msg_info "Setup Hardware Acceleration" + # Install pciutils if needed if ! command -v lspci &>/dev/null; then $STD apt -y update || { - msg_error "Failed to update package list" - return 1 + msg_warn "Failed to update package list" + return 0 } $STD apt -y install pciutils || { - msg_error "Failed to install pciutils" - return 1 + msg_warn "Failed to install pciutils" + return 0 } fi - # Detect GPU vendor (Intel, AMD, NVIDIA) - local gpu_vendor - gpu_vendor=$(lspci 2>/dev/null | grep -Ei 'vga|3d|display' | grep -Eo 'Intel|AMD|NVIDIA' | head -n1 || echo "") + # ═══════════════════════════════════════════════════════════════════════════ + # GPU Detection - Build list of all available GPUs with details + # ═══════════════════════════════════════════════════════════════════════════ + local -a GPU_LIST=() + local -a GPU_TYPES=() + local -a GPU_NAMES=() + local gpu_count=0 - # Detect CPU vendor (relevant for AMD APUs) + # Get all GPU entries from lspci + while IFS= read -r line; do + [[ -z "$line" ]] && continue + local pci_addr gpu_name gpu_type="" + + pci_addr=$(echo "$line" | awk '{print $1}') + gpu_name=$(echo "$line" | sed 's/^[^ ]* [^:]*: //') + + # Determine GPU type + # Note: Use -w (word boundary) for ATI to avoid matching "CorporATIon" + if echo "$gpu_name" | grep -qi 'Intel'; then + gpu_type="INTEL" + # Subtype detection for Intel + # Order matters: Check Arc first, then Gen9+ (UHD/Iris/HD 5xx-6xx), then Legacy (HD 2xxx-5xxx) + # HD Graphics 530/630 = Gen 9 (Skylake/Kaby Lake) - 3 digits + # HD Graphics 4600/5500 = Gen 7-8 (Haswell/Broadwell) - 4 digits starting with 2-5 + if echo "$gpu_name" | grep -qiE 'Arc|DG[12]'; then + gpu_type="INTEL_ARC" + elif echo "$gpu_name" | grep -qiE 'UHD|Iris|HD Graphics [5-6][0-9]{2}[^0-9]|HD Graphics [5-6][0-9]{2}$'; then + # HD Graphics 5xx/6xx (3 digits) = Gen 9+ (Skylake onwards) + gpu_type="INTEL_GEN9+" + elif echo "$gpu_name" | grep -qiE 'HD Graphics [2-5][0-9]{3}'; then + # HD Graphics 2xxx-5xxx (4 digits) = Gen 6-8 Legacy + gpu_type="INTEL_LEGACY" + fi + elif echo "$gpu_name" | grep -qiwE 'AMD|ATI|Radeon|Advanced Micro Devices'; then + gpu_type="AMD" + elif echo "$gpu_name" | grep -qi 'NVIDIA'; then + gpu_type="NVIDIA" + fi + + if [[ -n "$gpu_type" ]]; then + GPU_LIST+=("$pci_addr") + GPU_TYPES+=("$gpu_type") + GPU_NAMES+=("$gpu_name") + ((gpu_count++)) || true + fi + done < <(lspci 2>/dev/null | grep -Ei 'vga|3d|display') + + # Check for AMD APU via CPU vendor if no discrete GPU found local cpu_vendor - cpu_vendor=$(lscpu 2>/dev/null | grep -i 'Vendor ID' | awk '{print $3}' || echo "") + cpu_vendor=$(lscpu 2>/dev/null | grep -i 'Vendor ID' | awk '{print $3}' 2>/dev/null || echo "") - if [[ -z "$gpu_vendor" && -z "$cpu_vendor" ]]; then - msg_error "No GPU or CPU vendor detected (missing lspci/lscpu output)" - return 1 + if [[ $gpu_count -eq 0 ]]; then + if [[ "$cpu_vendor" == "AuthenticAMD" ]]; then + GPU_LIST+=("integrated") + GPU_TYPES+=("AMD_APU") + GPU_NAMES+=("AMD APU (Integrated Graphics)") + ((gpu_count++)) || true + else + msg_warn "No GPU detected - skipping hardware acceleration setup" + return 0 + fi fi - # Detect OS with fallbacks - local os_id os_codename - os_id=$(grep -oP '(?<=^ID=).+' /etc/os-release 2>/dev/null | tr -d '"' || grep '^ID=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "debian") - os_codename=$(grep -oP '(?<=^VERSION_CODENAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || grep '^VERSION_CODENAME=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "unknown") + # ═══════════════════════════════════════════════════════════════════════════ + # GPU Selection - Let user choose which GPU(s) to configure + # ═══════════════════════════════════════════════════════════════════════════ + local -a SELECTED_INDICES=() - # Validate os_id - if [[ -z "$os_id" ]]; then - os_id="debian" + if [[ $gpu_count -eq 1 ]]; then + # Single GPU - auto-select + SELECTED_INDICES=(0) + msg_ok "Detected GPU: ${GPU_NAMES[0]} (${GPU_TYPES[0]})" + else + # Multiple GPUs - show selection menu + echo "" + msg_info "Multiple GPUs detected:" + echo "" + for i in "${!GPU_LIST[@]}"; do + local type_display="${GPU_TYPES[$i]}" + case "${GPU_TYPES[$i]}" in + INTEL_ARC) type_display="Intel Arc" ;; + INTEL_GEN9+) type_display="Intel Gen9+" ;; + INTEL_LEGACY) type_display="Intel Legacy" ;; + INTEL) type_display="Intel" ;; + AMD) type_display="AMD" ;; + AMD_APU) type_display="AMD APU" ;; + NVIDIA) type_display="NVIDIA" ;; + esac + printf " %d) [%s] %s\n" "$((i + 1))" "$type_display" "${GPU_NAMES[$i]}" + done + printf " A) Configure ALL GPUs\n" + echo "" + + # Read with 60 second timeout + local selection="" + echo -n "Select GPU(s) to configure (1-${gpu_count}, A=all) [timeout 60s, default=all]: " + if read -r -t 60 selection; then + selection="${selection^^}" # uppercase + else + echo "" + msg_info "Timeout - configuring all GPUs automatically" + selection="A" + fi + + # Parse selection + if [[ "$selection" == "A" || -z "$selection" ]]; then + # Select all + for i in "${!GPU_LIST[@]}"; do + SELECTED_INDICES+=("$i") + done + elif [[ "$selection" =~ ^[0-9,]+$ ]]; then + # Parse comma-separated numbers + IFS=',' read -ra nums <<<"$selection" + for num in "${nums[@]}"; do + num=$(echo "$num" | tr -d ' ') + if [[ "$num" =~ ^[0-9]+$ ]] && ((num >= 1 && num <= gpu_count)); then + SELECTED_INDICES+=("$((num - 1))") + fi + done + else + # Invalid - default to all + msg_warn "Invalid selection - configuring all GPUs" + for i in "${!GPU_LIST[@]}"; do + SELECTED_INDICES+=("$i") + done + fi fi - # Determine if we are on a VM or LXC + # ═══════════════════════════════════════════════════════════════════════════ + # OS Detection + # ═══════════════════════════════════════════════════════════════════════════ + local os_id os_codename os_version + os_id=$(grep -oP '(?<=^ID=).+' /etc/os-release 2>/dev/null | tr -d '"' || echo "debian") + os_codename=$(grep -oP '(?<=^VERSION_CODENAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || echo "unknown") + os_version=$(grep -oP '(?<=^VERSION_ID=).+' /etc/os-release 2>/dev/null | tr -d '"' || echo "") + [[ -z "$os_id" ]] && os_id="debian" + local in_ct="${CTTYPE:-0}" - case "$gpu_vendor" in - Intel) - if [[ "$os_id" == "ubuntu" ]]; then - $STD apt -y install intel-opencl-icd || { - msg_error "Failed to install intel-opencl-icd" - return 1 - } - else - # For Debian: fetch Intel GPU drivers from GitHub - fetch_and_deploy_gh_release "" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-core-2_*_amd64.deb" || { - msg_warn "Failed to deploy Intel IGC core 2" - } - fetch_and_deploy_gh_release "" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-opencl-2_*_amd64.deb" || { - msg_warn "Failed to deploy Intel IGC OpenCL 2" - } - fetch_and_deploy_gh_release "" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb" || { - msg_warn "Failed to deploy Intel GDGMM12" - } - fetch_and_deploy_gh_release "" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb" || { - msg_warn "Failed to deploy Intel OpenCL ICD" - } - fi + # ═══════════════════════════════════════════════════════════════════════════ + # Process Selected GPUs + # ═══════════════════════════════════════════════════════════════════════════ + for idx in "${SELECTED_INDICES[@]}"; do + local gpu_type="${GPU_TYPES[$idx]}" + local gpu_name="${GPU_NAMES[$idx]}" - $STD apt -y install va-driver-all ocl-icd-libopencl1 vainfo intel-gpu-tools || { - msg_error "Failed to install Intel GPU dependencies" - return 1 - } - ;; - AMD) - $STD apt -y install mesa-va-drivers mesa-vdpau-drivers mesa-opencl-icd vainfo clinfo || { - msg_error "Failed to install AMD GPU dependencies" - return 1 - } + msg_info "Configuring: ${gpu_name}" - # For AMD CPUs without discrete GPU (APUs) - if [[ "$cpu_vendor" == "AuthenticAMD" && -n "$gpu_vendor" ]]; then - $STD apt -y install libdrm-amdgpu1 firmware-amd-graphics || true - fi - ;; - NVIDIA) - # NVIDIA needs manual driver setup - skip for now - msg_info "NVIDIA GPU detected - manual driver setup required" - ;; - *) - # If no discrete GPU, but AMD CPU (e.g., Ryzen APU) - if [[ "$cpu_vendor" == "AuthenticAMD" ]]; then - $STD apt -y install mesa-opencl-icd ocl-icd-libopencl1 clinfo || { - msg_error "Failed to install Mesa OpenCL stack" - return 1 - } - else - msg_warn "No supported GPU vendor detected - skipping GPU acceleration" - fi - ;; - esac + case "$gpu_type" in + # ───────────────────────────────────────────────────────────────────────── + # Intel Arc GPUs (DG1, DG2, Arc A-series) + # ───────────────────────────────────────────────────────────────────────── + INTEL_ARC) + _setup_intel_arc "$os_id" "$os_codename" + ;; - if [[ "$in_ct" == "0" ]]; then - chgrp video /dev/dri 2>/dev/null || true - chmod 755 /dev/dri 2>/dev/null || true - chmod 660 /dev/dri/* 2>/dev/null || true - $STD adduser "$(id -u -n)" video - $STD adduser "$(id -u -n)" render - fi + # ───────────────────────────────────────────────────────────────────────── + # Intel Gen 9+ (Skylake 2015+: UHD, Iris, HD 6xx+) + # ───────────────────────────────────────────────────────────────────────── + INTEL_GEN9+ | INTEL) + _setup_intel_modern "$os_id" "$os_codename" + ;; + + # ───────────────────────────────────────────────────────────────────────── + # Intel Legacy (Gen 6-8: HD 2000-5999, Sandy Bridge to Broadwell) + # ───────────────────────────────────────────────────────────────────────── + INTEL_LEGACY) + _setup_intel_legacy "$os_id" "$os_codename" + ;; + + # ───────────────────────────────────────────────────────────────────────── + # AMD Discrete GPUs + # ───────────────────────────────────────────────────────────────────────── + AMD) + _setup_amd_gpu "$os_id" "$os_codename" + ;; + + # ───────────────────────────────────────────────────────────────────────── + # AMD APU (Integrated Graphics) + # ───────────────────────────────────────────────────────────────────────── + AMD_APU) + _setup_amd_apu "$os_id" "$os_codename" + ;; + + # ───────────────────────────────────────────────────────────────────────── + # NVIDIA GPUs + # ───────────────────────────────────────────────────────────────────────── + NVIDIA) + _setup_nvidia_gpu "$os_id" "$os_codename" "$os_version" + ;; + esac + done + + # ═══════════════════════════════════════════════════════════════════════════ + # Device Permissions + # ═══════════════════════════════════════════════════════════════════════════ + _setup_gpu_permissions "$in_ct" cache_installed_version "hwaccel" "1.0" msg_ok "Setup Hardware Acceleration" } +# ══════════════════════════════════════════════════════════════════════════════ +# Intel Arc GPU Setup +# ══════════════════════════════════════════════════════════════════════════════ +_setup_intel_arc() { + local os_id="$1" os_codename="$2" + + msg_info "Installing Intel Arc GPU drivers" + + if [[ "$os_id" == "ubuntu" ]]; then + # Ubuntu 22.04+ has Arc support in HWE kernel + $STD apt -y install \ + intel-media-va-driver-non-free \ + intel-opencl-icd \ + vainfo \ + intel-gpu-tools 2>/dev/null || msg_warn "Some Intel Arc packages failed" + + elif [[ "$os_id" == "debian" ]]; then + # Add non-free repos + _add_debian_nonfree "$os_codename" + + # Arc requires latest drivers - fetch from GitHub + # Order matters: libigdgmm first (dependency), then IGC, then compute-runtime + msg_info "Fetching Intel compute-runtime for Arc support" + + # libigdgmm - bundled in compute-runtime releases (Debian version often too old) + fetch_and_deploy_gh_release "libigdgmm12" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb" || true + + # Intel Graphics Compiler (note: packages have -2 suffix) + fetch_and_deploy_gh_release "intel-igc-core" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-core-2_*_amd64.deb" || true + fetch_and_deploy_gh_release "intel-igc-opencl" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-opencl-2_*_amd64.deb" || true + + # Compute Runtime (depends on IGC and gmmlib) + fetch_and_deploy_gh_release "intel-opencl-icd" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb" || true + fetch_and_deploy_gh_release "intel-level-zero-gpu" "intel/compute-runtime" "binary" "latest" "" "libze-intel-gpu1_*_amd64.deb" || true + + $STD apt -y install \ + intel-media-va-driver-non-free \ + ocl-icd-libopencl1 \ + libvpl2 \ + vainfo \ + intel-gpu-tools 2>/dev/null || msg_warn "Some Intel Arc packages failed" + fi + + msg_ok "Intel Arc GPU configured" +} + +# ══════════════════════════════════════════════════════════════════════════════ +# Intel Modern GPU Setup (Gen 9+) +# ══════════════════════════════════════════════════════════════════════════════ +_setup_intel_modern() { + local os_id="$1" os_codename="$2" + + msg_info "Installing Intel Gen 9+ GPU drivers" + + if [[ "$os_id" == "ubuntu" ]]; then + $STD apt -y install \ + va-driver-all \ + intel-media-va-driver \ + ocl-icd-libopencl1 \ + vainfo \ + intel-gpu-tools 2>/dev/null || msg_warn "Some Intel packages failed" + + # Try non-free driver for better codec support + $STD apt -y install intel-media-va-driver-non-free 2>/dev/null || true + $STD apt -y install intel-opencl-icd 2>/dev/null || true + $STD apt -y install libmfx-gen1.2 2>/dev/null || true + + elif [[ "$os_id" == "debian" ]]; then + _add_debian_nonfree "$os_codename" + + # For Trixie/Sid: Fetch from GitHub (Debian packages too old or missing) + if [[ "$os_codename" == "trixie" || "$os_codename" == "sid" ]]; then + msg_info "Fetching Intel compute-runtime from GitHub" + + # libigdgmm first (bundled in compute-runtime releases) + fetch_and_deploy_gh_release "libigdgmm12" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb" || true + + # Intel Graphics Compiler (note: packages have -2 suffix) + fetch_and_deploy_gh_release "intel-igc-core" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-core-2_*_amd64.deb" || true + fetch_and_deploy_gh_release "intel-igc-opencl" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-opencl-2_*_amd64.deb" || true + + # Compute Runtime + fetch_and_deploy_gh_release "intel-opencl-icd" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb" || true + fi + + $STD apt -y install \ + intel-media-va-driver-non-free \ + ocl-icd-libopencl1 \ + vainfo \ + libmfx-gen1.2 \ + intel-gpu-tools 2>/dev/null || msg_warn "Some Intel packages failed" + + # Bookworm has intel-opencl-icd in repos (compatible version) + [[ "$os_codename" == "bookworm" ]] && $STD apt -y install intel-opencl-icd libigdgmm12 2>/dev/null || true + fi + + msg_ok "Intel Gen 9+ GPU configured" +} + +# ══════════════════════════════════════════════════════════════════════════════ +# Intel Legacy GPU Setup (Gen 6-8) +# ══════════════════════════════════════════════════════════════════════════════ +_setup_intel_legacy() { + local os_id="$1" os_codename="$2" + + msg_info "Installing Intel Legacy GPU drivers (Gen 6-8)" + + # Legacy GPUs use i965 driver - stable repo packages only + $STD apt -y install \ + va-driver-all \ + i965-va-driver \ + mesa-va-drivers \ + ocl-icd-libopencl1 \ + vainfo \ + intel-gpu-tools 2>/dev/null || msg_warn "Some Intel legacy packages failed" + + # beignet provides OpenCL for older Intel GPUs (if available) + $STD apt -y install beignet-opencl-icd 2>/dev/null || true + + msg_ok "Intel Legacy GPU configured" +} + +# ══════════════════════════════════════════════════════════════════════════════ +# AMD Discrete GPU Setup +# ══════════════════════════════════════════════════════════════════════════════ +_setup_amd_gpu() { + local os_id="$1" os_codename="$2" + + msg_info "Installing AMD GPU drivers" + + # Core Mesa drivers + $STD apt -y install \ + mesa-va-drivers \ + mesa-vdpau-drivers \ + mesa-opencl-icd \ + ocl-icd-libopencl1 \ + libdrm-amdgpu1 \ + vainfo \ + clinfo 2>/dev/null || msg_warn "Some AMD packages failed" + + # Firmware for AMD GPUs + if [[ "$os_id" == "debian" ]]; then + _add_debian_nonfree_firmware "$os_codename" + $STD apt -y install firmware-amd-graphics 2>/dev/null || msg_warn "AMD firmware not available" + fi + # Ubuntu includes AMD firmware in linux-firmware by default + + # ROCm for compute (optional - large download) + # Uncomment if needed: + # $STD apt -y install rocm-opencl-runtime 2>/dev/null || true + + msg_ok "AMD GPU configured" +} + +# ══════════════════════════════════════════════════════════════════════════════ +# AMD APU Setup (Integrated Graphics) +# ══════════════════════════════════════════════════════════════════════════════ +_setup_amd_apu() { + local os_id="$1" os_codename="$2" + + msg_info "Installing AMD APU drivers" + + $STD apt -y install \ + mesa-va-drivers \ + mesa-vdpau-drivers \ + mesa-opencl-icd \ + ocl-icd-libopencl1 \ + vainfo 2>/dev/null || msg_warn "Some AMD APU packages failed" + + if [[ "$os_id" == "debian" ]]; then + _add_debian_nonfree_firmware "$os_codename" + $STD apt -y install firmware-amd-graphics 2>/dev/null || true + fi + + msg_ok "AMD APU configured" +} + +# ══════════════════════════════════════════════════════════════════════════════ +# NVIDIA GPU Setup +# ══════════════════════════════════════════════════════════════════════════════ +_setup_nvidia_gpu() { + local os_id="$1" os_codename="$2" os_version="$3" + + msg_info "Installing NVIDIA GPU drivers" + + # Detect host driver version (passed through via /proc) + local nvidia_host_version="" + if [[ -f /proc/driver/nvidia/version ]]; then + nvidia_host_version=$(grep "NVRM version:" /proc/driver/nvidia/version 2>/dev/null | awk '{print $8}') + fi + + if [[ -z "$nvidia_host_version" ]]; then + msg_warn "NVIDIA host driver version not found in /proc/driver/nvidia/version" + msg_warn "Ensure NVIDIA drivers are installed on host and GPU passthrough is enabled" + $STD apt -y install va-driver-all vainfo 2>/dev/null || true + return 0 + fi + + msg_info "Host NVIDIA driver version: ${nvidia_host_version}" + + if [[ "$os_id" == "debian" ]]; then + # Enable non-free components + if [[ -f /etc/apt/sources.list.d/debian.sources ]]; then + if ! grep -q "non-free" /etc/apt/sources.list.d/debian.sources 2>/dev/null; then + sed -i -E 's/Components: (.*)$/Components: \1 contrib non-free non-free-firmware/g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || true + fi + fi + + # Determine CUDA repository + local cuda_repo="debian12" + case "$os_codename" in + bullseye) cuda_repo="debian11" ;; + bookworm) cuda_repo="debian12" ;; + trixie | sid) cuda_repo="debian12" ;; # Forward compatible + esac + + # Add NVIDIA CUDA repository + if [[ ! -f /usr/share/keyrings/cuda-archive-keyring.gpg ]]; then + msg_info "Adding NVIDIA CUDA repository (${cuda_repo})" + local cuda_keyring + cuda_keyring="$(mktemp)" + if curl -fsSL -o "$cuda_keyring" "https://developer.download.nvidia.com/compute/cuda/repos/${cuda_repo}/x86_64/cuda-keyring_1.1-1_all.deb" 2>/dev/null; then + $STD dpkg -i "$cuda_keyring" 2>/dev/null || true + else + msg_warn "Failed to download NVIDIA CUDA keyring" + fi + rm -f "$cuda_keyring" + fi + + # Pin NVIDIA repo for version matching + cat <<'NVIDIA_PIN' >/etc/apt/preferences.d/nvidia-cuda-pin +Package: * +Pin: origin developer.download.nvidia.com +Pin-Priority: 1001 +NVIDIA_PIN + + $STD apt -y update + + # Install version-matched NVIDIA libraries + local nvidia_pkgs="libcuda1=${nvidia_host_version}* libnvcuvid1=${nvidia_host_version}* libnvidia-encode1=${nvidia_host_version}* libnvidia-ml1=${nvidia_host_version}*" + + msg_info "Installing NVIDIA libraries (version ${nvidia_host_version})" + if $STD apt -y install --no-install-recommends $nvidia_pkgs 2>/dev/null; then + msg_ok "Installed version-matched NVIDIA libraries" + else + msg_warn "Version-pinned install failed - trying unpinned" + if $STD apt -y install --no-install-recommends libcuda1 libnvcuvid1 libnvidia-encode1 libnvidia-ml1 2>/dev/null; then + msg_warn "Installed NVIDIA libraries (unpinned) - version mismatch may occur" + else + msg_warn "NVIDIA library installation failed" + fi + fi + + $STD apt -y install --no-install-recommends nvidia-smi 2>/dev/null || true + + elif [[ "$os_id" == "ubuntu" ]]; then + # Ubuntu versioning + local ubuntu_cuda_repo="" + case "$os_version" in + 22.04) ubuntu_cuda_repo="ubuntu2204" ;; + 24.04) ubuntu_cuda_repo="ubuntu2404" ;; + *) ubuntu_cuda_repo="ubuntu2204" ;; # Fallback + esac + + # Add NVIDIA CUDA repository for Ubuntu + if [[ ! -f /usr/share/keyrings/cuda-archive-keyring.gpg ]]; then + msg_info "Adding NVIDIA CUDA repository (${ubuntu_cuda_repo})" + local cuda_keyring + cuda_keyring="$(mktemp)" + if curl -fsSL -o "$cuda_keyring" "https://developer.download.nvidia.com/compute/cuda/repos/${ubuntu_cuda_repo}/x86_64/cuda-keyring_1.1-1_all.deb" 2>/dev/null; then + $STD dpkg -i "$cuda_keyring" 2>/dev/null || true + else + msg_warn "Failed to download NVIDIA CUDA keyring" + fi + rm -f "$cuda_keyring" + fi + + $STD apt -y update + + # Try version-matched install + local nvidia_pkgs="libcuda1=${nvidia_host_version}* libnvcuvid1=${nvidia_host_version}* libnvidia-encode1=${nvidia_host_version}* libnvidia-ml1=${nvidia_host_version}*" + if $STD apt -y install --no-install-recommends $nvidia_pkgs 2>/dev/null; then + msg_ok "Installed version-matched NVIDIA libraries" + else + # Fallback to Ubuntu repo packages + $STD apt -y install --no-install-recommends libnvidia-decode libnvidia-encode nvidia-utils 2>/dev/null || msg_warn "NVIDIA installation failed" + fi + fi + + # VA-API for hybrid setups (Intel + NVIDIA) + $STD apt -y install va-driver-all vainfo 2>/dev/null || true + + msg_ok "NVIDIA GPU configured" +} + +# ══════════════════════════════════════════════════════════════════════════════ +# Helper: Add Debian non-free repositories +# ══════════════════════════════════════════════════════════════════════════════ +_add_debian_nonfree() { + local os_codename="$1" + + [[ -f /etc/apt/sources.list.d/non-free.sources ]] && return 0 + + case "$os_codename" in + bullseye) + cat <<'EOF' >/etc/apt/sources.list.d/non-free.sources +Types: deb +URIs: http://deb.debian.org/debian +Suites: bullseye bullseye-updates +Components: non-free +EOF + ;; + bookworm) + cat <<'EOF' >/etc/apt/sources.list.d/non-free.sources +Types: deb +URIs: http://deb.debian.org/debian +Suites: bookworm bookworm-updates +Components: non-free non-free-firmware +EOF + ;; + trixie | sid) + cat <<'EOF' >/etc/apt/sources.list.d/non-free.sources +Types: deb +URIs: http://deb.debian.org/debian +Suites: trixie trixie-updates +Components: non-free non-free-firmware + +Types: deb +URIs: http://deb.debian.org/debian-security +Suites: trixie-security +Components: non-free non-free-firmware +EOF + ;; + esac + $STD apt -y update +} + +# ══════════════════════════════════════════════════════════════════════════════ +# Helper: Add Debian non-free-firmware repository +# ══════════════════════════════════════════════════════════════════════════════ +_add_debian_nonfree_firmware() { + local os_codename="$1" + + [[ -f /etc/apt/sources.list.d/non-free-firmware.sources ]] && return 0 + + case "$os_codename" in + bullseye) + # Debian 11 uses 'non-free' component (no separate non-free-firmware) + cat <<'EOF' >/etc/apt/sources.list.d/non-free-firmware.sources +Types: deb +URIs: http://deb.debian.org/debian +Suites: bullseye bullseye-updates +Components: non-free + +Types: deb +URIs: http://deb.debian.org/debian-security +Suites: bullseye-security +Components: non-free +EOF + ;; + bookworm) + cat <<'EOF' >/etc/apt/sources.list.d/non-free-firmware.sources +Types: deb +URIs: http://deb.debian.org/debian +Suites: bookworm bookworm-updates +Components: non-free-firmware + +Types: deb +URIs: http://deb.debian.org/debian-security +Suites: bookworm-security +Components: non-free-firmware +EOF + ;; + trixie | sid) + cat <<'EOF' >/etc/apt/sources.list.d/non-free-firmware.sources +Types: deb +URIs: http://deb.debian.org/debian +Suites: trixie trixie-updates +Components: non-free-firmware + +Types: deb +URIs: http://deb.debian.org/debian-security +Suites: trixie-security +Components: non-free-firmware +EOF + ;; + esac + $STD apt -y update +} + +# ══════════════════════════════════════════════════════════════════════════════ +# Helper: Setup GPU device permissions +# ══════════════════════════════════════════════════════════════════════════════ +_setup_gpu_permissions() { + local in_ct="$1" + + # /dev/dri permissions (Intel/AMD) + if [[ "$in_ct" == "0" && -d /dev/dri ]]; then + if ls /dev/dri/card* /dev/dri/renderD* &>/dev/null; then + chgrp video /dev/dri 2>/dev/null || true + chmod 755 /dev/dri 2>/dev/null || true + chmod 660 /dev/dri/* 2>/dev/null || true + $STD adduser "$(id -u -n)" video 2>/dev/null || true + $STD adduser "$(id -u -n)" render 2>/dev/null || true + + # Sync GID with host + local host_video_gid host_render_gid + host_video_gid=$(getent group video | cut -d: -f3) + host_render_gid=$(getent group render | cut -d: -f3) + if [[ -n "$host_video_gid" ]]; then + sed -i "s/^video:x:[0-9]*:/video:x:$host_video_gid:/" /etc/group 2>/dev/null || true + fi + if [[ -n "$host_render_gid" ]]; then + sed -i "s/^render:x:[0-9]*:/render:x:$host_render_gid:/" /etc/group 2>/dev/null || true + fi + + # Verify VA-API + if command -v vainfo &>/dev/null; then + if vainfo &>/dev/null; then + msg_info "VA-API verified and working" + else + msg_warn "vainfo test failed - check GPU passthrough" + fi + fi + fi + fi + + # /dev/nvidia* permissions (NVIDIA) + if ls /dev/nvidia* &>/dev/null 2>&1; then + msg_info "Configuring NVIDIA device permissions" + for nvidia_dev in /dev/nvidia*; do + [[ -e "$nvidia_dev" ]] && { + chgrp video "$nvidia_dev" 2>/dev/null || true + chmod 666 "$nvidia_dev" 2>/dev/null || true + } + done + if [[ -d /dev/nvidia-caps ]]; then + chmod 755 /dev/nvidia-caps 2>/dev/null || true + for caps_dev in /dev/nvidia-caps/*; do + [[ -e "$caps_dev" ]] && { + chgrp video "$caps_dev" 2>/dev/null || true + chmod 666 "$caps_dev" 2>/dev/null || true + } + done + fi + + # Verify nvidia-smi + if command -v nvidia-smi &>/dev/null; then + if nvidia-smi &>/dev/null; then + msg_info "nvidia-smi verified and working" + else + msg_warn "nvidia-smi test failed - check driver version match" + fi + fi + fi + + # /dev/kfd permissions (AMD ROCm) + if [[ -e /dev/kfd ]]; then + chmod 666 /dev/kfd 2>/dev/null || true + msg_info "AMD ROCm compute device configured" + fi +} + # ------------------------------------------------------------------------------ # Installs ImageMagick 7 from source (Debian/Ubuntu only). # @@ -2953,12 +3566,12 @@ setup_mariadb() { # Resolve "latest" to actual version if [[ "$MARIADB_VERSION" == "latest" ]]; then if ! curl -fsI --max-time 10 http://mirror.mariadb.org/repo/ >/dev/null 2>&1; then - msg_warn "MariaDB mirror not reachable - trying mariadb_repo_setup fallback" + msg_warn "MariaDB mirror not reachable - trying mariadb_repo_setup fallback" # Try using official mariadb_repo_setup script as fallback if curl -fsSL --max-time 15 https://r.mariadb.com/downloads/mariadb_repo_setup 2>/dev/null | bash -s -- --skip-verify >/dev/null 2>&1; then msg_ok "MariaDB repository configured via mariadb_repo_setup" # Extract version from configured repo - MARIADB_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.list 2>/dev/null | head -n1 || echo "12.2")Expand commentComment on line R2948ResolvedCode has comments. Press enter to view. + MARIADB_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.list 2>/dev/null | head -n1 || echo "12.2") else msg_warn "mariadb_repo_setup failed - using hardcoded fallback version" MARIADB_VERSION="12.2" @@ -2972,7 +3585,7 @@ setup_mariadb() { head -n1 || echo "") if [[ -z "$MARIADB_VERSION" ]]; then - msg_warn "Could not parse latest GA MariaDB version from mirror - trying mariadb_repo_setup" + msg_warn "Could not parse latest GA MariaDB version from mirror - trying mariadb_repo_setup" if curl -fsSL --max-time 15 https://r.mariadb.com/downloads/mariadb_repo_setup 2>/dev/null | bash -s -- --skip-verify >/dev/null 2>&1; then msg_ok "MariaDB repository configured via mariadb_repo_setup" MARIADB_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.list 2>/dev/null | head -n1 || echo "12.2") @@ -3076,6 +3689,31 @@ setup_mariadb() { } fi + # Configure tmpfiles.d to ensure /run/mysqld directory is created on boot + # This fixes the issue where MariaDB fails to start after container reboot + msg_info "Configuring MariaDB runtime directory persistence" + + # Create tmpfiles.d configuration with error handling + if ! printf '# Ensure /run/mysqld directory exists with correct permissions for MariaDB\nd /run/mysqld 0755 mysql mysql -\n' >/etc/tmpfiles.d/mariadb.conf; then + msg_warn "Failed to create /etc/tmpfiles.d/mariadb.conf - runtime directory may not persist on reboot" + fi + + # Create the directory now if it doesn't exist + # Verify mysql user exists before attempting ownership change + if [[ ! -d /run/mysqld ]]; then + mkdir -p /run/mysqld + # Set permissions first (works regardless of user existence) + chmod 755 /run/mysqld + # Set ownership only if mysql user exists + if getent passwd mysql >/dev/null 2>&1; then + chown mysql:mysql /run/mysqld + else + msg_warn "mysql user not found - directory created with correct permissions but ownership not set" + fi + fi + + msg_ok "Configured MariaDB runtime directory persistence" + cache_installed_version "mariadb" "$MARIADB_VERSION" msg_ok "Setup MariaDB $MARIADB_VERSION" } @@ -3568,19 +4206,19 @@ function setup_nodejs() { # Check if the module is already installed if $STD npm list -g --depth=0 "$MODULE_NAME" 2>&1 | grep -q "$MODULE_NAME@"; then - MODULE_INSTALLED_VERSION="$($STD npm list -g --depth=0 "$MODULE_NAME" 2>&1 | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')" + MODULE_INSTALLED_VERSION="$(npm list -g --depth=0 "$MODULE_NAME" 2>&1 | grep "$MODULE_NAME@" | awk -F@ '{print $2}' 2>/dev/null | tr -d '[:space:]' || echo '')" if [[ "$MODULE_REQ_VERSION" != "latest" && "$MODULE_REQ_VERSION" != "$MODULE_INSTALLED_VERSION" ]]; then msg_info "Updating $MODULE_NAME from v$MODULE_INSTALLED_VERSION to v$MODULE_REQ_VERSION" if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" 2>/dev/null; then msg_warn "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION" - ((failed_modules++)) + ((failed_modules++)) || true continue fi elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then msg_info "Updating $MODULE_NAME to latest version" if ! $STD npm install -g "${MODULE_NAME}@latest" 2>/dev/null; then msg_warn "Failed to update $MODULE_NAME to latest version" - ((failed_modules++)) + ((failed_modules++)) || true continue fi fi @@ -3588,7 +4226,7 @@ function setup_nodejs() { msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION" if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" 2>/dev/null; then msg_warn "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION" - ((failed_modules++)) + ((failed_modules++)) || true continue fi fi @@ -3687,7 +4325,7 @@ EOF # Get available PHP version from repository local AVAILABLE_PHP_VERSION="" - AVAILABLE_PHP_VERSION=$(apt-cache show "php${PHP_VERSION}" 2>/dev/null | grep -m1 "^Version:" | awk '{print $2}' | cut -d- -f1) || true + AVAILABLE_PHP_VERSION=$(apt-cache show "php${PHP_VERSION}" 2>/dev/null | grep -m1 "^Version:" | awk '{print $2}' 2>/dev/null | cut -d- -f1 || true) if [[ -z "$AVAILABLE_PHP_VERSION" ]]; then msg_error "PHP ${PHP_VERSION} not found in configured repositories" @@ -4405,7 +5043,7 @@ function setup_rust() { # Get currently installed version local CURRENT_VERSION="" if command -v rustc &>/dev/null; then - CURRENT_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + CURRENT_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}' 2>/dev/null) || true fi # Scenario 1: Rustup not installed - fresh install @@ -4424,7 +5062,8 @@ function setup_rust() { return 1 fi - local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + local RUST_VERSION + RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}' 2>/dev/null) || true if [[ -z "$RUST_VERSION" ]]; then msg_error "Failed to determine Rust version" return 1 @@ -4455,7 +5094,8 @@ function setup_rust() { # Ensure PATH is updated for current shell session export PATH="$CARGO_BIN:$PATH" - local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + local RUST_VERSION + RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}' 2>/dev/null) || true if [[ -z "$RUST_VERSION" ]]; then msg_error "Failed to determine Rust version after update" return 1 @@ -4487,7 +5127,7 @@ function setup_rust() { # Check if already installed if echo "$CRATE_LIST" | grep -q "^${NAME} "; then - INSTALLED_VER=$(echo "$CRATE_LIST" | grep "^${NAME} " | head -1 | awk '{print $2}' | tr -d 'v:') + INSTALLED_VER=$(echo "$CRATE_LIST" | grep "^${NAME} " | head -1 | awk '{print $2}' 2>/dev/null | tr -d 'v:' || echo '') if [[ -n "$VER" && "$VER" != "$INSTALLED_VER" ]]; then msg_info "Upgrading $NAME from v$INSTALLED_VER to v$VER" @@ -4502,7 +5142,7 @@ function setup_rust() { msg_error "Failed to upgrade $NAME" return 1 } - local NEW_VER=$(cargo install --list 2>/dev/null | grep "^${NAME} " | head -1 | awk '{print $2}' | tr -d 'v:') + local NEW_VER=$(cargo install --list 2>/dev/null | grep "^${NAME} " | head -1 | awk '{print $2}' 2>/dev/null | tr -d 'v:' || echo 'unknown') msg_ok "Upgraded $NAME to v$NEW_VER" else msg_ok "$NAME v$INSTALLED_VER already installed" @@ -4520,7 +5160,7 @@ function setup_rust() { msg_error "Failed to install $NAME" return 1 } - local NEW_VER=$(cargo install --list 2>/dev/null | grep "^${NAME} " | head -1 | awk '{print $2}' | tr -d 'v:') + local NEW_VER=$(cargo install --list 2>/dev/null | grep "^${NAME} " | head -1 | awk '{print $2}' 2>/dev/null | tr -d 'v:' || echo 'unknown') msg_ok "Installed $NAME v$NEW_VER" fi fi @@ -4842,7 +5482,7 @@ function setup_docker() { # Install or upgrade Docker if [ "$docker_installed" = true ]; then msg_info "Checking for Docker updates" - DOCKER_LATEST_VERSION=$(apt-cache policy docker-ce | grep Candidate | awk '{print $2}' | cut -d':' -f2 | cut -d'-' -f1) + DOCKER_LATEST_VERSION=$(apt-cache policy docker-ce | grep Candidate | awk '{print $2}' 2>/dev/null | cut -d':' -f2 | cut -d'-' -f1 || echo '') if [ "$DOCKER_CURRENT_VERSION" != "$DOCKER_LATEST_VERSION" ]; then msg_info "Updating Docker $DOCKER_CURRENT_VERSION → $DOCKER_LATEST_VERSION" @@ -4988,4 +5628,4 @@ EOF fi msg_ok "Docker setup completed" -} \ No newline at end of file +}