diff --git a/scripts/core/build.func b/scripts/core/build.func index 9f59f50..c5fcf0f 100755 --- a/scripts/core/build.func +++ b/scripts/core/build.func @@ -217,22 +217,16 @@ update_motd_ip() { # Only update if file exists and is from community-scripts if [ -f "$PROFILE_FILE" ] && grep -q "community-scripts" "$PROFILE_FILE" 2>/dev/null; then # Get current values - local current_os="$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - Version: $(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')" local current_hostname="$(hostname)" local current_ip="$(hostname -I | awk '{print $1}')" # Escape sed special chars in replacement strings (& \ |) - current_os="${current_os//\\/\\\\}" - current_os="${current_os//&/\\&}" current_hostname="${current_hostname//\\/\\\\}" current_hostname="${current_hostname//&/\\&}" current_ip="${current_ip//\\/\\\\}" current_ip="${current_ip//&/\\&}" # Update only if values actually changed - if ! grep -q "OS:.*$current_os" "$PROFILE_FILE" 2>/dev/null; then - sed -i "s|OS:.*|OS: \${GN}$current_os\${CL}\\\"|" "$PROFILE_FILE" - fi if ! grep -q "Hostname:.*$current_hostname" "$PROFILE_FILE" 2>/dev/null; then sed -i "s|Hostname:.*|Hostname: \${GN}$current_hostname\${CL}\\\"|" "$PROFILE_FILE" fi @@ -513,7 +507,7 @@ validate_bridge() { [[ -z "$bridge" ]] && return 1 # Check if bridge interface exists - if ! ip link show "$bridge" &>/dev/null; then + if ! ip link show dev "$bridge" &>/dev/null; then return 1 fi @@ -979,7 +973,6 @@ base_settings() { fi IPV6_METHOD=${var_ipv6_method:-"none"} - IPV6_STATIC=${var_ipv6_static:-""} GATE=${var_gateway:-""} APT_CACHER=${var_apt_cacher:-""} APT_CACHER_IP=${var_apt_cacher_ip:-""} @@ -1015,8 +1008,12 @@ base_settings() { VLAN=${var_vlan:-""} SSH=${var_ssh:-"no"} SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""} - UDHCPC_FIX=${var_udhcpc_fix:-""} - TAGS="community-script,${var_tags:-}" + # Build TAGS: ensure community-script prefix, use semicolons (pct format), no duplicates + if [[ "${var_tags:-}" == *community-script* ]]; then + TAGS="${var_tags:-community-script}" + else + TAGS="community-script${var_tags:+;${var_tags}}" + fi ENABLE_FUSE=${var_fuse:-"${1:-no}"} ENABLE_TUN=${var_tun:-"${1:-no}"} @@ -1025,6 +1022,7 @@ base_settings() { ENABLE_NESTING=${var_nesting:-"1"} ENABLE_KEYCTL=${var_keyctl:-"0"} ENABLE_MKNOD=${var_mknod:-"0"} + ALLOW_MOUNT_FS=${var_mount_fs:-""} PROTECT_CT=${var_protection:-"no"} CT_TIMEZONE=${var_timezone:-"$timezone"} [[ "${CT_TIMEZONE:-}" == Etc/* ]] && CT_TIMEZONE="host" # pct doesn't accept Etc/* zones @@ -1058,6 +1056,7 @@ load_vars_file() { var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain + var_post_install ) # Whitelist check helper @@ -1203,6 +1202,22 @@ load_vars_file() { continue fi ;; + var_mknod) + if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then + msg_warn "Invalid mknod value '$var_val' in $file (must be 0 or 1), ignoring" + continue + fi + ;; + var_mount_fs) + # Normalize: strip spaces, trailing commas + var_val="${var_val// /}" + var_val="${var_val%%,}" + var_val="${var_val##,}" + if [[ -n "$var_val" ]] && [[ ! "$var_val" =~ ^[a-zA-Z0-9]+(,[a-zA-Z0-9]+)*$ ]]; then + msg_warn "Invalid mount_fs value '$var_val' in $file (comma-separated fs names only, e.g. nfs,cifs), ignoring" + continue + fi + ;; var_ipv6_method) if [[ "$var_val" != "auto" && "$var_val" != "dhcp" && "$var_val" != "static" && "$var_val" != "none" ]]; then msg_warn "Invalid IPv6 method '$var_val' in $file (must be auto/dhcp/static/none), ignoring" @@ -1259,6 +1274,7 @@ default_var_settings() { var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage + var_post_install ) # Snapshot: environment variables (highest precedence) @@ -1354,6 +1370,11 @@ var_verbose=no # GitHub Personal Access Token (optional – avoids API rate limits during installs) # Create at https://github.com/settings/tokens – read-only public access is sufficient # var_github_token=ghp_your_token_here + +# Optional post-install script (host-side path to a *.sh on the Proxmox host) +# Runs ON THE HOST after the container is fully provisioned. +# Available env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG +# var_post_install=/opt/post-install/myhook.sh EOF # Now choose storages (always prompt unless just one exists) @@ -1428,10 +1449,11 @@ 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_github_token var_gpu - var_gateway var_hostname var_ipv6_method var_mac var_mtu - var_net var_ns var_os var_pw var_ram var_tags var_tun var_unprivileged - var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token 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_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged + var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain + var_post_install ) fi @@ -1644,6 +1666,7 @@ _build_current_app_vars_tmp() { [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")" [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")" + [ -n "${var_post_install:-}" ] && echo "var_post_install=$(_sanitize_value "${var_post_install}")" } >"$tmpf" echo "$tmpf" @@ -1781,9 +1804,14 @@ advanced_settings() { trap 'tput rmcup 2>/dev/null || true' RETURN # Initialize defaults - TAGS="community-script;${var_tags:-}" + # Build TAGS: ensure community-script prefix, use semicolons (pct format), no duplicates + if [[ "${var_tags:-}" == *community-script* ]]; then + TAGS="${var_tags:-community-script}" + else + TAGS="community-script${var_tags:+;${var_tags}}" + fi local STEP=1 - local MAX_STEP=28 + local MAX_STEP=29 # Store values for back navigation - inherit from var_* app defaults local _ct_type="${var_unprivileged:-1}" @@ -1817,6 +1845,7 @@ advanced_settings() { local _enable_mknod="${var_mknod:-0}" local _mount_fs="${var_mount_fs:-}" local _protect_ct="${var_protection:-no}" + local _post_install="${var_post_install:-}" # Detect host timezone for default (if not set via var_timezone) local _host_timezone="" @@ -2518,6 +2547,13 @@ advanced_settings() { # STEP 22: Keyctl Support (Docker/systemd) # ═══════════════════════════════════════════════════════════════════════════ 22) + # Keyctl is always required for unprivileged containers — skip dialog + if [[ "$_ct_type" == "1" ]]; then + _enable_keyctl="1" + ((STEP++)) + continue + fi + local keyctl_default_flag="--defaultno" [[ "$_enable_keyctl" == "1" ]] && keyctl_default_flag="" @@ -2525,7 +2561,7 @@ advanced_settings() { --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 + --yesno "\nEnable Keyctl support?\n\nRequired for: Docker containers, systemd-networkd,\nand kernel keyring operations.\n\n(App default: ${var_keyctl:-0})" 14 62; then _enable_keyctl="1" else if [ $? -eq 1 ]; then @@ -2655,6 +2691,10 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nAllow specific filesystem mounts.\n\nComma-separated list: nfs, cifs, fuse, ext4, etc.\nLeave empty for defaults (none).\n\nCurrent: $mount_hint" 14 62 "$_mount_fs" \ 3>&1 1>&2 2>&3); then + # Normalize: strip spaces and trailing/leading commas + result="${result// /}" + result="${result%%,}" + result="${result##,}" _mount_fs="$result" ((STEP++)) else @@ -2663,9 +2703,61 @@ advanced_settings() { ;; # ═══════════════════════════════════════════════════════════════════════════ - # STEP 28: Verbose Mode & Confirmation + # STEP 28: Optional host-side post-install hook (path on the Proxmox HOST) # ═══════════════════════════════════════════════════════════════════════════ 28) + local _hook_prompt="Optional: absolute path to a *.sh file ON THE PROXMOX HOST. + +It runs as root on the HOST (NOT in the LXC) after the container +is fully provisioned and started. + +Available env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG. + +Leave empty to skip." + while true; do + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "POST-INSTALL HOOK (HOST)" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "$_hook_prompt" 16 70 "${_post_install}" \ + 3>&1 1>&2 2>&3); then + # Normalize: strip surrounding whitespace + result="$(printf '%s' "$result" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + if [[ -z "$result" ]]; then + _post_install="" + ((STEP++)) + break + fi + # Reject obvious shell-meta sneaking through + if [[ "$result" == *';'* || "$result" == *'$('* || "$result" == *'`'* || "$result" == *'&&'* || "$result" == *'||'* ]]; then + whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID PATH" \ + --msgbox "Path contains shell metacharacters. Please provide a plain absolute file path." 10 70 + continue + fi + if [[ "$result" != /* ]]; then + whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID PATH" \ + --msgbox "Path must be absolute (start with /).\n\nGot: $result" 10 70 + continue + fi + if [[ ! -f "$result" ]]; then + if ! whiptail --backtitle "Proxmox VE Helper Scripts" --title "FILE NOT FOUND" \ + --yesno "File does not exist on host:\n\n$result\n\nKeep this path anyway?" 12 70; then + continue + fi + fi + _post_install="$result" + ((STEP++)) + break + else + ((STEP--)) + break + fi + done + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 29: Verbose Mode & Confirmation + # ═══════════════════════════════════════════════════════════════════════════ + 29) local verbose_default_flag="--defaultno" [[ "$_verbose" == "yes" ]] && verbose_default_flag="" @@ -2694,6 +2786,11 @@ advanced_settings() { local apt_display="${_apt_cacher:-no}" [[ "$_apt_cacher" == "yes" && -n "$_apt_cacher_ip" ]] && apt_display="$_apt_cacher_ip" + local post_install_display="${_post_install:-(none)}" + local post_install_warn="" + [[ -n "$_post_install" ]] && post_install_warn=" + ⚠ Hook runs as root on Proxmox HOST (not in LXC)" + local summary="Container Type: $ct_type_desc Container ID: $_ct_id Hostname: $_hostname @@ -2711,12 +2808,14 @@ Network: Features: FUSE: $_enable_fuse | TUN: $_enable_tun Nesting: $nesting_desc | Keyctl: $keyctl_desc + Mknod: $([ "$_enable_mknod" == "1" ] && echo Enabled || echo Disabled) | Mount FS: ${_mount_fs:-(none)} GPU: $_enable_gpu | Protection: $protect_desc Advanced: Timezone: $tz_display APT Cacher: $apt_display - Verbose: $_verbose" + Verbose: $_verbose + Post-Install Script: ${post_install_display}${post_install_warn}" if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "CONFIRM SETTINGS" \ @@ -2759,6 +2858,7 @@ Advanced: APT_CACHER="$_apt_cacher" APT_CACHER_IP="$_apt_cacher_ip" VERBOSE="$_verbose" + var_post_install="$_post_install" # Update var_* based on user choice (for functions that check these) var_gpu="$_enable_gpu" @@ -2780,13 +2880,6 @@ Advanced: [[ -n "$_mac" ]] && MAC=",hwaddr=$_mac" || MAC="" [[ -n "$_vlan" ]] && VLAN=",tag=$_vlan" || VLAN="" - # Alpine UDHCPC fix - if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ -n "$_ns" ]; then - UDHCPC_FIX="yes" - else - UDHCPC_FIX="no" - fi - export UDHCPC_FIX export SSH_KEYS_FILE # Exit alternate screen buffer before showing summary (so output remains visible) @@ -2811,6 +2904,8 @@ Advanced: echo -e "${CONTAINERTYPE}${BOLD}${DGN}Nesting: ${BGN}$([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")${CL}" [[ "${ENABLE_KEYCTL:-0}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Keyctl: ${BGN}Enabled${CL}" echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}${ENABLE_GPU:-no}${CL}" + [[ "${ENABLE_MKNOD:-0}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Mknod: ${BGN}Enabled${CL}" + [[ -n "${ALLOW_MOUNT_FS:-}" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Mount FS: ${BGN}${ALLOW_MOUNT_FS}${CL}" [[ "${PROTECT_CT:-no}" == "yes" || "${PROTECT_CT:-no}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Protection: ${BGN}Enabled${CL}" [[ -n "${CT_TIMEZONE:-}" ]] && echo -e "${INFO}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}" [[ "$APT_CACHER" == "yes" ]] && echo -e "${INFO}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}" @@ -2833,6 +2928,8 @@ Advanced: log_msg "IPv6: $IPV6_METHOD" log_msg "FUSE Support: ${ENABLE_FUSE:-no}" log_msg "Nesting: $([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")" + log_msg "Mknod: $([ "${ENABLE_MKNOD:-0}" == "1" ] && echo "Enabled" || echo "Disabled")" + [[ -n "${ALLOW_MOUNT_FS:-}" ]] && log_msg "Mount FS: ${ALLOW_MOUNT_FS}" log_msg "GPU Passthrough: ${ENABLE_GPU:-no}" log_msg "Verbose Mode: $VERBOSE" log_msg "Session ID: ${SESSION_ID}" @@ -3196,6 +3293,10 @@ check_container_resources() { if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then msg_warn "Under-provisioned: Required ${var_cpu} CPU/${var_ram}MB RAM, Current ${current_cpu} CPU/${current_ram}MB RAM" echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n" + if is_unattended; then + msg_error "Aborted: under-provisioned LXC in unattended mode (${current_cpu} CPU/${current_ram}MB RAM < ${var_cpu} CPU/${var_ram}MB RAM)" + exit 113 + fi echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? " read -r prompt 80)); then msg_warn "Storage is dangerously low (${usage}% used on /boot)" + if is_unattended; then + msg_error "Aborted: storage too low in unattended mode (${usage}% used on /boot)" + exit 114 + fi echo -ne "Continue anyway? " read -r prompt /dev/null 2>&1; then install_script || return 0 return 0 @@ -3489,6 +3640,17 @@ start() { ensure_profile_loaded get_lxc_ip update_script + run_addon_updates + update_motd_ip + cleanup_lxc + elif ! command -v whiptail &>/dev/null || ! [ -t 0 ] || [[ "$TERM" == "dumb" ]]; then + msg_info "No interactive terminal detected – defaulting to silent update mode" + VERBOSE="no" + set_std_mode + ensure_profile_loaded + get_lxc_ip + update_script + run_addon_updates update_motd_ip cleanup_lxc else @@ -3517,6 +3679,7 @@ start() { ensure_profile_loaded get_lxc_ip update_script + run_addon_updates update_motd_ip cleanup_lxc fi @@ -3553,13 +3716,7 @@ build_container() { fi # IP (always required, default dhcp) - # Guard against the literal word "static" being passed as the IP value – - # that happens when var_net=static is exported without an actual address. - if [[ "$NET" == "static" || -z "$NET" ]]; then - msg_warn "NET value '${NET:-empty}' is not a valid IP/CIDR – falling back to dhcp" - NET="dhcp" - fi - NET_STRING+=",ip=${NET}" + NET_STRING+=",ip=${NET:-dhcp}" # Gateway if [[ -n "$GATE" ]]; then @@ -3618,6 +3775,26 @@ build_container() { FEATURES="${FEATURES}fuse=1" fi + # Mknod support (user configurable via advanced settings) + if [ "${ENABLE_MKNOD:-0}" == "1" ]; then + [ -n "$FEATURES" ] && FEATURES="$FEATURES," + FEATURES="${FEATURES}mknod=1" + fi + + # Mount filesystem types (user configurable via advanced settings) + if [ -n "${ALLOW_MOUNT_FS:-}" ]; then + # Sanitize: strip spaces, trailing/leading commas, then convert commas to semicolons + local _mount_clean="${ALLOW_MOUNT_FS// /}" + _mount_clean="${_mount_clean%%,}" + _mount_clean="${_mount_clean##,}" + _mount_clean="${_mount_clean%%;}" + _mount_clean="${_mount_clean//,/;}" + if [ -n "$_mount_clean" ]; then + [ -n "$FEATURES" ] && FEATURES="$FEATURES," + FEATURES="${FEATURES}mount=${_mount_clean}" + fi + fi + # Build PCT_OPTIONS as string for export local _func_file local SCRIPT_DIR @@ -3649,7 +3826,7 @@ build_container() { export FUNCTIONS_FILE_PATH="$(curl -fsSL "$_func_url")" fi if [[ -z "$FUNCTIONS_FILE_PATH" || ${#FUNCTIONS_FILE_PATH} -lt 100 ]]; then - msg_error "Failed to load install functions" + msg_error "Failed to download install functions from: $_func_url" exit 115 fi @@ -5234,15 +5411,185 @@ create_lxc_container() { # Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1" parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; } + # Switch to the previous OS major version template and retry pct create + # Determines the fallback version automatically based on available templates. + # Returns: 0 = success, 1 = failed + fallback_to_previous_os_version() { + local old_template="$TEMPLATE" + local os_type="${PCT_OSTYPE:-}" + local current_ver="${PCT_OSVERSION:-}" + + # Determine template search pattern based on OS type + local tpl_pattern="" + case "$os_type" in + debian | ubuntu) tpl_pattern="-standard_" ;; + alpine | fedora | rocky | centos) tpl_pattern="-default_" ;; + *) tpl_pattern="" ;; + esac + + msg_info "Searching for an older $os_type template (current: $os_type $current_ver)" + + # Collect all available versions for this OS type (local + online) + local -a all_versions=() + + # Local templates + mapfile -t _local_vers < <( + pveam list "$TEMPLATE_STORAGE" 2>/dev/null | + awk -v os="$os_type" -v pat="$tpl_pattern" '$1 ~ ("^"os"|/"os) && $1 ~ pat {print $1}' | + sed 's|.*/||' | + sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" | + sort -u -V + ) + all_versions+=("${_local_vers[@]}") + + # Online templates (only if needed) + if command -v timeout &>/dev/null; then + timeout 30 pveam update >/dev/null 2>&1 || true + else + pveam update >/dev/null 2>&1 || true + fi + mapfile -t _online_vers < <( + pveam available -section system 2>/dev/null | + awk '{print $2}' | + grep -E "^${os_type}-[0-9]" | + { [[ -n "$tpl_pattern" ]] && grep "$tpl_pattern" || cat; } | + sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" | + sort -u -V 2>/dev/null || true + ) + all_versions+=("${_online_vers[@]}") + + # Deduplicate and sort, find the highest version below current + local fallback_ver="" + fallback_ver=$(printf '%s\n' "${all_versions[@]}" | sort -u -V | awk -v cur="$current_ver" '{ + # Compare major versions: extract major part + split($0, a, ".") + split(cur, b, ".") + if (a[1]+0 < b[1]+0) ver=$0 + } END { if (ver) print ver }') + + if [[ -z "$fallback_ver" ]]; then + msg_error "No older $os_type template version found." + return 1 + fi + + msg_ok "Fallback version: $os_type $fallback_ver" + + # Find the actual template file for this version + local fallback_search="${os_type}-${fallback_ver}" + local fallback_template="" + + # Check local first + mapfile -t _fb_local < <( + pveam list "$TEMPLATE_STORAGE" 2>/dev/null | + awk -v search="$fallback_search" -v pat="$tpl_pattern" '$1 ~ search && $1 ~ pat {print $1}' | + sed 's|.*/||' | sort -t - -k 2 -V + ) + if [[ ${#_fb_local[@]} -gt 0 ]]; then + fallback_template="${_fb_local[-1]}" + else + # Check online + mapfile -t _fb_online < <( + pveam available -section system 2>/dev/null | + awk '{print $2}' | + grep -E "^${fallback_search}.*${tpl_pattern}" | + sort -t - -k 2 -V 2>/dev/null || true + ) + [[ ${#_fb_online[@]} -gt 0 ]] && fallback_template="${_fb_online[-1]}" + fi + + if [[ -z "$fallback_template" ]]; then + msg_error "No template found for $os_type $fallback_ver." + return 1 + fi + + msg_ok "Found template: $fallback_template" + + # Download if needed + local fallback_path + fallback_path="$(pvesm path "$TEMPLATE_STORAGE:vztmpl/$fallback_template" 2>/dev/null || true)" + [[ -z "$fallback_path" ]] && fallback_path="/var/lib/vz/template/cache/$fallback_template" + + if [[ ! -f "$fallback_path" ]]; then + msg_info "Downloading $os_type $fallback_ver template" + if ! pveam download "$TEMPLATE_STORAGE" "$fallback_template" >>"${BUILD_LOG:-/dev/null}" 2>&1; then + msg_error "Failed to download $os_type $fallback_ver template." + return 1 + fi + msg_ok "Template downloaded" + fi + + # Update variables + TEMPLATE="$fallback_template" + TEMPLATE_PATH="$fallback_path" + PCT_OSVERSION="$fallback_ver" + export PCT_OSVERSION + + # Retry pct create + msg_info "Retrying container creation with $os_type $fallback_ver" + if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then + msg_ok "Container created successfully with $os_type $fallback_ver (fallback from $old_template)." + return 0 + else + msg_error "Container creation with $os_type $fallback_ver also failed. See $LOGFILE" + return 1 + fi + } + # Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create # Returns: - # 0 = no upgrade needed + # 0 = no upgrade needed / container created after upgrade or explicit fallback # 1 = upgraded (and if do_retry=yes and retry succeeded, creation done) - # 2 = user declined + # 2 = user chose ignore # 3 = upgrade attempted but failed OR retry failed + # 4 = user cancelled offer_lxc_stack_upgrade_and_maybe_retry() { local do_retry="${1:-no}" # yes|no local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0 + local _ans + + has_previous_os_version_template() { + local os_type="${PCT_OSTYPE:-}" + local current_ver="${PCT_OSVERSION:-}" + local tpl_pattern="${TEMPLATE_PATTERN:-${TEMPLATE:-}}" + local -a all_versions=() + + [[ -n "$os_type" && -n "$current_ver" ]] || return 1 + + mapfile -t _local_vers < <( + pveam list "$TEMPLATE_STORAGE" 2>/dev/null | + awk '{print $1}' | + sed 's|.*/||' | + grep -E "^${os_type}-[0-9]" | + { [[ -n "$tpl_pattern" ]] && grep "$tpl_pattern" || cat; } | + sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" | + sort -u -V + ) + all_versions+=("${_local_vers[@]}") + + if command -v timeout &>/dev/null; then + timeout 30 pveam update >/dev/null 2>&1 || true + else + pveam update >/dev/null 2>&1 || true + fi + mapfile -t _online_vers < <( + pveam available -section system 2>/dev/null | + awk '{print $2}' | + grep -E "^${os_type}-[0-9]" | + { [[ -n "$tpl_pattern" ]] && grep "$tpl_pattern" || cat; } | + sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" | + sort -u -V 2>/dev/null || true + ) + all_versions+=("${_online_vers[@]}") + + printf '%s\n' "${all_versions[@]}" | sort -u -V | awk -v cur="$current_ver" ' + { + split($0, a, ".") + split(cur, b, ".") + if (a[1]+0 < b[1]+0) found=1 + } + END { exit found ? 0 : 1 } + ' + } _pvec_i="$(pkg_ver pve-container)" _lxcp_i="$(pkg_ver lxc-pve)" @@ -5263,14 +5610,78 @@ create_lxc_container() { msg_warn "An update for the Proxmox LXC stack is available" echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}" echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}" + echo " note : option 1 runs host apt update + apt upgrade" echo - read -rp "Do you want to upgrade now? [y/N] " _ans /dev/null; then + msg_error "LXC stack upgrade caused PVE tool breakage (likely Perl module incompatibility)." + msg_custom "⚠️" "${YW}" "A partial package upgrade has left the PVE stack in an inconsistent state." + msg_custom "🔧" "${YW}" "Please run the following on the Proxmox host, then retry:" + echo -e "${TAB} apt update && apt upgrade -y" + echo -e "${TAB} reboot" + return 3 + fi if [[ "$do_retry" == "yes" ]]; then msg_info "Retrying container creation after upgrade" if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then @@ -5369,10 +5780,35 @@ create_lxc_container() { fi msg_info "Validating storage '$CONTAINER_STORAGE'" - STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 | head -1 || true) + # Check if storage.cfg is accessible (pmxcfs must be mounted) + if [[ ! -f /etc/pve/storage.cfg ]]; then + if ! mountpoint -q /etc/pve 2>/dev/null; then + msg_error "Proxmox cluster filesystem (pmxcfs) is not mounted at /etc/pve." + msg_custom "🔧" "${YW}" "Try: systemctl restart pve-cluster" + else + msg_error "/etc/pve/storage.cfg does not exist." + msg_custom "🔧" "${YW}" "Check Proxmox cluster filesystem integrity: pvecm status" + fi + exit 213 + fi + + STORAGE_TYPE=$(grep -E "^[^:]+:[[:space:]]*$CONTAINER_STORAGE[[:space:]]*$" /etc/pve/storage.cfg | cut -d: -f1 | head -1 || true) + + # Fallback: use pvesm status to determine storage type + if [[ -z "$STORAGE_TYPE" ]]; then + STORAGE_TYPE=$(pvesm status -storage "$CONTAINER_STORAGE" 2>/dev/null | awk 'NR>1{print $2}') + fi if [[ -z "$STORAGE_TYPE" ]]; then msg_error "Storage '$CONTAINER_STORAGE' not found in /etc/pve/storage.cfg" + msg_custom "📋" "${YW}" "Available storages: $(pvesm status 2>/dev/null | awk 'NR>1{printf "%s (%s) ", $1, $2}' || echo 'n/a')" + if [[ -r /etc/pve/storage.cfg ]]; then + msg_custom "📋" "${YW}" "Storage definitions found in config:" + grep -E '^[a-z]+:' /etc/pve/storage.cfg 2>/dev/null | while IFS= read -r _line; do + echo "${TAB} $_line" + done + fi + msg_custom "📖" "${YW}" "See https://pve.proxmox.com/wiki/Storage for storage configuration details." exit 213 fi @@ -5403,6 +5839,10 @@ create_lxc_container() { if ! pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE"; then msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support 'rootdir' content." + msg_custom "💡" "${YW}" "Enable 'Disk image' (rootdir) for storage '${CONTAINER_STORAGE}' in:" + msg_custom " " "${YW}" "Datacenter → Storage → ${CONTAINER_STORAGE} → Edit → Content" + msg_custom "📖" "${YW}" "See: https://pve.proxmox.com/wiki/Storage" + msg_custom "🔗" "${YW}" "Help: https://github.com/community-scripts/ProxmoxVE/discussions" exit 213 fi msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated" @@ -5789,9 +6229,23 @@ create_lxc_container() { msg_debug "Logfile: $LOGFILE" # First attempt (PCT_OPTIONS is a multi-line string, use it directly) + # Disable globbing: unquoted $PCT_OPTIONS needs word-splitting but must not glob-expand + # (e.g. passwords containing * or ? would match filenames otherwise) + set -f if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..." + # Check for Perl module breakage (partial PVE upgrade) + if grep -qiE 'Compilation failed|Bareword.*not allowed' "$LOGFILE"; then + msg_error "Container creation failed due to broken Perl modules on the PVE host." + msg_custom "⚠️" "${YW}" "This usually happens after a partial PVE package upgrade." + msg_custom "🔧" "${YW}" "Please run the following on the Proxmox host, then retry:" + echo -e "${TAB} apt update && apt dist-upgrade -y" + echo -e "${TAB} reboot" + _flush_pct_log + exit 232 + fi + # Check if CTID collision (race condition: ID claimed between validation and creation) if grep -qiE 'already exists|already in use' "$LOGFILE"; then local old_ctid="$CTID" @@ -5838,12 +6292,17 @@ create_lxc_container() { case $rc in 0) : ;; # success - container created, continue 2) - msg_error "Upgrade declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve" + msg_error "LXC stack upgrade ignored. Please inspect: $LOGFILE" _flush_pct_log exit 231 ;; 3) - msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE" + msg_error "LXC stack upgrade failed. Please inspect: $LOGFILE" + _flush_pct_log + exit 231 + ;; + 4) + msg_error "Cancelled by user." _flush_pct_log exit 231 ;; @@ -5870,12 +6329,17 @@ create_lxc_container() { case $rc in 0) : ;; # success - container created, continue 2) - msg_error "Upgrade declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve" + msg_error "LXC stack upgrade ignored. Please inspect: $LOGFILE" _flush_pct_log exit 231 ;; 3) - msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE" + msg_error "LXC stack upgrade failed. Please inspect: $LOGFILE" + _flush_pct_log + exit 231 + ;; + 4) + msg_error "Cancelled by user." _flush_pct_log exit 231 ;; @@ -5896,6 +6360,7 @@ create_lxc_container() { fi fi # close CTID collision else-branch fi + set +f # re-enable globbing after pct create block # Verify container exists (allow up to 10s for pmxcfs sync in clusters) local _pct_visible=false @@ -6001,6 +6466,40 @@ EOF systemctl start ping-instances.service fi + # Optional host-side post-install hook + # Path comes from var_post_install (default.vars / app.vars / advanced settings). + # Runs ON THE PROXMOX HOST after the container is up and configured. + # Exposed env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG. + # Output (stdout/stderr) is captured to /var/log/community-scripts/post-install-.log + if [[ -n "${var_post_install:-}" ]]; then + local _hook_log_dir="/var/log/community-scripts" + local _hook_log="${_hook_log_dir}/post-install-${CTID}.log" + mkdir -p "$_hook_log_dir" 2>/dev/null || true + + if [[ ! -f "${var_post_install}" ]]; then + msg_error "Post-install hook not found on host: ${var_post_install}" + whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "POST-INSTALL HOOK FAILED" \ + --msgbox "The configured post-install hook was not found on the Proxmox host:\n\n${var_post_install}\n\nThe LXC was created successfully, but the hook did NOT run." 14 72 || true + else + msg_info "Running post-install hook: ${var_post_install}" + local _hook_rc=0 + APP="$APP" NSAPP="${NSAPP:-}" CTID="$CTID" IP="$IP" HN="${HN:-}" \ + STORAGE="${STORAGE:-}" BRG="${BRG:-}" \ + bash "${var_post_install}" >"${_hook_log}" 2>&1 || _hook_rc=$? + if [[ $_hook_rc -eq 0 ]]; then + msg_ok "Post-install hook completed (log: ${_hook_log})" + else + msg_error "Post-install hook failed (rc=${_hook_rc}) – see ${_hook_log}" + local _hook_tail="" + _hook_tail="$(tail -n 15 "${_hook_log}" 2>/dev/null || true)" + whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "POST-INSTALL HOOK FAILED" \ + --msgbox "Hook exited with code ${_hook_rc}.\n\nScript: ${var_post_install}\nLog: ${_hook_log}\n\n--- Last log lines ---\n${_hook_tail}\n\nThe LXC itself was created successfully." 22 78 || true + fi + fi + fi + INSTALL_COMPLETE=true post_update_to_api "done" "none" }