diff --git a/misc/build.func b/misc/build.func index 906d64022..5ea15757a 100644 --- a/misc/build.func +++ b/misc/build.func @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG # Author: tteck (tteckster) | MickLesk | michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE +# License: MIT | https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/LICENSE # ============================================================================== # BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION @@ -246,7 +246,7 @@ install_ssh_keys_into_ct() { return 0 fi - # Fallback: nichts ausgewählt + # Fallback msg_warn "No SSH keys to install (skipping)." return 0 } @@ -298,6 +298,53 @@ get_valid_container_id() { echo "$suggested_id" } +# ------------------------------------------------------------------------------ +# validate_container_id() +# +# - Validates if a container ID is available for use +# - Checks if ID is already used by VM or LXC container +# - Checks if ID is used in LVM logical volumes +# - Returns 0 if ID is available, 1 if already in use +# ------------------------------------------------------------------------------ +validate_container_id() { + local ctid="$1" + + # Check if ID is numeric + if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then + return 1 + fi + + # Check if config file exists for VM or LXC + if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then + return 1 + fi + + # Check if ID is used in LVM logical volumes + if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then + return 1 + fi + + return 0 +} + +# ------------------------------------------------------------------------------ +# get_valid_container_id() +# +# - Returns a valid, unused container ID +# - If provided ID is valid, returns it +# - Otherwise increments from suggested ID until a free one is found +# - Calls validate_container_id() to check availability +# ------------------------------------------------------------------------------ +get_valid_container_id() { + local suggested_id="${1:-$(pvesh get /cluster/nextid)}" + + while ! validate_container_id "$suggested_id"; do + suggested_id=$((suggested_id + 1)) + done + + echo "$suggested_id" +} + # ------------------------------------------------------------------------------ # validate_hostname() # @@ -316,7 +363,7 @@ validate_hostname() { # Split by dots and validate each label local IFS='.' - read -ra labels <<<"$hostname" + read -ra labels <<< "$hostname" for label in "${labels[@]}"; do # Each label: 1-63 chars, alphanumeric, hyphens allowed (not at start/end) if [[ -z "$label" ]] || [[ ${#label} -gt 63 ]]; then @@ -420,7 +467,7 @@ validate_ipv6_address() { # Check that no segment exceeds 4 hex chars local IFS=':' local -a segments - read -ra segments <<<"$addr" + read -ra segments <<< "$addr" for seg in "${segments[@]}"; do if [[ ${#seg} -gt 4 ]]; then return 1 @@ -470,14 +517,14 @@ validate_gateway_in_subnet() { # Convert IPs to integers local IFS='.' - read -r i1 i2 i3 i4 <<<"$ip" - read -r g1 g2 g3 g4 <<<"$gateway" + read -r i1 i2 i3 i4 <<< "$ip" + read -r g1 g2 g3 g4 <<< "$gateway" - local ip_int=$(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4)) - local gw_int=$(((g1 << 24) + (g2 << 16) + (g3 << 8) + g4)) + local ip_int=$(( (i1 << 24) + (i2 << 16) + (i3 << 8) + i4 )) + local gw_int=$(( (g1 << 24) + (g2 << 16) + (g3 << 8) + g4 )) # Check if both are in same network - if (((ip_int & mask) != (gw_int & mask))); then + if (( (ip_int & mask) != (gw_int & mask) )); then return 1 fi @@ -849,8 +896,6 @@ base_settings() { CORE_COUNT="${final_cpu}" RAM_SIZE="${final_ram}" VERBOSE=${var_verbose:-"${1:-no}"} - - # Password sanitization - clean up dashes and format properly PW="" if [[ -n "${var_pw:-}" ]]; then local _pw_raw="${var_pw}" @@ -1012,113 +1057,113 @@ load_vars_file() { # Validate values before setting (skip empty values - they use defaults) if [[ -n "$var_val" ]]; then case "$var_key" in - var_mac) - if ! validate_mac_address "$var_val"; then - msg_warn "Invalid MAC address '$var_val' in $file, ignoring" - continue - fi - ;; - var_vlan) - if ! validate_vlan_tag "$var_val"; then - msg_warn "Invalid VLAN tag '$var_val' in $file (must be 1-4094), ignoring" - continue - fi - ;; - var_mtu) - if ! validate_mtu "$var_val"; then - msg_warn "Invalid MTU '$var_val' in $file (must be 576-65535), ignoring" - continue - fi - ;; - var_tags) - if ! validate_tags "$var_val"; then - msg_warn "Invalid tags '$var_val' in $file (alphanumeric, -, _, ; only), ignoring" - continue - fi - ;; - var_timezone) - if ! validate_timezone "$var_val"; then - msg_warn "Invalid timezone '$var_val' in $file, ignoring" - continue - fi - ;; - var_brg) - if ! validate_bridge "$var_val"; then - msg_warn "Bridge '$var_val' not found in $file, ignoring" - continue - fi - ;; - var_gateway) - if ! validate_gateway_ip "$var_val"; then - msg_warn "Invalid gateway IP '$var_val' in $file, ignoring" - continue - fi - ;; - var_hostname) - if ! validate_hostname "$var_val"; then - msg_warn "Invalid hostname '$var_val' in $file, ignoring" - continue - fi - ;; - var_cpu) - if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1 || var_val > 128)); then - msg_warn "Invalid CPU count '$var_val' in $file (must be 1-128), ignoring" - continue - fi - ;; - var_ram) - if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 256)); then - msg_warn "Invalid RAM '$var_val' in $file (must be >= 256 MiB), ignoring" - continue - fi - ;; - var_disk) - if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1)); then - msg_warn "Invalid disk size '$var_val' in $file (must be >= 1 GB), ignoring" - continue - fi - ;; - var_unprivileged) - if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then - msg_warn "Invalid unprivileged value '$var_val' in $file (must be 0 or 1), ignoring" - continue - fi - ;; - var_nesting) - if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then - msg_warn "Invalid nesting value '$var_val' in $file (must be 0 or 1), ignoring" - continue - fi - ;; - var_keyctl) - if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then - msg_warn "Invalid keyctl value '$var_val' in $file (must be 0 or 1), ignoring" - continue - fi - ;; - var_net) - # var_net can be: dhcp, static IP/CIDR, or IP range - if [[ "$var_val" != "dhcp" ]]; then - if is_ip_range "$var_val"; then - : # IP range is valid, will be resolved at runtime - elif ! validate_ip_address "$var_val"; then - msg_warn "Invalid network '$var_val' in $file (must be dhcp or IP/CIDR), ignoring" + var_mac) + if ! validate_mac_address "$var_val"; then + msg_warn "Invalid MAC address '$var_val' in $file, ignoring" continue fi - fi - ;; - var_fuse | var_tun | var_gpu | var_ssh | var_verbose | var_protection) - if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then - msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring" - continue - fi - ;; - var_ipv6_method) - if [[ "$var_val" != "auto" && "$var_val" != "dhcp" && "$var_val" != "static" && "$var_val" != "none" ]]; then - msg_warn "Invalid IPv6 method '$var_val' in $file (must be auto/dhcp/static/none), ignoring" - continue - fi - ;; + ;; + var_vlan) + if ! validate_vlan_tag "$var_val"; then + msg_warn "Invalid VLAN tag '$var_val' in $file (must be 1-4094), ignoring" + continue + fi + ;; + var_mtu) + if ! validate_mtu "$var_val"; then + msg_warn "Invalid MTU '$var_val' in $file (must be 576-65535), ignoring" + continue + fi + ;; + var_tags) + if ! validate_tags "$var_val"; then + msg_warn "Invalid tags '$var_val' in $file (alphanumeric, -, _, ; only), ignoring" + continue + fi + ;; + var_timezone) + if ! validate_timezone "$var_val"; then + msg_warn "Invalid timezone '$var_val' in $file, ignoring" + continue + fi + ;; + var_brg) + if ! validate_bridge "$var_val"; then + msg_warn "Bridge '$var_val' not found in $file, ignoring" + continue + fi + ;; + var_gateway) + if ! validate_gateway_ip "$var_val"; then + msg_warn "Invalid gateway IP '$var_val' in $file, ignoring" + continue + fi + ;; + var_hostname) + if ! validate_hostname "$var_val"; then + msg_warn "Invalid hostname '$var_val' in $file, ignoring" + continue + fi + ;; + var_cpu) + if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1 || var_val > 128)); then + msg_warn "Invalid CPU count '$var_val' in $file (must be 1-128), ignoring" + continue + fi + ;; + var_ram) + if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 256)); then + msg_warn "Invalid RAM '$var_val' in $file (must be >= 256 MiB), ignoring" + continue + fi + ;; + var_disk) + if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1)); then + msg_warn "Invalid disk size '$var_val' in $file (must be >= 1 GB), ignoring" + continue + fi + ;; + var_unprivileged) + if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then + msg_warn "Invalid unprivileged value '$var_val' in $file (must be 0 or 1), ignoring" + continue + fi + ;; + var_nesting) + if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then + msg_warn "Invalid nesting value '$var_val' in $file (must be 0 or 1), ignoring" + continue + fi + ;; + var_keyctl) + if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then + msg_warn "Invalid keyctl value '$var_val' in $file (must be 0 or 1), ignoring" + continue + fi + ;; + var_net) + # var_net can be: dhcp, static IP/CIDR, or IP range + if [[ "$var_val" != "dhcp" ]]; then + if is_ip_range "$var_val"; then + : # IP range is valid, will be resolved at runtime + elif ! validate_ip_address "$var_val"; then + msg_warn "Invalid network '$var_val' in $file (must be dhcp or IP/CIDR), ignoring" + continue + fi + fi + ;; + var_fuse|var_tun|var_gpu|var_ssh|var_verbose|var_protection) + if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then + msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring" + continue + fi + ;; + var_ipv6_method) + if [[ "$var_val" != "auto" && "$var_val" != "dhcp" && "$var_val" != "static" && "$var_val" != "none" ]]; then + msg_warn "Invalid IPv6 method '$var_val' in $file (must be auto/dhcp/static/none), ignoring" + continue + fi + ;; esac fi @@ -1228,18 +1273,12 @@ var_fuse=no var_tun=no # Advanced Settings (Proxmox-official features) -# var_nesting: Allow nesting (required for Docker/LXC in CT) -var_nesting=1 -# var_keyctl: Allow keyctl() - needed for Docker (systemd-networkd workaround) -var_keyctl=0 -# var_mknod: Allow device node creation (requires kernel 5.3+, experimental) -var_mknod=0 -# var_mount_fs: Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults) -var_mount_fs= -# var_protection: Prevent accidental deletion of container -var_protection=no -# var_timezone: Container timezone (e.g. Europe/Berlin, leave empty for host timezone) -var_timezone= +var_nesting=1 # Allow nesting (required for Docker/LXC in CT) +var_keyctl=0 # Allow keyctl() - needed for Docker (systemd-networkd workaround) +var_mknod=0 # Allow device node creation (requires kernel 5.3+, experimental) +var_mount_fs= # Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults) +var_protection=no # Prevent accidental deletion of container +var_timezone= # Container timezone (e.g. Europe/Berlin, leave empty for host timezone) var_tags=community-script var_verbose=no @@ -1551,7 +1590,7 @@ maybe_offer_save_app_defaults() { # 1) if no file → offer to create if [[ ! -f "$app_vars_path" ]]; then - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + if whiptail --backtitle "Proxmox VE Helper Scripts" \ --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then mkdir -p "$(dirname "$app_vars_path")" install -m 0644 "$new_tmp" "$app_vars_path" @@ -1576,7 +1615,7 @@ maybe_offer_save_app_defaults() { while true; do local sel - sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + sel="$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "APP DEFAULTS – ${APP}" \ --menu "Differences detected. What do you want to do?" 20 78 10 \ "Update Defaults" "Write new values to ${app_vars_file}" \ @@ -1597,7 +1636,7 @@ maybe_offer_save_app_defaults() { break ;; "View Diff") - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "Diff – ${APP}" \ --scrolltext --textbox "$diff_tmp" 25 100 ;; @@ -1622,6 +1661,13 @@ ensure_storage_selection_for_vars_file() { if [[ -n "$tpl" && -n "$ct" ]]; then TEMPLATE_STORAGE="$tpl" CONTAINER_STORAGE="$ct" + + # Validate storage space for loaded container storage + if [[ -n "${DISK_SIZE:-}" ]]; then + validate_storage_space "$ct" "$DISK_SIZE" "yes" + # Continue even if validation fails - user was warned + fi + return 0 fi @@ -1702,7 +1748,7 @@ advanced_settings() { elif [ -f /etc/timezone ]; then _host_timezone=$(cat /etc/timezone 2>/dev/null || echo "") fi - # pct doesn't accept Etc/* zones - map to 'host' instead + # 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" @@ -1799,32 +1845,36 @@ advanced_settings() { elif [[ "$PW1" == *" "* ]]; then whiptail --msgbox "Password cannot contain spaces." 8 58 else - # Clean up leading dashes from password local _pw1_clean="$PW1" while [[ "$_pw1_clean" == -* ]]; do _pw1_clean="${_pw1_clean#-}" done if [[ -z "$_pw1_clean" ]]; then whiptail --msgbox "Password cannot be only '-' characters." 8 58 + continue elif ((${#_pw1_clean} < 5)); then whiptail --msgbox "Password must be at least 5 characters (after removing leading '-')." 8 70 - else - # Verify password - if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "PASSWORD VERIFICATION" \ - --ok-button "Confirm" --cancel-button "Back" \ - --passwordbox "\nVerify Root Password" 10 58 \ - 3>&1 1>&2 2>&3); then - if [[ "$PW1" == "$PW2" ]]; then - _pw="--password $_pw1_clean" - _pw_display="********" - ((STEP++)) - else - whiptail --msgbox "Passwords do not match. Please try again." 8 58 - fi + continue + fi + # Verify password + if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "PASSWORD VERIFICATION" \ + --ok-button "Confirm" --cancel-button "Back" \ + --passwordbox "\nVerify Root Password" 10 58 \ + 3>&1 1>&2 2>&3); then + local _pw2_clean="$PW2" + while [[ "$_pw2_clean" == -* ]]; do + _pw2_clean="${_pw2_clean#-}" + done + if [[ "$_pw1_clean" == "$_pw2_clean" ]]; then + _pw="--password $_pw1_clean" + _pw_display="********" + ((STEP++)) else - ((STEP--)) + whiptail --msgbox "Passwords do not match. Please try again." 8 58 fi + else + ((STEP--)) fi fi else @@ -1842,22 +1892,25 @@ advanced_settings() { --inputbox "\nSet Container ID" 10 58 "$_ct_id" \ 3>&1 1>&2 2>&3); then local input_id="${result:-$NEXTID}" - # Validate container ID is numeric + + # Validate that ID is numeric if ! [[ "$input_id" =~ ^[0-9]+$ ]]; then - whiptail --msgbox "Container ID must be numeric." 8 58 + whiptail --backtitle "Proxmox VE Helper Scripts" --title "Invalid ID" --msgbox "Container ID must be numeric." 8 58 continue fi - # Validate container ID is available + + # Check if ID is already in use if ! validate_container_id "$input_id"; then - if whiptail --yesno "Container/VM ID $input_id is already in use.\n\nWould you like to use the next available ID: $(get_valid_container_id "$input_id")?" 10 58; then + if whiptail --backtitle "Proxmox VE Helper Scripts" --title "ID Already In Use" \ + --yesno "Container/VM ID $input_id is already in use.\n\nWould you like to use the next available ID ($(get_valid_container_id "$input_id"))?" 10 58; then _ct_id=$(get_valid_container_id "$input_id") - ((STEP++)) + else + continue fi - # else stay on this step else _ct_id="$input_id" - ((STEP++)) fi + ((STEP++)) else ((STEP--)) fi @@ -1870,15 +1923,16 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "HOSTNAME" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Hostname (lowercase, alphanumeric, hyphens only)" 10 58 "$_hostname" \ + --inputbox "\nSet Hostname (or FQDN, e.g. host.example.com)" 10 58 "$_hostname" \ 3>&1 1>&2 2>&3); then local hn_test="${result:-$NSAPP}" hn_test=$(echo "${hn_test,,}" | tr -d ' ') - if [[ "$hn_test" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then + + if validate_hostname "$hn_test"; then _hostname="$hn_test" ((STEP++)) else - whiptail --msgbox "Invalid hostname: '$hn_test'\n\nOnly lowercase letters, digits and hyphens are allowed." 10 58 + whiptail --msgbox "Invalid hostname: '$hn_test'\n\nRules:\n- Only lowercase letters, digits, dots and hyphens\n- Labels separated by dots (max 63 chars each)\n- No leading/trailing hyphens or dots\n- No consecutive dots\n- Total max 253 characters" 14 60 fi else ((STEP--)) @@ -1953,8 +2007,14 @@ advanced_settings() { # ═══════════════════════════════════════════════════════════════════════════ 8) if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then - _bridge="vmbr0" - ((STEP++)) + # Validate default bridge exists + if validate_bridge "vmbr0"; then + _bridge="vmbr0" + ((STEP++)) + else + whiptail --msgbox "Default bridge 'vmbr0' not found!\n\nPlease configure a network bridge in Proxmox first." 10 58 + exit 1 + fi else if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "NETWORK BRIDGE" \ @@ -1962,8 +2022,13 @@ advanced_settings() { --menu "\nSelect network bridge:" 16 58 6 \ "${BRIDGE_MENU_OPTIONS[@]}" \ 3>&1 1>&2 2>&3); then - _bridge="${result:-vmbr0}" - ((STEP++)) + local bridge_test="${result:-vmbr0}" + if validate_bridge "$bridge_test"; then + _bridge="$bridge_test" + ((STEP++)) + else + whiptail --msgbox "Bridge '$bridge_test' is not available or not active." 8 58 + fi else ((STEP--)) fi @@ -1977,9 +2042,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 @@ -1990,7 +2056,7 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nEnter Static IPv4 CIDR Address\n(e.g. 192.168.1.100/24)" 12 58 "" \ 3>&1 1>&2 2>&3); then - if [[ "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then + if validate_ip_address "$static_ip"; then # Get gateway local gateway_ip if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ @@ -1998,16 +2064,62 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nEnter Gateway IP address" 10 58 "" \ 3>&1 1>&2 2>&3); then - if [[ "$gateway_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - _net="$static_ip" - _gate=",gw=$gateway_ip" - ((STEP++)) + if validate_gateway_ip "$gateway_ip"; then + # Validate gateway is in same subnet + if validate_gateway_in_subnet "$static_ip" "$gateway_ip"; then + _net="$static_ip" + _gate=",gw=$gateway_ip" + ((STEP++)) + else + whiptail --msgbox "Gateway is not in the same subnet as the static IP.\n\nStatic IP: $static_ip\nGateway: $gateway_ip" 10 58 + fi else - whiptail --msgbox "Invalid Gateway IP format." 8 58 + whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58 fi fi else - whiptail --msgbox "Invalid IPv4 CIDR format.\nExample: 192.168.1.100/24" 8 58 + whiptail --msgbox "Invalid IPv4 CIDR format.\n\nEach octet must be 0-255.\nCIDR must be 1-32.\nExample: 192.168.1.100/24" 12 58 + fi + fi + elif [[ "$result" == "range" ]]; then + # IP Range Scan + local ip_range + if ip_range=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "IP RANGE SCAN" \ + --ok-button "Scan" --cancel-button "Back" \ + --inputbox "\nEnter IP range to scan for free address\n(e.g. 192.168.1.100/24-192.168.1.200/24)" 12 65 "" \ + 3>&1 1>&2 2>&3); then + if is_ip_range "$ip_range"; then + # Exit whiptail screen temporarily to show scan progress + clear + header_info + echo -e "${INFO}${BOLD}${DGN}Scanning IP range for free address...${CL}\n" + if resolve_ip_from_range "$ip_range"; then + # Get gateway + local gateway_ip + if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "GATEWAY IP" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nFound free IP: $NET_RESOLVED\n\nEnter Gateway IP address" 12 58 "" \ + 3>&1 1>&2 2>&3); then + if validate_gateway_ip "$gateway_ip"; then + # Validate gateway is in same subnet + if validate_gateway_in_subnet "$NET_RESOLVED" "$gateway_ip"; then + _net="$NET_RESOLVED" + _gate=",gw=$gateway_ip" + ((STEP++)) + else + whiptail --msgbox "Gateway is not in the same subnet as the IP.\n\nIP: $NET_RESOLVED\nGateway: $gateway_ip" 10 58 + fi + else + whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58 + fi + fi + else + whiptail --msgbox "No free IP found in the specified range.\nAll IPs responded to ping." 10 58 + fi + else + whiptail --msgbox "Invalid IP range format.\n\nExample: 192.168.1.100/24-192.168.1.200/24" 10 58 fi fi else @@ -2043,16 +2155,33 @@ advanced_settings() { --title "STATIC IPv6 ADDRESS" \ --inputbox "\nEnter IPv6 CIDR address\n(e.g. 2001:db8::1/64)" 12 58 "" \ 3>&1 1>&2 2>&3); then - if [[ "$ipv6_addr" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then + if validate_ipv6_address "$ipv6_addr"; then _ipv6_addr="$ipv6_addr" - # Optional gateway - _ipv6_gate=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "IPv6 GATEWAY" \ - --inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \ - 3>&1 1>&2 2>&3) || true - ((STEP++)) + # Optional gateway - loop until valid or empty + local ipv6_gw_valid=false + while [[ "$ipv6_gw_valid" == "false" ]]; do + local ipv6_gw + ipv6_gw=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "IPv6 GATEWAY" \ + --inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \ + 3>&1 1>&2 2>&3) || true + # Validate gateway if provided + if [[ -n "$ipv6_gw" ]]; then + if validate_ipv6_address "$ipv6_gw"; then + _ipv6_gate="$ipv6_gw" + ipv6_gw_valid=true + ((STEP++)) + else + whiptail --msgbox "Invalid IPv6 gateway format.\n\nExample: 2001:db8::1" 8 58 + fi + else + _ipv6_gate="" + ipv6_gw_valid=true + ((STEP++)) + fi + done else - whiptail --msgbox "Invalid IPv6 CIDR format." 8 58 + whiptail --msgbox "Invalid IPv6 CIDR format.\n\nExample: 2001:db8::1/64\nCIDR must be 1-128." 10 58 fi fi ;; @@ -2061,11 +2190,7 @@ advanced_settings() { _ipv6_gate="" ((STEP++)) ;; - disable) - _ipv6_addr="" - _ipv6_gate="" - ((STEP++)) - ;; + none) _ipv6_addr="none" _ipv6_gate="" @@ -2089,10 +2214,14 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "MTU SIZE" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Interface MTU Size\n(leave blank for default 1500)" 12 58 "" \ + --inputbox "\nSet Interface MTU Size\n(leave blank for default 1500, common values: 1500, 9000)" 12 62 "" \ 3>&1 1>&2 2>&3); then - _mtu="$result" - ((STEP++)) + if validate_mtu "$result"; then + _mtu="$result" + ((STEP++)) + else + whiptail --msgbox "Invalid MTU size.\n\nMTU must be between 576 and 65535.\nCommon values: 1500 (default), 9000 (jumbo frames)" 10 58 + fi else ((STEP--)) fi @@ -2137,10 +2266,14 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "MAC ADDRESS" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet MAC Address\n(leave blank for auto-generated)" 12 58 "" \ + --inputbox "\nSet MAC Address\n(leave blank for auto-generated, format: XX:XX:XX:XX:XX:XX)" 12 62 "" \ 3>&1 1>&2 2>&3); then - _mac="$result" - ((STEP++)) + if validate_mac_address "$result"; then + _mac="$result" + ((STEP++)) + else + whiptail --msgbox "Invalid MAC address format.\n\nRequired format: XX:XX:XX:XX:XX:XX\nExample: 02:00:00:00:00:01" 10 58 + fi else ((STEP--)) fi @@ -2153,10 +2286,14 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "VLAN TAG" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet VLAN Tag\n(leave blank for no VLAN)" 12 58 "" \ + --inputbox "\nSet VLAN Tag (1-4094)\n(leave blank for no VLAN)" 12 58 "" \ 3>&1 1>&2 2>&3); then - _vlan="$result" - ((STEP++)) + if validate_vlan_tag "$result"; then + _vlan="$result" + ((STEP++)) + else + whiptail --msgbox "Invalid VLAN tag.\n\nVLAN must be a number between 1 and 4094." 8 58 + fi else ((STEP--)) fi @@ -2169,11 +2306,16 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "CONTAINER TAGS" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Custom Tags (semicolon-separated)\n(remove all for no tags)" 12 58 "$_tags" \ + --inputbox "\nSet Custom Tags (semicolon-separated)\n(alphanumeric, hyphens, underscores only)" 12 58 "$_tags" \ 3>&1 1>&2 2>&3); then - _tags="${result:-;}" - _tags=$(echo "$_tags" | tr -d '[:space:]') - ((STEP++)) + local tags_test="${result:-}" + tags_test=$(echo "$tags_test" | tr -d '[:space:]') + if validate_tags "$tags_test"; then + _tags="$tags_test" + ((STEP++)) + else + whiptail --msgbox "Invalid tag format.\n\nTags can only contain:\n- Letters (a-z, A-Z)\n- Numbers (0-9)\n- Hyphens (-)\n- Underscores (_)\n- Semicolons (;) as separator" 14 58 + fi else ((STEP--)) fi @@ -2352,8 +2494,14 @@ advanced_settings() { --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" - ((STEP++)) + local tz_test="$result" + [[ "${tz_test:-}" == Etc/* ]] && tz_test="host" # pct doesn't accept Etc/* zones + if validate_timezone "$tz_test"; then + _ct_timezone="$tz_test" + ((STEP++)) + else + whiptail --msgbox "Invalid timezone: '$result'\n\nTimezone must exist in /usr/share/zoneinfo/\n\nExamples:\n- Europe/Berlin\n- America/New_York\n- Asia/Tokyo\n- UTC" 14 58 + fi else ((STEP--)) fi @@ -2553,10 +2701,9 @@ Advanced: export UDHCPC_FIX export SSH_KEYS_FILE - # Exit alternate screen buffer BEFORE displaying summary - # so the summary is visible in the main terminal + # Exit alternate screen buffer before showing summary (so output remains visible) tput rmcup 2>/dev/null || true - trap - RETURN # Remove the trap since we already called rmcup + trap - RETURN # Display final summary echo -e "\n${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" @@ -2571,14 +2718,14 @@ Advanced: 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}" - [[ "$ENABLE_TUN" == "yes" ]] && echo -e "${NETWORK}${BOLD}${DGN}TUN/TAP Support: ${BGN}$ENABLE_TUN${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Nesting: ${BGN}$([ "$ENABLE_NESTING" == "1" ] && echo "Enabled" || echo "Disabled")${CL}" - [[ "$ENABLE_KEYCTL" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Keyctl: ${BGN}Enabled${CL}" - echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}$ENABLE_GPU${CL}" - [[ "$PROTECT_CT" == "yes" || "$PROTECT_CT" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Protection: ${BGN}Enabled${CL}" - [[ -n "$CT_TIMEZONE" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}" - [[ "$APT_CACHER" == "yes" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${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}" } @@ -2603,12 +2750,12 @@ diagnostics_check() { fi if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then - if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then cat </usr/local/community-scripts/diagnostics DIAGNOSTICS=yes #This file is used to store the diagnostics settings for the Community-Scripts API. -#https://github.com/community-scripts/ProxmoxVED/discussions/1836 +#https://git.community-scripts.org/community-scripts/ProxmoxVED/discussions/1836 #Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes. #You can review the data at https://community-scripts.github.io/ProxmoxVE/data #If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue. @@ -2633,7 +2780,7 @@ EOF DIAGNOSTICS=no #This file is used to store the diagnostics settings for the Community-Scripts API. -#https://github.com/community-scripts/ProxmoxVED/discussions/1836 +#https://git.community-scripts.org/community-scripts/ProxmoxVED/discussions/1836 #Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes. #You can review the data at https://community-scripts.github.io/ProxmoxVE/data #If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue. @@ -2662,7 +2809,7 @@ EOF diagnostics_menu() { if [ "${DIAGNOSTICS:-no}" = "yes" ]; then - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + if whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "DIAGNOSTIC SETTINGS" \ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ --yes-button "No" --no-button "Back"; then @@ -2671,7 +2818,7 @@ diagnostics_menu() { whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 fi else - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + if whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "DIAGNOSTIC SETTINGS" \ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ --yes-button "Yes" --no-button "Back"; then @@ -2701,8 +2848,8 @@ 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 [ "${var_gpu:-no}" == "yes" ]; then - echo -e "🎮${BOLD}${DGN} GPU Passthrough: ${BGN}Enabled${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}" @@ -2743,8 +2890,7 @@ install_script() { else timezone="UTC" fi - # pct doesn't accept Etc/* zones - map to 'host' instead - [[ "${timezone:-}" == Etc/* ]] && timezone="host" + [[ "${timezone:-}" == Etc/* ]] && timezone="host" # pct doesn't accept Etc/* zones # Show APP Header header_info @@ -2808,7 +2954,6 @@ install_script() { 2 | advanced | ADVANCED) header_info echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" METHOD="advanced" base_settings advanced_settings @@ -2829,8 +2974,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 @@ -2971,9 +3116,9 @@ ssh_extract_keys_from_file() { tr -d '\r' <"$f" | awk ' /^[[:space:]]*#/ {next} /^[[:space:]]*$/ {next} - # nackt: typ base64 [comment] + # bare format: type base64 [comment] /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next} - # mit Optionen: finde ab erstem Key-Typ + # with options: find from first key-type onward { match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/) if (RSTART>0) { print substr($0, RSTART) } @@ -3047,8 +3192,8 @@ ssh_discover_default_files() { configure_ssh_settings() { local step_info="${1:-}" - local backtitle="[dev] Proxmox VE Helper Scripts" - [[ -n "$step_info" ]] && backtitle="[dev] Proxmox VE Helper Scripts [${step_info}]" + local backtitle="Proxmox VE Helper Scripts" + [[ -n "$step_info" ]] && backtitle="Proxmox VE Helper Scripts [${step_info}]" SSH_KEYS_FILE="$(mktemp)" : >"$SSH_KEYS_FILE" @@ -3157,7 +3302,7 @@ start() { update_script cleanup_lxc else - CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ + CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ "Support/Update functions for ${APP} LXC. Choose an option:" \ 12 60 3 \ "1" "YES (Silent Mode)" \ @@ -3215,7 +3360,7 @@ build_container() { esac fi - # IP (immer zwingend, Standard dhcp) + # IP (always required, default dhcp) NET_STRING+=",ip=${NET:-dhcp}" # Gateway @@ -3272,14 +3417,14 @@ build_container() { FEATURES="${FEATURES}fuse=1" fi - # NEW IMPLEMENTATION (Fixed): Build PCT_OPTIONS properly - # Key insight: Bash cannot export arrays, so we build the options as a string - + # Build PCT_OPTIONS as string for export TEMP_DIR=$(mktemp -d) pushd "$TEMP_DIR" >/dev/null - - # Unified install.func automatically detects OS type (debian, alpine, fedora, etc.) - export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)" + if [ "$var_os" == "alpine" ]; then + export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)" + else + export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)" + fi # Core exports for install.func export DIAGNOSTICS="$DIAGNOSTICS" @@ -3298,11 +3443,11 @@ build_container() { export CTTYPE="$CT_TYPE" export ENABLE_FUSE="$ENABLE_FUSE" export ENABLE_TUN="$ENABLE_TUN" - export ENABLE_GPU="$ENABLE_GPU" - export IPV6_METHOD="$IPV6_METHOD" 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" @@ -3316,38 +3461,21 @@ build_container() { export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}" export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}" - # Validate storage space before container creation - if [[ -n "$CONTAINER_STORAGE" ]]; then - msg_info "Validating storage space" - if ! validate_storage_space "$CONTAINER_STORAGE" "$DISK_SIZE" "no"; then - local free_space - free_space=$(pvesm status 2>/dev/null | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') - local free_fmt - free_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free_space" 2>/dev/null || echo "${free_space}KB") - msg_error "Not enough space on '$CONTAINER_STORAGE'. Required: ${DISK_SIZE}GB, Available: ${free_fmt}" - exit 214 - fi - msg_ok "Storage space validated" - fi - # Build PCT_OPTIONS as multi-line string - PCT_OPTIONS_STRING="" + PCT_OPTIONS_STRING=" -hostname $HN" - # Add features if set - if [ -n "$FEATURES" ]; then - PCT_OPTIONS_STRING=" -features $FEATURES" - fi - - # Add hostname - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - -hostname $HN" - - # Add tags if set + # Only add -tags if TAGS is not empty if [ -n "$TAGS" ]; then PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING -tags $TAGS" fi + # Only add -features if FEATURES is not empty + if [ -n "$FEATURES" ]; then + PCT_OPTIONS_STRING=" -features $FEATURES +$PCT_OPTIONS_STRING" + fi + # Add storage if specified if [ -n "$SD" ]; then PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING @@ -3374,10 +3502,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) @@ -3391,10 +3521,20 @@ build_container() { export TEMPLATE_STORAGE="${var_template_storage:-}" export CONTAINER_STORAGE="${var_container_storage:-}" - # # DEBUG: Show final PCT_OPTIONS being exported - # echo "[DEBUG] PCT_OPTIONS to be exported:" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Calling create_lxc_container..." + # Validate storage space only if CONTAINER_STORAGE is already set + # (Storage selection happens in create_lxc_container for some modes) + if [[ -n "$CONTAINER_STORAGE" ]]; then + msg_info "Validating storage space" + if ! validate_storage_space "$CONTAINER_STORAGE" "$DISK_SIZE" "no"; then + local free_space + free_space=$(pvesm status 2>/dev/null | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') + local free_fmt + free_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free_space" 2>/dev/null || echo "${free_space}KB") + msg_error "Not enough space on '$CONTAINER_STORAGE'. Required: ${DISK_SIZE}GB, Available: ${free_fmt}" + exit 214 + fi + msg_ok "Storage space validated" + fi create_lxc_container || exit $? @@ -3453,12 +3593,16 @@ build_container() { msg_custom "🎮" "${GN}" "Detected NVIDIA GPU" # Simple passthrough - just bind /dev/nvidia* devices if they exist - # Skip directories like /dev/nvidia-caps (they need special handling) + # Only include character devices (-c), skip directories like /dev/nvidia-caps for d in /dev/nvidia*; do - [[ -e "$d" ]] || continue - [[ -d "$d" ]] && continue # Skip directories - NVIDIA_DEVICES+=("$d") + [[ -c "$d" ]] && NVIDIA_DEVICES+=("$d") done + # Also check for devices inside /dev/nvidia-caps/ directory + if [[ -d /dev/nvidia-caps ]]; then + for d in /dev/nvidia-caps/*; do + [[ -c "$d" ]] && NVIDIA_DEVICES+=("$d") + done + fi if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough" @@ -3691,7 +3835,6 @@ EOF msg_ok "Network in LXC is reachable (ping)" fi fi - # Function to get correct GID inside container get_container_gid() { local group="$1" @@ -3713,176 +3856,41 @@ EOF # install_gpu_userland "NVIDIA" # fi - # Continue with standard container setup - install core dependencies based on OS - sleep 3 - - case "$var_os" in - alpine) + # Continue with standard container setup + if [ "$var_os" == "alpine" ]; then + sleep 3 pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories http://dl-cdn.alpinelinux.org/alpine/latest-stable/main http://dl-cdn.alpinelinux.org/alpine/latest-stable/community EOF' pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null" - ;; + else + sleep 3 + 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" - debian | ubuntu | devuan) - # First install locales package (required for locale-gen on minimal templates) - pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y locales >/dev/null 2>&1 || true" - - # Locale setup for Debian-based - pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen 2>/dev/null || true" - pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen 2>/dev/null | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ - [[ -n \"\$locale_line\" ]] && echo LANG=\$locale_line >/etc/default/locale && \ - locale-gen >/dev/null 2>&1 && \ - export LANG=\$locale_line || true" - - # Timezone setup 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) + # Create /etc/timezone for backwards compatibility with older scripts pct exec "$CTID" -- bash -c "tz='$tz'; ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime && echo \"\$tz\" >/etc/timezone || true" else msg_warn "Skipping timezone setup – zone '$tz' not found in container" fi - # Core dependencies pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || { msg_error "apt-get base packages installation failed" exit 1 } - ;; - - fedora | rockylinux | almalinux | centos) - # RHEL-based: Fedora, Rocky, AlmaLinux, CentOS - # Detect OS major version for EL10+ compatibility (DNF 5, different packages) - local rhel_version - rhel_version=$(pct exec "$CTID" -- bash -c "grep -oP '(?<=VERSION_ID=\")[0-9]+' /etc/os-release 2>/dev/null || echo 9") - - # First run makecache to ensure repos are ready (critical for fresh templates) - msg_info "Initializing package manager (this may take a moment)..." - if ! pct exec "$CTID" -- bash -c "dnf makecache --refresh 2>&1 || yum makecache 2>&1" >/dev/null 2>&1; then - msg_warn "Package cache update had issues, continuing anyway..." - fi - - # Build package list - EL10+ may not have glibc-langpack-en in same form - local rhel_packages="curl sudo mc jq which tar procps-ng ncurses" - if [[ "$rhel_version" -lt 10 ]]; then - rhel_packages="$rhel_packages glibc-langpack-en" - else - # EL10 uses glibc-all-langpacks or langpacks-en - rhel_packages="$rhel_packages langpacks-en glibc-all-langpacks" - fi - - # Install base packages with better error handling - local install_log="/tmp/dnf_install_${CTID}.log" - if ! pct exec "$CTID" -- bash -c "dnf install -y $rhel_packages 2>&1 | tee $install_log; exit \${PIPESTATUS[0]}" >/dev/null 2>&1; then - # Check if it's just missing optional packages - if pct exec "$CTID" -- bash -c "rpm -q curl sudo mc jq which tar procps-ng" >/dev/null 2>&1; then - msg_warn "Some optional packages may have failed, but core packages installed" - else - # Real failure - try minimal install - msg_warn "Full package install failed, trying minimal set..." - if ! pct exec "$CTID" -- bash -c "dnf install -y curl sudo jq which tar 2>&1" >/dev/null 2>&1; then - msg_error "dnf/yum base packages installation failed" - pct exec "$CTID" -- bash -c "cat $install_log 2>/dev/null" || true - exit 1 - fi - fi - fi - - # Set locale for RHEL-based systems - pct exec "$CTID" -- bash -c "localectl set-locale LANG=en_US.UTF-8 2>/dev/null || echo 'LANG=en_US.UTF-8' > /etc/locale.conf" || true - - # Timezone setup for RHEL - if [[ -z "${tz:-}" ]]; then - tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC") - fi - [[ "${tz:-}" == Etc/* ]] && tz="UTC" - if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then - pct exec "$CTID" -- bash -c "timedatectl set-timezone '$tz' 2>/dev/null || ln -sf '/usr/share/zoneinfo/$tz' /etc/localtime" || true - fi - ;; - - opensuse) - # openSUSE - special handling for terminal/locale issues - # Use --gpg-auto-import-keys to avoid interactive prompts that cause hangs - msg_info "Initializing package manager for openSUSE..." - pct exec "$CTID" -- bash -c "zypper --gpg-auto-import-keys --non-interactive refresh 2>&1" >/dev/null 2>&1 || true - - # Install packages - ncurses and terminfo are CRITICAL for terminal to work - if ! pct exec "$CTID" -- bash -c "zypper --gpg-auto-import-keys --non-interactive install -y curl sudo mc jq glibc-locale ncurses terminfo-base 2>&1" >/dev/null 2>&1; then - # Try without glibc-locale - if ! pct exec "$CTID" -- bash -c "zypper --gpg-auto-import-keys --non-interactive install -y curl sudo mc jq ncurses terminfo-base 2>&1" >/dev/null 2>&1; then - msg_error "zypper base packages installation failed" - exit 1 - fi - fi - - # Fix 'unknown terminal type' error - set TERM in multiple places - pct exec "$CTID" -- bash -c "localectl set-locale LANG=en_US.UTF-8 2>/dev/null || echo 'LANG=en_US.UTF-8' > /etc/locale.conf" || true - - # Set TERM globally for all users - pct exec "$CTID" -- bash -c "cat > /etc/profile.d/term.sh << 'EOFTERM' -# Fix terminal type for LXC containers -if [ -z \"\$TERM\" ] || [ \"\$TERM\" = \"dumb\" ] || [ \"\$TERM\" = \"-\" ]; then - export TERM=xterm-256color -fi -EOFTERM -chmod +x /etc/profile.d/term.sh" || true - - # Also set in /etc/environment for non-login shells - pct exec "$CTID" -- bash -c "grep -q '^TERM=' /etc/environment 2>/dev/null || echo 'TERM=xterm-256color' >> /etc/environment" || true - ;; - - gentoo) - # Gentoo - OpenRC based, emerge is slow - # Use emerge-webrsync (faster, uses http instead of rsync) - msg_info "Syncing Gentoo portage via webrsync (faster than rsync)..." - pct exec "$CTID" -- bash -c "emerge-webrsync 2>&1" >/dev/null 2>&1 || { - msg_warn "emerge-webrsync failed, trying emerge --sync..." - pct exec "$CTID" -- bash -c "emerge --sync 2>&1" >/dev/null 2>&1 || true - } - - # Install curl FIRST - it's required for install.func to work - msg_info "Installing essential packages for Gentoo..." - if ! pct exec "$CTID" -- bash -c "emerge --quiet --noreplace net-misc/curl 2>&1" >/dev/null 2>&1; then - msg_error "Failed to install curl on Gentoo - this is required" - exit 1 - fi - - # Install remaining packages - pct exec "$CTID" -- bash -c "emerge --quiet --noreplace app-misc/jq app-misc/mc sys-libs/ncurses 2>&1" >/dev/null 2>&1 || { - msg_warn "Some Gentoo packages may need manual setup" - } - - # Set TERM for Gentoo - pct exec "$CTID" -- bash -c "echo 'export TERM=xterm-256color' >> /etc/profile.d/term.sh && chmod +x /etc/profile.d/term.sh" || true - ;; - - openeuler) - # openEuler (RHEL-compatible, uses DNF) - # Note: Template was patched with /etc/redhat-release in create_container - msg_info "Initializing package manager for openEuler..." - pct exec "$CTID" -- bash -c "dnf makecache --refresh 2>&1" >/dev/null 2>&1 || true - - # openEuler package names may differ from RHEL - local euler_packages="curl sudo mc jq procps-ng ncurses" - if ! pct exec "$CTID" -- bash -c "dnf install -y $euler_packages 2>&1" >/dev/null 2>&1; then - # Try without procps-ng (might be just 'procps' in openEuler) - if ! pct exec "$CTID" -- bash -c "dnf install -y curl sudo mc jq ncurses 2>&1" >/dev/null 2>&1; then - msg_error "dnf base packages installation failed" - exit 1 - fi - fi - # Set locale - pct exec "$CTID" -- bash -c "echo 'LANG=en_US.UTF-8' > /etc/locale.conf" || true - ;; - - *) - msg_warn "Unknown OS '$var_os' - skipping core dependency installation" - ;; - esac + fi msg_ok "Customized LXC Container" @@ -3890,12 +3898,7 @@ chmod +x /etc/profile.d/term.sh" || true install_ssh_keys_into_ct # Run application installer - # NOTE: We disable error handling here because: - # 1. Container errors are caught by error_handler INSIDE container - # 2. Container creates flag file with exit code - # 3. We read flag file and handle cleanup manually below - # 4. We DON'T want host error_handler to fire for lxc-attach command itself - + # Disable error trap - container errors are handled internally via flag file set +Eeuo pipefail # Disable ALL error handling temporarily trap - ERR # Remove ERR trap completely @@ -3965,35 +3968,129 @@ chmod +x /etc/profile.d/term.sh" || true exit $install_exit_code fi - # Report failure to API before container cleanup - post_update_to_api "failed" "$install_exit_code" - # Prompt user for cleanup with 60s timeout (plain echo - no msg_info to avoid spinner) echo "" - echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}" + + # Detect error type for smart recovery options + local is_oom=false + local error_explanation="" + if declare -f explain_exit_code >/dev/null 2>&1; then + error_explanation="$(explain_exit_code "$install_exit_code")" + fi + + # OOM detection: exit codes 134 (SIGABRT/heap), 137 (SIGKILL/OOM), 243 (Node.js heap) + if [[ $install_exit_code -eq 134 || $install_exit_code -eq 137 || $install_exit_code -eq 243 ]]; then + is_oom=true + fi + + # Show error explanation if available + if [[ -n "$error_explanation" ]]; then + echo -e "${TAB}${RD}Error: ${error_explanation}${CL}" + echo "" + fi + + # Build recovery menu based on error type + echo -e "${YW}What would you like to do?${CL}" + echo "" + echo -e " ${GN}1)${CL} Remove container and exit" + echo -e " ${GN}2)${CL} Keep container for debugging" + echo -e " ${GN}3)${CL} Retry with verbose mode" + if [[ "$is_oom" == true ]]; then + local new_ram=$((RAM_SIZE * 3 / 2)) + local new_cpu=$((CORE_COUNT + 1)) + echo -e " ${GN}4)${CL} Retry with more resources (RAM: ${RAM_SIZE}→${new_ram} MiB, CPU: ${CORE_COUNT}→${new_cpu} cores)" + fi + echo "" + echo -en "${YW}Select option [1-$([[ "$is_oom" == true ]] && echo "4" || echo "3")] (default: 1, auto-remove in 60s): ${CL}" if read -t 60 -r response; then - if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then - # Remove container - echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}" - pct stop "$CTID" &>/dev/null || true - pct destroy "$CTID" &>/dev/null || true - echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" - elif [[ "$response" =~ ^[Nn]$ ]]; then - echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}" - - # Dev mode: Setup MOTD/SSH for debugging access to broken container - if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then - echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}" - if pct exec "$CTID" -- bash -c " - source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func) - declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true - " >/dev/null 2>&1; then - local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) - echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}" + case "${response:-1}" in + 1) + # Remove container + echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}" + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" + ;; + 2) + echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}" + # Dev mode: Setup MOTD/SSH for debugging access to broken container + if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then + echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}" + if pct exec "$CTID" -- bash -c " + source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func) + declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true + " >/dev/null 2>&1; then + local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) + echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}" + fi fi - fi - fi + exit $install_exit_code + ;; + 3) + # Retry with verbose mode + echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild...${CL}" + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" + echo "" + # Get new container ID + local old_ctid="$CTID" + export CTID=$(get_valid_container_id "$CTID") + export VERBOSE="yes" + export var_verbose="yes" + + # Show rebuild summary + echo -e "${YW}Rebuilding with preserved settings:${CL}" + echo -e " Container ID: ${old_ctid} → ${CTID}" + echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores | Disk: ${DISK_SIZE} GB" + echo -e " Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}" + echo -e " Verbose: ${GN}enabled${CL}" + echo "" + msg_info "Restarting installation..." + # Re-run build_container + build_container + return $? + ;; + 4) + if [[ "$is_oom" == true ]]; then + # Retry with more resources + echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with more resources...${CL}" + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" + echo "" + # Get new container ID and increase resources + local old_ctid="$CTID" + local old_ram="$RAM_SIZE" + local old_cpu="$CORE_COUNT" + export CTID=$(get_valid_container_id "$CTID") + export RAM_SIZE=$((RAM_SIZE * 3 / 2)) + export CORE_COUNT=$((CORE_COUNT + 1)) + export var_ram="$RAM_SIZE" + export var_cpu="$CORE_COUNT" + + # Show rebuild summary + echo -e "${YW}Rebuilding with increased resources:${CL}" + echo -e " Container ID: ${old_ctid} → ${CTID}" + echo -e " RAM: ${old_ram} → ${GN}${RAM_SIZE}${CL} MiB (+50%)" + echo -e " CPU: ${old_cpu} → ${GN}${CORE_COUNT}${CL} cores (+1)" + echo -e " Disk: ${DISK_SIZE} GB | Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}" + echo "" + msg_info "Restarting installation..." + # Re-run build_container + build_container + return $? + else + echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}" + exit $install_exit_code + fi + ;; + *) + echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}" + exit $install_exit_code + ;; + esac else # Timeout - auto-remove echo -e "\n${YW}No response - auto-removing container${CL}" @@ -4013,12 +4110,12 @@ destroy_lxc() { return 1 fi - # Abbruch bei Ctrl-C / Ctrl-D / ESC + # Abort on Ctrl-C / Ctrl-D / ESC trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT local prompt if ! read -rp "Remove this Container? " prompt; then - # read gibt != 0 zurück bei Ctrl-D/ESC + # read returns non-zero on Ctrl-D/ESC msg_error "Aborted input (Ctrl-D/ESC)" return 130 fi @@ -4044,7 +4141,6 @@ destroy_lxc() { # ------------------------------------------------------------------------------ # Storage discovery / selection helpers # ------------------------------------------------------------------------------ -# ===== Storage discovery / selection helpers (ported from create_lxc.sh) ===== resolve_storage_preselect() { local class="$1" preselect="$2" required_content="" case "$class" in @@ -4120,15 +4216,14 @@ fix_gpu_gids() { # For privileged containers: also fix permissions inside container if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " + pct exec "$CTID" -- sh -c " if [ -d /dev/dri ]; then for dev in /dev/dri/*; do if [ -e \"\$dev\" ]; then - if [[ \"\$dev\" =~ renderD ]]; then - chgrp ${render_gid} \"\$dev\" 2>/dev/null || true - else - chgrp ${video_gid} \"\$dev\" 2>/dev/null || true - fi + case \"\$dev\" in + *renderD*) chgrp ${render_gid} \"\$dev\" 2>/dev/null || true ;; + *) chgrp ${video_gid} \"\$dev\" 2>/dev/null || true ;; + esac chmod 660 \"\$dev\" 2>/dev/null || true fi done @@ -4203,18 +4298,13 @@ select_storage() { if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}" STORAGE_INFO="${MENU[1]}" - - # Validate storage space for auto-picked container storage - if [[ "$CLASS" == "container" && -n "${DISK_SIZE:-}" ]]; then - validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes" - fi return 0 fi local WIDTH=$((COL_WIDTH + 42)) while true; do local DISPLAY_SELECTED - DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + DISPLAY_SELECTED=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "Storage Pools" \ --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \ 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; } @@ -4235,7 +4325,9 @@ select_storage() { # Validate storage space for container storage if [[ "$CLASS" == "container" && -n "${DISK_SIZE:-}" ]]; then validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes" + # Continue even if validation fails - user was warned fi + return 0 done } @@ -4298,6 +4390,18 @@ validate_storage_space() { return 0 } +# ============================================================================== +# SECTION 8: CONTAINER CREATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# create_lxc_container() +# +# - Main function for creating LXC containers +# - Handles all phases: validation, template discovery, container creation, +# network config, storage, etc. +# - Extensive error checking with detailed exit codes +# ------------------------------------------------------------------------------ create_lxc_container() { # ------------------------------------------------------------------------------ # Optional verbose mode (debug tracing) @@ -4469,14 +4573,6 @@ create_lxc_container() { fi msg_ok "Template storage '$TEMPLATE_STORAGE' validated" - # Free space check - STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') - REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024)) - [[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || { - msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G." - exit 214 - } - # Cluster quorum (if cluster) if [[ -f /etc/pve/corosync.conf ]]; then msg_info "Checking cluster quorum" @@ -4489,81 +4585,52 @@ create_lxc_container() { # ------------------------------------------------------------------------------ # Template discovery & validation - # Supported OS types (pveam available): alpine, almalinux, centos, debian, - # devuan, fedora, gentoo, openeuler, opensuse, rockylinux, ubuntu - # Template naming conventions: - # - Debian/Ubuntu/Devuan: --standard__.tar.zst - # - Alpine/Fedora/Rocky/CentOS/AlmaLinux/openEuler: --default__.tar.xz - # - Gentoo: gentoo-current-openrc__.tar.xz (note: underscore before date!) - # - openSUSE: opensuse--default__.tar.xz - # - CentOS: centos--stream-default__.tar.xz (note: stream in name) # ------------------------------------------------------------------------------ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" case "$PCT_OSTYPE" in - debian | ubuntu | devuan) TEMPLATE_PATTERN="-standard_" ;; - alpine | fedora | rockylinux | almalinux | openeuler) TEMPLATE_PATTERN="-default_" ;; - centos) TEMPLATE_PATTERN="-stream-default_" ;; - gentoo) TEMPLATE_PATTERN="-openrc_" ;; # Pattern: gentoo-current-openrc_ (underscore!) - opensuse) TEMPLATE_PATTERN="-default_" ;; + debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;; + alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;; *) TEMPLATE_PATTERN="" ;; esac msg_info "Searching for template '$TEMPLATE_SEARCH'" - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" + # Initialize variables + ONLINE_TEMPLATE="" + ONLINE_TEMPLATES=() + # Step 1: Check local templates first (instant) mapfile -t LOCAL_TEMPLATES < <( pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${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 ) - # Update template catalog with timeout to prevent hangs on slow networks - if command -v timeout &>/dev/null; then - if ! timeout 30 pveam update >/dev/null 2>&1; then - msg_warn "Template catalog update timed out or failed (continuing with cached data)" - fi - else - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - fi - - msg_ok "Template search completed" - - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" - set -u - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - + # Step 2: If local template found, use it immediately (skip pveam update) if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then TEMPLATE="${LOCAL_TEMPLATES[-1]}" TEMPLATE_SOURCE="local" + msg_ok "Template search completed" else + # Step 3: No local template - need to check online (this may be slow) + msg_info "No local template found, checking online catalog..." + + # Update catalog with timeout to prevent long hangs + if command -v timeout &>/dev/null; then + if ! timeout 30 pveam update >/dev/null 2>&1; then + msg_warn "Template catalog update timed out (possible network/DNS issue). Run 'pveam update' manually to diagnose." + fi + else + pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)" + fi + + ONLINE_TEMPLATES=() + mapfile -t ONLINE_TEMPLATES < <(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) + [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" + TEMPLATE="$ONLINE_TEMPLATE" TEMPLATE_SOURCE="online" + msg_ok "Template search completed" fi # If still no template, try to find alternatives @@ -4572,27 +4639,15 @@ create_lxc_container() { echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." # Get all available versions for this OS type - # Special handling for Gentoo which uses 'current' instead of numeric version - if [[ "$PCT_OSTYPE" == "gentoo" ]]; then - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk '{print $2}' | - grep "^gentoo-" | - sed -E 's/gentoo-([^-]+)-.*/\1/' | - sort -u 2>/dev/null || sort -u - ) - else - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk '{print $2}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - grep -E '^[0-9]' | - sort -u -V 2>/dev/null || sort -u - ) - fi + AVAILABLE_VERSIONS=() + mapfile -t AVAILABLE_VERSIONS < <( + pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk -F'\t' '{print $1}' | + grep "^${PCT_OSTYPE}-" | + sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | + sort -u -V 2>/dev/null + ) if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then echo "" @@ -4606,22 +4661,19 @@ 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}-" - - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" + ONLINE_TEMPLATES=() 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 ) if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then TEMPLATE="${ONLINE_TEMPLATES[-1]}" TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" else msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" exit 225 @@ -4636,9 +4688,6 @@ create_lxc_container() { fi fi - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" if [[ -z "$TEMPLATE_PATH" ]]; then TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) @@ -4679,18 +4728,17 @@ 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="" @@ -4804,7 +4852,6 @@ create_lxc_container() { if [[ "$PCT_OSTYPE" == "debian" ]]; then OSVER="$(parse_template_osver "$TEMPLATE")" if [[ -n "$OSVER" ]]; then - # Proactive, aber ohne Abbruch – nur Angebot offer_lxc_stack_upgrade_and_maybe_retry "no" || true fi fi @@ -4825,7 +4872,7 @@ create_lxc_container() { -rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}" fi - # Lock by template file (avoid concurrent downloads/creates) + # Lock by template file (avoid concurrent template downloads/validation) lockfile="/tmp/template.${TEMPLATE}.lock" # Cleanup stale lock files (older than 1 hour - likely from crashed processes) @@ -4859,84 +4906,54 @@ create_lxc_container() { LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" - # ------------------------------------------------------------------------------ - # openEuler Template Patch: Create /etc/redhat-release inside template - # PVE's post_create_hook expects this file for RHEL-family OS detection - # Without it, container creation fails with "error in setup task" - # ------------------------------------------------------------------------------ - if [[ "${var_os:-}" == "openeuler" ]]; then - msg_info "Patching openEuler template for PVE compatibility..." - local TEMP_EXTRACT_DIR="/tmp/openeuler_template_patch_$$" - local PATCHED_TEMPLATE="${TEMPLATE_PATH%.tar.xz}_patched.tar.xz" - - # Only patch if not already patched - if [[ ! -f "$PATCHED_TEMPLATE" ]]; then - mkdir -p "$TEMP_EXTRACT_DIR" - - # Extract template - if tar -xf "$TEMPLATE_PATH" -C "$TEMP_EXTRACT_DIR" 2>/dev/null; then - # Create /etc/redhat-release if it doesn't exist - if [[ ! -f "$TEMP_EXTRACT_DIR/etc/redhat-release" ]]; then - echo "openEuler release ${var_version:-25.03}" >"$TEMP_EXTRACT_DIR/etc/redhat-release" - fi - - # Repack template - if tar -cJf "$PATCHED_TEMPLATE" -C "$TEMP_EXTRACT_DIR" . 2>/dev/null; then - # Replace original with patched version - mv "$PATCHED_TEMPLATE" "$TEMPLATE_PATH" - msg_ok "openEuler template patched successfully" - else - msg_warn "Failed to repack template, trying without patch..." - fi - else - msg_warn "Failed to extract template for patching, trying without patch..." - fi - - rm -rf "$TEMP_EXTRACT_DIR" + # Validate template before pct create (while holding lock) + if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH" 2>/dev/null || echo 0)" -lt 1000000 ]]; then + msg_info "Template file missing or too small – downloading" + rm -f "$TEMPLATE_PATH" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template downloaded" + elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then + if [[ -n "$ONLINE_TEMPLATE" ]]; then + msg_info "Template appears corrupted – re-downloading" + rm -f "$TEMPLATE_PATH" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template re-downloaded" + else + msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." fi fi - # # DEBUG: Show the actual command that will be executed - # echo "[DEBUG] ===== PCT CREATE COMMAND DETAILS =====" - # echo "[DEBUG] CTID: $CTID" - # echo "[DEBUG] Template: ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" - # echo "[DEBUG] PCT_OPTIONS (will be word-split):" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Full command line:" - # echo " pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" - # echo "[DEBUG] ========================================" + # Release lock after template validation - pct create has its own internal locking + exec 9>&- msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" msg_debug "Logfile: $LOGFILE" # First attempt (PCT_OPTIONS is a multi-line string, use it directly) if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then - msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Validating template..." + msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..." - # Validate template file - if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - msg_warn "Template file too small or missing – re-downloading." + # Check if template issue - retry with fresh download + if grep -qiE 'unable to open|corrupt|invalid' "$LOGFILE"; then + msg_info "Template may be corrupted – re-downloading" rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - else - msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." - fi + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template re-downloaded" fi # Retry after repair if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then # Fallback to local storage if not already on local if [[ "$TEMPLATE_STORAGE" != "local" ]]; then - msg_info "Retrying container creation with fallback to local storage..." + msg_info "Retrying container creation with fallback to local storage" LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then - msg_info "Downloading template to local..." + msg_ok "Trying local storage fallback" + msg_info "Downloading template to local" pveam download local "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template downloaded to local" + else + msg_ok "Trying local storage fallback" fi if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then # Local fallback also failed - check for LXC stack version issue @@ -5060,15 +5077,15 @@ description() { - GitHub + Git - Discussions + Discussions - Issues + Issues EOF