From 5f5dc171beb906d7f308f84c96f7be60e0ae8b44 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Wed, 1 Apr 2026 23:09:51 +0200 Subject: [PATCH] update scripts gpu --- scripts/gpu_tpu/add_gpu_lxc.sh | 638 ++++++++++ scripts/gpu_tpu/install_coral_lxc.sh | 102 +- scripts/gpu_tpu/install_coral_pve9.sh | 88 +- scripts/gpu_tpu/nvidia_update.sh | 494 ++++++++ scripts/menus/hw_grafics_menu.sh | 84 +- scripts/menus/share_menu.sh | 10 +- scripts/share/disk_host.sh | 656 ++++++++++ scripts/share/iscsi_host.sh | 518 ++++++++ scripts/share/lxc-mount-manager_minimal.sh | 1327 +++++++------------ scripts/share/nfs_client.sh | 2 +- scripts/share/nfs_host.sh | 875 +++++-------- scripts/share/samba_client.sh | 4 +- scripts/share/samba_host.sh | 1331 +++++--------------- 13 files changed, 3532 insertions(+), 2597 deletions(-) create mode 100644 scripts/gpu_tpu/add_gpu_lxc.sh create mode 100644 scripts/gpu_tpu/nvidia_update.sh create mode 100644 scripts/share/disk_host.sh create mode 100644 scripts/share/iscsi_host.sh diff --git a/scripts/gpu_tpu/add_gpu_lxc.sh b/scripts/gpu_tpu/add_gpu_lxc.sh new file mode 100644 index 00000000..a57af109 --- /dev/null +++ b/scripts/gpu_tpu/add_gpu_lxc.sh @@ -0,0 +1,638 @@ +#!/bin/bash +# ProxMenux - Universal GPU/iGPU Passthrough to LXC +# ================================================== +# Author : MacRimi +# License : MIT +# Version : 1.0 +# Last Updated: 01/04/2026 +# ================================================== + +LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" +BASE_DIR="/usr/local/share/proxmenux" +UTILS_FILE="$BASE_DIR/utils.sh" +LOG_FILE="/tmp/add_gpu_lxc.log" +NVIDIA_WORKDIR="/opt/nvidia" +INSTALL_ABORTED=false +NVIDIA_INSTALL_SUCCESS=false +NVIDIA_SMI_OUTPUT="" +screen_capture="/tmp/proxmenux_add_gpu_screen_capture_$$.txt" + +if [[ -f "$UTILS_FILE" ]]; then + source "$UTILS_FILE" +fi + +load_language +initialize_cache + + +# ============================================================ +# Helper: next available devN index in LXC config +# ============================================================ +get_next_dev_index() { + local config="$1" + local idx=0 + while grep -q "^dev${idx}:" "$config" 2>/dev/null; do + idx=$((idx + 1)) + done + echo "$idx" +} + + +# ============================================================ +# GPU detection on host +# ============================================================ +detect_host_gpus() { + HAS_INTEL=false + HAS_AMD=false + HAS_NVIDIA=false + NVIDIA_READY=false + NVIDIA_HOST_VERSION="" + INTEL_NAME="" + AMD_NAME="" + NVIDIA_NAME="" + + local intel_line amd_line nvidia_line + intel_line=$(lspci | grep -iE "VGA compatible|3D controller|Display controller" \ + | grep -i "Intel" | grep -iv "Ethernet\|Audio\|Network" | head -1) + amd_line=$(lspci | grep -iE "VGA compatible|3D controller|Display controller" \ + | grep -iE "AMD|Advanced Micro|Radeon" | head -1) + nvidia_line=$(lspci | grep -iE "VGA compatible|3D controller|Display controller" \ + | grep -i "NVIDIA" | head -1) + + if [[ -n "$intel_line" ]]; then + HAS_INTEL=true + INTEL_NAME=$(echo "$intel_line" | sed 's/^.*: //' | cut -c1-58) + fi + if [[ -n "$amd_line" ]]; then + HAS_AMD=true + AMD_NAME=$(echo "$amd_line" | sed 's/^.*: //' | cut -c1-58) + fi + if [[ -n "$nvidia_line" ]]; then + HAS_NVIDIA=true + NVIDIA_NAME=$(echo "$nvidia_line" | sed 's/^.*: //' | cut -c1-58) + if lsmod | grep -q "^nvidia " && command -v nvidia-smi >/dev/null 2>&1; then + NVIDIA_HOST_VERSION=$(nvidia-smi --query-gpu=driver_version \ + --format=csv,noheader 2>/dev/null | head -n1 | tr -d '[:space:]') + [[ -n "$NVIDIA_HOST_VERSION" ]] && NVIDIA_READY=true + fi + fi +} + + +# ============================================================ +# Container selection +# ============================================================ +select_container() { + local menu_items=() + while IFS= read -r line; do + [[ "$line" =~ ^VMID ]] && continue + local ctid status name + ctid=$(echo "$line" | awk '{print $1}') + status=$(echo "$line" | awk '{print $2}') + name=$(echo "$line" | awk '{print $3}') + [[ -z "$ctid" ]] && continue + menu_items+=("$ctid" "${name:-CT-${ctid}} (${status})") + done < <(pct list 2>/dev/null) + + if [[ ${#menu_items[@]} -eq 0 ]]; then + dialog --backtitle "ProxMenux" \ + --title "$(translate 'Add GPU to LXC')" \ + --msgbox "\n$(translate 'No LXC containers found on this system.')" 8 60 + exit 0 + fi + + CONTAINER_ID=$(dialog --backtitle "ProxMenux" \ + --title "$(translate 'Add GPU to LXC')" \ + --menu "\n$(translate 'Select the LXC container:')" 20 72 12 \ + "${menu_items[@]}" \ + 2>&1 >/dev/tty) || exit 0 +} + + +# ============================================================ +# GPU checklist selection +# ============================================================ +select_gpus() { + local gpu_items=() + $HAS_INTEL && gpu_items+=("intel" "${INTEL_NAME:-Intel iGPU}" "off") + $HAS_AMD && gpu_items+=("amd" "${AMD_NAME:-AMD GPU}" "off") + $HAS_NVIDIA && gpu_items+=("nvidia" "${NVIDIA_NAME:-NVIDIA GPU}" "off") + + local count=$(( ${#gpu_items[@]} / 3 )) + + if [[ $count -eq 0 ]]; then + dialog --backtitle "ProxMenux" \ + --title "$(translate 'Add GPU to LXC')" \ + --msgbox "\n$(translate 'No compatible GPUs detected on this host.')" 8 60 + exit 0 + fi + + # Only one GPU — auto-select without menu + if [[ $count -eq 1 ]]; then + SELECTED_GPUS=("${gpu_items[0]}") + return + fi + + # Multiple GPUs — checklist with loop on empty selection + while true; do + local raw_selection + raw_selection=$(dialog --backtitle "ProxMenux" \ + --title "$(translate 'Add GPU to LXC')" \ + --checklist "\n$(translate 'Select the GPU(s) to add to LXC') ${CONTAINER_ID}:" \ + 18 80 10 \ + "${gpu_items[@]}" \ + 2>&1 >/dev/tty) || exit 0 + + local selection + selection=$(echo "$raw_selection" | tr -d '"') + + if [[ -z "$selection" ]]; then + dialog --backtitle "ProxMenux" \ + --title "$(translate 'Add GPU to LXC')" \ + --msgbox "\n$(translate 'No GPU selected. Please select at least one GPU to continue.')" 8 68 + continue + fi + + read -ra SELECTED_GPUS <<< "$selection" + break + done +} + + +# ============================================================ +# NVIDIA host driver readiness check +# ============================================================ +check_nvidia_ready() { + if ! $NVIDIA_READY; then + dialog --colors --backtitle "ProxMenux" \ + --title "$(translate 'NVIDIA Drivers Not Found')" \ + --msgbox "\n$(translate 'NVIDIA drivers are not installed or not loaded on this host.')\n\n$(translate 'Please install the NVIDIA drivers first using the option:')\n\n \Zb$(translate 'Install NVIDIA Drivers on Host')\Zn\n\n$(translate 'available in this same GPU and TPU menu.')" \ + 14 72 + exit 0 + fi +} + + +# ============================================================ +# LXC config: DRI device passthrough (Intel / AMD shared) +# ============================================================ +_configure_dri_devices() { + local cfg="$1" + local video_gid render_gid idx gid + + video_gid=$(getent group video 2>/dev/null | cut -d: -f3); [[ -z "$video_gid" ]] && video_gid="44" + render_gid=$(getent group render 2>/dev/null | cut -d: -f3); [[ -z "$render_gid" ]] && render_gid="104" + + # Remove any pre-existing lxc.mount.entry for /dev/dri — it conflicts with devN: entries + sed -i '/lxc\.mount\.entry:.*dev\/dri.*bind/d' "$cfg" 2>/dev/null || true + + for dri_dev in /dev/dri/card0 /dev/dri/card1 /dev/dri/renderD128 /dev/dri/renderD129; do + [[ ! -c "$dri_dev" ]] && continue + if ! grep -qE "dev[0-9]+:.*${dri_dev}[^0-9/]" "$cfg" 2>/dev/null; then + idx=$(get_next_dev_index "$cfg") + case "$dri_dev" in + /dev/dri/renderD*) gid="$render_gid" ;; + *) gid="$video_gid" ;; + esac + echo "dev${idx}: ${dri_dev},gid=${gid}" >> "$cfg" + fi + done +} + +_configure_intel() { + local cfg="$1" + _configure_dri_devices "$cfg" +} + +_configure_amd() { + local cfg="$1" + local render_gid idx + + _configure_dri_devices "$cfg" + + # /dev/kfd for ROCm / compute workloads + if [[ -c "/dev/kfd" ]]; then + render_gid=$(getent group render 2>/dev/null | cut -d: -f3) + [[ -z "$render_gid" ]] && render_gid="104" + if ! grep -q "dev.*/dev/kfd" "$cfg" 2>/dev/null; then + idx=$(get_next_dev_index "$cfg") + echo "dev${idx}: /dev/kfd,gid=${render_gid}" >> "$cfg" + fi + fi +} + +_configure_nvidia() { + local cfg="$1" + local idx dev video_gid + + video_gid=$(getent group video 2>/dev/null | cut -d: -f3) + [[ -z "$video_gid" ]] && video_gid="44" + + local -a nv_devs=() + for dev in /dev/nvidia[0-9]* /dev/nvidiactl /dev/nvidia-uvm /dev/nvidia-uvm-tools /dev/nvidia-modeset; do + [[ -c "$dev" ]] && nv_devs+=("$dev") + done + if [[ -d /dev/nvidia-caps ]]; then + for dev in /dev/nvidia-caps/nvidia-cap[0-9]*; do + [[ -c "$dev" ]] && nv_devs+=("$dev") + done + fi + + for dev in "${nv_devs[@]}"; do + if ! grep -q "dev.*${dev}" "$cfg" 2>/dev/null; then + idx=$(get_next_dev_index "$cfg") + echo "dev${idx}: ${dev},gid=${video_gid}" >> "$cfg" + fi + done +} + +configure_passthrough() { + local ctid="$1" + local cfg="/etc/pve/lxc/${ctid}.conf" + CT_WAS_RUNNING=false + + if pct status "$ctid" 2>/dev/null | grep -q "running"; then + CT_WAS_RUNNING=true + msg_info "$(translate 'Stopping container') ${ctid}..." + pct stop "$ctid" >>"$LOG_FILE" 2>&1 + msg_ok "$(translate 'Container stopped.')" | tee -a "$screen_capture" + fi + + for gpu_type in "${SELECTED_GPUS[@]}"; do + case "$gpu_type" in + intel) + msg_info "$(translate 'Configuring Intel iGPU passthrough...')" + _configure_intel "$cfg" + msg_ok "$(translate 'Intel iGPU passthrough configured.')" | tee -a "$screen_capture" + ;; + amd) + msg_info "$(translate 'Configuring AMD GPU passthrough...')" + _configure_amd "$cfg" + msg_ok "$(translate 'AMD GPU passthrough configured.')" | tee -a "$screen_capture" + ;; + nvidia) + msg_info "$(translate 'Configuring NVIDIA GPU passthrough...')" + _configure_nvidia "$cfg" + msg_ok "$(translate 'NVIDIA GPU passthrough configured.')" | tee -a "$screen_capture" + ;; + esac + done +} + + +# ============================================================ +# Driver / userspace library installation inside container +# ============================================================ +# ============================================================ +# Detect distro inside container (POSIX sh — works on Alpine too) +# ============================================================ +_detect_container_distro() { + local distro + distro=$(pct exec "$1" -- grep "^ID=" /etc/os-release 2>/dev/null \ + | cut -d= -f2 | tr -d '[:space:]"') + echo "${distro:-unknown}" +} + +# ============================================================ +# GID sync helper — POSIX sh, works on all distros +# ============================================================ +_sync_gids_in_container() { + local ctid="$1" + local hvid hrid + hvid=$(getent group video 2>/dev/null | cut -d: -f3); [[ -z "$hvid" ]] && hvid="44" + hrid=$(getent group render 2>/dev/null | cut -d: -f3); [[ -z "$hrid" ]] && hrid="104" + + pct exec "$ctid" -- sh -c " + sed -i 's/^video:x:[0-9]*:/video:x:${hvid}:/' /etc/group 2>/dev/null || true + sed -i 's/^render:x:[0-9]*:/render:x:${hrid}:/' /etc/group 2>/dev/null || true + grep -q '^video:' /etc/group 2>/dev/null || echo 'video:x:${hvid}:' >> /etc/group + grep -q '^render:' /etc/group 2>/dev/null || echo 'render:x:${hrid}:' >> /etc/group + " >>"$LOG_FILE" 2>&1 || true +} + +# ============================================================ +_install_intel_drivers() { + local ctid="$1" + local distro="$2" + + _sync_gids_in_container "$ctid" + + case "$distro" in + alpine) + pct exec "$ctid" -- sh -c \ + "apk update && apk add --no-cache mesa-va-gallium libva-utils" \ + 2>&1 | tee -a "$LOG_FILE" + ;; + arch|manjaro|endeavouros) + pct exec "$ctid" -- bash -c \ + "pacman -Sy --noconfirm intel-media-driver libva-utils mesa" \ + 2>&1 | tee -a "$LOG_FILE" + ;; + *) + pct exec "$ctid" -- bash -s >>"$LOG_FILE" 2>&1 << EOF +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y va-driver-all vainfo libva2 intel-media-va-driver-non-free i965-va-driver 2>/dev/null || \ +apt-get install -y va-driver-all vainfo libva2 2>/dev/null || true +EOF + ;; + esac +} + +_install_amd_drivers() { + local ctid="$1" + local distro="$2" + + _sync_gids_in_container "$ctid" + + case "$distro" in + alpine) + pct exec "$ctid" -- sh -c \ + "apk update && apk add --no-cache mesa-va-gallium mesa-dri-gallium libva-utils" \ + 2>&1 | tee -a "$LOG_FILE" + ;; + arch|manjaro|endeavouros) + pct exec "$ctid" -- bash -c \ + "pacman -Sy --noconfirm mesa libva-mesa-driver libva-utils" \ + 2>&1 | tee -a "$LOG_FILE" + ;; + *) + pct exec "$ctid" -- bash -s >>"$LOG_FILE" 2>&1 << EOF +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y mesa-va-drivers libdrm-amdgpu1 vainfo libva2 2>/dev/null || true +EOF + ;; + esac +} + +# ============================================================ +# Memory management helpers (for NVIDIA .run installer) +# ============================================================ +CT_ORIG_MEM="" +NVIDIA_INSTALL_MIN_MB=2048 + +_ensure_container_memory() { + local ctid="$1" + local cur_mem + cur_mem=$(pct config "$ctid" 2>/dev/null | awk '/^memory:/{print $2}') + [[ -z "$cur_mem" ]] && cur_mem=512 + + if [[ "$cur_mem" -lt "$NVIDIA_INSTALL_MIN_MB" ]]; then + if whiptail --title "$(translate 'Low Container Memory')" --yesno \ + "$(translate 'Container') ${ctid} $(translate 'has') ${cur_mem}MB RAM.\n\n$(translate 'The NVIDIA installer needs at least') ${NVIDIA_INSTALL_MIN_MB}MB $(translate 'to run without being killed by the OOM killer.')\n\n$(translate 'Increase container RAM temporarily to') ${NVIDIA_INSTALL_MIN_MB}MB?" \ + 13 72; then + CT_ORIG_MEM="$cur_mem" + pct set "$ctid" -memory "$NVIDIA_INSTALL_MIN_MB" >>"$LOG_FILE" 2>&1 || true + else + INSTALL_ABORTED=true + msg_warn "$(translate 'Insufficient memory. NVIDIA install aborted.')" + return 1 + fi + fi + return 0 +} + +_restore_container_memory() { + local ctid="$1" + if [[ -n "$CT_ORIG_MEM" ]]; then + msg_info "$(translate 'Restoring container memory to') ${CT_ORIG_MEM}MB..." + pct set "$ctid" -memory "$CT_ORIG_MEM" >>"$LOG_FILE" 2>&1 || true + msg_ok "$(translate 'Memory restored.')" + CT_ORIG_MEM="" + fi +} + +# ============================================================ +_install_nvidia_drivers() { + local ctid="$1" + local version="$NVIDIA_HOST_VERSION" + local distro="$2" + + case "$distro" in + alpine) + # Alpine uses apk — musl-compatible nvidia-utils from Alpine repos + msg_info2 "$(translate 'Installing NVIDIA utils (Alpine)...')" + pct exec "$ctid" -- sh -c \ + "apk update && apk add --no-cache nvidia-utils" \ + 2>&1 | tee -a "$LOG_FILE" + ;; + + arch|manjaro|endeavouros) + # Arch uses pacman — nvidia-utils provides nvidia-smi + msg_info2 "$(translate 'Installing NVIDIA utils (Arch)...')" + pct exec "$ctid" -- bash -c \ + "pacman -Sy --noconfirm nvidia-utils" \ + 2>&1 | tee -a "$LOG_FILE" + ;; + + *) + # Debian / Ubuntu / generic glibc: use the .run binary + local run_file="${NVIDIA_WORKDIR}/NVIDIA-Linux-x86_64-${version}.run" + + if [[ ! -f "$run_file" ]]; then + msg_warn "$(translate 'NVIDIA installer not found at') ${run_file}." + msg_warn "$(translate 'Run \"Install NVIDIA Drivers on Host\" first so the installer is cached.')" + return 1 + fi + + # Memory check — nvidia-installer needs ~2GB during install + _ensure_container_memory "$ctid" || return 1 + + # Disk space check — NVIDIA libs need ~1.5 GB free in the container + local free_mb + free_mb=$(pct exec "$ctid" -- df -m / 2>/dev/null | awk 'NR==2{print $4}' || echo 0) + if [[ "$free_mb" -lt 1500 ]]; then + _restore_container_memory "$ctid" + dialog --backtitle "ProxMenux" \ + --title "$(translate 'Insufficient Disk Space')" \ + --msgbox "\n$(translate 'Container') ${ctid} $(translate 'has only') ${free_mb}MB $(translate 'of free disk space.')\n\n$(translate 'NVIDIA libs require approximately 1.5GB of free space.')\n\n$(translate 'Please expand the container disk and run this option again.')" \ + 12 72 + INSTALL_ABORTED=true + return 1 + fi + + # Extract .run on the host — avoids decompression OOM inside container + # Use msg_info2 (no spinner) so tee output is not mixed with spinner animation + local extract_dir="${NVIDIA_WORKDIR}/extracted_${version}" + local archive="/tmp/nvidia_lxc_${version}.tar.gz" + + msg_info2 "$(translate 'Extracting NVIDIA installer on host...')" + rm -rf "$extract_dir" + sh "$run_file" --extract-only --target "$extract_dir" 2>&1 | tee -a "$LOG_FILE" + if [[ ${PIPESTATUS[0]} -ne 0 ]]; then + msg_warn "$(translate 'Extraction failed. Check log:') ${LOG_FILE}" + _restore_container_memory "$ctid" + return 1 + fi + msg_ok "$(translate 'NVIDIA installer extracted.')" | tee -a "$screen_capture" + + msg_info2 "$(translate 'Packing installer archive...')" + tar --checkpoint=5000 --checkpoint-action=dot \ + -czf "$archive" -C "$extract_dir" . 2>&1 | tee -a "$LOG_FILE" + echo "" + local archive_size + archive_size=$(du -sh "$archive" 2>/dev/null | cut -f1) + msg_ok "$(translate 'Archive ready') (${archive_size})." | tee -a "$screen_capture" + + msg_info "$(translate 'Copying installer to container') ${ctid}..." + if ! pct push "$ctid" "$archive" /tmp/nvidia_lxc.tar.gz >>"$LOG_FILE" 2>&1; then + msg_warn "$(translate 'pct push failed. Check log:') ${LOG_FILE}" + rm -f "$archive" + _restore_container_memory "$ctid" + return 1 + fi + rm -f "$archive" + msg_ok "$(translate 'Installer copied to container.')" | tee -a "$screen_capture" + + msg_info2 "$(translate 'Installing NVIDIA drivers in container. This may take several minutes...')" + echo "" >>"$LOG_FILE" + pct exec "$ctid" -- bash -c " + mkdir -p /tmp/nvidia_lxc_install + tar -xzf /tmp/nvidia_lxc.tar.gz -C /tmp/nvidia_lxc_install 2>&1 + /tmp/nvidia_lxc_install/nvidia-installer \ + --no-kernel-modules \ + --no-questions \ + --ui=none \ + --no-nouveau-check \ + --no-dkms \ + --no-install-compat32-libs + EXIT=\$? + rm -rf /tmp/nvidia_lxc_install /tmp/nvidia_lxc.tar.gz + exit \$EXIT + " 2>&1 | tee -a "$LOG_FILE" + local rc=${PIPESTATUS[0]} + + rm -rf "$extract_dir" + _restore_container_memory "$ctid" + + if [[ $rc -ne 0 ]]; then + msg_warn "$(translate 'NVIDIA installer returned error') ${rc}. $(translate 'Check log:') ${LOG_FILE}" + return 1 + fi + ;; + esac + + if pct exec "$ctid" -- sh -c "which nvidia-smi" >/dev/null 2>&1; then + return 0 + else + msg_warn "$(translate 'nvidia-smi not found after install. Check log:') ${LOG_FILE}" + return 1 + fi +} + + +start_container_and_wait() { + local ctid="$1" + msg_info "$(translate 'Starting container') ${ctid}..." + pct start "$ctid" >>"$LOG_FILE" 2>&1 || true + + local ready=false + for _ in {1..15}; do + sleep 2 + if pct exec "$ctid" -- true >/dev/null 2>&1; then + ready=true + break + fi + done + + if ! $ready; then + msg_warn "$(translate 'Container did not become ready in time. Skipping driver installation.')" + return 1 + fi + msg_ok "$(translate 'Container started.')" | tee -a "$screen_capture" + return 0 +} + +install_drivers() { + local ctid="$1" + + # Detect distro once — passed to each install function + msg_info "$(translate 'Detecting container OS...')" + local ct_distro + ct_distro=$(_detect_container_distro "$ctid") + msg_ok "$(translate 'Container OS:') ${ct_distro}" | tee -a "$screen_capture" + + for gpu_type in "${SELECTED_GPUS[@]}"; do + case "$gpu_type" in + intel) + msg_info "$(translate 'Installing Intel VA-API drivers in container...')" + _install_intel_drivers "$ctid" "$ct_distro" + msg_ok "$(translate 'Intel VA-API drivers installed.')" | tee -a "$screen_capture" + ;; + amd) + msg_info "$(translate 'Installing AMD mesa drivers in container...')" + _install_amd_drivers "$ctid" "$ct_distro" + msg_ok "$(translate 'AMD mesa drivers installed.')" | tee -a "$screen_capture" + ;; + nvidia) + # No outer msg_info here — _install_nvidia_drivers manages its own messages + # to avoid a dangling spinner before the whiptail memory dialog + if _install_nvidia_drivers "$ctid" "$ct_distro"; then + msg_ok "$(translate 'NVIDIA userspace libraries installed.')" | tee -a "$screen_capture" + NVIDIA_INSTALL_SUCCESS=true + elif [[ "$INSTALL_ABORTED" == "false" ]]; then + msg_warn "$(translate 'NVIDIA install incomplete. Check log:') ${LOG_FILE}" + fi + ;; + esac + done +} + + +# ============================================================ +# Main +# ============================================================ +main() { + : >"$LOG_FILE" + : >"$screen_capture" + + # ---- Phase 1: all dialogs (no terminal output yet) ---- + detect_host_gpus + select_container + select_gpus + + # NVIDIA check runs only if NVIDIA was selected + for gpu_type in "${SELECTED_GPUS[@]}"; do + [[ "$gpu_type" == "nvidia" ]] && check_nvidia_ready + done + + # ---- Phase 2: processing ---- + show_proxmenux_logo + msg_title "$(translate 'Add GPU to LXC')" + + configure_passthrough "$CONTAINER_ID" + + if start_container_and_wait "$CONTAINER_ID"; then + install_drivers "$CONTAINER_ID" + + # Capture nvidia-smi output while container is still running + if $NVIDIA_INSTALL_SUCCESS; then + NVIDIA_SMI_OUTPUT=$(pct exec "$CONTAINER_ID" -- nvidia-smi 2>/dev/null || true) + fi + + if [[ "$CT_WAS_RUNNING" == "false" ]]; then + pct stop "$CONTAINER_ID" >>"$LOG_FILE" 2>&1 || true + fi + fi + + if [[ "$INSTALL_ABORTED" == "true" ]]; then + rm -f "$screen_capture" + exit 0 + fi + + show_proxmenux_logo + msg_title "$(translate 'Add GPU to LXC')" + cat "$screen_capture" + echo -e "${TAB}${GN}📄 $(translate 'Log')${CL}: ${BL}${LOG_FILE}${CL}" + if [[ -n "$NVIDIA_SMI_OUTPUT" ]]; then + msg_info2 "$(translate 'NVIDIA driver verification in container:')" + echo "$NVIDIA_SMI_OUTPUT" + fi + msg_success "$(translate 'GPU passthrough configured for LXC') ${CONTAINER_ID}." + msg_success "$(translate 'Completed. Press Enter to return to menu...')" + read -r + rm -f "$screen_capture" +} + +main diff --git a/scripts/gpu_tpu/install_coral_lxc.sh b/scripts/gpu_tpu/install_coral_lxc.sh index 11f52ffd..a6f8e0d1 100644 --- a/scripts/gpu_tpu/install_coral_lxc.sh +++ b/scripts/gpu_tpu/install_coral_lxc.sh @@ -7,8 +7,8 @@ # Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral) # Copyright : (c) 2024 MacRimi # License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE) -# Version : 1.3 -# Last Updated: 28/03/2025 +# Version : 1.4 (unprivileged container support, PVE dev API for apex/iGPU) +# Last Updated: 01/04/2026 # ========================================================== # Description: # This script automates the configuration and installation of @@ -158,13 +158,25 @@ add_mount_if_needed() { cleanup_duplicate_entries() { local CONFIG_FILE="$1" local TEMP_FILE=$(mktemp) - + awk '!seen[$0]++' "$CONFIG_FILE" > "$TEMP_FILE" - + cat "$TEMP_FILE" > "$CONFIG_FILE" rm -f "$TEMP_FILE" } +# Returns the next available dev index (dev0, dev1, ...) in a container config. +# The PVE dev API (devN: /dev/foo,gid=N) works in both privileged and unprivileged +# containers, handling cgroup2 permissions automatically. +get_next_dev_index() { + local config="$1" + local idx=0 + while grep -q "^dev${idx}:" "$config" 2>/dev/null; do + idx=$((idx + 1)) + done + echo "$idx" +} + # ========================================================== # CONFIGURE LXC HARDWARE PASSTHROUGH # ========================================================== @@ -180,25 +192,6 @@ configure_lxc_hardware() { cleanup_duplicate_entries "$CONFIG_FILE" - # ============================================================ - # Convert to privileged container if needed - # ============================================================ - if grep -q "^unprivileged: 1" "$CONFIG_FILE"; then - msg_info "$(translate 'The container is unprivileged. Changing to privileged...')" - sed -i "s/^unprivileged: 1/unprivileged: 0/" "$CONFIG_FILE" - - STORAGE_TYPE=$(pct config "$CONTAINER_ID" | grep "^rootfs:" | awk -F, '{print $2}' | cut -d'=' -f2) - if [[ "$STORAGE_TYPE" == "dir" ]]; then - STORAGE_PATH=$(pct config "$CONTAINER_ID" | grep "^rootfs:" | awk '{print $2}' | cut -d',' -f1) - chown -R root:root "$STORAGE_PATH" - fi - msg_ok "$(translate 'Container changed to privileged.')" - else - msg_ok "$(translate 'The container is already privileged.')" - fi - - sed -i '/^dev[0-9]\+:/d' "$CONFIG_FILE" - # ============================================================ # Enable nesting feature # ============================================================ @@ -217,19 +210,24 @@ configure_lxc_hardware() { # iGPU support # ============================================================ msg_info "$(translate 'Configuring iGPU support...')" - - if ! grep -Pq "^lxc.cgroup2.devices.allow: c 226:0 rwm" "$CONFIG_FILE"; then - echo "lxc.cgroup2.devices.allow: c 226:0 rwm # iGPU" >> "$CONFIG_FILE" - fi - - if ! grep -Pq "^lxc.cgroup2.devices.allow: c 226:128 rwm" "$CONFIG_FILE"; then - echo "lxc.cgroup2.devices.allow: c 226:128 rwm # iGPU" >> "$CONFIG_FILE" - fi + # Bind-mount the /dev/dri directory so apps can enumerate available devices add_mount_if_needed "/dev/dri" "dev/dri" "$CONFIG_FILE" - add_mount_if_needed "/dev/dri/renderD128" "dev/dri/renderD128" "$CONFIG_FILE" - add_mount_if_needed "/dev/dri/card0" "dev/dri/card0" "$CONFIG_FILE" - + + # Add each DRI device via the PVE dev API (gid=44 = render group). + # This approach works in unprivileged containers: PVE manages cgroup2 + # permissions automatically and maps the GID into the container namespace. + local igpu_dev_idx + igpu_dev_idx=$(get_next_dev_index "$CONFIG_FILE") + for dri_dev in /dev/dri/renderD128 /dev/dri/renderD129 /dev/dri/card0 /dev/dri/card1; do + if [[ -c "$dri_dev" ]]; then + if ! grep -q ":.*${dri_dev}" "$CONFIG_FILE"; then + echo "dev${igpu_dev_idx}: ${dri_dev},gid=44" >> "$CONFIG_FILE" + igpu_dev_idx=$((igpu_dev_idx + 1)) + fi + fi + done + msg_ok "$(translate 'iGPU configuration added')" # ============================================================ @@ -277,18 +275,29 @@ configure_lxc_hardware() { if lspci | grep -iq "Global Unichip"; then msg_info "$(translate 'Coral M.2 Apex detected, configuring...')" - - if ! grep -Pq "^lxc.cgroup2.devices.allow: c 245:0 rwm" "$CONFIG_FILE"; then - echo "lxc.cgroup2.devices.allow: c 245:0 rwm # Coral M2 Apex" >> "$CONFIG_FILE" - fi - + local APEX_GID apex_dev_idx + APEX_GID=$(getent group apex 2>/dev/null | cut -d: -f3 || echo "0") + apex_dev_idx=$(get_next_dev_index "$CONFIG_FILE") - add_mount_if_needed "/dev/apex_0" "dev/apex_0" "$CONFIG_FILE" - if [ -e "/dev/apex_0" ]; then + # Device is visible — use PVE dev API (works in unprivileged containers). + # PVE handles cgroup2 permissions automatically. + if ! grep -q "dev.*apex_0" "$CONFIG_FILE"; then + echo "dev${apex_dev_idx}: /dev/apex_0,gid=${APEX_GID}" >> "$CONFIG_FILE" + fi msg_ok "$(translate 'Coral M.2 Apex configuration added - device ready')" else + # Device not yet visible (host module not loaded or reboot pending). + # Use cgroup2 + optional bind-mount as fallback; detect major number + # dynamically from /proc/devices to avoid hardcoding it. + local APEX_MAJOR + APEX_MAJOR=$(awk '/\bapex\b/{print $1}' /proc/devices 2>/dev/null | head -1) + [[ -z "$APEX_MAJOR" ]] && APEX_MAJOR="245" + if ! grep -q "lxc.cgroup2.devices.allow: c ${APEX_MAJOR}:0 rwm" "$CONFIG_FILE"; then + echo "lxc.cgroup2.devices.allow: c ${APEX_MAJOR}:0 rwm # Coral M2 Apex" >> "$CONFIG_FILE" + fi + add_mount_if_needed "/dev/apex_0" "dev/apex_0" "$CONFIG_FILE" msg_ok "$(translate 'Coral M.2 Apex configuration added - device will be available after reboot')" fi fi @@ -311,7 +320,13 @@ install_coral_in_container() { if ! pct status "$CONTAINER_ID" | grep -q "running"; then pct start "$CONTAINER_ID" - sleep 5 + for _ in {1..15}; do + pct status "$CONTAINER_ID" | grep -q "running" && break + sleep 1 + done + if ! pct status "$CONTAINER_ID" | grep -q "running"; then + msg_error "$(translate 'Container did not start in time.')"; exit 1 + fi fi @@ -337,7 +352,8 @@ install_coral_in_container() { # Install drivers inside container script -q -c "pct exec \"$CONTAINER_ID\" -- bash -c ' set -e - + export DEBIAN_FRONTEND=noninteractive + echo \"[1/6] Updating package lists...\" apt-get update -qq diff --git a/scripts/gpu_tpu/install_coral_pve9.sh b/scripts/gpu_tpu/install_coral_pve9.sh index 1eab069b..6fe32e6d 100644 --- a/scripts/gpu_tpu/install_coral_pve9.sh +++ b/scripts/gpu_tpu/install_coral_pve9.sh @@ -3,8 +3,8 @@ # ========================================= # Author : MacRimi # License : MIT -# Version : 1.3 (PVE9, silent build) -# Last Updated: 25/09/2025 +# Version : 1.4 (kernel-conditional patches, direct DKMS, no debuild) +# Last Updated: 01/04/2026 # ========================================= LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" @@ -74,68 +74,72 @@ pre_install_prompt() { install_coral_host() { show_proxmenux_logo - : >"$LOG_FILE" + : >"$LOG_FILE" + # Detect running kernel and parse major/minor for conditional patches + local KVER KMAJ KMIN + KVER=$(uname -r) + KMAJ=$(echo "$KVER" | cut -d. -f1) + KMIN=$(echo "$KVER" | cut -d. -f2 | cut -d+ -f1 | cut -d- -f1) msg_info "$(translate 'Installing build dependencies...')" + export DEBIAN_FRONTEND=noninteractive apt-get update -qq >>"$LOG_FILE" 2>&1 - apt-get install -y git devscripts dh-dkms dkms proxmox-headers-$(uname -r) >>"$LOG_FILE" 2>&1 - if [[ $? -ne 0 ]]; then msg_error "$(translate 'Error installing build dependencies. Check /tmp/coral_install.log')"; exit 1; fi - msg_ok "$(translate 'Build dependencies installed.')" - + if ! apt-get install -y git dkms build-essential "proxmox-headers-${KVER}" >>"$LOG_FILE" 2>&1; then + msg_error "$(translate 'Error installing build dependencies. Check /tmp/coral_install.log')"; exit 1 + fi + msg_ok "$(translate 'Build dependencies installed.')" cd /tmp || exit 1 rm -rf gasket-driver >>"$LOG_FILE" 2>&1 msg_info "$(translate 'Cloning Google Coral driver repository...')" - git clone https://github.com/google/gasket-driver.git >>"$LOG_FILE" 2>&1 - if [[ $? -ne 0 ]]; then msg_error "$(translate 'Could not clone the repository. Check /tmp/coral_install.log')"; exit 1; fi - msg_ok "$(translate 'Repository cloned successfully.')" - + if ! git clone https://github.com/google/gasket-driver.git >>"$LOG_FILE" 2>&1; then + msg_error "$(translate 'Could not clone the repository. Check /tmp/coral_install.log')"; exit 1 + fi + msg_ok "$(translate 'Repository cloned successfully.')" cd /tmp/gasket-driver || exit 1 msg_info "$(translate 'Patching source for kernel compatibility...')" - - sed -i 's/\.llseek = no_llseek/\.llseek = noop_llseek/' src/gasket_core.c - - sed -i 's/^MODULE_IMPORT_NS(DMA_BUF);/MODULE_IMPORT_NS("DMA_BUF");/' src/gasket_page_table.c - - sed -i "s/\(linux-headers-686-pae | linux-headers-amd64 | linux-headers-generic | linux-headers\)/\1 | proxmox-headers-$(uname -r) | pve-headers-$(uname -r)/" debian/control - if [[ $? -ne 0 ]]; then msg_error "$(translate 'Patching failed. Check /tmp/coral_install.log')"; exit 1; fi - msg_ok "$(translate 'Source patched successfully.')" - - - - msg_info "$(translate 'Building DKMS package...')" - debuild -us -uc -tc -b >>"$LOG_FILE" 2>&1 - if [[ $? -ne 0 ]]; then msg_error "$(translate 'Failed to build DKMS package. Check /tmp/coral_install.log')"; exit 1; fi - msg_ok "$(translate 'DKMS package built successfully.')" - - - - msg_info "$(translate 'Installing DKMS package...')" - dpkg -i ../gasket-dkms_*.deb >>"$LOG_FILE" 2>&1 || true - if ! dpkg -s gasket-dkms >/dev/null 2>&1; then - msg_error "$(translate 'Failed to install DKMS package. Check /tmp/coral_install.log')"; exit 1 + # Patch 1: no_llseek was removed in kernel 6.5 — replace with noop_llseek + if [[ "$KMAJ" -gt 6 ]] || [[ "$KMAJ" -eq 6 && "$KMIN" -ge 5 ]]; then + sed -i 's/\.llseek = no_llseek/\.llseek = noop_llseek/' src/gasket_core.c fi - msg_ok "$(translate 'DKMS package installed.')" + # Patch 2: MODULE_IMPORT_NS changed to string-literal syntax in kernel 6.13. + # IMPORTANT: applying this patch on kernel < 6.13 causes a compile error. + if [[ "$KMAJ" -gt 6 ]] || [[ "$KMAJ" -eq 6 && "$KMIN" -ge 13 ]]; then + sed -i 's/^MODULE_IMPORT_NS(DMA_BUF);/MODULE_IMPORT_NS("DMA_BUF");/' src/gasket_page_table.c + fi + + msg_ok "$(translate 'Source patched successfully.') (kernel ${KVER})" + + + msg_info "$(translate 'Preparing DKMS source tree...')" + local GASKET_SRC="/usr/src/gasket-1.0" + # Remove any previous installation (package or manual) to avoid conflicts + dpkg -r gasket-dkms >>"$LOG_FILE" 2>&1 || true + dkms remove gasket/1.0 --all >>"$LOG_FILE" 2>&1 || true + rm -rf "$GASKET_SRC" + cp -r /tmp/gasket-driver/. "$GASKET_SRC" + if ! dkms add "$GASKET_SRC" >>"$LOG_FILE" 2>&1; then + msg_error "$(translate 'DKMS add failed. Check /tmp/coral_install.log')"; exit 1 + fi + msg_ok "$(translate 'DKMS source tree prepared.')" msg_info "$(translate 'Compiling Coral TPU drivers for current kernel...')" - dkms remove -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1 || true - dkms add -m gasket -v 1.0 >>"$LOG_FILE" 2>&1 || true - dkms build -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1 - if [[ $? -ne 0 ]]; then + if ! dkms build gasket/1.0 -k "$KVER" >>"$LOG_FILE" 2>&1; then sed -n '1,200p' /var/lib/dkms/gasket/1.0/build/make.log >>"$LOG_FILE" 2>&1 || true msg_error "$(translate 'DKMS build failed. Check /tmp/coral_install.log')"; exit 1 fi - dkms install -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1 - if [[ $? -ne 0 ]]; then msg_error "$(translate 'DKMS install failed. Check /tmp/coral_install.log')"; exit 1; fi - msg_ok "$(translate 'Drivers compiled and installed via DKMS.')" + if ! dkms install gasket/1.0 -k "$KVER" >>"$LOG_FILE" 2>&1; then + msg_error "$(translate 'DKMS install failed. Check /tmp/coral_install.log')"; exit 1 + fi + msg_ok "$(translate 'Drivers compiled and installed via DKMS.')" ensure_apex_group_and_udev @@ -150,8 +154,6 @@ install_coral_host() { msg_warn "$(translate 'Installation finished but drivers are not loaded. Please check dmesg and /tmp/coral_install.log')" fi - - echo "---- dmesg | grep -i apex (last lines) ----" >>"$LOG_FILE" dmesg | grep -i apex | tail -n 20 >>"$LOG_FILE" 2>&1 } diff --git a/scripts/gpu_tpu/nvidia_update.sh b/scripts/gpu_tpu/nvidia_update.sh new file mode 100644 index 00000000..7b2d6ff5 --- /dev/null +++ b/scripts/gpu_tpu/nvidia_update.sh @@ -0,0 +1,494 @@ +#!/bin/bash +# ProxMenux - NVIDIA Driver Updater (Host + LXC) +# ================================================ +# Author : MacRimi +# License : MIT +# Version : 1.0 +# Last Updated: 01/04/2026 +# ================================================ + +LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" +BASE_DIR="/usr/local/share/proxmenux" +UTILS_FILE="$BASE_DIR/utils.sh" +LOG_FILE="/tmp/nvidia_update.log" + +NVIDIA_BASE_URL="https://download.nvidia.com/XFree86/Linux-x86_64" +NVIDIA_WORKDIR="/opt/nvidia" + +if [[ -f "$UTILS_FILE" ]]; then + source "$UTILS_FILE" +fi + +load_language +initialize_cache + + +# ============================================================ +# Host NVIDIA state detection +# ============================================================ +detect_host_nvidia() { + HOST_NVIDIA_VERSION="" + HOST_NVIDIA_READY=false + + if lsmod | grep -q "^nvidia " && command -v nvidia-smi >/dev/null 2>&1; then + HOST_NVIDIA_VERSION=$(nvidia-smi --query-gpu=driver_version \ + --format=csv,noheader 2>/dev/null | head -n1 | tr -d '[:space:]') + [[ -n "$HOST_NVIDIA_VERSION" ]] && HOST_NVIDIA_READY=true + fi + + if ! $HOST_NVIDIA_READY; then + dialog --backtitle "ProxMenux" \ + --title "$(translate 'NVIDIA Not Found')" \ + --msgbox "\n$(translate 'No NVIDIA driver is currently loaded on this host.')\n\n$(translate 'Please install NVIDIA drivers first using the option:')\n\n $(translate 'Install NVIDIA Drivers on Host')\n\n$(translate 'from this same GPU and TPU menu.')" \ + 13 72 + exit 0 + fi +} + + +# ============================================================ +# LXC containers with NVIDIA passthrough +# ============================================================ +find_nvidia_containers() { + NVIDIA_CONTAINERS=() + for conf in /etc/pve/lxc/*.conf; do + [[ -f "$conf" ]] || continue + if grep -qiE "dev[0-9]+:.*nvidia" "$conf"; then + NVIDIA_CONTAINERS+=("$(basename "$conf" .conf)") + fi + done +} + +get_lxc_nvidia_version() { + local ctid="$1" + local version="" + + # Prefer nvidia-smi when the container is running (works with .run-installed drivers) + if pct status "$ctid" 2>/dev/null | grep -q "running"; then + version=$(pct exec "$ctid" -- nvidia-smi \ + --query-gpu=driver_version --format=csv,noheader 2>/dev/null \ + | head -1 | tr -d '[:space:]' || true) + fi + + # Fallback: dpkg status for apt-installed libcuda1 (dir-type storage, no start needed) + if [[ -z "$version" ]]; then + local rootfs="/var/lib/lxc/${ctid}/rootfs" + if [[ -f "${rootfs}/var/lib/dpkg/status" ]]; then + version=$(grep -A5 "^Package: libcuda1$" "${rootfs}/var/lib/dpkg/status" \ + | grep "^Version:" | head -1 | awk '{print $2}' | cut -d- -f1) + fi + fi + + echo "${version:-$(translate 'not installed')}" +} + + +# ============================================================ +# Version list from NVIDIA servers +# ============================================================ +list_available_versions() { + local html + html=$(curl -s --connect-timeout 15 "${NVIDIA_BASE_URL}/" 2>/dev/null) || true + + if [[ -z "$html" ]]; then + echo "" + return 1 + fi + + echo "$html" \ + | grep -o 'href=[^ >]*' \ + | awk -F"'" '{print $2}' \ + | grep -E '^[0-9]' \ + | sed 's/\/$//' \ + | sed "s/^[[:space:]]*//;s/[[:space:]]*$//" \ + | sort -Vr \ + | uniq +} + +get_latest_version() { + local latest_line + latest_line=$(curl -fsSL --connect-timeout 15 "${NVIDIA_BASE_URL}/latest.txt" 2>/dev/null) || true + echo "$latest_line" | awk '{print $1}' | tr -d '[:space:]' +} + + +# ============================================================ +# Version selection menu +# ============================================================ +select_target_version() { + msg_info "$(translate 'Fetching available NVIDIA versions...')" + local latest versions_list + latest=$(get_latest_version 2>/dev/null) + versions_list=$(list_available_versions 2>/dev/null) + msg_ok "$(translate 'Version list retrieved.')" + + if [[ -z "$latest" && -z "$versions_list" ]]; then + dialog --backtitle "ProxMenux" \ + --title "$(translate 'Error')" \ + --msgbox "\n$(translate 'Could not retrieve versions from NVIDIA. Please check your internet connection.')" \ + 8 72 + exit 1 + fi + + [[ -z "$latest" && -n "$versions_list" ]] && latest=$(echo "$versions_list" | head -1) + [[ -z "$versions_list" ]] && versions_list="$latest" + latest=$(echo "$latest" | tr -d '[:space:]') + + local choices=() + choices+=("latest" "$(translate 'Latest available') (${latest:-?})") + choices+=("" "") + + while IFS= read -r ver; do + ver=$(echo "$ver" | tr -d '[:space:]') + [[ -z "$ver" ]] && continue + choices+=("$ver" "$ver") + done <<< "$versions_list" + + local menu_text + menu_text="\n$(translate 'Current host version:') ${HOST_NVIDIA_VERSION}\n" + menu_text+="$(translate 'Select the target version to install on host and all affected LXCs:')" + + TARGET_VERSION=$(dialog --backtitle "ProxMenux" \ + --title "$(translate 'NVIDIA Driver Version')" \ + --menu "$menu_text" 26 80 16 \ + "${choices[@]}" \ + 2>&1 >/dev/tty) || exit 0 + + [[ -z "$TARGET_VERSION" ]] && exit 0 + + if [[ "$TARGET_VERSION" == "latest" ]]; then + TARGET_VERSION="$latest" + fi + TARGET_VERSION=$(echo "$TARGET_VERSION" | tr -d '[:space:]') +} + + +# ============================================================ +# Update NVIDIA userspace libs inside a single LXC +# ============================================================ +update_lxc_nvidia() { + local ctid="$1" + local version="$2" + local was_running=false + + # Capture old version before update + local old_version + old_version=$(get_lxc_nvidia_version "$ctid") + + if pct status "$ctid" 2>/dev/null | grep -q "running"; then + was_running=true + else + msg_info "$(translate 'Starting container') ${ctid}..." + pct start "$ctid" >>"$LOG_FILE" 2>&1 || true + local ready=false + for _ in {1..15}; do + sleep 2 + pct exec "$ctid" -- true >/dev/null 2>&1 && ready=true && break + done + if ! $ready; then + msg_warn "$(translate 'Container') ${ctid} $(translate 'did not start. Skipping.')" + return 1 + fi + msg_ok "$(translate 'Container') ${ctid} $(translate 'started.')" + fi + + msg_info "$(translate 'Updating NVIDIA libs in container') ${ctid}..." + + local run_file="${NVIDIA_WORKDIR}/NVIDIA-Linux-x86_64-${version}.run" + + if [[ ! -f "$run_file" ]]; then + msg_warn "$(translate 'Installer not found:') ${run_file} — $(translate 'skipping container') ${ctid}" + if [[ "$was_running" == "false" ]]; then pct stop "$ctid" >>"$LOG_FILE" 2>&1 || true; fi + return 1 + fi + + # Extract .run on the host to avoid decompression failures inside the container + local extract_dir="${NVIDIA_WORKDIR}/extracted_${version}" + local archive="/tmp/nvidia_lxc_${version}.tar.gz" + + msg_info "$(translate 'Extracting NVIDIA installer on host...')" + rm -rf "$extract_dir" + if ! sh "$run_file" --extract-only --target "$extract_dir" >>"$LOG_FILE" 2>&1; then + msg_warn "$(translate 'Extraction failed. Check log:') ${LOG_FILE}" + if [[ "$was_running" == "false" ]]; then pct stop "$ctid" >>"$LOG_FILE" 2>&1 || true; fi + return 1 + fi + msg_ok "$(translate 'Extracted.')" + + msg_info "$(translate 'Packing and copying installer to container') ${ctid}..." + tar -czf "$archive" -C "$extract_dir" . >>"$LOG_FILE" 2>&1 + if ! pct push "$ctid" "$archive" /tmp/nvidia_lxc.tar.gz >>"$LOG_FILE" 2>&1; then + msg_warn "$(translate 'pct push failed. Check log:') ${LOG_FILE}" + rm -f "$archive" + if [[ "$was_running" == "false" ]]; then pct stop "$ctid" >>"$LOG_FILE" 2>&1 || true; fi + return 1 + fi + rm -f "$archive" + msg_ok "$(translate 'Installer copied to container.')" + + msg_info2 "$(translate 'Starting NVIDIA installer in container') ${ctid}. $(translate 'This may take several minutes...')" + echo "" >>"$LOG_FILE" + pct exec "$ctid" -- bash -c " + mkdir -p /tmp/nvidia_lxc_install + tar -xzf /tmp/nvidia_lxc.tar.gz -C /tmp/nvidia_lxc_install 2>&1 + /tmp/nvidia_lxc_install/nvidia-installer \ + --no-kernel-modules \ + --no-questions \ + --ui=none \ + --no-nouveau-check \ + --no-dkms + EXIT=\$? + rm -rf /tmp/nvidia_lxc_install /tmp/nvidia_lxc.tar.gz + exit \$EXIT + " 2>&1 | tee -a "$LOG_FILE" + local rc=${PIPESTATUS[0]} + + rm -rf "$extract_dir" + + if [[ $rc -ne 0 ]]; then + msg_warn "$(translate 'NVIDIA installer returned error') ${rc}. $(translate 'Check log:') ${LOG_FILE}" + if [[ "$was_running" == "false" ]]; then pct stop "$ctid" >>"$LOG_FILE" 2>&1 || true; fi + return 1 + fi + + msg_ok "$(translate 'Container') ${ctid}: ${old_version} → ${version}" + msg_info2 "$(translate 'NVIDIA driver verification in container') ${ctid}:" + pct exec "$ctid" -- nvidia-smi 2>/dev/null || true + + if [[ "$was_running" == "false" ]]; then + msg_info "$(translate 'Stopping container') ${ctid}..." + pct stop "$ctid" >>"$LOG_FILE" 2>&1 || true + msg_ok "$(translate 'Container stopped.')" + fi +} + + +# ============================================================ +# Host NVIDIA update +# ============================================================ +_stop_nvidia_services() { + for svc in nvidia-persistenced.service nvidia-powerd.service; do + systemctl is-active --quiet "$svc" 2>/dev/null && systemctl stop "$svc" >/dev/null 2>&1 || true + systemctl is-enabled --quiet "$svc" 2>/dev/null && systemctl disable "$svc" >/dev/null 2>&1 || true + done +} + +_unload_nvidia_modules() { + for mod in nvidia_uvm nvidia_drm nvidia_modeset nvidia; do + modprobe -r "$mod" >/dev/null 2>&1 || true + done + # Second pass for stubborn modules + for mod in nvidia_uvm nvidia_drm nvidia_modeset nvidia; do + modprobe -r --force "$mod" >/dev/null 2>&1 || true + done +} + +_purge_nvidia_host() { + msg_info "$(translate 'Uninstalling current NVIDIA driver from host...')" + + _stop_nvidia_services + _unload_nvidia_modules + + command -v nvidia-uninstall >/dev/null 2>&1 \ + && nvidia-uninstall --silent >>"$LOG_FILE" 2>&1 || true + + # Remove DKMS entries + local dkms_versions + dkms_versions=$(dkms status 2>/dev/null | awk -F, '/nvidia/ {gsub(/ /,"",$2); print $2}' || true) + while IFS= read -r ver; do + [[ -z "$ver" ]] && continue + dkms remove -m nvidia -v "$ver" --all >/dev/null 2>&1 || true + done <<< "$dkms_versions" + + apt-get -y purge 'nvidia-*' 'libnvidia-*' 'cuda-*' >>"$LOG_FILE" 2>&1 || true + apt-get -y autoremove --purge >>"$LOG_FILE" 2>&1 || true + + rm -f /etc/udev/rules.d/70-nvidia.rules + rm -f /etc/modprobe.d/nvidia*.conf /usr/lib/modprobe.d/nvidia*.conf + + msg_ok "$(translate 'Current NVIDIA driver removed from host.')" +} + +_download_installer() { + local version="$1" + local run_file="${NVIDIA_WORKDIR}/NVIDIA-Linux-x86_64-${version}.run" + + mkdir -p "$NVIDIA_WORKDIR" + + # Reuse cached file if valid + local existing_size + existing_size=$(stat -c%s "$run_file" 2>/dev/null || echo "0") + if [[ -f "$run_file" ]] && [[ "$existing_size" -gt 40000000 ]]; then + if file "$run_file" 2>/dev/null | grep -q "executable"; then + msg_ok "$(translate 'Installer already cached.')" + echo "$run_file" + return 0 + fi + fi + rm -f "$run_file" + + msg_info "$(translate 'Downloading NVIDIA driver') ${version}..." + + local urls=( + "${NVIDIA_BASE_URL}/${version}/NVIDIA-Linux-x86_64-${version}.run" + "${NVIDIA_BASE_URL}/${version}/NVIDIA-Linux-x86_64-${version}-no-compat32.run" + ) + + local ok=false + for url in "${urls[@]}"; do + if curl -fL --connect-timeout 30 --max-time 600 "$url" -o "$run_file" >>"$LOG_FILE" 2>&1; then + local sz + sz=$(stat -c%s "$run_file" 2>/dev/null || echo "0") + if [[ "$sz" -gt 40000000 ]] && file "$run_file" 2>/dev/null | grep -q "executable"; then + ok=true + break + fi + fi + rm -f "$run_file" + done + + if ! $ok; then + msg_error "$(translate 'Download failed. Check /tmp/nvidia_update.log')" + exit 1 + fi + + chmod +x "$run_file" + msg_ok "$(translate 'Download complete.')" + echo "$run_file" +} + +_run_installer() { + local installer="$1" + local tmp_dir="${NVIDIA_WORKDIR}/tmp_extract" + mkdir -p "$tmp_dir" + + msg_info "$(translate 'Installing NVIDIA driver on host. This may take several minutes...')" + + sh "$installer" \ + --tmpdir="$tmp_dir" \ + --no-questions \ + --ui=none \ + --disable-nouveau \ + --no-nouveau-check \ + --dkms \ + >>"$LOG_FILE" 2>&1 + local rc=$? + + rm -rf "$tmp_dir" + + if [[ $rc -ne 0 ]]; then + msg_error "$(translate 'NVIDIA installer failed. Check /tmp/nvidia_update.log')" + exit 1 + fi + + msg_ok "$(translate 'NVIDIA driver installed on host.')" +} + +update_host_nvidia() { + local version="$1" + + _purge_nvidia_host + + local installer + installer=$(_download_installer "$version") + + _run_installer "$installer" + + msg_info "$(translate 'Updating initramfs...')" + update-initramfs -u -k all >>"$LOG_FILE" 2>&1 || true + msg_ok "$(translate 'initramfs updated.')" +} + + +# ============================================================ +# Overview dialog (current state) +# ============================================================ +show_current_state_dialog() { + find_nvidia_containers + + local info + info="\n$(translate 'Host NVIDIA driver:') ${HOST_NVIDIA_VERSION}\n\n" + + if [[ ${#NVIDIA_CONTAINERS[@]} -eq 0 ]]; then + info+="$(translate 'No LXC containers with NVIDIA passthrough found.')\n" + else + info+="$(translate 'LXC containers with NVIDIA passthrough:')\n\n" + for ctid in "${NVIDIA_CONTAINERS[@]}"; do + local lxc_ver + lxc_ver=$(get_lxc_nvidia_version "$ctid") + local ct_name + ct_name=$(pct config "$ctid" 2>/dev/null | grep "^hostname:" | awk '{print $2}') + info+=" CT ${ctid} ${ct_name:+(${ct_name})} — libcuda1: ${lxc_ver}\n" + done + fi + + info+="\n$(translate 'After selecting a version, LXC containers will be updated first, then the host.')" + info+="\n$(translate 'A reboot is required after the host update.')" + + dialog --backtitle "ProxMenux" \ + --title "$(translate 'NVIDIA Update — Current State')" \ + --yesno "$info" 20 80 \ + >/dev/tty 2>&1 || exit 0 +} + + +# ============================================================ +# Restart prompt +# ============================================================ +restart_prompt() { + if whiptail --title "$(translate 'NVIDIA Update')" --yesno \ + "$(translate 'The host driver update requires a reboot to take effect. Reboot now?')" 10 70; then + msg_warn "$(translate 'Restarting the server...')" + reboot + else + msg_success "$(translate 'Update complete. Please reboot the server manually.')" + msg_success "$(translate 'Completed. Press Enter to return to menu...')" + read -r + fi +} + + +# ============================================================ +# Main +# ============================================================ +main() { + : >"$LOG_FILE" + + # ---- Phase 1: dialogs ---- + detect_host_nvidia + show_current_state_dialog + select_target_version + + # Same version confirmation + if [[ "$TARGET_VERSION" == "$HOST_NVIDIA_VERSION" ]]; then + if ! dialog --backtitle "ProxMenux" \ + --title "$(translate 'Same Version')" \ + --yesno "\n$(translate 'Version') ${TARGET_VERSION} $(translate 'is already installed on the host.')\n\n$(translate 'Reinstall and force-update all LXC containers anyway?')" \ + 10 70 >/dev/tty 2>&1; then + exit 0 + fi + fi + + # ---- Phase 2: processing ---- + show_proxmenux_logo + msg_title "$(translate 'NVIDIA Driver Update')" + + # Download installer once — reused by both LXC containers and host + local run_file + run_file=$(_download_installer "$TARGET_VERSION") + + # Update LXC containers first (no reboot needed for userspace libs) + if [[ ${#NVIDIA_CONTAINERS[@]} -gt 0 ]]; then + msg_info2 "$(translate 'Updating LXC containers...')" + for ctid in "${NVIDIA_CONTAINERS[@]}"; do + update_lxc_nvidia "$ctid" "$TARGET_VERSION" + done + fi + + # Update host kernel module + drivers (reuses the already-downloaded installer) + update_host_nvidia "$TARGET_VERSION" + + restart_prompt +} + +main diff --git a/scripts/menus/hw_grafics_menu.sh b/scripts/menus/hw_grafics_menu.sh index fd7288c7..f10c3d5f 100644 --- a/scripts/menus/hw_grafics_menu.sh +++ b/scripts/menus/hw_grafics_menu.sh @@ -1,17 +1,15 @@ #!/bin/bash - # ========================================================== -# ProxMenux - A menu-driven script for Proxmox VE management +# ProxMenux - GPU and TPU Menu # ========================================================== # Author : MacRimi # Copyright : (c) 2024 MacRimi -# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE) -# Version : 1.0 -# Last Updated: 28/01/2025 +# License : MIT +# Version : 2.0 +# Last Updated: 01/04/2026 # ========================================================== - -# Configuration ============================================ +# Configuration LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" BASE_DIR="/usr/local/share/proxmenux" UTILS_FILE="$BASE_DIR/utils.sh" @@ -20,39 +18,49 @@ VENV_PATH="/opt/googletrans-env" if [[ -f "$UTILS_FILE" ]]; then source "$UTILS_FILE" fi + load_language initialize_cache + # ========================================================== - while true; do - OPTION=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "GPUs and Coral-TPU Menu")" \ - --menu "\n$(translate "Select an option:")" 20 70 8 \ - "1" "$(translate "Add HW iGPU acceleration to an LXC")" \ - "2" "$(translate "Add Coral TPU to an LXC")" \ - "3" "$(translate "Install/Update Coral TPU on the Host")" \ - "4" "$(translate "Return to Main Menu")" \ - 2>&1 >/dev/tty) +while true; do + OPTION=$(dialog --colors --backtitle "ProxMenux" \ + --title "$(translate "GPUs and Coral-TPU Menu")" \ + --menu "\n$(translate "Select an option:")" 25 80 15 \ + "" "\Z4──────────────────────── $(translate "HOST") ─────────────────────────\Zn" \ + "1" "$(translate "Install NVIDIA Drivers on Host")" \ + "2" "$(translate "Update NVIDIA Drivers (Host + LXC)")" \ + "3" "$(translate "Install/Update Coral TPU on Host")" \ + "" "\Z4──────────────────────── $(translate "LXC") ──────────────────────────\Zn" \ + "4" "$(translate "Add GPU to LXC (Intel / AMD / NVIDIA)")" \ + "5" "$(translate "Add Coral TPU to LXC")" \ + "" "" \ + "0" "$(translate "Return to Main Menu")" \ + 2>&1 >/dev/tty + ) || { exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"; } - case $OPTION in - 1) - bash "$LOCAL_SCRIPTS/gpu_tpu/configure_igpu_lxc.sh" - if [ $? -ne 0 ]; then - return - fi - ;; - 2) - bash "$LOCAL_SCRIPTS/gpu_tpu/install_coral_lxc.sh" - if [ $? -ne 0 ]; then - return - fi - ;; - 3) - bash "$LOCAL_SCRIPTS/gpu_tpu/install_coral_pve9.sh" - if [ $? -ne 0 ]; then - return - fi - ;; - 4) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;; - *) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;; - esac - done + case "$OPTION" in + 1) + bash "$LOCAL_SCRIPTS/gpu_tpu/nvidia_installer.sh" + ;; + 2) + bash "$LOCAL_SCRIPTS/gpu_tpu/nvidia_update.sh" + ;; + 3) + bash "$LOCAL_SCRIPTS/gpu_tpu/install_coral_pve9.sh" + ;; + 4) + bash "$LOCAL_SCRIPTS/gpu_tpu/add_gpu_lxc.sh" + ;; + 5) + bash "$LOCAL_SCRIPTS/gpu_tpu/install_coral_lxc.sh" + ;; + 0) + exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" + ;; + *) + exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" + ;; + esac +done diff --git a/scripts/menus/share_menu.sh b/scripts/menus/share_menu.sh index 1bc7217c..422b35d0 100644 --- a/scripts/menus/share_menu.sh +++ b/scripts/menus/share_menu.sh @@ -32,6 +32,8 @@ while true; do "1" "$(translate "Configure NFS shared on Host")" \ "2" "$(translate "Configure Samba shared on Host")" \ "3" "$(translate "Configure Local Shared on Host")" \ + "9" "$(translate "Add Local Disk as Proxmox Storage")" \ + "10" "$(translate "Add iSCSI Target as Proxmox Storage")" \ "" "\Z4──────────────────────── $(translate "LXC") ─────────────────────────\Zn" \ "4" "$(translate "Configure LXC Mount Points (Host ↔ Container)")" \ "" "" \ @@ -59,7 +61,13 @@ while true; do ;; 3) bash "$LOCAL_SCRIPTS/share/local-shared-manager.sh" - ;; + ;; + 9) + bash "$LOCAL_SCRIPTS/share/disk_host.sh" + ;; + 10) + bash "$LOCAL_SCRIPTS/share/iscsi_host.sh" + ;; 4) bash "$LOCAL_SCRIPTS/share/lxc-mount-manager_minimal.sh" ;; diff --git a/scripts/share/disk_host.sh b/scripts/share/disk_host.sh new file mode 100644 index 00000000..eecbd548 --- /dev/null +++ b/scripts/share/disk_host.sh @@ -0,0 +1,656 @@ +#!/bin/bash +# ========================================================== +# ProxMenux - Local Disk Manager for Proxmox Host +# ========================================================== +# Author : MacRimi +# Copyright : (c) 2024 MacRimi +# License : MIT +# ========================================================== +# Description: +# Adds local SCSI/SATA/NVMe disks as Proxmox directory storage +# (pvesm add dir). The disk is formatted (ext4 or xfs), mounted +# permanently, and registered in Proxmox. +# ========================================================== + +LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" +BASE_DIR="/usr/local/share/proxmenux" +UTILS_FILE="$BASE_DIR/utils.sh" + +if [[ -f "$UTILS_FILE" ]]; then + source "$UTILS_FILE" +fi + +load_language +initialize_cache + +if ! command -v pveversion >/dev/null 2>&1; then + dialog --backtitle "ProxMenux" --title "$(translate "Error")" \ + --msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60 + exit 1 +fi + +# ========================================================== +# STORAGE CONFIG READER +# ========================================================== +get_storage_config() { + local storage_id="$1" + awk -v id="$storage_id" ' + /^[a-z]+: / { found = ($0 ~ ": "id"$"); next } + found && /^[^ \t]/ { exit } + found { print } + ' /etc/pve/storage.cfg +} + +# ========================================================== +# DISK DETECTION +# ========================================================== + +get_available_disks() { + # List block devices that are: + # - Whole disks (not partitions, not loop, not dm) + # - Not the system disk (where / is mounted) + local system_disk + system_disk=$(lsblk -ndo PKNAME "$(findmnt -n -o SOURCE /)" 2>/dev/null | head -1) + + while IFS= read -r line; do + local name size type model ro + name=$(echo "$line" | awk '{print $1}') + size=$(echo "$line" | awk '{print $2}') + type=$(echo "$line" | awk '{print $3}') + model=$(echo "$line" | awk '{for(i=4;i<=NF;i++) printf "%s ", $i; print ""}' | sed 's/[[:space:]]*$//') + ro=$(echo "$line" | awk '{print $NF}') + + # Only whole disks + [[ "$type" != "disk" ]] && continue + # Skip read-only + [[ "$ro" == "1" ]] && continue + # Skip system disk + [[ "$name" == "$system_disk" ]] && continue + + # Check if fully mounted (any partition or the disk itself is mounted at /) + local is_mounted=false + if lsblk -no MOUNTPOINT "/dev/$name" 2>/dev/null | grep -qE "^/[[:space:]]*$|^/boot"; then + is_mounted=true + fi + [[ "$is_mounted" == true ]] && continue + + local info="${size}" + [[ -n "$model" && "$model" != " " ]] && info="${size} — ${model}" + + # Show mount status + local mount_info + mount_info=$(lsblk -no MOUNTPOINT "/dev/$name" 2>/dev/null | grep -v "^$" | tr '\n' ' ' | sed 's/[[:space:]]*$//') + if [[ -n "$mount_info" ]]; then + info="${info} [${mount_info}]" + fi + + echo "/dev/$name|$info" + done < <(lsblk -ndo NAME,SIZE,TYPE,MODEL,RO 2>/dev/null) +} + +select_disk() { + show_proxmenux_logo + msg_title "$(translate "Add Local Disk as Proxmox Storage")" + msg_info "$(translate "Scanning available disks...")" + + local disk_list + disk_list=$(get_available_disks) + + if [[ -z "$disk_list" ]]; then + dialog --backtitle "ProxMenux" --title "$(translate "No Disks Found")" \ + --msgbox "\n$(translate "No available disks found.")\n\n$(translate "All disks may already be in use or mounted.")" 10 60 + return 1 + fi + + local options=() + while IFS='|' read -r device info; do + [[ -n "$device" ]] && options+=("$device" "$info") + done <<< "$disk_list" + + if [[ ${#options[@]} -eq 0 ]]; then + dialog --backtitle "ProxMenux" --title "$(translate "No Disks Found")" \ + --msgbox "\n$(translate "No suitable disks found.")" 8 60 + return 1 + fi + + SELECTED_DISK=$(dialog --backtitle "ProxMenux" --title "$(translate "Select Disk")" \ + --menu "\n$(translate "Select the disk to add as Proxmox storage:")\n$(translate "WARNING: All data on selected disk will be ERASED if formatted.")" \ + 20 80 10 "${options[@]}" 3>&1 1>&2 2>&3) + [[ -z "$SELECTED_DISK" ]] && return 1 + + return 0 +} + +inspect_disk() { + local disk="$1" + + # Check existing partitions/filesystem + local partition_info + partition_info=$(lsblk -no NAME,SIZE,FSTYPE,MOUNTPOINT "$disk" 2>/dev/null | tail -n +2) + + local existing_fs + existing_fs=$(blkid -s TYPE -o value "$disk" 2>/dev/null || true) + + DISK_HAS_DATA=false + DISK_EXISTING_FS="" + + if [[ -n "$partition_info" || -n "$existing_fs" ]]; then + DISK_HAS_DATA=true + DISK_EXISTING_FS="$existing_fs" + fi + + return 0 +} + +select_partition_action() { + local disk="$1" + inspect_disk "$disk" + + local disk_size + disk_size=$(lsblk -ndo SIZE "$disk" 2>/dev/null) + + if [[ "$DISK_HAS_DATA" == "true" ]]; then + local msg="$(translate "Disk:"): $disk ($disk_size)\n" + [[ -n "$DISK_EXISTING_FS" ]] && msg+="$(translate "Existing filesystem:"): $DISK_EXISTING_FS\n" + msg+="\n$(translate "Options:")\n" + msg+="• $(translate "Format: ERASE all data and create new filesystem")\n" + [[ -n "$DISK_EXISTING_FS" ]] && msg+="• $(translate "Use existing: mount without formatting")\n" + msg+="\n$(translate "Continue?")" + + DISK_ACTION=$(whiptail --title "$(translate "Disk Setup")" \ + --menu "$msg" 20 80 3 \ + "format" "$(translate "Format disk (ERASE all data)")" \ + $(if [[ -n "$DISK_EXISTING_FS" ]]; then echo '"use_existing" "'"$(translate "Use existing filesystem")"'"'; fi) \ + "cancel" "$(translate "Cancel")" \ + 3>&1 1>&2 2>&3) + else + DISK_ACTION=$(whiptail --title "$(translate "Disk Setup")" \ + --menu "$(translate "Disk:"): $disk ($disk_size)\n\n$(translate "Disk appears empty. It will be formatted.")" \ + 14 70 2 \ + "format" "$(translate "Format and add as Proxmox storage")" \ + "cancel" "$(translate "Cancel")" \ + 3>&1 1>&2 2>&3) + fi + + [[ -z "$DISK_ACTION" || "$DISK_ACTION" == "cancel" ]] && return 1 + return 0 +} + +select_filesystem() { + FILESYSTEM=$(whiptail --title "$(translate "Select Filesystem")" \ + --menu "$(translate "Choose filesystem for the disk:")" 14 60 3 \ + "ext4" "$(translate "ext4 — recommended, most compatible")" \ + "xfs" "$(translate "xfs — better for large files and VMs")" \ + 3>&1 1>&2 2>&3) + [[ -z "$FILESYSTEM" ]] && return 1 + return 0 +} + +# ========================================================== +# STORAGE CONFIGURATION +# ========================================================== + +configure_disk_storage() { + local disk_name + disk_name=$(basename "$SELECTED_DISK") + + STORAGE_ID=$(whiptail --inputbox "$(translate "Enter storage ID for Proxmox:")" \ + 10 60 "disk-${disk_name}" \ + --title "$(translate "Storage ID")" 3>&1 1>&2 2>&3) + [[ $? -ne 0 ]] && return 1 + [[ -z "$STORAGE_ID" ]] && STORAGE_ID="disk-${disk_name}" + + if [[ ! "$STORAGE_ID" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then + whiptail --msgbox "$(translate "Invalid storage ID. Use only letters, numbers, hyphens and underscores.")" 8 70 + return 1 + fi + + MOUNT_PATH="/mnt/${STORAGE_ID}" + MOUNT_PATH=$(whiptail --inputbox "$(translate "Enter mount path on host:")" \ + 10 60 "$MOUNT_PATH" \ + --title "$(translate "Mount Path")" 3>&1 1>&2 2>&3) + [[ $? -ne 0 || -z "$MOUNT_PATH" ]] && return 1 + + CONTENT_TYPE=$(whiptail --title "$(translate "Content Types")" \ + --menu "$(translate "Select content types for this storage:")" 16 70 5 \ + "1" "$(translate "VM Storage (images, backup)")" \ + "2" "$(translate "Standard NAS (backup, iso, vztmpl)")" \ + "3" "$(translate "All types (images, backup, iso, vztmpl, snippets)")" \ + "4" "$(translate "Custom")" \ + 3>&1 1>&2 2>&3) + [[ $? -ne 0 ]] && return 1 + + case "$CONTENT_TYPE" in + 1) MOUNT_CONTENT="images,backup" ;; + 2) MOUNT_CONTENT="backup,iso,vztmpl" ;; + 3) MOUNT_CONTENT="images,backup,iso,vztmpl,snippets" ;; + 4) + MOUNT_CONTENT=$(whiptail --inputbox "$(translate "Enter content types (comma-separated):")" \ + 10 70 "images,backup" --title "$(translate "Custom Content")" 3>&1 1>&2 2>&3) + [[ $? -ne 0 || -z "$MOUNT_CONTENT" ]] && MOUNT_CONTENT="images,backup" + ;; + *) return 1 ;; + esac + + return 0 +} + +# ========================================================== +# DISK SETUP AND MOUNT +# ========================================================== + +format_and_mount_disk() { + local disk="$1" + local mount_path="$2" + local filesystem="$3" + + # Final confirmation before any destructive operation + local disk_size + disk_size=$(lsblk -ndo SIZE "$disk" 2>/dev/null) + if ! whiptail --yesno \ + "$(translate "FINAL CONFIRMATION — DATA WILL BE ERASED")\n\n$(translate "Disk:"): $disk ($disk_size)\n$(translate "Filesystem:"): $filesystem\n$(translate "Mount path:"): $mount_path\n\n$(translate "ALL DATA ON") $disk $(translate "WILL BE PERMANENTLY ERASED.")\n\n$(translate "Are you absolutely sure?")" \ + 14 80 --title "$(translate "CONFIRM FORMAT")"; then + return 1 + fi + + msg_info "$(translate "Wiping existing partition table...")" + wipefs -a "$disk" >/dev/null 2>&1 || true + sgdisk --zap-all "$disk" >/dev/null 2>&1 || true + + msg_info "$(translate "Creating partition...")" + if ! parted -s "$disk" mklabel gpt mkpart primary 0% 100% >/dev/null 2>&1; then + msg_error "$(translate "Failed to create partition table")" + return 1 + fi + + # Wait for kernel to recognize new partition + sleep 2 + partprobe "$disk" 2>/dev/null || true + sleep 1 + + # Determine partition device + local partition + if [[ "$disk" =~ [0-9]$ ]]; then + partition="${disk}p1" + else + partition="${disk}1" + fi + + msg_info "$(translate "Formatting as") $filesystem..." + case "$filesystem" in + ext4) + if ! mkfs.ext4 -F -L "$STORAGE_ID" "$partition" >/dev/null 2>&1; then + msg_error "$(translate "Failed to format disk as ext4")" + return 1 + fi + ;; + xfs) + if ! mkfs.xfs -f -L "$STORAGE_ID" "$partition" >/dev/null 2>&1; then + msg_error "$(translate "Failed to format disk as xfs")" + return 1 + fi + ;; + esac + + msg_ok "$(translate "Disk formatted as") $filesystem" + + DISK_PARTITION="$partition" + return 0 +} + +mount_disk_permanently() { + local partition="$1" + local mount_path="$2" + local filesystem="$3" + + msg_info "$(translate "Creating mount point...")" + if ! mkdir -p "$mount_path"; then + msg_error "$(translate "Failed to create mount point:") $mount_path" + return 1 + fi + + msg_info "$(translate "Mounting disk...")" + if ! mount -t "$filesystem" "$partition" "$mount_path" 2>/dev/null; then + msg_error "$(translate "Failed to mount disk")" + return 1 + fi + msg_ok "$(translate "Disk mounted at") $mount_path" + + msg_info "$(translate "Adding to /etc/fstab for permanent mounting...")" + local disk_uuid + disk_uuid=$(blkid -s UUID -o value "$partition" 2>/dev/null) + + if [[ -n "$disk_uuid" ]]; then + # Remove any existing fstab entry for this UUID or mount point + sed -i "\|UUID=$disk_uuid|d" /etc/fstab + sed -i "\|[[:space:]]$mount_path[[:space:]]|d" /etc/fstab + echo "UUID=$disk_uuid $mount_path $filesystem defaults,nofail 0 2" >> /etc/fstab + msg_ok "$(translate "Added to /etc/fstab using UUID")" + else + sed -i "\|[[:space:]]$mount_path[[:space:]]|d" /etc/fstab + echo "$partition $mount_path $filesystem defaults,nofail 0 2" >> /etc/fstab + msg_ok "$(translate "Added to /etc/fstab using device path")" + fi + + systemctl daemon-reload 2>/dev/null || true + return 0 +} + +mount_existing_disk() { + local disk="$1" + local mount_path="$2" + + local existing_fs + existing_fs=$(blkid -s TYPE -o value "$disk" 2>/dev/null || true) + + if [[ -z "$existing_fs" ]]; then + msg_error "$(translate "Cannot detect filesystem on") $disk" + return 1 + fi + + msg_info "$(translate "Creating mount point...")" + mkdir -p "$mount_path" + + msg_info "$(translate "Mounting existing") $existing_fs $(translate "filesystem...")" + if ! mount "$disk" "$mount_path" 2>/dev/null; then + msg_error "$(translate "Failed to mount disk")" + return 1 + fi + msg_ok "$(translate "Disk mounted at") $mount_path" + + # Add to fstab + local disk_uuid + disk_uuid=$(blkid -s UUID -o value "$disk" 2>/dev/null) + if [[ -n "$disk_uuid" ]]; then + sed -i "\|UUID=$disk_uuid|d" /etc/fstab + sed -i "\|[[:space:]]$mount_path[[:space:]]|d" /etc/fstab + echo "UUID=$disk_uuid $mount_path $existing_fs defaults,nofail 0 2" >> /etc/fstab + msg_ok "$(translate "Added to /etc/fstab")" + fi + + DISK_PARTITION="$disk" + systemctl daemon-reload 2>/dev/null || true + return 0 +} + +add_proxmox_dir_storage() { + local storage_id="$1" + local path="$2" + local content="$3" + + if ! command -v pvesm >/dev/null 2>&1; then + msg_error "$(translate "pvesm command not found. This should not happen on Proxmox.")" + return 1 + fi + + if pvesm status "$storage_id" >/dev/null 2>&1; then + msg_warn "$(translate "Storage ID already exists:") $storage_id" + if ! whiptail --yesno "$(translate "Storage ID already exists. Do you want to remove and recreate it?")" \ + 8 60 --title "$(translate "Storage Exists")"; then + return 0 + fi + pvesm remove "$storage_id" 2>/dev/null || true + fi + + msg_info "$(translate "Registering disk as Proxmox storage...")" + local pvesm_output + if pvesm_output=$(pvesm add dir "$storage_id" \ + --path "$path" \ + --content "$content" 2>&1); then + + msg_ok "$(translate "Directory storage added successfully to Proxmox!")" + echo -e "" + echo -e "${TAB}${BOLD}$(translate "Storage Added:")${CL}" + echo -e "${TAB}${BGN}$(translate "Storage ID:")${CL} ${BL}$storage_id${CL}" + echo -e "${TAB}${BGN}$(translate "Path:")${CL} ${BL}$path${CL}" + echo -e "${TAB}${BGN}$(translate "Content Types:")${CL} ${BL}$content${CL}" + echo -e "" + msg_ok "$(translate "Storage is now available in Proxmox web interface under Datacenter > Storage")" + return 0 + else + msg_error "$(translate "Failed to add storage to Proxmox.")" + echo -e "${TAB}$(translate "Error details:"): $pvesm_output" + echo -e "" + msg_info2 "$(translate "You can add it manually through:")" + echo -e "${TAB}• $(translate "Proxmox web interface: Datacenter > Storage > Add > Directory")" + echo -e "${TAB}• pvesm add dir $storage_id --path $path --content $content" + return 1 + fi +} + +# ========================================================== +# MAIN OPERATIONS +# ========================================================== + +add_disk_to_proxmox() { + # Check required tools + for tool in parted mkfs.ext4 blkid lsblk; do + if ! command -v "$tool" >/dev/null 2>&1; then + msg_info "$(translate "Installing required tools...")" + apt-get update &>/dev/null + apt-get install -y parted e2fsprogs util-linux xfsprogs gdisk &>/dev/null + break + fi + done + + # Step 1: Select disk + select_disk || return + + # Step 2: Inspect and choose action + select_partition_action "$SELECTED_DISK" || return + + # Step 3: Filesystem selection (only if formatting) + if [[ "$DISK_ACTION" == "format" ]]; then + select_filesystem || return + fi + + # Step 4: Configure storage options + configure_disk_storage || return + + show_proxmenux_logo + msg_title "$(translate "Add Local Disk as Proxmox Storage")" + msg_ok "$(translate "Disk:") $SELECTED_DISK" + msg_ok "$(translate "Action:") $DISK_ACTION" + [[ "$DISK_ACTION" == "format" ]] && msg_ok "$(translate "Filesystem:") $FILESYSTEM" + msg_ok "$(translate "Mount path:") $MOUNT_PATH" + msg_ok "$(translate "Storage ID:") $STORAGE_ID" + msg_ok "$(translate "Content:") $MOUNT_CONTENT" + echo "" + + # Step 5: Format/mount + case "$DISK_ACTION" in + format) + format_and_mount_disk "$SELECTED_DISK" "$MOUNT_PATH" "$FILESYSTEM" || { + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return 1 + } + mount_disk_permanently "$DISK_PARTITION" "$MOUNT_PATH" "$FILESYSTEM" || { + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return 1 + } + ;; + use_existing) + mount_existing_disk "$SELECTED_DISK" "$MOUNT_PATH" || { + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return 1 + } + ;; + esac + + # Step 6: Register in Proxmox + add_proxmox_dir_storage "$STORAGE_ID" "$MOUNT_PATH" "$MOUNT_CONTENT" + + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r +} + +view_disk_storages() { + show_proxmenux_logo + msg_title "$(translate "Local Disk Storages in Proxmox")" + + echo "==================================================" + echo "" + + if ! command -v pvesm >/dev/null 2>&1; then + msg_error "$(translate "pvesm not found.")" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return + fi + + # Show all directory storages + DIR_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "dir" {print $1, $3}') + if [[ -z "$DIR_STORAGES" ]]; then + msg_warn "$(translate "No directory storage configured in Proxmox.")" + echo "" + msg_info2 "$(translate "Use option 1 to add a local disk as Proxmox storage.")" + else + echo -e "${BOLD}$(translate "Directory Storages:")${CL}" + echo "" + while IFS=" " read -r storage_id storage_status; do + [[ -z "$storage_id" ]] && continue + local storage_info path content + storage_info=$(get_storage_config "$storage_id") + path=$(echo "$storage_info" | awk '$1 == "path" {print $2}') + content=$(echo "$storage_info" | awk '$1 == "content" {print $2}') + + local disk_device="" + if [[ -n "$path" ]]; then + disk_device=$(findmnt -n -o SOURCE "$path" 2>/dev/null || true) + fi + + local disk_size="" + if [[ -n "$disk_device" ]]; then + disk_size=$(lsblk -ndo SIZE "$disk_device" 2>/dev/null || true) + fi + + echo -e "${TAB}${BOLD}$storage_id${CL}" + echo -e "${TAB} ${BGN}$(translate "Path:")${CL} ${BL}$path${CL}" + [[ -n "$disk_device" ]] && echo -e "${TAB} ${BGN}$(translate "Device:")${CL} ${BL}$disk_device${CL}" + [[ -n "$disk_size" ]] && echo -e "${TAB} ${BGN}$(translate "Size:")${CL} ${BL}$disk_size${CL}" + echo -e "${TAB} ${BGN}$(translate "Content:")${CL} ${BL}$content${CL}" + if [[ "$storage_status" == "active" ]]; then + echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${GN}$(translate "Active")${CL}" + else + echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${RD}$storage_status${CL}" + fi + echo "" + done <<< "$DIR_STORAGES" + fi + + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r +} + +remove_disk_storage() { + if ! command -v pvesm >/dev/null 2>&1; then + dialog --backtitle "ProxMenux" --title "$(translate "Error")" \ + --msgbox "\n$(translate "pvesm not found.")" 8 60 + return + fi + + DIR_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "dir" {print $1}') + if [[ -z "$DIR_STORAGES" ]]; then + dialog --backtitle "ProxMenux" --title "$(translate "No Disk Storage")" \ + --msgbox "\n$(translate "No directory storage found in Proxmox.")" 8 60 + return + fi + + OPTIONS=() + while IFS= read -r storage_id; do + [[ -z "$storage_id" ]] && continue + local path + path=$(get_storage_config "$storage_id" | awk '$1 == "path" {print $2}') + OPTIONS+=("$storage_id" "${path:-unknown}") + done <<< "$DIR_STORAGES" + + SELECTED=$(dialog --backtitle "ProxMenux" --title "$(translate "Remove Disk Storage")" \ + --menu "$(translate "Select storage to remove:")" 20 80 10 \ + "${OPTIONS[@]}" 3>&1 1>&2 2>&3) + [[ -z "$SELECTED" ]] && return + + local path content + path=$(get_storage_config "$SELECTED" | awk '$1 == "path" {print $2}') + content=$(get_storage_config "$SELECTED" | awk '$1 == "content" {print $2}') + + if whiptail --yesno "$(translate "Remove Proxmox storage:")\n\n$SELECTED\n\n$(translate "Path:"): $path\n$(translate "Content:"): $content\n\n$(translate "This removes the storage registration from Proxmox.")\n$(translate "The disk and its data will NOT be erased.")\n$(translate "The disk will remain mounted at:"): $path" \ + 18 80 --title "$(translate "Confirm Remove")"; then + + show_proxmenux_logo + msg_title "$(translate "Remove Disk Storage")" + + if pvesm remove "$SELECTED" 2>/dev/null; then + msg_ok "$(translate "Storage") $SELECTED $(translate "removed from Proxmox.")" + echo "" + msg_info2 "$(translate "The disk remains mounted at:"): $path" + msg_info2 "$(translate "The fstab entry is still present. Remove manually if needed.")" + else + msg_error "$(translate "Failed to remove storage.")" + fi + + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + fi +} + +list_available_disks() { + show_proxmenux_logo + msg_title "$(translate "Available Disks on Host")" + + echo "==================================================" + echo "" + + echo -e "${BOLD}$(translate "All block devices:")${CL}" + echo "" + lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL 2>/dev/null + echo "" + + echo -e "${BOLD}$(translate "Proxmox directory storages:")${CL}" + if command -v pvesm >/dev/null 2>&1; then + pvesm status 2>/dev/null | awk '$2 == "dir" {print " " $1, $2, $3}' || echo " $(translate "None")" + fi + + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r +} + +# ========================================================== +# MAIN MENU +# ========================================================== + +while true; do + CHOICE=$(dialog --backtitle "ProxMenux" \ + --title "$(translate "Local Disk Manager - Proxmox Host")" \ + --menu "$(translate "Choose an option:")" 18 70 6 \ + "1" "$(translate "Add Local Disk as Proxmox Storage")" \ + "2" "$(translate "View Disk Storages")" \ + "3" "$(translate "Remove Disk Storage")" \ + "4" "$(translate "List Available Disks")" \ + "5" "$(translate "Exit")" \ + 3>&1 1>&2 2>&3) + + RETVAL=$? + if [[ $RETVAL -ne 0 ]]; then + exit 0 + fi + + case $CHOICE in + 1) add_disk_to_proxmox ;; + 2) view_disk_storages ;; + 3) remove_disk_storage ;; + 4) list_available_disks ;; + 5) exit 0 ;; + *) exit 0 ;; + esac +done diff --git a/scripts/share/iscsi_host.sh b/scripts/share/iscsi_host.sh new file mode 100644 index 00000000..dfac2b50 --- /dev/null +++ b/scripts/share/iscsi_host.sh @@ -0,0 +1,518 @@ +#!/bin/bash +# ========================================================== +# ProxMenux - iSCSI Host Manager for Proxmox Host +# ========================================================== +# Author : MacRimi +# Copyright : (c) 2024 MacRimi +# License : MIT +# ========================================================== +# Description: +# Adds iSCSI targets as Proxmox storage (pvesm add iscsi). +# Proxmox manages the connection natively via open-iscsi. +# iSCSI storage provides block devices for VM disk images. +# ========================================================== + +LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" +BASE_DIR="/usr/local/share/proxmenux" +UTILS_FILE="$BASE_DIR/utils.sh" + +if [[ -f "$UTILS_FILE" ]]; then + source "$UTILS_FILE" +fi + +load_language +initialize_cache + +if ! command -v pveversion >/dev/null 2>&1; then + dialog --backtitle "ProxMenux" --title "$(translate "Error")" \ + --msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60 + exit 1 +fi + +# ========================================================== +# STORAGE CONFIG READER +# ========================================================== +get_storage_config() { + local storage_id="$1" + awk -v id="$storage_id" ' + /^[a-z]+: / { found = ($0 ~ ": "id"$"); next } + found && /^[^ \t]/ { exit } + found { print } + ' /etc/pve/storage.cfg +} + +# ========================================================== +# TOOLS +# ========================================================== + +ensure_iscsi_tools() { + if ! command -v iscsiadm >/dev/null 2>&1; then + msg_info "$(translate "Installing iSCSI initiator tools...")" + apt-get update &>/dev/null + apt-get install -y open-iscsi &>/dev/null + systemctl enable --now iscsid 2>/dev/null || true + msg_ok "$(translate "iSCSI tools installed")" + fi + + if ! systemctl is-active --quiet iscsid 2>/dev/null; then + msg_info "$(translate "Starting iSCSI daemon...")" + systemctl start iscsid 2>/dev/null || true + fi +} + +# ========================================================== +# TARGET DISCOVERY +# ========================================================== + +select_iscsi_portal() { + ISCSI_PORTAL=$(whiptail --inputbox \ + "$(translate "Enter iSCSI target portal IP or hostname:")\n\n$(translate "Examples:")\n 192.168.1.100\n 192.168.1.100:3260\n nas.local" \ + 14 65 \ + --title "$(translate "iSCSI Portal")" 3>&1 1>&2 2>&3) + [[ $? -ne 0 || -z "$ISCSI_PORTAL" ]] && return 1 + + # Normalise: if no port specified, add default 3260 + if [[ ! "$ISCSI_PORTAL" =~ :[0-9]+$ ]]; then + ISCSI_PORTAL_DISPLAY="$ISCSI_PORTAL" + ISCSI_PORTAL_FULL="${ISCSI_PORTAL}:3260" + else + ISCSI_PORTAL_DISPLAY="$ISCSI_PORTAL" + ISCSI_PORTAL_FULL="$ISCSI_PORTAL" + fi + + # Extract host for ping + ISCSI_HOST=$(echo "$ISCSI_PORTAL" | cut -d: -f1) + return 0 +} + +discover_iscsi_targets() { + show_proxmenux_logo + msg_title "$(translate "Add iSCSI Target as Proxmox Storage")" + msg_ok "$(translate "Portal:") $ISCSI_PORTAL_DISPLAY" + msg_info "$(translate "Testing connectivity to portal...")" + + if ! ping -c 1 -W 3 "$ISCSI_HOST" >/dev/null 2>&1; then + msg_error "$(translate "Cannot reach portal:") $ISCSI_HOST" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return 1 + fi + msg_ok "$(translate "Portal is reachable")" + + if ! nc -z -w 3 "$ISCSI_HOST" "${ISCSI_PORTAL_FULL##*:}" 2>/dev/null; then + msg_warn "$(translate "iSCSI port") ${ISCSI_PORTAL_FULL##*:} $(translate "may be closed — trying discovery anyway...")" + fi + + msg_info "$(translate "Discovering iSCSI targets...")" + DISCOVERY_OUTPUT=$(iscsiadm --mode discovery --type sendtargets \ + --portal "$ISCSI_PORTAL_FULL" 2>&1) + DISCOVERY_RESULT=$? + + if [[ $DISCOVERY_RESULT -ne 0 ]]; then + msg_error "$(translate "iSCSI discovery failed")" + echo -e "${TAB}$(translate "Error:"): $DISCOVERY_OUTPUT" + echo "" + msg_info2 "$(translate "Please check:")" + echo -e "${TAB}• $(translate "Portal IP and port are correct")" + echo -e "${TAB}• $(translate "iSCSI service is running on the target")" + echo -e "${TAB}• $(translate "Firewall allows port") ${ISCSI_PORTAL_FULL##*:}" + echo -e "${TAB}• $(translate "Initiator IQN is authorised on the target")" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return 1 + fi + + # Parse discovered targets: format is + TARGETS=$(echo "$DISCOVERY_OUTPUT" | awk '{print $2}' | grep "^iqn\." | sort -u) + + if [[ -z "$TARGETS" ]]; then + msg_warn "$(translate "No iSCSI targets found on portal") $ISCSI_PORTAL_DISPLAY" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return 1 + fi + + msg_ok "$(translate "Discovery successful")" + return 0 +} + +select_iscsi_target() { + local target_count + target_count=$(echo "$TARGETS" | wc -l) + + if [[ "$target_count" -eq 1 ]]; then + ISCSI_TARGET=$(echo "$TARGETS" | head -1) + msg_ok "$(translate "Single target found — selected automatically:") $ISCSI_TARGET" + return 0 + fi + + local options=() + local i=1 + while IFS= read -r iqn; do + [[ -z "$iqn" ]] && continue + # Try to get LUN info for display + local lun_info + lun_info=$(iscsiadm --mode node --targetname "$iqn" --portal "$ISCSI_PORTAL_FULL" \ + --op show 2>/dev/null | grep "node.conn\[0\].address" | awk -F= '{print $2}' | tr -d ' ' || true) + options+=("$i" "$iqn") + i=$((i + 1)) + done <<< "$TARGETS" + + local choice + choice=$(dialog --backtitle "ProxMenux" --title "$(translate "Select iSCSI Target")" \ + --menu "\n$(translate "Select target IQN:")" 20 90 10 \ + "${options[@]}" 3>&1 1>&2 2>&3) + [[ -z "$choice" ]] && return 1 + + ISCSI_TARGET=$(echo "$TARGETS" | sed -n "${choice}p") + [[ -z "$ISCSI_TARGET" ]] && return 1 + return 0 +} + +# ========================================================== +# STORAGE CONFIGURATION +# ========================================================== + +configure_iscsi_storage() { + # Suggest a storage ID derived from target IQN + local iqn_suffix + iqn_suffix=$(echo "$ISCSI_TARGET" | awk -F: '{print $NF}' | tr '.' '-' | cut -c1-20) + local default_id="iscsi-${iqn_suffix}" + + STORAGE_ID=$(whiptail --inputbox "$(translate "Enter storage ID for Proxmox:")" \ + 10 65 "$default_id" \ + --title "$(translate "Storage ID")" 3>&1 1>&2 2>&3) + [[ $? -ne 0 ]] && return 1 + [[ -z "$STORAGE_ID" ]] && STORAGE_ID="$default_id" + + if [[ ! "$STORAGE_ID" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then + whiptail --msgbox "$(translate "Invalid storage ID. Use only letters, numbers, hyphens and underscores.")" 8 70 + return 1 + fi + + # iSCSI in Proxmox exposes block devices — content is always 'images' + # (no file-level access like NFS/CIFS) + MOUNT_CONTENT="images" + + whiptail --title "$(translate "iSCSI Content Type")" \ + --msgbox "$(translate "iSCSI storage provides raw block devices for VM disk images.")\n\n$(translate "Content type is fixed to:")\n\n images\n\n$(translate "Each LUN will appear as a block device assignable to VMs.")" \ + 12 70 + + return 0 +} + +# ========================================================== +# PROXMOX INTEGRATION +# ========================================================== + +add_proxmox_iscsi_storage() { + local storage_id="$1" + local portal="$2" + local target="$3" + local content="${4:-images}" + + if ! command -v pvesm >/dev/null 2>&1; then + msg_error "$(translate "pvesm command not found. This should not happen on Proxmox.")" + return 1 + fi + + if pvesm status "$storage_id" >/dev/null 2>&1; then + msg_warn "$(translate "Storage ID already exists:") $storage_id" + if ! whiptail --yesno "$(translate "Storage ID already exists. Do you want to remove and recreate it?")" \ + 8 60 --title "$(translate "Storage Exists")"; then + return 0 + fi + pvesm remove "$storage_id" 2>/dev/null || true + fi + + msg_ok "$(translate "Storage ID is available")" + msg_info "$(translate "Adding iSCSI storage to Proxmox...")" + + local pvesm_output pvesm_result + pvesm_output=$(pvesm add iscsi "$storage_id" \ + --portal "$portal" \ + --target "$target" \ + --content "$content" 2>&1) + pvesm_result=$? + + if [[ $pvesm_result -eq 0 ]]; then + msg_ok "$(translate "iSCSI storage added successfully to Proxmox!")" + echo -e "" + echo -e "${TAB}${BOLD}$(translate "Storage Added:")${CL}" + echo -e "${TAB}${BGN}$(translate "Storage ID:")${CL} ${BL}$storage_id${CL}" + echo -e "${TAB}${BGN}$(translate "Portal:")${CL} ${BL}$portal${CL}" + echo -e "${TAB}${BGN}$(translate "Target IQN:")${CL} ${BL}$target${CL}" + echo -e "${TAB}${BGN}$(translate "Content Types:")${CL} ${BL}$content${CL}" + echo -e "" + msg_ok "$(translate "Storage is now available in Proxmox web interface under Datacenter > Storage")" + msg_info2 "$(translate "LUNs appear as block devices assignable to VMs")" + return 0 + else + msg_error "$(translate "Failed to add iSCSI storage to Proxmox.")" + echo -e "${TAB}$(translate "Error details:"): $pvesm_output" + echo -e "" + msg_info2 "$(translate "You can add it manually through:")" + echo -e "${TAB}• $(translate "Proxmox web interface: Datacenter > Storage > Add > iSCSI")" + echo -e "${TAB}• pvesm add iscsi $storage_id --portal $portal --target $target --content $content" + return 1 + fi +} + +# ========================================================== +# MAIN OPERATIONS +# ========================================================== + +add_iscsi_to_proxmox() { + ensure_iscsi_tools + + # Step 1: Enter portal + select_iscsi_portal || return + + # Step 2: Discover targets + discover_iscsi_targets || return + + # Step 3: Select target + select_iscsi_target || return + + show_proxmenux_logo + msg_title "$(translate "Add iSCSI Target as Proxmox Storage")" + msg_ok "$(translate "Portal:") $ISCSI_PORTAL_DISPLAY" + msg_ok "$(translate "Target:") $ISCSI_TARGET" + + # Step 4: Configure storage + configure_iscsi_storage || return + + # Step 5: Add to Proxmox + show_proxmenux_logo + msg_title "$(translate "Add iSCSI Target as Proxmox Storage")" + msg_ok "$(translate "Portal:") $ISCSI_PORTAL_DISPLAY" + msg_ok "$(translate "Target:") $ISCSI_TARGET" + msg_ok "$(translate "Storage ID:") $STORAGE_ID" + msg_ok "$(translate "Content:") $MOUNT_CONTENT" + echo -e "" + + add_proxmox_iscsi_storage "$STORAGE_ID" "$ISCSI_PORTAL_FULL" "$ISCSI_TARGET" "$MOUNT_CONTENT" + + echo -e "" + msg_success "$(translate "Press Enter to continue...")" + read -r +} + +view_iscsi_storages() { + show_proxmenux_logo + msg_title "$(translate "iSCSI Storages in Proxmox")" + + echo "==================================================" + echo "" + + if ! command -v pvesm >/dev/null 2>&1; then + msg_error "$(translate "pvesm not found.")" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return + fi + + ISCSI_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "iscsi" {print $1, $3}') + if [[ -z "$ISCSI_STORAGES" ]]; then + msg_warn "$(translate "No iSCSI storage configured in Proxmox.")" + echo "" + msg_info2 "$(translate "Use option 1 to add an iSCSI target as Proxmox storage.")" + else + echo -e "${BOLD}$(translate "iSCSI Storages:")${CL}" + echo "" + while IFS=" " read -r storage_id storage_status; do + [[ -z "$storage_id" ]] && continue + local storage_info portal target content + storage_info=$(get_storage_config "$storage_id") + portal=$(echo "$storage_info" | awk '$1 == "portal" {print $2}') + target=$(echo "$storage_info" | awk '$1 == "target" {print $2}') + content=$(echo "$storage_info" | awk '$1 == "content" {print $2}') + + echo -e "${TAB}${BOLD}$storage_id${CL}" + echo -e "${TAB} ${BGN}$(translate "Portal:")${CL} ${BL}$portal${CL}" + echo -e "${TAB} ${BGN}$(translate "Target IQN:")${CL} ${BL}$target${CL}" + echo -e "${TAB} ${BGN}$(translate "Content:")${CL} ${BL}$content${CL}" + if [[ "$storage_status" == "active" ]]; then + echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${GN}$(translate "Active")${CL}" + else + echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${RD}$storage_status${CL}" + fi + echo "" + done <<< "$ISCSI_STORAGES" + fi + + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r +} + +remove_iscsi_storage() { + if ! command -v pvesm >/dev/null 2>&1; then + dialog --backtitle "ProxMenux" --title "$(translate "Error")" \ + --msgbox "\n$(translate "pvesm not found.")" 8 60 + return + fi + + ISCSI_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "iscsi" {print $1}') + if [[ -z "$ISCSI_STORAGES" ]]; then + dialog --backtitle "ProxMenux" --title "$(translate "No iSCSI Storage")" \ + --msgbox "\n$(translate "No iSCSI storage found in Proxmox.")" 8 60 + return + fi + + local options=() + while IFS= read -r storage_id; do + [[ -z "$storage_id" ]] && continue + local storage_info portal target + storage_info=$(get_storage_config "$storage_id") + portal=$(echo "$storage_info" | awk '$1 == "portal" {print $2}') + target=$(echo "$storage_info" | awk '$1 == "target" {print $2}') + options+=("$storage_id" "$portal — ${target:0:40}") + done <<< "$ISCSI_STORAGES" + + local SELECTED + SELECTED=$(dialog --backtitle "ProxMenux" --title "$(translate "Remove iSCSI Storage")" \ + --menu "$(translate "Select storage to remove:")" 20 90 10 \ + "${options[@]}" 3>&1 1>&2 2>&3) + [[ -z "$SELECTED" ]] && return + + local storage_info portal target content + storage_info=$(get_storage_config "$SELECTED") + portal=$(echo "$storage_info" | awk '$1 == "portal" {print $2}') + target=$(echo "$storage_info" | awk '$1 == "target" {print $2}') + content=$(echo "$storage_info" | awk '$1 == "content" {print $2}') + + if whiptail --yesno "$(translate "Remove Proxmox iSCSI storage:")\n\n$SELECTED\n\n$(translate "Portal:"): $portal\n$(translate "Target:"): $target\n$(translate "Content:"): $content\n\n$(translate "This removes the storage from Proxmox. The iSCSI target is not affected.")" \ + 16 80 --title "$(translate "Confirm Remove")"; then + + show_proxmenux_logo + msg_title "$(translate "Remove iSCSI Storage")" + + if pvesm remove "$SELECTED" 2>/dev/null; then + msg_ok "$(translate "Storage") $SELECTED $(translate "removed successfully from Proxmox.")" + else + msg_error "$(translate "Failed to remove storage.")" + fi + + echo -e "" + msg_success "$(translate "Press Enter to continue...")" + read -r + fi +} + +test_iscsi_connectivity() { + show_proxmenux_logo + msg_title "$(translate "Test iSCSI Connectivity")" + + echo "==================================================" + echo "" + + if command -v iscsiadm >/dev/null 2>&1; then + msg_ok "$(translate "iSCSI Initiator: AVAILABLE")" + local initiator_iqn + initiator_iqn=$(cat /etc/iscsi/initiatorname.iscsi 2>/dev/null | grep "^InitiatorName=" | cut -d= -f2) + [[ -n "$initiator_iqn" ]] && echo -e " ${BGN}$(translate "Initiator IQN:")${CL} ${BL}$initiator_iqn${CL}" + + if systemctl is-active --quiet iscsid 2>/dev/null; then + msg_ok "$(translate "iSCSI Daemon (iscsid): RUNNING")" + else + msg_warn "$(translate "iSCSI Daemon (iscsid): STOPPED")" + fi + else + msg_warn "$(translate "iSCSI Initiator: NOT INSTALLED")" + echo -e " $(translate "Install with: apt-get install open-iscsi")" + fi + + echo "" + + if command -v pvesm >/dev/null 2>&1; then + echo -e "${BOLD}$(translate "Proxmox iSCSI Storage Status:")${CL}" + ISCSI_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "iscsi" {print $1, $3}') + + if [[ -n "$ISCSI_STORAGES" ]]; then + while IFS=" " read -r storage_id storage_status; do + [[ -z "$storage_id" ]] && continue + local portal + portal=$(get_storage_config "$storage_id" | awk '$1 == "portal" {print $2}') + local portal_host="${portal%%:*}" + + echo -n " $storage_id ($portal): " + + if ping -c 1 -W 2 "$portal_host" >/dev/null 2>&1; then + echo -ne "${GN}$(translate "Reachable")${CL}" + local portal_port="${portal##*:}" + [[ "$portal_port" == "$portal" ]] && portal_port="3260" + if nc -z -w 2 "$portal_host" "$portal_port" 2>/dev/null; then + echo -e " | iSCSI port $portal_port: ${GN}$(translate "Open")${CL}" + else + echo -e " | iSCSI port $portal_port: ${RD}$(translate "Closed")${CL}" + fi + else + echo -e "${RD}$(translate "Unreachable")${CL}" + fi + + if [[ "$storage_status" == "active" ]]; then + echo -e " $(translate "Proxmox status:") ${GN}$storage_status${CL}" + else + echo -e " $(translate "Proxmox status:") ${RD}$storage_status${CL}" + fi + + # Show active iSCSI sessions for this target + local target + target=$(get_storage_config "$storage_id" | awk '$1 == "target" {print $2}') + if command -v iscsiadm >/dev/null 2>&1; then + local session + session=$(iscsiadm --mode session 2>/dev/null | grep "$target" || true) + if [[ -n "$session" ]]; then + echo -e " $(translate "Active session:") ${GN}$(translate "Connected")${CL}" + else + echo -e " $(translate "Active session:") ${YW}$(translate "No active session")${CL}" + fi + fi + echo "" + done <<< "$ISCSI_STORAGES" + else + echo " $(translate "No iSCSI storage configured.")" + fi + else + msg_warn "$(translate "pvesm not available.")" + fi + + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r +} + +# ========================================================== +# MAIN MENU +# ========================================================== + +while true; do + CHOICE=$(dialog --backtitle "ProxMenux" \ + --title "$(translate "iSCSI Host Manager - Proxmox Host")" \ + --menu "$(translate "Choose an option:")" 18 70 6 \ + "1" "$(translate "Add iSCSI Target as Proxmox Storage")" \ + "2" "$(translate "View iSCSI Storages")" \ + "3" "$(translate "Remove iSCSI Storage")" \ + "4" "$(translate "Test iSCSI Connectivity")" \ + "5" "$(translate "Exit")" \ + 3>&1 1>&2 2>&3) + + RETVAL=$? + if [[ $RETVAL -ne 0 ]]; then + exit 0 + fi + + case $CHOICE in + 1) add_iscsi_to_proxmox ;; + 2) view_iscsi_storages ;; + 3) remove_iscsi_storage ;; + 4) test_iscsi_connectivity ;; + 5) exit 0 ;; + *) exit 0 ;; + esac +done diff --git a/scripts/share/lxc-mount-manager_minimal.sh b/scripts/share/lxc-mount-manager_minimal.sh index 6f72e4ef..19c315e4 100644 --- a/scripts/share/lxc-mount-manager_minimal.sh +++ b/scripts/share/lxc-mount-manager_minimal.sh @@ -5,8 +5,14 @@ # Author : MacRimi # Copyright : (c) 2024 MacRimi # License : MIT -# Version : 5.0-minimal -# Last Updated: $(date +%d/%m/%Y) +# ========================================================== +# Description: +# Adds bind mounts from Proxmox host directories into LXC +# containers using pct set -mpX (Proxmox native). +# +# SAFE DESIGN: This script NEVER modifies permissions, ownership, +# or ACLs on the host or inside the container. All existing +# configurations are preserved as-is. # ========================================================== BASE_DIR="/usr/local/share/proxmenux" @@ -15,406 +21,207 @@ source "$BASE_DIR/utils.sh" load_language initialize_cache - +# ========================================================== +# DIRECTORY DETECTION +# ========================================================== detect_mounted_shares() { local mounted_shares=() - + while IFS= read -r line; do - local device mount_point fs_type options dump pass - read -r device mount_point fs_type options dump pass <<< "$line" - - local is_network=false + local device mount_point fs_type + read -r device mount_point fs_type _ <<< "$line" + local type="" - case "$fs_type" in - nfs|nfs4) - is_network=true - type="NFS" - ;; - cifs) - is_network=true - type="CIFS/SMB" - ;; + nfs|nfs4) type="NFS" ;; + cifs) type="CIFS/SMB" ;; + *) continue ;; esac - - if [[ "$is_network" == true ]]; then - - local exclude_internal=false - local internal_mounts=( - "/mnt/pve/local" - "/mnt/pve/local-lvm" - "/mnt/pve/local-zfs" - "/mnt/pve/backup" - "/mnt/pve/snippets" - "/mnt/pve/dump" - "/mnt/pve/images" - "/mnt/pve/template" - "/mnt/pve/private" - "/mnt/pve/vztmpl" - ) - - for internal_mount in "${internal_mounts[@]}"; do - if [[ "$mount_point" == "$internal_mount" || "$mount_point" =~ ^${internal_mount}/ ]]; then - exclude_internal=true - break - fi - done - - - if [[ "$exclude_internal" == false ]]; then - - local size used - local df_info=$(df -h "$mount_point" 2>/dev/null | tail -n1) - if [[ -n "$df_info" ]]; then - size=$(echo "$df_info" | awk '{print $2}') - used=$(echo "$df_info" | awk '{print $3}') - else - size="N/A" - used="N/A" - fi - - - local mount_source="Manual" - if [[ "$mount_point" =~ ^/mnt/pve/ ]]; then - mount_source="Proxmox-GUI" - fi - - mounted_shares+=("$mount_point|$device|$type|$size|$used|$mount_source") + # Skip internal Proxmox mounts + local skip=false + for internal in /mnt/pve/local /mnt/pve/local-lvm /mnt/pve/local-zfs \ + /mnt/pve/backup /mnt/pve/dump /mnt/pve/images \ + /mnt/pve/template /mnt/pve/snippets /mnt/pve/vztmpl; do + if [[ "$mount_point" == "$internal" || "$mount_point" =~ ^${internal}/ ]]; then + skip=true + break fi + done + [[ "$skip" == true ]] && continue + + local size used + local df_info + df_info=$(df -h "$mount_point" 2>/dev/null | tail -n1) + if [[ -n "$df_info" ]]; then + size=$(echo "$df_info" | awk '{print $2}') + used=$(echo "$df_info" | awk '{print $3}') + else + size="N/A" + used="N/A" fi + + local source="Manual" + [[ "$mount_point" =~ ^/mnt/pve/ ]] && source="Proxmox-Storage" + + mounted_shares+=("$mount_point|$device|$type|$size|$used|$source") done < /proc/mounts - + printf '%s\n' "${mounted_shares[@]}" } detect_fstab_network_mounts() { local fstab_mounts=() - while IFS= read -r line; do - [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ -z "${line// }" ]] && continue - - local source mount_point fs_type options dump pass - read -r source mount_point fs_type options dump pass <<< "$line" - + local source mount_point fs_type + read -r source mount_point fs_type _ <<< "$line" - local is_network=false local type="" - case "$fs_type" in - nfs|nfs4) - is_network=true - type="NFS" - ;; - cifs) - is_network=true - type="CIFS/SMB" - ;; + nfs|nfs4) type="NFS" ;; + cifs) type="CIFS/SMB" ;; + *) continue ;; esac - - if [[ "$is_network" == true && -d "$mount_point" ]]; then - local is_mounted=false - while IFS= read -r proc_line; do - local proc_device proc_mount_point proc_fs_type - read -r proc_device proc_mount_point proc_fs_type _ <<< "$proc_line" - if [[ "$proc_mount_point" == "$mount_point" && ("$proc_fs_type" == "nfs" || "$proc_fs_type" == "nfs4" || "$proc_fs_type" == "cifs") ]]; then - is_mounted=true - break - fi - done < /proc/mounts - + [[ ! -d "$mount_point" ]] && continue - if [[ "$is_mounted" == false ]]; then - fstab_mounts+=("$mount_point|$source|$type|0|0|fstab-inactive") + # Skip if already mounted (already captured by detect_mounted_shares) + local is_mounted=false + while IFS= read -r proc_line; do + local proc_mp proc_fs + read -r _ proc_mp proc_fs _ <<< "$proc_line" + if [[ "$proc_mp" == "$mount_point" && ("$proc_fs" == "nfs" || "$proc_fs" == "nfs4" || "$proc_fs" == "cifs") ]]; then + is_mounted=true + break fi - fi + done < /proc/mounts + + [[ "$is_mounted" == false ]] && fstab_mounts+=("$mount_point|$source|$type|0|0|fstab-inactive") done < /etc/fstab - + printf '%s\n' "${fstab_mounts[@]}" } - detect_local_directories() { local local_dirs=() - local network_mounts=() - + local network_mps=() - local all_network_mounts - all_network_mounts=$(detect_mounted_shares) - local fstab_network_mounts - fstab_network_mounts=$(detect_fstab_network_mounts) - + # Collect network mount points to exclude + while IFS='|' read -r mp _ _ _ _ _; do + [[ -n "$mp" ]] && network_mps+=("$mp") + done < <({ detect_mounted_shares; detect_fstab_network_mounts; }) - local combined_network_mounts="$all_network_mounts"$'\n'"$fstab_network_mounts" - - while IFS='|' read -r mount_point source type size used mount_source; do - [[ -n "$mount_point" ]] && network_mounts+=("$mount_point") - done <<< "$combined_network_mounts" - if [[ -d "/mnt" ]]; then for dir in /mnt/*/; do - if [[ -d "$dir" && "$(basename "$dir")" != "pve" ]]; then - local dir_path="${dir%/}" - local dir_name=$(basename "$dir_path") - + [[ ! -d "$dir" ]] && continue + local dir_path="${dir%/}" + [[ "$(basename "$dir_path")" == "pve" ]] && continue - local is_network_mount=false - - for network_mount in "${network_mounts[@]}"; do - if [[ "$dir_path" == "$network_mount" ]]; then - is_network_mount=true - break - fi - done - + local is_network=false + for nmp in "${network_mps[@]}"; do + [[ "$dir_path" == "$nmp" ]] && is_network=true && break + done + [[ "$is_network" == true ]] && continue - if [[ "$is_network_mount" == false ]]; then - local dir_size=$(du -sh "$dir_path" 2>/dev/null | awk '{print $1}') - local_dirs+=("$dir_path|Local|Directory|$dir_size|-|Manual") - fi - fi + local dir_size + dir_size=$(du -sh "$dir_path" 2>/dev/null | awk '{print $1}') + local_dirs+=("$dir_path|Local|Directory|$dir_size|-|Manual") done fi - + printf '%s\n' "${local_dirs[@]}" } - -are_same_resource() { - local path1="$1" source1="$2" type1="$3" - local path2="$4" source2="$5" type2="$6" - - - [[ "$type1" != "$type2" ]] && return 1 - - - local server1 share1 server2 share2 - - if [[ "$type1" == "NFS" ]]; then - - server1=$(echo "$source1" | cut -d: -f1) - share1=$(echo "$source1" | cut -d: -f2) - server2=$(echo "$source2" | cut -d: -f1) - share2=$(echo "$source2" | cut -d: -f2) - elif [[ "$type1" == "CIFS/SMB" ]]; then - - server1=$(echo "$source1" | cut -d/ -f3) - share1=$(echo "$source1" | cut -d/ -f4-) - server2=$(echo "$source2" | cut -d/ -f3) - share2=$(echo "$source2" | cut -d/ -f4-) - else - return 1 - fi - - - if [[ "$server1" == "$server2" && "$share1" == "$share2" ]]; then - return 0 - else - return 1 - fi -} - - +# ========================================================== +# HOST DIRECTORY SELECTION +# ========================================================== detect_problematic_storage() { - local mount_point="$1" - local mount_source="$2" - local type="$3" - + local dir="$1" + local check_source="$2" + local check_type="$3" - if [[ "$mount_source" == "Proxmox-GUI" && "$type" == "CIFS/SMB" ]]; then - - local permissions=$(stat -c '%a' "$mount_point" 2>/dev/null) - local owner=$(stat -c '%U' "$mount_point" 2>/dev/null) - local group=$(stat -c '%G' "$mount_point" 2>/dev/null) - - - if [[ "$owner" == "root" && "$group" == "root" && "$permissions" =~ ^75[0-5]$ ]]; then - return 0 + while IFS='|' read -r mp _ type _ _ source; do + if [[ "$mp" == "$dir" && "$source" == "$check_source" && "$type" == "$check_type" ]]; then + return 0 fi - fi - - return 1 + done < <(detect_mounted_shares) + return 1 } - - - select_host_directory_unified() { - local mounted_shares local_dirs options=() fstab_mounts - - + local mounted_shares fstab_mounts local_dirs mounted_shares=$(detect_mounted_shares) fstab_mounts=$(detect_fstab_network_mounts) local_dirs=$(detect_local_directories) - - local all_network_shares="$mounted_shares" - if [[ -n "$fstab_mounts" ]]; then - all_network_shares="$all_network_shares"$'\n'"$fstab_mounts" - fi - + # Deduplicate and build option list + local all_entries=() + declare -A seen_paths - local has_local_dirs=false - local has_network_shares=false - - [[ -n "$local_dirs" ]] && has_local_dirs=true - [[ -n "$all_network_shares" ]] && has_network_shares=true - - if [[ "$has_local_dirs" == false && "$has_network_shares" == false ]]; then - whiptail --title "$(translate "No Directories Found")" \ - --msgbox "$(translate "No directories found in /mnt and no mounted network shares detected.")\n\n$(translate "Please:")\n• Mount shares using Proxmox GUI\n• Create directories in /mnt\n• Use manual path entry" 12 70 - + # Process network shares (mounted + fstab) + while IFS='|' read -r mp device type size used source; do + [[ -z "$mp" ]] && continue + [[ -n "${seen_paths[$mp]}" ]] && continue + seen_paths["$mp"]=1 - local manual_path - manual_path=$(whiptail --title "$(translate "Manual Path Entry")" \ - --inputbox "$(translate "Enter the full path to the host directory:")" 10 70 "/mnt/" 3>&1 1>&2 2>&3) - - if [[ -n "$manual_path" && -d "$manual_path" ]]; then - echo "$manual_path" - return 0 - else - return 1 - fi - fi + local prefix="" + case "$source" in + "Proxmox-Storage") prefix="PVE-" ;; + "fstab-inactive") prefix="fstab(off)-" ;; + *) prefix="" ;; + esac - local processed_resources=() - local final_shares=() - + local info="${prefix}${type}" + [[ "$size" != "N/A" && "$size" != "0" ]] && info="${info} [${used}/${size}]" + all_entries+=("$mp" "$info") + done < <(echo "$mounted_shares"; echo "$fstab_mounts") - while IFS='|' read -r mount_point source type size used mount_source; do - if [[ -n "$mount_point" ]]; then - - local is_duplicate=false - local duplicate_index=-1 - - for i in "${!processed_resources[@]}"; do - IFS='|' read -r proc_path proc_source proc_type proc_size proc_used proc_mount_source <<< "${processed_resources[$i]}" - if are_same_resource "$mount_point" "$source" "$type" "$proc_path" "$proc_source" "$proc_type"; then - - if [[ ("$mount_source" == "Manual" || "$proc_mount_source" =~ ^fstab) && "$proc_mount_source" == "Proxmox-GUI" ]]; then - - is_duplicate=true - duplicate_index=$i - break - elif [[ "$mount_source" == "Proxmox-GUI" && ("$proc_mount_source" == "Manual" || "$proc_mount_source" =~ ^fstab) ]]; then - - is_duplicate=true - break - fi - fi - done - - if [[ "$is_duplicate" == true && "$duplicate_index" -ge 0 ]]; then - - processed_resources[$duplicate_index]="$mount_point|$source|$type|$size|$used|$mount_source" - elif [[ "$is_duplicate" == false ]]; then - - processed_resources+=("$mount_point|$source|$type|$size|$used|$mount_source") - fi - fi - done <<< "$all_network_shares" - - if [[ "$has_local_dirs" == true ]]; then - options+=("" "\Z4──────────────── LOCAL DIRECTORIES ────────────────\Zn") - - while IFS='|' read -r dir_path source type size used mount_source; do - if [[ -n "$dir_path" && "$type" == "Directory" ]]; then - local dir_name=$(basename "$dir_path") - local permissions=$(stat -c '%a' "$dir_path" 2>/dev/null) - local owner_group=$(stat -c '%U:%G' "$dir_path" 2>/dev/null) - options+=("$dir_path" "$dir_name ($size)") - fi - done <<< "$local_dirs" - fi - - if [[ ${#processed_resources[@]} -gt 0 ]]; then - - [[ "$has_local_dirs" == true ]] && options+=("" "") - options+=("" "\Z4────────────────── NETWORK SHARES──────────────────\Zn") - - for resource in "${processed_resources[@]}"; do - IFS='|' read -r mount_point source type size used mount_source <<< "$resource" - - local share_name=$(basename "$source") - local mount_name=$(basename "$mount_point") - local permissions=$(stat -c '%a' "$mount_point" 2>/dev/null) - local owner_group=$(stat -c '%U:%G' "$mount_point" 2>/dev/null) - - - local warning="" - if detect_problematic_storage "$mount_point" "$mount_source" "$type"; then - warning=" [READ-ONLY]" - fi - - local prefix="" - case "$mount_source" in - "Proxmox-GUI") - prefix="GUI-" - ;; - "fstab-active") - prefix="fstab-" - ;; - "fstab-inactive") - prefix="fstab(off)-" - ;; - *) - prefix="" - ;; - esac - - options+=("$mount_point" "$prefix$type: $share_name → $mount_name ($size)$warning") + # Process local directories + while IFS='|' read -r mp _ type size _ _; do + [[ -z "$mp" ]] && continue + [[ -n "${seen_paths[$mp]}" ]] && continue + seen_paths["$mp"]=1 + local info="Local" + [[ -n "$size" && "$size" != "0" ]] && info="Local [${size}]" + all_entries+=("$mp" "$info") + done < <(echo "$local_dirs") + # Add Proxmox storage paths (/mnt/pve/*) + if [[ -d "/mnt/pve" ]]; then + for dir in /mnt/pve/*/; do + [[ ! -d "$dir" ]] && continue + local dir_path="${dir%/}" + [[ -n "${seen_paths[$dir_path]}" ]] && continue + seen_paths["$dir_path"]=1 + all_entries+=("$dir_path" "Proxmox-Storage") done fi - - options+=("" "") - options+=("" "\Z4────────────────────── OTHER ──────────────────────\Zn") - options+=("MANUAL" "$(translate "Enter path manually")") - - if [[ ${#options[@]} -eq 0 ]]; then - dialog --title "$(translate "No Valid Options")" \ - --msgbox "$(translate "No valid directories or shares found.")" 8 50 - return 1 - fi - + all_entries+=("MANUAL" "$(translate "Enter path manually")") local result result=$(dialog --clear --colors --title "$(translate "Select Host Directory")" \ --menu "\n$(translate "Select the directory to bind to container:")" 25 85 15 \ - "${options[@]}" 3>&1 1>&2 2>&3) + "${all_entries[@]}" 3>&1 1>&2 2>&3) + local dialog_exit=$? + [[ $dialog_exit -ne 0 ]] && return 1 + [[ -z "$result" || "$result" =~ ^━ ]] && return 1 - - local dialog_result=$? - if [[ $dialog_result -ne 0 ]]; then - return 1 - fi - - - if [[ -z "$result" || "$result" =~ ^━ ]]; then - return 1 - fi - - if [[ "$result" == "MANUAL" ]]; then result=$(whiptail --title "$(translate "Manual Path Entry")" \ - --inputbox "$(translate "Enter the full path to the host directory:")" 10 70 "/mnt/" 3>&1 1>&2 2>&3) - if [[ $? -ne 0 ]]; then - return 1 - fi + --inputbox "$(translate "Enter the full path to the host directory:")" \ + 10 70 "/mnt/" 3>&1 1>&2 2>&3) + [[ $? -ne 0 ]] && return 1 fi - if [[ -z "$result" ]]; then - return 1 - fi + [[ -z "$result" ]] && return 1 if [[ ! -d "$result" ]]; then whiptail --title "$(translate "Invalid Path")" \ @@ -422,504 +229,155 @@ select_host_directory_unified() { return 1 fi - - if detect_problematic_storage "$result" "Proxmox-GUI" "CIFS/SMB"; then -dialog --clear --title "$(translate "CIFS Storage Notice")" --yesno "\ -$(translate "\nThis directory is a CIFS storage configured from the Proxmox web interface.")\n\n\ -$(translate "When CIFS storage is configured through the Proxmox GUI, it applies restrictive permissions.")\n\ -$(translate "As a result, LXC containers can usually READ files but may NOT be able to WRITE.")\n\n\ -$(translate "If you need WRITE access, cancel this operation and instead use the option:")\n\ -$(translate "Configure Samba shared on Host")\n\n\ - -$(translate "Do you want to continue anyway?")" 18 80 3>&1 1>&2 2>&3 - - dialog_result=$? - - case $dialog_result in - 0) - ;; - 1|255) - return 1 - ;; - esac + # Warn about CIFS Proxmox-GUI storage (read-only limitation) + if detect_problematic_storage "$result" "Proxmox-Storage" "CIFS/SMB"; then + dialog --clear --title "$(translate "CIFS Storage Notice")" --yesno "\ +$(translate "This directory is a CIFS storage managed by Proxmox.")\n\n\ +$(translate "CIFS storage configured through Proxmox GUI applies restrictive permissions.")\n\ +$(translate "LXC containers can usually READ but may NOT be able to WRITE.")\n\n\ +$(translate "For write access, use 'Add Samba Share as Proxmox Storage' option instead.")\n\n\ +$(translate "Do you want to continue anyway?")" 14 80 3>&1 1>&2 2>&3 + [[ $? -ne 0 ]] && return 1 fi echo "$result" return 0 } - - +# ========================================================== +# CONTAINER SELECTION +# ========================================================== select_lxc_container() { - local ct_list ctid ct_status - + local ct_list ct_list=$(pct list 2>/dev/null | awk 'NR>1 {print $1, $2, $3}') if [[ -z "$ct_list" ]]; then - whiptail --title "Error" \ - --msgbox "No LXC containers available" 8 50 + whiptail --title "Error" --msgbox "$(translate "No LXC containers available")" 8 50 return 1 fi local options=() while read -r id name status; do - if [[ -n "$id" && "$id" =~ ^[0-9]+$ ]]; then - name=${name:-"unnamed"} - status=${status:-"unknown"} - options+=("$id" "$name ($status)") - fi + [[ -n "$id" && "$id" =~ ^[0-9]+$ ]] && options+=("$id" "${name:-unnamed} ($status)") done <<< "$ct_list" - + if [[ ${#options[@]} -eq 0 ]]; then - dialog --title "Error" \ - --msgbox "No valid containers found" 8 50 + dialog --title "Error" --msgbox "$(translate "No valid containers found")" 8 50 return 1 fi - ctid=$(dialog --title "Select LXC Container" \ - --menu "Select container:" 25 85 15 \ + local ctid + ctid=$(dialog --title "$(translate "Select LXC Container")" \ + --menu "$(translate "Select container:")" 25 85 15 \ "${options[@]}" 3>&1 1>&2 2>&3) - - local result=$? - if [[ $result -ne 0 || -z "$ctid" ]]; then - return 1 - fi + [[ $? -ne 0 || -z "$ctid" ]] && return 1 echo "$ctid" return 0 } - select_container_mount_point() { local ctid="$1" local host_dir="$2" - local choice mount_point base_name - + local base_name base_name=$(basename "$host_dir") while true; do + local choice choice=$(dialog --clear --title "$(translate "Configure Mount Point inside LXC")" \ - --menu "\n$(translate "Where to mount inside container?")" 18 70 5 \ + --menu "\n$(translate "Where to mount inside container?")" 16 70 3 \ "1" "$(translate "Create new directory in /mnt")" \ "2" "$(translate "Enter path manually")" \ "3" "$(translate "Cancel")" 3>&1 1>&2 2>&3) - - local dialog_result=$? - if [[ $dialog_result -ne 0 ]]; then - return 1 - fi + [[ $? -ne 0 ]] && return 1 + local mount_point case "$choice" in 1) mount_point=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" \ 10 60 "$base_name" 3>&1 1>&2 2>&3) - if [[ $? -ne 0 ]]; then - continue - fi - [[ -z "$mount_point" ]] && continue + [[ $? -ne 0 || -z "$mount_point" ]] && continue mount_point="/mnt/$mount_point" - pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null ;; - 2) mount_point=$(whiptail --inputbox "$(translate "Enter full path:")" \ 10 70 "/mnt/$base_name" 3>&1 1>&2 2>&3) - if [[ $? -ne 0 ]]; then - continue - fi - [[ -z "$mount_point" ]] && continue - pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null - ;; - - 3) - return 1 + [[ $? -ne 0 || -z "$mount_point" ]] && continue ;; + 3) return 1 ;; esac - if pct exec "$ctid" -- test -d "$mount_point" 2>/dev/null; then - echo "$mount_point" - return 0 - else - whiptail --msgbox "$(translate "Could not create or access directory:") $mount_point" 8 70 + # Validate path format + if [[ ! "$mount_point" =~ ^/ ]]; then + whiptail --msgbox "$(translate "Path must be absolute (start with /)")" 8 60 continue fi + + # Check if path is already used as a mount point in this CT + if pct config "$ctid" 2>/dev/null | grep -q "mp=.*$mount_point"; then + whiptail --msgbox "$(translate "This path is already used as a mount point in this container.")" 8 70 + continue + fi + + # Create directory inside CT (only if CT is running) + local ct_status + ct_status=$(pct status "$ctid" 2>/dev/null | awk '{print $2}') + if [[ "$ct_status" == "running" ]]; then + pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null + fi + + echo "$mount_point" + return 0 done } # ========================================================== -# MOUNT MANAGEMENT FUNCTIONS +# MOUNT MANAGEMENT # ========================================================== - -view_mount_points() { - show_proxmenux_logo - msg_title "$(translate 'Current LXC Mount Points')" - - local ct_list - ct_list=$(pct list 2>/dev/null | awk 'NR>1 {print $1, $2, $3}') - - if [[ -z "$ct_list" ]]; then - msg_warn "$(translate 'No LXC containers found')" - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" - read -r - return 1 - fi - - local found_mounts=false - - while read -r id name status; do - if [[ -n "$id" && "$id" =~ ^[0-9]+$ ]]; then - local conf="/etc/pve/lxc/${id}.conf" - if [[ -f "$conf" ]]; then - local mounts - mounts=$(grep "^mp[0-9]*:" "$conf" 2>/dev/null) - - if [[ -n "$mounts" ]]; then - if [[ "$found_mounts" == false ]]; then - found_mounts=true - fi - - echo -e "${TAB}${BOLD}$(translate 'Container') $id: $name ($status)${CL}" - - while IFS= read -r mount_line; do - if [[ -n "$mount_line" ]]; then - local mp_id=$(echo "$mount_line" | cut -d: -f1) - local mount_info=$(echo "$mount_line" | cut -d: -f2-) - local host_path=$(echo "$mount_info" | cut -d, -f1) - local container_path=$(echo "$mount_info" | grep -o 'mp=[^,]*' | cut -d= -f2) - local options=$(echo "$mount_info" | sed 's/^[^,]*,mp=[^,]*,*//') - - echo -e "${TAB} ${BGN}$mp_id:${CL} ${BL}$host_path${CL} → ${BL}$container_path${CL}" - [[ -n "$options" ]] && echo -e "${TAB} ${DGN}Options: $options${CL}" - fi - done <<< "$mounts" - echo "" - fi - fi - fi - done <<< "$ct_list" - - if [[ "$found_mounts" == false ]]; then - msg_ok "$(translate 'No mount points found in any container')" - fi - - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" - read -r -} - - - - - -remove_mount_point() { - show_proxmenux_logo - msg_title "$(translate 'Remove LXC Mount Point')" - - - local container_id - container_id=$(select_lxc_container) - if [[ $? -ne 0 || -z "$container_id" ]]; then - return 1 - fi - - local conf="/etc/pve/lxc/${container_id}.conf" - if [[ ! -f "$conf" ]]; then - msg_error "$(translate 'Container configuration not found')" - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" - read -r - return 1 - fi - - - local mounts - mounts=$(grep "^mp[0-9]*:" "$conf" 2>/dev/null) - - if [[ -z "$mounts" ]]; then - show_proxmenux_logo - msg_title "$(translate 'Remove LXC Mount Point')" - msg_warn "$(translate 'No mount points found in container') $container_id" - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" - read -r - return 1 - fi - - - local options=() - while IFS= read -r mount_line; do - if [[ -n "$mount_line" ]]; then - local mp_id=$(echo "$mount_line" | cut -d: -f1) - local mount_info=$(echo "$mount_line" | cut -d: -f2-) - local host_path=$(echo "$mount_info" | cut -d, -f1) - local container_path=$(echo "$mount_info" | grep -o 'mp=[^,]*' | cut -d= -f2) - - options+=("$mp_id" "$host_path → $container_path") - fi - done <<< "$mounts" - - if [[ ${#options[@]} -eq 0 ]]; then - show_proxmenux_logo - msg_title "$(translate 'Remove LXC Mount Point')" - msg_warn "$(translate 'No valid mount points found')" - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" - read -r - return 1 - fi - - - local selected_mp - selected_mp=$(dialog --clear --title "$(translate "Select Mount Point to Remove")" \ - --menu "\n$(translate "Select mount point to remove from container") $container_id:" 20 80 10 \ - "${options[@]}" 3>&1 1>&2 2>&3) - - if [[ $? -ne 0 || -z "$selected_mp" ]]; then - return 1 - fi - - - local selected_mount_line - selected_mount_line=$(grep "^${selected_mp}:" "$conf") - local mount_info=$(echo "$selected_mount_line" | cut -d: -f2-) - local host_path=$(echo "$mount_info" | cut -d, -f1) - local container_path=$(echo "$mount_info" | grep -o 'mp=[^,]*' | cut -d= -f2) - - local confirm_msg="$(translate "Remove Mount Point Confirmation:") - -$(translate "Container ID"): $container_id -$(translate "Mount Point ID"): $selected_mp -$(translate "Host Path"): $host_path -$(translate "Container Path"): $container_path - -$(translate "WARNING"): $(translate "This will remove the mount point from the container configuration.") -$(translate "The host directory and its contents will remain unchanged.") - -$(translate "Proceed with removal")?" - - if ! dialog --clear --title "$(translate "Confirm Mount Point Removal")" --yesno "$confirm_msg" 18 80; then - return 1 - fi - - show_proxmenux_logo - msg_title "$(translate 'Remove LXC Mount Point')" - - msg_info "$(translate 'Removing mount point') $selected_mp $(translate 'from container') $container_id..." - - - if pct set "$container_id" --delete "$selected_mp" 2>/dev/null; then - msg_ok "$(translate 'Mount point removed successfully')" - - - local ct_status - ct_status=$(pct status "$container_id" | awk '{print $2}') - - if [[ "$ct_status" == "running" ]]; then - echo -e "" - if whiptail --yesno "$(translate "Container is running. Restart to apply changes?")" 8 60; then - msg_info "$(translate 'Restarting container...')" - if pct reboot "$container_id"; then - sleep 3 - msg_ok "$(translate 'Container restarted successfully')" - else - msg_warn "$(translate 'Failed to restart container - restart manually')" - fi - fi - fi - - echo -e "" - echo -e "${TAB}${BOLD}$(translate 'Mount Point Removal Summary:')${CL}" - echo -e "${TAB}${BGN}$(translate 'Container:')${CL} ${BL}$container_id${CL}" - echo -e "${TAB}${BGN}$(translate 'Removed Mount:')${CL} ${BL}$selected_mp${CL}" - echo -e "${TAB}${BGN}$(translate 'Host Path:')${CL} ${BL}$host_dir (preserved)${CL}" - echo -e "${TAB}${BGN}$(translate 'Container Path:')${CL} ${BL}$container_path (unmounted)${CL}" - - else - msg_error "$(translate 'Failed to remove mount point')" - fi - - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" - read -r -} - - - - - -# ========================================================== -# MINIMAL CONTAINER SETUP (NO HOST MODIFICATIONS) -# ========================================================== - -get_container_uid_shift() { - local ctid="$1" - local conf="/etc/pve/lxc/${ctid}.conf" - - if [[ ! -f "$conf" ]]; then - echo "100000" - return 0 - fi - - local unpriv - unpriv=$(grep "^unprivileged:" "$conf" | awk '{print $2}') - - if [[ "$unpriv" == "1" ]]; then - local uid_shift=$(grep "^lxc.idmap" "$conf" | grep 'u 0' | awk '{print $5}' | head -1) - echo "${uid_shift:-100000}" - return 0 - fi - - echo "0" - return 0 -} - - - - - - -setup_minimal_container_access() { - local ctid="$1" host_dir="$2" ct_mount_point="$3" - local uid_shift container_type host_gid host_group mapped_gid - - - host_gid=$(stat -c '%g' "$host_dir" 2>/dev/null) - host_group=$(getent group "$host_gid" | cut -d: -f1 2>/dev/null) - host_group=${host_group:-"root"} - - msg_ok "$(translate "Host directory info detected - preserving existing configuration")" >&2 - - uid_shift=$(get_container_uid_shift "$ctid") - - if [[ "$uid_shift" -eq 0 ]]; then - msg_ok "$(translate "PRIVILEGED container detected - using direct UID/GID mapping")" >&2 - mapped_gid="$host_gid" - container_type="privileged" - else - msg_ok "$(translate "UNPRIVILEGED container detected - using mapped UID/GID")" >&2 - mapped_gid=$((uid_shift + host_gid)) - container_type="unprivileged" - msg_ok "$(translate "UID shift:") $uid_shift, $(translate "Host GID:") $host_gid → $(translate "Container GID:") $mapped_gid" >&2 - fi - - msg_info "$(translate "Creating compatible group in container...")" >&2 - - local container_group="shared_${host_gid}" - - pct exec "$ctid" -- groupadd -g "$mapped_gid" "$container_group" 2>/dev/null || true - - - local users_added=0 - local user_list="" - - local temp_file="/tmp/users_$$.txt" - pct exec "$ctid" -- awk -F: '$3 >= 25 && $3 < 65534 {print $1}' /etc/passwd > "$temp_file" - - if [[ -s "$temp_file" ]]; then - while IFS= read -r username; do - if [[ -n "$username" ]]; then - if pct exec "$ctid" -- usermod -aG "$container_group" "$username" 2>/dev/null; then - users_added=$((users_added + 1)) - user_list="$user_list $username" - fi - fi - done < "$temp_file" - - if [[ $users_added -gt 0 ]]; then - msg_ok "$(translate "Users added to group") $container_group: $users_added" >&2 - fi - fi - - rm -f "$temp_file" - - if [[ "$container_type" == "unprivileged" ]]; then - - if ! command -v setfacl >/dev/null 2>&1; then - apt-get update >/dev/null 2>&1 - apt-get install -y acl >/dev/null 2>&1 - msg_ok "$(translate "ACL tools installed")" >&2 - fi - - local acls_applied=0 - local acl_users=() - - - while IFS=: read -r username _ ct_uid _; do - if [[ $ct_uid -ge 25 && $ct_uid -lt 65534 ]]; then - local host_uid=$((uid_shift + ct_uid)) - - if setfacl -m u:$host_uid:rwx "$host_dir" 2>/dev/null && \ - setfacl -m d:u:$host_uid:rwx "$host_dir" 2>/dev/null; then - acls_applied=$((acls_applied + 1)) - acl_users+=("$username") - fi - fi - done < <(pct exec "$ctid" -- cat /etc/passwd) - - if [[ $acls_applied -gt 0 ]]; then - msg_ok "$(translate "ACL entries applied for") $acls_applied $(translate "users:") ${acl_users[*]}" >&2 - fi - fi - - msg_info "$(translate "Configuring container mount point with setgid...")" >&2 - - pct exec "$ctid" -- chgrp "$container_group" "$ct_mount_point" 2>/dev/null || true - pct exec "$ctid" -- chmod 2775 "$ct_mount_point" 2>/dev/null || true - - msg_ok "$(translate "Container mount point configured with setgid")" >&2 - - echo "$container_type|$host_group|$host_gid|$container_group|$mapped_gid" - return 0 -} - - - - - get_next_mp_index() { local ctid="$1" local conf="/etc/pve/lxc/${ctid}.conf" - + if [[ ! "$ctid" =~ ^[0-9]+$ ]] || [[ ! -f "$conf" ]]; then echo "0" return 0 fi - - local used idx next=0 + + local next=0 + local used used=$(awk -F: '/^mp[0-9]+:/ {print $1}' "$conf" | sed 's/mp//' | sort -n) for idx in $used; do - [[ "$idx" -ge "$next" ]] && next=$((idx+1)) + [[ "$idx" -ge "$next" ]] && next=$((idx + 1)) done echo "$next" } add_bind_mount() { - local ctid="$1" host_path="$2" ct_path="$3" - local mpidx result - - if [[ ! "$ctid" =~ ^[0-9]+$ ]]; then - return 1 - fi - - if [[ -z "$ctid" || -z "$host_path" || -z "$ct_path" ]]; then + local ctid="$1" + local host_path="$2" + local ct_path="$3" + + if [[ ! "$ctid" =~ ^[0-9]+$ || -z "$host_path" || -z "$ct_path" ]]; then + msg_error "$(translate "Invalid parameters for bind mount")" return 1 fi - if pct config "$ctid" | grep -q "$host_path"; then - msg_warn "$(translate "Mount already exists for this path")" + # Check if this host path is already mounted in this CT + if pct config "$ctid" 2>/dev/null | grep -q "^mp[0-9]*:.*${host_path},"; then + msg_warn "$(translate "Mount already exists for this path in container") $ctid" return 1 fi + local mpidx mpidx=$(get_next_mp_index "$ctid") - - result=$(pct set "$ctid" -mp${mpidx} "$host_path,mp=$ct_path,shared=1,backup=0,acl=1" 2>&1) + + local result + result=$(pct set "$ctid" -mp${mpidx} "$host_path,mp=$ct_path,shared=1,backup=0" 2>&1) if [[ $? -eq 0 ]]; then - msg_ok "$(translate "Successfully mounted:") $host_path → $ct_path" + msg_ok "$(translate "Bind mount added:") $host_path → $ct_path (mp${mpidx})" return 0 - else msg_error "$(translate "Failed to add bind mount:") $result" return 1 @@ -927,177 +385,320 @@ add_bind_mount() { } # ========================================================== -# MAIN FUNCTION +# VIEW / REMOVE +# ========================================================== + +view_mount_points() { + show_proxmenux_logo + msg_title "$(translate "Current LXC Mount Points")" + + local ct_list + ct_list=$(pct list 2>/dev/null | awk 'NR>1 {print $1, $2, $3}') + if [[ -z "$ct_list" ]]; then + msg_warn "$(translate "No LXC containers found")" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return 1 + fi + + local found_mounts=false + + while read -r id name status; do + [[ -z "$id" || ! "$id" =~ ^[0-9]+$ ]] && continue + local conf="/etc/pve/lxc/${id}.conf" + [[ ! -f "$conf" ]] && continue + + local mounts + mounts=$(grep "^mp[0-9]*:" "$conf" 2>/dev/null) + [[ -z "$mounts" ]] && continue + + found_mounts=true + echo -e "${TAB}${BOLD}$(translate "Container") $id: $name ($status)${CL}" + + while IFS= read -r mount_line; do + [[ -z "$mount_line" ]] && continue + local mp_id mount_info host_path container_path options + mp_id=$(echo "$mount_line" | cut -d: -f1) + mount_info=$(echo "$mount_line" | cut -d: -f2-) + host_path=$(echo "$mount_info" | cut -d, -f1) + container_path=$(echo "$mount_info" | grep -o 'mp=[^,]*' | cut -d= -f2) + options=$(echo "$mount_info" | sed 's/^[^,]*,mp=[^,]*,*//') + + echo -e "${TAB} ${BGN}$mp_id:${CL} ${BL}$host_path${CL} → ${BL}$container_path${CL}" + [[ -n "$options" ]] && echo -e "${TAB} ${DGN}$options${CL}" + done <<< "$mounts" + echo "" + done <<< "$ct_list" + + if [[ "$found_mounts" == false ]]; then + msg_ok "$(translate "No mount points found in any container")" + fi + + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r +} + +remove_mount_point() { + show_proxmenux_logo + msg_title "$(translate "Remove LXC Mount Point")" + + local container_id + container_id=$(select_lxc_container) + [[ $? -ne 0 || -z "$container_id" ]] && return 1 + + local conf="/etc/pve/lxc/${container_id}.conf" + if [[ ! -f "$conf" ]]; then + msg_error "$(translate "Container configuration not found")" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return 1 + fi + + local mounts + mounts=$(grep "^mp[0-9]*:" "$conf" 2>/dev/null) + if [[ -z "$mounts" ]]; then + show_proxmenux_logo + msg_title "$(translate "Remove LXC Mount Point")" + msg_warn "$(translate "No mount points found in container") $container_id" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return 1 + fi + + local options=() + while IFS= read -r mount_line; do + [[ -z "$mount_line" ]] && continue + local mp_id mount_info host_path container_path + mp_id=$(echo "$mount_line" | cut -d: -f1) + mount_info=$(echo "$mount_line" | cut -d: -f2-) + host_path=$(echo "$mount_info" | cut -d, -f1) + container_path=$(echo "$mount_info" | grep -o 'mp=[^,]*' | cut -d= -f2) + options+=("$mp_id" "$host_path → $container_path") + done <<< "$mounts" + + if [[ ${#options[@]} -eq 0 ]]; then + show_proxmenux_logo + msg_title "$(translate "Remove LXC Mount Point")" + msg_warn "$(translate "No valid mount points found")" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r + return 1 + fi + + local selected_mp + selected_mp=$(dialog --clear --title "$(translate "Select Mount Point to Remove")" \ + --menu "\n$(translate "Select mount point to remove from container") $container_id:" 20 80 10 \ + "${options[@]}" 3>&1 1>&2 2>&3) + [[ $? -ne 0 || -z "$selected_mp" ]] && return 1 + + local selected_mount_line mount_info host_path container_path + selected_mount_line=$(grep "^${selected_mp}:" "$conf") + mount_info=$(echo "$selected_mount_line" | cut -d: -f2-) + host_path=$(echo "$mount_info" | cut -d, -f1) + container_path=$(echo "$mount_info" | grep -o 'mp=[^,]*' | cut -d= -f2) + + local confirm_msg + confirm_msg="$(translate "Remove Mount Point Confirmation:") + +$(translate "Container ID"): $container_id +$(translate "Mount Point ID"): $selected_mp +$(translate "Host Path"): $host_path +$(translate "Container Path"): $container_path + +$(translate "NOTE: The host directory and its contents will remain unchanged.") + +$(translate "Proceed with removal")?" + + if ! dialog --clear --title "$(translate "Confirm Mount Point Removal")" --yesno "$confirm_msg" 18 80; then + return 1 + fi + + show_proxmenux_logo + msg_title "$(translate "Remove LXC Mount Point")" + msg_info "$(translate "Removing mount point") $selected_mp $(translate "from container") $container_id..." + + if pct set "$container_id" --delete "$selected_mp" 2>/dev/null; then + msg_ok "$(translate "Mount point removed successfully")" + + local ct_status + ct_status=$(pct status "$container_id" | awk '{print $2}') + if [[ "$ct_status" == "running" ]]; then + echo "" + if whiptail --yesno "$(translate "Container is running. Restart to apply changes?")" 8 60; then + msg_info "$(translate "Restarting container...")" + if pct reboot "$container_id"; then + sleep 3 + msg_ok "$(translate "Container restarted successfully")" + else + msg_warn "$(translate "Failed to restart container — restart manually")" + fi + fi + fi + + echo "" + echo -e "${TAB}${BOLD}$(translate "Mount Point Removal Summary:")${CL}" + echo -e "${TAB}${BGN}$(translate "Container:")${CL} ${BL}$container_id${CL}" + echo -e "${TAB}${BGN}$(translate "Removed Mount:")${CL} ${BL}$selected_mp${CL}" + echo -e "${TAB}${BGN}$(translate "Host Path:")${CL} ${BL}$host_path (preserved)${CL}" + echo -e "${TAB}${BGN}$(translate "Container Path:")${CL} ${BL}$container_path (unmounted)${CL}" + else + msg_error "$(translate "Failed to remove mount point")" + fi + + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r +} + +# ========================================================== +# MAIN FUNCTION — ADD MOUNT # ========================================================== mount_host_directory_minimal() { # Step 1: Select container local container_id container_id=$(select_lxc_container) - if [[ $? -ne 0 || -z "$container_id" ]]; then - return 1 - fi + [[ $? -ne 0 || -z "$container_id" ]] && return 1 - # Step 1.1: Ensure running - ct_status=$(pct status "$container_id" | awk '{print $2}') - if [[ "$ct_status" != "running" ]]; then - show_proxmenux_logo - msg_title "$(translate 'Mount Host Directory to LXC')" - msg_info "$(translate "Starting container") $container_id..." - if pct start "$container_id"; then - sleep 3 - cleanup - else - msg_error "$(translate "Failed to start container")" - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" - read -r - return 1 - fi - fi - - - # Step 2: Select host directory (unified menu) + # Step 2: Select host directory local host_dir host_dir=$(select_host_directory_unified) - if [[ $? -ne 0 || -z "$host_dir" ]]; then - return 1 - fi - + [[ $? -ne 0 || -z "$host_dir" ]] && return 1 # Step 3: Select container mount point local ct_mount_point ct_mount_point=$(select_container_mount_point "$container_id" "$host_dir") - if [[ $? -ne 0 || -z "$ct_mount_point" ]]; then - return 1 - fi + [[ $? -ne 0 || -z "$ct_mount_point" ]] && return 1 - - # Step 4: Get container info for confirmation + # Step 4: Get container type info (for display only) local uid_shift container_type_display - uid_shift=$(get_container_uid_shift "$container_id") - if [[ "$uid_shift" -eq 0 ]]; then - container_type_display="$(translate 'Privileged')" + uid_shift=$(awk -F: '/^lxc.idmap.*u 0/ {print $5}' "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | head -1) + local is_unprivileged + is_unprivileged=$(grep "^unprivileged:" "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | awk '{print $2}') + if [[ "$is_unprivileged" == "1" ]]; then + container_type_display="$(translate "Unprivileged")" + uid_shift="${uid_shift:-100000}" else - container_type_display="$(translate 'Unprivileged')" + container_type_display="$(translate "Privileged")" + uid_shift="0" fi - - # Step 4.1: Confirmation - local confirm_msg="$(translate "Mount Configuration Summary:") + # Step 5: Confirmation + local confirm_msg + confirm_msg="$(translate "Mount Configuration Summary:") $(translate "Container ID"): $container_id ($container_type_display) $(translate "Host Directory"): $host_dir $(translate "Container Mount Point"): $ct_mount_point -$(translate "Notes:") -- $(translate "The host directory will remain unchanged") -- $(translate "Basic permissions will be set inside the container") -- $(translate "ACL and setgid will be applied for group consistency") +$(translate "IMPORTANT NOTES:") +- $(translate "Host directory permissions and ownership are NOT modified") +- $(translate "Container filesystem is NOT modified") +- $(translate "If access fails after mounting, adjust permissions manually:") + +$(if [[ "$is_unprivileged" == "1" ]]; then + echo " # Allow container UID ${uid_shift}+ to access host dir:" + echo " setfacl -m u:${uid_shift}:rwx \"$host_dir\"" + echo " setfacl -d:m u:${uid_shift}:rwx \"$host_dir\"" +else + echo " chmod 755 \"$host_dir\"" +fi) $(translate "Proceed")?" - if ! dialog --clear --title "$(translate "Confirm Mount point")" --yesno "$confirm_msg" 18 80; then + if ! dialog --clear --title "$(translate "Confirm Mount")" --yesno "$confirm_msg" 22 80; then return 1 fi - + show_proxmenux_logo - msg_title "$(translate 'Mount Host Directory to LXC')" + msg_title "$(translate "Mount Host Directory to LXC")" + msg_ok "$(translate "Container:") $container_id ($container_type_display)" + msg_ok "$(translate "Host directory:") $host_dir" + msg_ok "$(translate "Container mount point:") $ct_mount_point" - msg_ok "$(translate 'Container selected:') $container_id" - msg_ok "$(translate 'Container is running')" - msg_ok "$(translate 'Host directory selected:') $host_dir" - msg_ok "$(translate 'Container mount point selected:') $ct_mount_point" - - - # Step 5: Add mount + # Step 6: Add bind mount (the ONLY operation that changes anything) if ! add_bind_mount "$container_id" "$host_dir" "$ct_mount_point"; then - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" + echo "" + msg_success "$(translate "Press Enter to continue...")" read -r return 1 fi - - # Step 6: Container setup - local setup_info - setup_info=$(setup_minimal_container_access "$container_id" "$host_dir" "$ct_mount_point") - - # Parse setup info - IFS='|' read -r container_type host_group host_gid container_group mapped_gid fix_type <<< "$setup_info" - - msg_ok "$(translate "container configuration completed")" - - # Step 7: Summary - echo -e "" - echo -e "${TAB}${BOLD}$(translate 'Mount Added Successfully:')${CL}" - echo -e "${TAB}${BGN}$(translate 'Container:')${CL} ${BL}$container_id ($container_type_display)${CL}" - echo -e "${TAB}${BGN}$(translate 'Host Directory:')${CL} ${BL}$host_dir${CL}" - echo -e "${TAB}${BGN}$(translate 'Mount Point:')${CL} ${BL}$ct_mount_point${CL}" - echo -e "${TAB}${BGN}$(translate 'Action Taken:')${CL} ${BL}PRESERVE existing permissions${CL}" - - if [[ "$fix_type" == "cifs-fixed" ]]; then - echo -e "${TAB}${BGN}$(translate 'Permission Strategy:')${CL} ${BL}CIFS compatibility fixes applied${CL}" - echo -e "${TAB}${YW}$(translate 'WARNING:')${CL} ${BL}Storage CIFS de Proxmox puede ser solo LECTURA${CL}" + + # Step 7: Summary with permission hints + echo "" + echo -e "${TAB}${BOLD}$(translate "Mount Added Successfully:")${CL}" + echo -e "${TAB}${BGN}$(translate "Container:")${CL} ${BL}$container_id${CL}" + echo -e "${TAB}${BGN}$(translate "Host Directory:")${CL} ${BL}$host_dir${CL}" + echo -e "${TAB}${BGN}$(translate "Mount Point:")${CL} ${BL}$ct_mount_point${CL}" + echo "" + + if [[ "$is_unprivileged" == "1" ]]; then + local mapped_uid="$uid_shift" + echo -e "${TAB}${YW}$(translate "UNPRIVILEGED container — UID mapping active:")${CL}" + echo -e "${TAB} $(translate "Container UID 0") → $(translate "Host UID") $mapped_uid" + echo -e "${TAB} $(translate "If access fails, run on the host:")" + echo -e "${TAB} ${DGN}setfacl -m u:${mapped_uid}:rwx \"$host_dir\"${CL}" + echo -e "${TAB} ${DGN}setfacl -d:m u:${mapped_uid}:rwx \"$host_dir\"${CL}" else - echo -e "${TAB}${BGN}$(translate 'Permission Strategy:')${CL} ${BL}$(if [[ "$container_type" == "unprivileged" ]]; then echo "ACL (mapped UIDs)"; else echo "Direct mapping"; fi)${CL}" + echo -e "${TAB}${DGN}$(translate "PRIVILEGED container — direct UID mapping")${CL}" + echo -e "${TAB} $(translate "Ensure") $host_dir $(translate "is accessible by root (chmod 755 or wider)")" fi - # Step 8: Restart - echo -e "" + # Step 8: Offer restart + echo "" if whiptail --yesno "$(translate "Restart container to activate mount?")" 8 60; then - msg_info "$(translate 'Restarting container...')" + msg_info "$(translate "Restarting container...")" if pct reboot "$container_id"; then sleep 5 - msg_ok "$(translate 'Container restarted successfully')" - - echo -e "" - echo -e "${TAB}${BOLD}$(translate 'Testing access and read/write:')${CL}" - test_user=$(pct exec "$container_id" -- sh -c "id -u www-data >/dev/null 2>&1 && echo www-data || echo root") + msg_ok "$(translate "Container restarted successfully")" - if pct exec "$container_id" -- su -s /bin/bash $test_user -c "touch $ct_mount_point/test_access.txt" 2>/dev/null; then - msg_ok "$(translate "Mount access and read/write successful (tested as $test_user)")" - rm -f "$host_dir/test_access.txt" 2>/dev/null || true - else - msg_warn "$(translate "⚠ Test read/write failed - may need additional configuration")" - + # Quick access test (read-only, no files written) + local ct_status + ct_status=$(pct status "$container_id" 2>/dev/null | awk '{print $2}') + if [[ "$ct_status" == "running" ]]; then + echo "" + if pct exec "$container_id" -- test -d "$ct_mount_point" 2>/dev/null; then + msg_ok "$(translate "Mount point is accessible inside container")" + else + msg_warn "$(translate "Mount point not yet accessible — may need manual permission adjustment")" + fi fi - else - msg_warn "$(translate 'Failed to restart - restart manually')" + msg_warn "$(translate "Failed to restart — restart manually to activate mount")" fi fi - - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" + + echo "" + msg_success "$(translate "Press Enter to continue...")" read -r } -# Main menu +# ========================================================== +# MAIN MENU +# ========================================================== + main_menu() { while true; do - choice=$(dialog --title "$(translate 'LXC Mount Manager')" \ - --menu "\n$(translate 'Choose an option:')" 25 85 15 \ - "1" "$(translate 'Mount point: Host Directory to LXC')" \ - "2" "$(translate 'View Mount Points')" \ - "3" "$(translate 'Remove Mount Point')" \ - "4" "$(translate 'Exit')" 3>&1 1>&2 2>&3) - + local choice + choice=$(dialog --title "$(translate "LXC Mount Manager")" \ + --menu "\n$(translate "Choose an option:")" 18 80 5 \ + "1" "$(translate "Add: Mount Host Directory into LXC")" \ + "2" "$(translate "View Mount Points")" \ + "3" "$(translate "Remove Mount Point")" \ + "4" "$(translate "Exit")" 3>&1 1>&2 2>&3) + case $choice in - 1) - mount_host_directory_minimal - ;; - 2) - view_mount_points - ;; - 3) - remove_mount_point - ;; - 4|"") - exit 0 - ;; + 1) mount_host_directory_minimal ;; + 2) view_mount_points ;; + 3) remove_mount_point ;; + 4|"") exit 0 ;; esac done } - main_menu diff --git a/scripts/share/nfs_client.sh b/scripts/share/nfs_client.sh index 6e1cc969..9cc253da 100644 --- a/scripts/share/nfs_client.sh +++ b/scripts/share/nfs_client.sh @@ -414,7 +414,7 @@ mount_nfs_share() { # Add to fstab if permanent if [[ "$PERMANENT_MOUNT" == "true" ]]; then pct exec "$CTID" -- sed -i "\|$MOUNT_POINT|d" /etc/fstab - FSTAB_ENTRY="$NFS_PATH $MOUNT_POINT nfs $MOUNT_OPTIONS 0 0" + FSTAB_ENTRY="$NFS_PATH $MOUNT_POINT nfs ${MOUNT_OPTIONS},_netdev,x-systemd.automount,noauto 0 0" pct exec "$CTID" -- bash -c "echo '$FSTAB_ENTRY' >> /etc/fstab" msg_ok "$(translate "Added to /etc/fstab for permanent mounting.")" fi diff --git a/scripts/share/nfs_host.sh b/scripts/share/nfs_host.sh index 4446a1d9..0a96ebb5 100644 --- a/scripts/share/nfs_host.sh +++ b/scripts/share/nfs_host.sh @@ -1,18 +1,16 @@ #!/bin/bash # ========================================================== -# ProxMenux Host - NFS Host Manager for Proxmox Host +# ProxMenux - NFS Host Manager for Proxmox Host # ========================================================== -# Based on ProxMenux by MacRimi +# Author : MacRimi +# Copyright : (c) 2024 MacRimi +# License : MIT # ========================================================== # Description: -# This script allows you to manage NFS client mounts on Proxmox Host: -# - Mount external NFS shares on the host -# - Configure permanent mounts -# - Auto-discover NFS servers -# - Integrate with Proxmox storage system +# Adds external NFS shares as Proxmox storage (pvesm). +# Proxmox manages the mount natively — no fstab entries needed. # ========================================================== -# Configuration LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" BASE_DIR="/usr/local/share/proxmenux" UTILS_FILE="$BASE_DIR/utils.sh" @@ -25,20 +23,31 @@ fi load_language initialize_cache -# Load common share functions -SHARE_COMMON_FILE="$LOCAL_SCRIPTS/global/share-common.func" -if ! source "$SHARE_COMMON_FILE" 2>/dev/null; then - msg_warn "$(translate "Could not load shared functions. Using fallback methods.")" - SHARE_COMMON_LOADED=false -else - SHARE_COMMON_LOADED=true +if ! command -v pveversion >/dev/null 2>&1; then + dialog --backtitle "ProxMenux" --title "$(translate "Error")" \ + --msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60 + exit 1 fi +# ========================================================== +# STORAGE CONFIG READER +# ========================================================== +get_storage_config() { + local storage_id="$1" + awk -v id="$storage_id" ' + /^[a-z]+: / { found = ($0 ~ ": "id"$"); next } + found && /^[^ \t]/ { exit } + found { print } + ' /etc/pve/storage.cfg +} +# ========================================================== +# SERVER DISCOVERY +# ========================================================== discover_nfs_servers() { show_proxmenux_logo - msg_title "$(translate "Mount NFS Share on Host")" + msg_title "$(translate "Add NFS Share as Proxmox Storage")" msg_info "$(translate "Scanning network for NFS servers...")" HOST_IP=$(hostname -I | awk '{print $1}') @@ -49,63 +58,51 @@ discover_nfs_servers() { fi SERVERS=$(nmap -p 2049 --open "$NETWORK" 2>/dev/null | grep -B 4 "2049/tcp open" | grep "Nmap scan report" | awk '{print $5}' | sort -u || true) - + if [[ -z "$SERVERS" ]]; then cleanup - dialog --clear --title "$(translate "No Servers Found")" --msgbox "$(translate "No NFS servers found on the network.")\n\n$(translate "You can add servers manually.")" 10 60 + dialog --clear --title "$(translate "No Servers Found")" \ + --msgbox "$(translate "No NFS servers found on the network.")\n\n$(translate "You can add servers manually.")" 10 60 return 1 fi - + OPTIONS=() while IFS= read -r server; do if [[ -n "$server" ]]; then EXPORTS_COUNT=$(showmount -e "$server" 2>/dev/null | tail -n +2 | wc -l || echo "0") - SERVER_INFO="NFS Server ($EXPORTS_COUNT exports)" - OPTIONS+=("$server" "$SERVER_INFO") + OPTIONS+=("$server" "NFS Server ($EXPORTS_COUNT exports)") fi done <<< "$SERVERS" - + if [[ ${#OPTIONS[@]} -eq 0 ]]; then cleanup dialog --clear --title "$(translate "No Valid Servers")" --msgbox "$(translate "No accessible NFS servers found.")" 8 50 return 1 fi + cleanup - NFS_SERVER=$(whiptail --backtitle "ProxMenux" --title "$(translate "Select NFS Server")" --menu "$(translate "Choose an NFS server:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) + NFS_SERVER=$(whiptail --backtitle "ProxMenux" --title "$(translate "Select NFS Server")" \ + --menu "$(translate "Choose an NFS server:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) [[ -n "$NFS_SERVER" ]] && return 0 || return 1 } select_nfs_server() { - METHOD=$(dialog --backtitle "ProxMenux" --title "$(translate "NFS Server Selection")" --menu "$(translate "How do you want to select the NFS server?")" 15 70 3 \ - "auto" "$(translate "Auto-discover servers on network")" \ - "manual" "$(translate "Enter server IP/hostname manually")" \ - "recent" "$(translate "Select from recent servers")" 3>&1 1>&2 2>&3) - + METHOD=$(dialog --backtitle "ProxMenux" --title "$(translate "NFS Server Selection")" \ + --menu "$(translate "How do you want to select the NFS server?")" 15 70 3 \ + "auto" "$(translate "Auto-discover servers on network")" \ + "manual" "$(translate "Enter server IP/hostname manually")" \ + 3>&1 1>&2 2>&3) + case "$METHOD" in auto) discover_nfs_servers || return 1 ;; manual) clear - NFS_SERVER=$(whiptail --inputbox "$(translate "Enter NFS server IP or hostname:")" 10 60 --title "$(translate "NFS Server")" 3>&1 1>&2 2>&3) + NFS_SERVER=$(whiptail --inputbox "$(translate "Enter NFS server IP or hostname:")" \ + 10 60 --title "$(translate "NFS Server")" 3>&1 1>&2 2>&3) [[ -z "$NFS_SERVER" ]] && return 1 ;; - recent) - clear - RECENT=$(grep "nfs" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d: -f1 | sort -u || true) - if [[ -z "$RECENT" ]]; then - dialog --title "$(translate "No Recent Servers")" --msgbox "\n$(translate "No recent NFS servers found.")" 8 50 - return 1 - fi - - OPTIONS=() - while IFS= read -r server; do - [[ -n "$server" ]] && OPTIONS+=("$server" "$(translate "Recent NFS server")") - done <<< "$RECENT" - - NFS_SERVER=$(whiptail --title "$(translate "Recent NFS Servers")" --menu "$(translate "Choose a recent server:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) - [[ -n "$NFS_SERVER" ]] && return 0 || return 1 - ;; *) return 1 ;; @@ -116,51 +113,44 @@ select_nfs_server() { select_nfs_export() { if ! which showmount >/dev/null 2>&1; then whiptail --title "$(translate "NFS Client Error")" \ - --msgbox "$(translate "showmount command is not working properly.")\n\n$(translate "Please check the installation.")" \ - 10 60 + --msgbox "$(translate "showmount command is not working properly.")\n\n$(translate "Please check the installation.")" \ + 10 60 return 1 fi if ! ping -c 1 -W 3 "$NFS_SERVER" >/dev/null 2>&1; then whiptail --title "$(translate "Connection Error")" \ - --msgbox "$(translate "Cannot reach server") $NFS_SERVER\n\n$(translate "Please check:")\n• $(translate "Server IP/hostname is correct")\n• $(translate "Network connectivity")\n• $(translate "Server is online")" \ - 12 70 + --msgbox "$(translate "Cannot reach server") $NFS_SERVER\n\n$(translate "Please check:")\n• $(translate "Server IP/hostname is correct")\n• $(translate "Network connectivity")\n• $(translate "Server is online")" \ + 12 70 return 1 fi if ! nc -z -w 3 "$NFS_SERVER" 2049 2>/dev/null; then whiptail --title "$(translate "NFS Port Error")" \ - --msgbox "$(translate "NFS port (2049) is not accessible on") $NFS_SERVER\n\n$(translate "Please check:")\n• $(translate "NFS server is running")\n• $(translate "Firewall settings")\n• $(translate "NFS service is enabled")" \ - 12 70 + --msgbox "$(translate "NFS port (2049) is not accessible on") $NFS_SERVER\n\n$(translate "Please check:")\n• $(translate "NFS server is running")\n• $(translate "Firewall settings")\n• $(translate "NFS service is enabled")" \ + 12 70 return 1 fi EXPORTS_OUTPUT=$(showmount -e "$NFS_SERVER" 2>&1) EXPORTS_RESULT=$? - + if [[ $EXPORTS_RESULT -ne 0 ]]; then ERROR_MSG=$(echo "$EXPORTS_OUTPUT" | grep -i "error\|failed\|denied" | head -1) - - if echo "$EXPORTS_OUTPUT" | grep -qi "connection refused\|network unreachable"; then - whiptail --title "$(translate "Network Error")" \ - --msgbox "$(translate "Network connection failed to") $NFS_SERVER\n\n$(translate "Error:"): $ERROR_MSG\n\n$(translate "Please check:")\n• $(translate "Server is running")\n• $(translate "Network connectivity")\n• $(translate "Firewall settings")" \ - 14 80 - else - whiptail --title "$(translate "NFS Error")" \ - --msgbox "$(translate "Failed to connect to") $NFS_SERVER\n\n$(translate "Error:"): $ERROR_MSG" \ - 12 80 - fi + whiptail --title "$(translate "NFS Error")" \ + --msgbox "$(translate "Failed to connect to") $NFS_SERVER\n\n$(translate "Error:"): $ERROR_MSG" \ + 12 80 return 1 fi - + EXPORTS=$(echo "$EXPORTS_OUTPUT" | tail -n +2 | awk '{print $1}' | grep -v "^$") if [[ -z "$EXPORTS" ]]; then whiptail --title "$(translate "No Exports Found")" \ - --msgbox "$(translate "No exports found on server") $NFS_SERVER\n\n$(translate "Server response:")\n$(echo "$EXPORTS_OUTPUT" | head -10)\n\n$(translate "You can enter the export path manually.")" \ - 16 80 - - NFS_EXPORT=$(whiptail --inputbox "$(translate "Enter NFS export path (e.g., /mnt/shared):")" 10 60 --title "$(translate "Export Path")" 3>&1 1>&2 2>&3) + --msgbox "$(translate "No exports found on server") $NFS_SERVER\n\n$(translate "You can enter the export path manually.")" \ + 12 70 + NFS_EXPORT=$(whiptail --inputbox "$(translate "Enter NFS export path (e.g., /mnt/shared):")" \ + 10 60 --title "$(translate "Export Path")" 3>&1 1>&2 2>&3) [[ -z "$NFS_EXPORT" ]] && return 1 return 0 fi @@ -169,7 +159,7 @@ select_nfs_export() { while IFS= read -r export_line; do if [[ -n "$export_line" ]]; then EXPORT_PATH=$(echo "$export_line" | awk '{print $1}') - CLIENTS=$(echo "$EXPORTS_OUTPUT" | grep "^$EXPORT_PATH" | awk '{for(i=2;i<=NF;i++) printf "%s ", $i; print ""}' | sed 's/[[:space:]]*$//') + CLIENTS=$(echo "$EXPORTS_OUTPUT" | grep "^$EXPORT_PATH" | awk '{for(i=2;i<=NF;i++) printf "%s ",$i; print ""}' | sed 's/[[:space:]]*$//') if [[ -n "$CLIENTS" ]]; then OPTIONS+=("$EXPORT_PATH" "$CLIENTS") else @@ -177,126 +167,22 @@ select_nfs_export() { fi fi done <<< "$EXPORTS" - + if [[ ${#OPTIONS[@]} -eq 0 ]]; then - whiptail --title "$(translate "No Available Exports")" \ - --msgbox "$(translate "No accessible exports found.")\n\n$(translate "You can enter the export path manually.")" \ - 10 70 - - NFS_EXPORT=$(whiptail --inputbox "$(translate "Enter NFS export path (e.g., /mnt/shared):")" 10 60 --title "$(translate "Export Path")" 3>&1 1>&2 2>&3) + NFS_EXPORT=$(whiptail --inputbox "$(translate "Enter NFS export path (e.g., /mnt/shared):")" \ + 10 60 --title "$(translate "Export Path")" 3>&1 1>&2 2>&3) [[ -n "$NFS_EXPORT" ]] && return 0 || return 1 fi - - NFS_EXPORT=$(whiptail --title "$(translate "Select NFS Export")" --menu "$(translate "Choose an export to mount:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) + + NFS_EXPORT=$(whiptail --title "$(translate "Select NFS Export")" \ + --menu "$(translate "Choose an export to mount:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) [[ -n "$NFS_EXPORT" ]] && return 0 || return 1 } - -select_host_mount_point() { - local export_name=$(basename "$NFS_EXPORT") - local default_path="/mnt/shared_nfs_${export_name}" - - MOUNT_POINT=$(pmx_select_host_mount_point "$(translate "NFS Mount Point")" "$default_path") - [[ -n "$MOUNT_POINT" ]] && return 0 || return 1 -} - - - -configure_host_mount_options() { - MOUNT_TYPE=$(whiptail --title "$(translate "Mount Options")" --menu "$(translate "Select mount configuration:")" 15 70 4 \ - "1" "$(translate "Default options read/write")" \ - "2" "$(translate "Read-only mount")" \ - "3" "$(translate "Enter custom options")" 3>&1 1>&2 2>&3) - - [[ $? -ne 0 ]] && return 1 - - case "$MOUNT_TYPE" in - 1) - MOUNT_OPTIONS="rw,hard,nofail,rsize=131072,wsize=131072,timeo=600,retrans=2" - ;; - 2) - MOUNT_OPTIONS="ro,hard,nofail,rsize=131072,wsize=131072,timeo=600,retrans=2" - ;; - 3) - - MOUNT_OPTIONS=$(whiptail --inputbox "$(translate "Enter custom mount options:")" \ - 10 70 "rw,hard,nofail,rsize=131072,wsize=131072,timeo=600,retrans=2" \ - --title "$(translate "Custom Options")" 3>&1 1>&2 2>&3) - [[ $? -ne 0 ]] && return 1 - [[ -z "$MOUNT_OPTIONS" ]] && MOUNT_OPTIONS="rw,hard,nofail" - ;; - *) - MOUNT_OPTIONS="rw,hard,nofail,rsize=131072,wsize=131072,timeo=600,retrans=2" - ;; - esac - - if whiptail --yesno "$(translate "Do you want to make this mount permanent?")\n\n$(translate "This will add the mount to /etc/fstab so it persists after reboot.")" 10 70 --title "$(translate "Permanent Mount")"; then - PERMANENT_MOUNT=true - else - if [[ $? -eq 1 ]]; then - PERMANENT_MOUNT=false - else - return 1 - fi - fi - - - TEMP_MOUNT="/tmp/nfs_test_$$" - mkdir -p "$TEMP_MOUNT" 2>/dev/null - - NFS_PATH="$NFS_SERVER:$NFS_EXPORT" - if timeout 10 mount -t nfs -o ro,soft,timeo=5 "$NFS_PATH" "$TEMP_MOUNT" 2>/dev/null; then - umount "$TEMP_MOUNT" 2>/dev/null || true - rmdir "$TEMP_MOUNT" 2>/dev/null || true - msg_ok "$(translate "NFS export is accessible")" - - - if whiptail --yesno "$(translate "Do you want to add this as Proxmox storage?")\n\n$(translate "This will make the NFS share available as storage in Proxmox web interface.")" 10 70 --title "$(translate "Proxmox Storage")"; then - PROXMOX_STORAGE=true - - STORAGE_ID=$(whiptail --inputbox "$(translate "Enter storage ID for Proxmox:")" 10 60 "nfs-$(echo $NFS_SERVER | tr '.' '-')" --title "$(translate "Storage ID")" 3>&1 1>&2 2>&3) - STORAGE_ID_RESULT=$? - - if [[ $STORAGE_ID_RESULT -ne 0 ]]; then - if whiptail --yesno "$(translate "Storage ID input was cancelled.")\n\n$(translate "Do you want to continue without Proxmox storage integration?")" 10 70 --title "$(translate "Continue Without Storage")"; then - PROXMOX_STORAGE=false - else - return 1 - fi - else - [[ -z "$STORAGE_ID" ]] && STORAGE_ID="nfs-$(echo $NFS_SERVER | tr '.' '-')" - fi - else - DIALOG_RESULT=$? - if [[ $DIALOG_RESULT -eq 1 ]]; then - PROXMOX_STORAGE=false - else - return 1 - fi - fi - else - - rmdir "$TEMP_MOUNT" 2>/dev/null || true - msg_warn "$(translate "NFS export accessibility test failed")" - - if whiptail --yesno "$(translate "The NFS export could not be validated for accessibility.")\n\n$(translate "This might be due to:")\n• $(translate "Network connectivity issues")\n• $(translate "Export permission restrictions")\n• $(translate "Firewall blocking access")\n\n$(translate "Do you want to continue mounting anyway?")\n$(translate "(Proxmox storage integration will be skipped)")" 16 80 --title "$(translate "Export Validation Failed")"; then - PROXMOX_STORAGE=false - msg_info2 "$(translate "Continuing without Proxmox storage integration due to accessibility issues.")" - sleep 2 - else - return 1 - fi - fi - - return 0 -} - validate_host_export_exists() { local server="$1" local export="$2" - VALIDATION_OUTPUT=$(showmount -e "$server" 2>/dev/null | grep "^$export[[:space:]]") - if [[ -n "$VALIDATION_OUTPUT" ]]; then return 0 else @@ -307,546 +193,353 @@ validate_host_export_exists() { fi } +# ========================================================== +# STORAGE CONFIGURATION +# ========================================================== + +configure_nfs_storage() { + STORAGE_ID=$(whiptail --inputbox "$(translate "Enter storage ID for Proxmox:")" \ + 10 60 "nfs-$(echo "$NFS_SERVER" | tr '.' '-')" \ + --title "$(translate "Storage ID")" 3>&1 1>&2 2>&3) + [[ $? -ne 0 ]] && return 1 + [[ -z "$STORAGE_ID" ]] && STORAGE_ID="nfs-$(echo "$NFS_SERVER" | tr '.' '-')" + + if [[ ! "$STORAGE_ID" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then + whiptail --msgbox "$(translate "Invalid storage ID. Use only letters, numbers, hyphens and underscores.")" 8 70 + return 1 + fi + + local raw_content + raw_content=$(dialog --backtitle "ProxMenux" \ + --title "$(translate "Content Types")" \ + --checklist "\n$(translate "Select content types for this storage:")\n$(translate "(Import is selected by default — required for disk image imports)")" 18 65 7 \ + "import" "$(translate "Import — disk image imports")" on \ + "backup" "$(translate "Backup — VM and CT backups")" off \ + "iso" "$(translate "ISO image — installation images")" off \ + "vztmpl" "$(translate "Container template— LXC templates")" off \ + "images" "$(translate "Disk image — VM disk images")" off \ + "rootdir" "$(translate "Container — LXC root directories")" off \ + "snippets" "$(translate "Snippets — hook scripts / config")" off \ + 3>&1 1>&2 2>&3) + [[ $? -ne 0 ]] && return 1 + + # Convert dialog checklist output (quoted space-separated) to comma-separated + MOUNT_CONTENT=$(echo "$raw_content" | tr -d '"' | tr -s ' ' ',' | sed 's/^,//;s/,$//') + [[ -z "$MOUNT_CONTENT" ]] && MOUNT_CONTENT="import" + + return 0 +} + add_proxmox_nfs_storage() { local storage_id="$1" local server="$2" local export="$3" - local content="${4:-backup,iso,vztmpl}" - + local content="${4:-import}" + msg_info "$(translate "Starting Proxmox storage integration...")" - + if ! command -v pvesm >/dev/null 2>&1; then - show_proxmenux_logo msg_error "$(translate "pvesm command not found. This should not happen on Proxmox.")" - echo "Press Enter to continue..." - read -r return 1 fi - - msg_ok "$(translate "pvesm command found")" - # Check if storage ID already exists if pvesm status "$storage_id" >/dev/null 2>&1; then msg_warn "$(translate "Storage ID already exists:") $storage_id" - if ! whiptail --yesno "$(translate "Storage ID already exists. Do you want to remove and recreate it?")" 8 60 --title "$(translate "Storage Exists")"; then - return 0 + if ! whiptail --yesno "$(translate "Storage ID already exists. Do you want to remove and recreate it?")" \ + 8 60 --title "$(translate "Storage Exists")"; then + return 0 fi pvesm remove "$storage_id" 2>/dev/null || true fi - + msg_ok "$(translate "Storage ID is available")" - - # Let Proxmox handle NFS version negotiation automatically if pvesm_output=$(pvesm add nfs "$storage_id" \ --server "$server" \ --export "$export" \ --content "$content" 2>&1); then - + msg_ok "$(translate "NFS storage added successfully!")" - - # Get the actual NFS version that Proxmox negotiated + local nfs_version="Auto-negotiated" - if pvesm config "$storage_id" 2>/dev/null | grep -q "options.*vers="; then - nfs_version="v$(pvesm config "$storage_id" | grep "options" | grep -o "vers=[0-9.]*" | cut -d= -f2)" + if get_storage_config "$storage_id" | grep -q "options.*vers="; then + nfs_version="v$(get_storage_config "$storage_id" | grep "options" | grep -o "vers=[0-9.]*" | cut -d= -f2)" fi - + echo -e "" + echo -e "${TAB}${BOLD}$(translate "Storage Added:")${CL}" echo -e "${TAB}${BGN}$(translate "Storage ID:")${CL} ${BL}$storage_id${CL}" echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$server${CL}" echo -e "${TAB}${BGN}$(translate "Export:")${CL} ${BL}$export${CL}" echo -e "${TAB}${BGN}$(translate "Content Types:")${CL} ${BL}$content${CL}" echo -e "${TAB}${BGN}$(translate "NFS Version:")${CL} ${BL}$nfs_version${CL}" + echo -e "${TAB}${BGN}$(translate "Mount Path:")${CL} ${BL}/mnt/pve/$storage_id${CL}" echo -e "" msg_ok "$(translate "Storage is now available in Proxmox web interface under Datacenter > Storage")" return 0 else msg_error "$(translate "Failed to add NFS storage to Proxmox.")" - echo "$(translate "Error details:"): $pvesm_output" - msg_warn "$(translate "The NFS share is still mounted, but not added as Proxmox storage.")" + echo -e "${TAB}$(translate "Error details:"): $pvesm_output" + echo -e "" msg_info2 "$(translate "You can add it manually through:")" echo -e "${TAB}• $(translate "Proxmox web interface: Datacenter > Storage > Add > NFS")" - echo -e "${TAB}• $(translate "Command line:"): pvesm add nfs $storage_id --server $server --export $export --content backup,iso,vztmpl" + echo -e "${TAB}• pvesm add nfs $storage_id --server $server --export $export --content $content" return 1 fi } -prepare_host_directory() { - local mount_point="$1" - - if [[ "$SHARE_COMMON_LOADED" == "true" ]]; then - # Use common functions for advanced directory preparation - local group_name - group_name=$(pmx_choose_or_create_group "sharedfiles") - if [[ -n "$group_name" ]]; then - local host_gid - host_gid=$(pmx_ensure_host_group "$group_name") - if [[ -n "$host_gid" ]]; then - pmx_prepare_host_shared_dir "$mount_point" "$group_name" - pmx_share_map_set "$mount_point" "$group_name" - msg_ok "$(translate "Directory prepared with shared group:") $group_name (GID: $host_gid)" - return 0 - fi - fi - msg_warn "$(translate "Failed to use shared functions, using basic directory creation.")" - fi - - # Fallback: basic directory creation - if ! test -d "$mount_point"; then - if mkdir -p "$mount_point"; then - msg_ok "$(translate "Mount point created on host.")" - return 0 - else - msg_error "$(translate "Failed to create mount point on host.")" - return 1 - fi - fi - return 0 -} +# ========================================================== +# MAIN OPERATIONS +# ========================================================== -mount_host_nfs_share() { +add_nfs_to_proxmox() { if ! which showmount >/dev/null 2>&1; then - msg_error "$(translate "NFS client tools not found. Please check Proxmox installation.")" - return 1 + msg_info "$(translate "Installing NFS client tools...")" + apt-get update &>/dev/null + apt-get install -y nfs-common &>/dev/null + msg_ok "$(translate "NFS client tools installed")" fi - - # Step 1: + + # Step 1: Select server select_nfs_server || return - - # Step 2: + + # Step 2: Select export select_nfs_export || return - - # Step 2.5: + + # Step 3: Validate export if ! validate_host_export_exists "$NFS_SERVER" "$NFS_EXPORT"; then echo -e "" msg_error "$(translate "Cannot proceed with invalid export path.")" - msg_success "$(translate "Press Enter to return to menu...")" + msg_success "$(translate "Press Enter to continue...")" read -r return fi - - # Step 3: - select_host_mount_point || return - - # Step 4: - configure_host_mount_options || return show_proxmenux_logo - msg_title "$(translate "Mount NFS Share on Host")" - msg_ok "$(translate "NFS server selected")" + msg_title "$(translate "Add NFS Share as Proxmox Storage")" + msg_ok "$(translate "NFS server:")" "$NFS_SERVER" + msg_ok "$(translate "NFS export:")" "$NFS_EXPORT" - prepare_host_directory "$MOUNT_POINT" || return 1 + # Step 4: Configure storage + configure_nfs_storage || return - if mount | grep -q "$MOUNT_POINT"; then - msg_warn "$(translate "Something is already mounted at") $MOUNT_POINT" - if ! whiptail --yesno "$(translate "Do you want to unmount it first?")" 8 60 --title "$(translate "Already Mounted")"; then - return - fi - umount "$MOUNT_POINT" 2>/dev/null || true - fi - - NFS_PATH="$NFS_SERVER:$NFS_EXPORT" - - if mount -t nfs -o "$MOUNT_OPTIONS" "$NFS_PATH" "$MOUNT_POINT" > /dev/null 2>&1; then - msg_ok "$(translate "NFS share mounted successfully on host!")" - - if touch "$MOUNT_POINT/.test_write" 2>/dev/null; then - rm "$MOUNT_POINT/.test_write" 2>/dev/null - msg_ok "$(translate "Write access confirmed.")" - else - msg_warn "$(translate "Read-only access (or no write permissions).")" - fi - - if [[ "$PERMANENT_MOUNT" == "true" ]]; then - sed -i "\|$MOUNT_POINT|d" /etc/fstab - FSTAB_ENTRY="$NFS_PATH $MOUNT_POINT nfs $MOUNT_OPTIONS 0 0" - echo "$FSTAB_ENTRY" >> /etc/fstab - msg_ok "$(translate "Added to /etc/fstab for permanent mounting.")" - - msg_info "$(translate "Reloading systemd configuration...")" - systemctl daemon-reload 2>/dev/null || true - msg_ok "$(translate "Systemd configuration reloaded.")" - fi - - if [[ "$PROXMOX_STORAGE" == "true" ]]; then - add_proxmox_nfs_storage "$STORAGE_ID" "$NFS_SERVER" "$NFS_EXPORT" "$MOUNT_CONTENT" - fi - - echo -e "" - echo -e "${TAB}${BOLD}$(translate "Host Mount Information:")${CL}" - echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$NFS_SERVER${CL}" - echo -e "${TAB}${BGN}$(translate "Export:")${CL} ${BL}$NFS_EXPORT${CL}" - echo -e "${TAB}${BGN}$(translate "Host Mount Point:")${CL} ${BL}$MOUNT_POINT${CL}" - echo -e "${TAB}${BGN}$(translate "Options:")${CL} ${BL}$MOUNT_OPTIONS${CL}" - echo -e "${TAB}${BGN}$(translate "Permanent:")${CL} ${BL}$PERMANENT_MOUNT${CL}" - if [[ "$PROXMOX_STORAGE" == "true" ]]; then - echo -e "${TAB}${BGN}$(translate "Proxmox Storage ID:")${CL} ${BL}$STORAGE_ID${CL}" - fi - - else - msg_error "$(translate "Failed to mount NFS share on host.")" - echo -e "${TAB}$(translate "Please check:")" - echo -e "${TAB}• $(translate "Server is accessible:"): $NFS_SERVER" - echo -e "${TAB}• $(translate "Export exists:"): $NFS_EXPORT" - echo -e "${TAB}• $(translate "Network connectivity")" - echo -e "${TAB}• $(translate "NFS server is running")" - echo -e "${TAB}• $(translate "Export permissions allow access")" - fi - + # Step 5: Add to Proxmox + show_proxmenux_logo + msg_title "$(translate "Add NFS Share as Proxmox Storage")" + msg_ok "$(translate "NFS server:") $NFS_SERVER" + msg_ok "$(translate "NFS export:") $NFS_EXPORT" + msg_ok "$(translate "Storage ID:") $STORAGE_ID" + msg_ok "$(translate "Content:") $MOUNT_CONTENT" echo -e "" - msg_success "$(translate "Press Enter to return to menu...")" + + add_proxmox_nfs_storage "$STORAGE_ID" "$NFS_SERVER" "$NFS_EXPORT" "$MOUNT_CONTENT" + + echo -e "" + msg_success "$(translate "Press Enter to continue...")" read -r } -view_host_nfs_mounts() { +view_nfs_storages() { show_proxmenux_logo - msg_title "$(translate "Current NFS Mounts on Host")" - - echo -e "$(translate "NFS mounts on Proxmox host:"):" - echo "==================================" - - CURRENT_MOUNTS=$(mount | grep -E "type nfs|:.*on.*nfs" 2>/dev/null || true) - if [[ -n "$CURRENT_MOUNTS" ]]; then - echo -e "${BOLD}$(translate "Currently Mounted:")${CL}" - echo "$CURRENT_MOUNTS" - echo "" - else - echo "$(translate "No NFS shares currently mounted on host.")" - echo "" - fi - - FSTAB_NFS=$(grep "nfs" /etc/fstab 2>/dev/null || true) - if [[ -n "$FSTAB_NFS" ]]; then - echo -e "${BOLD}$(translate "Permanent Mounts (fstab):")${CL}" - echo "$FSTAB_NFS" - echo "" - - echo -e "${TAB}${BOLD}$(translate "Mount Details:")${CL}" - while IFS= read -r fstab_line; do - if [[ -n "$fstab_line" && ! "$fstab_line" =~ ^# ]]; then - NFS_PATH=$(echo "$fstab_line" | awk '{print $1}') - MOUNT_POINT=$(echo "$fstab_line" | awk '{print $2}') - OPTIONS=$(echo "$fstab_line" | awk '{print $4}') - - SERVER=$(echo "$NFS_PATH" | cut -d: -f1) - EXPORT=$(echo "$NFS_PATH" | cut -d: -f2) - - echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$SERVER${CL}" - echo -e "${TAB}${BGN}$(translate "Export:")${CL} ${BL}$EXPORT${CL}" - echo -e "${TAB}${BGN}$(translate "Host Mount Point:")${CL} ${BL}$MOUNT_POINT${CL}" - echo -e "${TAB}${BGN}$(translate "Options:")${CL} ${BL}$OPTIONS${CL}" - - if mount | grep -q "$MOUNT_POINT"; then - echo -e "${TAB}${BGN}$(translate "Status:")${CL} ${GN}$(translate "Mounted")${CL}" - else - echo -e "${TAB}${BGN}$(translate "Status:")${CL} ${RD}$(translate "Not Mounted")${CL}" - fi - echo "" - fi - done <<< "$FSTAB_NFS" - else - echo "$(translate "No NFS mounts found in fstab.")" - fi + msg_title "$(translate "NFS Storages in Proxmox")" + echo "==================================================" echo "" - echo "$(translate "Proxmox NFS Storage Status:")" - if which pvesm >/dev/null 2>&1; then - NFS_STORAGES=$(pvesm status 2>/dev/null | grep "nfs" || true) - if [[ -n "$NFS_STORAGES" ]]; then - echo "$NFS_STORAGES" - else - echo "$(translate "No NFS storage configured in Proxmox.")" - fi - else - echo "$(translate "pvesm command not available.")" - fi - - echo "" - msg_success "$(translate "Press Enter to return to menu...")" - read -r -} -unmount_host_nfs_share() { - MOUNTS=$(mount | grep -E "type nfs|:.*on.*nfs" | awk '{print $3}' | sort -u || true) - FSTAB_MOUNTS=$(grep -E "nfs" /etc/fstab 2>/dev/null | grep -v "^#" | awk '{print $2}' | sort -u || true) - - ALL_MOUNTS=$(echo -e "$MOUNTS\n$FSTAB_MOUNTS" | sort -u | grep -v "^$" || true) - - if [[ -z "$ALL_MOUNTS" ]]; then - dialog --backtitle "ProxMenux" --title "$(translate "No Mounts")" --msgbox "\n$(translate "No NFS mounts found on host.")" 8 50 + if ! command -v pvesm >/dev/null 2>&1; then + msg_error "$(translate "pvesm not found.")" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r return fi - - OPTIONS=() - while IFS= read -r mount_point; do - if [[ -n "$mount_point" ]]; then - NFS_PATH=$(mount | grep "$mount_point" | awk '{print $1}' || grep "$mount_point" /etc/fstab | awk '{print $1}' || echo "Unknown") - SERVER=$(echo "$NFS_PATH" | cut -d: -f1) - EXPORT=$(echo "$NFS_PATH" | cut -d: -f2) - OPTIONS+=("$mount_point" "$SERVER:$EXPORT") - fi - done <<< "$ALL_MOUNTS" - - SELECTED_MOUNT=$(dialog --backtitle "ProxMenux" --title "$(translate "Unmount NFS Share")" --menu "$(translate "Select mount point to unmount:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) - [[ -z "$SELECTED_MOUNT" ]] && return - NFS_PATH=$(mount | grep "$SELECTED_MOUNT" | awk '{print $1}' || grep "$SELECTED_MOUNT" /etc/fstab | awk '{print $1}' || echo "Unknown") - SERVER=$(echo "$NFS_PATH" | cut -d: -f1) - EXPORT=$(echo "$NFS_PATH" | cut -d: -f2) + NFS_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "nfs" {print $1, $3}') + if [[ -z "$NFS_STORAGES" ]]; then + msg_warn "$(translate "No NFS storage configured in Proxmox.")" + echo "" + msg_info2 "$(translate "Use option 1 to add an NFS share as Proxmox storage.")" + else + echo -e "${BOLD}$(translate "NFS Storages:")${CL}" + echo "" + while IFS=" " read -r storage_id storage_status; do + [[ -z "$storage_id" ]] && continue + local storage_info + storage_info=$(get_storage_config "$storage_id") + local server export_path content + server=$(echo "$storage_info" | awk '$1 == "server" {print $2}') + export_path=$(echo "$storage_info" | awk '$1 == "export" {print $2}') + content=$(echo "$storage_info" | awk '$1 == "content" {print $2}') - PROXMOX_STORAGE="" - if which pvesm >/dev/null 2>&1; then - NFS_STORAGES=$(pvesm status 2>/dev/null | grep "nfs" | awk '{print $1}' || true) - while IFS= read -r storage_id; do - if [[ -n "$storage_id" ]]; then - STORAGE_INFO=$(pvesm config "$storage_id" 2>/dev/null || true) - STORAGE_SERVER=$(echo "$STORAGE_INFO" | grep "server" | awk '{print $2}') - STORAGE_EXPORT=$(echo "$STORAGE_INFO" | grep "export" | awk '{print $2}') - if [[ "$STORAGE_SERVER" == "$SERVER" && "$STORAGE_EXPORT" == "$EXPORT" ]]; then - PROXMOX_STORAGE="$storage_id" - break - fi + echo -e "${TAB}${BOLD}$storage_id${CL}" + echo -e "${TAB} ${BGN}$(translate "Server:")${CL} ${BL}$server${CL}" + echo -e "${TAB} ${BGN}$(translate "Export:")${CL} ${BL}$export_path${CL}" + echo -e "${TAB} ${BGN}$(translate "Content:")${CL} ${BL}$content${CL}" + echo -e "${TAB} ${BGN}$(translate "Mount Path:")${CL} ${BL}/mnt/pve/$storage_id${CL}" + if [[ "$storage_status" == "active" ]]; then + echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${GN}$(translate "Active")${CL}" + else + echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${RD}$storage_status${CL}" fi + echo "" done <<< "$NFS_STORAGES" fi - CONFIRMATION_MSG="$(translate "Are you sure you want to unmount this NFS share?")\n\n$(translate "Mount Point:"): $SELECTED_MOUNT\n$(translate "Server:"): $SERVER\n$(translate "Export:"): $EXPORT\n\n$(translate "This will:")\n• $(translate "Unmount the NFS share")\n• $(translate "Remove from /etc/fstab")" - - if [[ -n "$PROXMOX_STORAGE" ]]; then - CONFIRMATION_MSG="$CONFIRMATION_MSG\n• $(translate "Remove Proxmox storage:"): $PROXMOX_STORAGE" - fi - - CONFIRMATION_MSG="$CONFIRMATION_MSG\n• $(translate "Remove mount point directory")" - - if whiptail --yesno "$CONFIRMATION_MSG" 16 80 --title "$(translate "Confirm Unmount")"; then - show_proxmenux_logo - msg_title "$(translate "Unmount NFS Share from Host")" - - if [[ -n "$PROXMOX_STORAGE" ]]; then - if pvesm remove "$PROXMOX_STORAGE" 2>/dev/null; then - msg_ok "$(translate "Proxmox storage removed successfully.")" - else - msg_warn "$(translate "Failed to remove Proxmox storage, continuing with unmount...")" - fi - fi - - if mount | grep -q "$SELECTED_MOUNT"; then - if umount "$SELECTED_MOUNT"; then - msg_ok "$(translate "Successfully unmounted.")" - else - msg_warn "$(translate "Failed to unmount. Trying force unmount...")" - if umount -f "$SELECTED_MOUNT" 2>/dev/null; then - msg_ok "$(translate "Force unmount successful.")" - else - msg_error "$(translate "Failed to unmount. Mount point may be busy.")" - echo -e "${TAB}$(translate "Try closing any applications using the mount point.")" - fi - fi - fi - - msg_info "$(translate "Removing from /etc/fstab...")" - sed -i "\|[[:space:]]$SELECTED_MOUNT[[:space:]]|d" /etc/fstab - msg_ok "$(translate "Removed from /etc/fstab.")" - - echo -e "" - msg_ok "$(translate "NFS share unmounted successfully from host!")" - - if [[ -n "$PROXMOX_STORAGE" ]]; then - echo -e "${TAB}${BGN}$(translate "Proxmox storage removed:")${CL} ${BL}$PROXMOX_STORAGE${CL}" - fi - echo -e "${TAB}${BGN}$(translate "Mount point unmounted:")${CL} ${BL}$SELECTED_MOUNT${CL}" - echo -e "${TAB}${BGN}$(translate "Removed from fstab:")${CL} ${BL}Yes${CL}" - fi - - echo -e "" - msg_success "$(translate "Press Enter to return to menu...")" + echo "" + msg_success "$(translate "Press Enter to continue...")" read -r } -manage_proxmox_storage() { +remove_nfs_storage() { if ! command -v pvesm >/dev/null 2>&1; then - dialog --backtitle "ProxMenux" --title "$(translate "Error")" --msgbox "\n$(translate "pvesm command not found. This should not happen on Proxmox.")" 8 60 + dialog --backtitle "ProxMenux" --title "$(translate "Error")" \ + --msgbox "\n$(translate "pvesm not found.")" 8 60 return fi NFS_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "nfs" {print $1}') if [[ -z "$NFS_STORAGES" ]]; then - dialog --backtitle "ProxMenux" --title "$(translate "No NFS Storage")" --msgbox "\n$(translate "No NFS storage found in Proxmox.")" 8 60 + dialog --backtitle "ProxMenux" --title "$(translate "No NFS Storage")" \ + --msgbox "\n$(translate "No NFS storage found in Proxmox.")" 8 60 return fi OPTIONS=() while IFS= read -r storage_id; do - if [[ -n "$storage_id" ]]; then - STORAGE_INFO=$(pvesm config "$storage_id" 2>/dev/null || true) - SERVER=$(echo "$STORAGE_INFO" | grep "server" | awk '{print $2}') - EXPORT=$(echo "$STORAGE_INFO" | grep "export" | awk '{print $2}') - - if [[ -n "$SERVER" && -n "$EXPORT" ]]; then - OPTIONS+=("$storage_id" "$SERVER:$EXPORT") - else - OPTIONS+=("$storage_id" "$(translate "NFS Storage")") - fi - fi + [[ -z "$storage_id" ]] && continue + local storage_info server export_path + storage_info=$(get_storage_config "$storage_id") + server=$(echo "$storage_info" | awk '$1 == "server" {print $2}') + export_path=$(echo "$storage_info" | awk '$1 == "export" {print $2}') + OPTIONS+=("$storage_id" "$server:$export_path") done <<< "$NFS_STORAGES" - - SELECTED_STORAGE=$(dialog --backtitle "ProxMenux" --title "$(translate "Manage Proxmox NFS Storage")" --menu "$(translate "Select storage to manage:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) - [[ -z "$SELECTED_STORAGE" ]] && return - STORAGE_INFO=$(pvesm config "$SELECTED_STORAGE" 2>/dev/null || true) - SERVER=$(echo "$STORAGE_INFO" | grep "server" | awk '{print $2}') - EXPORT=$(echo "$STORAGE_INFO" | grep "export" | awk '{print $2}') - CONTENT=$(echo "$STORAGE_INFO" | grep "content" | awk '{print $2}') + SELECTED=$(dialog --backtitle "ProxMenux" --title "$(translate "Remove NFS Storage")" \ + --menu "$(translate "Select storage to remove:")" 20 80 10 \ + "${OPTIONS[@]}" 3>&1 1>&2 2>&3) + [[ -z "$SELECTED" ]] && return - FSTAB_NFS=$(grep "nfs" /etc/fstab 2>/dev/null || true) - if [[ -n "$FSTAB_NFS" ]]; then - while IFS= read -r fstab_line; do - if [[ -n "$fstab_line" && ! "$fstab_line" =~ ^# ]]; then - NFS_PATH=$(echo "$fstab_line" | awk '{print $1}') - MOUNT_POINT=$(echo "$fstab_line" | awk '{print $2}') - OPTIONS=$(echo "$fstab_line" | awk '{print $4}') - - SERVER=$(echo "$NFS_PATH" | cut -d: -f1) - EXPORT=$(echo "$NFS_PATH" | cut -d: -f2) - fi - done <<< "$FSTAB_NFS" - fi + local storage_info server export_path content + storage_info=$(get_storage_config "$SELECTED") + server=$(echo "$storage_info" | awk '$1 == "server" {print $2}') + export_path=$(echo "$storage_info" | awk '$1 == "export" {print $2}') + content=$(echo "$storage_info" | awk '$1 == "content" {print $2}') + + if whiptail --yesno "$(translate "Remove Proxmox NFS storage:")\n\n$SELECTED\n\n$(translate "Server:"): $server\n$(translate "Export:"): $export_path\n$(translate "Content:"): $content\n\n$(translate "WARNING: This removes the storage from Proxmox. The NFS server is not affected.")" \ + 16 80 --title "$(translate "Confirm Remove")"; then - if whiptail --yesno "$(translate "Are you sure you want to REMOVE storage") $SELECTED_STORAGE?\n\n$(translate "Server:"): $SERVER\n$(translate "Export:"): $EXPORT\n\n$(translate "WARNING: This will permanently remove the storage from Proxmox configuration.")\n$(translate "The NFS mount on the host will NOT be affected.")" 14 80 --title "$(translate "Remove Storage")"; then show_proxmenux_logo - msg_title "$(translate "Remove Storage")" - - if pvesm remove "$SELECTED_STORAGE" 2>/dev/null; then - msg_ok "$(translate "Storage removed successfully from Proxmox.")" - echo -e "" - msg_success "$(translate "Press Enter to return to menu...")" - read -r + msg_title "$(translate "Remove NFS Storage")" + + if pvesm remove "$SELECTED" 2>/dev/null; then + msg_ok "$(translate "Storage") $SELECTED $(translate "removed successfully from Proxmox.")" else msg_error "$(translate "Failed to remove storage.")" fi + + echo -e "" + msg_success "$(translate "Press Enter to continue...")" + read -r fi } -test_host_nfs_connectivity() { +test_nfs_connectivity() { show_proxmenux_logo - msg_title "$(translate "Test NFS Connectivity on Host")" - - echo -e "$(translate "NFS Client Status on Proxmox Host:"):" - echo "==================================" + msg_title "$(translate "Test NFS Connectivity")" + + echo "==================================================" + echo "" if which showmount >/dev/null 2>&1; then - echo "$(translate "NFS Client Tools: AVAILABLE")" + msg_ok "$(translate "NFS Client Tools: AVAILABLE")" if systemctl is-active --quiet rpcbind 2>/dev/null; then - echo "$(translate "RPC Bind Service: RUNNING")" + msg_ok "$(translate "RPC Bind Service: RUNNING")" else - echo "$(translate "RPC Bind Service: STOPPED")" - msg_warn "$(translate "Starting rpcbind service...")" + msg_warn "$(translate "RPC Bind Service: STOPPED - starting...")" systemctl start rpcbind 2>/dev/null || true fi - - echo "" - echo "$(translate "Current NFS mounts on host:")" - CURRENT_MOUNTS=$(mount | grep -E "type nfs|:.*on.*nfs" 2>/dev/null || true) - if [[ -n "$CURRENT_MOUNTS" ]]; then - echo "$CURRENT_MOUNTS" - else - echo "$(translate "No NFS mounts active on host.")" - fi - - echo "" - echo "$(translate "Testing network connectivity...")" + else + msg_warn "$(translate "NFS Client Tools: NOT AVAILABLE")" + fi - FSTAB_SERVERS=$(grep "nfs" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d: -f1 | sort -u || true) - if [[ -n "$FSTAB_SERVERS" ]]; then - while IFS= read -r server; do - if [[ -n "$server" ]]; then - echo -n "$(translate "Testing") $server: " - if ping -c 1 -W 2 "$server" >/dev/null 2>&1; then - echo -e "${GN}$(translate "Reachable")${CL}" + echo "" - echo -n " $(translate "NFS port 2049"): " - if nc -z -w 2 "$server" 2049 2>/dev/null; then - echo -e "${GN}$(translate "Open")${CL}" - else - echo -e "${RD}$(translate "Closed")${CL}" - fi + if command -v pvesm >/dev/null 2>&1; then + echo -e "${BOLD}$(translate "Proxmox NFS Storage Status:")${CL}" + NFS_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "nfs" {print $1, $3}') - echo -n " $(translate "Export list test"): " - if showmount -e "$server" >/dev/null 2>&1; then - echo -e "${GN}$(translate "Available")${CL}" - else - echo -e "${RD}$(translate "Failed")${CL}" - fi + if [[ -n "$NFS_STORAGES" ]]; then + while IFS=" " read -r storage_id storage_status; do + [[ -z "$storage_id" ]] && continue + local server + server=$(get_storage_config "$storage_id" | awk '$1 == "server" {print $2}') + + echo -n " $storage_id ($server): " + + if ping -c 1 -W 2 "$server" >/dev/null 2>&1; then + echo -ne "${GN}$(translate "Reachable")${CL}" + + if nc -z -w 2 "$server" 2049 2>/dev/null; then + echo -e " | NFS port 2049: ${GN}$(translate "Open")${CL}" else - echo -e "${RD}$(translate "Unreachable")${CL}" + echo -e " | NFS port 2049: ${RD}$(translate "Closed")${CL}" fi + + if showmount -e "$server" >/dev/null 2>&1; then + echo -e " $(translate "Export list:") ${GN}$(translate "Available")${CL}" + else + echo -e " $(translate "Export list:") ${RD}$(translate "Failed")${CL}" + fi + else + echo -e "${RD}$(translate "Unreachable")${CL}" fi - done <<< "$FSTAB_SERVERS" - else - echo "$(translate "No NFS servers configured to test.")" - fi - echo "" - echo "$(translate "Proxmox NFS Storage Status:")" - if which pvesm >/dev/null 2>&1; then - NFS_STORAGES=$(pvesm status 2>/dev/null | grep "nfs" || true) - if [[ -n "$NFS_STORAGES" ]]; then - echo "$NFS_STORAGES" - else - echo "$(translate "No NFS storage configured in Proxmox.")" - fi + if [[ "$storage_status" == "active" ]]; then + echo -e " $(translate "Proxmox status:") ${GN}$storage_status${CL}" + else + echo -e " $(translate "Proxmox status:") ${RD}$storage_status${CL}" + fi + echo "" + done <<< "$NFS_STORAGES" else - echo "$(translate "pvesm command not available.")" + echo " $(translate "No NFS storage configured.")" fi - else - echo "$(translate "NFS Client Tools: NOT AVAILABLE")" - echo "" - echo "$(translate "This is unusual for Proxmox. NFS client tools should be installed.")" + msg_warn "$(translate "pvesm not available.")" fi echo "" - echo "$(translate "ProxMenux Extensions:")" - if [[ "$SHARE_COMMON_LOADED" == "true" ]]; then - echo "$(translate "Shared Functions: LOADED")" - if [[ -f "$PROXMENUX_SHARE_MAP_DB" ]]; then - MAPPED_DIRS=$(wc -l < "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || echo "0") - echo "$(translate "Mapped directories:"): $MAPPED_DIRS" - fi - else - echo "$(translate "Shared Functions: NOT LOADED (using fallback methods)")" - fi - - echo "" - msg_success "$(translate "Press Enter to return to menu...")" + msg_success "$(translate "Press Enter to continue...")" read -r } -# === Main Menu === +# ========================================================== +# MAIN MENU +# ========================================================== + while true; do - CHOICE=$(dialog --backtitle "ProxMenux" --title "$(translate "NFS Host Manager - Proxmox Host")" \ - --menu "$(translate "Choose an option:")" 22 80 14 \ - "1" "$(translate "Mount NFS Share on Host")" \ - "2" "$(translate "View Current Host NFS Mounts")" \ - "3" "$(translate "Unmount NFS Share from Host")" \ - "4" "$(translate "Remove Proxmox NFS Storage")" \ - "5" "$(translate "Test NFS Connectivity")" \ - "6" "$(translate "Exit")" \ - 3>&1 1>&2 2>&3) - + CHOICE=$(dialog --backtitle "ProxMenux" \ + --title "$(translate "NFS Host Manager - Proxmox Host")" \ + --menu "$(translate "Choose an option:")" 18 70 6 \ + "1" "$(translate "Add NFS Share as Proxmox Storage")" \ + "2" "$(translate "View NFS Storages")" \ + "3" "$(translate "Remove NFS Storage")" \ + "4" "$(translate "Test NFS Connectivity")" \ + "5" "$(translate "Exit")" \ + 3>&1 1>&2 2>&3) + RETVAL=$? if [[ $RETVAL -ne 0 ]]; then exit 0 fi - + case $CHOICE in - 1) mount_host_nfs_share ;; - 2) view_host_nfs_mounts ;; - 3) unmount_host_nfs_share ;; - 4) manage_proxmox_storage ;; - 5) test_host_nfs_connectivity ;; - 6) exit 0 ;; + 1) add_nfs_to_proxmox ;; + 2) view_nfs_storages ;; + 3) remove_nfs_storage ;; + 4) test_nfs_connectivity ;; + 5) exit 0 ;; *) exit 0 ;; esac done diff --git a/scripts/share/samba_client.sh b/scripts/share/samba_client.sh index 7f193f02..1c5f29c9 100644 --- a/scripts/share/samba_client.sh +++ b/scripts/share/samba_client.sh @@ -643,7 +643,7 @@ configure_mount_options() { 1) MOUNT_OPTIONS="rw,file_mode=0664,dir_mode=0775,iocharset=utf8" ;; - 1) + 2) MOUNT_OPTIONS="ro,file_mode=0444,dir_mode=0555,iocharset=utf8" ;; 3) @@ -792,7 +792,7 @@ mount_samba_share() { pct exec "$CTID" -- sed -i "\|$MOUNT_POINT|d" /etc/fstab - FSTAB_ENTRY="$UNC_PATH $MOUNT_POINT cifs $FULL_OPTIONS 0 0" + FSTAB_ENTRY="$UNC_PATH $MOUNT_POINT cifs ${FULL_OPTIONS},_netdev,x-systemd.automount,noauto 0 0" pct exec "$CTID" -- bash -c "echo '$FSTAB_ENTRY' >> /etc/fstab" msg_ok "$(translate "Added to /etc/fstab for permanent mounting.")" fi diff --git a/scripts/share/samba_host.sh b/scripts/share/samba_host.sh index 552b9ad3..48e0df4b 100644 --- a/scripts/share/samba_host.sh +++ b/scripts/share/samba_host.sh @@ -1,22 +1,19 @@ #!/bin/bash # ========================================================== -# ProxMenux Host - Samba Host Manager for Proxmox Host +# ProxMenux - Samba Host Manager for Proxmox Host # ========================================================== -# Based on ProxMenux by MacRimi +# Author : MacRimi +# Copyright : (c) 2024 MacRimi +# License : MIT # ========================================================== # Description: -# This script allows you to manage Samba/CIFS client mounts on Proxmox Host: -# - Mount external Samba shares on the host -# - Configure permanent mounts with credentials -# - Auto-discover Samba servers -# - Integrate with Proxmox storage system +# Adds external Samba/CIFS shares as Proxmox storage (pvesm). +# Proxmox manages the mount natively — no fstab entries needed. # ========================================================== -# Configuration LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" BASE_DIR="/usr/local/share/proxmenux" UTILS_FILE="$BASE_DIR/utils.sh" -CREDENTIALS_DIR="/etc/samba/credentials" if [[ -f "$UTILS_FILE" ]]; then source "$UTILS_FILE" @@ -25,33 +22,38 @@ fi load_language initialize_cache -# Load common share functions -SHARE_COMMON_FILE="$LOCAL_SCRIPTS/global/share-common.func" -if ! source "$SHARE_COMMON_FILE" 2>/dev/null; then - msg_warn "$(translate "Could not load shared functions. Using fallback methods.")" - SHARE_COMMON_LOADED=false -else - SHARE_COMMON_LOADED=true -fi - - if ! command -v pveversion >/dev/null 2>&1; then - dialog --backtitle "ProxMenux" --title "$(translate "Error")" --msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60 + dialog --backtitle "ProxMenux" --title "$(translate "Error")" \ + --msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60 exit 1 fi +# ========================================================== +# STORAGE CONFIG READER +# ========================================================== +get_storage_config() { + local storage_id="$1" + awk -v id="$storage_id" ' + /^[a-z]+: / { found = ($0 ~ ": "id"$"); next } + found && /^[^ \t]/ { exit } + found { print } + ' /etc/pve/storage.cfg +} + +# ========================================================== +# SERVER DISCOVERY +# ========================================================== + discover_samba_servers() { show_proxmenux_logo - msg_title "$(translate "Samba Host Manager - Proxmox Host")" + msg_title "$(translate "Add Samba Share as Proxmox Storage")" msg_info "$(translate "Scanning network for Samba servers...")" - HOST_IP=$(hostname -I | awk '{print $1}') NETWORK=$(echo "$HOST_IP" | cut -d. -f1-3).0/24 - for pkg in nmap samba-common-bin; do - if ! which ${pkg%%-*} >/dev/null 2>&1; then + if ! which "${pkg%%-*}" >/dev/null 2>&1; then apt-get install -y "$pkg" &>/dev/null fi done @@ -59,20 +61,18 @@ discover_samba_servers() { SERVERS=$(nmap -p 139,445 --open "$NETWORK" 2>/dev/null | grep -B 4 -E "(139|445)/tcp open" | grep "Nmap scan report" | awk '{print $5}' | sort -u || true) if [[ -z "$SERVERS" ]]; then cleanup - whiptail --title "$(translate "No Servers Found")" --msgbox "$(translate "No Samba servers found on the network.")\n\n$(translate "You can add servers manually.")" 10 60 + whiptail --title "$(translate "No Servers Found")" \ + --msgbox "$(translate "No Samba servers found on the network.")\n\n$(translate "You can add servers manually.")" 10 60 return 1 fi SERVER_LINES=() while IFS= read -r server; do [[ -z "$server" ]] && continue - NB_NAME=$(nmblookup -A "$server" 2>/dev/null | awk '/<00> -.*B / {print $1; exit}') - if [[ -z "$NB_NAME" || "$NB_NAME" == "$server" || "$NB_NAME" == "address" || "$NB_NAME" == "-" ]]; then NB_NAME="Unknown" fi - SERVER_LINES+=("$server|$NB_NAME ($server)") done <<< "$SERVERS" @@ -107,41 +107,23 @@ discover_samba_servers() { fi } - - - - select_samba_server() { - METHOD=$(dialog --backtitle "ProxMenux" --title "$(translate "Samba Server Selection")" --menu "$(translate "How do you want to select the Samba server?")" 15 70 3 \ - "auto" "$(translate "Auto-discover servers on network")" \ - "manual" "$(translate "Enter server IP/hostname manually")" \ - "recent" "$(translate "Select from recent servers")" 3>&1 1>&2 2>&3) - + METHOD=$(dialog --backtitle "ProxMenux" --title "$(translate "Samba Server Selection")" \ + --menu "$(translate "How do you want to select the Samba server?")" 15 70 2 \ + "auto" "$(translate "Auto-discover servers on network")" \ + "manual" "$(translate "Enter server IP/hostname manually")" \ + 3>&1 1>&2 2>&3) + case "$METHOD" in auto) discover_samba_servers || return 1 ;; manual) clear - SAMBA_SERVER=$(whiptail --inputbox "$(translate "Enter Samba server IP:")" 10 60 --title "$(translate "Samba Server")" 3>&1 1>&2 2>&3) + SAMBA_SERVER=$(whiptail --inputbox "$(translate "Enter Samba server IP:")" \ + 10 60 --title "$(translate "Samba Server")" 3>&1 1>&2 2>&3) [[ -z "$SAMBA_SERVER" ]] && return 1 ;; - recent) - clear - RECENT=$(grep "cifs" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d/ -f3 | sort -u || true) - if [[ -z "$RECENT" ]]; then - dialog --backtitle "ProxMenux" --title "$(translate "No Recent Servers")" --msgbox "\n$(translate "No recent Samba servers found.")" 8 50 - return 1 - fi - - OPTIONS=() - while IFS= read -r server; do - [[ -n "$server" ]] && OPTIONS+=("$server" "$(translate "Recent Samba server")") - done <<< "$RECENT" - - SAMBA_SERVER=$(whiptail --title "$(translate "Recent Samba Servers")" --menu "$(translate "Choose a recent server:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) - [[ -n "$SAMBA_SERVER" ]] && return 0 || return 1 - ;; *) return 1 ;; @@ -149,1118 +131,437 @@ select_samba_server() { return 0 } -validate_guest_access() { - local server="$1" - - show_proxmenux_logo - msg_info "$(translate "Testing comprehensive guest access to server") $server..." - - GUEST_LIST_OUTPUT=$(smbclient -L "$server" -N 2>&1) - GUEST_LIST_RESULT=$? - - if [[ $GUEST_LIST_RESULT -ne 0 ]]; then - cleanup - if echo "$GUEST_LIST_OUTPUT" | grep -qi "access denied\|logon failure"; then - whiptail --title "$(translate "Guest Access Denied")" \ - --msgbox "$(translate "Guest access is not allowed on this server.")\n\n$(translate "You need to use username and password authentication.")" \ - 10 70 - else - whiptail --title "$(translate "Guest Access Error")" \ - --msgbox "$(translate "Guest access failed.")\n\n$(translate "Error details:")\n$(echo "$GUEST_LIST_OUTPUT" | head -3)" \ - 12 70 - fi - return 1 - fi - sleep 2 - msg_ok "$(translate "Guest share listing successful")" - - GUEST_SHARES=$(echo "$GUEST_LIST_OUTPUT" | awk '/Disk/ && !/IPC\$/ && !/ADMIN\$/ && !/print\$/ {print $1}' | grep -v "^$") - if [[ -z "$GUEST_SHARES" ]]; then - whiptail --title "$(translate "No Guest Shares")" \ - --msgbox "$(translate "Guest access works for listing, but no shares are available.")\n\n$(translate "The server may require authentication for actual share access.")" \ - 10 70 - return 1 - fi - - msg_ok "$(translate "Found guest-accessible shares:") $(echo "$GUEST_SHARES" | wc -l)" - - msg_info "$(translate "Step 2: Testing actual share access with guest...")" - ACCESSIBLE_SHARES="" - FAILED_SHARES="" - sleep 1 - while IFS= read -r share; do - if [[ -n "$share" ]]; then - - SHARE_TEST_OUTPUT=$(smbclient "//$server/$share" -N -c "ls" 2>&1) - SHARE_TEST_RESULT=$? - - if [[ $SHARE_TEST_RESULT -eq 0 ]]; then - msg_ok "$(translate "Guest access confirmed for share:") $share" - ACCESSIBLE_SHARES="$ACCESSIBLE_SHARES$share\n" - else - msg_warn "$(translate "Guest access denied for share:") $share" - FAILED_SHARES="$FAILED_SHARES$share\n" - - if echo "$SHARE_TEST_OUTPUT" | grep -qi "access denied\|logon failure\|authentication"; then - msg_warn " $(translate "Reason: Authentication required")" - elif echo "$SHARE_TEST_OUTPUT" | grep -qi "permission denied"; then - msg_warn " $(translate "Reason: Permission denied")" - else - msg_warn " $(translate "Reason: Access denied")" - fi - fi - fi - done <<< "$GUEST_SHARES" - - - ACCESSIBLE_COUNT=$(echo -e "$ACCESSIBLE_SHARES" | grep -v "^$" | wc -l) - FAILED_COUNT=$(echo -e "$FAILED_SHARES" | grep -v "^$" | wc -l) - - echo -e "" - msg_info2 "$(translate "Guest Access Validation Results:")" - echo -e "${TAB}${BGN}$(translate "Shares found:")${CL} ${BL}$(echo "$GUEST_SHARES" | wc -l)${CL}" - echo -e "${TAB}${BGN}$(translate "Guest accessible:")${CL} ${GN}$ACCESSIBLE_COUNT${CL}" - echo -e "${TAB}${BGN}$(translate "Authentication required:")${CL} ${YW}$FAILED_COUNT${CL}" - - if [[ $ACCESSIBLE_COUNT -gt 0 ]]; then - msg_ok "$(translate "Guest access validated successfully!")" - echo -e "" - echo -e "${TAB}${BOLD}$(translate "Available shares for guest access:")${CL}" - while IFS= read -r share; do - [[ -n "$share" ]] && echo -e "${TAB}• ${BL}$share${CL}" - done <<< "$(echo -e "$ACCESSIBLE_SHARES" | grep -v "^$")" - echo -e - msg_success "$(translate "Press Enter to continue...")" - read -r - clear - - VALIDATED_GUEST_SHARES="$ACCESSIBLE_SHARES" - return 0 - else - msg_success "$(translate "Press Enter to continue...")" - read -r - whiptail --title "$(translate "Guest Access Failed")" \ - --msgbox "$(translate "While the server allows guest listing, no shares are actually accessible without authentication.")\n\n$(translate "You need to use username and password authentication.")" \ - 12 70 - clear - return 1 - fi -} +# ========================================================== +# CREDENTIALS +# ========================================================== get_samba_credentials() { - while true; do - CHOICE=$(whiptail --title "$(translate "Samba Credentials")" \ - --menu "$(translate "Select authentication mode:")" 13 60 2 \ - "1" "$(translate "Configure with username and password")" \ - "2" "$(translate "Configure as guest (no authentication)")" \ - 3>&1 1>&2 2>&3) + AUTH_TYPE=$(whiptail --title "$(translate "Authentication")" \ + --menu "$(translate "Select authentication type:")" 12 60 2 \ + "user" "$(translate "Username and password")" \ + "guest" "$(translate "Guest access (no authentication)")" \ + 3>&1 1>&2 2>&3) + [[ $? -ne 0 ]] && return 1 - if [[ $? -ne 0 ]]; then - return 1 - fi + if [[ "$AUTH_TYPE" == "guest" ]]; then + USE_GUEST=true + USERNAME="" + PASSWORD="" + return 0 + fi - case "$CHOICE" in - 1) + USE_GUEST=false - while true; do - USERNAME=$(whiptail --inputbox "$(translate "Enter username for Samba server:")" 10 60 --title "$(translate "Username")" 3>&1 1>&2 2>&3) - if [[ $? -ne 0 ]]; then - break - fi - if [[ -z "$USERNAME" ]]; then - whiptail --title "$(translate "Error")" --msgbox "$(translate "Username cannot be empty.")" 8 50 - continue - fi + USERNAME=$(whiptail --inputbox "$(translate "Enter username:")" \ + 10 60 --title "$(translate "Samba Username")" 3>&1 1>&2 2>&3) + [[ $? -ne 0 || -z "$USERNAME" ]] && return 1 - while true; do - PASSWORD=$(whiptail --passwordbox "$(translate "Enter password for") $USERNAME:" 10 60 --title "$(translate "Password")" 3>&1 1>&2 2>&3) - if [[ $? -ne 0 ]]; then - break - fi - if [[ -z "$PASSWORD" ]]; then - whiptail --title "$(translate "Error")" --msgbox "$(translate "Password cannot be empty.")" 8 50 - continue - fi + PASSWORD=$(whiptail --passwordbox "$(translate "Enter password:")" \ + 10 60 --title "$(translate "Samba Password")" 3>&1 1>&2 2>&3) + [[ $? -ne 0 ]] && return 1 - PASSWORD_CONFIRM=$(whiptail --passwordbox "$(translate "Confirm password for") $USERNAME:" 10 60 --title "$(translate "Confirm Password")" 3>&1 1>&2 2>&3) - if [[ $? -ne 0 ]]; then - continue - fi - if [[ -z "$PASSWORD_CONFIRM" ]]; then - whiptail --title "$(translate "Error")" --msgbox "$(translate "Password confirmation cannot be empty.")" 8 50 - continue - fi - - if [[ "$PASSWORD" == "$PASSWORD_CONFIRM" ]]; then - - show_proxmenux_logo - msg_info "$(translate "Validating credentials with server") $SAMBA_SERVER..." - - - TEMP_CRED="/tmp/validate_cred_$$" - cat > "$TEMP_CRED" << EOF -username=$USERNAME -password=$PASSWORD -EOF - chmod 600 "$TEMP_CRED" - - - SHARES_OUTPUT=$(smbclient -L "$SAMBA_SERVER" -A "$TEMP_CRED" 2>&1) - SHARES_RESULT=$? - - if [[ $SHARES_RESULT -eq 0 ]]; then - - FIRST_SHARE=$(echo "$SHARES_OUTPUT" | awk '/Disk/ && !/IPC\$/ && !/ADMIN\$/ && !/print\$/ {print $1; exit}') - - if [[ -n "$FIRST_SHARE" ]]; then - - SHARE_TEST_OUTPUT=$(smbclient "//$SAMBA_SERVER/$FIRST_SHARE" -A "$TEMP_CRED" -c "ls" 2>&1) - SHARE_TEST_RESULT=$? - - rm -f "$TEMP_CRED" - - if [[ $SHARE_TEST_RESULT -eq 0 ]]; then - - cleanup - if echo "$SHARE_TEST_OUTPUT" | grep -qi "guest"; then - whiptail --title "$(translate "Authentication Error")" \ - --msgbox "$(translate "The server connected you as guest instead of the specified user.")\n\n$(translate "This means the credentials are incorrect.")\n\n$(translate "Please check:")\n• $(translate "Username is correct")\n• $(translate "Password is correct")\n• $(translate "User account exists on server")" \ - 14 70 - else - msg_ok "$(translate "Credentials validated successfully")" - USE_GUEST=false - return 0 - fi - else - - cleanup - if echo "$SHARE_TEST_OUTPUT" | grep -qi "access denied\|logon failure\|authentication\|NT_STATUS_LOGON_FAILURE"; then - whiptail --title "$(translate "Authentication Error")" \ - --msgbox "$(translate "Invalid username or password.")\n\n$(translate "Error details:")\n$(echo "$SHARE_TEST_OUTPUT" | head -2)\n\n$(translate "Please check:")\n• $(translate "Username is correct")\n• $(translate "Password is correct")\n• $(translate "User account exists on server")" \ - 16 70 - elif echo "$SHARE_TEST_OUTPUT" | grep -qi "connection refused\|network unreachable"; then - whiptail --title "$(translate "Network Error")" \ - --msgbox "$(translate "Cannot connect to server") $SAMBA_SERVER\n\n$(translate "Please check network connectivity.")" \ - 10 60 - return 1 - else - whiptail --title "$(translate "Share Access Error")" \ - --msgbox "$(translate "Failed to access share with provided credentials.")\n\n$(translate "Error details:")\n$(echo "$SHARE_TEST_OUTPUT" | head -3)" \ - 12 70 - fi - fi - else - - cleanup - whiptail --title "$(translate "No Shares Available")" \ - --msgbox "$(translate "Cannot validate credentials - no shares available for testing.")\n\n$(translate "The server may not have accessible shares.")" \ - 10 70 - fi - else - - rm -f "$TEMP_CRED" - - - if echo "$SHARES_OUTPUT" | grep -qi "access denied\|logon failure\|authentication\|NT_STATUS_LOGON_FAILURE"; then - cleanup - whiptail --title "$(translate "Authentication Error")" \ - --msgbox "$(translate "Invalid username or password.")\n\n$(translate "Please check:")\n• $(translate "Username is correct")\n• $(translate "Password is correct")\n• $(translate "User account exists on server")\n• $(translate "Account is not locked")" \ - 12 70 - elif echo "$SHARES_OUTPUT" | grep -qi "connection refused\|network unreachable"; then - cleanup - whiptail --title "$(translate "Network Error")" \ - --msgbox "$(translate "Cannot connect to server") $SAMBA_SERVER\n\n$(translate "Please check network connectivity.")" \ - 10 60 - return 1 - else - cleanup - whiptail --title "$(translate "Connection Error")" \ - --msgbox "$(translate "Failed to connect to server.")\n\n$(translate "Error details:")\n$(echo "$SHARES_OUTPUT" | head -3)" \ - 12 70 - fi - fi - - break - else - cleanup - whiptail --title "$(translate "Password Mismatch")" \ - --msgbox "$(translate "Passwords do not match. Please try again.")" \ - 8 50 - - fi - done - - if [[ $? -ne 0 ]]; then - break - fi - done - ;; - 2) - - if validate_guest_access "$SAMBA_SERVER"; then - USE_GUEST=true - return 0 - fi - ;; - *) - return 1 - ;; - esac - - - if ! whiptail --yesno "$(translate "Authentication failed.")\n\n$(translate "Do you want to try different credentials or authentication method?")" 10 70 --title "$(translate "Try Again")"; then - return 1 - fi - - done + return 0 } +# ========================================================== +# SHARE SELECTION +# ========================================================== + select_samba_share() { - if ! which smbclient >/dev/null 2>&1; then - whiptail --title "$(translate "SMB Client Error")" \ - --msgbox "$(translate "smbclient command is not working properly.")\n\n$(translate "Please check the installation.")" \ - 10 60 - return 1 - fi - - if [[ "$USE_GUEST" == "true" ]]; then - - if [[ -n "$VALIDATED_GUEST_SHARES" ]]; then - SHARES=$(echo -e "$VALIDATED_GUEST_SHARES" | grep -v "^$") - msg_ok "$(translate "Using pre-validated guest shares")" - else - - SHARES_OUTPUT=$(smbclient -L "$SAMBA_SERVER" -N 2>&1) - SHARES_RESULT=$? - if [[ $SHARES_RESULT -eq 0 ]]; then - SHARES=$(echo "$SHARES_OUTPUT" | awk '/Disk/ && !/IPC\$/ && !/ADMIN\$/ && !/print\$/ {print $1}' | grep -v "^$") - else - msg_error "$(translate "Failed to get shares")" - return 1 - fi - fi + SHARES=$(smbclient -L "$SAMBA_SERVER" -N 2>/dev/null | awk '/Disk/ {print $1}' | sort -u || true) else - - TEMP_CRED="/tmp/temp_smb_cred_$$" - cat > "$TEMP_CRED" << EOF -username=$USERNAME -password=$PASSWORD -EOF - chmod 600 "$TEMP_CRED" - - SHARES_OUTPUT=$(smbclient -L "$SAMBA_SERVER" -A "$TEMP_CRED" 2>&1) - SHARES_RESULT=$? - - rm -f "$TEMP_CRED" - - if [[ $SHARES_RESULT -ne 0 ]]; then - msg_error "$(translate "Unexpected error getting shares")" - whiptail --title "$(translate "SMB Error")" \ - --msgbox "$(translate "Failed to get shares from") $SAMBA_SERVER\n\n$(translate "This is unexpected since credentials were validated.")" \ - 12 80 - return 1 - fi - - SHARES=$(echo "$SHARES_OUTPUT" | awk '/Disk/ && !/IPC\$/ && !/ADMIN\$/ && !/print\$/ {print $1}' | grep -v "^$") + SHARES=$(smbclient -L "$SAMBA_SERVER" -U "$USERNAME%$PASSWORD" 2>/dev/null | awk '/Disk/ {print $1}' | sort -u || true) fi - - msg_ok "$(translate "Shares retrieved successfully")" if [[ -z "$SHARES" ]]; then - whiptail --title "$(translate "No Shares Found")" \ - --msgbox "$(translate "No shares found on server") $SAMBA_SERVER\n\n$(translate "You can enter the share name manually.")" \ - 12 70 - - SAMBA_SHARE=$(whiptail --inputbox "$(translate "Enter Samba share name:")" 10 60 --title "$(translate "Share Name")" 3>&1 1>&2 2>&3) + whiptail --title "$(translate "No Available Shares")" \ + --msgbox "$(translate "No accessible shares found.")\n\n$(translate "You can enter the share name manually.")" \ + 10 70 + SAMBA_SHARE=$(whiptail --inputbox "$(translate "Enter Samba share name:")" \ + 10 60 --title "$(translate "Share Name")" 3>&1 1>&2 2>&3) [[ -z "$SAMBA_SHARE" ]] && return 1 return 0 fi - OPTIONS=() while IFS= read -r share; do - if [[ -n "$share" && "$share" != "IPC$" && "$share" != "ADMIN$" && "$share" != "print$" ]]; then - - if [[ "$USE_GUEST" == "true" ]]; then - if echo -e "$VALIDATED_GUEST_SHARES" | grep -q "^$share$"; then - OPTIONS+=("$share" "$(translate "Guest accessible share")") - fi - else - - OPTIONS+=("$share" "$(translate "Samba share")") - fi - fi + [[ -n "$share" ]] && OPTIONS+=("$share" "$(translate "Samba share")") done <<< "$SHARES" - + if [[ ${#OPTIONS[@]} -eq 0 ]]; then - whiptail --title "$(translate "No Available Shares")" \ - --msgbox "$(translate "No accessible shares found.")\n\n$(translate "You can enter the share name manually.")" \ - 10 70 - - SAMBA_SHARE=$(whiptail --inputbox "$(translate "Enter Samba share name:")" 10 60 --title "$(translate "Share Name")" 3>&1 1>&2 2>&3) + SAMBA_SHARE=$(whiptail --inputbox "$(translate "Enter Samba share name:")" \ + 10 60 --title "$(translate "Share Name")" 3>&1 1>&2 2>&3) [[ -z "$SAMBA_SHARE" ]] && return 1 return 0 fi - - SAMBA_SHARE=$(whiptail --title "$(translate "Select Samba Share")" --menu "$(translate "Choose a share to mount:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) + + SAMBA_SHARE=$(whiptail --title "$(translate "Select Samba Share")" \ + --menu "$(translate "Choose a share to mount:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) [[ -n "$SAMBA_SHARE" ]] && return 0 || return 1 } +# ========================================================== +# STORAGE CONFIGURATION +# ========================================================== - - - - -select_host_mount_point() { - local default_path="/mnt/shared_samba_${SAMBA_SHARE}" - - MOUNT_POINT=$(pmx_select_host_mount_point "$(translate "Samba Mount Point")" "$default_path") - [[ -n "$MOUNT_POINT" ]] && return 0 || return 1 -} - - - - -configure_host_mount_options() { - MOUNT_TYPE=$(whiptail --title "$(translate "Mount Options")" --menu "$(translate "Select mount configuration:")" 15 70 4 \ - "1" "$(translate "Default options read/write")" \ - "2" "$(translate "Read-only mount")" \ - "3" "$(translate "Custom options")" 3>&1 1>&2 2>&3) - +configure_cifs_storage() { + STORAGE_ID=$(whiptail --inputbox "$(translate "Enter storage ID for Proxmox:")" \ + 10 60 "cifs-$(echo "$SAMBA_SERVER" | tr '.' '-')" \ + --title "$(translate "Storage ID")" 3>&1 1>&2 2>&3) [[ $? -ne 0 ]] && return 1 - - case "$MOUNT_TYPE" in - 1) - MOUNT_OPTIONS="rw,noperm,file_mode=0664,dir_mode=0775,iocharset=utf8" - ;; - 2) - MOUNT_OPTIONS="ro,noperm,file_mode=0444,dir_mode=0555,iocharset=utf8" - ;; - 3) - MOUNT_OPTIONS=$(whiptail --inputbox "$(translate "Enter custom mount options:")" 10 70 "rw,file_mode=0664,dir_mode=0775" --title "$(translate "Custom Options")" 3>&1 1>&2 2>&3) - [[ $? -ne 0 ]] && return 1 - [[ -z "$MOUNT_OPTIONS" ]] && MOUNT_OPTIONS="rw,noperm,file_mode=0664,dir_mode=0775" - ;; - *) - MOUNT_OPTIONS="rw,noperm,file_mode=0664,dir_mode=0775,iocharset=utf8" - ;; - esac - + [[ -z "$STORAGE_ID" ]] && STORAGE_ID="cifs-$(echo "$SAMBA_SERVER" | tr '.' '-')" - if whiptail --yesno "$(translate "Do you want to make this mount permanent?")\n\n$(translate "This will add the mount to /etc/fstab so it persists after reboot.")" 10 70 --title "$(translate "Permanent Mount")"; then - PERMANENT_MOUNT=true - else - if [[ $? -eq 1 ]]; then - PERMANENT_MOUNT=false - else - return 1 - fi + if [[ ! "$STORAGE_ID" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then + whiptail --msgbox "$(translate "Invalid storage ID. Use only letters, numbers, hyphens and underscores.")" 8 70 + return 1 fi - - # Only ask about Proxmox storage if using username/password authentication - if [[ "$USE_GUEST" != "true" ]]; then - if whiptail --yesno "$(translate "Do you want to add this as Proxmox storage?")\n\n$(translate "This will make the Samba share available as storage in Proxmox web interface.")" 10 70 --title "$(translate "Proxmox Storage")"; then - PROXMOX_STORAGE=true - - STORAGE_ID=$(whiptail --inputbox "$(translate "Enter storage ID for Proxmox:")" 10 60 "cifs-$(echo $SAMBA_SERVER | tr '.' '-')" --title "$(translate "Storage ID")" 3>&1 1>&2 2>&3) - STORAGE_ID_RESULT=$? - - if [[ $STORAGE_ID_RESULT -ne 0 ]]; then - if whiptail --yesno "$(translate "Storage ID input was cancelled.")\n\n$(translate "Do you want to continue without Proxmox storage integration?")" 10 70 --title "$(translate "Continue Without Storage")"; then - PROXMOX_STORAGE=false - else - return 1 - fi - else - [[ -z "$STORAGE_ID" ]] && STORAGE_ID="cifs-$(echo $SAMBA_SERVER | tr '.' '-')" - fi - else - DIALOG_RESULT=$? - if [[ $DIALOG_RESULT -eq 1 ]]; then - PROXMOX_STORAGE=false - else - return 1 - fi - fi - else - # For guest access, don't offer Proxmox storage integration - PROXMOX_STORAGE=false + local raw_content + raw_content=$(dialog --backtitle "ProxMenux" \ + --title "$(translate "Content Types")" \ + --checklist "\n$(translate "Select content types for this storage:")\n$(translate "(Import is selected by default — required for disk image imports)")" 18 65 7 \ + "import" "$(translate "Import — disk image imports")" on \ + "backup" "$(translate "Backup — VM and CT backups")" off \ + "iso" "$(translate "ISO image — installation images")" off \ + "vztmpl" "$(translate "Container template— LXC templates")" off \ + "images" "$(translate "Disk image — VM disk images")" off \ + "snippets" "$(translate "Snippets — hook scripts / config")" off \ + 3>&1 1>&2 2>&3) + [[ $? -ne 0 ]] && return 1 + + # Convert dialog checklist output (quoted space-separated) to comma-separated + MOUNT_CONTENT=$(echo "$raw_content" | tr -d '"' | tr -s ' ' ',' | sed 's/^,//;s/,$//') + [[ -z "$MOUNT_CONTENT" ]] && MOUNT_CONTENT="import" + + # Warn if images selected (CIFS locking issues with VM disks) + if echo "$MOUNT_CONTENT" | grep -q "images"; then + whiptail --title "$(translate "Warning: Disk Images on CIFS")" \ + --msgbox "$(translate "You selected 'Disk image' content on a CIFS/SMB storage.")\n\n$(translate "CIFS can cause file locking issues with VM disk operations.")\n$(translate "NFS is recommended for VM disk image storage.")\n\n$(translate "Continuing with your selection.")" \ + 12 70 fi - + return 0 } -create_credentials_file() { - if [[ "$USE_GUEST" == "true" ]]; then - return 0 - fi - - mkdir -p "$CREDENTIALS_DIR" - chmod 700 "$CREDENTIALS_DIR" - - - CRED_FILE="$CREDENTIALS_DIR/${SAMBA_SERVER}_${SAMBA_SHARE}.cred" - - - cat > "$CRED_FILE" << EOF -username=$USERNAME -password=$PASSWORD -EOF - - chmod 600 "$CRED_FILE" - msg_ok "$(translate "Credentials file created securely.")" -} - add_proxmox_cifs_storage() { local storage_id="$1" local server="$2" local share="$3" - local mount_point="$4" - - if ! which pvesm >/dev/null 2>&1; then + local content="${4:-import}" + + if ! command -v pvesm >/dev/null 2>&1; then msg_error "$(translate "pvesm command not found. This should not happen on Proxmox.")" - echo "Press Enter to continue..." - read -r return 1 fi - + msg_ok "$(translate "pvesm command found")" - if pvesm status "$storage_id" >/dev/null 2>&1; then msg_warn "$(translate "Storage ID already exists:") $storage_id" - if ! whiptail --yesno "$(translate "Storage ID already exists. Do you want to remove and recreate it?")" 8 60 --title "$(translate "Storage Exists")"; then + if ! whiptail --yesno "$(translate "Storage ID already exists. Do you want to remove and recreate it?")" \ + 8 60 --title "$(translate "Storage Exists")"; then return 0 fi pvesm remove "$storage_id" 2>/dev/null || true fi - + msg_ok "$(translate "Storage ID is available")" - + msg_info "$(translate "Adding CIFS storage to Proxmox...")" - CONTENT_LIST="backup,iso,vztmpl" - + local pvesm_result pvesm_output + if [[ "$USE_GUEST" == "true" ]]; then + pvesm_output=$(pvesm add cifs "$storage_id" \ + --server "$server" \ + --share "$share" \ + --content "$content" 2>&1) + pvesm_result=$? + else + pvesm_output=$(pvesm add cifs "$storage_id" \ + --server "$server" \ + --share "$share" \ + --username "$USERNAME" \ + --password "$PASSWORD" \ + --content "$content" 2>&1) + pvesm_result=$? + fi - msg_info "$(translate "Adding authenticated storage to Proxmox...")" - PVESM_OUTPUT=$(pvesm add cifs "$storage_id" \ - --server "$server" \ - --share "$share" \ - --username "$USERNAME" \ - --password "$PASSWORD" \ - --content "$CONTENT_LIST" 2>&1) - PVESM_RESULT=$? - - if [[ $PVESM_RESULT -eq 0 ]]; then + if [[ $pvesm_result -eq 0 ]]; then msg_ok "$(translate "CIFS storage added successfully to Proxmox!")" echo -e "" - echo -e "${TAB}${BOLD}$(translate "Storage Added Information:")${CL}" + echo -e "${TAB}${BOLD}$(translate "Storage Added:")${CL}" echo -e "${TAB}${BGN}$(translate "Storage ID:")${CL} ${BL}$storage_id${CL}" echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$server${CL}" echo -e "${TAB}${BGN}$(translate "Share:")${CL} ${BL}$share${CL}" - echo -e "${TAB}${BGN}$(translate "Content Types:")${CL} ${BL}$CONTENT_LIST${CL}" - echo -e "${TAB}${BGN}$(translate "Authentication:")${CL} ${BL}User: $USERNAME${CL}" + echo -e "${TAB}${BGN}$(translate "Content Types:")${CL} ${BL}$content${CL}" + echo -e "${TAB}${BGN}$(translate "Authentication:")${CL} ${BL}$([ "$USE_GUEST" == "true" ] && echo "Guest" || echo "User: $USERNAME")${CL}" + echo -e "${TAB}${BGN}$(translate "Mount Path:")${CL} ${BL}/mnt/pve/$storage_id${CL}" echo -e "" msg_ok "$(translate "Storage is now available in Proxmox web interface under Datacenter > Storage")" return 0 else - msg_error "$(translate "Failed to add CIFS storage to Proxmox.")" - echo -e "${TAB}$(translate "Error details:"): $PVESM_OUTPUT" - msg_warn "$(translate "The Samba share is still mounted, but not added as Proxmox storage.")" + echo -e "${TAB}$(translate "Error details:"): $pvesm_output" echo -e "" msg_info2 "$(translate "You can add it manually through:")" echo -e "${TAB}• $(translate "Proxmox web interface: Datacenter > Storage > Add > SMB/CIFS")" - echo -e "${TAB}• $(translate "Command line:"): pvesm add cifs $storage_id --server $server --share $share --username $USERNAME --password [PASSWORD] --content backup,iso,vztmpl" - + echo -e "${TAB}• pvesm add cifs $storage_id --server $server --share $share --username USER --password PASS --content $content" return 1 fi } -mount_host_samba_share() { +# ========================================================== +# MAIN OPERATIONS +# ========================================================== +add_cifs_to_proxmox() { if ! which smbclient >/dev/null 2>&1; then msg_info "$(translate "Installing Samba client tools...")" apt-get update &>/dev/null apt-get install -y cifs-utils smbclient &>/dev/null msg_ok "$(translate "Samba client tools installed")" fi - - # Step 1: + + # Step 1: Select server select_samba_server || return - - # Step 2: + + # Step 2: Get credentials get_samba_credentials || return - - # Step 3: + + # Step 3: Select share select_samba_share || return - - # Step 4: - select_host_mount_point || return - - # Step 5: - configure_host_mount_options || return - show_proxmenux_logo - msg_title "$(translate "Mount Samba Share on Host")" - + msg_title "$(translate "Add Samba Share as Proxmox Storage")" + msg_ok "$(translate "Server:") $SAMBA_SERVER" + msg_ok "$(translate "Share:") $SAMBA_SHARE" + msg_ok "$(translate "Auth:") $([ "$USE_GUEST" == "true" ] && echo "Guest" || echo "User: $USERNAME")" - prepare_host_directory "$MOUNT_POINT" || return 1 - + # Step 4: Configure storage + configure_cifs_storage || return - if mount | grep -q "$MOUNT_POINT"; then - msg_warn "$(translate "Something is already mounted at") $MOUNT_POINT" - if ! whiptail --yesno "$(translate "Do you want to unmount it first?")" 8 60 --title "$(translate "Already Mounted")"; then - return - fi - umount "$MOUNT_POINT" 2>/dev/null || true - fi - - - if [[ "$USE_GUEST" != "true" ]]; then - create_credentials_file - CRED_OPTION="credentials=$CRED_FILE" - else - CRED_OPTION="guest" - fi - - - # --- Ensure correct group mapping --- - if [[ "$SHARE_COMMON_LOADED" == "true" ]]; then - GROUP=$(pmx_share_map_get "$MOUNT_POINT") - if [[ -z "$GROUP" ]]; then - GROUP=$(pmx_choose_or_create_group "sharedfiles") || return 1 - pmx_share_map_set "$MOUNT_POINT" "$GROUP" - fi - - HOST_GID=$(pmx_ensure_host_group "$GROUP" 101000) || return 1 - MOUNT_OPTIONS="$MOUNT_OPTIONS,gid=$HOST_GID,uid=0" - fi - - - - FULL_OPTIONS="$MOUNT_OPTIONS,$CRED_OPTION" - UNC_PATH="//$SAMBA_SERVER/$SAMBA_SHARE" - - msg_info "$(translate "Mounting Samba share...")" - if mount -t cifs "$UNC_PATH" "$MOUNT_POINT" -o "$FULL_OPTIONS" > /dev/null 2>&1; then - msg_ok "$(translate "Samba share mounted successfully on host!")" - - - if touch "$MOUNT_POINT/.test_write" 2>/dev/null; then - rm "$MOUNT_POINT/.test_write" 2>/dev/null - msg_ok "$(translate "Write access confirmed.")" - else - msg_warn "$(translate "Read-only access (or no write permissions).")" - fi - - - if [[ "$PERMANENT_MOUNT" == "true" ]]; then - - sed -i "\|$MOUNT_POINT|d" /etc/fstab - FSTAB_ENTRY="$UNC_PATH $MOUNT_POINT cifs $FULL_OPTIONS 0 0" - echo "$FSTAB_ENTRY" >> /etc/fstab - msg_ok "$(translate "Added to /etc/fstab for permanent mounting.")" - - - systemctl daemon-reload 2>/dev/null || true - msg_ok "$(translate "Systemd configuration reloaded.")" - fi - - - if [[ "$PROXMOX_STORAGE" == "true" ]]; then - add_proxmox_cifs_storage "$STORAGE_ID" "$SAMBA_SERVER" "$SAMBA_SHARE" "$MOUNT_POINT" - fi - - - echo -e "" - echo -e "${TAB}${BOLD}$(translate "Host Mount Information:")${CL}" - echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$SAMBA_SERVER${CL}" - echo -e "${TAB}${BGN}$(translate "Share:")${CL} ${BL}$SAMBA_SHARE${CL}" - echo -e "${TAB}${BGN}$(translate "Host Mount Point:")${CL} ${BL}$MOUNT_POINT${CL}" - echo -e "${TAB}${BGN}$(translate "Options:")${CL} ${BL}$MOUNT_OPTIONS${CL}" - echo -e "${TAB}${BGN}$(translate "Authentication:")${CL} ${BL}$([ "$USE_GUEST" == "true" ] && echo "Guest" || echo "User: $USERNAME")${CL}" - echo -e "${TAB}${BGN}$(translate "Permanent:")${CL} ${BL}$PERMANENT_MOUNT${CL}" - if [[ "$PROXMOX_STORAGE" == "true" ]]; then - echo -e "${TAB}${BGN}$(translate "Proxmox Storage ID:")${CL} ${BL}$STORAGE_ID${CL}" - fi - - else - msg_error "$(translate "Failed to mount Samba share on host.")" - echo -e "${TAB}$(translate "This should not happen since credentials were validated.")" - echo -e "${TAB}$(translate "Please check system logs for details.")" - - - if [[ "$USE_GUEST" != "true" && -n "$CRED_FILE" ]]; then - rm -f "$CRED_FILE" 2>/dev/null || true - fi - fi - + # Step 5: Add to Proxmox + show_proxmenux_logo + msg_title "$(translate "Add Samba Share as Proxmox Storage")" + msg_ok "$(translate "Server:") $SAMBA_SERVER" + msg_ok "$(translate "Share:") $SAMBA_SHARE" + msg_ok "$(translate "Storage ID:") $STORAGE_ID" + msg_ok "$(translate "Content:") $MOUNT_CONTENT" echo -e "" - msg_success "$(translate "Press Enter to return to menu...")" + + add_proxmox_cifs_storage "$STORAGE_ID" "$SAMBA_SERVER" "$SAMBA_SHARE" "$MOUNT_CONTENT" + + echo -e "" + msg_success "$(translate "Press Enter to continue...")" read -r } -view_host_samba_mounts() { +view_cifs_storages() { show_proxmenux_logo - msg_title "$(translate "Current Samba Mounts on Host")" - - echo -e "$(translate "Samba/CIFS mounts on Proxmox host:"):" - echo "==================================" - + msg_title "$(translate "CIFS Storages in Proxmox")" - CURRENT_MOUNTS=$(mount -t cifs 2>/dev/null || true) - if [[ -n "$CURRENT_MOUNTS" ]]; then - echo -e "${BOLD}$(translate "Currently Mounted:")${CL}" - echo "$CURRENT_MOUNTS" - echo "" - else - echo "$(translate "No Samba shares currently mounted on host.")" - echo "" - fi - - - FSTAB_CIFS=$(grep "cifs" /etc/fstab 2>/dev/null || true) - if [[ -n "$FSTAB_CIFS" ]]; then - echo -e "${BOLD}$(translate "Permanent Mounts (fstab):")${CL}" - echo "$FSTAB_CIFS" - echo "" - - echo -e "${TAB}${BOLD}$(translate "Mount Details:")${CL}" - while IFS= read -r fstab_line; do - if [[ -n "$fstab_line" && ! "$fstab_line" =~ ^# ]]; then - UNC_PATH=$(echo "$fstab_line" | awk '{print $1}') - MOUNT_POINT=$(echo "$fstab_line" | awk '{print $2}') - OPTIONS=$(echo "$fstab_line" | awk '{print $4}') - - - SERVER=$(echo "$UNC_PATH" | cut -d/ -f3) - SHARE=$(echo "$UNC_PATH" | cut -d/ -f4) - - echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$SERVER${CL}" - echo -e "${TAB}${BGN}$(translate "Share:")${CL} ${BL}$SHARE${CL}" - echo -e "${TAB}${BGN}$(translate "Host Mount Point:")${CL} ${BL}$MOUNT_POINT${CL}" - echo -e "${TAB}${BGN}$(translate "Options:")${CL} ${BL}$OPTIONS${CL}" - - - if echo "$OPTIONS" | grep -q "guest"; then - echo -e "${TAB}${BGN}$(translate "Authentication:")${CL} ${BL}Guest${CL}" - elif echo "$OPTIONS" | grep -q "credentials="; then - CRED_FILE=$(echo "$OPTIONS" | grep -o "credentials=[^,]*" | cut -d= -f2) - echo -e "${TAB}${BGN}$(translate "Authentication:")${CL} ${BL}Credentials ($CRED_FILE)${CL}" - fi - - - if mount | grep -q "$MOUNT_POINT"; then - echo -e "${TAB}${BGN}$(translate "Status:")${CL} ${GN}$(translate "Mounted")${CL}" - else - echo -e "${TAB}${BGN}$(translate "Status:")${CL} ${RD}$(translate "Not Mounted")${CL}" - fi - echo "" - fi - done <<< "$FSTAB_CIFS" - else - echo "$(translate "No Samba mounts found in fstab.")" - fi - - - echo -e "${BOLD}$(translate "Proxmox CIFS Storage:")${CL}" - if which pvesm >/dev/null 2>&1; then - CIFS_STORAGES=$(pvesm status 2>/dev/null | grep "cifs" | awk '{print $1}' || true) - if [[ -n "$CIFS_STORAGES" ]]; then - while IFS= read -r storage_id; do - if [[ -n "$storage_id" ]]; then - echo -e "${TAB}${BGN}$(translate "Storage ID:")${CL} ${BL}$storage_id${CL}" - - - STORAGE_INFO=$(pvesm config "$storage_id" 2>/dev/null || true) - if [[ -n "$STORAGE_INFO" ]]; then - SERVER=$(echo "$STORAGE_INFO" | grep "server" | awk '{print $2}') - SHARE=$(echo "$STORAGE_INFO" | grep "share" | awk '{print $2}') - CONTENT=$(echo "$STORAGE_INFO" | grep "content" | awk '{print $2}') - USERNAME=$(echo "$STORAGE_INFO" | grep "username" | awk '{print $2}') - - [[ -n "$SERVER" ]] && echo -e "${TAB} ${BGN}$(translate "Server:")${CL} ${BL}$SERVER${CL}" - [[ -n "$SHARE" ]] && echo -e "${TAB} ${BGN}$(translate "Share:")${CL} ${BL}$SHARE${CL}" - [[ -n "$CONTENT" ]] && echo -e "${TAB} ${BGN}$(translate "Content:")${CL} ${BL}$CONTENT${CL}" - [[ -n "$USERNAME" ]] && echo -e "${TAB} ${BGN}$(translate "Username:")${CL} ${BL}$USERNAME${CL}" - fi - echo "" - fi - done <<< "$CIFS_STORAGES" - else - echo -e "${TAB}$(translate "No CIFS storage configured in Proxmox")" - fi - else - echo -e "${TAB}$(translate "pvesm command not available")" - fi - - - CRED_FILES=$(find "$CREDENTIALS_DIR" -name "*.cred" 2>/dev/null || true) - if [[ -n "$CRED_FILES" ]]; then - echo -e "${BOLD}$(translate "Stored Credentials:")${CL}" - while IFS= read -r cred_file; do - if [[ -n "$cred_file" ]]; then - FILENAME=$(basename "$cred_file") - echo -e "${TAB}• $FILENAME" - fi - done <<< "$CRED_FILES" - echo "" - fi - + echo "==================================================" echo "" - msg_success "$(translate "Press Enter to return to menu...")" - read -r -} -unmount_host_samba_share() { - - MOUNTS=$(mount -t cifs 2>/dev/null | awk '{print $3}' | sort -u || true) - FSTAB_MOUNTS=$(grep -E "cifs" /etc/fstab 2>/dev/null | grep -v "^#" | awk '{print $2}' | sort -u || true) - - - ALL_MOUNTS=$(echo -e "$MOUNTS\n$FSTAB_MOUNTS" | sort -u | grep -v "^$" || true) - - if [[ -z "$ALL_MOUNTS" ]]; then - dialog --backtitle "ProxMenux" --title "$(translate "No Mounts")" --msgbox "\n$(translate "No Samba mounts found on host.")" 8 50 + if ! command -v pvesm >/dev/null 2>&1; then + msg_error "$(translate "pvesm not found.")" + echo "" + msg_success "$(translate "Press Enter to continue...")" + read -r return fi - - OPTIONS=() - while IFS= read -r mount_point; do - if [[ -n "$mount_point" ]]; then - UNC_PATH=$(mount | grep "$mount_point" | awk '{print $1}' || grep "$mount_point" /etc/fstab | awk '{print $1}' || echo "Unknown") - SERVER=$(echo "$UNC_PATH" | cut -d/ -f3) - SHARE=$(echo "$UNC_PATH" | cut -d/ -f4) - OPTIONS+=("$mount_point" "$SERVER/$SHARE") - fi - done <<< "$ALL_MOUNTS" - - SELECTED_MOUNT=$(dialog --backtitle "ProxMenux" --title "$(translate "Unmount Samba Share")" --menu "$(translate "Select mount point to unmount:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) - [[ -z "$SELECTED_MOUNT" ]] && return - + CIFS_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "cifs" {print $1, $3}') + if [[ -z "$CIFS_STORAGES" ]]; then + msg_warn "$(translate "No CIFS storage configured in Proxmox.")" + echo "" + msg_info2 "$(translate "Use option 1 to add a Samba share as Proxmox storage.")" + else + echo -e "${BOLD}$(translate "CIFS Storages:")${CL}" + echo "" + while IFS=" " read -r storage_id storage_status; do + [[ -z "$storage_id" ]] && continue + local storage_info server share content username + storage_info=$(get_storage_config "$storage_id") + server=$(echo "$storage_info" | awk '$1 == "server" {print $2}') + share=$(echo "$storage_info" | awk '$1 == "share" {print $2}') + content=$(echo "$storage_info" | awk '$1 == "content" {print $2}') + username=$(echo "$storage_info" | awk '$1 == "username" {print $2}') - UNC_PATH=$(mount | grep "$SELECTED_MOUNT" | awk '{print $1}' || grep "$SELECTED_MOUNT" /etc/fstab | awk '{print $1}' || echo "Unknown") - SERVER=$(echo "$UNC_PATH" | cut -d/ -f3) - SHARE=$(echo "$UNC_PATH" | cut -d/ -f4) - - - PROXMOX_STORAGE="" - if which pvesm >/dev/null 2>&1; then - - CIFS_STORAGES=$(pvesm status 2>/dev/null | grep "cifs" | awk '{print $1}' || true) - while IFS= read -r storage_id; do - if [[ -n "$storage_id" ]]; then - STORAGE_INFO=$(pvesm config "$storage_id" 2>/dev/null || true) - STORAGE_SERVER=$(echo "$STORAGE_INFO" | grep "server" | awk '{print $2}') - STORAGE_SHARE=$(echo "$STORAGE_INFO" | grep "share" | awk '{print $2}') - if [[ "$STORAGE_SERVER" == "$SERVER" && "$STORAGE_SHARE" == "$SHARE" ]]; then - PROXMOX_STORAGE="$storage_id" - break - fi + echo -e "${TAB}${BOLD}$storage_id${CL}" + echo -e "${TAB} ${BGN}$(translate "Server:")${CL} ${BL}$server${CL}" + echo -e "${TAB} ${BGN}$(translate "Share:")${CL} ${BL}$share${CL}" + echo -e "${TAB} ${BGN}$(translate "Content:")${CL} ${BL}$content${CL}" + if [[ -n "$username" ]]; then + echo -e "${TAB} ${BGN}$(translate "Username:")${CL} ${BL}$username${CL}" + else + echo -e "${TAB} ${BGN}$(translate "Auth:")${CL} ${BL}Guest${CL}" fi + echo -e "${TAB} ${BGN}$(translate "Mount Path:")${CL} ${BL}/mnt/pve/$storage_id${CL}" + if [[ "$storage_status" == "active" ]]; then + echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${GN}$(translate "Active")${CL}" + else + echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${RD}$storage_status${CL}" + fi + echo "" done <<< "$CIFS_STORAGES" fi - - CONFIRMATION_MSG="$(translate "Are you sure you want to unmount this Samba share?")\n\n$(translate "Mount Point:"): $SELECTED_MOUNT\n$(translate "Server:"): $SERVER\n$(translate "Share:"): $SHARE\n\n$(translate "This will:")\n• $(translate "Unmount the Samba share")\n• $(translate "Remove from /etc/fstab")" - - if [[ -n "$PROXMOX_STORAGE" ]]; then - CONFIRMATION_MSG="$CONFIRMATION_MSG\n• $(translate "Remove Proxmox storage:"): $PROXMOX_STORAGE" - fi - - CONFIRMATION_MSG="$CONFIRMATION_MSG\n• $(translate "Remove credentials file")\n• $(translate "Remove mount point directory")" - - if whiptail --yesno "$CONFIRMATION_MSG" 18 80 --title "$(translate "Confirm Unmount")"; then - show_proxmenux_logo - msg_title "$(translate "Unmount Samba Share from Host")" - - - if [[ -n "$PROXMOX_STORAGE" ]]; then - if pvesm remove "$PROXMOX_STORAGE" 2>/dev/null; then - msg_ok "$(translate "Proxmox storage removed successfully.")" - else - msg_warn "$(translate "Failed to remove Proxmox storage, continuing with unmount...")" - fi - fi - - - if mount | grep -q "$SELECTED_MOUNT"; then - if umount "$SELECTED_MOUNT"; then - msg_ok "$(translate "Successfully unmounted.")" - else - msg_warn "$(translate "Failed to unmount. Trying force unmount...")" - if umount -f "$SELECTED_MOUNT" 2>/dev/null; then - msg_ok "$(translate "Force unmount successful.")" - else - msg_error "$(translate "Failed to unmount. Mount point may be busy.")" - echo -e "${TAB}$(translate "Try closing any applications using the mount point.")" - fi - fi - fi - - - CRED_FILE=$(grep "$SELECTED_MOUNT" /etc/fstab 2>/dev/null | grep -o "credentials=[^,]*" | cut -d= -f2 || true) - - - sed -i "\|[[:space:]]$SELECTED_MOUNT[[:space:]]|d" /etc/fstab - msg_ok "$(translate "Removed from /etc/fstab.")" - - - if [[ -n "$CRED_FILE" && "$CRED_FILE" != "guest" ]]; then - if test -f "$CRED_FILE"; then - rm -f "$CRED_FILE" - msg_ok "$(translate "Credentials file removed.")" - fi - fi - - echo -e "" - msg_ok "$(translate "Samba share unmounted successfully from host!")" - - if [[ -n "$PROXMOX_STORAGE" ]]; then - echo -e "${TAB}${BGN}$(translate "Proxmox storage removed:")${CL} ${BL}$PROXMOX_STORAGE${CL}" - fi - echo -e "${TAB}${BGN}$(translate "Mount point unmounted:")${CL} ${BL}$SELECTED_MOUNT${CL}" - echo -e "${TAB}${BGN}$(translate "Removed from fstab:")${CL} ${BL}Yes${CL}" - fi - - echo -e "" - msg_success "$(translate "Press Enter to return to menu...")" + echo "" + msg_success "$(translate "Press Enter to continue...")" read -r } -manage_proxmox_cifs_storage() { +remove_cifs_storage() { if ! command -v pvesm >/dev/null 2>&1; then - dialog --backtitle "ProxMenux" --title "$(translate "Error")" --msgbox "\n$(translate "pvesm command not found. This should not happen on Proxmox.")" 8 60 + dialog --backtitle "ProxMenux" --title "$(translate "Error")" \ + --msgbox "\n$(translate "pvesm not found.")" 8 60 return fi - CIFS_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "cifs" {print $1}') if [[ -z "$CIFS_STORAGES" ]]; then - dialog --backtitle "ProxMenux" --title "$(translate "No CIFS Storage")" --msgbox "\n$(translate "No CIFS storage found in Proxmox.")" 8 60 + dialog --backtitle "ProxMenux" --title "$(translate "No CIFS Storage")" \ + --msgbox "\n$(translate "No CIFS storage found in Proxmox.")" 8 60 return fi - OPTIONS=() while IFS= read -r storage_id; do - if [[ -n "$storage_id" ]]; then - STORAGE_INFO=$(pvesm config "$storage_id" 2>/dev/null || true) - SERVER=$(echo "$STORAGE_INFO" | grep "server" | awk '{print $2}') - SHARE=$(echo "$STORAGE_INFO" | grep "share" | awk '{print $2}') - - if [[ -n "$SERVER" && -n "$SHARE" ]]; then - OPTIONS+=("$storage_id" "$SERVER/$SHARE") - else - OPTIONS+=("$storage_id" "$(translate "CIFS Storage")") - fi - fi + [[ -z "$storage_id" ]] && continue + local storage_info server share + storage_info=$(get_storage_config "$storage_id") + server=$(echo "$storage_info" | awk '$1 == "server" {print $2}') + share=$(echo "$storage_info" | awk '$1 == "share" {print $2}') + OPTIONS+=("$storage_id" "$server/$share") done <<< "$CIFS_STORAGES" - - SELECTED_STORAGE=$(dialog --backtitle "ProxMenux" --title "$(translate "Manage Proxmox CIFS Storage")" --menu "$(translate "Select storage to manage:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3) - [[ -z "$SELECTED_STORAGE" ]] && return + SELECTED=$(dialog --backtitle "ProxMenux" --title "$(translate "Remove CIFS Storage")" \ + --menu "$(translate "Select storage to remove:")" 20 80 10 \ + "${OPTIONS[@]}" 3>&1 1>&2 2>&3) + [[ -z "$SELECTED" ]] && return - STORAGE_INFO=$(pvesm config "$SELECTED_STORAGE" 2>/dev/null || true) - SERVER=$(echo "$STORAGE_INFO" | grep "server" | awk '{print $2}') - SHARE=$(echo "$STORAGE_INFO" | grep "share" | awk '{print $2}') - CONTENT=$(echo "$STORAGE_INFO" | grep "content" | awk '{print $2}') + local storage_info server share content username + storage_info=$(get_storage_config "$SELECTED") + server=$(echo "$storage_info" | awk '$1 == "server" {print $2}') + share=$(echo "$storage_info" | awk '$1 == "share" {print $2}') + content=$(echo "$storage_info" | awk '$1 == "content" {print $2}') + username=$(echo "$storage_info" | awk '$1 == "username" {print $2}') + + if whiptail --yesno "$(translate "Remove Proxmox CIFS storage:")\n\n$SELECTED\n\n$(translate "Server:"): $server\n$(translate "Share:"): $share\n$(translate "Content:"): $content\n\n$(translate "WARNING: This removes the storage from Proxmox. The Samba server is not affected.")" \ + 16 80 --title "$(translate "Confirm Remove")"; then - if whiptail --yesno "$(translate "Are you sure you want to REMOVE storage:")\n\n$SELECTED_STORAGE\n\n$(translate "WARNING: This will permanently remove the storage from Proxmox configuration.")\n$(translate "The Samba mount on the host will NOT be affected.")" 14 80 --title "$(translate "Remove Storage")"; then show_proxmenux_logo - msg_title "$(translate "Remove Storage")" - - if pvesm remove "$SELECTED_STORAGE" 2>/dev/null; then - msg_ok "$(translate "Storage removed successfully from Proxmox.")" - echo -e "" - msg_success "$(translate "Press Enter to return to menu...")" - read -r + msg_title "$(translate "Remove CIFS Storage")" + + if pvesm remove "$SELECTED" 2>/dev/null; then + msg_ok "$(translate "Storage") $SELECTED $(translate "removed successfully from Proxmox.")" else msg_error "$(translate "Failed to remove storage.")" fi + + echo -e "" + msg_success "$(translate "Press Enter to continue...")" + read -r fi } -test_host_samba_connectivity() { +test_samba_connectivity() { show_proxmenux_logo - msg_title "$(translate "Test Samba Connectivity on Host")" - - echo -e "$(translate "Samba/CIFS Client Status on Proxmox Host:"):" - echo "==================================" - + msg_title "$(translate "Test Samba Connectivity")" + + echo "==================================================" + echo "" if which smbclient >/dev/null 2>&1; then - echo "$(translate "CIFS Client Tools: AVAILABLE")" - - - if which mount.cifs >/dev/null 2>&1; then - echo "$(translate "CIFS Mount Tools: AVAILABLE")" - else - echo "$(translate "CIFS Mount Tools: NOT AVAILABLE")" - fi - - echo "" - echo "$(translate "Current CIFS mounts on host:")" - CURRENT_MOUNTS=$(mount -t cifs 2>/dev/null || true) - if [[ -n "$CURRENT_MOUNTS" ]]; then - echo "$CURRENT_MOUNTS" - else - echo "$(translate "No CIFS mounts active on host.")" - fi - - echo "" - echo "$(translate "Testing network connectivity...")" - - - FSTAB_SERVERS=$(grep "cifs" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d/ -f3 | sort -u || true) - if [[ -n "$FSTAB_SERVERS" ]]; then - while IFS= read -r server; do - if [[ -n "$server" ]]; then - echo -n "$(translate "Testing") $server: " - if ping -c 1 -W 2 "$server" >/dev/null 2>&1; then - echo -e "${GN}$(translate "Reachable")${CL}" - - - if nc -z -w 2 "$server" 445 2>/dev/null; then - echo " $(translate "SMB port 445:"): ${GN}$(translate "Open")${CL}" - elif nc -z -w 2 "$server" 139 2>/dev/null; then - echo " $(translate "NetBIOS port 139:"): ${GN}$(translate "Open")${CL}" - else - echo " $(translate "SMB ports:"): ${RD}$(translate "Closed")${CL}" - fi - - - echo -n " $(translate "Guest access test:"): " - if smbclient -L "$server" -N >/dev/null 2>&1; then - echo -e "${GN}$(translate "Available")${CL}" - else - echo -e "${YW}$(translate "Requires authentication")${CL}" - fi - else - echo -e "${RD}$(translate "Unreachable")${CL}" - fi - fi - done <<< "$FSTAB_SERVERS" - else - echo "$(translate "No Samba servers configured to test.")" - fi - - - echo "" - echo "$(translate "Proxmox CIFS Storage Status:")" - if which pvesm >/dev/null 2>&1; then - CIFS_STORAGES=$(pvesm status 2>/dev/null | grep "cifs" || true) - if [[ -n "$CIFS_STORAGES" ]]; then - echo "$CIFS_STORAGES" - else - echo "$(translate "No CIFS storage configured in Proxmox.")" - fi - else - echo "$(translate "pvesm command not available.")" - fi - - - echo "" - echo "$(translate "Stored credentials:")" - CRED_FILES=$(find "$CREDENTIALS_DIR" -name "*.cred" 2>/dev/null || true) - if [[ -n "$CRED_FILES" ]]; then - while IFS= read -r cred_file; do - if [[ -n "$cred_file" ]]; then - FILENAME=$(basename "$cred_file") - echo " • $FILENAME" - fi - done <<< "$CRED_FILES" - else - echo " $(translate "No stored credentials found.")" - fi - + msg_ok "$(translate "CIFS Client Tools: AVAILABLE")" else - echo "$(translate "CIFS Client Tools: NOT AVAILABLE")" - echo "" - echo "$(translate "Installing CIFS client tools...")" + msg_warn "$(translate "CIFS Client Tools: NOT AVAILABLE - installing...")" apt-get update &>/dev/null apt-get install -y cifs-utils smbclient &>/dev/null - echo "$(translate "CIFS client tools installed.")" + msg_ok "$(translate "CIFS client tools installed.")" fi echo "" - echo "$(translate "ProxMenux Extensions:")" - if [[ "$SHARE_COMMON_LOADED" == "true" ]]; then - echo "$(translate "Shared Functions: LOADED")" - if [[ -f "$PROXMENUX_SHARE_MAP_DB" ]]; then - MAPPED_DIRS=$(wc -l < "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || echo "0") - echo "$(translate "Mapped directories:"): $MAPPED_DIRS" + + if command -v pvesm >/dev/null 2>&1; then + echo -e "${BOLD}$(translate "Proxmox CIFS Storage Status:")${CL}" + CIFS_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "cifs" {print $1, $3}') + + if [[ -n "$CIFS_STORAGES" ]]; then + while IFS=" " read -r storage_id storage_status; do + [[ -z "$storage_id" ]] && continue + local server + server=$(get_storage_config "$storage_id" | awk '$1 == "server" {print $2}') + + echo -n " $storage_id ($server): " + + if ping -c 1 -W 2 "$server" >/dev/null 2>&1; then + echo -ne "${GN}$(translate "Reachable")${CL}" + + if nc -z -w 2 "$server" 445 2>/dev/null; then + echo -e " | SMB 445: ${GN}$(translate "Open")${CL}" + elif nc -z -w 2 "$server" 139 2>/dev/null; then + echo -e " | NetBIOS 139: ${GN}$(translate "Open")${CL}" + else + echo -e " | SMB ports: ${RD}$(translate "Closed")${CL}" + fi + + echo -n " $(translate "Guest access test:"): " + if smbclient -L "$server" -N >/dev/null 2>&1; then + echo -e "${GN}$(translate "Available")${CL}" + else + echo -e "${YW}$(translate "Requires authentication")${CL}" + fi + else + echo -e "${RD}$(translate "Unreachable")${CL}" + fi + + if [[ "$storage_status" == "active" ]]; then + echo -e " $(translate "Proxmox status:") ${GN}$storage_status${CL}" + else + echo -e " $(translate "Proxmox status:") ${RD}$storage_status${CL}" + fi + echo "" + done <<< "$CIFS_STORAGES" + else + echo " $(translate "No CIFS storage configured.")" fi else - echo "$(translate "Shared Functions: NOT LOADED (using fallback methods)")" + msg_warn "$(translate "pvesm not available.")" fi - + echo "" - msg_success "$(translate "Press Enter to return to menu...")" + msg_success "$(translate "Press Enter to continue...")" read -r } -prepare_host_directory() { - local mount_point="$1" - - if [[ "$SHARE_COMMON_LOADED" == "true" ]]; then - # Use common functions for advanced directory preparation - local group_name - group_name=$(pmx_choose_or_create_group "sharedfiles") - if [[ -n "$group_name" ]]; then - local host_gid - host_gid=$(pmx_ensure_host_group "$group_name") - if [[ -n "$host_gid" ]]; then - pmx_prepare_host_shared_dir "$mount_point" "$group_name" - pmx_share_map_set "$mount_point" "$group_name" - msg_ok "$(translate "Directory prepared with shared group:") $group_name (GID: $host_gid)" - return 0 - fi - fi - msg_warn "$(translate "Failed to use shared functions, using basic directory creation.")" - fi - - # Fallback: basic directory creation - if ! test -d "$mount_point"; then - if mkdir -p "$mount_point"; then - msg_ok "$(translate "Mount point created on host.")" - return 0 - else - msg_error "$(translate "Failed to create mount point on host.")" - return 1 - fi - fi - return 0 -} +# ========================================================== +# MAIN MENU +# ========================================================== -# === Main Menu === while true; do - CHOICE=$(dialog --backtitle "ProxMenux" --title "$(translate "Samba Host Manager - Proxmox Host")" \ - --menu "$(translate "Choose an option:")" 22 80 14 \ - "1" "$(translate "Mount Samba Share on Host")" \ - "2" "$(translate "View Current Host Samba Mounts")" \ - "3" "$(translate "Unmount Samba Share from Host")" \ - "4" "$(translate "Remove Proxmox CIFS Storage")" \ - "5" "$(translate "Test Samba Connectivity")" \ - "6" "$(translate "Exit")" \ - 3>&1 1>&2 2>&3) - + CHOICE=$(dialog --backtitle "ProxMenux" \ + --title "$(translate "Samba Host Manager - Proxmox Host")" \ + --menu "$(translate "Choose an option:")" 18 70 6 \ + "1" "$(translate "Add Samba Share as Proxmox Storage")" \ + "2" "$(translate "View CIFS Storages")" \ + "3" "$(translate "Remove CIFS Storage")" \ + "4" "$(translate "Test Samba Connectivity")" \ + "5" "$(translate "Exit")" \ + 3>&1 1>&2 2>&3) + RETVAL=$? if [[ $RETVAL -ne 0 ]]; then exit 0 fi - + case $CHOICE in - 1) mount_host_samba_share ;; - 2) view_host_samba_mounts ;; - 3) unmount_host_samba_share ;; - 4) manage_proxmox_cifs_storage ;; - 5) test_host_samba_connectivity ;; - 6) exit 0 ;; + 1) add_cifs_to_proxmox ;; + 2) view_cifs_storages ;; + 3) remove_cifs_storage ;; + 4) test_samba_connectivity ;; + 5) exit 0 ;; *) exit 0 ;; esac done