mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-05 20:03:48 +00:00
update scripts gpu
This commit is contained in:
638
scripts/gpu_tpu/add_gpu_lxc.sh
Normal file
638
scripts/gpu_tpu/add_gpu_lxc.sh
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
494
scripts/gpu_tpu/nvidia_update.sh
Normal file
494
scripts/gpu_tpu/nvidia_update.sh
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
;;
|
||||
|
||||
656
scripts/share/disk_host.sh
Normal file
656
scripts/share/disk_host.sh
Normal file
@@ -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
|
||||
518
scripts/share/iscsi_host.sh
Normal file
518
scripts/share/iscsi_host.sh
Normal file
@@ -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 <portal> <iqn>
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user