mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 00:46:21 +00:00
Update scripts
This commit is contained in:
385
scripts/global/disk_ops_helpers.sh
Normal file
385
scripts/global/disk_ops_helpers.sh
Normal file
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenux - Disk Operations Helpers
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : MIT
|
||||
# Version : 1.0
|
||||
# Last Updated: 11/04/2026
|
||||
# ==========================================================
|
||||
# Shared low-level disk operations: wipe, partition, format.
|
||||
# Consumed by format-disk.sh, disk_host.sh and future scripts.
|
||||
#
|
||||
# Output variables (set by helpers, read by callers):
|
||||
# DOH_CREATED_PARTITION — partition path set by doh_create_partition()
|
||||
# DOH_PARTITION_ERROR_DETAIL — error detail set by doh_create_partition()
|
||||
# ==========================================================
|
||||
|
||||
if [[ -n "${__PROXMENUX_DISK_OPS_HELPERS__}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
__PROXMENUX_DISK_OPS_HELPERS__=1
|
||||
|
||||
# shellcheck disable=SC2034 # these are output variables read by callers (format-disk.sh, disk_host.sh)
|
||||
DOH_CREATED_PARTITION=""
|
||||
DOH_PARTITION_ERROR_DETAIL=""
|
||||
DOH_FORMAT_ERROR_DETAIL=""
|
||||
DOH_WIPE_ERROR_DETAIL=""
|
||||
|
||||
# Internal: print progress lines only when explicitly enabled by caller.
|
||||
# Enabled with: export DOH_SHOW_PROGRESS=1
|
||||
_doh_progress() {
|
||||
[[ "${DOH_SHOW_PROGRESS:-0}" == "1" ]] || return 0
|
||||
echo -e "${TAB}${YW}${HOLD}$*${CL}"
|
||||
}
|
||||
|
||||
# Internal: collect command stdout with timeout protection (best-effort).
|
||||
# Usage: _doh_collect_cmd <seconds> <cmd> [args...]
|
||||
_doh_collect_cmd() {
|
||||
local seconds="$1"
|
||||
shift
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout --kill-after=2 "${seconds}s" "$@" 2>/dev/null || true
|
||||
else
|
||||
"$@" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Internal: run a command with a timeout, suppressing all output including
|
||||
# the bash "Killed" job notification that leaks when --kill-after re-raises
|
||||
# SIGKILL. Plain SIGTERM is not enough for processes stuck in kernel D-state
|
||||
# (uninterruptible I/O wait on a busy ZFS/LVM disk), so --kill-after=2 is
|
||||
# needed. The notification is suppressed by temporarily redirecting the
|
||||
# current shell's stderr with exec before the call and restoring it after.
|
||||
# Usage: _doh_run_quick_cmd <seconds> <cmd> [args...]
|
||||
_doh_run_quick_cmd() {
|
||||
local seconds="$1"
|
||||
shift
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
local _saved_stderr
|
||||
exec {_saved_stderr}>&2 2>/dev/null
|
||||
timeout --kill-after=2 "${seconds}s" "$@" >/dev/null 2>&1
|
||||
local rc=$?
|
||||
exec 2>&"${_saved_stderr}" {_saved_stderr}>&-
|
||||
return $rc
|
||||
fi
|
||||
"$@" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Internal: unmount all ZFS datasets then export (or destroy) any ZFS pools
|
||||
# whose vdevs live on <disk>. Called at the very start of doh_wipe_disk so
|
||||
# ZFS fully releases the device before wipefs/sgdisk/partprobe touch it.
|
||||
# If the pool is still held after export, processes on it will be in D-state
|
||||
# and --kill-after in _doh_run_quick_cmd handles the force-kill.
|
||||
_doh_release_zfs_pools() {
|
||||
local disk="$1"
|
||||
command -v zpool >/dev/null 2>&1 || return 0
|
||||
|
||||
local pool_name dev resolved base parent
|
||||
while read -r pool_name; do
|
||||
[[ -z "$pool_name" ]] && continue
|
||||
local found=false
|
||||
while read -r dev; do
|
||||
[[ -z "$dev" ]] && continue
|
||||
if [[ "$dev" == /dev/* ]]; then
|
||||
resolved=$(readlink -f "$dev" 2>/dev/null)
|
||||
elif [[ -e "/dev/disk/by-id/$dev" ]]; then
|
||||
resolved=$(readlink -f "/dev/disk/by-id/$dev" 2>/dev/null)
|
||||
elif [[ -e "/dev/$dev" ]]; then
|
||||
resolved=$(readlink -f "/dev/$dev" 2>/dev/null)
|
||||
else
|
||||
continue
|
||||
fi
|
||||
[[ -z "$resolved" ]] && continue
|
||||
base=$(lsblk -no PKNAME "$resolved" 2>/dev/null)
|
||||
parent="${base:+/dev/$base}"
|
||||
[[ -z "$parent" ]] && parent="$resolved"
|
||||
if [[ "$parent" == "$disk" || "$resolved" == "$disk" ]]; then
|
||||
found=true; break
|
||||
fi
|
||||
done < <(_doh_collect_cmd 12 zpool list -v -H "$pool_name" | awk '{print $1}' | \
|
||||
grep -v '^-' | grep -v '^mirror' | grep -v '^raidz' | \
|
||||
grep -v "^${pool_name}$")
|
||||
if $found; then
|
||||
_doh_progress "- Releasing active ZFS pool: $pool_name"
|
||||
# Unmount all datasets (reverse order: deepest first)
|
||||
if command -v zfs >/dev/null 2>&1; then
|
||||
while read -r ds; do
|
||||
[[ -z "$ds" ]] && continue
|
||||
timeout 10s zfs unmount -f "$ds" >/dev/null 2>&1 || true
|
||||
done < <(_doh_collect_cmd 10 zfs list -H -o name -r "$pool_name" | sort -r)
|
||||
fi
|
||||
# Export the pool so the kernel releases the block device
|
||||
timeout 30s zpool export -f "$pool_name" >/dev/null 2>&1 || true
|
||||
# Wait for udev to finish processing the device release
|
||||
udevadm settle --timeout=5 >/dev/null 2>&1 || true
|
||||
sleep 1
|
||||
fi
|
||||
done < <(_doh_collect_cmd 8 zpool list -H -o name)
|
||||
}
|
||||
|
||||
# Internal: run a partitioning command with timeout, appending combined output to a file.
|
||||
# Usage: _doh_part_cmd <seconds> <outfile> <cmd> [args...]
|
||||
_doh_part_cmd() {
|
||||
local secs="$1" outfile="$2"
|
||||
shift 2
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout --kill-after=3 "${secs}s" "$@" >>"$outfile" 2>&1
|
||||
else
|
||||
"$@" >>"$outfile" 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
# doh_wipe_disk <disk>
|
||||
# Unmounts all partitions, deactivates swap, wipes all filesystem metadata
|
||||
# and partition tables (wipefs + sgdisk + dd first/last 16 MiB).
|
||||
# Never fails — all sub-commands run with "|| true".
|
||||
doh_wipe_disk() {
|
||||
local disk="$1"
|
||||
local node mountpoint total_sectors seek_sectors discard_max base
|
||||
|
||||
DOH_WIPE_ERROR_DETAIL=""
|
||||
_doh_progress "[1/8] Preparing disk $disk"
|
||||
|
||||
# Optional heavy release flow (disabled by default to avoid hangs in busy hosts).
|
||||
if [[ "${DOH_ENABLE_STACK_RELEASE:-0}" == "1" ]]; then
|
||||
# Release any ZFS pools using this disk so the kernel lets go of it
|
||||
_doh_release_zfs_pools "$disk"
|
||||
|
||||
# Deactivate any LVM VGs backed by this disk
|
||||
if command -v vgchange >/dev/null 2>&1; then
|
||||
local pv rp vg
|
||||
while read -r pv; do
|
||||
rp=$(readlink -f "$pv" 2>/dev/null)
|
||||
base=$(lsblk -no PKNAME "${rp:-$pv}" 2>/dev/null)
|
||||
if [[ "/dev/${base}" == "$disk" || "$rp" == "$disk" ]]; then
|
||||
vg=$(_doh_collect_cmd 8 pvs --noheadings -o vg_name "${rp:-$pv}" | xargs)
|
||||
[[ -n "$vg" ]] && _doh_run_quick_cmd 8 vgchange -an "$vg" || true
|
||||
fi
|
||||
done < <(_doh_collect_cmd 8 pvs --noheadings -o pv_name | xargs -r -n1)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Unmount all partitions
|
||||
_doh_progress "[2/8] Unmounting partitions"
|
||||
while read -r node mountpoint; do
|
||||
[[ -z "$node" || -z "$mountpoint" ]] && continue
|
||||
_doh_run_quick_cmd 8 umount -f "$node" || true
|
||||
done < <(lsblk -lnpo NAME,MOUNTPOINT "$disk" 2>/dev/null | awk 'NR>1 && $2!="" {print $1" "$2}')
|
||||
|
||||
# Deactivate swap
|
||||
_doh_progress "[3/8] Disabling swap signatures"
|
||||
while read -r node; do
|
||||
[[ -z "$node" ]] && continue
|
||||
_doh_run_quick_cmd 8 swapoff "$node" || true
|
||||
done < <(lsblk -lnpo NAME "$disk" 2>/dev/null | awk 'NR>1 {print $1}')
|
||||
|
||||
# Wipe filesystem signatures and RAID superblocks on every node
|
||||
_doh_progress "[4/8] Removing filesystem/RAID signatures"
|
||||
while read -r node; do
|
||||
[[ -z "$node" ]] && continue
|
||||
_doh_run_quick_cmd 10 wipefs -a -f "$node" || true
|
||||
if command -v mdadm >/dev/null 2>&1; then
|
||||
_doh_run_quick_cmd 8 mdadm --zero-superblock --force "$node" || true
|
||||
fi
|
||||
done < <(lsblk -lnpo NAME "$disk" 2>/dev/null)
|
||||
|
||||
# Zap partition table
|
||||
_doh_progress "[5/8] Resetting partition table"
|
||||
_doh_run_quick_cmd 12 sgdisk --zap-all "$disk" || true
|
||||
|
||||
# TRIM/discard if device supports it
|
||||
_doh_progress "[6/8] Attempting discard/TRIM when supported"
|
||||
discard_max=$(lsblk -dn -o DISC-MAX "$disk" 2>/dev/null | xargs)
|
||||
if [[ -n "$discard_max" && "$discard_max" != "0B" && "$discard_max" != "0" ]]; then
|
||||
_doh_run_quick_cmd 15 blkdiscard -f "$disk" || true
|
||||
fi
|
||||
|
||||
# Zero first 16 MiB (destroys partition table / filesystem headers)
|
||||
_doh_progress "[7/8] Zeroing first metadata region"
|
||||
_doh_run_quick_cmd 20 dd if=/dev/zero of="$disk" bs=1M count=16 conv=fsync status=none || true
|
||||
|
||||
# Zero last 16 MiB (destroys backup GPT header)
|
||||
_doh_progress "[8/8] Zeroing backup GPT region"
|
||||
total_sectors=$(blockdev --getsz "$disk" 2>/dev/null || echo 0)
|
||||
if [[ "$total_sectors" =~ ^[0-9]+$ ]] && (( total_sectors > 32768 )); then
|
||||
seek_sectors=$(( total_sectors - 32768 ))
|
||||
_doh_run_quick_cmd 20 dd if=/dev/zero of="$disk" bs=512 seek="$seek_sectors" count=32768 conv=fsync status=none || true
|
||||
fi
|
||||
|
||||
udevadm settle --timeout=10 >/dev/null 2>&1 || true
|
||||
_doh_run_quick_cmd 8 partprobe "$disk" || true
|
||||
sleep 1
|
||||
}
|
||||
|
||||
# doh_create_partition <disk>
|
||||
# Creates a single GPT partition spanning the whole disk.
|
||||
# Tries parted → sgdisk → sfdisk in order; stops at first success.
|
||||
#
|
||||
# On success: sets DOH_CREATED_PARTITION to the new partition path, returns 0.
|
||||
# On failure: sets DOH_PARTITION_ERROR_DETAIL with tool diagnostics, returns 1.
|
||||
doh_create_partition() {
|
||||
local disk="$1"
|
||||
local created=false tmp_out err_snippet
|
||||
|
||||
DOH_CREATED_PARTITION=""
|
||||
DOH_PARTITION_ERROR_DETAIL=""
|
||||
|
||||
_doh_run_quick_cmd 5 blockdev --setrw "$disk" || true
|
||||
|
||||
# --- attempt 1: parted ---
|
||||
if command -v parted >/dev/null 2>&1; then
|
||||
tmp_out=$(mktemp)
|
||||
if _doh_part_cmd 15 "$tmp_out" parted -s -f "$disk" mklabel gpt; then
|
||||
if _doh_part_cmd 20 "$tmp_out" parted -s -f "$disk" mkpart primary 1MiB 100%; then
|
||||
created=true
|
||||
else
|
||||
err_snippet=$(tr '\n' ' ' <"$tmp_out" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||
DOH_PARTITION_ERROR_DETAIL+="parted mkpart: ${err_snippet:-no details}"$'\n'
|
||||
fi
|
||||
else
|
||||
err_snippet=$(tr '\n' ' ' <"$tmp_out" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||
DOH_PARTITION_ERROR_DETAIL+="parted mklabel: ${err_snippet:-no details}"$'\n'
|
||||
fi
|
||||
rm -f "$tmp_out"
|
||||
else
|
||||
DOH_PARTITION_ERROR_DETAIL+="parted command not found"$'\n'
|
||||
fi
|
||||
|
||||
# --- attempt 2: sgdisk ---
|
||||
if [[ "$created" != "true" ]] && command -v sgdisk >/dev/null 2>&1; then
|
||||
tmp_out=$(mktemp)
|
||||
_doh_run_quick_cmd 10 sgdisk --zap-all "$disk" || true
|
||||
# sgdisk does not accept "1MiB" notation — use sector 2048 (= 1 MiB at 512 B/sector)
|
||||
if _doh_part_cmd 20 "$tmp_out" sgdisk -o -n 1:2048:0 -t 1:8300 "$disk"; then
|
||||
created=true
|
||||
else
|
||||
err_snippet=$(tr '\n' ' ' <"$tmp_out" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||
DOH_PARTITION_ERROR_DETAIL+="sgdisk create: ${err_snippet:-no details}"$'\n'
|
||||
fi
|
||||
rm -f "$tmp_out"
|
||||
elif [[ "$created" != "true" ]]; then
|
||||
DOH_PARTITION_ERROR_DETAIL+="sgdisk command not found"$'\n'
|
||||
fi
|
||||
|
||||
# --- attempt 3: sfdisk ---
|
||||
if [[ "$created" != "true" ]] && command -v sfdisk >/dev/null 2>&1; then
|
||||
tmp_out=$(mktemp)
|
||||
local sfdisk_ok=1
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
printf 'label: gpt\n,;\n' | timeout --kill-after=3 20s sfdisk --wipe always "$disk" >>"$tmp_out" 2>&1
|
||||
sfdisk_ok=$?
|
||||
else
|
||||
printf 'label: gpt\n,;\n' | sfdisk --wipe always "$disk" >>"$tmp_out" 2>&1
|
||||
sfdisk_ok=$?
|
||||
fi
|
||||
if [[ $sfdisk_ok -eq 0 ]]; then
|
||||
created=true
|
||||
else
|
||||
err_snippet=$(tr '\n' ' ' <"$tmp_out" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||
DOH_PARTITION_ERROR_DETAIL+="sfdisk create: ${err_snippet:-no details}"$'\n'
|
||||
fi
|
||||
rm -f "$tmp_out"
|
||||
elif [[ "$created" != "true" ]]; then
|
||||
DOH_PARTITION_ERROR_DETAIL+="sfdisk command not found"$'\n'
|
||||
fi
|
||||
|
||||
[[ "$created" == "true" ]] || return 1
|
||||
|
||||
# Wait for the kernel to expose the new partition node
|
||||
udevadm settle --timeout=10 >/dev/null 2>&1 || true
|
||||
_doh_run_quick_cmd 8 partprobe "$disk" || true
|
||||
|
||||
local part
|
||||
for _ in {1..15}; do
|
||||
sleep 0.3
|
||||
part=$(lsblk -lnpo NAME "$disk" 2>/dev/null | awk 'NR==2{print; exit}')
|
||||
if [[ -n "$part" && -b "$part" ]]; then
|
||||
DOH_CREATED_PARTITION="$part"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Fallback: derive partition name from disk path (handles NVMe p-suffix)
|
||||
local fallback
|
||||
if [[ "$disk" =~ [0-9]$ ]]; then
|
||||
fallback="${disk}p1"
|
||||
else
|
||||
fallback="${disk}1"
|
||||
fi
|
||||
if [[ -b "$fallback" ]]; then
|
||||
DOH_CREATED_PARTITION="$fallback"
|
||||
return 0
|
||||
fi
|
||||
|
||||
DOH_PARTITION_ERROR_DETAIL+="partition node not detected after table refresh"$'\n'
|
||||
return 1
|
||||
}
|
||||
|
||||
# doh_format_partition <partition> <filesystem> [label] [zfs_pool_name] [zfs_mountpoint]
|
||||
#
|
||||
# Formats <partition> with <filesystem>.
|
||||
# label : optional FS label for ext4/xfs/btrfs (ignored for ZFS)
|
||||
# zfs_pool_name : required when filesystem=zfs; defaults to label if empty
|
||||
# zfs_mountpoint : ZFS pool mountpoint (default: "none" — no automatic mount)
|
||||
#
|
||||
# On failure: sets DOH_FORMAT_ERROR_DETAIL with tool diagnostics.
|
||||
# Returns 0 on success, 1 on failure.
|
||||
doh_format_partition() {
|
||||
local partition="$1"
|
||||
local filesystem="$2"
|
||||
local label="${3:-}"
|
||||
local zfs_pool="${4:-}"
|
||||
local zfs_mountpoint="${5:-none}"
|
||||
local tmp_out rc=1
|
||||
|
||||
DOH_FORMAT_ERROR_DETAIL=""
|
||||
tmp_out=$(mktemp)
|
||||
|
||||
case "$filesystem" in
|
||||
ext4)
|
||||
if [[ -n "$label" ]]; then
|
||||
mkfs.ext4 -F -L "$label" "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||
else
|
||||
mkfs.ext4 -F "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||
fi
|
||||
;;
|
||||
xfs)
|
||||
if [[ -n "$label" ]]; then
|
||||
mkfs.xfs -f -L "$label" "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||
else
|
||||
mkfs.xfs -f "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||
fi
|
||||
;;
|
||||
exfat)
|
||||
mkfs.exfat "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||
;;
|
||||
btrfs)
|
||||
if [[ -n "$label" ]]; then
|
||||
mkfs.btrfs -f -L "$label" "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||
else
|
||||
mkfs.btrfs -f "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||
fi
|
||||
;;
|
||||
zfs)
|
||||
[[ -z "$zfs_pool" ]] && zfs_pool="${label:-pool}"
|
||||
zpool labelclear -f "$partition" >/dev/null 2>&1 || true
|
||||
zpool create -f -o ashift=12 \
|
||||
-O compression=lz4 -O atime=off -O xattr=sa -O acltype=posixacl \
|
||||
-m "$zfs_mountpoint" "$zfs_pool" "$partition" >"$tmp_out" 2>&1
|
||||
rc=$?
|
||||
;;
|
||||
*)
|
||||
echo "Unknown filesystem: $filesystem" >"$tmp_out"
|
||||
rc=1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
DOH_FORMAT_ERROR_DETAIL=$(tr '\n' ' ' <"$tmp_out" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||
fi
|
||||
rm -f "$tmp_out"
|
||||
return $rc
|
||||
}
|
||||
@@ -222,9 +222,9 @@ attach_proxmenux_gpu_guard_to_vm() {
|
||||
fi
|
||||
|
||||
if qm set "$vmid" --hookscript "$PROXMENUX_GPU_HOOK_STORAGE_REF" >/dev/null 2>&1; then
|
||||
_gpu_guard_msg_ok "GPU guard hook attached to VM ${vmid}"
|
||||
_gpu_guard_msg_ok "PCIe passthrough guard attached to VM ${vmid}"
|
||||
else
|
||||
_gpu_guard_msg_warn "Could not attach GPU guard hook to VM ${vmid}. Ensure 'local' storage supports snippets."
|
||||
_gpu_guard_msg_warn "Could not attach PCIe passthrough guard to VM ${vmid}. Ensure 'local' storage supports snippets."
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -239,9 +239,9 @@ attach_proxmenux_gpu_guard_to_lxc() {
|
||||
fi
|
||||
|
||||
if pct set "$ctid" -hookscript "$PROXMENUX_GPU_HOOK_STORAGE_REF" >/dev/null 2>&1; then
|
||||
_gpu_guard_msg_ok "GPU guard hook attached to LXC ${ctid}"
|
||||
_gpu_guard_msg_ok "PCIe passthrough guard attached to LXC ${ctid}"
|
||||
else
|
||||
_gpu_guard_msg_warn "Could not attach GPU guard hook to LXC ${ctid}. Ensure 'local' storage supports snippets."
|
||||
_gpu_guard_msg_warn "Could not attach PCIe passthrough guard to LXC ${ctid}. Ensure 'local' storage supports snippets."
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,66 @@ function _array_contains() {
|
||||
return 1
|
||||
}
|
||||
|
||||
function _vm_boot_order_add_unique() {
|
||||
local arr_name="$1"
|
||||
shift
|
||||
local -n arr_ref="$arr_name"
|
||||
local entry
|
||||
for entry in "$@"; do
|
||||
[[ -z "$entry" ]] && continue
|
||||
_array_contains "$entry" "${arr_ref[@]}" || arr_ref+=("$entry")
|
||||
done
|
||||
}
|
||||
|
||||
function _vm_boot_order_join() {
|
||||
local -a unique_entries=()
|
||||
local entry
|
||||
for entry in "$@"; do
|
||||
[[ -z "$entry" ]] && continue
|
||||
_array_contains "$entry" "${unique_entries[@]}" || unique_entries+=("$entry")
|
||||
done
|
||||
[[ ${#unique_entries[@]} -gt 0 ]] || return 0
|
||||
local joined
|
||||
joined=$(IFS=';'; echo "${unique_entries[*]}")
|
||||
echo "$joined"
|
||||
}
|
||||
|
||||
function _vm_boot_order_hostpci_entries_for_pcis() {
|
||||
local vmid="$1"
|
||||
shift
|
||||
|
||||
local cfg
|
||||
cfg=$(qm config "$vmid" 2>/dev/null || true)
|
||||
[[ -n "$cfg" ]] || return 0
|
||||
|
||||
local -a hostpci_entries=()
|
||||
local pci bdf bdf_re slot_base slot_re line entry
|
||||
|
||||
for pci in "$@"; do
|
||||
[[ -n "$pci" ]] || continue
|
||||
bdf="${pci#0000:}"
|
||||
bdf_re="${bdf//./\\.}"
|
||||
|
||||
line=$(grep -E "^hostpci[0-9]+:.*(0000:)?${bdf_re}([,[:space:]]|$)" <<< "$cfg" | head -n1)
|
||||
if [[ -z "$line" ]]; then
|
||||
slot_base="${bdf%.*}"
|
||||
slot_re="${slot_base//./\\.}"
|
||||
line=$(grep -E "^hostpci[0-9]+:.*(0000:)?${slot_re}(\\.[0-7])?([,[:space:]]|$)" <<< "$cfg" | head -n1)
|
||||
fi
|
||||
|
||||
[[ -n "$line" ]] || continue
|
||||
entry="${line%%:*}"
|
||||
_array_contains "$entry" "${hostpci_entries[@]}" || hostpci_entries+=("$entry")
|
||||
done
|
||||
|
||||
printf '%s\n' "${hostpci_entries[@]}"
|
||||
}
|
||||
|
||||
function _vmids_scope_key() {
|
||||
[[ "$#" -eq 0 ]] && { echo ""; return 0; }
|
||||
printf '%s\n' "$@" | awk 'NF' | sort -u | paste -sd',' -
|
||||
}
|
||||
|
||||
function _refresh_host_storage_cache() {
|
||||
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
|
||||
SWAP_DISKS=$(swapon --noheadings --raw --show=NAME 2>/dev/null)
|
||||
@@ -23,17 +83,24 @@ function _refresh_host_storage_cache() {
|
||||
|
||||
ZFS_DISKS=""
|
||||
local zfs_raw entry path base_disk
|
||||
zfs_raw=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
|
||||
zfs_raw=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror' | grep -v '^raidz')
|
||||
for entry in $zfs_raw; do
|
||||
path=""
|
||||
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
|
||||
[[ -e "/dev/disk/by-id/$entry" ]] && path=$(readlink -f "/dev/disk/by-id/$entry")
|
||||
elif [[ "$entry" == /dev/* ]]; then
|
||||
path="$entry"
|
||||
if [[ "$entry" == /dev/* ]]; then
|
||||
path=$(readlink -f "$entry" 2>/dev/null)
|
||||
elif [[ -e "/dev/disk/by-id/$entry" ]]; then
|
||||
path=$(readlink -f "/dev/disk/by-id/$entry" 2>/dev/null)
|
||||
elif [[ -e "/dev/$entry" ]]; then
|
||||
path=$(readlink -f "/dev/$entry" 2>/dev/null)
|
||||
fi
|
||||
if [[ -n "$path" ]]; then
|
||||
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
|
||||
[[ -n "$base_disk" ]] && ZFS_DISKS+="/dev/$base_disk"$'\n'
|
||||
if [[ -n "$base_disk" ]]; then
|
||||
ZFS_DISKS+="/dev/$base_disk"$'\n'
|
||||
else
|
||||
# Whole-disk vdev — path is already the resolved disk itself
|
||||
ZFS_DISKS+="$path"$'\n'
|
||||
fi
|
||||
fi
|
||||
done
|
||||
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
|
||||
@@ -77,7 +144,7 @@ function _disk_is_host_system_used() {
|
||||
DISK_USAGE_REASON="$(translate "Disk is part of host LVM")"
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "$ZFS_DISKS" && "$ZFS_DISKS" == *"$disk"* ]]; then
|
||||
if [[ -n "$ZFS_DISKS" ]] && grep -qFx "$disk" <<< "$ZFS_DISKS"; then
|
||||
DISK_USAGE_REASON="$(translate "Disk is part of a host ZFS pool")"
|
||||
return 0
|
||||
fi
|
||||
@@ -86,23 +153,181 @@ function _disk_is_host_system_used() {
|
||||
|
||||
function _disk_used_in_guest_configs() {
|
||||
local disk="$1"
|
||||
local real_path
|
||||
local real_path escaped
|
||||
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||
|
||||
if [[ -n "$real_path" ]] && grep -Fq "$real_path" <<< "$CONFIG_DATA"; then
|
||||
return 0
|
||||
# Use boundary matching: path must be followed by comma, whitespace, or EOL
|
||||
# This prevents /dev/sdb from falsely matching /dev/sdb1 or /dev/sdb2
|
||||
if [[ -n "$real_path" ]]; then
|
||||
escaped="${real_path//./\\.}"
|
||||
if grep -qE "${escaped}(,|[[:space:]]|$)" <<< "$CONFIG_DATA"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local symlink
|
||||
local symlink symlink_escaped
|
||||
for symlink in /dev/disk/by-id/*; do
|
||||
[[ -e "$symlink" ]] || continue
|
||||
if [[ "$(readlink -f "$symlink")" == "$real_path" ]] && grep -Fq "$symlink" <<< "$CONFIG_DATA"; then
|
||||
[[ "$(readlink -f "$symlink")" == "$real_path" ]] || continue
|
||||
symlink_escaped="${symlink//./\\.}"
|
||||
if grep -qE "${symlink_escaped}(,|[[:space:]]|$)" <<< "$CONFIG_DATA"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Returns 0 if the disk is referenced in a RUNNING VM or CT config.
|
||||
# Mirrors _disk_used_in_guest_configs but checks guest status per-file.
|
||||
function _disk_used_in_running_guest() {
|
||||
local disk="$1"
|
||||
local real_path
|
||||
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||
|
||||
local -a aliases=()
|
||||
[[ -n "$disk" ]] && aliases+=("$disk")
|
||||
[[ -n "$real_path" && "$real_path" != "$disk" ]] && aliases+=("$real_path")
|
||||
local symlink
|
||||
for symlink in /dev/disk/by-id/*; do
|
||||
[[ -e "$symlink" ]] || continue
|
||||
[[ "$(readlink -f "$symlink" 2>/dev/null)" == "$real_path" ]] && aliases+=("$symlink")
|
||||
done
|
||||
|
||||
local conf vmid alias escaped
|
||||
for conf in /etc/pve/qemu-server/*.conf; do
|
||||
[[ -f "$conf" ]] || continue
|
||||
vmid=$(basename "$conf" .conf)
|
||||
for alias in "${aliases[@]}"; do
|
||||
escaped="${alias//./\\.}"
|
||||
if grep -qE "${escaped}(,|[[:space:]]|$)" "$conf" 2>/dev/null; then
|
||||
if qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
local ctid
|
||||
for conf in /etc/pve/lxc/*.conf; do
|
||||
[[ -f "$conf" ]] || continue
|
||||
ctid=$(basename "$conf" .conf)
|
||||
for alias in "${aliases[@]}"; do
|
||||
escaped="${alias//./\\.}"
|
||||
if grep -qE "${escaped}(,|[[:space:]]|$)" "$conf" 2>/dev/null; then
|
||||
if pct status "$ctid" 2>/dev/null | grep -q "status: running"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Prints "VM:VMID" or "CT:CTID" for each stopped guest that references the disk.
|
||||
function _disk_guest_ids() {
|
||||
local disk="$1"
|
||||
local real_path
|
||||
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||
|
||||
local -a aliases=()
|
||||
[[ -n "$disk" ]] && aliases+=("$disk")
|
||||
[[ -n "$real_path" && "$real_path" != "$disk" ]] && aliases+=("$real_path")
|
||||
local symlink
|
||||
for symlink in /dev/disk/by-id/*; do
|
||||
[[ -e "$symlink" ]] || continue
|
||||
[[ "$(readlink -f "$symlink" 2>/dev/null)" == "$real_path" ]] && aliases+=("$symlink")
|
||||
done
|
||||
|
||||
local conf vmid alias escaped
|
||||
for conf in /etc/pve/qemu-server/*.conf; do
|
||||
[[ -f "$conf" ]] || continue
|
||||
vmid=$(basename "$conf" .conf)
|
||||
for alias in "${aliases[@]}"; do
|
||||
escaped="${alias//./\\.}"
|
||||
if grep -qE "${escaped}(,|[[:space:]]|$)" "$conf" 2>/dev/null; then
|
||||
echo "VM:$vmid"
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
local ctid
|
||||
for conf in /etc/pve/lxc/*.conf; do
|
||||
[[ -f "$conf" ]] || continue
|
||||
ctid=$(basename "$conf" .conf)
|
||||
for alias in "${aliases[@]}"; do
|
||||
escaped="${alias//./\\.}"
|
||||
if grep -qE "${escaped}(,|[[:space:]]|$)" "$conf" 2>/dev/null; then
|
||||
echo "CT:$ctid"
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# Print the slot names (e.g. sata0, scsi1) in a VM config that reference the disk.
|
||||
function _find_disk_slots_in_vm() {
|
||||
local vmid="$1"
|
||||
local disk="$2"
|
||||
local real_path conf
|
||||
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||
conf="/etc/pve/qemu-server/${vmid}.conf"
|
||||
[[ -f "$conf" ]] || return
|
||||
|
||||
local -a aliases=("$disk")
|
||||
[[ -n "$real_path" && "$real_path" != "$disk" ]] && aliases+=("$real_path")
|
||||
local symlink
|
||||
for symlink in /dev/disk/by-id/*; do
|
||||
[[ -e "$symlink" ]] || continue
|
||||
[[ "$(readlink -f "$symlink" 2>/dev/null)" == "$real_path" ]] && aliases+=("$symlink")
|
||||
done
|
||||
|
||||
local key rest alias escaped
|
||||
while IFS=: read -r key rest; do
|
||||
key=$(echo "$key" | xargs)
|
||||
[[ "$key" =~ ^(scsi|sata|ide|virtio)[0-9]+$ ]] || continue
|
||||
for alias in "${aliases[@]}"; do
|
||||
escaped="${alias//./\\.}"
|
||||
if echo "$rest" | grep -qE "${escaped}(,|[[:space:]]|$)"; then
|
||||
echo "$key"
|
||||
break
|
||||
fi
|
||||
done
|
||||
done < "$conf"
|
||||
}
|
||||
|
||||
# Print the mp names (e.g. mp0, mp1) in a CT config that reference the disk.
|
||||
function _find_disk_slots_in_ct() {
|
||||
local ctid="$1"
|
||||
local disk="$2"
|
||||
local real_path conf
|
||||
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||
conf="/etc/pve/lxc/${ctid}.conf"
|
||||
[[ -f "$conf" ]] || return
|
||||
|
||||
local -a aliases=("$disk")
|
||||
[[ -n "$real_path" && "$real_path" != "$disk" ]] && aliases+=("$real_path")
|
||||
local symlink
|
||||
for symlink in /dev/disk/by-id/*; do
|
||||
[[ -e "$symlink" ]] || continue
|
||||
[[ "$(readlink -f "$symlink" 2>/dev/null)" == "$real_path" ]] && aliases+=("$symlink")
|
||||
done
|
||||
|
||||
local key rest alias escaped
|
||||
while IFS=: read -r key rest; do
|
||||
key=$(echo "$key" | xargs)
|
||||
[[ "$key" =~ ^mp[0-9]+$ ]] || continue
|
||||
for alias in "${aliases[@]}"; do
|
||||
escaped="${alias//./\\.}"
|
||||
if echo "$rest" | grep -qE "${escaped}(,|[[:space:]]|$)"; then
|
||||
echo "$key"
|
||||
break
|
||||
fi
|
||||
done
|
||||
done < "$conf"
|
||||
}
|
||||
|
||||
function _controller_block_devices() {
|
||||
local pci_full="$1"
|
||||
local pci_root="/sys/bus/pci/devices/$pci_full"
|
||||
@@ -137,6 +362,14 @@ function _vm_is_q35() {
|
||||
[[ "$machine_line" == *q35* ]]
|
||||
}
|
||||
|
||||
function _vm_storage_register_vfio_iommu_tool() {
|
||||
local tools_json="${BASE_DIR:-/usr/local/share/proxmenux}/installed_tools.json"
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
[[ -f "$tools_json" ]] || echo "{}" > "$tools_json"
|
||||
jq '.vfio_iommu=true' "$tools_json" > "$tools_json.tmp" \
|
||||
&& mv "$tools_json.tmp" "$tools_json" || true
|
||||
}
|
||||
|
||||
function _vm_storage_enable_iommu_cmdline() {
|
||||
local cpu_vendor iommu_param
|
||||
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
||||
@@ -175,18 +408,28 @@ function _vm_storage_ensure_iommu_or_offer() {
|
||||
local reboot_policy="${VM_STORAGE_IOMMU_REBOOT_POLICY:-ask_now}"
|
||||
|
||||
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||
_vm_storage_register_vfio_iommu_tool
|
||||
return 0
|
||||
fi
|
||||
|
||||
if grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null && \
|
||||
[[ -d /sys/kernel/iommu_groups ]] && \
|
||||
[[ -n "$(ls /sys/kernel/iommu_groups/ 2>/dev/null)" ]]; then
|
||||
_vm_storage_register_vfio_iommu_tool
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Wizard flow: if IOMMU was already configured in this run and reboot is pending,
|
||||
# allow the user to continue planning storage selections without re-prompting.
|
||||
if [[ "$reboot_policy" == "defer" && "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||
# Dedup: if IOMMU was already configured/announced in this wizard run, skip prompt
|
||||
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Detect if another script already wrote IOMMU params (e.g. GPU script ran first)
|
||||
if grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
|
||||
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
|
||||
_vm_storage_register_vfio_iommu_tool
|
||||
VM_STORAGE_IOMMU_PENDING_REBOOT=1
|
||||
export VM_STORAGE_IOMMU_PENDING_REBOOT
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -206,6 +449,8 @@ function _vm_storage_ensure_iommu_or_offer() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_vm_storage_register_vfio_iommu_tool
|
||||
|
||||
if [[ "$reboot_policy" == "defer" ]]; then
|
||||
VM_STORAGE_IOMMU_PENDING_REBOOT=1
|
||||
export VM_STORAGE_IOMMU_PENDING_REBOOT
|
||||
@@ -230,47 +475,71 @@ function _vm_storage_confirm_controller_passthrough_risk() {
|
||||
local vmid="${1:-}"
|
||||
local vm_name="${2:-}"
|
||||
local title="${3:-Controller + NVMe}"
|
||||
local ui_mode="${4:-auto}" # wizard | standalone | auto
|
||||
local vm_label=""
|
||||
if [[ -n "$vmid" ]]; then
|
||||
vm_label="$vmid"
|
||||
[[ -n "$vm_name" ]] && vm_label="${vm_label} (${vm_name})"
|
||||
fi
|
||||
|
||||
local msg
|
||||
msg="$(translate "Important compatibility notice")\n\n"
|
||||
msg+="$(translate "Not all motherboards support physical Controller/NVMe passthrough to VMs reliably, especially systems with old platforms or limited BIOS/UEFI firmware.")\n\n"
|
||||
msg+="$(translate "On some systems, the VM may fail to start or the host may freeze when the VM boots.")\n\n"
|
||||
|
||||
local reinforce_limited_firmware="no"
|
||||
local bios_date bios_year current_year cpu_model
|
||||
local bios_date bios_year current_year bios_age cpu_model risk_detail=""
|
||||
bios_date=$(cat /sys/class/dmi/id/bios_date 2>/dev/null)
|
||||
bios_year=$(echo "$bios_date" | grep -oE '[0-9]{4}' | tail -n1)
|
||||
current_year=$(date +%Y 2>/dev/null)
|
||||
if [[ -n "$bios_year" && -n "$current_year" ]]; then
|
||||
if (( current_year - bios_year >= 7 )); then
|
||||
bios_age=$(( current_year - bios_year ))
|
||||
if (( bios_age >= 7 )); then
|
||||
reinforce_limited_firmware="yes"
|
||||
risk_detail="$(translate "BIOS from") ${bios_year} (${bios_age} $(translate "years old")) — $(translate "older firmware may increase passthrough instability")"
|
||||
fi
|
||||
fi
|
||||
cpu_model=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | xargs)
|
||||
if echo "$cpu_model" | grep -qiE 'J4[0-9]{3}|J3[0-9]{3}|N4[0-9]{3}|N3[0-9]{3}|Apollo Lake'; then
|
||||
reinforce_limited_firmware="yes"
|
||||
[[ -z "$risk_detail" ]] && risk_detail="$(translate "Low-power CPU platform"): ${cpu_model}"
|
||||
fi
|
||||
|
||||
if [[ "$reinforce_limited_firmware" == "yes" ]]; then
|
||||
msg+="$(translate "Detected risk factor: this host may use an older or limited firmware platform, which increases passthrough instability risk.")\n\n"
|
||||
if [[ "$ui_mode" == "auto" ]]; then
|
||||
if [[ "${PROXMENUX_UI_MODE:-}" == "wizard" || "${WIZARD_CALL:-false}" == "true" ]]; then
|
||||
ui_mode="wizard"
|
||||
else
|
||||
ui_mode="standalone"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$vm_label" ]]; then
|
||||
msg+="$(translate "Target VM"): ${vm_label}\n\n"
|
||||
fi
|
||||
msg+="$(translate "If this happens after assignment"):\n"
|
||||
msg+=" - $(translate "Power cycle the host if it is frozen.")\n"
|
||||
msg+=" - $(translate "Remove the hostpci controller/NVMe entries from the VM config file.")\n"
|
||||
msg+=" /etc/pve/qemu-server/${vmid:-<VMID>}.conf\n"
|
||||
msg+=" - $(translate "Start the VM again without that passthrough device.")\n\n"
|
||||
msg+="$(translate "Do you want to continue with this assignment?")"
|
||||
local height=20
|
||||
[[ "$reinforce_limited_firmware" == "yes" ]] && height=23
|
||||
|
||||
whiptail --title "$title" --yesno "$msg" 21 96
|
||||
if [[ "$ui_mode" == "wizard" ]]; then
|
||||
# whiptail: plain text (no color codes)
|
||||
local msg
|
||||
[[ -n "$vm_label" ]] && msg+="$(translate "Target VM"): ${vm_label}\n\n"
|
||||
msg+="⚠ $(translate "Controller/NVMe passthrough — compatibility notice")\n\n"
|
||||
msg+="$(translate "Not all platforms support Controller/NVMe passthrough reliably.")\n"
|
||||
msg+="$(translate "On some systems, when starting the VM the host may slow down for several minutes until it stabilizes, or freeze completely.")\n"
|
||||
if [[ "$reinforce_limited_firmware" == "yes" && -n "$risk_detail" ]]; then
|
||||
msg+="\n$(translate "Detected risk factor"): ${risk_detail}\n"
|
||||
fi
|
||||
msg+="\n$(translate "If the host freezes, remove hostpci entries from") /etc/pve/qemu-server/${vmid:-<VMID>}.conf\n"
|
||||
msg+="\n$(translate "Do you want to continue?")"
|
||||
whiptail --title "$title" --yesno "$msg" $height 96
|
||||
else
|
||||
# dialog: colored format matching add_controller_nvme_vm.sh
|
||||
local msg
|
||||
[[ -n "$vm_label" ]] && msg+="\n\Zb$(translate "Target VM"): ${vm_label}\Zn\n"
|
||||
msg+="\n\Zb\Z4⚠ $(translate "Controller/NVMe passthrough — compatibility notice")\Zn\n\n"
|
||||
msg+="$(translate "Not all platforms support Controller/NVMe passthrough reliably.")\n"
|
||||
msg+="$(translate "On some systems, when starting the VM the host may slow down for several minutes until it stabilizes, or freeze completely.")\n"
|
||||
if [[ "$reinforce_limited_firmware" == "yes" && -n "$risk_detail" ]]; then
|
||||
msg+="\n\Z1$(translate "Detected risk factor"): ${risk_detail}\Zn\n"
|
||||
fi
|
||||
msg+="\n$(translate "If the host freezes, remove hostpci entries from") /etc/pve/qemu-server/${vmid:-<VMID>}.conf\n"
|
||||
msg+="\n\Zb$(translate "Do you want to continue?")\Zn"
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$title" \
|
||||
--yesno "$msg" $height 96
|
||||
fi
|
||||
}
|
||||
|
||||
function _shorten_text() {
|
||||
@@ -284,6 +553,30 @@ function _shorten_text() {
|
||||
fi
|
||||
}
|
||||
|
||||
function _pci_storage_display_name() {
|
||||
local pci_full="$1"
|
||||
local raw_line name_part
|
||||
|
||||
raw_line=$(lspci -nn -s "${pci_full#0000:}" 2>/dev/null | sed 's/^[^ ]* //')
|
||||
if [[ -z "$raw_line" ]]; then
|
||||
translate "Unknown storage controller"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Prefer the right side after class prefix (e.g. "...: Vendor Model ...").
|
||||
name_part="${raw_line#*: }"
|
||||
[[ "$name_part" == "$raw_line" ]] && name_part="$raw_line"
|
||||
|
||||
# Remove noisy suffixes while keeping the meaningful model name.
|
||||
name_part="${name_part%% (rev *}"
|
||||
name_part=$(echo "$name_part" | sed -E 's/\[[0-9a-fA-F]{4}:[0-9a-fA-F]{4}\]//g')
|
||||
name_part=$(echo "$name_part" | sed -E 's/ Technology Inc\.?//g; s/ Corporation//g; s/ Co\., Ltd\.?//g')
|
||||
name_part=$(echo "$name_part" | sed -E 's/[[:space:]]+/ /g; s/^ +| +$//g')
|
||||
|
||||
[[ -z "$name_part" ]] && name_part="$raw_line"
|
||||
echo "$name_part"
|
||||
}
|
||||
|
||||
function _pci_slot_base() {
|
||||
local pci_full="$1"
|
||||
local slot
|
||||
|
||||
@@ -28,6 +28,7 @@ LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
TOOLS_JSON="$BASE_DIR/installed_tools.json"
|
||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||
@@ -190,12 +191,80 @@ _vm_switch_action_label() {
|
||||
esac
|
||||
}
|
||||
|
||||
_gpu_register_vfio_iommu_tool() {
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
[[ -f "$TOOLS_JSON" ]] || echo "{}" > "$TOOLS_JSON"
|
||||
jq '.vfio_iommu=true' "$TOOLS_JSON" > "$TOOLS_JSON.tmp" \
|
||||
&& mv "$TOOLS_JSON.tmp" "$TOOLS_JSON" || true
|
||||
}
|
||||
|
||||
_set_wizard_result() {
|
||||
local result="$1"
|
||||
[[ -z "${GPU_WIZARD_RESULT_FILE:-}" ]] && return 0
|
||||
printf '%s\n' "$result" >"$GPU_WIZARD_RESULT_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
# UI wrapper helpers — dialog in standalone, whiptail in wizard
|
||||
# ==========================================================
|
||||
# Strips dialog color sequences (\Zb, \Z1, \Zn, etc.) from a string
|
||||
_strip_colors() {
|
||||
printf '%s' "$1" | sed 's/\\Z[0-9a-zA-Z]//g'
|
||||
}
|
||||
|
||||
# Msgbox: dialog in standalone mode, whiptail in wizard mode
|
||||
_pmx_msgbox() {
|
||||
local title="$1" msg="$2" h="${3:-10}" w="${4:-72}"
|
||||
if [[ "$WIZARD_CALL" == "true" ]]; then
|
||||
whiptail --backtitle "ProxMenux" --title "$title" \
|
||||
--msgbox "$(_strip_colors "$msg")" "$h" "$w"
|
||||
else
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$title" --msgbox "$msg" "$h" "$w"
|
||||
fi
|
||||
}
|
||||
|
||||
# Yesno: dialog in standalone mode, whiptail in wizard mode
|
||||
# Returns 0 for yes, 1 for no (same as dialog/whiptail)
|
||||
_pmx_yesno() {
|
||||
local title="$1" msg="$2" h="${3:-10}" w="${4:-72}"
|
||||
if [[ "$WIZARD_CALL" == "true" ]]; then
|
||||
whiptail --backtitle "ProxMenux" --title "$title" \
|
||||
--yesno "$(_strip_colors "$msg")" "$h" "$w"
|
||||
else
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$title" --yesno "$msg" "$h" "$w"
|
||||
fi
|
||||
return $?
|
||||
}
|
||||
|
||||
# Menu: dialog in standalone mode, whiptail in wizard mode
|
||||
# Accepts optional --default-item VALUE before title
|
||||
# Usage: _pmx_menu [--default-item VAL] title msg h w list_h item desc ...
|
||||
_pmx_menu() {
|
||||
local -a extra_opts=()
|
||||
while [[ "${1:-}" == --* ]]; do
|
||||
case "$1" in
|
||||
--default-item) extra_opts+=("--default-item" "$2"); shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
local title="$1" msg="$2" h="$3" w="$4" lh="$5"
|
||||
shift 5
|
||||
if [[ "$WIZARD_CALL" == "true" ]]; then
|
||||
whiptail --backtitle "ProxMenux" "${extra_opts[@]}" \
|
||||
--title "$title" \
|
||||
--menu "$(_strip_colors "$msg")" "$h" "$w" "$lh" \
|
||||
"$@" 3>&1 1>&2 2>&3
|
||||
else
|
||||
dialog --backtitle "ProxMenux" --colors "${extra_opts[@]}" \
|
||||
--title "$title" \
|
||||
--menu "$msg" "$h" "$w" "$lh" \
|
||||
"$@" 2>&1 >/dev/tty
|
||||
fi
|
||||
return $?
|
||||
}
|
||||
|
||||
_file_has_exact_line() {
|
||||
local line="$1"
|
||||
local file="$2"
|
||||
@@ -398,18 +467,16 @@ ensure_selected_gpu_not_already_in_target_vm() {
|
||||
TARGET_VM_ALREADY_HAS_GPU=true
|
||||
local popup_title
|
||||
popup_title=$(_get_vm_run_title)
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "${popup_title}" \
|
||||
--msgbox "\n$(translate 'The selected GPU is already assigned to this VM, but the host is not currently using vfio-pci for this device.')\n\n$(translate 'Current driver'): ${current_driver}\n\n$(translate 'The script will continue to restore VM passthrough mode on the host and reuse existing hostpci entries.')" \
|
||||
_pmx_msgbox "${popup_title}" \
|
||||
"\n$(translate 'The selected GPU is already assigned to this VM, but the host is not currently using vfio-pci for this device.')\n\n$(translate 'Current driver'): ${current_driver}\n\n$(translate 'The script will continue to restore VM passthrough mode on the host and reuse existing hostpci entries.')" \
|
||||
13 78
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Single GPU system: nothing else to choose
|
||||
if [[ $GPU_COUNT -le 1 ]]; then
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'GPU Already Added')" \
|
||||
--msgbox "\n$(translate 'The selected GPU is already assigned to this VM.')\n\n$(translate 'No changes are required.')" \
|
||||
_pmx_msgbox "$(translate 'GPU Already Added')" \
|
||||
"\n$(translate 'The selected GPU is already assigned to this VM.')\n\n$(translate 'No changes are required.')" \
|
||||
9 66
|
||||
exit 0
|
||||
fi
|
||||
@@ -428,22 +495,18 @@ ensure_selected_gpu_not_already_in_target_vm() {
|
||||
done
|
||||
|
||||
if [[ $available -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'All GPUs Already Assigned')" \
|
||||
--msgbox "\n$(translate 'All detected GPUs are already assigned to this VM.')\n\n$(translate 'No additional GPU can be added.')" \
|
||||
_pmx_msgbox "$(translate 'All GPUs Already Assigned')" \
|
||||
"\n$(translate 'All detected GPUs are already assigned to this VM.')\n\n$(translate 'No additional GPU can be added.')" \
|
||||
10 70
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local choice
|
||||
local -a clear_opt=()
|
||||
[[ "$WIZARD_CALL" != "true" ]] && clear_opt+=(--clear)
|
||||
choice=$(dialog "${clear_opt[@]}" --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'GPU Already Assigned to This VM')" \
|
||||
--menu "\n$(translate 'The selected GPU is already present in this VM. Select another GPU to continue:')" \
|
||||
choice=$(_pmx_menu \
|
||||
"$(translate 'GPU Already Assigned to This VM')" \
|
||||
"\n$(translate 'The selected GPU is already present in this VM. Select another GPU to continue:')" \
|
||||
18 82 10 \
|
||||
"${menu_items[@]}" \
|
||||
2>&1 >/dev/tty) || exit 0
|
||||
"${menu_items[@]}") || exit 0
|
||||
|
||||
SELECTED_GPU="${ALL_GPU_TYPES[$choice]}"
|
||||
SELECTED_GPU_PCI="${ALL_GPU_PCIS[$choice]}"
|
||||
@@ -492,9 +555,8 @@ detect_host_gpus() {
|
||||
|
||||
if [[ $GPU_COUNT -eq 0 ]]; then
|
||||
_set_wizard_result "no_gpu"
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'No GPU Detected')" \
|
||||
--msgbox "\n$(translate 'No compatible GPU was detected on this host.')" 8 60
|
||||
_pmx_msgbox "$(translate 'No GPU Detected')" \
|
||||
"\n$(translate 'No compatible GPU was detected on this host.')" 8 60
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -506,7 +568,16 @@ detect_host_gpus() {
|
||||
# Phase 1 — Step 2: Check IOMMU, offer to enable it
|
||||
# ==========================================================
|
||||
check_iommu_enabled() {
|
||||
# Dedup: if IOMMU was already configured by another script in this wizard run, skip prompt
|
||||
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||
IOMMU_PENDING_REBOOT=true
|
||||
HOST_CONFIG_CHANGED=true
|
||||
_gpu_register_vfio_iommu_tool
|
||||
return 0
|
||||
fi
|
||||
|
||||
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||
_gpu_register_vfio_iommu_tool
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -519,9 +590,11 @@ check_iommu_enabled() {
|
||||
if [[ "$configured_next_boot" == "true" ]]; then
|
||||
IOMMU_PENDING_REBOOT=true
|
||||
HOST_CONFIG_CHANGED=true
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'IOMMU Pending Reboot')" \
|
||||
--msgbox "\n$(translate 'IOMMU is already configured for next boot, but it is not active yet.')\n\n$(translate 'GPU passthrough configuration will continue now and will become effective after host reboot.')" \
|
||||
_gpu_register_vfio_iommu_tool
|
||||
VM_STORAGE_IOMMU_PENDING_REBOOT=1
|
||||
export VM_STORAGE_IOMMU_PENDING_REBOOT
|
||||
_pmx_msgbox "$(translate 'IOMMU Pending Reboot')" \
|
||||
"\n$(translate 'IOMMU is already configured for next boot, but it is not active yet.')\n\n$(translate 'GPU passthrough configuration will continue now and will become effective after host reboot.')" \
|
||||
11 78
|
||||
return 0
|
||||
fi
|
||||
@@ -533,9 +606,7 @@ check_iommu_enabled() {
|
||||
msg+="$(translate 'Note: A system reboot will be required after enabling IOMMU.')\n"
|
||||
msg+="$(translate 'Configuration will continue now and be effective after reboot.')"
|
||||
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'IOMMU Required')" \
|
||||
--yesno "$msg" 15 72
|
||||
_pmx_yesno "$(translate 'IOMMU Required')" "$msg" 15 72
|
||||
|
||||
local response=$?
|
||||
[[ "$WIZARD_CALL" != "true" ]] && clear
|
||||
@@ -553,6 +624,9 @@ check_iommu_enabled() {
|
||||
fi
|
||||
IOMMU_PENDING_REBOOT=true
|
||||
HOST_CONFIG_CHANGED=true
|
||||
_gpu_register_vfio_iommu_tool
|
||||
VM_STORAGE_IOMMU_PENDING_REBOOT=1
|
||||
export VM_STORAGE_IOMMU_PENDING_REBOOT
|
||||
echo
|
||||
msg_success "$(translate 'IOMMU configured. GPU passthrough setup will continue now and will be effective after reboot.')"
|
||||
echo
|
||||
@@ -632,14 +706,11 @@ select_gpu() {
|
||||
done
|
||||
|
||||
local choice
|
||||
local -a clear_opt=()
|
||||
[[ "$WIZARD_CALL" != "true" ]] && clear_opt+=(--clear)
|
||||
choice=$(dialog "${clear_opt[@]}" --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'Select GPU for VM Passthrough')" \
|
||||
--menu "\n$(translate 'Select the GPU to pass through to the VM:')" \
|
||||
choice=$(_pmx_menu \
|
||||
"$(translate 'Select GPU for VM Passthrough')" \
|
||||
"\n$(translate 'Select the GPU to pass through to the VM:')" \
|
||||
18 82 10 \
|
||||
"${menu_items[@]}" \
|
||||
2>&1 >/dev/tty) || exit 0
|
||||
"${menu_items[@]}") || exit 0
|
||||
|
||||
SELECTED_GPU="${ALL_GPU_TYPES[$choice]}"
|
||||
SELECTED_GPU_PCI="${ALL_GPU_PCIS[$choice]}"
|
||||
@@ -665,9 +736,7 @@ warn_single_gpu() {
|
||||
msg+="$(translate 'Make sure you have SSH or Web UI access before rebooting.')\n\n"
|
||||
msg+="$(translate 'Do you want to continue?')"
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'Single GPU Warning')" \
|
||||
--yesno "$msg" 22 76
|
||||
_pmx_yesno "$(translate 'Single GPU Warning')" "$msg" 22 76
|
||||
|
||||
[[ $? -ne 0 ]] && exit 0
|
||||
}
|
||||
@@ -765,9 +834,7 @@ check_intel_vm_compatibility() {
|
||||
msg+="$(translate 'This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.')\n\n"
|
||||
msg+="$(translate 'Recommended: use GPU with LXC workloads instead of VM passthrough on this hardware.')"
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'Blocked GPU ID')" \
|
||||
--msgbox "$msg" 20 84
|
||||
_pmx_msgbox "$(translate 'Blocked GPU ID')" "$msg" 20 84
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -782,9 +849,7 @@ check_intel_vm_compatibility() {
|
||||
msg+="$(translate 'This state has a high probability of VM startup/reset failures.')\n\n"
|
||||
msg+="\Zb$(translate 'Configuration has been stopped to prevent an unusable VM state.')\Zn"
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'High-Risk GPU Power State')" \
|
||||
--msgbox "$msg" 20 80
|
||||
_pmx_msgbox "$(translate 'High-Risk GPU Power State')" "$msg" 20 80
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -800,9 +865,7 @@ check_intel_vm_compatibility() {
|
||||
msg+="$(translate 'startup/restart errors are likely.')\n\n"
|
||||
msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn"
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'Reset Capability Blocked')" \
|
||||
--msgbox "$msg" 20 80
|
||||
_pmx_msgbox "$(translate 'Reset Capability Blocked')" "$msg" 20 80
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -818,9 +881,7 @@ check_intel_vm_compatibility() {
|
||||
msg+="$(translate 'start/restart failures and reset instability.')\n\n"
|
||||
msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn"
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'Reset Capability Blocked')" \
|
||||
--msgbox "$msg" 20 80
|
||||
_pmx_msgbox "$(translate 'Reset Capability Blocked')" "$msg" 20 80
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -834,9 +895,7 @@ check_intel_vm_compatibility() {
|
||||
msg+="$(translate 'Passthrough may work, but startup/restart reliability is not guaranteed.')\n\n"
|
||||
msg+="$(translate 'Do you want to continue anyway?')"
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'Reset Capability Warning')" \
|
||||
--yesno "$msg" 18 78
|
||||
_pmx_yesno "$(translate 'Reset Capability Warning')" "$msg" 18 78
|
||||
[[ $? -ne 0 ]] && exit 0
|
||||
fi
|
||||
}
|
||||
@@ -872,9 +931,7 @@ check_gpu_vm_compatibility() {
|
||||
msg+=" • $(translate 'Potential QEMU startup/assertion failures')\n\n"
|
||||
msg+="\Zb$(translate 'Configuration has been stopped to prevent an unusable VM state.')\Zn"
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'High-Risk GPU Power State')" \
|
||||
--msgbox "$msg" 22 80
|
||||
_pmx_msgbox "$(translate 'High-Risk GPU Power State')" "$msg" 22 80
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -903,9 +960,7 @@ check_gpu_vm_compatibility() {
|
||||
msg+=" — QEMU IRQ assertion failure → VM does not start\n\n"
|
||||
msg+="\Zb$(translate 'Configuration has been stopped to prevent leaving the VM in an unusable state.')\Zn"
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'Incompatible GPU for VM Passthrough')" \
|
||||
--msgbox "$msg" 26 80
|
||||
_pmx_msgbox "$(translate 'Incompatible GPU for VM Passthrough')" "$msg" 26 80
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -922,9 +977,7 @@ check_gpu_vm_compatibility() {
|
||||
msg+="$(translate 'for this policy and may fail after first use or on subsequent VM starts.')\n\n"
|
||||
msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn"
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'Reset Capability Blocked')" \
|
||||
--msgbox "$msg" 20 80
|
||||
_pmx_msgbox "$(translate 'Reset Capability Blocked')" "$msg" 20 80
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -939,9 +992,7 @@ check_gpu_vm_compatibility() {
|
||||
msg+="$(translate 'Passthrough may fail depending on hardware/firmware implementation.')\n\n"
|
||||
msg+="$(translate 'Do you want to continue anyway?')"
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'Reset Capability Warning')" \
|
||||
--yesno "$msg" 18 78
|
||||
_pmx_yesno "$(translate 'Reset Capability Warning')" "$msg" 18 78
|
||||
[[ $? -ne 0 ]] && exit 0
|
||||
fi
|
||||
}
|
||||
@@ -965,16 +1016,14 @@ analyze_iommu_group() {
|
||||
did=$(cat "/sys/bus/pci/devices/${pci_full}/device" 2>/dev/null | sed 's/0x//')
|
||||
[[ -n "$vid" && -n "$did" ]] && IOMMU_VFIO_IDS+=("${vid}:${did}")
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'IOMMU Group Pending')" \
|
||||
--msgbox "\n$(translate 'IOMMU groups are not available yet because reboot is pending.')\n\n$(translate 'The script will preconfigure the selected GPU now and finalize hardware binding after reboot.')\n\n$(translate 'Selected GPU function'):\n • ${pci_full}" \
|
||||
_pmx_msgbox "$(translate 'IOMMU Group Pending')" \
|
||||
"\n$(translate 'IOMMU groups are not available yet because reboot is pending.')\n\n$(translate 'The script will preconfigure the selected GPU now and finalize hardware binding after reboot.')\n\n$(translate 'Selected GPU function'):\n • ${pci_full}" \
|
||||
14 82
|
||||
return 0
|
||||
fi
|
||||
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'IOMMU Group Error')" \
|
||||
--msgbox "\n$(translate 'Could not determine the IOMMU group for the selected GPU.')\n\n$(translate 'Make sure IOMMU is properly enabled and the system has been rebooted after activation.')" \
|
||||
_pmx_msgbox "$(translate 'IOMMU Group Error')" \
|
||||
"\n$(translate 'Could not determine the IOMMU group for the selected GPU.')\n\n$(translate 'Make sure IOMMU is properly enabled and the system has been rebooted after activation.')" \
|
||||
10 72
|
||||
exit 1
|
||||
fi
|
||||
@@ -1016,19 +1065,6 @@ analyze_iommu_group() {
|
||||
[[ "$dev" != "$pci_full" ]] && extra_devices=$((extra_devices + 1))
|
||||
done
|
||||
|
||||
local msg
|
||||
msg="$(translate 'IOMMU Group'): ${IOMMU_GROUP}\n\n"
|
||||
msg+="$(translate 'The following devices will all be passed to the VM') "
|
||||
msg+="($(translate 'IOMMU isolation rule')):\n\n"
|
||||
msg+="${display_lines}"
|
||||
|
||||
if [[ $extra_devices -gt 0 ]]; then
|
||||
msg+="\n\Z1$(translate 'All devices in the same IOMMU group must be passed together.')\Zn"
|
||||
fi
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'IOMMU Group') ${IOMMU_GROUP}" \
|
||||
--msgbox "\n${msg}" 22 82
|
||||
}
|
||||
|
||||
detect_optional_gpu_audio() {
|
||||
@@ -1078,9 +1114,8 @@ select_vm() {
|
||||
VM_NAME=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^name:" | awk '{print $2}')
|
||||
return 0
|
||||
fi
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'Invalid VMID')" \
|
||||
--msgbox "\n$(translate 'The preselected VMID does not exist on this host:') ${PRESELECT_VMID}" 9 72
|
||||
_pmx_msgbox "$(translate 'Invalid VMID')" \
|
||||
"\n$(translate 'The preselected VMID does not exist on this host:') ${PRESELECT_VMID}" 9 72
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -1097,19 +1132,17 @@ select_vm() {
|
||||
done < <(qm list 2>/dev/null)
|
||||
|
||||
if [[ ${#menu_items[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'No VMs Found')" \
|
||||
--msgbox "\n$(translate 'No Virtual Machines found on this system.')\n\n$(translate 'Create a VM first (machine type q35 + UEFI BIOS), then run this option again.')" \
|
||||
_pmx_msgbox "$(translate 'No VMs Found')" \
|
||||
"\n$(translate 'No Virtual Machines found on this system.')\n\n$(translate 'Create a VM first (machine type q35 + UEFI BIOS), then run this option again.')" \
|
||||
10 68
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SELECTED_VMID=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'Select Virtual Machine')" \
|
||||
--menu "\n$(translate 'Select the VM to add the GPU to:')" \
|
||||
SELECTED_VMID=$(_pmx_menu \
|
||||
"$(translate 'Select Virtual Machine')" \
|
||||
"\n$(translate 'Select the VM to add the GPU to:')" \
|
||||
20 72 12 \
|
||||
"${menu_items[@]}" \
|
||||
2>&1 >/dev/tty) || exit 0
|
||||
"${menu_items[@]}") || exit 0
|
||||
|
||||
VM_NAME=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^name:" | awk '{print $2}')
|
||||
}
|
||||
@@ -1138,9 +1171,7 @@ check_vm_machine_type() {
|
||||
msg+=" • $(translate 'BIOS: OVMF (UEFI)')\n"
|
||||
msg+=" • $(translate 'Storage controller: VirtIO SCSI')"
|
||||
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'Incompatible Machine Type')" \
|
||||
--msgbox "$msg" 20 78
|
||||
_pmx_msgbox "$(translate 'Incompatible Machine Type')" "$msg" 20 78
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -1210,13 +1241,11 @@ check_switch_mode() {
|
||||
msg+="\Z1\Zb$(translate 'Start on boot enabled (onboot=1)'): ${onboot_count}\Zn\n"
|
||||
msg+="\n\Z1$(translate 'After this LXC → VM switch, reboot the host so the new binding state is applied cleanly.')\Zn"
|
||||
|
||||
action_choice=$(dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'GPU Used in LXC Containers')" \
|
||||
--default-item "2" \
|
||||
--menu "$msg" 25 96 8 \
|
||||
action_choice=$(_pmx_menu --default-item "2" \
|
||||
"$(translate 'GPU Used in LXC Containers')" \
|
||||
"$msg" 25 96 8 \
|
||||
"1" "$(translate 'Keep GPU in LXC config (disable Start on boot)')" \
|
||||
"2" "$(translate 'Remove GPU from LXC config (keep Start on boot)')" \
|
||||
2>&1 >/dev/tty) || exit 0
|
||||
"2" "$(translate 'Remove GPU from LXC config (keep Start on boot)')") || exit 0
|
||||
|
||||
case "$action_choice" in
|
||||
1) LXC_SWITCH_ACTION="keep_gpu_disable_onboot" ;;
|
||||
@@ -1254,9 +1283,7 @@ check_switch_mode() {
|
||||
msg+=" Hardware Graphics → Add GPU to VM\n"
|
||||
msg+="$(translate 'to move the GPU safely.')"
|
||||
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate 'GPU Busy in Running VM')" \
|
||||
--msgbox "$msg" 16 78
|
||||
_pmx_msgbox "$(translate 'GPU Busy in Running VM')" "$msg" 16 78
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -1292,13 +1319,11 @@ check_switch_mode() {
|
||||
msg+="$(translate 'Choose conflict policy for the source VM:')"
|
||||
|
||||
local vm_action_choice
|
||||
vm_action_choice=$(dialog --clear --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate 'GPU Already Assigned to Another VM')" \
|
||||
--default-item "1" \
|
||||
--menu "$msg" 24 98 8 \
|
||||
vm_action_choice=$(_pmx_menu --default-item "1" \
|
||||
"$(translate 'GPU Already Assigned to Another VM')" \
|
||||
"$msg" 24 84 8 \
|
||||
"1" "$(translate 'Keep GPU in source VM config (disable Start on boot if enabled)')" \
|
||||
"2" "$(translate 'Remove GPU from source VM config (keep Start on boot)')" \
|
||||
2>&1 >/dev/tty) || exit 0
|
||||
"2" "$(translate 'Remove GPU from source VM config (keep Start on boot)')") || exit 0
|
||||
|
||||
case "$vm_action_choice" in
|
||||
1) SWITCH_VM_ACTION="keep_gpu_disable_onboot" ;;
|
||||
@@ -1376,9 +1401,7 @@ confirm_summary() {
|
||||
local run_title
|
||||
run_title=$(_get_vm_run_title)
|
||||
|
||||
dialog --clear --backtitle "ProxMenux" --colors \
|
||||
--title "${run_title}" \
|
||||
--yesno "$msg" 28 78
|
||||
_pmx_yesno "${run_title}" "$msg" 28 78
|
||||
[[ $? -ne 0 ]] && exit 0
|
||||
}
|
||||
|
||||
@@ -1724,7 +1747,7 @@ cleanup_vm_config() {
|
||||
local pci_slot="${SELECTED_GPU_PCI#0000:}"
|
||||
pci_slot="${pci_slot%.*}" # 01:00
|
||||
|
||||
if [[ "$VM_SWITCH_ACTION" == "keep_gpu_disable_onboot" ]]; then
|
||||
if [[ "$SWITCH_VM_ACTION" == "keep_gpu_disable_onboot" ]]; then
|
||||
msg_info "$(translate 'Keeping GPU in source VM config') ${SWITCH_VM_SRC}..."
|
||||
if _vm_onboot_enabled "$SWITCH_VM_SRC"; then
|
||||
if qm set "$SWITCH_VM_SRC" -onboot 0 >>"$LOG_FILE" 2>&1; then
|
||||
@@ -1916,7 +1939,6 @@ main() {
|
||||
if [[ "$WIZARD_CALL" == "true" ]]; then
|
||||
echo
|
||||
else
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "${run_title}"
|
||||
fi
|
||||
|
||||
160
scripts/gpu_tpu/gpu-tpu-manual-guide.sh
Normal file
160
scripts/gpu_tpu/gpu-tpu-manual-guide.sh
Normal file
@@ -0,0 +1,160 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenux - GPU/TPU Manual CLI Guide
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : GPL-3.0
|
||||
# Version : 1.0
|
||||
# Last Updated: 07/04/2026
|
||||
# ==========================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||
elif [[ ! -f "$UTILS_FILE" ]]; then
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
fi
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
GREEN=$'\033[0;32m'
|
||||
NC=$'\033[0m'
|
||||
|
||||
_cl() {
|
||||
# _cl <num> <display_cmd> <description>
|
||||
# Prints a numbered command line with fixed-column alignment (separator at col 52).
|
||||
local num="$1" disp="$2" desc="$3"
|
||||
local pad=$((47 - ${#disp}))
|
||||
[[ $pad -lt 1 ]] && pad=1
|
||||
local spaces
|
||||
spaces=$(printf '%*s' "$pad" '')
|
||||
printf " %2d) %s%s%s%s - %s\n" "$num" "$GREEN" "$disp" "$NC" "$spaces" "$desc"
|
||||
}
|
||||
|
||||
while true; do
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "GPU/TPU - Manual CLI Guide")"
|
||||
echo -e "${TAB}${YW}$(translate 'Inspection commands run directly. Template commands [T] require parameter substitution.')${CL}"
|
||||
echo
|
||||
|
||||
_cl 1 "lspci -nn | grep -iE 'VGA|3D|Display'" "$(translate 'Detect GPUs in host')"
|
||||
_cl 2 "lspci -nnk | grep -A3 -Ei 'VGA|3D'" "$(translate 'Show GPU kernel driver in use')"
|
||||
_cl 3 "cat /proc/cmdline" "$(translate 'Check kernel params (IOMMU flags)')"
|
||||
_cl 4 "dmesg -T | grep -Ei 'DMAR|IOMMU|vfio|pcie'" "$(translate 'Inspect passthrough/kernel events')"
|
||||
_cl 5 "find /sys/kernel/iommu_groups -type l" "$(translate 'List IOMMU group mapping')"
|
||||
_cl 6 "lsmod | grep -E 'vfio|nvidia|amdgpu|apex'" "$(translate 'Check loaded GPU/TPU modules')"
|
||||
_cl 7 "grep -R \"vfio-pci|blacklist\" /etc/modprobe.d" "$(translate 'Review passthrough config files')"
|
||||
_cl 8 "nvidia-smi" "$(translate 'Check NVIDIA driver and devices')"
|
||||
_cl 9 "qm config <vmid> | grep 'hostpci|bios'" "$(translate 'Check VM passthrough settings')"
|
||||
_cl 10 "pct config <ctid> | grep 'dev|lxc.cgroup2'" "$(translate 'Check LXC GPU/TPU mapping')"
|
||||
_cl 11 "ls -l /dev/dri /dev/kfd /dev/nvidia*" "$(translate 'Inspect host device nodes')"
|
||||
_cl 12 "qm set <vmid> --hostpci<slot> <BDF>,pcie=1" "[T] $(translate 'Assign GPU PCI function to VM')"
|
||||
_cl 13 "qm set <vmid> -delete hostpci<slot>" "[T] $(translate 'Remove passthrough device from VM')"
|
||||
_cl 14 "qm set <vmid> -onboot 0" "[T] $(translate 'Disable autostart on conflicting VM')"
|
||||
_cl 15 "sed -i '/GRUB_CMDLINE_LINUX_DEFAULT/ s|...|'" "[T] $(translate 'Enable IOMMU in GRUB or ZFS boot')"
|
||||
_cl 16 "update-initramfs -u && proxmox-boot-tool" "[T] $(translate 'Apply boot/initramfs changes')"
|
||||
_cl 17 "lsusb | grep Coral ; lspci | grep Unichip" "$(translate 'Check Coral USB/M.2 detection')"
|
||||
echo -e " ${DEF} 0) $(translate 'Back to previous menu or Esc + Enter')${CL}"
|
||||
echo
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}"
|
||||
read -r user_input
|
||||
|
||||
if [[ "$user_input" == $'\x1b' ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
mode="exec"
|
||||
case "$user_input" in
|
||||
1) cmd="lspci -nn | grep -iE 'VGA compatible|3D controller|Display controller'" ;;
|
||||
2) cmd="lspci -nnk | grep -A3 -Ei 'VGA compatible|3D controller|Display controller'" ;;
|
||||
3) cmd="cat /proc/cmdline" ;;
|
||||
4) cmd="dmesg -T | grep -Ei 'DMAR|IOMMU|vfio|pcie|AER|reset'" ;;
|
||||
5) cmd="find /sys/kernel/iommu_groups -type l" ;;
|
||||
6) cmd="lsmod | grep -E 'vfio|nvidia|amdgpu|i915|apex|gasket'" ;;
|
||||
7) cmd="grep -R \"vfio-pci\\|blacklist .*nvidia\\|blacklist .*amdgpu\\|blacklist .*radeon\" /etc/modprobe.d /etc/modules /etc/default/grub /etc/kernel/cmdline 2>/dev/null" ;;
|
||||
8) cmd="nvidia-smi" ;;
|
||||
9)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"
|
||||
read -r vmid
|
||||
cmd="qm config $vmid | grep -E '^(hostpci|cpu:|machine:|bios:|args:|boot:)'"
|
||||
;;
|
||||
10)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"
|
||||
read -r ctid
|
||||
cmd="pct config $ctid | grep -E '^(dev[0-9]+:|lxc\\.cgroup2\\.devices\\.allow:|lxc\\.mount\\.entry:|features:)'"
|
||||
;;
|
||||
11) cmd="ls -l /dev/dri /dev/kfd /dev/nvidia* /dev/apex* 2>/dev/null" ;;
|
||||
12)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot (e.g. 0): ')${CL}"; read -r slot
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter PCI BDF (e.g. 0000:01:00.0): ')${CL}"; read -r bdf
|
||||
cmd="qm set $vmid --hostpci${slot} ${bdf},pcie=1"
|
||||
mode="template"
|
||||
;;
|
||||
13)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot (e.g. 0): ')${CL}"; read -r slot
|
||||
cmd="qm set $vmid -delete hostpci${slot}"
|
||||
mode="template"
|
||||
;;
|
||||
14)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||
cmd="qm set $vmid -onboot 0"
|
||||
mode="template"
|
||||
;;
|
||||
15)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Boot type (grub/zfs): ')${CL}"; read -r boot_type
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'CPU vendor (intel/amd): ')${CL}"; read -r cpu_vendor
|
||||
case "$cpu_vendor" in
|
||||
amd|AMD) iommu_param="amd_iommu=on iommu=pt" ;;
|
||||
*) iommu_param="intel_iommu=on iommu=pt" ;;
|
||||
esac
|
||||
case "$boot_type" in
|
||||
zfs|ZFS) cmd="sed -i 's/\$/ ${iommu_param}/' /etc/kernel/cmdline" ;;
|
||||
*) cmd="sed -i '/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param}\"|' /etc/default/grub" ;;
|
||||
esac
|
||||
mode="template"
|
||||
;;
|
||||
16)
|
||||
cmd="update-initramfs -u -k all && (proxmox-boot-tool refresh || update-grub)"
|
||||
mode="template"
|
||||
;;
|
||||
17) cmd="lsusb | grep -Ei '18d1:9302|1a6e:089a' ; lspci | grep -i 'Global Unichip'" ;;
|
||||
0) break ;;
|
||||
*)
|
||||
if [[ -n "$user_input" ]]; then
|
||||
cmd="$user_input"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$mode" == "template" ]]; then
|
||||
echo -e "\n${GREEN}$(translate 'Manual command template (copy/paste):')${NC}\n"
|
||||
echo "$cmd"
|
||||
echo
|
||||
msg_success "$(translate 'Press ENTER to continue...')"
|
||||
read -r tmp
|
||||
continue
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}> $cmd${NC}\n"
|
||||
bash -c "$cmd"
|
||||
echo
|
||||
msg_success "$(translate 'Press ENTER to continue...')"
|
||||
read -r tmp
|
||||
done
|
||||
|
||||
@@ -57,6 +57,33 @@ detect_nvidia_gpus() {
|
||||
fi
|
||||
}
|
||||
|
||||
check_gpu_not_in_vm_passthrough() {
|
||||
local dev vendor driver vfio_list=""
|
||||
for dev in /sys/bus/pci/devices/*; do
|
||||
vendor=$(cat "$dev/vendor" 2>/dev/null)
|
||||
[[ "$vendor" != "0x10de" ]] && continue
|
||||
if [[ -L "$dev/driver" ]]; then
|
||||
driver=$(basename "$(readlink "$dev/driver")")
|
||||
if [[ "$driver" == "vfio-pci" ]]; then
|
||||
vfio_list+=" • $(basename "$dev")\n"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -z "$vfio_list" ]] && return 0
|
||||
|
||||
local msg
|
||||
msg="\n$(translate "One or more NVIDIA GPUs are currently configured for VM passthrough (vfio-pci):")\n\n"
|
||||
msg+="${vfio_list}\n"
|
||||
msg+="$(translate "Installing host drivers while the GPU is assigned to a VM could break passthrough and destabilize the system.")\n\n"
|
||||
msg+="$(translate "To install host drivers, first remove the GPU from VM passthrough configuration and reboot.")"
|
||||
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "GPU in VM Passthrough Mode")" \
|
||||
--msgbox "$msg" 16 78
|
||||
exit 0
|
||||
}
|
||||
|
||||
detect_driver_status() {
|
||||
CURRENT_DRIVER_INSTALLED=false
|
||||
CURRENT_DRIVER_VERSION=""
|
||||
@@ -842,6 +869,7 @@ main() {
|
||||
|
||||
detect_nvidia_gpus
|
||||
detect_driver_status
|
||||
check_gpu_not_in_vm_passthrough
|
||||
|
||||
if ! $NVIDIA_GPU_PRESENT; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate 'NVIDIA GPU Driver Installation')" --msgbox \
|
||||
|
||||
@@ -23,6 +23,37 @@ load_language
|
||||
initialize_cache
|
||||
|
||||
|
||||
# ============================================================
|
||||
# GPU passthrough guard — block update when GPU is in VM passthrough mode
|
||||
# ============================================================
|
||||
check_gpu_not_in_vm_passthrough() {
|
||||
local dev vendor driver vfio_list=""
|
||||
for dev in /sys/bus/pci/devices/*; do
|
||||
vendor=$(cat "$dev/vendor" 2>/dev/null)
|
||||
[[ "$vendor" != "0x10de" ]] && continue
|
||||
if [[ -L "$dev/driver" ]]; then
|
||||
driver=$(basename "$(readlink "$dev/driver")")
|
||||
if [[ "$driver" == "vfio-pci" ]]; then
|
||||
vfio_list+=" • $(basename "$dev")\n"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -z "$vfio_list" ]] && return 0
|
||||
|
||||
local msg
|
||||
msg="\n$(translate "One or more NVIDIA GPUs are currently configured for VM passthrough (vfio-pci):")\n\n"
|
||||
msg+="${vfio_list}\n"
|
||||
msg+="$(translate "Updating host drivers while the GPU is assigned to a VM could break passthrough and destabilize the system.")\n\n"
|
||||
msg+="$(translate "To update host drivers, first remove the GPU from VM passthrough configuration and reboot.")"
|
||||
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "GPU in VM Passthrough Mode")" \
|
||||
--msgbox "$msg" 16 78
|
||||
exit 0
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Host NVIDIA state detection
|
||||
# ============================================================
|
||||
@@ -436,13 +467,25 @@ show_current_state_dialog() {
|
||||
# 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...')"
|
||||
echo
|
||||
msg_success "$(translate 'NVIDIA driver update completed.')"
|
||||
echo
|
||||
msg_info "$(translate 'Removing no longer required packages and purging old cached updates...')"
|
||||
apt-get -y autoremove >/dev/null 2>&1
|
||||
apt-get -y autoclean >/dev/null 2>&1
|
||||
msg_ok "$(translate 'Cleanup finished.')"
|
||||
echo -e "${TAB}${BL}Log: ${LOG_FILE}${CL}"
|
||||
echo
|
||||
|
||||
if whiptail --title "$(translate 'Reboot Required')" \
|
||||
--yesno "$(translate 'The host driver update requires a reboot to take effect. Do you want to restart now?')" 10 70; then
|
||||
msg_success "$(translate 'Press Enter to continue...')"
|
||||
read -r
|
||||
msg_warn "$(translate 'Rebooting the system...')"
|
||||
reboot
|
||||
else
|
||||
msg_success "$(translate 'Update complete. Please reboot the server manually.')"
|
||||
msg_success "$(translate 'Completed. Press Enter to return to menu...')"
|
||||
msg_info2 "$(translate 'You can reboot later manually.')"
|
||||
msg_success "$(translate 'Press Enter to continue...')"
|
||||
read -r
|
||||
fi
|
||||
}
|
||||
@@ -455,6 +498,7 @@ main() {
|
||||
: >"$LOG_FILE"
|
||||
|
||||
# ---- Phase 1: dialogs ----
|
||||
check_gpu_not_in_vm_passthrough
|
||||
detect_host_nvidia
|
||||
show_current_state_dialog
|
||||
select_target_version
|
||||
|
||||
@@ -888,6 +888,48 @@ apply_vm_action_for_lxc_mode() {
|
||||
done
|
||||
}
|
||||
|
||||
_register_iommu_tool() {
|
||||
local tools_json="${BASE_DIR:-/usr/local/share/proxmenux}/installed_tools.json"
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
[[ -f "$tools_json" ]] || echo "{}" > "$tools_json"
|
||||
jq '.vfio_iommu=true' "$tools_json" > "$tools_json.tmp" \
|
||||
&& mv "$tools_json.tmp" "$tools_json" || true
|
||||
}
|
||||
|
||||
_enable_iommu_cmdline() {
|
||||
local cpu_vendor
|
||||
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
||||
|
||||
local iommu_param
|
||||
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
|
||||
iommu_param="intel_iommu=on"
|
||||
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
|
||||
iommu_param="amd_iommu=on"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
local cmdline_file="/etc/kernel/cmdline"
|
||||
local grub_file="/etc/default/grub"
|
||||
|
||||
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
|
||||
if ! grep -q "$iommu_param" "$cmdline_file"; then
|
||||
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
|
||||
proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
elif [[ -f "$grub_file" ]]; then
|
||||
if ! grep -q "$iommu_param" "$grub_file"; then
|
||||
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
|
||||
update-grub >>"$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
switch_to_vm_mode() {
|
||||
detect_affected_lxc_for_selected
|
||||
prompt_lxc_action_for_vm_mode
|
||||
@@ -897,6 +939,25 @@ switch_to_vm_mode() {
|
||||
apply_lxc_action_for_vm_mode
|
||||
|
||||
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
|
||||
|
||||
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||
_register_iommu_tool
|
||||
msg_ok "$(translate 'IOMMU is already active on this system')" | tee -a "$screen_capture"
|
||||
elif grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
|
||||
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
|
||||
_register_iommu_tool
|
||||
HOST_CONFIG_CHANGED=true
|
||||
msg_ok "$(translate 'IOMMU already configured in kernel parameters')" | tee -a "$screen_capture"
|
||||
else
|
||||
if _enable_iommu_cmdline; then
|
||||
_register_iommu_tool
|
||||
HOST_CONFIG_CHANGED=true
|
||||
msg_ok "$(translate 'IOMMU kernel parameters configured')" | tee -a "$screen_capture"
|
||||
else
|
||||
msg_warn "$(translate 'Could not configure IOMMU kernel parameters automatically. Configure manually and reboot.')" | tee -a "$screen_capture"
|
||||
fi
|
||||
fi
|
||||
|
||||
_add_vfio_modules
|
||||
msg_ok "$(translate 'VFIO modules configured in /etc/modules')" | tee -a "$screen_capture"
|
||||
_configure_iommu_options
|
||||
@@ -1011,6 +1072,45 @@ switch_to_lxc_mode() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
# Send notification when GPU mode switch completes
|
||||
# ==========================================================
|
||||
_send_gpu_mode_notification() {
|
||||
local new_mode="$1"
|
||||
local old_mode="$2"
|
||||
local notify_script="/usr/bin/notification_manager.py"
|
||||
|
||||
[[ ! -f "$notify_script" ]] && return 0
|
||||
|
||||
local hostname_short
|
||||
hostname_short=$(hostname -s)
|
||||
|
||||
# Build GPU list for notification
|
||||
local gpu_list=""
|
||||
local idx
|
||||
for idx in "${SELECTED_GPU_IDX[@]}"; do
|
||||
gpu_list+="${ALL_GPU_NAMES[$idx]} (${ALL_GPU_PCIS[$idx]}), "
|
||||
done
|
||||
gpu_list="${gpu_list%, }"
|
||||
|
||||
local mode_label details
|
||||
if [[ "$new_mode" == "vm" ]]; then
|
||||
mode_label="GPU -> VM (VFIO passthrough)"
|
||||
details="GPU(s) ready for VM passthrough. A host reboot may be required."
|
||||
else
|
||||
mode_label="GPU -> LXC (native driver)"
|
||||
details="GPU(s) available for LXC containers with native drivers."
|
||||
fi
|
||||
|
||||
python3 "$notify_script" --action send-raw --severity INFO \
|
||||
--title "${hostname_short}: GPU mode changed to ${mode_label}" \
|
||||
--message "GPU passthrough mode switched.
|
||||
GPU(s): ${gpu_list}
|
||||
Previous: ${old_mode}
|
||||
New: ${mode_label}
|
||||
${details}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
confirm_plan() {
|
||||
local msg mode_line
|
||||
if [[ "$TARGET_MODE" == "vm" ]]; then
|
||||
@@ -1079,12 +1179,22 @@ main() {
|
||||
_set_title
|
||||
echo
|
||||
|
||||
# Determine old mode before switch for notification
|
||||
local old_mode_label
|
||||
if [[ "$CURRENT_MODE" == "vm" ]]; then
|
||||
old_mode_label="GPU -> VM (VFIO)"
|
||||
else
|
||||
old_mode_label="GPU -> LXC (native)"
|
||||
fi
|
||||
|
||||
if [[ "$TARGET_MODE" == "vm" ]]; then
|
||||
switch_to_vm_mode
|
||||
msg_success "$(translate 'GPU switch complete: VM mode prepared.')"
|
||||
_send_gpu_mode_notification "vm" "$old_mode_label"
|
||||
else
|
||||
switch_to_lxc_mode
|
||||
msg_success "$(translate 'GPU switch complete: LXC mode prepared.')"
|
||||
_send_gpu_mode_notification "lxc" "$old_mode_label"
|
||||
fi
|
||||
|
||||
final_summary
|
||||
|
||||
@@ -816,6 +816,48 @@ apply_vm_action_for_lxc_mode() {
|
||||
# ==========================================================
|
||||
# Switch Mode Functions
|
||||
# ==========================================================
|
||||
_register_iommu_tool() {
|
||||
local tools_json="${BASE_DIR:-/usr/local/share/proxmenux}/installed_tools.json"
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
[[ -f "$tools_json" ]] || echo "{}" > "$tools_json"
|
||||
jq '.vfio_iommu=true' "$tools_json" > "$tools_json.tmp" \
|
||||
&& mv "$tools_json.tmp" "$tools_json" || true
|
||||
}
|
||||
|
||||
_enable_iommu_cmdline() {
|
||||
local cpu_vendor
|
||||
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
||||
|
||||
local iommu_param
|
||||
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
|
||||
iommu_param="intel_iommu=on"
|
||||
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
|
||||
iommu_param="amd_iommu=on"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
local cmdline_file="/etc/kernel/cmdline"
|
||||
local grub_file="/etc/default/grub"
|
||||
|
||||
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
|
||||
if ! grep -q "$iommu_param" "$cmdline_file"; then
|
||||
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
|
||||
proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
elif [[ -f "$grub_file" ]]; then
|
||||
if ! grep -q "$iommu_param" "$grub_file"; then
|
||||
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
|
||||
update-grub >>"$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
switch_to_vm_mode() {
|
||||
detect_affected_lxc_for_selected
|
||||
prompt_lxc_action_for_vm_mode
|
||||
@@ -825,6 +867,25 @@ switch_to_vm_mode() {
|
||||
apply_lxc_action_for_vm_mode
|
||||
|
||||
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
|
||||
|
||||
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||
_register_iommu_tool
|
||||
msg_ok "$(translate 'IOMMU is already active on this system')" | tee -a "$screen_capture"
|
||||
elif grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
|
||||
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
|
||||
_register_iommu_tool
|
||||
HOST_CONFIG_CHANGED=true
|
||||
msg_ok "$(translate 'IOMMU already configured in kernel parameters')" | tee -a "$screen_capture"
|
||||
else
|
||||
if _enable_iommu_cmdline; then
|
||||
_register_iommu_tool
|
||||
HOST_CONFIG_CHANGED=true
|
||||
msg_ok "$(translate 'IOMMU kernel parameters configured')" | tee -a "$screen_capture"
|
||||
else
|
||||
msg_warn "$(translate 'Could not configure IOMMU kernel parameters automatically. Configure manually and reboot.')" | tee -a "$screen_capture"
|
||||
fi
|
||||
fi
|
||||
|
||||
_add_vfio_modules
|
||||
msg_ok "$(translate 'VFIO modules configured in /etc/modules')" | tee -a "$screen_capture"
|
||||
_configure_iommu_options
|
||||
@@ -986,6 +1047,39 @@ final_summary() {
|
||||
|
||||
# ==========================================================
|
||||
# Parse Arguments (supports both CLI args and env vars)
|
||||
# ==========================================================
|
||||
# Send notification when GPU mode switch completes
|
||||
# ==========================================================
|
||||
_send_gpu_mode_notification() {
|
||||
local new_mode="$1"
|
||||
local gpu_name="$2"
|
||||
local gpu_pci="$3"
|
||||
local old_mode="$4"
|
||||
local notify_script="/usr/bin/notification_manager.py"
|
||||
|
||||
[[ ! -f "$notify_script" ]] && return 0
|
||||
|
||||
local hostname_short
|
||||
hostname_short=$(hostname -s)
|
||||
|
||||
local mode_label details
|
||||
if [[ "$new_mode" == "vm" ]]; then
|
||||
mode_label="GPU -> VM (VFIO passthrough)"
|
||||
details="GPU is now ready for VM passthrough. A host reboot may be required."
|
||||
else
|
||||
mode_label="GPU -> LXC (native driver)"
|
||||
details="GPU is now available for LXC containers with native drivers."
|
||||
fi
|
||||
|
||||
python3 "$notify_script" --action send-raw --severity INFO \
|
||||
--title "${hostname_short}: GPU mode changed to ${mode_label}" \
|
||||
--message "GPU passthrough mode switched.
|
||||
GPU: ${gpu_name} (${gpu_pci})
|
||||
Previous: ${old_mode}
|
||||
New: ${mode_label}
|
||||
${details}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
parse_arguments() {
|
||||
# First, check combined parameter (format: "SLOT|MODE")
|
||||
@@ -1066,13 +1160,28 @@ main() {
|
||||
_set_title
|
||||
echo
|
||||
|
||||
# Determine old mode before switch for notification
|
||||
local old_mode_label
|
||||
if [[ "$CURRENT_MODE" == "vm" ]]; then
|
||||
old_mode_label="GPU -> VM (VFIO)"
|
||||
else
|
||||
old_mode_label="GPU -> LXC (native)"
|
||||
fi
|
||||
|
||||
# Get GPU info for notification
|
||||
local gpu_idx="${SELECTED_GPU_IDX[0]}"
|
||||
local gpu_name="${ALL_GPU_NAMES[$gpu_idx]}"
|
||||
local gpu_pci="${ALL_GPU_PCIS[$gpu_idx]}"
|
||||
|
||||
# Execute the switch
|
||||
if [[ "$TARGET_MODE" == "vm" ]]; then
|
||||
switch_to_vm_mode
|
||||
msg_success "$(translate 'GPU switch complete: VM mode prepared.')"
|
||||
_send_gpu_mode_notification "vm" "$gpu_name" "$gpu_pci" "$old_mode_label"
|
||||
else
|
||||
switch_to_lxc_mode
|
||||
msg_success "$(translate 'GPU switch complete: LXC mode prepared.')"
|
||||
_send_gpu_mode_notification "lxc" "$gpu_name" "$gpu_pci" "$old_mode_label"
|
||||
fi
|
||||
|
||||
final_summary
|
||||
|
||||
@@ -303,7 +303,7 @@ show_storage_commands() {
|
||||
15) cmd="lvs" ;;
|
||||
16) cmd="cat /etc/pve/storage.cfg" ;;
|
||||
17) cmd="pvesm status" ;;
|
||||
19)
|
||||
18)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter storage ID: ')${CL}"
|
||||
read -r store
|
||||
cmd="pvesm list $store"
|
||||
@@ -591,42 +591,116 @@ show_update_commands() {
|
||||
|
||||
|
||||
# ===============================================================
|
||||
# 06 GPU Passthrough Commands
|
||||
# 06 GPU/TPU Passthrough Commands
|
||||
# ===============================================================
|
||||
show_gpu_commands() {
|
||||
while true; do
|
||||
clear
|
||||
echo -e "${YELLOW}$(translate 'GPU Passthrough Commands')${NC}"
|
||||
echo "------------------------------------------------"
|
||||
echo -e " 1) ${GREEN}lspci -nn | grep -i nvidia${NC} - $(translate 'List NVIDIA PCI devices')"
|
||||
echo -e " 2) ${GREEN}lspci -nn | grep -i vga${NC} - $(translate 'List all VGA compatible devices')"
|
||||
echo -e " 3) ${GREEN}dmesg | grep -i vfio${NC} - $(translate 'Check VFIO module messages')"
|
||||
echo -e " 4) ${GREEN}cat /etc/modprobe.d/vfio.conf${NC} - $(translate 'Review VFIO passthrough configuration')"
|
||||
echo -e " 5) ${GREEN}update-initramfs -u${NC} - $(translate 'Apply initramfs changes (VFIO)')"
|
||||
echo -e " 6) ${GREEN}cat /etc/default/grub${NC} - $(translate 'Review GRUB options for IOMMU')"
|
||||
echo -e " 7) ${GREEN}update-grub${NC} - $(translate 'Apply GRUB changes')"
|
||||
echo -e "${YELLOW}$(translate 'GPU/TPU Passthrough Commands')${NC}"
|
||||
echo -e "${TAB}${YW}$(translate 'Inspection commands run directly. Template commands [T] require parameter substitution.')${CL}"
|
||||
echo "------------------------------------------------------------"
|
||||
echo -e " 1) ${GREEN}lspci -nn | grep -iE 'VGA|3D|Display'${NC} - $(translate 'Detect GPUs in host')"
|
||||
echo -e " 2) ${GREEN}lspci -nnk | grep -A3 -Ei 'VGA|3D'${NC} - $(translate 'Show GPU kernel driver in use')"
|
||||
echo -e " 3) ${GREEN}cat /proc/cmdline${NC} - $(translate 'Check kernel params (IOMMU flags)')"
|
||||
echo -e " 4) ${GREEN}dmesg -T | grep -Ei 'DMAR|IOMMU|vfio|pcie'${NC} - $(translate 'Inspect passthrough/kernel events')"
|
||||
echo -e " 5) ${GREEN}find /sys/kernel/iommu_groups -type l${NC} - $(translate 'List IOMMU group mapping')"
|
||||
echo -e " 6) ${GREEN}lsmod | grep -E 'vfio|nvidia|amdgpu|apex'${NC} - $(translate 'Check loaded GPU/TPU modules')"
|
||||
echo -e " 7) ${GREEN}grep -R \"vfio-pci|blacklist\" /etc/modprobe.d${NC} - $(translate 'Review passthrough config files')"
|
||||
echo -e " 8) ${GREEN}nvidia-smi${NC} - $(translate 'Check NVIDIA driver and devices')"
|
||||
echo -e " 9) ${GREEN}qm config <vmid> | grep 'hostpci|bios'${NC} - [T] $(translate 'Check VM passthrough settings')"
|
||||
echo -e "10) ${GREEN}pct config <ctid> | grep 'dev|lxc.cgroup2'${NC} - [T] $(translate 'Check LXC GPU/TPU mapping')"
|
||||
echo -e "11) ${GREEN}ls -l /dev/dri /dev/kfd /dev/nvidia*${NC} - $(translate 'Inspect host device nodes')"
|
||||
echo -e "12) ${GREEN}qm set <vmid> --hostpci<slot> <BDF>,pcie=1${NC} - [T] $(translate 'Assign GPU PCI function to VM')"
|
||||
echo -e "13) ${GREEN}qm set <vmid> -delete hostpci<slot>${NC} - [T] $(translate 'Remove passthrough device from VM')"
|
||||
echo -e "14) ${GREEN}qm set <vmid> -onboot 0${NC} - [T] $(translate 'Disable autostart on conflicting VM')"
|
||||
echo -e "15) ${GREEN}sed -i '/GRUB_CMDLINE_LINUX_DEFAULT/ s|...|'${NC} - [T] $(translate 'Enable IOMMU in GRUB or ZFS boot')"
|
||||
echo -e "16) ${GREEN}update-initramfs -u && proxmox-boot-tool${NC} - [T] $(translate 'Apply boot/initramfs changes')"
|
||||
echo -e "17) ${GREEN}lsusb | grep Coral ; lspci | grep Unichip${NC} - $(translate 'Check Coral USB/M.2 detection')"
|
||||
echo -e " ${DEF}0) $(translate ' Back to previous menu or Esc + Enter')"
|
||||
echo
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}"
|
||||
read -r user_input
|
||||
|
||||
# Check for Esc key press
|
||||
if [[ "$user_input" == $'\x1b' ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
mode="exec"
|
||||
case "$user_input" in
|
||||
1) cmd="lspci -nn | grep -i nvidia" ;;
|
||||
2) cmd="lspci -nn | grep -i vga" ;;
|
||||
3) cmd="dmesg | grep -i vfio" ;;
|
||||
4) cmd="cat /etc/modprobe.d/vfio.conf" ;;
|
||||
5) cmd="update-initramfs -u" ;;
|
||||
6) cmd="cat /etc/default/grub" ;;
|
||||
7) cmd="update-grub" ;;
|
||||
1) cmd="lspci -nn | grep -iE 'VGA compatible|3D controller|Display controller'" ;;
|
||||
2) cmd="lspci -nnk | grep -A3 -Ei 'VGA compatible|3D controller|Display controller'" ;;
|
||||
3) cmd="cat /proc/cmdline" ;;
|
||||
4) cmd="dmesg -T | grep -Ei 'DMAR|IOMMU|vfio|pcie|AER|reset'" ;;
|
||||
5) cmd="find /sys/kernel/iommu_groups -type l" ;;
|
||||
6) cmd="lsmod | grep -E 'vfio|nvidia|amdgpu|i915|apex|gasket'" ;;
|
||||
7) cmd="grep -R \"vfio-pci\\|blacklist .*nvidia\\|blacklist .*amdgpu\\|blacklist .*radeon\" /etc/modprobe.d /etc/modules /etc/default/grub /etc/kernel/cmdline 2>/dev/null" ;;
|
||||
8) cmd="nvidia-smi" ;;
|
||||
9)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"
|
||||
read -r vmid
|
||||
cmd="qm config $vmid | grep -E '^(hostpci|cpu:|machine:|bios:|args:|boot:)'"
|
||||
;;
|
||||
10)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"
|
||||
read -r ctid
|
||||
cmd="pct config $ctid | grep -E '^(dev[0-9]+:|lxc\\.cgroup2\\.devices\\.allow:|lxc\\.mount\\.entry:|features:)'"
|
||||
;;
|
||||
11) cmd="ls -l /dev/dri /dev/kfd /dev/nvidia* /dev/apex* 2>/dev/null" ;;
|
||||
12)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot (e.g. 0): ')${CL}"; read -r slot
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter PCI BDF (e.g. 0000:01:00.0): ')${CL}"; read -r bdf
|
||||
cmd="qm set $vmid --hostpci${slot} ${bdf},pcie=1"
|
||||
mode="template"
|
||||
;;
|
||||
13)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot (e.g. 0): ')${CL}"; read -r slot
|
||||
cmd="qm set $vmid -delete hostpci${slot}"
|
||||
mode="template"
|
||||
;;
|
||||
14)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||
cmd="qm set $vmid -onboot 0"
|
||||
mode="template"
|
||||
;;
|
||||
15)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Boot type (grub/zfs): ')${CL}"; read -r boot_type
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'CPU vendor (intel/amd): ')${CL}"; read -r cpu_vendor
|
||||
case "$cpu_vendor" in
|
||||
amd|AMD) iommu_param="amd_iommu=on iommu=pt" ;;
|
||||
*) iommu_param="intel_iommu=on iommu=pt" ;;
|
||||
esac
|
||||
case "$boot_type" in
|
||||
zfs|ZFS) cmd="sed -i 's/\$/ ${iommu_param}/' /etc/kernel/cmdline" ;;
|
||||
*) cmd="sed -i '/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param}\"|' /etc/default/grub" ;;
|
||||
esac
|
||||
mode="template"
|
||||
;;
|
||||
16)
|
||||
cmd="update-initramfs -u -k all && (proxmox-boot-tool refresh || update-grub)"
|
||||
mode="template"
|
||||
;;
|
||||
17) cmd="lsusb | grep -Ei '18d1:9302|1a6e:089a' ; lspci | grep -i 'Global Unichip'" ;;
|
||||
0) break ;;
|
||||
*) cmd="$user_input" ;;
|
||||
*)
|
||||
if [[ -n "$user_input" ]]; then
|
||||
cmd="$user_input"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$mode" == "template" ]]; then
|
||||
echo -e "\n${GREEN}$(translate 'Manual command template (copy/paste):')${NC}\n"
|
||||
echo "$cmd"
|
||||
echo
|
||||
msg_success "$(translate 'Press ENTER to continue...')"
|
||||
read -r tmp
|
||||
continue
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}> $cmd${NC}\n"
|
||||
bash -c "$cmd"
|
||||
echo
|
||||
@@ -913,7 +987,7 @@ show_tools_commands() {
|
||||
while true; do
|
||||
OPTION=$(dialog --stdout \
|
||||
--title "$(translate 'Help and Info')" \
|
||||
--menu "\n$(translate 'Select a category of useful commands:')" 20 70 9 \
|
||||
--menu "$(translate 'Select a category of useful commands:')" 20 70 9 \
|
||||
1 "$(translate 'Useful System Commands')" \
|
||||
2 "$(translate 'VM and CT Management Commands')" \
|
||||
3 "$(translate 'Storage and Disks Commands')" \
|
||||
|
||||
@@ -134,14 +134,21 @@ function start_vm_configuration() {
|
||||
|
||||
while true; do
|
||||
VM_STORAGE_IOMMU_PENDING_REBOOT=0
|
||||
OS_TYPE=$(dialog --backtitle "ProxMenux" \
|
||||
WIZARD_CONFLICT_POLICY=""
|
||||
WIZARD_CONFLICT_SCOPE=""
|
||||
export WIZARD_CONFLICT_POLICY WIZARD_CONFLICT_SCOPE
|
||||
OS_TYPE=$(dialog --colors --backtitle "ProxMenux" \
|
||||
--title "$(translate "Select System Type")" \
|
||||
--menu "\n$(translate "Choose the type of virtual system to install:")" 20 70 10 \
|
||||
1 "$(translate "Create") VM System NAS" \
|
||||
2 "$(translate "Create") VM System Windows" \
|
||||
3 "$(translate "Create") VM System Linux" \
|
||||
"" "" \
|
||||
"" "\Z4──────────────────────────────────────────────────\Zn" \
|
||||
"" "" \
|
||||
4 "$(translate "Create") VM System macOS (OSX-PROXMOX)" \
|
||||
5 "$(translate "Create") VM System Others (based Linux)" \
|
||||
"" "" \
|
||||
6 "$(translate "Return to Main Menu")" \
|
||||
3>&1 1>&2 2>&3)
|
||||
|
||||
|
||||
@@ -27,20 +27,24 @@ initialize_cache
|
||||
while true; do
|
||||
OPTION=$(dialog --colors --backtitle "ProxMenux" \
|
||||
--title "$(translate "GPUs and Coral-TPU Menu")" \
|
||||
--menu "\n$(translate "Select an option:")" 25 80 15 \
|
||||
--menu "\n$(translate "Select an option:")" 26 78 18 \
|
||||
"" "\Z4──────────────────────── 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──────────────────────── LXC ──────────────────────────\Zn" \
|
||||
"4" "$(translate "Add GPU to LXC (Intel | AMD | NVIDIA)") \Zb\Z4Switch Mode\Zn" \
|
||||
"5" "$(translate "Add Coral TPU to LXC")" \
|
||||
"" "" \
|
||||
"" "\Z4──────────────────────── VM ───────────────────────────\Zn" \
|
||||
"6" "$(translate "Add GPU to VM (Intel | AMD | NVIDIA)") \Zb\Z4Switch Mode\Zn" \
|
||||
"" "" \
|
||||
"" "\Z4──────────────────── SWICHT MODE ───────────────────────\Zn" \
|
||||
"7" "$(translate "Switch GPU Mode (VM <-> LXC)")" \
|
||||
"" "" \
|
||||
"" "" \
|
||||
"" "\Z4────────────────────── Utilities ───────────────────────\Zn" \
|
||||
"8" "$(translate "Manual CLI Guide (GPU/TPU)")" \
|
||||
"0" "$(translate "Return to Main Menu")" \
|
||||
2>&1 >/dev/tty
|
||||
) || { exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"; }
|
||||
@@ -67,6 +71,9 @@ while true; do
|
||||
7)
|
||||
bash "$LOCAL_SCRIPTS/gpu_tpu/switch_gpu_mode.sh"
|
||||
;;
|
||||
8)
|
||||
bash "$LOCAL_SCRIPTS/gpu_tpu/gpu-tpu-manual-guide.sh"
|
||||
;;
|
||||
0)
|
||||
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||
;;
|
||||
|
||||
@@ -94,12 +94,12 @@ show_menu() {
|
||||
dialog --clear \
|
||||
--backtitle "ProxMenux" \
|
||||
--title "$(translate "$menu_title")" \
|
||||
--menu "$(translate "Select an option:")" 20 70 11 \
|
||||
--menu "\n$(translate "Select an option:")" 20 70 11 \
|
||||
1 "$(translate "Settings post-install Proxmox")" \
|
||||
2 "$(translate "Hardware: GPUs and Coral-TPU")" \
|
||||
3 "$(translate "Create VM from template or script")" \
|
||||
4 "$(translate "Disk and Storage Manager")" \
|
||||
5 "$(translate "Mount and Share Manager")" \
|
||||
4 "$(translate "Disk Manager")" \
|
||||
5 "$(translate "Storage & Share Manager")" \
|
||||
6 "$(translate "Proxmox VE Helper Scripts")" \
|
||||
7 "$(translate "Network Management")" \
|
||||
8 "$(translate "Security")" \
|
||||
|
||||
@@ -398,7 +398,7 @@ while true; do
|
||||
SELECTED_IDX=$(dialog --backtitle "ProxMenux" \
|
||||
--title "Proxmox VE Helper-Scripts" \
|
||||
--menu "$(translate "Select a category or search for scripts:"):" \
|
||||
20 70 14 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
|
||||
22 75 15 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
|
||||
dialog --clear --title "ProxMenux" \
|
||||
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
|
||||
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||
@@ -440,7 +440,7 @@ while true; do
|
||||
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" \
|
||||
--title "$(translate "Scripts in") ${CATEGORY_NAMES[$SELECTED]}" \
|
||||
--menu "$(translate "Choose a script to execute:"):" \
|
||||
20 70 14 "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
|
||||
22 75 15 "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
|
||||
|
||||
SCRIPT_SELECTED="${INDEX_TO_SLUG[$SCRIPT_INDEX]}"
|
||||
run_script_by_slug "$SCRIPT_SELECTED"
|
||||
|
||||
@@ -26,13 +26,12 @@ initialize_cache
|
||||
security_menu() {
|
||||
while true; do
|
||||
local menu_text
|
||||
menu_text="\n$(translate 'Security tools for hardening and auditing your Proxmox VE system.')\n\n"
|
||||
menu_text+="$(translate 'Select an option:')"
|
||||
menu_text+="\n$(translate 'Select an option:')"
|
||||
|
||||
local OPTION
|
||||
OPTION=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "$SCRIPT_TITLE")" \
|
||||
--menu "$menu_text" 18 70 4 \
|
||||
--menu "$menu_text" 20 70 10 \
|
||||
"1" "$(translate 'Fail2Ban - Intrusion Prevention')" \
|
||||
"2" "$(translate 'Lynis - Security Audit')" \
|
||||
3>&1 1>&2 2>&3) || OPTION="0"
|
||||
|
||||
@@ -26,21 +26,22 @@ initialize_cache
|
||||
|
||||
while true; do
|
||||
OPTION=$(dialog --colors --backtitle "ProxMenux" \
|
||||
--title "$(translate "Mount and Share Manager")" \
|
||||
--menu "\n$(translate "Select an option:")" 25 80 15 \
|
||||
--title "$(translate "Storage & Share Manager")" \
|
||||
--menu "\n$(translate "Select an option:")" 26 78 17 \
|
||||
"" "\Z4──────────────────────── HOST ─────────────────────────\Zn" \
|
||||
"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──────────────────────── LXC ─────────────────────────\Zn" \
|
||||
"4" "$(translate "Configure LXC Mount Points (Host ↔ Container)")" \
|
||||
"4" "$(translate "Add Local Disk as Proxmox Storage")" \
|
||||
"5" "$(translate "Add iSCSI Target as Proxmox Storage")" \
|
||||
"" "" \
|
||||
"5" "$(translate "Configure NFS Client in LXC (only privileged)")" \
|
||||
"6" "$(translate "Configure Samba Client in LXC (only privileged)")" \
|
||||
"7" "$(translate "Configure NFS Server in LXC (only privileged)")" \
|
||||
"8" "$(translate "configure Samba Server in LXC (only privileged)")" \
|
||||
"" "\Z4──────────────────────── LXC ─────────────────────────\Zn" \
|
||||
"6" "$(translate "Configure LXC Mount Points (Host ↔ Container)")" \
|
||||
"" "" \
|
||||
"7" "$(translate "Configure NFS Client in LXC (only privileged)")" \
|
||||
"8" "$(translate "Configure Samba Client in LXC (only privileged)")" \
|
||||
"9" "$(translate "Configure NFS Server in LXC (only privileged)")" \
|
||||
"10" "$(translate "configure Samba Server in LXC (only privileged)")" \
|
||||
"" "" \
|
||||
"h" "$(translate "Help & Info (commands)")" \
|
||||
"0" "$(translate "Return to Main Menu")" \
|
||||
@@ -62,25 +63,25 @@ while true; do
|
||||
3)
|
||||
bash "$LOCAL_SCRIPTS/share/local-shared-manager.sh"
|
||||
;;
|
||||
9)
|
||||
4)
|
||||
bash "$LOCAL_SCRIPTS/share/disk_host.sh"
|
||||
;;
|
||||
10)
|
||||
5)
|
||||
bash "$LOCAL_SCRIPTS/share/iscsi_host.sh"
|
||||
;;
|
||||
4)
|
||||
6)
|
||||
bash "$LOCAL_SCRIPTS/share/lxc-mount-manager_minimal.sh"
|
||||
;;
|
||||
5)
|
||||
7)
|
||||
bash "$LOCAL_SCRIPTS/share/nfs_client.sh"
|
||||
;;
|
||||
6)
|
||||
8)
|
||||
bash "$LOCAL_SCRIPTS/share/samba_client.sh"
|
||||
;;
|
||||
7)
|
||||
9)
|
||||
bash "$LOCAL_SCRIPTS/share/nfs_lxc_server.sh"
|
||||
;;
|
||||
8)
|
||||
10)
|
||||
bash "$LOCAL_SCRIPTS/share/samba_lxc_server.sh"
|
||||
;;
|
||||
h)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : GPL-3.0
|
||||
# Version : 2.0
|
||||
# Last Updated: 06/04/2026
|
||||
# Last Updated: 07/04/2026
|
||||
# ==========================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -30,15 +30,21 @@ initialize_cache
|
||||
|
||||
while true; do
|
||||
OPTION=$(dialog --colors --backtitle "ProxMenux" \
|
||||
--title "$(translate "Disk and Storage Manager Menu")" \
|
||||
--menu "\n$(translate "Select an option:")" 24 84 14 \
|
||||
--title "$(translate "Disk Manager")" \
|
||||
--menu "\n$(translate "Select an option:")" 24 78 16 \
|
||||
"" "\Z4──────────────────────── VM ───────────────────────────\Zn" \
|
||||
"1" "$(translate "Import Disk to VM")" \
|
||||
"2" "$(translate "Import Disk Image to VM")" \
|
||||
"3" "$(translate "Add Controller or NVMe PCIe to VM")" \
|
||||
"" "" \
|
||||
"" "\Z4──────────────────────── LXC ──────────────────────────\Zn" \
|
||||
"4" "$(translate "Import Disk to LXC")" \
|
||||
"" "" \
|
||||
"" "\Z4────────────────────── Utilities ───────────────────────\Zn" \
|
||||
"5" "$(translate "Format / Wipe Physical Disk (Safe)")" \
|
||||
"6" "$(translate "SMART Disk Health & Test")" \
|
||||
"7" "$(translate "Manual CLI Guide (Disk and Storage Manager)")" \
|
||||
"" "" \
|
||||
"0" "$(translate "Return to Main Menu")" \
|
||||
2>&1 >/dev/tty
|
||||
) || { exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"; }
|
||||
@@ -56,6 +62,15 @@ while true; do
|
||||
4)
|
||||
bash "$LOCAL_SCRIPTS/storage/disk-passthrough_ct.sh"
|
||||
;;
|
||||
5)
|
||||
bash "$LOCAL_SCRIPTS/storage/format-disk.sh"
|
||||
;;
|
||||
6)
|
||||
bash "$LOCAL_SCRIPTS/storage/smart-disk-test.sh"
|
||||
;;
|
||||
7)
|
||||
bash "$LOCAL_SCRIPTS/storage/disk-storage-manual-guide.sh"
|
||||
;;
|
||||
0)
|
||||
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||
;;
|
||||
|
||||
@@ -26,12 +26,14 @@ initialize_cache
|
||||
|
||||
while true; do
|
||||
OPTION=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Utilities Menu")" \
|
||||
--menu "$(translate "Select an option:")" 20 70 8 \
|
||||
--menu "\n$(translate "Select an option:")" 20 70 11 \
|
||||
"1" "$(translate "UUp Dump ISO creator Custom")" \
|
||||
"2" "$(translate "System Utilities Installer")" \
|
||||
"3" "$(translate "Proxmox System Update")" \
|
||||
"4" "$(translate "Upgrade PVE 8 to PVE 9")" \
|
||||
"5" "$(translate "Return to Main Menu")" \
|
||||
"5" "$(translate "Export VM to OVA or OVF")" \
|
||||
"6" "$(translate "Import VM from OVA or OVF")" \
|
||||
"7" "$(translate "Return to Main Menu")" \
|
||||
2>&1 >/dev/tty)
|
||||
|
||||
case $OPTION in
|
||||
@@ -76,8 +78,20 @@ initialize_cache
|
||||
return
|
||||
fi
|
||||
;;
|
||||
5) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
||||
5)
|
||||
bash "$LOCAL_SCRIPTS/utilities/export_vm_ova_ovf.sh"
|
||||
if [ $? -ne 0 ]; then
|
||||
return
|
||||
fi
|
||||
;;
|
||||
6)
|
||||
bash "$LOCAL_SCRIPTS/utilities/import_vm_ova_ovf.sh"
|
||||
if [ $? -ne 0 ]; then
|
||||
return
|
||||
fi
|
||||
;;
|
||||
7) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
||||
*) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||
# Version : 1.5
|
||||
# Last Updated: 04/08/2025
|
||||
# Version : 1.6
|
||||
# Last Updated: 07/04/2026
|
||||
# ==========================================================
|
||||
|
||||
# Configuration ============================================
|
||||
@@ -29,11 +29,14 @@ show_command() {
|
||||
local command="$3"
|
||||
local note="$4"
|
||||
local command_extra="$5"
|
||||
|
||||
echo -e "${BGN}${step}.${CL} ${BL}${description}${CL}"
|
||||
|
||||
echo -e " ${DARK_GRAY}────────────────────────────────────────────────${CL}"
|
||||
echo -e " ${BGN}${step}.${CL} ${description}"
|
||||
echo ""
|
||||
while IFS= read -r line; do
|
||||
echo -e "${TAB}${line}"
|
||||
done <<< "$(echo -e "$command")"
|
||||
echo ""
|
||||
echo -e "${TAB}${command}"
|
||||
echo -e
|
||||
[[ -n "$note" ]] && echo -e "${TAB}${DARK_GRAY}${note}${CL}"
|
||||
[[ -n "$command_extra" ]] && echo -e "${TAB}${YW}${command_extra}${CL}"
|
||||
echo ""
|
||||
@@ -43,10 +46,10 @@ show_how_to_enter_lxc() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "How to Access an LXC Terminal from Proxmox Host")"
|
||||
|
||||
|
||||
msg_info2 "$(translate "Use these commands on your Proxmox host to access an LXC container's terminal:")"
|
||||
echo -e
|
||||
|
||||
echo -e
|
||||
|
||||
show_command "1" \
|
||||
"$(translate "Get a list of all your containers:")" \
|
||||
"pct list" \
|
||||
@@ -54,93 +57,203 @@ show_how_to_enter_lxc() {
|
||||
""
|
||||
|
||||
show_command "2" \
|
||||
"$(translate "Enter the container's terminal")" \
|
||||
"$(translate "Enter the container terminal:")" \
|
||||
"pct enter ${CUS}<container-id>${CL}" \
|
||||
"$(translate "Replace <container-id> with the actual ID.")"\
|
||||
"$(translate "Replace <container-id> with the actual ID.")" \
|
||||
"$(translate "For example: pct enter 101")"
|
||||
|
||||
show_command "3" \
|
||||
"$(translate "To exit the container's terminal, press:")" \
|
||||
"CTRL + D" \
|
||||
"" \
|
||||
"$(translate "Exit the container terminal:")" \
|
||||
"exit" \
|
||||
"$(translate "Or press CTRL + D")" \
|
||||
""
|
||||
|
||||
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
}
|
||||
|
||||
show_host_mount_resources_help() {
|
||||
show_host_storage_help() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Mount Remote Resources on Proxmox Host")"
|
||||
|
||||
msg_info2 "$(translate "How to mount NFS and Samba shares directly on the Proxmox host. Proxmox already has the necessary tools installed.")"
|
||||
echo -e
|
||||
msg_title "$(translate "Host Storage (NFS / Samba via Proxmox)")"
|
||||
|
||||
echo -e "${BOLD}${BL}=== MOUNT NFS SHARE ===${CL}"
|
||||
msg_info2 "$(translate "Current ProxMenux host scripts register remote shares as Proxmox storages using pvesm.")"
|
||||
msg_info2 "$(translate "This means Proxmox handles mount lifecycle natively (no manual /etc/fstab needed for NFS/CIFS host storages).")"
|
||||
echo -e
|
||||
|
||||
|
||||
echo -e "${BOLD}${BL}=== NFS AS PROXMOX STORAGE ===${CL}"
|
||||
echo -e
|
||||
|
||||
show_command "1" \
|
||||
"$(translate "Create mount point:")" \
|
||||
"mkdir -p ${CUS}/mnt/nfs_share${CL}" \
|
||||
"$(translate "Replace with your preferred path.")" \
|
||||
""
|
||||
"$(translate "Add NFS storage:")" \
|
||||
"pvesm add nfs ${CUS}<storage-id>${CL} --server ${CUS}<nfs-server-ip>${CL} --export ${CUS}</export/path>${CL} --content ${CUS}import,backup,iso,vztmpl,images,snippets${CL}" \
|
||||
"$(translate "Use content types according to your use case.")" \
|
||||
"$(translate "Example: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup")"
|
||||
|
||||
show_command "2" \
|
||||
"$(translate "Mount NFS share:")" \
|
||||
"mount -t nfs ${CUS}192.168.1.100${CL}:${CUS}/path/to/share${CL} ${CUS}/mnt/nfs_share${CL}" \
|
||||
"$(translate "Replace IP and paths with your values.")" \
|
||||
"$(translate "List configured storages:")" \
|
||||
"pvesm status" \
|
||||
"$(translate "Shows status and type (nfs/cifs/dir/iscsi...).")" \
|
||||
""
|
||||
|
||||
show_command "3" \
|
||||
"$(translate "Make permanent (optional):")" \
|
||||
"echo '${CUS}192.168.1.100${CL}:${CUS}/path/to/share${CL} ${CUS}/mnt/nfs_share${CL} nfs4 rw,hard,intr,_netdev,rsize=1048576,wsize=1048576,timeo=600,retrans=2 0 0' >> /etc/fstab" \
|
||||
"$(translate "_netdev waits for network before mounting.")" \
|
||||
"$(translate "Remove NFS storage:")" \
|
||||
"pvesm remove ${CUS}<storage-id>${CL}" \
|
||||
"$(translate "Only removes storage definition, not remote data.")" \
|
||||
""
|
||||
|
||||
echo -e "${BOLD}${BL}=== MOUNT SAMBA SHARE ===${CL}"
|
||||
echo -e "${BOLD}${BL}=== SAMBA/CIFS AS PROXMOX STORAGE ===${CL}"
|
||||
echo -e
|
||||
|
||||
show_command "4" \
|
||||
"$(translate "Create mount point:")" \
|
||||
"mkdir -p ${CUS}/mnt/samba_share${CL}" \
|
||||
"$(translate "Replace with your preferred path.")" \
|
||||
"$(translate "Add CIFS storage:")" \
|
||||
"pvesm add cifs ${CUS}<storage-id>${CL} --server ${CUS}<samba-server-ip>${CL} --share ${CUS}<share-name>${CL} --username ${CUS}<user>${CL} --password ${CUS}<pass>${CL} --content ${CUS}import,backup,iso,vztmpl,images,snippets${CL}" \
|
||||
"$(translate "For guest shares add: --options guest")" \
|
||||
""
|
||||
|
||||
show_command "5" \
|
||||
"$(translate "Mount Samba share:")" \
|
||||
"mount -t cifs //${CUS}192.168.1.100${CL}/${CUS}sharename${CL} ${CUS}/mnt/samba_share${CL} -o username=${CUS}user${CL}" \
|
||||
"$(translate "You will be prompted for password. Replace IP, share and user.")" \
|
||||
"$(translate "Inspect storage config block:")" \
|
||||
"sed -n '/^${CUS}<storage-id>${CL}:/,/^[^ ]/p' /etc/pve/storage.cfg" \
|
||||
"$(translate "Useful to verify options/content after script execution.")" \
|
||||
""
|
||||
|
||||
show_command "6" \
|
||||
"$(translate "Make permanent (optional):")" \
|
||||
"echo '//${CUS}192.168.1.100${CL}/${CUS}sharename${CL} ${CUS}/mnt/samba_share${CL} cifs username=${CUS}user${CL},password=${CUS}pass${CL},_netdev 0 0' >> /etc/fstab" \
|
||||
"$(translate "Replace with your credentials.")" \
|
||||
"$(translate "Remove CIFS storage:")" \
|
||||
"pvesm remove ${CUS}<storage-id>${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
echo -e "${BOLD}${BL}=== CREATE LOCAL DIRECTORY ===${CL}"
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
}
|
||||
|
||||
show_local_share_help() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Local Shared Directory on Host")"
|
||||
|
||||
msg_info2 "$(translate "Equivalent manual flow used by Local Shared Manager.")"
|
||||
msg_info2 "$(translate "No group creation required — uses world-writable sticky bit permissions.")"
|
||||
echo -e
|
||||
|
||||
show_command "7" \
|
||||
"$(translate "Create directory:")" \
|
||||
"mkdir -p ${CUS}/mnt/local_share${CL}" \
|
||||
"$(translate "Creates a local directory on Proxmox host.")" \
|
||||
show_command "1" \
|
||||
"$(translate "Create shared directory:")" \
|
||||
"mkdir -p ${CUS}/mnt/shared${CL}" \
|
||||
"$(translate "Choose any host path you want to share with CTs.")" \
|
||||
""
|
||||
|
||||
show_command "8" \
|
||||
"$(translate "Set permissions:")" \
|
||||
"chmod 755 ${CUS}/mnt/local_share${CL}" \
|
||||
"$(translate "Sets basic read/write permissions.")" \
|
||||
show_command "2" \
|
||||
"$(translate "Set ownership and permissions:")" \
|
||||
"chown root:root ${CUS}/mnt/shared${CL}\nchmod 1777 ${CUS}/mnt/shared${CL}" \
|
||||
"$(translate "1777 = sticky bit + rwx for all. No shared group needed.")" \
|
||||
""
|
||||
|
||||
show_command "9" \
|
||||
"$(translate "Verify mounts:")" \
|
||||
"df -h" \
|
||||
"$(translate "Shows all mounted filesystems.")" \
|
||||
show_command "3" \
|
||||
"$(translate "Optional: apply default ACL so new files inherit permissions:")" \
|
||||
"setfacl -R -m d:u::rwx,d:g::rwx,d:o::rwx,m::rwx ${CUS}/mnt/shared${CL}" \
|
||||
"$(translate "Requires acl package. Skip if setfacl is not available.")" \
|
||||
""
|
||||
|
||||
|
||||
show_command "4" \
|
||||
"$(translate "Optional: register this path as Proxmox dir storage:")" \
|
||||
"pvesm add dir ${CUS}<storage-id>${CL} --path ${CUS}/mnt/shared${CL} --content ${CUS}backup,iso,vztmpl,snippets${CL}" \
|
||||
"$(translate "Use images only if the directory is on suitable storage.")" \
|
||||
""
|
||||
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
}
|
||||
|
||||
show_disk_host_help() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Add Local Disk as Proxmox Storage")"
|
||||
|
||||
msg_info2 "$(translate "Equivalent manual flow of disk_host.sh: partition, format, mount, persist, register in Proxmox.")"
|
||||
echo -e
|
||||
|
||||
show_command "1" \
|
||||
"$(translate "Identify candidate disk (never use system disk):")" \
|
||||
"lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL" \
|
||||
"$(translate "Example target: /dev/sdb")" \
|
||||
""
|
||||
|
||||
show_command "2" \
|
||||
"$(translate "Wipe old signatures and partition table (DESTRUCTIVE):")" \
|
||||
"wipefs -a ${CUS}/dev/sdb${CL}\nsgdisk --zap-all ${CUS}/dev/sdb${CL}" \
|
||||
"$(translate "This erases existing metadata.")" \
|
||||
""
|
||||
|
||||
show_command "3" \
|
||||
"$(translate "Create GPT and one partition:")" \
|
||||
"parted -s ${CUS}/dev/sdb${CL} mklabel gpt\nparted -s ${CUS}/dev/sdb${CL} mkpart primary 0% 100%" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "4" \
|
||||
"$(translate "Format partition:")" \
|
||||
"mkfs.ext4 -F ${CUS}/dev/sdb1${CL}\n# or\nmkfs.xfs -f ${CUS}/dev/sdb1${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "5" \
|
||||
"$(translate "Mount and persist with UUID:")" \
|
||||
"mkdir -p ${CUS}/mnt/disk-sdb${CL}\nmount ${CUS}/dev/sdb1${CL} ${CUS}/mnt/disk-sdb${CL}\nblkid ${CUS}/dev/sdb1${CL}\n# Add UUID line to /etc/fstab" \
|
||||
"$(translate "Using UUID is recommended over /dev/sdX.")" \
|
||||
""
|
||||
|
||||
show_command "6" \
|
||||
"$(translate "Register mount path in Proxmox:")" \
|
||||
"pvesm add dir ${CUS}<storage-id>${CL} --path ${CUS}/mnt/disk-sdb${CL} --content ${CUS}images,backup${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
}
|
||||
|
||||
show_iscsi_host_help() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Add iSCSI Target as Proxmox Storage")"
|
||||
|
||||
msg_info2 "$(translate "Equivalent manual flow of iscsi_host.sh.")"
|
||||
echo -e
|
||||
|
||||
show_command "1" \
|
||||
"$(translate "Install and start iSCSI initiator:")" \
|
||||
"apt-get update && apt-get install -y open-iscsi\nsystemctl enable --now iscsid" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "2" \
|
||||
"$(translate "Discover targets on portal:")" \
|
||||
"iscsiadm -m discovery -t sendtargets -p ${CUS}<portal-ip>:3260${CL}" \
|
||||
"$(translate "This returns available IQNs.")" \
|
||||
""
|
||||
|
||||
show_command "3" \
|
||||
"$(translate "Add iSCSI storage in Proxmox:")" \
|
||||
"pvesm add iscsi ${CUS}<storage-id>${CL} --portal ${CUS}<portal-ip>:3260${CL} --target ${CUS}<target-iqn>${CL} --content images" \
|
||||
"$(translate "Content is usually images for VM block devices.")" \
|
||||
""
|
||||
|
||||
show_command "4" \
|
||||
"$(translate "Verify iSCSI sessions and storage status:")" \
|
||||
"iscsiadm -m session\npvesm status" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "5" \
|
||||
"$(translate "Remove iSCSI storage definition:")" \
|
||||
"pvesm remove ${CUS}<storage-id>${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
@@ -149,42 +262,42 @@ show_host_mount_resources_help() {
|
||||
show_host_to_lxc_mount_help() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Mount Host Directory to LXC Container")"
|
||||
|
||||
msg_info2 "$(translate "How to mount a Proxmox host directory into an LXC container. Execute these commands on the Proxmox host.")"
|
||||
echo -e
|
||||
|
||||
msg_title "$(translate "Host Directory to LXC Mount Point")"
|
||||
|
||||
msg_info2 "$(translate "Current script uses native bind mounts with pct set -mpX.")"
|
||||
msg_info2 "$(translate "Safe design: no automatic ACL/ownership mutation on host or CT.")"
|
||||
echo -e
|
||||
|
||||
show_command "1" \
|
||||
"$(translate "Add mount point to container:")" \
|
||||
"pct set ${CUS}<container-id>${CL} -mp0 ${CUS}/host/directory${CL},mp=${CUS}/container/path${CL},backup=0,shared=1" \
|
||||
"$(translate "Replace container-id, host directory and container path.")" \
|
||||
"$(translate "Example: pct set 101 -mp0 /mnt/shared,mp=/mnt/shared,,backup=0,shared=1")"
|
||||
"$(translate "List containers:")" \
|
||||
"pct list" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "2" \
|
||||
"$(translate "Restart container:")" \
|
||||
"pct reboot ${CUS}<container-id>${CL}" \
|
||||
"$(translate "Required to activate the mount point.")" \
|
||||
"$(translate "Add bind mount to container:")" \
|
||||
"pct set ${CUS}<ctid>${CL} -mp0 ${CUS}/host/path${CL},mp=${CUS}/container/path${CL},backup=0,shared=1" \
|
||||
"$(translate "Use mp1/mp2/... for extra mount points.")" \
|
||||
""
|
||||
|
||||
show_command "3" \
|
||||
"$(translate "Verify mount inside container:")" \
|
||||
"pct enter ${CUS}<container-id>${CL}
|
||||
df -h | grep ${CUS}/container/path${CL}" \
|
||||
"$(translate "Check if the directory is mounted.")" \
|
||||
"$(translate "Check resulting config:")" \
|
||||
"pct config ${CUS}<ctid>${CL} | grep '^mp'" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "4" \
|
||||
"$(translate "Remove mount point (if needed):")" \
|
||||
"pct set ${CUS}<container-id>${CL} --delete mp0" \
|
||||
"$(translate "Removes the mount point. Use mp1, mp2, etc. for other mounts.")" \
|
||||
"$(translate "Remove mount point:")" \
|
||||
"pct set ${CUS}<ctid>${CL} --delete mp0" \
|
||||
"" \
|
||||
""
|
||||
|
||||
echo -e "${BOR}"
|
||||
echo -e "${BOLD}$(translate "Notes:")${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Mount indices:")${CL} ${BL}Use mp0, mp1, mp2, etc. for multiple mounts${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Permissions:")${CL} ${BL}May need adjustment depending on directory type${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Container types:")${CL} ${BL}Works with both privileged and unprivileged containers${CL}"
|
||||
|
||||
|
||||
show_command "5" \
|
||||
"$(translate "Verify inside container:")" \
|
||||
"pct enter ${CUS}<ctid>${CL}\ndf -h" \
|
||||
"$(translate "Confirm the mount path is visible.")" \
|
||||
""
|
||||
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
@@ -193,67 +306,41 @@ show_host_to_lxc_mount_help() {
|
||||
show_nfs_server_help() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "NFS Server Installation")"
|
||||
|
||||
msg_info2 "$(translate "How to install and configure an NFS server in an LXC container.")"
|
||||
echo -e
|
||||
|
||||
msg_title "$(translate "NFS Server in LXC (Privileged)")"
|
||||
|
||||
msg_warn "$(translate "Use a privileged LXC for NFS server/client workflows.")"
|
||||
echo -e
|
||||
|
||||
show_command "1" \
|
||||
"$(translate "Update and install packages:")" \
|
||||
"apt-get update && apt-get install -y nfs-kernel-server" \
|
||||
"$(translate "Install server packages inside CT:")" \
|
||||
"apt-get update && apt-get install -y nfs-kernel-server nfs-common rpcbind" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "2" \
|
||||
"$(translate "Create export directory:")" \
|
||||
"mkdir -p ${CUS}/mnt/nfs_export${CL}" \
|
||||
"$(translate "Replace with your preferred path.")" \
|
||||
"mkdir -p ${CUS}/mnt/nfs_export${CL}\nchmod 755 ${CUS}/mnt/nfs_export${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
|
||||
show_command "3" \
|
||||
"$(translate "Set directory permissions:")" \
|
||||
"chmod 755 ${CUS}/mnt/nfs_export${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "4.1" \
|
||||
"$(translate "Configure exports (safe root_squash):")" \
|
||||
"$(translate "Add export rule:")" \
|
||||
"echo '${CUS}/mnt/nfs_export${CL} ${CUS}192.168.1.0/24${CL}(rw,sync,no_subtree_check,root_squash)' >> /etc/exports" \
|
||||
"$(translate "Replace directory path and network range.")" \
|
||||
"$(translate "Adjust network/CIDR to your environment.")" \
|
||||
""
|
||||
|
||||
show_command "4.2" \
|
||||
"$(translate "Or Configure exports (map all users):")" \
|
||||
"echo '${CUS}/mnt/nfs_export${CL} ${CUS}192.168.1.0/24${CL}(rw,sync,no_subtree_check,all_squash,anonuid=0,anongid=0)' >> /etc/exports" \
|
||||
"$(translate "Replace directory path and network range.")" \
|
||||
show_command "4" \
|
||||
"$(translate "Apply and restart services:")" \
|
||||
"exportfs -ra\nsystemctl restart rpcbind nfs-kernel-server\nsystemctl enable rpcbind nfs-kernel-server" \
|
||||
"" \
|
||||
""
|
||||
|
||||
|
||||
show_command "5" \
|
||||
"$(translate "Apply configuration:")" \
|
||||
"exportfs -ra" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "6" \
|
||||
"$(translate "Start and enable service:")" \
|
||||
"systemctl restart nfs-kernel-server
|
||||
systemctl enable nfs-kernel-server" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "7" \
|
||||
"$(translate "Verify exports:")" \
|
||||
"$(translate "Verify active exports:")" \
|
||||
"showmount -e localhost" \
|
||||
"$(translate "Shows available NFS exports.")" \
|
||||
"" \
|
||||
""
|
||||
|
||||
echo -e "${BOR}"
|
||||
echo -e "${BOLD}$(translate "Export Options:")${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "rw:")${CL} ${BL}Read-write access${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "sync:")${CL} ${BL}Synchronous writes${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "no_subtree_check:")${CL} ${BL}Improves performance${CL}"
|
||||
|
||||
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
@@ -262,67 +349,47 @@ show_nfs_server_help() {
|
||||
show_samba_server_help() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Samba Server Installation")"
|
||||
|
||||
msg_info2 "$(translate "How to install and configure a Samba server in an LXC container.")"
|
||||
msg_title "$(translate "Samba Server in LXC (Privileged)")"
|
||||
|
||||
msg_warn "$(translate "Use a privileged LXC for Samba client/server workflows.")"
|
||||
echo -e
|
||||
|
||||
|
||||
show_command "1" \
|
||||
"$(translate "Update and install packages:")" \
|
||||
"apt-get update && apt-get install -y samba" \
|
||||
"$(translate "Install Samba inside CT:")" \
|
||||
"apt-get update && apt-get install -y samba samba-common-bin acl" \
|
||||
"" \
|
||||
""
|
||||
|
||||
|
||||
show_command "2" \
|
||||
"$(translate "Create share directory:")" \
|
||||
"mkdir -p ${CUS}/mnt/samba_share${CL}" \
|
||||
"$(translate "Replace with your preferred path.")" \
|
||||
"mkdir -p ${CUS}/mnt/samba_share${CL}\nchmod 755 ${CUS}/mnt/samba_share${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
|
||||
show_command "3" \
|
||||
"$(translate "Set directory permissions:")" \
|
||||
"chmod 755 ${CUS}/mnt/samba_share${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "4" \
|
||||
"$(translate "Create Samba user:")" \
|
||||
"adduser ${CUS}sambauser${CL}
|
||||
smbpasswd -a ${CUS}sambauser${CL}" \
|
||||
"$(translate "Replace with your username. You'll be prompted for password.")" \
|
||||
""
|
||||
|
||||
show_command "5" \
|
||||
"$(translate "Configure share:")" \
|
||||
"cat >> /etc/samba/smb.conf << EOF
|
||||
[shared]
|
||||
comment = Shared folder
|
||||
path = ${CUS}/mnt/samba_share${CL}
|
||||
read only = no
|
||||
browseable = yes
|
||||
valid users = ${CUS}sambauser${CL}
|
||||
EOF" \
|
||||
"$(translate "Replace path and username.")" \
|
||||
""
|
||||
|
||||
show_command "6" \
|
||||
"$(translate "Restart and enable service:")" \
|
||||
"systemctl restart smbd
|
||||
systemctl enable smbd" \
|
||||
"adduser ${CUS}sambauser${CL}\nsmbpasswd -a ${CUS}sambauser${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "7" \
|
||||
"$(translate "Test configuration:")" \
|
||||
"smbclient -L localhost -U ${CUS}sambauser${CL}" \
|
||||
"$(translate "Lists available shares. You'll be prompted for password.")" \
|
||||
|
||||
show_command "4" \
|
||||
"$(translate "Add share block in /etc/samba/smb.conf:")" \
|
||||
"cat >> /etc/samba/smb.conf << 'EOF'\n[shared]\n path = /mnt/samba_share\n browseable = yes\n read only = no\n valid users = sambauser\nEOF" \
|
||||
"" \
|
||||
""
|
||||
|
||||
echo -e "${BOR}"
|
||||
echo -e "${BOLD}$(translate "Connection Examples:")${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Windows:")${CL} ${YW}\\\\<server-ip>\\shared${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Linux:")${CL} ${YW}smbclient //server-ip/shared -U sambauser${CL}"
|
||||
|
||||
|
||||
show_command "5" \
|
||||
"$(translate "Restart and enable Samba:")" \
|
||||
"systemctl restart smbd\nsystemctl enable smbd" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "6" \
|
||||
"$(translate "Test share visibility:")" \
|
||||
"smbclient -L localhost -U ${CUS}sambauser${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
@@ -331,47 +398,41 @@ EOF" \
|
||||
show_nfs_client_help() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "NFS Client Configuration")"
|
||||
|
||||
msg_info2 "$(translate "How to configure an NFS client in an LXC container.")"
|
||||
msg_title "$(translate "NFS Client in LXC (Privileged)")"
|
||||
|
||||
msg_warn "$(translate "Current NFS client script supports privileged LXC only.")"
|
||||
echo -e
|
||||
|
||||
|
||||
show_command "1" \
|
||||
"$(translate "Update and install packages:")" \
|
||||
"$(translate "Install NFS client packages inside CT:")" \
|
||||
"apt-get update && apt-get install -y nfs-common" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "2" \
|
||||
"$(translate "Create mount point:")" \
|
||||
"mkdir -p ${CUS}/mnt/nfsmount${CL}" \
|
||||
"$(translate "Replace with your preferred path.")" \
|
||||
"mkdir -p ${CUS}/mnt/nfs_share${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "3" \
|
||||
"$(translate "Mount NFS share:")" \
|
||||
"mount -t nfs ${CUS}192.168.1.100${CL}:${CUS}/mnt/nfs_export${CL} ${CUS}/mnt/nfsmount${CL}" \
|
||||
"$(translate "Replace server IP and paths.")" \
|
||||
"mount -t nfs ${CUS}<server-ip>:/export/path${CL} ${CUS}/mnt/nfs_share${CL}" \
|
||||
"$(translate "Adjust options if needed (vers=4,hard,timeo,...).")" \
|
||||
""
|
||||
|
||||
show_command "4" \
|
||||
"$(translate "Test access:")" \
|
||||
"ls -la ${CUS}/mnt/nfsmount${CL}" \
|
||||
"$(translate "Verify you can access the mounted share.")" \
|
||||
"$(translate "Persist mount in CT /etc/fstab (optional):")" \
|
||||
"echo '${CUS}<server-ip>:/export/path${CL} ${CUS}/mnt/nfs_share${CL} nfs defaults,_netdev,x-systemd.automount,noauto 0 0' >> /etc/fstab" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "5" \
|
||||
"$(translate "Make permanent (optional):")" \
|
||||
"echo '${CUS}192.168.1.100${CL}:${CUS}/path/to/share${CL} ${CUS}/mnt/nfs_share${CL} nfs4 rw,hard,intr,_netdev,rsize=1048576,wsize=1048576,timeo=600,retrans=2 0 0' >> /etc/fstab" \
|
||||
"$(translate "Replace with your server IP and paths.")" \
|
||||
""
|
||||
|
||||
show_command "6" \
|
||||
"$(translate "Verify mount:")" \
|
||||
"df -h | grep nfs" \
|
||||
"$(translate "Shows NFS mounts.")" \
|
||||
"mount | grep nfs\ndf -h | grep nfs" \
|
||||
"" \
|
||||
""
|
||||
|
||||
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
@@ -380,63 +441,47 @@ show_nfs_client_help() {
|
||||
show_samba_client_help() {
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Samba Client Configuration")"
|
||||
|
||||
msg_info2 "$(translate "How to configure a Samba client in an LXC container.")"
|
||||
msg_title "$(translate "Samba Client in LXC (Privileged)")"
|
||||
|
||||
msg_warn "$(translate "Current Samba client script supports privileged LXC only.")"
|
||||
echo -e
|
||||
|
||||
|
||||
show_command "1" \
|
||||
"$(translate "Update and install packages:")" \
|
||||
"$(translate "Install CIFS client packages inside CT:")" \
|
||||
"apt-get update && apt-get install -y cifs-utils" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "2" \
|
||||
"$(translate "Create mount point:")" \
|
||||
"mkdir -p ${CUS}/mnt/sambamount${CL}" \
|
||||
"$(translate "Replace with your preferred path.")" \
|
||||
"mkdir -p ${CUS}/mnt/samba_share${CL}" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "3" \
|
||||
"$(translate "Mount Samba share:")" \
|
||||
"mount -t cifs //${CUS}192.168.1.100${CL}/${CUS}shared${CL} ${CUS}/mnt/sambamount${CL} -o username=${CUS}sambauser${CL}" \
|
||||
"$(translate "Replace server IP, share name and username. You'll be prompted for password.")" \
|
||||
"$(translate "Create credentials file (recommended):")" \
|
||||
"cat > /etc/samba/credentials/proxmenux.cred << 'EOF'\nusername=${CUS}<user>${CL}\npassword=${CUS}<pass>${CL}\nEOF\nchmod 600 /etc/samba/credentials/proxmenux.cred" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "4" \
|
||||
"$(translate "Test access:")" \
|
||||
"ls -la ${CUS}/mnt/sambamount${CL}" \
|
||||
"$(translate "Verify you can access the mounted share.")" \
|
||||
"$(translate "Mount CIFS share:")" \
|
||||
"mount -t cifs //${CUS}<server-ip>/<share>${CL} ${CUS}/mnt/samba_share${CL} -o credentials=/etc/samba/credentials/proxmenux.cred,iocharset=utf8,file_mode=0664,dir_mode=0775" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "5" \
|
||||
"$(translate "Create credentials file (optional):")" \
|
||||
"cat > /etc/samba/credentials << EOF
|
||||
username=${CUS}sambauser${CL}
|
||||
password=${CUS}your_password${CL}
|
||||
EOF
|
||||
chmod 600 /etc/samba/credentials" \
|
||||
"$(translate "Secure way to store credentials.")" \
|
||||
"$(translate "Persist mount in CT /etc/fstab (optional):")" \
|
||||
"echo '//${CUS}<server-ip>/<share>${CL} ${CUS}/mnt/samba_share${CL} cifs credentials=/etc/samba/credentials/proxmenux.cred,_netdev,x-systemd.automount,noauto 0 0' >> /etc/fstab" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "6" \
|
||||
"$(translate "Mount with credentials file:")" \
|
||||
"mount -t cifs //${CUS}192.168.1.100${CL}/${CUS}shared${CL} ${CUS}/mnt/sambamount${CL} -o credentials=/etc/samba/credentials" \
|
||||
"$(translate "No password prompt needed.")" \
|
||||
"$(translate "Verify mount:")" \
|
||||
"mount -t cifs\ndf -h | grep cifs" \
|
||||
"" \
|
||||
""
|
||||
|
||||
show_command "7" \
|
||||
"$(translate "Make permanent (optional):")" \
|
||||
"echo '//${CUS}192.168.1.100${CL}/${CUS}shared${CL} ${CUS}/mnt/sambamount${CL} cifs credentials=/etc/samba/credentials,_netdev 0 0' >> /etc/fstab" \
|
||||
"$(translate "Replace with your values.")" \
|
||||
""
|
||||
|
||||
show_command "8" \
|
||||
"$(translate "Verify mount:")" \
|
||||
"df -h | grep cifs" \
|
||||
"$(translate "Shows CIFS/Samba mounts.")" \
|
||||
""
|
||||
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
@@ -445,28 +490,35 @@ chmod 600 /etc/samba/credentials" \
|
||||
show_help_menu() {
|
||||
while true; do
|
||||
CHOICE=$(dialog --title "$(translate "Help & Information")" \
|
||||
--menu "$(translate "Select help topic:")" 24 80 14 \
|
||||
--menu "$(translate "Select help topic:")" 24 90 14 \
|
||||
"0" "$(translate "How to Access an LXC Terminal")" \
|
||||
"1" "$(translate "Mount Remote Resources on Proxmox Host")" \
|
||||
"2" "$(translate "Mount Host Directory to LXC Container")" \
|
||||
"3" "$(translate "NFS Server Installation")" \
|
||||
"4" "$(translate "Samba Server Installation")" \
|
||||
"5" "$(translate "NFS Client Configuration")" \
|
||||
"6" "$(translate "Samba Client Configuration")" \
|
||||
"7" "$(translate "Return to Main Menu")" \
|
||||
"1" "$(translate "Host NFS/Samba as Proxmox Storage (pvesm)")" \
|
||||
"2" "$(translate "Local Shared Directory on Host")" \
|
||||
"3" "$(translate "Add Local Disk as Proxmox Storage")" \
|
||||
"4" "$(translate "Add iSCSI Target as Proxmox Storage")" \
|
||||
"5" "$(translate "Mount Host Directory to LXC Container")" \
|
||||
"6" "$(translate "NFS Client in LXC (privileged)")" \
|
||||
"7" "$(translate "Samba Client in LXC (privileged)")" \
|
||||
"8" "$(translate "NFS Server in LXC (privileged)")" \
|
||||
"9" "$(translate "Samba Server in LXC (privileged)")" \
|
||||
"10" "$(translate "Return to Share Menu")" \
|
||||
3>&1 1>&2 2>&3)
|
||||
|
||||
case $CHOICE in
|
||||
|
||||
case "$CHOICE" in
|
||||
0) show_how_to_enter_lxc ;;
|
||||
1) show_host_mount_resources_help ;;
|
||||
2) show_host_to_lxc_mount_help ;;
|
||||
3) show_nfs_server_help ;;
|
||||
4) show_samba_server_help ;;
|
||||
5) show_nfs_client_help ;;
|
||||
6) show_samba_client_help ;;
|
||||
7) return ;;
|
||||
1) show_host_storage_help ;;
|
||||
2) show_local_share_help ;;
|
||||
3) show_disk_host_help ;;
|
||||
4) show_iscsi_host_help ;;
|
||||
5) show_host_to_lxc_mount_help ;;
|
||||
6) show_nfs_client_help ;;
|
||||
7) show_samba_client_help ;;
|
||||
8) show_nfs_server_help ;;
|
||||
9) show_samba_server_help ;;
|
||||
10) return ;;
|
||||
*) return ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
show_help_menu
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,6 @@ ensure_iscsi_tools() {
|
||||
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
|
||||
}
|
||||
@@ -65,10 +64,9 @@ ensure_iscsi_tools() {
|
||||
# ==========================================================
|
||||
|
||||
select_iscsi_portal() {
|
||||
ISCSI_PORTAL=$(whiptail --inputbox \
|
||||
ISCSI_PORTAL=$(dialog --backtitle "ProxMenux" --title "$(translate "iSCSI Portal")" --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)
|
||||
14 65 3>&1 1>&2 2>&3)
|
||||
[[ $? -ne 0 || -z "$ISCSI_PORTAL" ]] && return 1
|
||||
|
||||
# Normalise: if no port specified, add default 3260
|
||||
|
||||
@@ -6,82 +6,236 @@
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : MIT
|
||||
# Version : 1.0
|
||||
# Last Updated: $(date +%d/%m/%Y)
|
||||
# Last Updated: 08/04/2026
|
||||
# ==========================================================
|
||||
|
||||
# Configuration
|
||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
|
||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||
elif [[ ! -f "$UTILS_FILE" ]]; then
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
fi
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
|
||||
|
||||
SHARE_COMMON_FILE="$LOCAL_SCRIPTS/global/share-common.func"
|
||||
if ! source "$SHARE_COMMON_FILE" 2>/dev/null; then
|
||||
SHARE_COMMON_LOADED=false
|
||||
else
|
||||
SHARE_COMMON_LOADED=true
|
||||
msg_error "$(translate "Could not load shared functions. Script cannot continue.")"
|
||||
exit 1
|
||||
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
|
||||
|
||||
# ==========================================================
|
||||
|
||||
|
||||
|
||||
|
||||
create_shared_directory() {
|
||||
SHARED_DIR=$(pmx_select_host_mount_point "$(translate "Select Shared Directory Location")" "/mnt/shared")
|
||||
[[ -z "$SHARED_DIR" ]] && return
|
||||
lsm_apply_multi_unpriv_permissions() {
|
||||
local dir="$1"
|
||||
|
||||
[[ -z "$dir" || ! -d "$dir" ]] && return 1
|
||||
|
||||
if [[ -d "$SHARED_DIR" ]]; then
|
||||
if ! whiptail --yesno "$(translate "Directory already exists. Continue with permission setup?")" 10 70 --title "$(translate "Directory Exists")"; then
|
||||
return
|
||||
# root:root ownership — no new group needed.
|
||||
chown root:root "$dir" 2>/dev/null || true
|
||||
|
||||
# 1777 = sticky bit (prevents cross-container file deletion) + world-rwx.
|
||||
# Unprivileged LXC UIDs (100000+) appear as 'others' on the host,
|
||||
# so 'o+rwx' is what grants them read+write access.
|
||||
chmod 1777 "$dir" 2>/dev/null || true
|
||||
|
||||
# Ensure existing content is readable/writable regardless of UID mapping.
|
||||
chmod -R a+rwX "$dir" 2>/dev/null || true
|
||||
find "$dir" -type d -exec chmod 1777 {} + 2>/dev/null || true
|
||||
|
||||
if command -v setfacl >/dev/null 2>&1; then
|
||||
# Remove restrictive ACLs and enforce permissive inheritance for new files.
|
||||
setfacl -b -R "$dir" 2>/dev/null || true
|
||||
setfacl -R -m u::rwx,g::rwx,o::rwx,m::rwx "$dir" 2>/dev/null || true
|
||||
setfacl -R -m d:u::rwx,d:g::rwx,d:o::rwx,d:m::rwx "$dir" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Returns a free name like /mnt/shared, /mnt/shared2, /mnt/shared3 …
|
||||
lsm_next_free_name() {
|
||||
local base="${1:-shared}"
|
||||
local candidate="/mnt/$base"
|
||||
[[ ! -d "$candidate" ]] && echo "$candidate" && return
|
||||
local n=2
|
||||
while [[ -d "/mnt/${base}${n}" ]]; do
|
||||
((n++))
|
||||
done
|
||||
echo "/mnt/${base}${n}"
|
||||
}
|
||||
|
||||
lsm_list_mnt_folders() {
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Folders in /mnt")"
|
||||
|
||||
echo "=================================================="
|
||||
|
||||
if [[ ! -d /mnt ]] || [[ -z "$(ls -A /mnt 2>/dev/null)" ]]; then
|
||||
echo ""
|
||||
echo -e "${TAB}$(translate "No folders found in /mnt.")"
|
||||
else
|
||||
local found=false
|
||||
while IFS= read -r dir; do
|
||||
[[ ! -d "$dir" ]] && continue
|
||||
found=true
|
||||
local perms owner
|
||||
perms=$(stat -c "%a" "$dir" 2>/dev/null)
|
||||
owner=$(stat -c "%U:%G" "$dir" 2>/dev/null)
|
||||
echo ""
|
||||
echo -e "${TAB}${BGN}$(translate "Directory:")${CL} ${BL}$dir${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Permissions:")${CL} ${BL}${perms} $(stat -c "(%A)" "$dir" 2>/dev/null)${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Owner:")${CL} ${BL}${owner}${CL}"
|
||||
done < <(find /mnt -mindepth 1 -maxdepth 1 -type d | sort)
|
||||
|
||||
if [[ "$found" = false ]]; then
|
||||
echo ""
|
||||
echo -e "${TAB}$(translate "No folders found in /mnt.")"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
|
||||
SHARE_GROUP=$(pmx_choose_or_create_group "sharedfiles") || return 1
|
||||
SHARE_GID=$(pmx_ensure_host_group "$SHARE_GROUP" 101000) || return 1
|
||||
|
||||
|
||||
if command -v setfacl >/dev/null 2>&1; then
|
||||
setfacl -k /mnt 2>/dev/null || true
|
||||
setfacl -b /mnt 2>/dev/null || true
|
||||
fi
|
||||
chmod 755 /mnt 2>/dev/null || true
|
||||
|
||||
|
||||
pmx_prepare_host_shared_dir "$SHARED_DIR" "$SHARE_GROUP" || return 1
|
||||
|
||||
|
||||
if command -v setfacl >/dev/null 2>&1; then
|
||||
setfacl -b -R "$SHARED_DIR" 2>/dev/null || true
|
||||
# Summary of /mnt available space
|
||||
if mountpoint -q /mnt 2>/dev/null || [[ -d /mnt ]]; then
|
||||
local mnt_avail mnt_total
|
||||
mnt_avail=$(df -h /mnt 2>/dev/null | awk 'NR==2{print $4}')
|
||||
mnt_total=$(df -h /mnt 2>/dev/null | awk 'NR==2{print $2}')
|
||||
if [[ -n "$mnt_avail" ]]; then
|
||||
echo -e "${TAB}${BGN}$(translate "Available space in /mnt:")${CL} ${BL}${mnt_avail} $(translate "of") ${mnt_total}${CL}"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
}
|
||||
|
||||
chown root:"$SHARE_GROUP" "$SHARED_DIR"
|
||||
chmod 2775 "$SHARED_DIR"
|
||||
# Result is stored in LSM_SELECTED_MOUNT_POINT (not echoed) to avoid subshell issues
|
||||
LSM_SELECTED_MOUNT_POINT=""
|
||||
|
||||
pmx_share_map_set "$SHARED_DIR" "$SHARE_GROUP"
|
||||
lsm_select_host_mount_point_dialog() {
|
||||
local title="${1:-$(translate "Select Shared Directory Location")}"
|
||||
local base_name="${2:-shared}"
|
||||
local choice folder_name result mount_point
|
||||
LSM_SELECTED_MOUNT_POINT=""
|
||||
|
||||
# Auto-suggest a free name in /mnt
|
||||
local suggested
|
||||
suggested=$(lsm_next_free_name "$base_name")
|
||||
|
||||
while true; do
|
||||
choice=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$title" \
|
||||
--menu "\n$(translate "Where do you want the host folder?")" 16 72 4 \
|
||||
"1" "$(translate "Create new folder in /mnt")" \
|
||||
"2" "$(translate "Enter custom path")" \
|
||||
"3" "$(translate "View existing folders in /mnt")" \
|
||||
"4" "$(translate "Cancel")" \
|
||||
3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
folder_name=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Folder Name")" \
|
||||
--inputbox "\n$(translate "Enter folder name for /mnt:")" 10 70 "$(basename "$suggested")" \
|
||||
3>&1 1>&2 2>&3) || continue
|
||||
[[ -z "$folder_name" ]] && continue
|
||||
mount_point="/mnt/$folder_name"
|
||||
# Only warn if the user manually typed an existing name
|
||||
if [[ -d "$mount_point" ]]; then
|
||||
if ! dialog --backtitle "ProxMenux" --title "$(translate "Directory Exists")" \
|
||||
--yesno "\n$(translate "Directory already exists. Continue with permission setup?")" 8 70; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
result=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Custom Path")" \
|
||||
--inputbox "\n$(translate "Enter full path:")" 10 80 "" \
|
||||
3>&1 1>&2 2>&3) || continue
|
||||
[[ -z "$result" ]] && continue
|
||||
mount_point="$result"
|
||||
if [[ -d "$mount_point" ]]; then
|
||||
if ! dialog --backtitle "ProxMenux" --title "$(translate "Directory Exists")" \
|
||||
--yesno "\n$(translate "Directory already exists. Continue with permission setup?")" 8 70; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
lsm_list_mnt_folders
|
||||
# Refresh suggestion after viewing
|
||||
suggested=$(lsm_next_free_name "$base_name")
|
||||
continue
|
||||
;;
|
||||
4) return 1 ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
if [[ ! "$mount_point" =~ ^/ ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Invalid Path")" \
|
||||
--msgbox "\n$(translate "Path must be absolute (start with /).")" 8 60
|
||||
continue
|
||||
fi
|
||||
|
||||
LSM_SELECTED_MOUNT_POINT="$mount_point"
|
||||
return 0
|
||||
done
|
||||
}
|
||||
|
||||
create_shared_directory() {
|
||||
lsm_select_host_mount_point_dialog "$(translate "Select Shared Directory Location")" "shared"
|
||||
[[ -z "$LSM_SELECTED_MOUNT_POINT" ]] && return
|
||||
SHARED_DIR="$LSM_SELECTED_MOUNT_POINT"
|
||||
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Create Shared Directory")"
|
||||
|
||||
if ! mkdir -p "$SHARED_DIR" 2>/dev/null; then
|
||||
msg_error "$(translate "Failed to create directory:") $SHARED_DIR"
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to continue...")"
|
||||
read -r
|
||||
return 1
|
||||
fi
|
||||
msg_ok "$(translate "Directory created:") $SHARED_DIR"
|
||||
|
||||
lsm_apply_multi_unpriv_permissions "$SHARED_DIR"
|
||||
|
||||
pmx_share_map_set "$SHARED_DIR" "open"
|
||||
|
||||
echo -e ""
|
||||
echo -e "${TAB}${BOLD}$(translate "Shared Directory Created:")${CL}"
|
||||
echo -e "${TAB}${BOLD}$(translate "Shared Directory Ready:")${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Directory:")${CL} ${BL}$SHARED_DIR${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Group:")${CL} ${BL}$SHARE_GROUP (GID: $SHARE_GID)${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Permissions:")${CL} ${BL}2775 (rwxrwsr-x)${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Owner:")${CL} ${BL}root:$SHARE_GROUP${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "ACL Status:")${CL} ${BL}$(translate "Cleaned and set for POSIX inheritance")${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Permissions:")${CL} ${BL}1777 (rwxrwxrwt)${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Owner:")${CL} ${BL}root:root${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Access profile:")${CL} ${BL}$(translate "Compatible with privileged and unprivileged LXC containers")${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "ACL Status:")${CL} ${BL}$(translate "Open rwx + default inheritance for new files")${CL}"
|
||||
echo -e ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
|
||||
@@ -229,15 +229,13 @@ select_host_directory_unified() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Warn about CIFS Proxmox-GUI storage (read-only limitation)
|
||||
# Store the storage type as a global so the main flow can act on it later.
|
||||
# We don't block the user here — the active fix happens after we know the container type.
|
||||
LMM_HOST_DIR_TYPE="local"
|
||||
if detect_problematic_storage "$result" "Proxmox-Storage" "CIFS/SMB"; then
|
||||
dialog --clear --title "$(translate "CIFS Storage Notice")" --yesno "\
|
||||
$(translate "This directory is a CIFS storage managed by Proxmox.")\n\n\
|
||||
$(translate "CIFS storage configured through Proxmox GUI applies restrictive permissions.")\n\
|
||||
$(translate "LXC containers can usually READ but may NOT be able to WRITE.")\n\n\
|
||||
$(translate "For write access, use 'Add Samba Share as Proxmox Storage' option instead.")\n\n\
|
||||
$(translate "Do you want to continue anyway?")" 14 80 3>&1 1>&2 2>&3
|
||||
[[ $? -ne 0 ]] && return 1
|
||||
LMM_HOST_DIR_TYPE="cifs"
|
||||
elif detect_problematic_storage "$result" "Proxmox-Storage" "NFS"; then
|
||||
LMM_HOST_DIR_TYPE="nfs"
|
||||
fi
|
||||
|
||||
echo "$result"
|
||||
@@ -314,7 +312,7 @@ select_container_mount_point() {
|
||||
fi
|
||||
|
||||
# Check if path is already used as a mount point in this CT
|
||||
if pct config "$ctid" 2>/dev/null | grep -q "mp=.*$mount_point"; then
|
||||
if pct config "$ctid" 2>/dev/null | grep -qE "mp=${mount_point}(,|$)"; then
|
||||
whiptail --msgbox "$(translate "This path is already used as a mount point in this container.")" 8 70
|
||||
continue
|
||||
fi
|
||||
@@ -364,7 +362,7 @@ add_bind_mount() {
|
||||
fi
|
||||
|
||||
# Check if this host path is already mounted in this CT
|
||||
if pct config "$ctid" 2>/dev/null | grep -q "^mp[0-9]*:.*${host_path},"; then
|
||||
if pct config "$ctid" 2>/dev/null | grep -qF " ${host_path},"; then
|
||||
msg_warn "$(translate "Mount already exists for this path in container") $ctid"
|
||||
return 1
|
||||
fi
|
||||
@@ -555,6 +553,199 @@ $(translate "Proceed with removal")?"
|
||||
read -r
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
# ACTIVE FIXES FOR NETWORK STORAGE (CIFS / NFS)
|
||||
# These functions act on problems instead of just warning about them.
|
||||
# ==========================================================
|
||||
|
||||
lmm_fix_cifs_access() {
|
||||
local host_dir="$1"
|
||||
local is_unprivileged="$2"
|
||||
|
||||
# CIFS mounted by Proxmox GUI uses uid=0/gid=0 by default (root only).
|
||||
# The fix: remount with uid/gid that the LXC can access.
|
||||
# We detect the current mount options and propose a corrected remount.
|
||||
|
||||
local mount_src mount_opts
|
||||
mount_src=$(findmnt -n -o SOURCE --target "$host_dir" 2>/dev/null)
|
||||
mount_opts=$(findmnt -n -o OPTIONS --target "$host_dir" 2>/dev/null)
|
||||
|
||||
if [[ -z "$mount_src" ]]; then
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "CIFS Mount Not Found")" \
|
||||
--msgbox "$(translate "Could not detect the CIFS mount for this directory. Try accessing it manually.")" 8 70
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Determine which uid/gid to use
|
||||
local target_uid target_gid
|
||||
if [[ "$is_unprivileged" == "1" ]]; then
|
||||
# Unprivileged LXC: container root (UID 0) maps to host UID 100000.
|
||||
# Use file_mode/dir_mode 0777 + uid=0/gid=0 — CIFS maps them to everyone.
|
||||
target_uid=0
|
||||
target_gid=0
|
||||
else
|
||||
target_uid=0
|
||||
target_gid=0
|
||||
fi
|
||||
|
||||
# Build new options: strip existing uid/gid/file_mode/dir_mode, add ours
|
||||
local new_opts
|
||||
new_opts=$(echo "$mount_opts" | sed -E \
|
||||
's/(^|,)(uid|gid|file_mode|dir_mode)=[^,]*//g' | \
|
||||
sed 's/^,//')
|
||||
new_opts="${new_opts},uid=${target_uid},gid=${target_gid},file_mode=0777,dir_mode=0777"
|
||||
new_opts="${new_opts/#,/}"
|
||||
|
||||
if dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Fix CIFS Permissions")" \
|
||||
--yesno \
|
||||
"$(translate "This CIFS share is mounted with restrictive permissions.")\n\n\
|
||||
$(translate "ProxMenux can remount it with open permissions so any LXC can read and write.")\n\n\
|
||||
$(translate "Current mount options:")\n${mount_opts}\n\n\
|
||||
$(translate "New mount options to apply:")\n${new_opts}\n\n\
|
||||
$(translate "Apply fix now? (The share will be briefly remounted)")" \
|
||||
18 84 3>&1 1>&2 2>&3; then
|
||||
|
||||
msg_info "$(translate "Remounting CIFS share with open permissions...")"
|
||||
if umount "$host_dir" 2>/dev/null && \
|
||||
mount -t cifs "$mount_src" "$host_dir" -o "$new_opts" 2>/dev/null; then
|
||||
msg_ok "$(translate "CIFS share remounted — LXC containers can now read and write")"
|
||||
|
||||
# Update fstab if the mount is there
|
||||
if grep -qF "$host_dir" /etc/fstab 2>/dev/null; then
|
||||
sed -i "s|^\(${mount_src}[[:space:]].*${host_dir}.*cifs[[:space:]]\).*|\1${new_opts} 0 0|" /etc/fstab 2>/dev/null || true
|
||||
msg_ok "$(translate "/etc/fstab updated — permissions will persist after reboot")"
|
||||
fi
|
||||
else
|
||||
msg_warn "$(translate "Could not remount automatically. Try manually or check credentials.")"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
lmm_fix_nfs_access() {
|
||||
local host_dir="$1"
|
||||
local is_unprivileged="$2"
|
||||
local uid_shift="${3:-100000}"
|
||||
|
||||
# NFS: the host cannot override server-side permissions.
|
||||
# BUT: if the server exports with root_squash (default), we can check
|
||||
# if no_root_squash or all_squash is possible, and guide the user.
|
||||
# What we CAN do on the host: apply a sticky+open directory as a cache layer
|
||||
# if the NFS mount allows it.
|
||||
|
||||
local mount_src mount_opts
|
||||
mount_src=$(findmnt -n -o SOURCE --target "$host_dir" 2>/dev/null)
|
||||
mount_opts=$(findmnt -n -o OPTIONS --target "$host_dir" 2>/dev/null)
|
||||
|
||||
# Try to detect if we can write to the NFS share as root
|
||||
local can_write=false
|
||||
local testfile="${host_dir}/.proxmenux_write_test_$$"
|
||||
if touch "$testfile" 2>/dev/null; then
|
||||
rm -f "$testfile" 2>/dev/null
|
||||
can_write=true
|
||||
fi
|
||||
|
||||
local server_hint=""
|
||||
if [[ -n "$mount_src" ]]; then
|
||||
server_hint="${mount_src%%:*}"
|
||||
fi
|
||||
|
||||
if [[ "$can_write" == "true" && "$is_unprivileged" == "1" ]]; then
|
||||
# Root on host CAN write to NFS, but unprivileged LXC UIDs (100000+)
|
||||
# will be squashed by the NFS server. We can set a world-writable sticky
|
||||
# dir on the share itself so the container can write to it.
|
||||
if dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Fix NFS Access for Unprivileged LXC")" \
|
||||
--yesno \
|
||||
"$(translate "NFS server export is writable from the host, but unprivileged LXC containers use mapped UIDs (${uid_shift}+) which the NFS server will squash.")\n\n\
|
||||
$(translate "ProxMenux can apply open permissions on this NFS directory from the host so the container can read and write:")\n\n\
|
||||
$(translate " chmod 1777 + setfacl o::rwx (applied on the NFS share from this host)")\n\n\
|
||||
$(translate "Note: this only works if the NFS server does NOT use 'all_squash' for root.")\n\
|
||||
$(translate "If it still fails, the NFS server export options must be changed on the server.")\n\n\
|
||||
$(translate "Apply fix now?")" \
|
||||
18 84 3>&1 1>&2 2>&3; then
|
||||
|
||||
if chmod 1777 "$host_dir" 2>/dev/null; then
|
||||
msg_ok "$(translate "NFS directory permissions set — containers should now be able to write")"
|
||||
else
|
||||
msg_warn "$(translate "chmod failed — NFS server may be restricting changes from root")"
|
||||
fi
|
||||
|
||||
if command -v setfacl >/dev/null 2>&1; then
|
||||
setfacl -m o::rwx "$host_dir" 2>/dev/null || true
|
||||
setfacl -m d:o::rwx "$host_dir" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
elif [[ "$can_write" == "false" ]]; then
|
||||
# Even root cannot write — NFS server is fully restrictive
|
||||
local server_msg=""
|
||||
[[ -n "$server_hint" ]] && server_msg="\n$(translate "NFS server:"): ${server_hint}"
|
||||
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "NFS Access Restricted")" \
|
||||
--msgbox \
|
||||
"$(translate "This NFS share is fully restricted — even the host root cannot write to it.")\n\
|
||||
${server_msg}\n\n\
|
||||
$(translate "ProxMenux cannot override NFS server-side permissions from the host.")\n\n\
|
||||
$(translate "To allow LXC write access, change the NFS export on the server to include:")\n\n\
|
||||
$(translate " no_root_squash") $(translate "(if only privileged LXCs need write access)")\n\
|
||||
$(translate " all_squash,anonuid=65534,anongid=65534") $(translate "(for unprivileged LXCs)")\n\n\
|
||||
$(translate "You can still mount this share for READ-ONLY access.")" \
|
||||
20 84 3>&1 1>&2 2>&3
|
||||
fi
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
# HOST PERMISSION CHECK (host-side only, never touches the container)
|
||||
# ==========================================================
|
||||
|
||||
lmm_offer_host_permissions() {
|
||||
local host_dir="$1"
|
||||
local is_unprivileged="$2"
|
||||
|
||||
# Privileged containers: UID 0 inside = UID 0 on host — always accessible
|
||||
[[ "$is_unprivileged" != "1" ]] && return 0
|
||||
|
||||
# Check if 'others' already have r+x (minimum to traverse and read)
|
||||
local stat_perms others_bits
|
||||
stat_perms=$(stat -c "%a" "$host_dir" 2>/dev/null) || return 0
|
||||
others_bits=$(( 8#${stat_perms} & 7 ))
|
||||
|
||||
# Check ACLs first if available (takes precedence over mode bits)
|
||||
if command -v getfacl >/dev/null 2>&1; then
|
||||
if getfacl -p "$host_dir" 2>/dev/null | grep -q "^other::.*r.*x"; then
|
||||
return 0 # ACL already grants others r+x or better
|
||||
fi
|
||||
fi
|
||||
|
||||
# 5 = r-x (bits: r=4, x=1). If already r+x or rwx we're fine.
|
||||
(( (others_bits & 5) == 5 )) && return 0
|
||||
|
||||
# Permissions are insufficient — offer to fix HOST directory only
|
||||
local current_perms
|
||||
current_perms=$(stat -c "%A" "$host_dir" 2>/dev/null)
|
||||
|
||||
if dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Unprivileged Container Access")" \
|
||||
--yesno \
|
||||
"$(translate "The host directory may not be accessible from an unprivileged container.")\n\n\
|
||||
$(translate "Unprivileged containers map their UIDs to high host UIDs (e.g. 100000+), which appear as 'others' on the host filesystem.")\n\n\
|
||||
$(translate "Current permissions:"): ${current_perms}\n\n\
|
||||
$(translate "Apply read+write access for 'others' on the host directory?")\n\n\
|
||||
$(translate "(Only the host directory is modified. Nothing inside the container is changed.")" \
|
||||
16 80 3>&1 1>&2 2>&3; then
|
||||
|
||||
chmod o+rwx "$host_dir" 2>/dev/null || true
|
||||
if command -v setfacl >/dev/null 2>&1; then
|
||||
setfacl -m o::rwx "$host_dir" 2>/dev/null || true
|
||||
setfacl -m d:o::rwx "$host_dir" 2>/dev/null || true
|
||||
fi
|
||||
msg_ok "$(translate "Host directory permissions updated — unprivileged containers can now access it")"
|
||||
fi
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
# MAIN FUNCTION — ADD MOUNT
|
||||
# ==========================================================
|
||||
@@ -577,7 +768,7 @@ mount_host_directory_minimal() {
|
||||
|
||||
# Step 4: Get container type info (for display only)
|
||||
local uid_shift container_type_display
|
||||
uid_shift=$(awk -F: '/^lxc.idmap.*u 0/ {print $5}' "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | head -1)
|
||||
uid_shift=$(awk '/^lxc.idmap.*u 0/ {print $5}' "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | head -1)
|
||||
local is_unprivileged
|
||||
is_unprivileged=$(grep "^unprivileged:" "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | awk '{print $2}')
|
||||
if [[ "$is_unprivileged" == "1" ]]; then
|
||||
@@ -588,7 +779,13 @@ mount_host_directory_minimal() {
|
||||
uid_shift="0"
|
||||
fi
|
||||
|
||||
# Step 5: Confirmation
|
||||
# Step 5: Active fix for network storage (before confirmation, while we know container type)
|
||||
case "${LMM_HOST_DIR_TYPE:-local}" in
|
||||
cifs) lmm_fix_cifs_access "$host_dir" "$is_unprivileged" ;;
|
||||
nfs) lmm_fix_nfs_access "$host_dir" "$is_unprivileged" "$uid_shift" ;;
|
||||
esac
|
||||
|
||||
# Step 6: Confirmation
|
||||
local confirm_msg
|
||||
confirm_msg="$(translate "Mount Configuration Summary:")
|
||||
|
||||
@@ -597,17 +794,12 @@ $(translate "Host Directory"): $host_dir
|
||||
$(translate "Container Mount Point"): $ct_mount_point
|
||||
|
||||
$(translate "IMPORTANT NOTES:")
|
||||
- $(translate "Host directory permissions and ownership are NOT modified")
|
||||
- $(translate "Container filesystem is NOT modified")
|
||||
- $(translate "If access fails after mounting, adjust permissions manually:")
|
||||
|
||||
$(if [[ "$is_unprivileged" == "1" ]]; then
|
||||
echo " # Allow container UID ${uid_shift}+ to access host dir:"
|
||||
echo " setfacl -m u:${uid_shift}:rwx \"$host_dir\""
|
||||
echo " setfacl -d:m u:${uid_shift}:rwx \"$host_dir\""
|
||||
else
|
||||
echo " chmod 755 \"$host_dir\""
|
||||
fi)
|
||||
- $(translate "Nothing inside the container is modified")
|
||||
- $(if [[ "$is_unprivileged" == "1" ]]; then
|
||||
translate "Host directory access for unprivileged containers has been prepared above"
|
||||
else
|
||||
translate "Privileged container — host root maps directly, no permission changes needed"
|
||||
fi)
|
||||
|
||||
$(translate "Proceed")?"
|
||||
|
||||
@@ -621,7 +813,7 @@ $(translate "Proceed")?"
|
||||
msg_ok "$(translate "Host directory:") $host_dir"
|
||||
msg_ok "$(translate "Container mount point:") $ct_mount_point"
|
||||
|
||||
# Step 6: Add bind mount (the ONLY operation that changes anything)
|
||||
# Step 7: Add bind mount
|
||||
if ! add_bind_mount "$container_id" "$host_dir" "$ct_mount_point"; then
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to continue...")"
|
||||
@@ -629,27 +821,25 @@ $(translate "Proceed")?"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Step 7: Summary with permission hints
|
||||
# Step 8: Host permission check for local dirs (only if not already handled above for CIFS/NFS)
|
||||
if [[ "${LMM_HOST_DIR_TYPE:-local}" == "local" ]]; then
|
||||
lmm_offer_host_permissions "$host_dir" "$is_unprivileged"
|
||||
fi
|
||||
|
||||
# Step 9: Summary
|
||||
echo ""
|
||||
echo -e "${TAB}${BOLD}$(translate "Mount Added Successfully:")${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Container:")${CL} ${BL}$container_id${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Host Directory:")${CL} ${BL}$host_dir${CL}"
|
||||
echo -e "${TAB}${BGN}$(translate "Mount Point:")${CL} ${BL}$ct_mount_point${CL}"
|
||||
if [[ "$is_unprivileged" == "1" ]]; then
|
||||
echo -e "${TAB}${YW}$(translate "Unprivileged container — UID offset:") ${uid_shift}${CL}"
|
||||
else
|
||||
echo -e "${TAB}${DGN}$(translate "Privileged container — direct root access")${CL}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [[ "$is_unprivileged" == "1" ]]; then
|
||||
local mapped_uid="$uid_shift"
|
||||
echo -e "${TAB}${YW}$(translate "UNPRIVILEGED container — UID mapping active:")${CL}"
|
||||
echo -e "${TAB} $(translate "Container UID 0") → $(translate "Host UID") $mapped_uid"
|
||||
echo -e "${TAB} $(translate "If access fails, run on the host:")"
|
||||
echo -e "${TAB} ${DGN}setfacl -m u:${mapped_uid}:rwx \"$host_dir\"${CL}"
|
||||
echo -e "${TAB} ${DGN}setfacl -d:m u:${mapped_uid}:rwx \"$host_dir\"${CL}"
|
||||
else
|
||||
echo -e "${TAB}${DGN}$(translate "PRIVILEGED container — direct UID mapping")${CL}"
|
||||
echo -e "${TAB} $(translate "Ensure") $host_dir $(translate "is accessible by root (chmod 755 or wider)")"
|
||||
fi
|
||||
|
||||
# Step 8: Offer restart
|
||||
# Step 10: Offer restart
|
||||
echo ""
|
||||
if whiptail --yesno "$(translate "Restart container to activate mount?")" 8 60; then
|
||||
msg_info "$(translate "Restarting container...")"
|
||||
|
||||
@@ -253,7 +253,7 @@ add_proxmox_nfs_storage() {
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Storage ID is available")"
|
||||
|
||||
msg_info "$(translate "NFS storage adding in progress...")"
|
||||
if pvesm_output=$(pvesm add nfs "$storage_id" \
|
||||
--server "$server" \
|
||||
--export "$export" \
|
||||
|
||||
@@ -14,6 +14,7 @@ LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
TOOLS_JSON="$BASE_DIR/installed_tools.json"
|
||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||
@@ -51,22 +52,45 @@ SELECTED_VMID=""
|
||||
SELECTED_VM_NAME=""
|
||||
declare -a SELECTED_CONTROLLER_PCIS=()
|
||||
IOMMU_PENDING_REBOOT=0
|
||||
IOMMU_ALREADY_ACTIVE=0
|
||||
NEED_HOOK_SYNC=false
|
||||
WIZARD_CONFLICT_POLICY=""
|
||||
WIZARD_CONFLICT_SCOPE=""
|
||||
|
||||
set_title() {
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Add Controller or NVMe PCIe to VM")"
|
||||
}
|
||||
|
||||
ensure_tools_json() {
|
||||
[[ -f "$TOOLS_JSON" ]] || echo "{}" > "$TOOLS_JSON"
|
||||
}
|
||||
|
||||
register_tool() {
|
||||
local tool="$1"
|
||||
local state="$2"
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
ensure_tools_json
|
||||
jq --arg t "$tool" --argjson v "$state" \
|
||||
'.[$t]=$v' "$TOOLS_JSON" > "$TOOLS_JSON.tmp" \
|
||||
&& mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
|
||||
}
|
||||
|
||||
register_vfio_iommu_tool() {
|
||||
register_tool "vfio_iommu" true || true
|
||||
}
|
||||
|
||||
enable_iommu_cmdline() {
|
||||
local silent="${1:-}"
|
||||
local cpu_vendor iommu_param
|
||||
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
||||
|
||||
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
|
||||
iommu_param="intel_iommu=on"
|
||||
msg_info "$(translate "Intel CPU detected")"
|
||||
[[ "$silent" != "silent" ]] && msg_info "$(translate "Intel CPU detected")"
|
||||
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
|
||||
iommu_param="amd_iommu=on"
|
||||
msg_info "$(translate "AMD CPU detected")"
|
||||
[[ "$silent" != "silent" ]] && msg_info "$(translate "AMD CPU detected")"
|
||||
else
|
||||
msg_error "$(translate "Unknown CPU vendor. Cannot determine IOMMU parameter.")"
|
||||
return 1
|
||||
@@ -76,22 +100,22 @@ enable_iommu_cmdline() {
|
||||
local grub_file="/etc/default/grub"
|
||||
|
||||
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
|
||||
if ! grep -q "$iommu_param" "$cmdline_file"; then
|
||||
if ! grep -q "$iommu_param" "$cmdline_file" || ! grep -q "iommu=pt" "$cmdline_file"; then
|
||||
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
|
||||
proxmox-boot-tool refresh >/dev/null 2>&1 || true
|
||||
msg_ok "$(translate "IOMMU parameters added to /etc/kernel/cmdline")"
|
||||
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU parameters added to /etc/kernel/cmdline")"
|
||||
else
|
||||
msg_ok "$(translate "IOMMU already configured in /etc/kernel/cmdline")"
|
||||
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU already configured in /etc/kernel/cmdline")"
|
||||
fi
|
||||
elif [[ -f "$grub_file" ]]; then
|
||||
if ! grep -q "$iommu_param" "$grub_file"; then
|
||||
if ! grep -q "$iommu_param" "$grub_file" || ! grep -q "iommu=pt" "$grub_file"; then
|
||||
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
|
||||
update-grub >/dev/null 2>&1 || true
|
||||
msg_ok "$(translate "IOMMU parameters added to GRUB")"
|
||||
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU parameters added to GRUB")"
|
||||
else
|
||||
msg_ok "$(translate "IOMMU already configured in GRUB")"
|
||||
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU already configured in GRUB")"
|
||||
fi
|
||||
else
|
||||
msg_error "$(translate "Neither /etc/kernel/cmdline nor /etc/default/grub found.")"
|
||||
@@ -101,24 +125,29 @@ enable_iommu_cmdline() {
|
||||
|
||||
check_iommu_or_offer_enable() {
|
||||
if [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||
register_vfio_iommu_tool
|
||||
return 0
|
||||
fi
|
||||
|
||||
if grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
|
||||
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
|
||||
IOMMU_PENDING_REBOOT=1
|
||||
msg_warn "$(translate "IOMMU is configured for next boot, but not active yet.")"
|
||||
msg_info2 "$(translate "Controller/NVMe assignment can continue now and will be effective after reboot.")"
|
||||
register_vfio_iommu_tool
|
||||
|
||||
return 0
|
||||
fi
|
||||
|
||||
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||
IOMMU_ALREADY_ACTIVE=1
|
||||
register_vfio_iommu_tool
|
||||
return 0
|
||||
fi
|
||||
|
||||
if grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null && \
|
||||
[[ -d /sys/kernel/iommu_groups ]] && \
|
||||
[[ -n "$(ls /sys/kernel/iommu_groups/ 2>/dev/null)" ]]; then
|
||||
IOMMU_ALREADY_ACTIVE=1
|
||||
register_vfio_iommu_tool
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -133,13 +162,11 @@ check_iommu_or_offer_enable() {
|
||||
--title "$(translate "IOMMU Required")" \
|
||||
--yesno "$msg" 15 74
|
||||
local response=$?
|
||||
clear
|
||||
|
||||
[[ $response -ne 0 ]] && return 1
|
||||
|
||||
set_title
|
||||
msg_title "$(translate "Enabling IOMMU")"
|
||||
echo
|
||||
if ! enable_iommu_cmdline; then
|
||||
echo
|
||||
msg_error "$(translate "Failed to configure IOMMU automatically.")"
|
||||
@@ -148,37 +175,36 @@ check_iommu_or_offer_enable() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo
|
||||
msg_success "$(translate "IOMMU configured. Reboot required before using Controller/NVMe passthrough.")"
|
||||
echo
|
||||
if whiptail --title "$(translate "Reboot Required")" \
|
||||
--yesno "$(translate "Do you want to reboot now?")" 10 64; then
|
||||
msg_warn "$(translate "Rebooting the system...")"
|
||||
reboot
|
||||
else
|
||||
IOMMU_PENDING_REBOOT=1
|
||||
msg_warn "$(translate "Reboot postponed by user.")"
|
||||
msg_info2 "$(translate "You can continue assigning Controller/NVMe now, but reboot the host before starting the VM.")"
|
||||
msg_success "$(translate "Press Enter to continue...")"
|
||||
read -r
|
||||
fi
|
||||
register_vfio_iommu_tool
|
||||
IOMMU_PENDING_REBOOT=1
|
||||
return 0
|
||||
}
|
||||
|
||||
select_target_vm() {
|
||||
local -a vm_menu=()
|
||||
local line vmid vmname vmstatus vm_machine status_label
|
||||
local max_name_len=0 padded_name
|
||||
|
||||
while IFS= read -r line; do
|
||||
vmid=$(awk '{print $1}' <<< "$line")
|
||||
vmname=$(awk '{print $2}' <<< "$line")
|
||||
[[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
|
||||
[[ -f "/etc/pve/qemu-server/${vmid}.conf" ]] || continue
|
||||
[[ ${#vmname} -gt $max_name_len ]] && max_name_len=${#vmname}
|
||||
done < <(qm list 2>/dev/null)
|
||||
|
||||
while IFS= read -r line; do
|
||||
vmid=$(awk '{print $1}' <<< "$line")
|
||||
vmname=$(awk '{print $2}' <<< "$line")
|
||||
vmstatus=$(awk '{print $3}' <<< "$line")
|
||||
[[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
|
||||
[[ -f "/etc/pve/qemu-server/${vmid}.conf" ]] || continue
|
||||
|
||||
vm_machine=$(qm config "$vmid" 2>/dev/null | awk -F': ' '/^machine:/ {print $2}')
|
||||
[[ -z "$vm_machine" ]] && vm_machine="unknown"
|
||||
status_label="${vmstatus}, ${vm_machine}"
|
||||
vm_menu+=("$vmid" "${vmname} [${status_label}]")
|
||||
printf -v padded_name "%-${max_name_len}s" "$vmname"
|
||||
vm_menu+=("$vmid" "${padded_name} [${status_label}]")
|
||||
done < <(qm list 2>/dev/null)
|
||||
if [[ ${#vm_menu[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" \
|
||||
@@ -221,6 +247,10 @@ validate_vm_requirements() {
|
||||
}
|
||||
|
||||
select_controller_nvme() {
|
||||
# Show progress during potentially slow PCIe + disk detection
|
||||
set_title
|
||||
msg_info "$(translate "Analyzing system for available PCIe storage devices...")"
|
||||
|
||||
_refresh_host_storage_cache
|
||||
|
||||
local -a menu_items=()
|
||||
@@ -251,12 +281,19 @@ select_controller_nvme() {
|
||||
_array_contains "$disk" "${controller_disks[@]}" || controller_disks+=("$disk")
|
||||
done < <(_controller_block_devices "$pci_full")
|
||||
|
||||
# blocked_reasons: system disk OR disk in RUNNING guest → hide controller
|
||||
# warn_reasons: disk in STOPPED guest only → show with ⚠ but allow selection
|
||||
local -a blocked_reasons=()
|
||||
local -a warn_reasons=()
|
||||
for disk in "${controller_disks[@]}"; do
|
||||
if _disk_is_host_system_used "$disk"; then
|
||||
blocked_reasons+=("${disk} (${DISK_USAGE_REASON})")
|
||||
elif _disk_used_in_guest_configs "$disk"; then
|
||||
blocked_reasons+=("${disk} ($(translate "In use by VM/LXC config"))")
|
||||
if _disk_used_in_running_guest "$disk"; then
|
||||
blocked_reasons+=("${disk} ($(translate "In use by running VM/LXC — stop it first"))")
|
||||
else
|
||||
warn_reasons+=("$disk")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -266,21 +303,30 @@ select_controller_nvme() {
|
||||
continue
|
||||
fi
|
||||
|
||||
local short_name
|
||||
short_name=$(_shorten_text "$name" 42)
|
||||
local short_name display_name
|
||||
display_name=$(_pci_storage_display_name "$pci_full")
|
||||
short_name=$(_shorten_text "$display_name" 56)
|
||||
|
||||
local assigned_suffix=""
|
||||
if [[ -n "$(_pci_assigned_vm_ids "$pci_full" "$SELECTED_VMID" 2>/dev/null | head -1)" ]]; then
|
||||
assigned_suffix=" | $(translate "Assigned to VM")"
|
||||
fi
|
||||
|
||||
controller_desc="${short_name}${assigned_suffix}"
|
||||
# Warn if some disks are referenced in stopped VM/CT configs
|
||||
local warn_suffix=""
|
||||
if [[ ${#warn_reasons[@]} -gt 0 ]]; then
|
||||
warn_suffix=" ⚠"
|
||||
fi
|
||||
|
||||
controller_desc="${short_name}${assigned_suffix}${warn_suffix}"
|
||||
|
||||
state="off"
|
||||
menu_items+=("$pci_full" "$controller_desc" "$state")
|
||||
safe_count=$((safe_count + 1))
|
||||
done < <(ls -d /sys/bus/pci/devices/* 2>/dev/null | sort)
|
||||
|
||||
stop_spinner
|
||||
|
||||
if [[ "$safe_count" -eq 0 ]]; then
|
||||
local msg
|
||||
if [[ "$hidden_target_count" -gt 0 && "$blocked_count" -eq 0 ]]; then
|
||||
@@ -318,29 +364,100 @@ select_controller_nvme() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if declare -F _vm_storage_confirm_controller_passthrough_risk >/dev/null 2>&1; then
|
||||
if ! _vm_storage_confirm_controller_passthrough_risk "$SELECTED_VMID" "$SELECTED_VM_NAME" "$(translate "Controller + NVMe")"; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
confirm_summary() {
|
||||
local msg
|
||||
msg="\n$(translate "The following devices will be added to VM") ${SELECTED_VMID} (${SELECTED_VM_NAME}):\n\n"
|
||||
local pci info
|
||||
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
||||
info=$(lspci -nn -s "${pci#0000:}" 2>/dev/null | sed 's/^[^ ]* //')
|
||||
msg+=" - ${pci}${info:+ (${info})}\n"
|
||||
done
|
||||
msg+="\n$(translate "Do you want to continue?")"
|
||||
_prompt_raw_disk_conflict_policy() {
|
||||
local disk="$1"
|
||||
shift
|
||||
local -a guest_ids=("$@")
|
||||
local msg gid gtype gid_num gname gstatus
|
||||
|
||||
dialog --backtitle "ProxMenux" --colors \
|
||||
msg="$(translate "Disk") ${disk} $(translate "is referenced in the following stopped VM(s)/CT(s):")\\n\\n"
|
||||
for gid in "${guest_ids[@]}"; do
|
||||
gtype="${gid%%:*}"; gid_num="${gid##*:}"
|
||||
if [[ "$gtype" == "VM" ]]; then
|
||||
gname=$(_vm_name_by_id "$gid_num")
|
||||
gstatus=$(qm status "$gid_num" 2>/dev/null | awk '{print $2}')
|
||||
msg+=" - VM $gid_num ($gname) [${gstatus}]\\n"
|
||||
else
|
||||
gname=$(pct config "$gid_num" 2>/dev/null | awk '/^hostname:/ {print $2}')
|
||||
[[ -z "$gname" ]] && gname="CT-$gid_num"
|
||||
gstatus=$(pct status "$gid_num" 2>/dev/null | awk '{print $2}')
|
||||
msg+=" - CT $gid_num ($gname) [${gstatus}]\\n"
|
||||
fi
|
||||
done
|
||||
msg+="\\n$(translate "Choose action:")"
|
||||
|
||||
local choice
|
||||
choice=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Disk Reference Conflict")" \
|
||||
--menu "$msg" 22 84 3 \
|
||||
"1" "$(translate "Disable onboot on affected VM(s)/CT(s)")" \
|
||||
"2" "$(translate "Remove disk references from affected VM(s)/CT(s) config")" \
|
||||
"3" "$(translate "Skip — leave as-is")" \
|
||||
2>&1 >/dev/tty) || { echo "skip"; return; }
|
||||
|
||||
case "$choice" in
|
||||
1) echo "disable_onboot" ;;
|
||||
2) echo "remove_refs" ;;
|
||||
*) echo "skip" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
confirm_summary() {
|
||||
# ── Risk detection ─────────────────────────────────────────────────────────
|
||||
local reinforce_limited_firmware="no"
|
||||
local bios_date bios_year current_year bios_age cpu_model risk_detail=""
|
||||
bios_date=$(cat /sys/class/dmi/id/bios_date 2>/dev/null)
|
||||
bios_year=$(echo "$bios_date" | grep -oE '[0-9]{4}' | tail -n1)
|
||||
current_year=$(date +%Y 2>/dev/null)
|
||||
if [[ -n "$bios_year" && -n "$current_year" ]]; then
|
||||
bios_age=$(( current_year - bios_year ))
|
||||
if (( bios_age >= 7 )); then
|
||||
reinforce_limited_firmware="yes"
|
||||
risk_detail="$(translate "BIOS from") ${bios_year} (${bios_age} $(translate "years old")) — $(translate "older firmware may increase passthrough instability")"
|
||||
fi
|
||||
fi
|
||||
cpu_model=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | xargs)
|
||||
if echo "$cpu_model" | grep -qiE 'J4[0-9]{3}|J3[0-9]{3}|N4[0-9]{3}|N3[0-9]{3}|Apollo Lake'; then
|
||||
reinforce_limited_firmware="yes"
|
||||
[[ -z "$risk_detail" ]] && risk_detail="$(translate "Low-power CPU platform"): ${cpu_model}"
|
||||
fi
|
||||
|
||||
# ── Build unified message ──────────────────────────────────────────────────
|
||||
local msg pci display_name
|
||||
msg="\n"
|
||||
|
||||
# Devices to add
|
||||
msg+="\Zb$(translate "Devices to add to VM") ${SELECTED_VMID} (${SELECTED_VM_NAME}):\Zn\n"
|
||||
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
||||
display_name=$(_pci_storage_display_name "$pci")
|
||||
msg+=" \Zb•\Zn ${pci} ${display_name}\n"
|
||||
done
|
||||
msg+="\n"
|
||||
|
||||
# Compatibility notice (always shown)
|
||||
msg+="\Zb\Z4⚠ $(translate "Controller/NVMe passthrough — compatibility notice")\Zn\n\n"
|
||||
msg+="$(translate "Not all platforms support Controller/NVMe passthrough reliably.")\n"
|
||||
msg+="$(translate "On some systems, when starting the VM the host may slow down for several minutes until it stabilizes, or freeze completely.")\n"
|
||||
|
||||
# Detected risk (only when applicable)
|
||||
if [[ "$reinforce_limited_firmware" == "yes" && -n "$risk_detail" ]]; then
|
||||
msg+="\n\Z1$(translate "Detected risk factor"): ${risk_detail}\Zn\n"
|
||||
fi
|
||||
|
||||
msg+="\n$(translate "If the host freezes, remove hostpci entries from") /etc/pve/qemu-server/${SELECTED_VMID}.conf\n"
|
||||
msg+="\n\Zb$(translate "Do you want to continue?")\Zn"
|
||||
|
||||
local height=22
|
||||
[[ "$reinforce_limited_firmware" == "yes" ]] && height=25
|
||||
|
||||
if ! dialog --backtitle "ProxMenux" --colors \
|
||||
--title "$(translate "Confirm Controller + NVMe Assignment")" \
|
||||
--yesno "$msg" 18 90
|
||||
[[ $? -ne 0 ]] && return 1
|
||||
--yesno "$msg" $height 90; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -349,7 +466,7 @@ prompt_controller_conflict_policy() {
|
||||
shift
|
||||
local -a source_vms=("$@")
|
||||
local msg vmid vm_name st ob
|
||||
msg="$(translate "Selected device is already assigned to other VM(s):")\n\n"
|
||||
msg="\n$(translate "Selected device is already assigned to other VM(s):")\n\n"
|
||||
for vmid in "${source_vms[@]}"; do
|
||||
vm_name=$(_vm_name_by_id "$vmid")
|
||||
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
||||
@@ -359,11 +476,13 @@ prompt_controller_conflict_policy() {
|
||||
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
||||
|
||||
local choice
|
||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 22 96 10 \
|
||||
choice=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Controller/NVMe Conflict Policy")" \
|
||||
--menu "$msg" 20 80 10 \
|
||||
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
||||
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
||||
"3" "$(translate "Skip this device")" \
|
||||
3>&1 1>&2 2>&3) || { echo "skip"; return; }
|
||||
2>&1 >/dev/tty) || { echo "skip"; return; }
|
||||
|
||||
case "$choice" in
|
||||
1) echo "keep_disable_onboot" ;;
|
||||
@@ -372,17 +491,143 @@ prompt_controller_conflict_policy() {
|
||||
esac
|
||||
}
|
||||
|
||||
# ── DIALOG PHASE: resolve all conflicts before terminal ───────────────────────
|
||||
resolve_disk_conflicts() {
|
||||
local -a new_pci_list=()
|
||||
local pci vmid action slot_base scope_key has_running
|
||||
|
||||
# ── hostpci conflicts: controller already assigned to another VM ──────────
|
||||
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
||||
local -a source_vms=()
|
||||
mapfile -t source_vms < <(_pci_assigned_vm_ids "$pci" "$SELECTED_VMID" 2>/dev/null)
|
||||
|
||||
if [[ ${#source_vms[@]} -eq 0 ]]; then
|
||||
new_pci_list+=("$pci")
|
||||
continue
|
||||
fi
|
||||
|
||||
has_running=false
|
||||
for vmid in "${source_vms[@]}"; do
|
||||
if _vm_status_is_running "$vmid"; then
|
||||
has_running=true
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Device In Use")" \
|
||||
--msgbox "\n$(translate "Controller") $pci $(translate "is in use by running VM") $vmid.\n\n$(translate "Stop it first and run this option again.")" \
|
||||
10 72
|
||||
break
|
||||
fi
|
||||
done
|
||||
$has_running && continue
|
||||
|
||||
scope_key=$(printf '%s,' "${source_vms[@]}")
|
||||
if [[ -n "$WIZARD_CONFLICT_POLICY" && "$WIZARD_CONFLICT_SCOPE" == "$scope_key" ]]; then
|
||||
action="$WIZARD_CONFLICT_POLICY"
|
||||
else
|
||||
action=$(prompt_controller_conflict_policy "$pci" "${source_vms[@]}")
|
||||
WIZARD_CONFLICT_POLICY="$action"
|
||||
WIZARD_CONFLICT_SCOPE="$scope_key"
|
||||
fi
|
||||
|
||||
case "$action" in
|
||||
keep_disable_onboot)
|
||||
for vmid in "${source_vms[@]}"; do
|
||||
_vm_onboot_is_enabled "$vmid" && qm set "$vmid" -onboot 0 >/dev/null 2>&1
|
||||
done
|
||||
NEED_HOOK_SYNC=true
|
||||
new_pci_list+=("$pci")
|
||||
;;
|
||||
move_remove_source)
|
||||
slot_base=$(_pci_slot_base "$pci")
|
||||
for vmid in "${source_vms[@]}"; do
|
||||
_remove_pci_slot_from_vm_config "$vmid" "$slot_base"
|
||||
done
|
||||
new_pci_list+=("$pci")
|
||||
;;
|
||||
*) ;; # skip — do not add to new_pci_list
|
||||
esac
|
||||
done
|
||||
|
||||
SELECTED_CONTROLLER_PCIS=("${new_pci_list[@]}")
|
||||
|
||||
if [[ ${#SELECTED_CONTROLLER_PCIS[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Controller + NVMe")" \
|
||||
--msgbox "\n$(translate "No controllers remaining after conflict resolution.")" 8 64
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ── Raw disk passthrough conflicts ───────────────────────────────────────
|
||||
local raw_disk_policy="" raw_disk_scope=""
|
||||
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
||||
local -a cdisks=()
|
||||
while IFS= read -r disk; do
|
||||
[[ -z "$disk" ]] && continue
|
||||
_array_contains "$disk" "${cdisks[@]}" || cdisks+=("$disk")
|
||||
done < <(_controller_block_devices "$pci")
|
||||
|
||||
for disk in "${cdisks[@]}"; do
|
||||
_disk_used_in_guest_configs "$disk" || continue
|
||||
_disk_used_in_running_guest "$disk" && continue
|
||||
|
||||
local -a guest_ids=()
|
||||
mapfile -t guest_ids < <(_disk_guest_ids "$disk")
|
||||
[[ ${#guest_ids[@]} -eq 0 ]] && continue
|
||||
|
||||
local gscope gaction
|
||||
gscope=$(printf '%s,' "${guest_ids[@]}")
|
||||
if [[ -n "$raw_disk_policy" && "$raw_disk_scope" == "$gscope" ]]; then
|
||||
gaction="$raw_disk_policy"
|
||||
else
|
||||
gaction=$(_prompt_raw_disk_conflict_policy "$disk" "${guest_ids[@]}")
|
||||
raw_disk_policy="$gaction"
|
||||
raw_disk_scope="$gscope"
|
||||
fi
|
||||
|
||||
local gid gtype gid_num slot
|
||||
case "$gaction" in
|
||||
disable_onboot)
|
||||
for gid in "${guest_ids[@]}"; do
|
||||
gtype="${gid%%:*}"; gid_num="${gid##*:}"
|
||||
if [[ "$gtype" == "VM" ]]; then
|
||||
_vm_onboot_is_enabled "$gid_num" && qm set "$gid_num" -onboot 0 >/dev/null 2>&1
|
||||
else
|
||||
grep -qE '^onboot:\s*1' "/etc/pve/lxc/$gid_num.conf" 2>/dev/null && \
|
||||
pct set "$gid_num" -onboot 0 >/dev/null 2>&1
|
||||
fi
|
||||
done
|
||||
;;
|
||||
remove_refs)
|
||||
for gid in "${guest_ids[@]}"; do
|
||||
gtype="${gid%%:*}"; gid_num="${gid##*:}"
|
||||
if [[ "$gtype" == "VM" ]]; then
|
||||
while IFS= read -r slot; do
|
||||
[[ -z "$slot" ]] && continue
|
||||
qm set "$gid_num" -delete "$slot" >/dev/null 2>&1
|
||||
done < <(_find_disk_slots_in_vm "$gid_num" "$disk")
|
||||
else
|
||||
while IFS= read -r slot; do
|
||||
[[ -z "$slot" ]] && continue
|
||||
pct set "$gid_num" -delete "$slot" >/dev/null 2>&1
|
||||
done < <(_find_disk_slots_in_ct "$gid_num" "$disk")
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
done
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_assignment() {
|
||||
: >"$LOG_FILE"
|
||||
set_title
|
||||
echo
|
||||
|
||||
msg_info "$(translate "Applying Controller/NVMe passthrough to VM") ${SELECTED_VMID}..."
|
||||
msg_ok "$(translate "Target VM validated") (${SELECTED_VM_NAME} / ${SELECTED_VMID})"
|
||||
msg_ok "$(translate "Selected devices"): ${#SELECTED_CONTROLLER_PCIS[@]}"
|
||||
|
||||
local hostpci_idx=0
|
||||
msg_info "$(translate "Calculating next available hostpci slot...")"
|
||||
if declare -F _pci_next_hostpci_index >/dev/null 2>&1; then
|
||||
hostpci_idx=$(_pci_next_hostpci_index "$SELECTED_VMID" 2>/dev/null || echo 0)
|
||||
else
|
||||
@@ -392,10 +637,8 @@ apply_assignment() {
|
||||
hostpci_idx=$((hostpci_idx + 1))
|
||||
done
|
||||
fi
|
||||
msg_ok "$(translate "Next available hostpci slot"): hostpci${hostpci_idx}"
|
||||
|
||||
local pci bdf assigned_count=0
|
||||
local need_hook_sync=false
|
||||
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
||||
bdf="${pci#0000:}"
|
||||
if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then
|
||||
@@ -408,50 +651,11 @@ apply_assignment() {
|
||||
continue
|
||||
fi
|
||||
|
||||
local -a source_vms=()
|
||||
mapfile -t source_vms < <(_pci_assigned_vm_ids "$pci" "$SELECTED_VMID" 2>/dev/null)
|
||||
if [[ ${#source_vms[@]} -gt 0 ]]; then
|
||||
local has_running=false vmid action slot_base
|
||||
for vmid in "${source_vms[@]}"; do
|
||||
if _vm_status_is_running "$vmid"; then
|
||||
has_running=true
|
||||
msg_warn "$(translate "Controller/NVMe is in use by running VM") ${vmid} ($(translate "stop source VM first"))"
|
||||
fi
|
||||
done
|
||||
|
||||
if $has_running; then
|
||||
continue
|
||||
fi
|
||||
|
||||
action=$(prompt_controller_conflict_policy "$pci" "${source_vms[@]}")
|
||||
case "$action" in
|
||||
keep_disable_onboot)
|
||||
for vmid in "${source_vms[@]}"; do
|
||||
if _vm_onboot_is_enabled "$vmid"; then
|
||||
if qm set "$vmid" -onboot 0 >>"$LOG_FILE" 2>&1; then
|
||||
msg_warn "$(translate "Start on boot disabled for VM") ${vmid}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
need_hook_sync=true
|
||||
;;
|
||||
move_remove_source)
|
||||
slot_base=$(_pci_slot_base "$pci")
|
||||
for vmid in "${source_vms[@]}"; do
|
||||
if _remove_pci_slot_from_vm_config "$vmid" "$slot_base"; then
|
||||
msg_ok "$(translate "Controller/NVMe removed from source VM") ${vmid} (${pci})"
|
||||
fi
|
||||
done
|
||||
;;
|
||||
*)
|
||||
msg_info2 "$(translate "Skipped device"): ${pci}"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if qm set "$SELECTED_VMID" --hostpci${hostpci_idx} "${pci},pcie=1" >>"$LOG_FILE" 2>&1; then
|
||||
msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx} -> ${pci})"
|
||||
local display_name
|
||||
display_name=$(_pci_storage_display_name "$pci")
|
||||
msg_info "$(translate "Adding") ${display_name} (${pci}) → hostpci${hostpci_idx}..."
|
||||
if qm set "$SELECTED_VMID" "--hostpci${hostpci_idx}" "${pci},pcie=1" >>"$LOG_FILE" 2>&1; then
|
||||
msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx} → ${pci})"
|
||||
assigned_count=$((assigned_count + 1))
|
||||
hostpci_idx=$((hostpci_idx + 1))
|
||||
else
|
||||
@@ -459,33 +663,50 @@ apply_assignment() {
|
||||
fi
|
||||
done
|
||||
|
||||
if $need_hook_sync && declare -F sync_proxmenux_gpu_guard_hooks >/dev/null 2>&1; then
|
||||
if $NEED_HOOK_SYNC && declare -F sync_proxmenux_gpu_guard_hooks >/dev/null 2>&1; then
|
||||
ensure_proxmenux_gpu_guard_hookscript
|
||||
sync_proxmenux_gpu_guard_hooks
|
||||
msg_ok "$(translate "VM hook guard synced for shared controller/NVMe protection")"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo ""
|
||||
echo -e "${TAB}${BL}Log: ${LOG_FILE}${CL}"
|
||||
|
||||
if [[ "$assigned_count" -gt 0 ]]; then
|
||||
msg_success "$(translate "Completed. Controller/NVMe passthrough configured for VM") ${SELECTED_VMID}."
|
||||
if [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||
msg_warn "$(translate "IOMMU was configured during this run. Reboot the host before starting the VM.")"
|
||||
fi
|
||||
msg_ok "$(translate "Completed.") $assigned_count $(translate "device(s) added to VM") ${SELECTED_VMID}."
|
||||
else
|
||||
msg_warn "$(translate "No new Controller/NVMe entries were added.")"
|
||||
fi
|
||||
|
||||
if [[ "${IOMMU_ALREADY_ACTIVE:-0}" == "1" ]]; then
|
||||
msg_ok "$(translate "IOMMU is enabled on the system")"
|
||||
elif [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||
msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")"
|
||||
echo ""
|
||||
if whiptail --title "$(translate "Reboot Required")" \
|
||||
--yesno "\n$(translate "IOMMU has been enabled on this system. A reboot is required to apply the changes. Reboot now?")" 11 64; then
|
||||
msg_success "$(translate "Press Enter to continue...")"
|
||||
read -r
|
||||
msg_warn "$(translate "Rebooting the system...")"
|
||||
reboot
|
||||
else
|
||||
msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")"
|
||||
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to continue...")"
|
||||
read -r
|
||||
}
|
||||
|
||||
main() {
|
||||
select_target_vm || exit 0
|
||||
export WIZARD_CONFLICT_POLICY
|
||||
export WIZARD_CONFLICT_SCOPE
|
||||
select_target_vm || exit 0
|
||||
validate_vm_requirements || exit 0
|
||||
select_controller_nvme || exit 0
|
||||
confirm_summary || exit 0
|
||||
clear
|
||||
select_controller_nvme || exit 0
|
||||
resolve_disk_conflicts || exit 0
|
||||
confirm_summary || exit 0
|
||||
apply_assignment
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
# 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
|
||||
# Version : 1.2
|
||||
# Last Updated: 12/04/2026
|
||||
# ==========================================================
|
||||
# Description:
|
||||
# This script allows users to assign physical disks to existing
|
||||
@@ -20,6 +20,7 @@
|
||||
# - Ensures that disks are not already assigned to active VMs.
|
||||
# - Warns about disk sharing between multiple VMs to avoid data corruption.
|
||||
# - Configures the selected disks for the VM and verifies the assignment.
|
||||
# - Prefers persistent /dev/disk/by-id paths for assignment when available.
|
||||
#
|
||||
# The goal of this script is to simplify the process of assigning
|
||||
# physical disks to Proxmox VMs, reducing manual configurations
|
||||
@@ -28,134 +29,181 @@
|
||||
|
||||
|
||||
# Configuration ============================================
|
||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
VENV_PATH="/opt/googletrans-env"
|
||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||
elif [[ ! -f "$UTILS_FILE" ]]; then
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh"
|
||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh"
|
||||
fi
|
||||
|
||||
BACKTITLE="ProxMenux"
|
||||
UI_MENU_H=20
|
||||
UI_MENU_W=84
|
||||
UI_MENU_LIST_H=10
|
||||
UI_SHORT_MENU_H=16
|
||||
UI_SHORT_MENU_W=72
|
||||
UI_SHORT_MENU_LIST_H=6
|
||||
UI_MSG_H=10
|
||||
UI_MSG_W=72
|
||||
UI_YESNO_H=18
|
||||
UI_YESNO_W=86
|
||||
UI_RESULT_H=18
|
||||
UI_RESULT_W=86
|
||||
|
||||
load_language
|
||||
initialize_cache
|
||||
# ==========================================================
|
||||
|
||||
if ! command -v pveversion >/dev/null 2>&1; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "Error")" \
|
||||
--msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
|
||||
exit 1
|
||||
fi
|
||||
# ==========================================================
|
||||
|
||||
|
||||
get_disk_info() {
|
||||
local disk=$1
|
||||
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
|
||||
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
|
||||
echo "$MODEL" "$SIZE"
|
||||
local model size
|
||||
model=$(lsblk -dn -o MODEL "$disk" | xargs)
|
||||
size=$(lsblk -dn -o SIZE "$disk" | xargs)
|
||||
[[ -z "$model" ]] && model="Unknown"
|
||||
printf '%s\t%s\n' "$model" "$size"
|
||||
}
|
||||
|
||||
get_all_disk_paths() {
|
||||
local disk="$1"
|
||||
local real_path
|
||||
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||
|
||||
[[ -n "$disk" ]] && echo "$disk"
|
||||
[[ -n "$real_path" ]] && echo "$real_path"
|
||||
|
||||
local link
|
||||
for link in /dev/disk/by-id/* /dev/disk/by-path/*; do
|
||||
[[ -e "$link" ]] || continue
|
||||
[[ "$(readlink -f "$link" 2>/dev/null)" == "$real_path" ]] || continue
|
||||
echo "$link"
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
get_preferred_disk_path() {
|
||||
local disk="$1"
|
||||
local real_path
|
||||
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||
[[ -z "$real_path" ]] && { echo "$disk"; return 0; }
|
||||
|
||||
local best="" best_score=99999
|
||||
local link name score
|
||||
for link in /dev/disk/by-id/*; do
|
||||
[[ -e "$link" ]] || continue
|
||||
[[ "$(readlink -f "$link" 2>/dev/null)" == "$real_path" ]] || continue
|
||||
name=$(basename "$link")
|
||||
[[ "$name" == *-part* ]] && continue
|
||||
|
||||
case "$name" in
|
||||
ata-*|scsi-*|nvme-*) score=100 ;;
|
||||
wwn-*) score=200 ;;
|
||||
*) score=300 ;;
|
||||
esac
|
||||
score=$((score + ${#name}))
|
||||
if (( score < best_score )); then
|
||||
best="$link"
|
||||
best_score=$score
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "$best" ]]; then
|
||||
echo "$best"
|
||||
else
|
||||
echo "$disk"
|
||||
fi
|
||||
}
|
||||
|
||||
disk_referenced_in_config() {
|
||||
local config_text="$1"
|
||||
local disk="$2"
|
||||
local alias
|
||||
while read -r alias; do
|
||||
[[ -z "$alias" ]] && continue
|
||||
if grep -Fq "$alias" <<< "$config_text"; then
|
||||
return 0
|
||||
fi
|
||||
done < <(get_all_disk_paths "$disk")
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
# ── DIALOG PHASE ──────────────────────────────────────────────────────────────
|
||||
|
||||
VM_LIST=$(qm list | awk 'NR>1 {print $1, $2}')
|
||||
if [ -z "$VM_LIST" ]; then
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No VMs available in the system.")" 8 40
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Error")" \
|
||||
--msgbox "$(translate "No VMs available in the system.")" $UI_MSG_H $UI_MSG_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
VMID=$(whiptail --title "$(translate "Select VM")" --menu "$(translate "Select the VM to which you want to add disks:")" 15 60 8 $VM_LIST 3>&1 1>&2 2>&3)
|
||||
# shellcheck disable=SC2086
|
||||
VMID=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Select VM")" \
|
||||
--menu "$(translate "Select the VM to which you want to add disks:")" $UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
$VM_LIST \
|
||||
2>&1 >/dev/tty)
|
||||
|
||||
if [ -z "$VMID" ]; then
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No VM was selected.")" 8 40
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Error")" \
|
||||
--msgbox "$(translate "No VM was selected.")" $UI_MSG_H $UI_MSG_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VMID=$(echo "$VMID" | tr -d '"')
|
||||
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
echo -e
|
||||
msg_title "$(translate "Import Disk to VM")"
|
||||
echo -e
|
||||
|
||||
msg_ok "$(translate "VM selected successfully.")"
|
||||
|
||||
|
||||
VM_STATUS=$(qm status "$VMID" | awk '{print $2}')
|
||||
if [ "$VM_STATUS" == "running" ]; then
|
||||
whiptail --title "$(translate "Warning")" --msgbox "$(translate "The VM is powered on. Turn it off before adding disks.")" 12 60
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Warning")" \
|
||||
--msgbox "$(translate "The VM is powered on. Turn it off before adding disks.")" $UI_MSG_H $UI_MSG_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
##########################################
|
||||
|
||||
# ── TERMINAL PHASE 1: detect disks ────────────────────────────────────────────
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Import Disk to VM")"
|
||||
msg_ok "$(translate "VM $VMID selected successfully.")"
|
||||
msg_info "$(translate "Detecting available disks...")"
|
||||
|
||||
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
|
||||
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
|
||||
|
||||
ZFS_DISKS=""
|
||||
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
|
||||
|
||||
for entry in $ZFS_RAW; do
|
||||
|
||||
path=""
|
||||
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
|
||||
if [ -e "/dev/disk/by-id/$entry" ]; then
|
||||
path=$(readlink -f "/dev/disk/by-id/$entry")
|
||||
fi
|
||||
elif [[ "$entry" == /dev/* ]]; then
|
||||
path="$entry"
|
||||
fi
|
||||
|
||||
|
||||
if [ -n "$path" ]; then
|
||||
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
|
||||
if [ -n "$base_disk" ]; then
|
||||
ZFS_DISKS+="/dev/$base_disk"$'\n'
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
|
||||
|
||||
|
||||
is_disk_in_use() {
|
||||
local disk="$1"
|
||||
|
||||
|
||||
while read -r part fstype; do
|
||||
case "$fstype" in
|
||||
zfs_member|linux_raid_member)
|
||||
return 0 ;;
|
||||
esac
|
||||
|
||||
if echo "$MOUNTED_DISKS" | grep -q "/dev/$part"; then
|
||||
return 0
|
||||
fi
|
||||
done < <(lsblk -ln -o NAME,FSTYPE "$disk" | tail -n +2)
|
||||
|
||||
|
||||
if echo "$USED_DISKS" | grep -q "$disk" || echo "$ZFS_DISKS" | grep -q "$disk"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
|
||||
_refresh_host_storage_cache
|
||||
VM_CONFIG=$(qm config "$VMID" 2>/dev/null | grep -vE '^\s*#|^description:')
|
||||
|
||||
FREE_DISKS=()
|
||||
|
||||
LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u)
|
||||
RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u)
|
||||
|
||||
while read -r DISK; do
|
||||
|
||||
[[ "$DISK" =~ /dev/zd ]] && continue
|
||||
|
||||
INFO=($(get_disk_info "$DISK"))
|
||||
MODEL="${INFO[@]::${#INFO[@]}-1}"
|
||||
SIZE="${INFO[-1]}"
|
||||
IFS=$'\t' read -r MODEL SIZE < <(get_disk_info "$DISK")
|
||||
LABEL=""
|
||||
SHOW_DISK=true
|
||||
|
||||
IS_MOUNTED=false
|
||||
IS_RAID=false
|
||||
IS_ZFS=false
|
||||
@@ -175,46 +223,34 @@ while read -r DISK; do
|
||||
IS_MOUNTED=true
|
||||
fi
|
||||
|
||||
|
||||
|
||||
USED_BY=""
|
||||
REAL_PATH=$(readlink -f "$DISK")
|
||||
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
|
||||
|
||||
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
|
||||
if _disk_used_in_guest_configs "$DISK"; then
|
||||
USED_BY="⚠ $(translate "In use")"
|
||||
else
|
||||
for SYMLINK in /dev/disk/by-id/*; do
|
||||
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
|
||||
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
|
||||
USED_BY="⚠ $(translate "In use")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
|
||||
|
||||
if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
|
||||
if grep -q "active raid" /proc/mdstat; then
|
||||
SHOW_DISK=false
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
if $IS_ZFS; then
|
||||
SHOW_DISK=false
|
||||
fi
|
||||
|
||||
# Catch whole-disk ZFS vdevs with no partitions (e.g. bare NVMe ZFS)
|
||||
# The tail -n +2 trick misses them; ZFS_DISKS from _refresh_host_storage_cache covers them.
|
||||
if [[ -n "$ZFS_DISKS" ]] && \
|
||||
{ grep -qFx "$DISK" <<< "$ZFS_DISKS" || \
|
||||
{ [[ -n "$REAL_PATH" ]] && grep -qFx "$REAL_PATH" <<< "$ZFS_DISKS"; }; }; then
|
||||
SHOW_DISK=false
|
||||
fi
|
||||
|
||||
if $IS_MOUNTED; then
|
||||
SHOW_DISK=false
|
||||
fi
|
||||
|
||||
|
||||
if qm config "$VMID" | grep -vE '^\s*#|^description:' | grep -q "$DISK"; then
|
||||
if disk_referenced_in_config "$VM_CONFIG" "$DISK"; then
|
||||
SHOW_DISK=false
|
||||
fi
|
||||
|
||||
@@ -229,144 +265,188 @@ while read -r DISK; do
|
||||
fi
|
||||
done < <(lsblk -dn -e 7,11 -o PATH)
|
||||
|
||||
|
||||
|
||||
if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
|
||||
cleanup
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks available for this VM.")" 8 40
|
||||
clear
|
||||
stop_spinner
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Error")" \
|
||||
--msgbox "$(translate "No disks available for this VM.")" $UI_MSG_H $UI_MSG_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
stop_spinner
|
||||
msg_ok "$(translate "Available disks detected.")"
|
||||
|
||||
|
||||
|
||||
######################################################
|
||||
|
||||
|
||||
|
||||
# ── DIALOG PHASE: select disks + interface ────────────────────────────────────
|
||||
|
||||
MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1)
|
||||
TOTAL_WIDTH=$((MAX_WIDTH + 20))
|
||||
|
||||
if [ $TOTAL_WIDTH -lt 50 ]; then
|
||||
TOTAL_WIDTH=50
|
||||
if [ $TOTAL_WIDTH -lt $UI_MENU_W ]; then
|
||||
TOTAL_WIDTH=$UI_MENU_W
|
||||
fi
|
||||
if [ $TOTAL_WIDTH -gt 116 ]; then
|
||||
TOTAL_WIDTH=116
|
||||
fi
|
||||
|
||||
|
||||
SELECTED=$(whiptail --title "$(translate "Select Disks")" --checklist \
|
||||
"$(translate "Select the disks you want to add:")" 20 $TOTAL_WIDTH 10 "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3)
|
||||
SELECTED=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Select Disks")" \
|
||||
--checklist "\n$(translate "Select the disks you want to add:")" $UI_MENU_H $TOTAL_WIDTH $UI_MENU_LIST_H \
|
||||
"${FREE_DISKS[@]}" \
|
||||
2>&1 >/dev/tty)
|
||||
|
||||
if [ -z "$SELECTED" ]; then
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks were selected.")" 10 64
|
||||
clear
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Error")" \
|
||||
--msgbox "$(translate "No disks were selected.")" $UI_MSG_H $UI_MSG_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Disks selected successfully.")"
|
||||
|
||||
|
||||
INTERFACE=$(whiptail --title "$(translate "Interface Type")" --menu "$(translate "Select the interface type for all disks:")" 15 40 4 \
|
||||
"sata" "$(translate "Add as SATA")" \
|
||||
"scsi" "$(translate "Add as SCSI")" \
|
||||
INTERFACE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Interface Type")" \
|
||||
--menu "$(translate "Select the interface type for all disks:")" $UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
|
||||
"sata" "$(translate "Add as SATA")" \
|
||||
"scsi" "$(translate "Add as SCSI")" \
|
||||
"virtio" "$(translate "Add as VirtIO")" \
|
||||
"ide" "$(translate "Add as IDE")" 3>&1 1>&2 2>&3)
|
||||
"ide" "$(translate "Add as IDE")" \
|
||||
2>&1 >/dev/tty)
|
||||
|
||||
if [ -z "$INTERFACE" ]; then
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No interface type was selected for the disks.")" 8 40
|
||||
clear
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Error")" \
|
||||
--msgbox "$(translate "No interface type was selected for the disks.")" $UI_MSG_H $UI_MSG_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Interface type selected: $INTERFACE")"
|
||||
|
||||
DISKS_ADDED=0
|
||||
ERROR_MESSAGES=""
|
||||
SUCCESS_MESSAGES=""
|
||||
|
||||
|
||||
|
||||
msg_info "$(translate "Processing selected disks...")"
|
||||
# ── DIALOG PHASE: per-disk pre-check ──────────────────────────────────────────
|
||||
declare -a DISK_LIST=()
|
||||
declare -a DISK_DESCRIPTIONS=()
|
||||
declare -a DISK_ASSIGNED_TOS=()
|
||||
declare -a NVME_SKIPPED=()
|
||||
|
||||
for DISK in $SELECTED; do
|
||||
DISK=$(echo "$DISK" | tr -d '"')
|
||||
DISK="${DISK//\"/}"
|
||||
DISK_INFO=$(get_disk_info "$DISK")
|
||||
|
||||
ASSIGNED_TO=""
|
||||
RUNNING_VMS=""
|
||||
RUNNING_CTS=""
|
||||
|
||||
|
||||
while read -r VM_ID VM_NAME; do
|
||||
if [[ "$VM_ID" =~ ^[0-9]+$ ]] && qm config "$VM_ID" | grep -q "$DISK"; then
|
||||
VM_CONFIG_RAW=$(qm config "$VM_ID" 2>/dev/null)
|
||||
if [[ "$VM_ID" =~ ^[0-9]+$ ]] && disk_referenced_in_config "$VM_CONFIG_RAW" "$DISK"; then
|
||||
ASSIGNED_TO+="VM $VM_ID $VM_NAME\n"
|
||||
VM_STATUS=$(qm status "$VM_ID" | awk '{print $2}')
|
||||
if [ "$VM_STATUS" == "running" ]; then
|
||||
RUNNING_VMS+="VM $VM_ID $VM_NAME\n"
|
||||
fi
|
||||
VM_STATUS_CHK=$(qm status "$VM_ID" | awk '{print $2}')
|
||||
[[ "$VM_STATUS_CHK" == "running" ]] && RUNNING_VMS+="VM $VM_ID $VM_NAME\n"
|
||||
fi
|
||||
done < <(qm list | awk 'NR>1 {print $1, $2}')
|
||||
|
||||
|
||||
while read -r CT_ID CT_NAME; do
|
||||
if [[ "$CT_ID" =~ ^[0-9]+$ ]] && pct config "$CT_ID" | grep -q "$DISK"; then
|
||||
CT_CONFIG_RAW=$(pct config "$CT_ID" 2>/dev/null)
|
||||
if [[ "$CT_ID" =~ ^[0-9]+$ ]] && disk_referenced_in_config "$CT_CONFIG_RAW" "$DISK"; then
|
||||
ASSIGNED_TO+="CT $CT_ID $CT_NAME\n"
|
||||
CT_STATUS=$(pct status "$CT_ID" | awk '{print $2}')
|
||||
if [ "$CT_STATUS" == "running" ]; then
|
||||
RUNNING_CTS+="CT $CT_ID $CT_NAME\n"
|
||||
fi
|
||||
CT_STATUS_CHK=$(pct status "$CT_ID" | awk '{print $2}')
|
||||
[[ "$CT_STATUS_CHK" == "running" ]] && RUNNING_CTS+="CT $CT_ID $CT_NAME\n"
|
||||
fi
|
||||
done < <(pct list | awk 'NR>1 {print $1, $2}')
|
||||
done < <(pct list | awk 'NR>1 {print $1, $3}')
|
||||
|
||||
if [ -n "$RUNNING_VMS" ] || [ -n "$RUNNING_CTS" ]; then
|
||||
ERROR_MESSAGES+="$(translate "The disk") $DISK_INFO $(translate "is currently in use by the following running VM(s) or CT(s):")\\n$RUNNING_VMS$RUNNING_CTS\\n\\n$(translate "You cannot add this disk while the VM or CT is running.")\\n$(translate "Please shut it down first and run this script again to add the disk.")\\n\\n"
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Disk In Use")" \
|
||||
--msgbox "$(translate "The disk") $DISK_INFO $(translate "is in use by the following running VM(s) or CT(s):")\\n$RUNNING_VMS$RUNNING_CTS\\n\\n$(translate "Stop them first and run this script again.")" $UI_RESULT_H $UI_RESULT_W
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -n "$ASSIGNED_TO" ]; then
|
||||
cleanup
|
||||
whiptail --title "$(translate "Disk Already Assigned")" --yesno "$(translate "The disk") $DISK_INFO $(translate "is already assigned to the following VM(s) or CT(s):")\\n$ASSIGNED_TO\\n\\n$(translate "Do you want to continue anyway?")" 15 70
|
||||
if [ $? -ne 0 ]; then
|
||||
sleep 1
|
||||
exec "$0"
|
||||
if ! dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Disk Already Assigned")" \
|
||||
--yesno "\n\n$(translate "The disk") $DISK_INFO $(translate "is already assigned to the following VM(s) or CT(s):")\\n$ASSIGNED_TO\\n\\n$(translate "Do you want to continue anyway?")" $UI_YESNO_H $UI_YESNO_W; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# NVMe: suggest PCIe passthrough for better performance
|
||||
if [[ "$DISK" =~ /dev/nvme ]] || \
|
||||
[[ "$(lsblk -dn -o TRAN "$DISK" 2>/dev/null | xargs)" == "nvme" ]]; then
|
||||
NVME_CHOICE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "NVMe Disk Detected")" \
|
||||
--default-item "disk" \
|
||||
--menu "\n$(translate "Adding this NVMe as a PCIe device (via 'Add Controller or NVMe PCIe to VM') gives better performance.")\n\n$(translate "How do you want to add it?")" \
|
||||
$UI_YESNO_H $UI_YESNO_W 2 \
|
||||
"disk" "$(translate "Add as disk (standard)")" \
|
||||
"pci" "$(translate "Skip — I will add it as PCIe device")" \
|
||||
2>&1 >/dev/tty)
|
||||
if [[ "$NVME_CHOICE" == "pci" ]]; then
|
||||
NVME_SKIPPED+=("$DISK")
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
DISK_LIST+=("$DISK")
|
||||
DISK_DESCRIPTIONS+=("$DISK_INFO")
|
||||
DISK_ASSIGNED_TOS+=("$ASSIGNED_TO")
|
||||
done
|
||||
|
||||
if [ "${#DISK_LIST[@]}" -eq 0 ]; then
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Import Disk to VM")"
|
||||
msg_warn "$(translate "No disks were configured for processing.")"
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
# ── TERMINAL PHASE: execute all disk operations ───────────────────────────────
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Import Disk to VM")"
|
||||
msg_ok "$(translate "VM $VMID selected successfully.")"
|
||||
msg_ok "$(translate "Disks to process:") ${#DISK_LIST[@]}"
|
||||
for i in "${!DISK_LIST[@]}"; do
|
||||
IFS=$'\t' read -r _desc_model _desc_size <<< "${DISK_DESCRIPTIONS[$i]}"
|
||||
echo -e "${TAB}${BL}${DISK_LIST[$i]} $_desc_model $_desc_size${CL}"
|
||||
done
|
||||
if [[ ${#NVME_SKIPPED[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
msg_warn "$(translate "NVMe skipped (to add as PCIe use 'Add Controller or NVMe PCIe to VM'):")"
|
||||
for _nvme in "${NVME_SKIPPED[@]}"; do
|
||||
echo -e "${TAB}${BL}${_nvme}${CL}"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
msg_ok "$(translate "Interface type:") $INTERFACE"
|
||||
echo ""
|
||||
|
||||
DISKS_ADDED=0
|
||||
|
||||
for i in "${!DISK_LIST[@]}"; do
|
||||
DISK="${DISK_LIST[$i]}"
|
||||
ASSIGNED_TO="${DISK_ASSIGNED_TOS[$i]}"
|
||||
IFS=$'\t' read -r _model _size <<< "${DISK_DESCRIPTIONS[$i]}"
|
||||
|
||||
INDEX=0
|
||||
while qm config "$VMID" | grep -q "${INTERFACE}${INDEX}"; do
|
||||
((INDEX++))
|
||||
done
|
||||
|
||||
RESULT=$(qm set "$VMID" -${INTERFACE}${INDEX} "$DISK" 2>&1)
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
MESSAGE="$(translate "The disk") $DISK_INFO $(translate "has been successfully added to VM") $VMID."
|
||||
if [ -n "$ASSIGNED_TO" ]; then
|
||||
MESSAGE+="\\n\\n$(translate "WARNING: This disk is also assigned to the following VM(s):")\\n$ASSIGNED_TO"
|
||||
MESSAGE+="\\n$(translate "Make sure not to start VMs that share this disk at the same time to avoid data corruption.")"
|
||||
fi
|
||||
SUCCESS_MESSAGES+="$MESSAGE\\n\\n"
|
||||
ASSIGN_PATH=$(get_preferred_disk_path "$DISK")
|
||||
msg_info "$(translate "Adding") $_model $_size $(translate "as") ${INTERFACE}${INDEX}..."
|
||||
if RESULT=$(qm set "$VMID" "-${INTERFACE}${INDEX}" "$ASSIGN_PATH" 2>&1); then
|
||||
msg_ok "$(translate "Disk added as") ${INTERFACE}${INDEX} $(translate "using") $ASSIGN_PATH"
|
||||
[[ -n "$ASSIGNED_TO" ]] && msg_warn "$(translate "WARNING: This disk is also assigned to:") $(echo -e "$ASSIGNED_TO" | tr '\n' ' ')"
|
||||
((DISKS_ADDED++))
|
||||
else
|
||||
ERROR_MESSAGES+="$(translate "Could not add disk") $DISK_INFO $(translate "to VM") $VMID.\\n$(translate "Error:") $RESULT\\n\\n"
|
||||
msg_error "$(translate "Could not add") $_model $_size: $RESULT"
|
||||
fi
|
||||
done
|
||||
|
||||
msg_ok "$(translate "Disk processing completed.")"
|
||||
|
||||
|
||||
|
||||
if [ -n "$SUCCESS_MESSAGES" ]; then
|
||||
MSG_LINES=$(echo "$SUCCESS_MESSAGES" | wc -l)
|
||||
whiptail --title "$(translate "Successful Operations")" --msgbox "$SUCCESS_MESSAGES" 16 70
|
||||
echo ""
|
||||
if [ "$DISKS_ADDED" -gt 0 ]; then
|
||||
msg_ok "$(translate "Completed.") $DISKS_ADDED $(translate "disk(s) added to VM") $VMID."
|
||||
else
|
||||
msg_warn "$(translate "No disks were added.")"
|
||||
fi
|
||||
|
||||
if [ -n "$ERROR_MESSAGES" ]; then
|
||||
whiptail --title "$(translate "Warnings and Errors")" --msgbox "$ERROR_MESSAGES" 16 70
|
||||
fi
|
||||
|
||||
|
||||
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
exit 0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
183
scripts/storage/disk-storage-manual-guide.sh
Normal file
183
scripts/storage/disk-storage-manual-guide.sh
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenux - Disk and Storage Manager Manual CLI Guide
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : GPL-3.0
|
||||
# Version : 1.0
|
||||
# Last Updated: 07/04/2026
|
||||
# ==========================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||
elif [[ ! -f "$UTILS_FILE" ]]; then
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
fi
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
GREEN=$'\033[0;32m'
|
||||
NC=$'\033[0m'
|
||||
|
||||
_cl() {
|
||||
local num="$1" disp="$2" desc="$3"
|
||||
local pad=$((47 - ${#disp}))
|
||||
[[ $pad -lt 1 ]] && pad=1
|
||||
local spaces
|
||||
spaces=$(printf '%*s' "$pad" '')
|
||||
printf " %2d) %s%s%s%s - %s\n" "$num" "$GREEN" "$disp" "$NC" "$spaces" "$desc"
|
||||
}
|
||||
|
||||
while true; do
|
||||
clear
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Disk and Storage Manager - Manual CLI Guide")"
|
||||
echo -e "${TAB}${YW}$(translate 'Inspection commands run directly. Template commands [T] require parameter substitution.')${CL}"
|
||||
echo
|
||||
|
||||
_cl 1 "lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL" "$(translate 'Inspect disks before any action')"
|
||||
_cl 2 "ls -lh /dev/disk/by-id/" "$(translate 'Identify persistent disk paths')"
|
||||
_cl 3 "qm list && pct list" "$(translate 'List VM/CT IDs to operate on')"
|
||||
_cl 4 "qm config <vmid> | grep 'sata|scsi|hostpci'" "$(translate 'Check VM disk/PCI slots')"
|
||||
_cl 5 "pvesm status -content images" "$(translate 'List storages valid for image import')"
|
||||
_cl 6 "qm importdisk <vmid> <image_path> <storage>" "[T] $(translate 'Import disk image to VM')"
|
||||
_cl 7 "qm set <vmid> --<iface><slot> <imported-disk>" "[T] $(translate 'Attach imported disk to VM')"
|
||||
_cl 8 "qm set <vmid> --boot order=<iface><slot>" "[T] $(translate 'Set VM boot order')"
|
||||
_cl 9 "lspci -nn | grep -Ei 'SATA|RAID|NVMe'" "$(translate 'Detect controller/NVMe BDF')"
|
||||
_cl 10 "find /sys/kernel/iommu_groups -type l | grep BDF" "$(translate 'Verify IOMMU group for PCI device')"
|
||||
_cl 11 "qm set <vmid> --hostpci<slot> <BDF>,pcie=1" "[T] $(translate 'Assign controller/NVMe passthrough')"
|
||||
_cl 12 "pct config <ctid> | grep '^mp'" "$(translate 'Check container mount points')"
|
||||
_cl 13 "pct set <ctid> -mp<slot> <disk>,mp=<path>" "[T] $(translate 'Add disk to LXC container')"
|
||||
_cl 14 "wipefs -a -f /dev/sdX && sgdisk --zap-all /dev/sdX" "[T] $(translate 'Clean disk metadata')"
|
||||
_cl 15 "parted -s /dev/sdX mklabel gpt mkpart primary" "[T] $(translate 'Create GPT partition')"
|
||||
_cl 16 "mkfs.ext4 -F /dev/sdX1 (or mkfs.xfs / mkfs.btrfs)" "[T] $(translate 'Format filesystem')"
|
||||
_cl 17 "pvesm status && zpool status" "$(translate 'Final storage health/status check')"
|
||||
echo -e " ${DEF} 0) $(translate 'Back to previous menu or Esc + Enter')${CL}"
|
||||
echo
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}"
|
||||
read -r user_input
|
||||
|
||||
if [[ "$user_input" == $'\x1b' ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
mode="exec"
|
||||
case "$user_input" in
|
||||
1) cmd="lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL" ;;
|
||||
2) cmd="ls -lh /dev/disk/by-id/" ;;
|
||||
3) cmd="qm list && pct list" ;;
|
||||
4)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"
|
||||
read -r vmid
|
||||
cmd="qm config $vmid | grep -E '^(sata|scsi|virtio|ide|hostpci|boot:)'"
|
||||
;;
|
||||
5) cmd="pvesm status -content images" ;;
|
||||
6)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter image full path: ')${CL}"; read -r image_path
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter target storage: ')${CL}"; read -r storage
|
||||
cmd="qm importdisk $vmid $image_path $storage"
|
||||
mode="template"
|
||||
;;
|
||||
7)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter interface (sata/scsi/virtio/ide): ')${CL}"; read -r iface
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter slot number (e.g. 0): ')${CL}"; read -r slot
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter imported disk reference (e.g. local-lvm:vm-100-disk-0): ')${CL}"; read -r imported_disk
|
||||
cmd="qm set $vmid --${iface}${slot} $imported_disk"
|
||||
mode="template"
|
||||
;;
|
||||
8)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter boot target (e.g. scsi0, sata0, ide0): ')${CL}"; read -r boot_target
|
||||
cmd="qm set $vmid --boot order=$boot_target"
|
||||
mode="template"
|
||||
;;
|
||||
9) cmd="lspci -nn | grep -Ei 'SATA|RAID|Non-Volatile|NVMe'" ;;
|
||||
10)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter PCI BDF (e.g. 0000:04:00.0): ')${CL}"
|
||||
read -r bdf
|
||||
cmd="find /sys/kernel/iommu_groups -type l | grep $bdf"
|
||||
;;
|
||||
11)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot number (e.g. 0): ')${CL}"; read -r slot
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter PCI BDF (e.g. 0000:04:00.0): ')${CL}"; read -r bdf
|
||||
cmd="qm set $vmid --hostpci${slot} ${bdf},pcie=1"
|
||||
mode="template"
|
||||
;;
|
||||
12)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"
|
||||
read -r ctid
|
||||
cmd="pct config $ctid | grep '^mp'"
|
||||
;;
|
||||
13)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"; read -r ctid
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter mp slot number (e.g. 0): ')${CL}"; read -r mpslot
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter disk or partition path (prefer /dev/disk/by-id/...): ')${CL}"; read -r disk_part
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter mount point in CT (e.g. /mnt/data): ')${CL}"; read -r mount_point
|
||||
cmd="pct set $ctid -mp${mpslot} ${disk_part},mp=${mount_point},backup=0,ro=0"
|
||||
mode="template"
|
||||
;;
|
||||
14)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter target disk (e.g. /dev/sdX): ')${CL}"
|
||||
read -r disk
|
||||
cmd="wipefs -a -f $disk && sgdisk --zap-all $disk"
|
||||
mode="template"
|
||||
;;
|
||||
15)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter target disk (e.g. /dev/sdX): ')${CL}"
|
||||
read -r disk
|
||||
cmd="parted -s -f $disk mklabel gpt mkpart primary 1MiB 100%"
|
||||
mode="template"
|
||||
;;
|
||||
16)
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter partition path (e.g. /dev/sdX1): ')${CL}"; read -r part
|
||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter filesystem (ext4/xfs/btrfs): ')${CL}"; read -r fs
|
||||
case "$fs" in
|
||||
ext4) cmd="mkfs.ext4 -F $part" ;;
|
||||
xfs) cmd="mkfs.xfs -f $part" ;;
|
||||
btrfs) cmd="mkfs.btrfs -f $part" ;;
|
||||
*) cmd="mkfs.ext4 -F $part" ;;
|
||||
esac
|
||||
mode="template"
|
||||
;;
|
||||
17) cmd="pvesm status && zpool status" ;;
|
||||
0) break ;;
|
||||
*)
|
||||
if [[ -n "$user_input" ]]; then
|
||||
cmd="$user_input"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$mode" == "template" ]]; then
|
||||
echo -e "\n${GREEN}$(translate 'Manual command template (copy/paste):')${NC}\n"
|
||||
echo "$cmd"
|
||||
echo
|
||||
msg_success "$(translate 'Press ENTER to continue...')"
|
||||
read -r tmp
|
||||
continue
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}> $cmd${NC}\n"
|
||||
bash -c "$cmd"
|
||||
echo
|
||||
msg_success "$(translate 'Press ENTER to continue...')"
|
||||
read -r tmp
|
||||
done
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,24 +6,14 @@
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||
# Version : 1.1
|
||||
# Last Updated: 29/05/2025
|
||||
# Version : 1.3
|
||||
# Last Updated: 12/04/2026
|
||||
# ==========================================================
|
||||
# Description:
|
||||
# This script automates the process of importing disk images into Proxmox VE virtual machines (VMs),
|
||||
# making it easy to attach pre-existing disk files without manual configuration.
|
||||
#
|
||||
# Before running the script, ensure that disk images are available in /var/lib/vz/template/images/.
|
||||
# The script scans this directory for compatible formats (.img, .qcow2, .vmdk, .raw) and lists the available files.
|
||||
#
|
||||
# Using an interactive menu, you can:
|
||||
# - Select a VM to attach the imported disk.
|
||||
# - Choose one or multiple disk images for import.
|
||||
# - Pick a storage volume in Proxmox for disk placement.
|
||||
# - Assign a suitable interface (SATA, SCSI, VirtIO, or IDE).
|
||||
# - Enable optional settings like SSD emulation or bootable disk configuration.
|
||||
#
|
||||
# Once completed, the script ensures the selected images are correctly attached and ready to use.
|
||||
# Imports disk images (.img, .qcow2, .vmdk, .raw) into Proxmox VE VMs.
|
||||
# Supports the default system ISO directory and custom paths.
|
||||
# All user decisions are collected in Phase 1 (dialogs) before
|
||||
# any operation is executed in Phase 2 (terminal output).
|
||||
# ==========================================================
|
||||
|
||||
# Configuration ============================================
|
||||
@@ -31,256 +21,340 @@ LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
VENV_PATH="/opt/googletrans-env"
|
||||
BACKTITLE="ProxMenux"
|
||||
UI_MENU_H=20
|
||||
UI_MENU_W=84
|
||||
UI_MENU_LIST_H=10
|
||||
UI_SHORT_MENU_H=16
|
||||
UI_SHORT_MENU_W=72
|
||||
UI_SHORT_MENU_LIST_H=6
|
||||
UI_MSG_H=10
|
||||
UI_MSG_W=72
|
||||
UI_YESNO_H=10
|
||||
UI_YESNO_W=72
|
||||
UI_RESULT_H=14
|
||||
UI_RESULT_W=86
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
|
||||
load_language
|
||||
initialize_cache
|
||||
# Configuration ============================================
|
||||
|
||||
|
||||
|
||||
detect_image_dir() {
|
||||
for store in $(pvesm status -content images | awk 'NR>1 {print $1}'); do
|
||||
_get_default_images_dir() {
|
||||
for dir in /var/lib/vz/template/iso /var/lib/vz/template/images; do
|
||||
[[ -d "$dir" ]] && echo "$dir" && return 0
|
||||
done
|
||||
local store path
|
||||
for store in $(pvesm status -content images 2>/dev/null | awk 'NR>1 {print $1}'); do
|
||||
path=$(pvesm path "${store}:template" 2>/dev/null)
|
||||
if [[ -d "$path" ]]; then
|
||||
for ext in raw img qcow2 vmdk; do
|
||||
if compgen -G "$path/*.$ext" > /dev/null; then
|
||||
echo "$path"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
for sub in images iso; do
|
||||
dir="$path/$sub"
|
||||
if [[ -d "$dir" ]]; then
|
||||
for ext in raw img qcow2 vmdk; do
|
||||
if compgen -G "$dir/*.$ext" > /dev/null; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
fi
|
||||
[[ -d "$path" ]] && echo "$path" && return 0
|
||||
done
|
||||
for fallback in /var/lib/vz/template/images /var/lib/vz/template/iso; do
|
||||
if [[ -d "$fallback" ]]; then
|
||||
for ext in raw img qcow2 vmdk; do
|
||||
if compgen -G "$fallback/*.$ext" > /dev/null; then
|
||||
echo "$fallback"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
echo "/var/lib/vz/template/iso"
|
||||
}
|
||||
|
||||
|
||||
IMAGES_DIR=$(detect_image_dir)
|
||||
if [[ -z "$IMAGES_DIR" ]]; then
|
||||
dialog --title "$(translate 'No Images Found')" \
|
||||
--msgbox "$(translate 'Could not find any directory containing disk images')\n\n$(translate 'Make sure there is at least one file with extension .img, .qcow2, .vmdk or .raw')" 15 60
|
||||
# ==========================================================
|
||||
# PHASE 1 — SELECTION
|
||||
# All dialogs run here. No execution, no show_proxmenux_logo.
|
||||
# ==========================================================
|
||||
|
||||
# ── Step 1: Select VM ─────────────────────────────────────
|
||||
VM_OPTIONS=()
|
||||
while read -r vmid vmname _rest; do
|
||||
VM_OPTIONS+=("$vmid" "${vmname:-VM-$vmid}")
|
||||
done < <(qm list 2>/dev/null | awk 'NR>1')
|
||||
stop_spinner
|
||||
|
||||
if [[ ${#VM_OPTIONS[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'No VMs Found')" \
|
||||
--msgbox "\n$(translate 'No VMs available in the system.')" \
|
||||
$UI_MSG_H $UI_MSG_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGES=$(ls -A "$IMAGES_DIR" | grep -E "\.(img|qcow2|vmdk|raw)$")
|
||||
if [ -z "$IMAGES" ]; then
|
||||
dialog --title "$(translate 'No Disk Images Found')" \
|
||||
--msgbox "$(translate 'No compatible disk images found in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" 15 60
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# 1. Select VM
|
||||
msg_info "$(translate 'Getting VM list')"
|
||||
VM_LIST=$(qm list | awk 'NR>1 {print $1" "$2}')
|
||||
if [ -z "$VM_LIST" ]; then
|
||||
msg_error "$(translate 'No VMs available in the system')"
|
||||
exit 1
|
||||
fi
|
||||
msg_ok "$(translate 'VM list obtained')"
|
||||
|
||||
VMID=$(whiptail --title "$(translate 'Select VM')" --menu "$(translate 'Select the VM where you want to import the disk image:')" 15 60 8 $VM_LIST 3>&1 1>&2 2>&3)
|
||||
|
||||
if [ -z "$VMID" ]; then
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# 2. Select storage volume
|
||||
msg_info "$(translate 'Getting storage volumes')"
|
||||
STORAGE_LIST=$(pvesm status -content images | awk 'NR>1 {print $1}')
|
||||
if [ -z "$STORAGE_LIST" ]; then
|
||||
msg_error "$(translate 'No storage volumes available')"
|
||||
exit 1
|
||||
fi
|
||||
msg_ok "$(translate 'Storage volumes obtained')"
|
||||
VMID=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Select VM')" \
|
||||
--menu "$(translate 'Select the VM where you want to import the disk image:')" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"${VM_OPTIONS[@]}" \
|
||||
2>&1 >/dev/tty)
|
||||
[[ -z "$VMID" ]] && exit 0
|
||||
|
||||
|
||||
# ── Step 2: Select storage ────────────────────────────────
|
||||
STORAGE_OPTIONS=()
|
||||
while read -r storage; do
|
||||
STORAGE_OPTIONS+=("$storage" "")
|
||||
done <<< "$STORAGE_LIST"
|
||||
while read -r storage type _rest; do
|
||||
STORAGE_OPTIONS+=("$storage" "$type")
|
||||
done < <(pvesm status -content images 2>/dev/null | awk 'NR>1')
|
||||
stop_spinner
|
||||
|
||||
STORAGE=$(whiptail --title "$(translate 'Select Storage')" --menu "$(translate 'Select the storage volume for disk import:')" 15 60 8 "${STORAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
||||
if [[ ${#STORAGE_OPTIONS[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'No Storage Found')" \
|
||||
--msgbox "\n$(translate 'No storage volumes available for disk images.')" \
|
||||
$UI_MSG_H $UI_MSG_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$STORAGE" ]; then
|
||||
|
||||
exit 1
|
||||
if [[ ${#STORAGE_OPTIONS[@]} -eq 2 ]]; then
|
||||
# Only one storage available — auto-select it
|
||||
STORAGE="${STORAGE_OPTIONS[0]}"
|
||||
else
|
||||
STORAGE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Select Storage')" \
|
||||
--menu "$(translate 'Select the storage volume for disk import:')" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"${STORAGE_OPTIONS[@]}" \
|
||||
2>&1 >/dev/tty)
|
||||
[[ -z "$STORAGE" ]] && exit 0
|
||||
fi
|
||||
|
||||
|
||||
# ── Step 3: Select image source directory ────────────────
|
||||
ISO_DIR="/var/lib/vz/template/iso"
|
||||
|
||||
# 3. Select disk images
|
||||
msg_info "$(translate 'Scanning disk images')"
|
||||
if [ -z "$IMAGES" ]; then
|
||||
msg_warn "$(translate 'No compatible disk images found in') $IMAGES_DIR"
|
||||
exit 0
|
||||
DIR_CHOICE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Image Source Directory')" \
|
||||
--menu "$(translate 'Select the directory containing disk images:')" \
|
||||
$UI_SHORT_MENU_H $UI_MENU_W $UI_SHORT_MENU_LIST_H \
|
||||
"$ISO_DIR" "$(translate 'Default ISO directory')" \
|
||||
"custom" "$(translate 'Custom path...')" \
|
||||
2>&1 >/dev/tty)
|
||||
[[ -z "$DIR_CHOICE" ]] && exit 0
|
||||
|
||||
if [[ "$DIR_CHOICE" == "custom" ]]; then
|
||||
IMAGES_DIR=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Custom Directory')" \
|
||||
--inputbox "\n$(translate 'Enter the full path to the directory containing disk images:')\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" \
|
||||
10 $UI_RESULT_W "" \
|
||||
2>&1 >/dev/tty)
|
||||
[[ -z "$IMAGES_DIR" ]] && exit 0
|
||||
else
|
||||
IMAGES_DIR="$ISO_DIR"
|
||||
fi
|
||||
msg_ok "$(translate 'Disk images found')"
|
||||
|
||||
if [[ ! -d "$IMAGES_DIR" ]]; then
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Directory Not Found')" \
|
||||
--msgbox "\n$(translate 'The specified directory does not exist:')\n\n$IMAGES_DIR" \
|
||||
$UI_MSG_H $UI_MSG_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGES=$(find "$IMAGES_DIR" -maxdepth 1 -type f \
|
||||
\( -name "*.img" -o -name "*.qcow2" -o -name "*.vmdk" -o -name "*.raw" \) \
|
||||
-printf '%f\n' 2>/dev/null | sort)
|
||||
|
||||
if [[ -z "$IMAGES" ]]; then
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'No Disk Images Found')" \
|
||||
--msgbox "\n$(translate 'No compatible disk images found in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" \
|
||||
$UI_RESULT_H $UI_RESULT_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# ── Step 4: Select images ─────────────────────────────────
|
||||
IMAGE_OPTIONS=()
|
||||
while read -r img; do
|
||||
IMAGE_OPTIONS+=("$img" "" "OFF")
|
||||
while IFS= read -r img; do
|
||||
IMAGE_OPTIONS+=("$img" "" "OFF")
|
||||
done <<< "$IMAGES"
|
||||
|
||||
SELECTED_IMAGES=$(whiptail --title "$(translate 'Select Disk Images')" --checklist "$(translate 'Select the disk images to import:')" 20 60 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
||||
SELECTED_IMAGES_STR=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Select Disk Images')" \
|
||||
--checklist "$(translate 'Select one or more disk images to import:')" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"${IMAGE_OPTIONS[@]}" \
|
||||
2>&1 >/dev/tty)
|
||||
[[ -z "$SELECTED_IMAGES_STR" ]] && exit 0
|
||||
|
||||
if [ -z "$SELECTED_IMAGES" ]; then
|
||||
|
||||
exit 1
|
||||
eval "declare -a SELECTED_ARRAY=($SELECTED_IMAGES_STR)"
|
||||
|
||||
|
||||
# ── Step 5: Per-image options ─────────────────────────────
|
||||
declare -a IMG_NAMES=()
|
||||
declare -a IMG_INTERFACES=()
|
||||
declare -a IMG_SSD_OPTIONS=()
|
||||
declare -a IMG_BOOTABLE=()
|
||||
|
||||
for IMAGE in "${SELECTED_ARRAY[@]}"; do
|
||||
IMAGE="${IMAGE//\"/}"
|
||||
|
||||
INTERFACE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Interface Type') — $IMAGE" \
|
||||
--default-item "scsi" \
|
||||
--menu "$(translate 'Select the interface type for:') $IMAGE" \
|
||||
$UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
|
||||
"scsi" "SCSI $(translate '(recommended)')" \
|
||||
"virtio" "VirtIO" \
|
||||
"sata" "SATA" \
|
||||
"ide" "IDE" \
|
||||
2>&1 >/dev/tty)
|
||||
[[ -z "$INTERFACE" ]] && continue
|
||||
|
||||
SSD_OPTION=""
|
||||
if [[ "$INTERFACE" != "virtio" ]]; then
|
||||
if dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'SSD Emulation') — $IMAGE" \
|
||||
--yesno "\n$(translate 'Enable SSD emulation for this disk?')" \
|
||||
$UI_YESNO_H $UI_YESNO_W; then
|
||||
SSD_OPTION=",ssd=1"
|
||||
fi
|
||||
fi
|
||||
|
||||
BOOTABLE="no"
|
||||
if dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Boot Disk') — $IMAGE" \
|
||||
--yesno "\n$(translate 'Set this disk as the primary boot disk?')" \
|
||||
$UI_YESNO_H $UI_YESNO_W; then
|
||||
BOOTABLE="yes"
|
||||
fi
|
||||
|
||||
IMG_NAMES+=("$IMAGE")
|
||||
IMG_INTERFACES+=("$INTERFACE")
|
||||
IMG_SSD_OPTIONS+=("$SSD_OPTION")
|
||||
IMG_BOOTABLE+=("$BOOTABLE")
|
||||
done
|
||||
|
||||
if [[ ${#IMG_NAMES[@]} -eq 0 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
# ==========================================================
|
||||
# PHASE 2 — EXECUTION
|
||||
# show_proxmenux_logo appears here exactly once.
|
||||
# No dialogs from this point on.
|
||||
# ==========================================================
|
||||
|
||||
# 4. Import each selected image
|
||||
for IMAGE in $SELECTED_IMAGES; do
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate 'Import Disk Image to VM')"
|
||||
|
||||
VM_NAME=$(qm config "$VMID" 2>/dev/null | awk '/^name:/ {print $2}')
|
||||
msg_ok "$(translate 'VM:') ${VM_NAME:-VM-$VMID} (${VMID})"
|
||||
msg_ok "$(translate 'Storage:') $STORAGE"
|
||||
msg_ok "$(translate 'Image directory:') $IMAGES_DIR"
|
||||
msg_ok "$(translate 'Images to import:') ${#IMG_NAMES[@]}"
|
||||
echo ""
|
||||
|
||||
IMAGE=$(echo "$IMAGE" | tr -d '"')
|
||||
PROCESSED=0
|
||||
FAILED=0
|
||||
|
||||
for i in "${!IMG_NAMES[@]}"; do
|
||||
IMAGE="${IMG_NAMES[$i]}"
|
||||
INTERFACE="${IMG_INTERFACES[$i]}"
|
||||
SSD_OPTION="${IMG_SSD_OPTIONS[$i]}"
|
||||
BOOTABLE="${IMG_BOOTABLE[$i]}"
|
||||
FULL_PATH="$IMAGES_DIR/$IMAGE"
|
||||
|
||||
INTERFACE=$(whiptail --title "$(translate 'Interface Type')" --menu "$(translate 'Select the interface type for the image:') $IMAGE" 15 40 4 \
|
||||
"sata" "SATA" \
|
||||
"scsi" "SCSI" \
|
||||
"virtio" "VirtIO" \
|
||||
"ide" "IDE" 3>&1 1>&2 2>&3)
|
||||
if [[ ! -f "$FULL_PATH" ]]; then
|
||||
msg_error "$(translate 'Image file not found:') $FULL_PATH"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "$INTERFACE" ]; then
|
||||
msg_error "$(translate 'No interface type selected for') $IMAGE"
|
||||
continue
|
||||
# Snapshot of unused entries before import for reliable detection
|
||||
BEFORE_UNUSED=$(qm config "$VMID" 2>/dev/null | grep -E '^unused[0-9]+:' || true)
|
||||
|
||||
TEMP_STATUS_FILE=$(mktemp)
|
||||
TEMP_DISK_FILE=$(mktemp)
|
||||
|
||||
msg_info "$(translate 'Importing') $IMAGE..."
|
||||
|
||||
(
|
||||
qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1
|
||||
echo $? > "$TEMP_STATUS_FILE"
|
||||
) | while IFS= read -r line; do
|
||||
if [[ "$line" =~ [0-9]+\.[0-9]+% ]]; then
|
||||
echo -ne "\r${TAB}${BL}$(translate 'Importing') ${IMAGE}${CL} ${BASH_REMATCH[0]} "
|
||||
fi
|
||||
if echo "$line" | grep -qiF "successfully imported disk"; then
|
||||
echo "$line" | sed -n "s/.*successfully imported disk as '\\([^']*\\)'.*/\\1/p" > "$TEMP_DISK_FILE"
|
||||
fi
|
||||
done
|
||||
echo -ne "\n"
|
||||
|
||||
IMPORT_STATUS=$(cat "$TEMP_STATUS_FILE" 2>/dev/null)
|
||||
rm -f "$TEMP_STATUS_FILE"
|
||||
[[ -z "$IMPORT_STATUS" ]] && IMPORT_STATUS=1
|
||||
|
||||
if [[ "$IMPORT_STATUS" -ne 0 ]]; then
|
||||
msg_error "$(translate 'Failed to import') $IMAGE"
|
||||
rm -f "$TEMP_DISK_FILE"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
msg_ok "$(translate 'Image imported:') $IMAGE"
|
||||
|
||||
# Primary: parse disk name from qm importdisk output
|
||||
IMPORTED_DISK=$(cat "$TEMP_DISK_FILE" 2>/dev/null | xargs)
|
||||
rm -f "$TEMP_DISK_FILE"
|
||||
|
||||
# Fallback: compare unused entries before/after import
|
||||
if [[ -z "$IMPORTED_DISK" ]]; then
|
||||
AFTER_UNUSED=$(qm config "$VMID" 2>/dev/null | grep -E '^unused[0-9]+:' || true)
|
||||
NEW_LINE=$(comm -13 \
|
||||
<(echo "$BEFORE_UNUSED" | sort) \
|
||||
<(echo "$AFTER_UNUSED" | sort) | head -1)
|
||||
if [[ -n "$NEW_LINE" ]]; then
|
||||
IMPORTED_DISK=$(echo "$NEW_LINE" | cut -d':' -f2- | xargs)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$IMPORTED_DISK" ]]; then
|
||||
msg_error "$(translate 'Could not identify the imported disk in VM config')"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Find the unusedN key that holds this disk (needed to clean it up after assignment)
|
||||
IMPORTED_ID=$(qm config "$VMID" 2>/dev/null | grep -F "$IMPORTED_DISK" | cut -d':' -f1 | head -1)
|
||||
|
||||
# Find next available slot for the chosen interface
|
||||
LAST_SLOT=$(qm config "$VMID" 2>/dev/null | grep -oE "^${INTERFACE}[0-9]+" | grep -oE '[0-9]+' | sort -n | tail -1)
|
||||
if [[ -z "$LAST_SLOT" ]]; then
|
||||
NEXT_SLOT=0
|
||||
else
|
||||
NEXT_SLOT=$((LAST_SLOT + 1))
|
||||
fi
|
||||
|
||||
msg_info "$(translate 'Configuring disk as') ${INTERFACE}${NEXT_SLOT}..."
|
||||
if qm set "$VMID" "--${INTERFACE}${NEXT_SLOT}" "${IMPORTED_DISK}${SSD_OPTION}" >/dev/null 2>&1; then
|
||||
msg_ok "$(translate 'Disk configured as') ${INTERFACE}${NEXT_SLOT}${SSD_OPTION:+ (SSD)}"
|
||||
|
||||
# Remove the unusedN entry now that the disk is properly assigned
|
||||
if [[ -n "$IMPORTED_ID" ]]; then
|
||||
qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
FULL_PATH="$IMAGES_DIR/$IMAGE"
|
||||
|
||||
|
||||
msg_info "$(translate 'Importing image:')"
|
||||
|
||||
|
||||
TEMP_DISK_FILE=$(mktemp)
|
||||
|
||||
|
||||
qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1 | while read -r line; do
|
||||
if [[ "$line" =~ transferred ]]; then
|
||||
|
||||
PERCENT=$(echo "$line" | grep -oP "\d+\.\d+(?=%)")
|
||||
|
||||
echo -ne "\r${TAB}${BL}-$(translate 'Importing image:') $IMAGE-${CL} ${PERCENT}%"
|
||||
elif [[ "$line" =~ successfully\ imported\ disk ]]; then
|
||||
|
||||
echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE"
|
||||
fi
|
||||
done
|
||||
echo -ne "\n"
|
||||
|
||||
IMPORT_STATUS=${PIPESTATUS[0]}
|
||||
|
||||
if [ $IMPORT_STATUS -eq 0 ]; then
|
||||
msg_ok "$(translate 'Image imported successfully')"
|
||||
|
||||
|
||||
IMPORTED_DISK=$(cat "$TEMP_DISK_FILE")
|
||||
rm -f "$TEMP_DISK_FILE"
|
||||
|
||||
|
||||
if [ -z "$IMPORTED_DISK" ]; then
|
||||
|
||||
STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}')
|
||||
|
||||
if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then
|
||||
|
||||
UNUSED_LINE=$(qm config "$VMID" | grep -E '^unused[0-9]+:')
|
||||
IMPORTED_ID=$(echo "$UNUSED_LINE" | cut -d: -f1)
|
||||
IMPORTED_DISK=$(echo "$UNUSED_LINE" | cut -d: -f2- | xargs)
|
||||
else
|
||||
|
||||
IMPORTED_DISK=$(qm config "$VMID" | grep -E 'unused[0-9]+' | tail -1 | cut -d: -f2- | xargs)
|
||||
IMPORTED_ID=$(qm config "$VMID" | grep -E 'unused[0-9]+' | tail -1 | cut -d: -f1)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$IMPORTED_DISK" ]; then
|
||||
|
||||
EXISTING_DISKS=$(qm config "$VMID" | grep -oP "${INTERFACE}\d+" | sort -n)
|
||||
if [ -z "$EXISTING_DISKS" ]; then
|
||||
NEXT_SLOT=0
|
||||
else
|
||||
LAST_SLOT=$(echo "$EXISTING_DISKS" | tail -n1 | sed "s/${INTERFACE}//")
|
||||
NEXT_SLOT=$((LAST_SLOT + 1))
|
||||
fi
|
||||
|
||||
|
||||
if [ "$INTERFACE" != "virtio" ]; then
|
||||
if (whiptail --title "$(translate 'SSD Emulation')" --yesno "$(translate 'Do you want to use SSD emulation for this disk?')" 10 60); then
|
||||
SSD_OPTION=",ssd=1"
|
||||
else
|
||||
SSD_OPTION=""
|
||||
fi
|
||||
else
|
||||
SSD_OPTION=""
|
||||
fi
|
||||
|
||||
msg_info "$(translate 'Configuring disk')"
|
||||
|
||||
|
||||
if qm set "$VMID" --${INTERFACE}${NEXT_SLOT} "$IMPORTED_DISK${SSD_OPTION}" &>/dev/null; then
|
||||
msg_ok "$(translate 'Image') $IMAGE $(translate 'configured as') ${INTERFACE}${NEXT_SLOT}"
|
||||
|
||||
|
||||
if [[ -n "$IMPORTED_ID" ]]; then
|
||||
qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
|
||||
if (whiptail --title "$(translate 'Make Bootable')" --yesno "$(translate 'Do you want to make this disk bootable?')" 10 60); then
|
||||
msg_info "$(translate 'Configuring disk as bootable')"
|
||||
|
||||
if qm set "$VMID" --boot c --bootdisk ${INTERFACE}${NEXT_SLOT} &>/dev/null; then
|
||||
msg_ok "$(translate 'Disk configured as bootable')"
|
||||
else
|
||||
msg_error "$(translate 'Could not configure the disk as bootable')"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
msg_error "$(translate 'Could not configure disk') ${INTERFACE}${NEXT_SLOT} $(translate 'for VM') $VMID"
|
||||
echo "DEBUG: Tried to configure: --${INTERFACE}${NEXT_SLOT} \"$IMPORTED_DISK${SSD_OPTION}\""
|
||||
echo "DEBUG: VM config after import:"
|
||||
qm config "$VMID" | grep -E "(unused|${INTERFACE})"
|
||||
fi
|
||||
else
|
||||
msg_error "$(translate 'Could not find the imported disk')"
|
||||
echo "DEBUG: VM config after import:"
|
||||
qm config "$VMID"
|
||||
fi
|
||||
else
|
||||
msg_error "$(translate 'Could not import') $IMAGE"
|
||||
if [[ "$BOOTABLE" == "yes" ]]; then
|
||||
msg_info "$(translate 'Setting boot order...')"
|
||||
if qm set "$VMID" --boot "order=${INTERFACE}${NEXT_SLOT}" >/dev/null 2>&1; then
|
||||
msg_ok "$(translate 'Boot order set to') ${INTERFACE}${NEXT_SLOT}"
|
||||
else
|
||||
msg_error "$(translate 'Could not set boot order for') ${INTERFACE}${NEXT_SLOT}"
|
||||
fi
|
||||
fi
|
||||
|
||||
PROCESSED=$((PROCESSED + 1))
|
||||
else
|
||||
msg_error "$(translate 'Could not assign disk') ${INTERFACE}${NEXT_SLOT} $(translate 'to VM') $VMID"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [[ $FAILED -eq 0 ]]; then
|
||||
msg_ok "$(translate 'All images imported and configured successfully')"
|
||||
elif [[ $PROCESSED -gt 0 ]]; then
|
||||
msg_warn "$(translate 'Completed with errors —') $(translate 'imported:') $PROCESSED, $(translate 'failed:') $FAILED"
|
||||
else
|
||||
msg_error "$(translate 'All imports failed')"
|
||||
fi
|
||||
|
||||
|
||||
msg_ok "$(translate 'All selected images have been processed')"
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
msg_success "$(translate 'Press Enter to return to menu...')"
|
||||
read -r
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenux - Mount independent disk on Proxmox host
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||
# Version : 1.0
|
||||
# Last Updated: 08/04/2025
|
||||
# ==========================================================
|
||||
# Description:
|
||||
# This script detects unassigned physical disks and allows
|
||||
# the user to mount one of them on the host Proxmox system.
|
||||
# - Detects unmounted and unassigned disks.
|
||||
# - Filters out ZFS, LVM, RAID and system disks.
|
||||
# - Allows selecting a disk.
|
||||
# - Prepares partition and filesystem if needed.
|
||||
# - Mounts the disk in the host at a defined mount point.
|
||||
# ==========================================================
|
||||
|
||||
# Configuration ============================================
|
||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
VENV_PATH="/opt/googletrans-env"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
load_language
|
||||
initialize_cache
|
||||
# ==========================================================
|
||||
|
||||
get_disk_info() {
|
||||
local disk=$1
|
||||
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
|
||||
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
|
||||
echo "$MODEL" "$SIZE"
|
||||
}
|
||||
|
||||
msg_info "$(translate "Detecting available disks...")"
|
||||
|
||||
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
|
||||
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
|
||||
|
||||
ZFS_DISKS=""
|
||||
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
|
||||
|
||||
for entry in $ZFS_RAW; do
|
||||
path=""
|
||||
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
|
||||
if [ -e "/dev/disk/by-id/$entry" ]; then
|
||||
path=$(readlink -f "/dev/disk/by-id/$entry")
|
||||
fi
|
||||
elif [[ "$entry" == /dev/* ]]; then
|
||||
path="$entry"
|
||||
fi
|
||||
|
||||
if [ -n "$path" ]; then
|
||||
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
|
||||
if [ -n "$base_disk" ]; then
|
||||
ZFS_DISKS+="/dev/$base_disk"$'\n'
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
|
||||
|
||||
is_disk_in_use() {
|
||||
local disk="$1"
|
||||
|
||||
while read -r part fstype; do
|
||||
case "$fstype" in
|
||||
zfs_member|linux_raid_member)
|
||||
return 0 ;;
|
||||
esac
|
||||
|
||||
if echo "$MOUNTED_DISKS" | grep -q "/dev/$part"; then
|
||||
return 0
|
||||
fi
|
||||
done < <(lsblk -ln -o NAME,FSTYPE "$disk" | tail -n +2)
|
||||
|
||||
if echo "$USED_DISKS" | grep -q "$disk" || echo "$ZFS_DISKS" | grep -q "$disk"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
FREE_DISKS=()
|
||||
|
||||
LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u)
|
||||
RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u)
|
||||
|
||||
while read -r DISK; do
|
||||
[[ "$DISK" =~ /dev/zd ]] && continue
|
||||
|
||||
INFO=($(get_disk_info "$DISK"))
|
||||
MODEL="${INFO[@]::${#INFO[@]}-1}"
|
||||
SIZE="${INFO[-1]}"
|
||||
LABEL=""
|
||||
SHOW_DISK=true
|
||||
|
||||
IS_MOUNTED=false
|
||||
IS_RAID=false
|
||||
IS_ZFS=false
|
||||
IS_LVM=false
|
||||
|
||||
while read -r part fstype; do
|
||||
[[ "$fstype" == "zfs_member" ]] && IS_ZFS=true
|
||||
[[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true
|
||||
[[ "$fstype" == "LVM2_member" ]] && IS_LVM=true
|
||||
if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then
|
||||
IS_MOUNTED=true
|
||||
fi
|
||||
done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2)
|
||||
|
||||
REAL_PATH=$(readlink -f "$DISK")
|
||||
if echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
|
||||
IS_MOUNTED=true
|
||||
fi
|
||||
|
||||
|
||||
USED_BY=""
|
||||
REAL_PATH=$(readlink -f "$DISK")
|
||||
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
|
||||
|
||||
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
|
||||
USED_BY="⚠ $(translate "In use")"
|
||||
else
|
||||
for SYMLINK in /dev/disk/by-id/*; do
|
||||
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
|
||||
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
|
||||
USED_BY="⚠ $(translate "In use")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
|
||||
if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
|
||||
if grep -q "active raid" /proc/mdstat; then
|
||||
SHOW_DISK=false
|
||||
fi
|
||||
fi
|
||||
|
||||
if $IS_ZFS; then
|
||||
SHOW_DISK=false
|
||||
fi
|
||||
|
||||
if $IS_MOUNTED; then
|
||||
SHOW_DISK=false
|
||||
fi
|
||||
|
||||
if $SHOW_DISK; then
|
||||
[[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]"
|
||||
[[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID"
|
||||
[[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM"
|
||||
[[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS"
|
||||
|
||||
DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
|
||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF")
|
||||
fi
|
||||
|
||||
done < <(lsblk -dn -e 7,11 -o PATH)
|
||||
|
||||
if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
|
||||
cleanup
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No available disks found on the host.")" 8 50
|
||||
clear
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Available disks detected.")"
|
||||
|
||||
MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1)
|
||||
TOTAL_WIDTH=$((MAX_WIDTH + 20))
|
||||
TOTAL_WIDTH=$((TOTAL_WIDTH < 50 ? 50 : TOTAL_WIDTH))
|
||||
|
||||
SELECTED=$(whiptail --title "$(translate "Select Disk")" --radiolist \
|
||||
"$(translate "Select the disk you want to mount on the host:")" 20 $TOTAL_WIDTH 10 "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
if [ -z "$SELECTED" ]; then
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No disk was selected.")" 10 50
|
||||
clear
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
|
||||
msg_ok "$(translate "Disk selected successfully:") $SELECTED"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
################################################################
|
||||
|
||||
|
||||
|
||||
|
||||
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
|
||||
|
||||
SKIP_FORMAT=false
|
||||
DEFAULT_MOUNT="/mnt/data_shared"
|
||||
|
||||
if [ -n "$PARTITION" ]; then
|
||||
PARTITION="/dev/$PARTITION"
|
||||
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
|
||||
|
||||
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
|
||||
SKIP_FORMAT=true
|
||||
msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION."
|
||||
else
|
||||
whiptail --title "$(translate "Unsupported Filesystem")" --yesno "$(translate "The partition") $PARTITION $(translate "has an unsupported filesystem ($CURRENT_FS).\\nDo you want to format it?")" 10 70
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
CURRENT_FS=$(lsblk -no FSTYPE "$SELECTED" | xargs)
|
||||
|
||||
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
|
||||
SKIP_FORMAT=true
|
||||
PARTITION="$SELECTED"
|
||||
msg_ok "$(translate "Detected filesystem") $CURRENT_FS $(translate "directly on disk") $SELECTED."
|
||||
else
|
||||
whiptail --title "$(translate "No Valid Partitions")" --yesno "$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e "$(translate "Creating partition table and partition...")"
|
||||
parted -s "$SELECTED" mklabel gpt
|
||||
parted -s "$SELECTED" mkpart primary 0% 100%
|
||||
sleep 2
|
||||
partprobe "$SELECTED"
|
||||
sleep 2
|
||||
|
||||
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
|
||||
if [ -n "$PARTITION" ]; then
|
||||
PARTITION="/dev/$PARTITION"
|
||||
else
|
||||
whiptail --title "$(translate "Partition Error")" --msgbox "$(translate "Failed to create partition on disk") $SELECTED." 8 70
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$SKIP_FORMAT" != true ]; then
|
||||
FORMAT_TYPE=$(whiptail --title "$(translate "Select Format Type")" --menu "$(translate "Select the filesystem type for") $PARTITION:" 15 60 5 \
|
||||
"ext4" "$(translate "Extended Filesystem 4 (recommended)")" \
|
||||
"xfs" "XFS" \
|
||||
"btrfs" "Btrfs" 3>&1 1>&2 2>&3)
|
||||
|
||||
if [ -z "$FORMAT_TYPE" ]; then
|
||||
whiptail --title "$(translate "Format Cancelled")" --msgbox "$(translate "Format operation cancelled. The disk will not be added.")" 8 60
|
||||
exit 0
|
||||
fi
|
||||
|
||||
whiptail --title "$(translate "WARNING")" --yesno "$(translate "WARNING: This operation will FORMAT the disk") $PARTITION $(translate "with") $FORMAT_TYPE.\\n\\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\\n\\n$(translate "Are you sure you want to continue")" 15 70
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
|
||||
case "$FORMAT_TYPE" in
|
||||
"ext4") mkfs.ext4 -F "$PARTITION" ;;
|
||||
"xfs") mkfs.xfs -f "$PARTITION" ;;
|
||||
"btrfs") mkfs.btrfs -f "$PARTITION" ;;
|
||||
esac
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
cleanup
|
||||
whiptail --title "$(translate "Format Failed")" --msgbox "$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE." 12 70
|
||||
exit 1
|
||||
else
|
||||
msg_ok "$(translate "Partition") $PARTITION $(translate "successfully formatted with") $FORMAT_TYPE."
|
||||
partprobe "$SELECTED"
|
||||
sleep 2
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
||||
|
||||
################################################################
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
MOUNT_POINT=$(whiptail --title "$(translate "Mount Point")" \
|
||||
--inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/data_shared):")" \
|
||||
10 60 "$DEFAULT_MOUNT" 3>&1 1>&2 2>&3)
|
||||
|
||||
if [ -z "$MOUNT_POINT" ]; then
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No mount point was specified.")" 8 40
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Mount point specified:") $MOUNT_POINT"
|
||||
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
|
||||
UUID=$(blkid -s UUID -o value "$PARTITION")
|
||||
|
||||
# Obtener sistema de archivos real
|
||||
FS_TYPE=$(lsblk -no FSTYPE "$PARTITION" | xargs)
|
||||
|
||||
FSTAB_ENTRY="UUID=$UUID $MOUNT_POINT $FS_TYPE defaults 0 0"
|
||||
|
||||
if grep -q "UUID=$UUID" /etc/fstab; then
|
||||
sed -i "s|^.*UUID=$UUID.*|$FSTAB_ENTRY|" /etc/fstab
|
||||
msg_ok "$(translate "fstab entry updated for") $UUID"
|
||||
else
|
||||
echo "$FSTAB_ENTRY" >> /etc/fstab
|
||||
msg_ok "$(translate "fstab entry added for") $UUID"
|
||||
fi
|
||||
|
||||
|
||||
##################################################################
|
||||
|
||||
mount "$MOUNT_POINT" 2> >(grep -v "systemd still uses")
|
||||
|
||||
##################################################################
|
||||
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
if ! getent group sharedfiles >/dev/null; then
|
||||
groupadd sharedfiles
|
||||
msg_ok "$(translate "Group 'sharedfiles' created")"
|
||||
else
|
||||
msg_ok "$(translate "Group 'sharedfiles' already exists")"
|
||||
fi
|
||||
|
||||
chown root:sharedfiles "$MOUNT_POINT"
|
||||
chmod 2775 "$MOUNT_POINT"
|
||||
|
||||
whiptail --title "$(translate "Success")" --msgbox "$(translate "The disk has been successfully mounted at") $MOUNT_POINT" 8 60
|
||||
msg_ok "$(translate "Disk mounted at") $MOUNT_POINT"
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
else
|
||||
whiptail --title "$(translate "Mount Error")" --msgbox "$(translate "Failed to mount the disk at") $MOUNT_POINT" 8 60
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,146 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenux - Mount point from host into LXC container (CT)
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# License : MIT
|
||||
# Description : Mount a folder from /mnt on the host to a mount point in a CT
|
||||
# ==========================================================
|
||||
|
||||
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
|
||||
|
||||
#######################################################
|
||||
|
||||
CT_LIST=($(pct list | awk 'NR>1 {print $1":"$3}'))
|
||||
|
||||
if [[ ${#CT_LIST[@]} -eq 0 ]]; then
|
||||
whiptail --title "$(translate "No CTs")" --msgbox "$(translate "No containers found.")" 8 40
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CT_OPTIONS=()
|
||||
for entry in "${CT_LIST[@]}"; do
|
||||
ID="${entry%%:*}"
|
||||
NAME="${entry##*:}"
|
||||
CT_OPTIONS+=("$ID" "$NAME")
|
||||
done
|
||||
|
||||
CTID=$(whiptail --title "$(translate "Select CT")" --menu "$(translate "Select the container:")" 20 60 10 "${CT_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
||||
[[ -z "$CTID" ]] && exit 0
|
||||
|
||||
CT_STATUS=$(pct status "$CTID" | awk '{print $2}')
|
||||
if [ "$CT_STATUS" != "running" ]; then
|
||||
msg_info "$(translate "Starting CT") $CTID..."
|
||||
pct start "$CTID"
|
||||
sleep 2
|
||||
if [ "$(pct status "$CTID" | awk '{print $2}')" != "running" ]; then
|
||||
msg_error "$(translate "Failed to start the CT.")"
|
||||
exit 1
|
||||
fi
|
||||
msg_ok "$(translate "CT started successfully.")"
|
||||
fi
|
||||
|
||||
#######################################################
|
||||
|
||||
select_origin_path() {
|
||||
METHOD=$(whiptail --title "$(translate "Select Host Folder")" --menu "$(translate "How do you want to select the host folder to mount?")" 15 60 5 \
|
||||
"auto" "$(translate "Select from /mnt")" \
|
||||
"manual" "$(translate "Enter path manually")" 3>&1 1>&2 2>&3)
|
||||
|
||||
case "$METHOD" in
|
||||
auto)
|
||||
HOST_DIRS=(/mnt/*)
|
||||
OPTIONS=()
|
||||
for dir in "${HOST_DIRS[@]}"; do
|
||||
[[ -d "$dir" ]] && OPTIONS+=("$dir" "")
|
||||
done
|
||||
|
||||
ORIGIN=$(whiptail --title "$(translate "Select Host Folder")" --menu "$(translate "Select the folder to mount:")" 20 60 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
||||
[[ -z "$ORIGIN" ]] && return 1
|
||||
;;
|
||||
|
||||
manual)
|
||||
ORIGIN=$(whiptail --title "$(translate "Enter Path")" --inputbox "$(translate "Enter the full path to the host folder:")" 10 60 "/mnt/" 3>&1 1>&2 2>&3)
|
||||
[[ -z "$ORIGIN" ]] && return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -d "$ORIGIN" ]]; then
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "The selected path is not a valid directory:")\n$ORIGIN" 8 60
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Preparar permisos en el host para uso compartido
|
||||
SHARE_GID=999
|
||||
if ! getent group sharedfiles >/dev/null; then
|
||||
groupadd -g "$SHARE_GID" sharedfiles
|
||||
msg_ok "$(translate "Group 'sharedfiles' created in the host with GID $SHARE_GID")"
|
||||
else
|
||||
msg_ok "$(translate "Group 'sharedfiles' already exists in the host")"
|
||||
fi
|
||||
|
||||
chown root:sharedfiles "$ORIGIN"
|
||||
chmod 2775 "$ORIGIN"
|
||||
setfacl -d -m g:sharedfiles:rwx "$ORIGIN"
|
||||
setfacl -m g:sharedfiles:rwx "$ORIGIN"
|
||||
|
||||
msg_ok "$(translate "Host folder prepared with shared group and permissions.")"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
select_origin_path || exit 0
|
||||
|
||||
#######################################################
|
||||
|
||||
CT_NAME=$(pct config "$CTID" | awk -F: '/hostname/ {print $2}' | xargs)
|
||||
DEFAULT_MOUNT_POINT="/mnt/host_share"
|
||||
|
||||
MOUNT_POINT=$(whiptail --title "$(translate "Mount Point to CT")" \
|
||||
--inputbox "$(translate "Enter the mount point inside the CT (e.g., /mnt/host_share):")" \
|
||||
10 70 "$DEFAULT_MOUNT_POINT" 3>&1 1>&2 2>&3)
|
||||
|
||||
if [[ -z "$MOUNT_POINT" ]]; then
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No mount point specified.")" 8 60
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! pct exec "$CTID" -- test -d "$MOUNT_POINT"; then
|
||||
if whiptail --yesno "$(translate "Directory does not exist in the CT.")\n\n$MOUNT_POINT\n\n$(translate "Do you want to create it?")" 12 70 --title "$(translate "Create Directory")"; then
|
||||
pct exec "$CTID" -- mkdir -p "$MOUNT_POINT"
|
||||
msg_ok "$(translate "Directory created inside CT:") $MOUNT_POINT"
|
||||
else
|
||||
msg_error "$(translate "Directory not created. Operation cancelled.")"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
INDEX=0
|
||||
while pct config "$CTID" | grep -q "mp${INDEX}:"; do
|
||||
((INDEX++))
|
||||
[[ $INDEX -ge 100 ]] && msg_error "Too many mount points." && exit 1
|
||||
done
|
||||
|
||||
msg_info "$(translate "Mounting folder from host to CT...")"
|
||||
RESULT=$(pct set "$CTID" -mp${INDEX} "$ORIGIN,mp=$MOUNT_POINT,backup=0,ro=0,acl=1" 2>&1)
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
msg_ok "$(translate "Successfully mounted:")\n$ORIGIN → $CT_NAME:$MOUNT_POINT"
|
||||
else
|
||||
msg_error "$(translate "Error mounting folder:")\n$RESULT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
|
||||
exit 0
|
||||
@@ -1,446 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenux - Mount independent disk on Proxmox host
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : MIT
|
||||
# Version : 1.3-dialog
|
||||
# Last Updated: 13/12/2024
|
||||
# ==========================================================
|
||||
|
||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
VENV_PATH="/opt/googletrans-env"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
|
||||
mount_disk_host_bk() {
|
||||
|
||||
|
||||
|
||||
get_disk_info() {
|
||||
local disk=$1
|
||||
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
|
||||
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
|
||||
echo "$MODEL" "$SIZE"
|
||||
}
|
||||
|
||||
|
||||
is_usb_disk() {
|
||||
local disk=$1
|
||||
local disk_name=$(basename "$disk")
|
||||
|
||||
|
||||
if readlink -f "/sys/block/$disk_name/device" 2>/dev/null | grep -q "usb"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
||||
if udevadm info --query=property --name="$disk" 2>/dev/null | grep -q "ID_BUS=usb"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
is_system_disk() {
|
||||
local disk=$1
|
||||
local disk_name=$(basename "$disk")
|
||||
|
||||
|
||||
local system_mounts=$(df -h | grep -E '^\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/|/boot|/usr|/var|/home)$' | awk '{print $1}')
|
||||
|
||||
|
||||
for mount_dev in $system_mounts; do
|
||||
|
||||
local mount_disk=""
|
||||
if [[ "$mount_dev" =~ ^/dev/mapper/ ]]; then
|
||||
|
||||
local vg_name=$(lvs --noheadings -o vg_name "$mount_dev" 2>/dev/null | xargs)
|
||||
if [[ -n "$vg_name" ]]; then
|
||||
local pvs_list=$(pvs --noheadings -o pv_name -S vg_name="$vg_name" 2>/dev/null | xargs)
|
||||
for pv in $pvs_list; do
|
||||
if [[ -n "$pv" && -e "$pv" ]]; then
|
||||
mount_disk=$(lsblk -no PKNAME "$pv" 2>/dev/null)
|
||||
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
elif [[ "$mount_dev" =~ ^/dev/[hsv]d[a-z][0-9]* || "$mount_dev" =~ ^/dev/nvme[0-9]+n[0-9]+p[0-9]+ ]]; then
|
||||
|
||||
mount_disk=$(lsblk -no PKNAME "$mount_dev" 2>/dev/null)
|
||||
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
local fs_type=$(lsblk -no FSTYPE "$disk" 2>/dev/null | head -1)
|
||||
if [[ "$fs_type" == "btrfs" ]]; then
|
||||
|
||||
local temp_mount=$(mktemp -d)
|
||||
if mount -o ro "$disk" "$temp_mount" 2>/dev/null; then
|
||||
|
||||
if btrfs subvolume list "$temp_mount" 2>/dev/null | grep -qE '(@|@home|@var|@boot|@root|root)'; then
|
||||
umount "$temp_mount" 2>/dev/null
|
||||
rmdir "$temp_mount" 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
umount "$temp_mount" 2>/dev/null
|
||||
fi
|
||||
rmdir "$temp_mount" 2>/dev/null
|
||||
|
||||
|
||||
while read -r part; do
|
||||
if [[ -n "$part" ]]; then
|
||||
local part_fs=$(lsblk -no FSTYPE "/dev/$part" 2>/dev/null)
|
||||
if [[ "$part_fs" == "btrfs" ]]; then
|
||||
local mount_point=$(lsblk -no MOUNTPOINT "/dev/$part" 2>/dev/null)
|
||||
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
|
||||
fi
|
||||
|
||||
|
||||
local disk_uuid=$(blkid -s UUID -o value "$disk" 2>/dev/null)
|
||||
local part_uuids=()
|
||||
while read -r part; do
|
||||
if [[ -n "$part" ]]; then
|
||||
local uuid=$(blkid -s UUID -o value "/dev/$part" 2>/dev/null)
|
||||
if [[ -n "$uuid" ]]; then
|
||||
part_uuids+=("$uuid")
|
||||
fi
|
||||
fi
|
||||
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
|
||||
|
||||
|
||||
for uuid in "${part_uuids[@]}" "$disk_uuid"; do
|
||||
if [[ -n "$uuid" ]] && grep -q "UUID=$uuid" /etc/fstab; then
|
||||
local mount_point=$(grep "UUID=$uuid" /etc/fstab | awk '{print $2}')
|
||||
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
if grep -q "$disk" /etc/fstab; then
|
||||
local mount_point=$(grep "$disk" /etc/fstab | awk '{print $2}')
|
||||
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
local disk_count=$(lsblk -dn -e 7,11 -o PATH | wc -l)
|
||||
if [[ "$disk_count" -eq 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
msg_info "$(translate "Detecting available disks...")"
|
||||
|
||||
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
|
||||
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
|
||||
|
||||
ZFS_DISKS=""
|
||||
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
|
||||
|
||||
for entry in $ZFS_RAW; do
|
||||
path=""
|
||||
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
|
||||
if [ -e "/dev/disk/by-id/$entry" ]; then
|
||||
path=$(readlink -f "/dev/disk/by-id/$entry")
|
||||
fi
|
||||
elif [[ "$entry" == /dev/* ]]; then
|
||||
path="$entry"
|
||||
fi
|
||||
|
||||
if [ -n "$path" ]; then
|
||||
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
|
||||
if [ -n "$base_disk" ]; then
|
||||
ZFS_DISKS+="/dev/$base_disk"$'\n'
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
|
||||
|
||||
LVM_DEVICES=$(
|
||||
pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') |
|
||||
while read -r dev; do
|
||||
[[ -n "$dev" && -e "$dev" ]] && readlink -f "$dev"
|
||||
done | sort -u
|
||||
)
|
||||
|
||||
FREE_DISKS=()
|
||||
|
||||
while read -r DISK; do
|
||||
[[ "$DISK" =~ /dev/zd ]] && continue
|
||||
|
||||
INFO=($(get_disk_info "$DISK"))
|
||||
MODEL="${INFO[@]::${#INFO[@]}-1}"
|
||||
SIZE="${INFO[-1]}"
|
||||
LABEL=""
|
||||
SHOW_DISK=true
|
||||
|
||||
IS_MOUNTED=false
|
||||
IS_RAID=false
|
||||
IS_ZFS=false
|
||||
IS_LVM=false
|
||||
IS_SYSTEM=false
|
||||
IS_USB=false
|
||||
|
||||
|
||||
if is_system_disk "$DISK"; then
|
||||
IS_SYSTEM=true
|
||||
fi
|
||||
|
||||
|
||||
if is_usb_disk "$DISK"; then
|
||||
IS_USB=true
|
||||
fi
|
||||
|
||||
while read -r part fstype; do
|
||||
[[ "$fstype" == "zfs_member" ]] && IS_ZFS=true
|
||||
[[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true
|
||||
[[ "$fstype" == "LVM2_member" ]] && IS_LVM=true
|
||||
if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then
|
||||
IS_MOUNTED=true
|
||||
fi
|
||||
done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2)
|
||||
|
||||
REAL_PATH=""
|
||||
if [[ -n "$DISK" && -e "$DISK" ]]; then
|
||||
REAL_PATH=$(readlink -f "$DISK")
|
||||
fi
|
||||
if [[ -n "$REAL_PATH" ]] && echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
|
||||
IS_MOUNTED=true
|
||||
fi
|
||||
|
||||
USED_BY=""
|
||||
REAL_PATH=""
|
||||
if [[ -n "$DISK" && -e "$DISK" ]]; then
|
||||
REAL_PATH=$(readlink -f "$DISK")
|
||||
fi
|
||||
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
|
||||
|
||||
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
|
||||
USED_BY="⚠ $(translate "In use")"
|
||||
else
|
||||
for SYMLINK in /dev/disk/by-id/*; do
|
||||
[[ -e "$SYMLINK" ]] || continue
|
||||
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
|
||||
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
|
||||
USED_BY="⚠ $(translate "In use")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
|
||||
if grep -q "active raid" /proc/mdstat; then
|
||||
SHOW_DISK=false
|
||||
fi
|
||||
fi
|
||||
if $IS_ZFS; then SHOW_DISK=false; fi
|
||||
if $IS_MOUNTED; then SHOW_DISK=false; fi
|
||||
if $IS_SYSTEM; then SHOW_DISK=false; fi
|
||||
|
||||
if $SHOW_DISK; then
|
||||
[[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]"
|
||||
[[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID"
|
||||
[[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM"
|
||||
[[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS"
|
||||
|
||||
|
||||
if $IS_USB; then
|
||||
LABEL+=" USB"
|
||||
else
|
||||
LABEL+=" $(translate "Internal")"
|
||||
fi
|
||||
|
||||
DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
|
||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "off")
|
||||
fi
|
||||
done < <(lsblk -dn -e 7,11 -o PATH)
|
||||
|
||||
if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
|
||||
dialog --title "$(translate "Error")" --msgbox "$(translate "No available disks found on the host.")" 8 60
|
||||
clear
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Available disks detected.")"
|
||||
|
||||
# Building the array for dialog (format: tag item on/off tag item on/off...)
|
||||
DLG_LIST=()
|
||||
for ((i=0; i<${#FREE_DISKS[@]}; i+=3)); do
|
||||
DLG_LIST+=("${FREE_DISKS[i]}" "${FREE_DISKS[i+1]}" "${FREE_DISKS[i+2]}")
|
||||
done
|
||||
|
||||
SELECTED=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Select Disk")" \
|
||||
--radiolist "\n$(translate "Select the disk you want to mount on the host:")" 20 90 10 \
|
||||
"${DLG_LIST[@]}" 2>&1 >/dev/tty)
|
||||
|
||||
if [ -z "$SELECTED" ]; then
|
||||
dialog --title "$(translate "Error")" --msgbox "$(translate "No disk was selected.")" 8 50
|
||||
clear
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Disk selected successfully:") $SELECTED"
|
||||
|
||||
# ------------------- Partitions and formatting ------------------------
|
||||
|
||||
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
|
||||
SKIP_FORMAT=false
|
||||
DEFAULT_MOUNT="/mnt/backup"
|
||||
|
||||
if [ -n "$PARTITION" ]; then
|
||||
PARTITION="/dev/$PARTITION"
|
||||
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
|
||||
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
|
||||
SKIP_FORMAT=true
|
||||
msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION."
|
||||
else
|
||||
dialog --title "$(translate "Unsupported Filesystem")" --yesno \
|
||||
"$(translate "The partition") $PARTITION $(translate "has an unsupported filesystem ($CURRENT_FS).\nDo you want to format it?")" 10 70
|
||||
if [ $? -ne 0 ]; then exit 0; fi
|
||||
fi
|
||||
else
|
||||
CURRENT_FS=$(lsblk -no FSTYPE "$SELECTED" | xargs)
|
||||
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
|
||||
SKIP_FORMAT=true
|
||||
PARTITION="$SELECTED"
|
||||
msg_ok "$(translate "Detected filesystem") $CURRENT_FS $(translate "directly on disk") $SELECTED."
|
||||
else
|
||||
dialog --title "$(translate "No Valid Partitions")" --yesno \
|
||||
"$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70
|
||||
if [ $? -ne 0 ]; then exit 0; fi
|
||||
|
||||
echo -e "$(translate "Creating partition table and partition...")"
|
||||
parted -s "$SELECTED" mklabel gpt
|
||||
parted -s "$SELECTED" mkpart primary 0% 100%
|
||||
sleep 2
|
||||
partprobe "$SELECTED"
|
||||
sleep 2
|
||||
|
||||
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
|
||||
if [ -n "$PARTITION" ]; then
|
||||
PARTITION="/dev/$PARTITION"
|
||||
else
|
||||
dialog --title "$(translate "Partition Error")" --msgbox \
|
||||
"$(translate "Failed to create partition on disk") $SELECTED." 8 70
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$SKIP_FORMAT" != true ]; then
|
||||
FORMAT_TYPE=$(dialog --title "$(translate "Select Format Type")" --menu \
|
||||
"$(translate "Select the filesystem type for") $PARTITION:" 15 60 5 \
|
||||
"ext4" "$(translate "Extended Filesystem 4 (recommended)")" \
|
||||
"xfs" "XFS" \
|
||||
"btrfs" "Btrfs" 2>&1 >/dev/tty)
|
||||
if [ -z "$FORMAT_TYPE" ]; then
|
||||
dialog --title "$(translate "Format Cancelled")" --msgbox \
|
||||
"$(translate "Format operation cancelled. The disk will not be added.")" 8 60
|
||||
exit 0
|
||||
fi
|
||||
|
||||
dialog --title "$(translate "WARNING")" --yesno \
|
||||
"$(translate "WARNING: This operation will FORMAT the disk") $PARTITION $(translate "with") $FORMAT_TYPE.\n\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\n\n$(translate "Are you sure you want to continue")" 15 70
|
||||
if [ $? -ne 0 ]; then exit 0; fi
|
||||
|
||||
echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
|
||||
case "$FORMAT_TYPE" in
|
||||
"ext4") mkfs.ext4 -F "$PARTITION" ;;
|
||||
"xfs") mkfs.xfs -f "$PARTITION" ;;
|
||||
"btrfs") mkfs.btrfs -f "$PARTITION" ;;
|
||||
esac
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
cleanup
|
||||
dialog --title "$(translate "Format Failed")" --msgbox \
|
||||
"$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE." 12 70
|
||||
exit 1
|
||||
else
|
||||
msg_ok "$(translate "Partition") $PARTITION $(translate "successfully formatted with") $FORMAT_TYPE."
|
||||
partprobe "$SELECTED"
|
||||
sleep 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ------------------- Mount point and permissions -------------------
|
||||
|
||||
MOUNT_POINT=$(dialog --title "$(translate "Mount Point")" \
|
||||
--inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/backup):")" \
|
||||
10 60 "$DEFAULT_MOUNT" 2>&1 >/dev/tty)
|
||||
if [ -z "$MOUNT_POINT" ]; then
|
||||
dialog --title "$(translate "Error")" --msgbox "$(translate "No mount point was specified.")" 8 40
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Mount point specified:") $MOUNT_POINT"
|
||||
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
|
||||
UUID=$(blkid -s UUID -o value "$PARTITION")
|
||||
FS_TYPE=$(lsblk -no FSTYPE "$PARTITION" | xargs)
|
||||
FSTAB_ENTRY="UUID=$UUID $MOUNT_POINT $FS_TYPE defaults 0 0"
|
||||
|
||||
if grep -q "UUID=$UUID" /etc/fstab; then
|
||||
sed -i "s|^.*UUID=$UUID.*|$FSTAB_ENTRY|" /etc/fstab
|
||||
msg_ok "$(translate "fstab entry updated for") $UUID"
|
||||
else
|
||||
echo "$FSTAB_ENTRY" >> /etc/fstab
|
||||
msg_ok "$(translate "fstab entry added for") $UUID"
|
||||
fi
|
||||
|
||||
mount "$MOUNT_POINT" 2> >(grep -v "systemd still uses")
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
if ! getent group sharedfiles >/dev/null; then
|
||||
groupadd sharedfiles
|
||||
msg_ok "$(translate "Group 'sharedfiles' created")"
|
||||
else
|
||||
msg_ok "$(translate "Group 'sharedfiles' already exists")"
|
||||
fi
|
||||
|
||||
chown root:sharedfiles "$MOUNT_POINT"
|
||||
chmod 2775 "$MOUNT_POINT"
|
||||
|
||||
dialog --title "$(translate "Success")" --msgbox "$(translate "The disk has been successfully mounted at") $MOUNT_POINT" 8 60
|
||||
echo "$MOUNT_POINT" > /usr/local/share/proxmenux/last_backup_mount.txt
|
||||
msg_ok "$(translate "Disk mounted at") $MOUNT_POINT"
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
else
|
||||
dialog --title "$(translate "Mount Error")" --msgbox "$(translate "Failed to mount the disk at") $MOUNT_POINT" 8 60
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
}
|
||||
|
||||
399
scripts/storage/smart-disk-test.sh
Normal file
399
scripts/storage/smart-disk-test.sh
Normal file
@@ -0,0 +1,399 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenux - SMART Disk Health & Test Tool
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||
# Version : 1.0
|
||||
# Last Updated: 12/04/2026
|
||||
# ==========================================================
|
||||
# Description:
|
||||
# SMART health check and disk testing tool for Proxmox VE.
|
||||
# Supports SATA/SAS disks (smartmontools) and NVMe drives (nvme-cli).
|
||||
# Exports results as JSON to /usr/local/share/proxmenux/smart/
|
||||
# for ProxMenux Monitor integration.
|
||||
# Long tests run on the drive hardware and persist after terminal close.
|
||||
# ==========================================================
|
||||
|
||||
# Configuration ============================================
|
||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
VENV_PATH="/opt/googletrans-env"
|
||||
BACKTITLE="ProxMenux"
|
||||
SMART_DIR="$BASE_DIR/smart"
|
||||
UI_MENU_H=22
|
||||
UI_MENU_W=84
|
||||
UI_MENU_LIST_H=12
|
||||
UI_SHORT_MENU_H=16
|
||||
UI_SHORT_MENU_W=72
|
||||
UI_SHORT_MENU_LIST_H=6
|
||||
UI_MSG_H=10
|
||||
UI_MSG_W=72
|
||||
UI_RESULT_H=14
|
||||
UI_RESULT_W=86
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
SCRIPT_DIR_SMART="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR_SMART/.." && pwd)"
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/utils-install-functions.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_LOCAL/global/utils-install-functions.sh"
|
||||
elif [[ -f "$LOCAL_SCRIPTS/global/utils-install-functions.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS/global/utils-install-functions.sh"
|
||||
fi
|
||||
# Configuration ============================================
|
||||
|
||||
|
||||
# ==========================================================
|
||||
# Helpers
|
||||
# ==========================================================
|
||||
|
||||
_smart_is_nvme() {
|
||||
[[ "$1" == *nvme* ]]
|
||||
}
|
||||
|
||||
_smart_disk_label() {
|
||||
local disk="$1"
|
||||
local model size
|
||||
model=$(lsblk -dn -o MODEL "$disk" 2>/dev/null | xargs)
|
||||
size=$(lsblk -dn -o SIZE "$disk" 2>/dev/null | xargs)
|
||||
[[ -z "$model" ]] && model="Unknown"
|
||||
[[ -z "$size" ]] && size="?"
|
||||
printf '%-8s — %s' "$size" "$model"
|
||||
}
|
||||
|
||||
_smart_json_path() {
|
||||
local disk="$1"
|
||||
echo "${SMART_DIR}/$(basename "$disk").json"
|
||||
}
|
||||
|
||||
_smart_ensure_packages() {
|
||||
local need_smartctl=0 need_nvme=0
|
||||
command -v smartctl >/dev/null 2>&1 || need_smartctl=1
|
||||
command -v nvme >/dev/null 2>&1 || need_nvme=1
|
||||
if [[ $need_smartctl -eq 1 || $need_nvme -eq 1 ]]; then
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate 'SMART Disk Health & Test')"
|
||||
ensure_repositories
|
||||
[[ $need_smartctl -eq 1 ]] && install_single_package "smartmontools" "smartctl" "SMART monitoring tools"
|
||||
[[ $need_nvme -eq 1 ]] && install_single_package "nvme-cli" "nvme" "NVMe management tools"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# ==========================================================
|
||||
# PHASE 1 — SELECTION
|
||||
# All dialogs run here. No execution, no show_proxmenux_logo.
|
||||
# ==========================================================
|
||||
|
||||
# ── Install packages if missing ───────────────────────────
|
||||
_smart_ensure_packages
|
||||
|
||||
# ── Step 1: Detect disks ──────────────────────────────────
|
||||
DISK_OPTIONS=()
|
||||
while read -r disk; do
|
||||
[[ -z "$disk" ]] && continue
|
||||
[[ "$disk" =~ ^/dev/zd ]] && continue
|
||||
label=$(_smart_disk_label "$disk")
|
||||
DISK_OPTIONS+=("$disk" "$label")
|
||||
done < <(lsblk -dn -e 7,11 -o PATH 2>/dev/null | grep -E '^/dev/(sd|nvme|vd|hd)')
|
||||
stop_spinner
|
||||
|
||||
if [[ ${#DISK_OPTIONS[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'No Disks Found')" \
|
||||
--msgbox "\n$(translate 'No physical disks detected for SMART testing.')" \
|
||||
$UI_MSG_H $UI_MSG_W
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Step 2: Select disk ───────────────────────────────────
|
||||
SELECTED_DISK=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Select Disk')" \
|
||||
--menu "\n$(translate 'Select the disk to test or inspect:')" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"${DISK_OPTIONS[@]}" \
|
||||
2>&1 >/dev/tty)
|
||||
[[ -z "$SELECTED_DISK" ]] && exit 0
|
||||
|
||||
# ── Steps 3+: Action loop for the selected disk ───────────
|
||||
DISK_LABEL=$(_smart_disk_label "$SELECTED_DISK")
|
||||
mkdir -p "$SMART_DIR"
|
||||
|
||||
while true; do
|
||||
|
||||
# ── Select action ───────────────────────────────────────
|
||||
ACTION=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'SMART Action') — $(basename "$SELECTED_DISK") (${DISK_LABEL})" \
|
||||
--menu "\n$(translate 'Select what to do with this disk:')" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"status" "$(translate 'Quick health status — overall SMART result + key attributes')" \
|
||||
"report" "$(translate 'Full report — complete SMART data (scrollable)')" \
|
||||
"short" "$(translate 'Short test — ~2 minutes, basic surface check')" \
|
||||
"long" "$(translate 'Long test — full scan, runs in background if closed')" \
|
||||
"progress" "$(translate 'Check test progress — show active or last test result')" \
|
||||
2>&1 >/dev/tty)
|
||||
[[ -z "$ACTION" ]] && exit 0
|
||||
|
||||
# ── Long test confirmation ───────────────────────────────
|
||||
if [[ "$ACTION" == "long" ]]; then
|
||||
DISK_SIZE=$(lsblk -dn -o SIZE "$SELECTED_DISK" 2>/dev/null | xargs)
|
||||
if ! dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Long Test — Background')" \
|
||||
--yesno "\n$(translate 'The long test runs directly on the disk hardware.')\n\n$(translate 'Disk:') $SELECTED_DISK ($DISK_SIZE)\n\n$(translate 'The test will continue even if you close this terminal.')\n$(translate 'Results will be saved automatically to:')\n$(_smart_json_path "$SELECTED_DISK")\n\n$(translate 'Start long test now?')" \
|
||||
16 $UI_RESULT_W; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# ========================================================
|
||||
# PHASE 2 — EXECUTION
|
||||
# show_proxmenux_logo appears here exactly once per action.
|
||||
# No dialogs from this point until "Press Enter".
|
||||
# ========================================================
|
||||
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate 'SMART Disk Health & Test')"
|
||||
msg_ok "$(translate 'Disk:') ${BL}${SELECTED_DISK} — ${DISK_LABEL}${CL}"
|
||||
echo ""
|
||||
|
||||
case "$ACTION" in
|
||||
|
||||
# ── Quick status ────────────────────────────────────────
|
||||
status)
|
||||
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||
msg_info "$(translate 'Reading NVMe SMART data...')"
|
||||
OUTPUT=$(nvme smart-log "$SELECTED_DISK" 2>/dev/null)
|
||||
stop_spinner
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
msg_error "$(translate 'Could not read SMART data from') $SELECTED_DISK"
|
||||
else
|
||||
HEALTH=$(echo "$OUTPUT" | grep -i "critical_warning" | awk '{print $NF}')
|
||||
if [[ "$HEALTH" == "0" ]]; then
|
||||
msg_ok "$(translate 'NVMe health status: PASSED')"
|
||||
else
|
||||
msg_warn "$(translate 'NVMe health status: WARNING (critical_warning =') $HEALTH)"
|
||||
fi
|
||||
echo ""
|
||||
echo "$OUTPUT" | head -20
|
||||
fi
|
||||
else
|
||||
msg_info "$(translate 'Reading SMART data...')"
|
||||
HEALTH=$(smartctl -H "$SELECTED_DISK" 2>/dev/null | grep -i "overall-health")
|
||||
ATTRS=$(smartctl -A "$SELECTED_DISK" 2>/dev/null)
|
||||
stop_spinner
|
||||
if [[ -z "$HEALTH" ]]; then
|
||||
msg_error "$(translate 'Could not read SMART data from') $SELECTED_DISK"
|
||||
else
|
||||
if echo "$HEALTH" | grep -qi "PASSED"; then
|
||||
msg_ok "$(translate 'SMART health status: PASSED')"
|
||||
else
|
||||
msg_warn "$HEALTH"
|
||||
fi
|
||||
echo ""
|
||||
echo "$ATTRS" | awk 'NR==1 || /Reallocated_Sector|Current_Pending|Uncorrectable|Temperature_Celsius|Power_On_Hours|Wear_Leveling|Media_Wearout/'
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
# ── Full report (scrollable) ────────────────────────────
|
||||
report)
|
||||
msg_info "$(translate 'Reading full SMART report...')"
|
||||
TMPFILE=$(mktemp)
|
||||
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||
nvme smart-log "$SELECTED_DISK" > "$TMPFILE" 2>/dev/null
|
||||
nvme id-ctrl "$SELECTED_DISK" >> "$TMPFILE" 2>/dev/null
|
||||
else
|
||||
smartctl -x "$SELECTED_DISK" > "$TMPFILE" 2>/dev/null
|
||||
fi
|
||||
stop_spinner
|
||||
if [[ -s "$TMPFILE" ]]; then
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate 'Full SMART Report') — $SELECTED_DISK" \
|
||||
--textbox "$TMPFILE" 40 $UI_RESULT_W
|
||||
else
|
||||
msg_error "$(translate 'Could not read SMART data from') $SELECTED_DISK"
|
||||
fi
|
||||
rm -f "$TMPFILE"
|
||||
;;
|
||||
|
||||
# ── Short test ──────────────────────────────────────────
|
||||
short)
|
||||
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||
msg_info "$(translate 'Starting NVMe short self-test...')"
|
||||
if nvme device-self-test "$SELECTED_DISK" --self-test-code=1 >/dev/null 2>&1; then
|
||||
stop_spinner
|
||||
msg_ok "$(translate 'Short self-test started on') $SELECTED_DISK"
|
||||
msg_ok "$(translate 'Test typically completes in ~2 minutes.')"
|
||||
msg_ok "$(translate 'Use "Check test progress" to see results.')"
|
||||
else
|
||||
stop_spinner
|
||||
msg_error "$(translate 'Failed to start self-test on') $SELECTED_DISK"
|
||||
fi
|
||||
else
|
||||
msg_info "$(translate 'Starting SMART short self-test...')"
|
||||
OUTPUT=$(smartctl -t short "$SELECTED_DISK" 2>/dev/null)
|
||||
stop_spinner
|
||||
if echo "$OUTPUT" | grep -qi "Test will complete"; then
|
||||
msg_ok "$(translate 'Short self-test started on') $SELECTED_DISK"
|
||||
ESTIMATE=$(echo "$OUTPUT" | grep -i "complete after" | head -1)
|
||||
[[ -n "$ESTIMATE" ]] && msg_ok "$ESTIMATE"
|
||||
msg_ok "$(translate 'Use "Check test progress" to see results.')"
|
||||
else
|
||||
msg_error "$(translate 'Failed to start self-test on') $SELECTED_DISK"
|
||||
echo "$OUTPUT" | tail -5
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
# ── Long test (background) ──────────────────────────────
|
||||
long)
|
||||
JSON_PATH=$(_smart_json_path "$SELECTED_DISK")
|
||||
DISK_SAFE=$(printf '%q' "$SELECTED_DISK")
|
||||
JSON_SAFE=$(printf '%q' "$JSON_PATH")
|
||||
|
||||
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||
msg_info "$(translate 'Starting NVMe long self-test...')"
|
||||
if nvme device-self-test "$SELECTED_DISK" --self-test-code=2 >/dev/null 2>&1; then
|
||||
stop_spinner
|
||||
msg_ok "$(translate 'Long self-test started on') $SELECTED_DISK"
|
||||
DISK_LABEL_SAFE=$(printf '%q' "$DISK_LABEL")
|
||||
NOTIFY_SCRIPT="/usr/bin/notification_manager.py"
|
||||
nohup bash -c "
|
||||
while nvme device-self-test ${DISK_SAFE} --self-test-code=0 2>/dev/null | grep -qi 'in progress'; do
|
||||
sleep 60
|
||||
done
|
||||
nvme smart-log -o json ${DISK_SAFE} > ${JSON_SAFE} 2>/dev/null
|
||||
|
||||
# Send notification when test completes
|
||||
if [[ -f \"${NOTIFY_SCRIPT}\" ]]; then
|
||||
HOSTNAME=\$(hostname -s)
|
||||
TEST_RESULT=\$(nvme self-test-log ${DISK_SAFE} 2>/dev/null | head -20)
|
||||
if echo \"\$TEST_RESULT\" | grep -qi 'completed without error\|success'; then
|
||||
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity INFO \
|
||||
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
|
||||
--message \"NVMe disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed successfully.\" 2>/dev/null || true
|
||||
else
|
||||
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity WARNING \
|
||||
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
|
||||
--message \"NVMe disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed. Check results for details.\" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
" >/dev/null 2>&1 &
|
||||
disown $!
|
||||
else
|
||||
stop_spinner
|
||||
msg_error "$(translate 'Failed to start long self-test on') $SELECTED_DISK"
|
||||
fi
|
||||
else
|
||||
msg_info "$(translate 'Starting SMART long self-test...')"
|
||||
OUTPUT=$(smartctl -t long "$SELECTED_DISK" 2>/dev/null)
|
||||
stop_spinner
|
||||
if echo "$OUTPUT" | grep -qi "Test will complete"; then
|
||||
msg_ok "$(translate 'Long self-test started on') $SELECTED_DISK"
|
||||
ESTIMATE=$(echo "$OUTPUT" | grep -i "complete after" | head -1)
|
||||
[[ -n "$ESTIMATE" ]] && msg_ok "$ESTIMATE"
|
||||
echo ""
|
||||
msg_ok "$(translate 'Test runs on the drive hardware — safe to close this terminal.')"
|
||||
msg_ok "$(translate 'Results will be saved to:') $JSON_PATH"
|
||||
DISK_LABEL_SAFE=$(printf '%q' "$DISK_LABEL")
|
||||
NOTIFY_SCRIPT="/usr/bin/notification_manager.py"
|
||||
nohup bash -c "
|
||||
while smartctl -c ${DISK_SAFE} 2>/dev/null | grep -qiE 'Self-test routine in progress|[1-9][0-9]?% of test remaining'; do
|
||||
sleep 60
|
||||
done
|
||||
smartctl --json=c ${DISK_SAFE} > ${JSON_SAFE} 2>/dev/null
|
||||
|
||||
# Send notification when test completes
|
||||
if [[ -f \"${NOTIFY_SCRIPT}\" ]]; then
|
||||
HOSTNAME=\$(hostname -s)
|
||||
TEST_RESULT=\$(smartctl -l selftest ${DISK_SAFE} 2>/dev/null | grep -E '^# ?1')
|
||||
if echo \"\$TEST_RESULT\" | grep -qi 'Completed without error'; then
|
||||
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity INFO \
|
||||
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
|
||||
--message \"Disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed successfully.\" 2>/dev/null || true
|
||||
elif echo \"\$TEST_RESULT\" | grep -qi 'error\|fail'; then
|
||||
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity CRITICAL \
|
||||
--title \"\${HOSTNAME}: SMART Long Test FAILED\" \
|
||||
--message \"Disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed with ERRORS. Check disk health immediately.\" 2>/dev/null || true
|
||||
else
|
||||
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity INFO \
|
||||
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
|
||||
--message \"Disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed. Check results for details.\" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
" >/dev/null 2>&1 &
|
||||
disown $!
|
||||
else
|
||||
msg_error "$(translate 'Failed to start long self-test on') $SELECTED_DISK"
|
||||
echo "$OUTPUT" | tail -5
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
# ── Check progress ──────────────────────────────────────
|
||||
progress)
|
||||
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||
msg_info "$(translate 'Reading NVMe self-test log...')"
|
||||
OUTPUT=$(nvme self-test-log "$SELECTED_DISK" 2>/dev/null)
|
||||
stop_spinner
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
msg_warn "$(translate 'No self-test log available for') $SELECTED_DISK"
|
||||
else
|
||||
echo "$OUTPUT" | head -30
|
||||
fi
|
||||
else
|
||||
msg_info "$(translate 'Reading SMART self-test log...')"
|
||||
# Active test: only "X% of test remaining" appears when a test is actually running
|
||||
ACTIVE=$(smartctl -c "$SELECTED_DISK" 2>/dev/null | grep -iE "[1-9][0-9]?% of test remaining|Self-test routine in progress")
|
||||
# Log: grab only result rows (^# N ...) and the column header (^Num)
|
||||
LOG_OUT=$(smartctl -l selftest "$SELECTED_DISK" 2>/dev/null)
|
||||
LOG_HEADER=$(echo "$LOG_OUT" | grep -E "^Num")
|
||||
LOG_ENTRIES=$(echo "$LOG_OUT" | grep -E "^# ?[0-9]")
|
||||
stop_spinner
|
||||
if [[ -n "$ACTIVE" ]]; then
|
||||
msg_ok "$(translate 'Test in progress:')"
|
||||
echo "$ACTIVE"
|
||||
echo ""
|
||||
else
|
||||
msg_ok "$(translate 'No test currently running')"
|
||||
echo ""
|
||||
fi
|
||||
if [[ -n "$LOG_ENTRIES" ]]; then
|
||||
msg_ok "$(translate 'Recent test results:')"
|
||||
[[ -n "$LOG_HEADER" ]] && echo "$LOG_HEADER"
|
||||
echo "$LOG_ENTRIES"
|
||||
else
|
||||
msg_warn "$(translate 'No self-test history found for') $SELECTED_DISK"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
esac
|
||||
|
||||
# ── Auto-export JSON (except long — handled by background monitor)
|
||||
if [[ "$ACTION" != "long" && "$ACTION" != "report" ]]; then
|
||||
JSON_PATH=$(_smart_json_path "$SELECTED_DISK")
|
||||
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||
nvme smart-log -o json "$SELECTED_DISK" > "$JSON_PATH" 2>/dev/null
|
||||
else
|
||||
smartctl --json=c "$SELECTED_DISK" > "$JSON_PATH" 2>/dev/null
|
||||
fi
|
||||
[[ -s "$JSON_PATH" ]] || rm -f "$JSON_PATH"
|
||||
fi
|
||||
|
||||
# ── "report" uses dialog --textbox, no Press Enter needed
|
||||
if [[ "$ACTION" != "report" ]]; then
|
||||
echo ""
|
||||
msg_success "$(translate 'Press Enter to continue...')"
|
||||
read -r
|
||||
fi
|
||||
|
||||
done
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================
|
||||
# ProxMenux - A menu-driven script for Proxmox VE management
|
||||
# ==========================================================
|
||||
# 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
|
||||
# Description : Allows unmounting a previously mounted disk
|
||||
# ==========================================================
|
||||
|
||||
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
|
||||
|
||||
|
||||
MOUNTED_DISKS=($(mount | grep '^/dev/' | grep 'on /mnt/' | awk '{print $3}'))
|
||||
|
||||
if [[ ${#MOUNTED_DISKS[@]} -eq 0 ]]; then
|
||||
whiptail --title "$(translate "No Disks")" --msgbox "$(translate "No mounted disks found under /mnt.")" 8 50
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
MENU_ITEMS=()
|
||||
for MNT in "${MOUNTED_DISKS[@]}"; do
|
||||
UUID=$(blkid | grep "$MNT" | awk '{print $2}' | tr -d '"')
|
||||
DESC="$MNT $UUID"
|
||||
MENU_ITEMS+=("$MNT" "$DESC")
|
||||
done
|
||||
|
||||
SELECTED=$(whiptail --title "$(translate "Unmount Disk")" --menu "$(translate "Select the disk you want to unmount:")" 20 70 10 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
[[ -z "$SELECTED" ]] && exit 0
|
||||
|
||||
|
||||
whiptail --title "$(translate "Confirm Unmount")" --yesno "$(translate "Are you sure you want to unmount") $SELECTED?" 10 60 || exit 0
|
||||
|
||||
|
||||
umount "$SELECTED" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "Failed to unmount disk at") $SELECTED" 8 60
|
||||
exit 1
|
||||
else
|
||||
msg_ok "$(translate "Unmounted:") $SELECTED"
|
||||
fi
|
||||
|
||||
|
||||
whiptail --title "$(translate "Delete Mount Folder")" --yesno "$(translate "Do you want to delete the mount point folder") $SELECTED?" 10 60
|
||||
if [ $? -eq 0 ]; then
|
||||
rm -rf "$SELECTED"
|
||||
msg_ok "$(translate "Deleted folder:") $SELECTED"
|
||||
fi
|
||||
|
||||
|
||||
DEVICE=$(findmnt -no SOURCE "$SELECTED")
|
||||
UUID=$(blkid -s UUID -o value "$DEVICE")
|
||||
|
||||
if [ -n "$UUID" ]; then
|
||||
sed -i "/UUID=$UUID/d" /etc/fstab
|
||||
msg_ok "$(translate "fstab entry removed for") $UUID"
|
||||
fi
|
||||
|
||||
whiptail --title "$(translate "Done")" --msgbox "$(translate "Disk unmounted and cleaned successfully.")" 8 60
|
||||
|
||||
628
scripts/utilities/export_vm_ova_ovf.sh
Executable file
628
scripts/utilities/export_vm_ova_ovf.sh
Executable file
@@ -0,0 +1,628 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenux - Export VM to OVA or OVF
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||
# Version : 1.0
|
||||
# Last Updated: 07/04/2026
|
||||
# ==========================================================
|
||||
|
||||
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
|
||||
|
||||
require_cmd() {
|
||||
local cmd="$1"
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Missing dependency")" \
|
||||
--msgbox "$(translate "Required command not found:") $cmd" 8 60
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
human_bytes() {
|
||||
local bytes="$1"
|
||||
local units=("B" "KB" "MB" "GB" "TB" "PB")
|
||||
local idx=0
|
||||
local value="$bytes"
|
||||
|
||||
[[ -z "$value" || ! "$value" =~ ^[0-9]+$ ]] && { echo "N/A"; return; }
|
||||
|
||||
while [[ "$value" -ge 1024 && "$idx" -lt 5 ]]; do
|
||||
value=$((value / 1024))
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
|
||||
echo "${value}${units[$idx]}"
|
||||
}
|
||||
|
||||
sanitize_name() {
|
||||
local raw="$1"
|
||||
local out
|
||||
out=$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-zA-Z0-9._-]/_/g' | sed 's/__*/_/g' | sed 's/^_\+//;s/_\+$//')
|
||||
[[ -z "$out" ]] && out="vm"
|
||||
echo "$out"
|
||||
}
|
||||
|
||||
xml_escape() {
|
||||
local s="$1"
|
||||
s=${s//&/&}
|
||||
s=${s//</<}
|
||||
s=${s//>/>}
|
||||
s=${s//\"/"}
|
||||
s=${s//\'/'}
|
||||
echo "$s"
|
||||
}
|
||||
|
||||
validate_destination_dir() {
|
||||
local dir="$1"
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Directory error")" \
|
||||
--msgbox "$(translate "Destination directory does not exist:")\n$dir" 8 74
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -w "$dir" ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Permission error")" \
|
||||
--msgbox "$(translate "Destination directory is not writable:")\n$dir" 8 70
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
select_vm() {
|
||||
local options=()
|
||||
local line vmid name status
|
||||
|
||||
while read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
vmid=$(echo "$line" | awk '{print $1}')
|
||||
name=$(echo "$line" | awk '{print $2}')
|
||||
status=$(echo "$line" | awk '{print $3}')
|
||||
[[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
|
||||
[[ -z "$name" ]] && name="vm-${vmid}"
|
||||
options+=("$vmid" "$name [$status]")
|
||||
done < <(qm list 2>/dev/null)
|
||||
|
||||
if [[ ${#options[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No VMs found")" \
|
||||
--msgbox "$(translate "No virtual machines were found on this host.")" 8 60
|
||||
return 1
|
||||
fi
|
||||
|
||||
VMID=$(dialog --backtitle "ProxMenux" --title "$(translate "Export VM to OVA or OVF")" \
|
||||
--menu "$(translate "Select VM to export:")" 20 80 12 \
|
||||
"${options[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
[[ -n "$VMID" ]] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_vm_stopped() {
|
||||
local status
|
||||
status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||
|
||||
if [[ "$status" == "stopped" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! dialog --backtitle "ProxMenux" --title "$(translate "VM is running")" --yesno \
|
||||
"$(translate "For a consistent export, the VM should be stopped.")\n\n$(translate "Do you want ProxMenux to stop it now?")" 10 70; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
qm shutdown "$VMID" --timeout 120 >/dev/null 2>&1 || true
|
||||
|
||||
local i
|
||||
for i in $(seq 1 60); do
|
||||
status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||
[[ "$status" == "stopped" ]] && return 0
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if dialog --backtitle "ProxMenux" --title "$(translate "Shutdown timeout")" --yesno \
|
||||
"$(translate "Graceful shutdown timed out.")\n\n$(translate "Force stop VM now?")" 10 60; then
|
||||
qm stop "$VMID" >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||
[[ "$status" == "stopped" ]] && return 0
|
||||
fi
|
||||
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Cannot continue")" \
|
||||
--msgbox "$(translate "VM is still running. Export cancelled.")" 8 60
|
||||
return 1
|
||||
}
|
||||
|
||||
select_export_mode() {
|
||||
EXPORT_MODE=$(dialog --backtitle "ProxMenux" --title "$(translate "Export Format")" \
|
||||
--menu "$(translate "Select export format:")" 14 70 4 \
|
||||
"ova" "$(translate "OVA (single portable file)")" \
|
||||
"ovf" "$(translate "OVF (descriptor + VMDK files)")" \
|
||||
3>&1 1>&2 2>&3)
|
||||
[[ -n "$EXPORT_MODE" ]] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
select_destination_dir() {
|
||||
local dump_dir="/var/lib/vz/dump"
|
||||
local iso_dir="/var/lib/vz/template/iso"
|
||||
local options=(
|
||||
"1" "$dump_dir [$(translate "recommended")]"
|
||||
"2" "$iso_dir [$(translate "recommended")]"
|
||||
"M" "$(translate "Manual path entry")"
|
||||
)
|
||||
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(dialog --backtitle "ProxMenux" --title "$(translate "Destination Directory")" \
|
||||
--menu "$(translate "Select where to export VM files (OVA/OVF + temporary workspace):")" \
|
||||
16 84 8 "${options[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
[[ -n "$choice" ]] || return 1
|
||||
|
||||
case "$choice" in
|
||||
M)
|
||||
DEST_DIR=$(dialog --backtitle "ProxMenux" --title "$(translate "Manual destination path")" \
|
||||
--inputbox "$(translate "Enter destination directory for exported file(s):")" \
|
||||
10 90 "/var/lib/vz/dump" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$DEST_DIR" ]] || continue
|
||||
if [[ ! -d "$DEST_DIR" ]]; then
|
||||
if dialog --backtitle "ProxMenux" --title "$(translate "Create directory")" \
|
||||
--yesno "$(translate "The selected directory does not exist:")\n$DEST_DIR\n\n$(translate "Do you want to create it now?")" \
|
||||
11 80; then
|
||||
if ! mkdir -p "$DEST_DIR" 2>/dev/null; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Directory error")" \
|
||||
--msgbox "$(translate "Could not create destination directory:")\n$DEST_DIR" 8 74
|
||||
continue
|
||||
fi
|
||||
else
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
validate_destination_dir "$DEST_DIR" || continue
|
||||
return 0
|
||||
;;
|
||||
1)
|
||||
DEST_DIR="$dump_dir"
|
||||
validate_destination_dir "$DEST_DIR" || continue
|
||||
return 0
|
||||
;;
|
||||
2)
|
||||
DEST_DIR="$iso_dir"
|
||||
validate_destination_dir "$DEST_DIR" || continue
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
get_vm_metadata() {
|
||||
VM_CONF=$(qm config "$VMID" 2>/dev/null) || return 1
|
||||
|
||||
VM_NAME=$(echo "$VM_CONF" | awk -F': ' '/^name:/{print $2; exit}')
|
||||
[[ -z "$VM_NAME" ]] && VM_NAME="vm-${VMID}"
|
||||
|
||||
VM_MEMORY=$(echo "$VM_CONF" | awk -F': ' '/^memory:/{print $2; exit}')
|
||||
[[ -z "$VM_MEMORY" ]] && VM_MEMORY=1024
|
||||
|
||||
VM_CORES=$(echo "$VM_CONF" | awk -F': ' '/^cores:/{print $2; exit}')
|
||||
VM_SOCKETS=$(echo "$VM_CONF" | awk -F': ' '/^sockets:/{print $2; exit}')
|
||||
[[ -z "$VM_CORES" ]] && VM_CORES=1
|
||||
[[ -z "$VM_SOCKETS" ]] && VM_SOCKETS=1
|
||||
VM_VCPUS=$((VM_CORES * VM_SOCKETS))
|
||||
|
||||
VM_OSTYPE=$(echo "$VM_CONF" | awk -F': ' '/^ostype:/{print $2; exit}')
|
||||
case "$VM_OSTYPE" in
|
||||
l26|l24) VM_OS_DESC="Linux" ;;
|
||||
win11|win10|win8|win7|w2k8|w2k12|w2k16|w2k19|w2k22|wxp|w2k|w2k3)
|
||||
VM_OS_DESC="Windows"
|
||||
;;
|
||||
*) VM_OS_DESC="Other" ;;
|
||||
esac
|
||||
|
||||
NET_COUNT=$(echo "$VM_CONF" | grep -E '^net[0-9]+:' | wc -l)
|
||||
}
|
||||
|
||||
get_virtual_size_bytes() {
|
||||
local src="$1"
|
||||
local bytes=""
|
||||
|
||||
bytes=$(qemu-img info "$src" 2>/dev/null | sed -n 's/.*virtual size:.*(\([0-9]\+\) bytes).*/\1/p' | head -1)
|
||||
if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
|
||||
echo "$bytes"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -b "$src" ]]; then
|
||||
bytes=$(blockdev --getsize64 "$src" 2>/dev/null || true)
|
||||
if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
|
||||
echo "$bytes"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
bytes=$(stat -c%s "$src" 2>/dev/null || true)
|
||||
if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
|
||||
echo "$bytes"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "0"
|
||||
return 0
|
||||
}
|
||||
|
||||
collect_vm_disks() {
|
||||
DISK_COUNT=0
|
||||
unset DISK_SLOTS DISK_SRCS DISK_VSIZES
|
||||
declare -ga DISK_SLOTS DISK_SRCS DISK_VSIZES
|
||||
|
||||
local line slot value source src
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^(scsi|sata|virtio|ide)[0-9]+: ]]; then
|
||||
slot="${line%%:*}"
|
||||
value="${line#*: }"
|
||||
|
||||
[[ "$value" == *"media=cdrom"* ]] && continue
|
||||
[[ "$value" == *"cloudinit"* ]] && continue
|
||||
|
||||
source="${value%%,*}"
|
||||
[[ -z "$source" || "$source" == "none" ]] && continue
|
||||
|
||||
src=""
|
||||
if [[ "$source" == /dev/* || "$source" == /* ]]; then
|
||||
src="$source"
|
||||
elif [[ "$source" == *:* ]]; then
|
||||
src=$(pvesm path "$source" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [[ -z "$src" || ! -e "$src" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
DISK_SLOTS+=("$slot")
|
||||
DISK_SRCS+=("$src")
|
||||
DISK_VSIZES+=("$(get_virtual_size_bytes "$src")")
|
||||
DISK_COUNT=$((DISK_COUNT + 1))
|
||||
fi
|
||||
done <<< "$VM_CONF"
|
||||
|
||||
[[ "$DISK_COUNT" -gt 0 ]] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
check_destination_space() {
|
||||
local total=0
|
||||
local i
|
||||
for i in "${DISK_VSIZES[@]}"; do
|
||||
[[ "$i" =~ ^[0-9]+$ ]] && total=$((total + i))
|
||||
done
|
||||
|
||||
local factor=120
|
||||
[[ "$EXPORT_MODE" == "ova" ]] && factor=220
|
||||
REQUIRED_BYTES=$((total * factor / 100))
|
||||
|
||||
AVAILABLE_BYTES=$(df -PB1 "$DEST_DIR" | awk 'NR==2{print $4}')
|
||||
[[ "$AVAILABLE_BYTES" =~ ^[0-9]+$ ]] || AVAILABLE_BYTES=0
|
||||
|
||||
if [[ "$AVAILABLE_BYTES" -lt "$REQUIRED_BYTES" ]]; then
|
||||
if ! dialog --backtitle "ProxMenux" --title "$(translate "Low free space warning")" --yesno \
|
||||
"$(translate "Estimated required free space:") $(human_bytes "$REQUIRED_BYTES") ($REQUIRED_BYTES bytes)\n$(translate "Current free space:") $(human_bytes "$AVAILABLE_BYTES") ($AVAILABLE_BYTES bytes)\n\n$(translate "Do you want to continue anyway?")" 13 90; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
generate_ovf_descriptor() {
|
||||
local ovf_path="$1"
|
||||
local vm_name_xml os_desc_xml
|
||||
vm_name_xml=$(xml_escape "$VM_NAME")
|
||||
os_desc_xml=$(xml_escape "$VM_OS_DESC")
|
||||
|
||||
{
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
echo '<Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">'
|
||||
echo ' <References>'
|
||||
} > "$ovf_path"
|
||||
|
||||
local idx file_id disk_id file_name file_size capacity
|
||||
for idx in "${!EXPORT_DISK_FILES[@]}"; do
|
||||
file_id="file$((idx + 1))"
|
||||
file_name="${EXPORT_DISK_FILES[$idx]}"
|
||||
file_size=$(stat -c%s "$WORK_DIR/$file_name")
|
||||
echo " <File ovf:id=\"$file_id\" ovf:href=\"$file_name\" ovf:size=\"$file_size\"/>" >> "$ovf_path"
|
||||
done
|
||||
|
||||
{
|
||||
echo ' </References>'
|
||||
echo ' <DiskSection>'
|
||||
echo ' <Info>Virtual disk information</Info>'
|
||||
} >> "$ovf_path"
|
||||
|
||||
for idx in "${!EXPORT_DISK_FILES[@]}"; do
|
||||
file_id="file$((idx + 1))"
|
||||
disk_id="vmdisk$((idx + 1))"
|
||||
capacity="${DISK_VSIZES[$idx]}"
|
||||
[[ -z "$capacity" || "$capacity" -le 0 ]] && capacity=$(stat -c%s "$WORK_DIR/${EXPORT_DISK_FILES[$idx]}")
|
||||
echo " <Disk ovf:diskId=\"$disk_id\" ovf:fileRef=\"$file_id\" ovf:capacity=\"$capacity\" ovf:capacityAllocationUnits=\"byte\" ovf:format=\"http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized\"/>" >> "$ovf_path"
|
||||
done
|
||||
|
||||
{
|
||||
echo ' </DiskSection>'
|
||||
echo " <VirtualSystem ovf:id=\"$(sanitize_name "$VM_NAME")\">"
|
||||
echo ' <Info>A virtual machine</Info>'
|
||||
echo " <Name>$vm_name_xml</Name>"
|
||||
echo ' <OperatingSystemSection ovf:id="94">'
|
||||
echo ' <Info>Guest operating system</Info>'
|
||||
echo " <Description>$os_desc_xml</Description>"
|
||||
echo ' </OperatingSystemSection>'
|
||||
echo ' <VirtualHardwareSection>'
|
||||
echo ' <Info>Virtual hardware requirements</Info>'
|
||||
echo ' <System>'
|
||||
echo ' <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>'
|
||||
echo ' <vssd:InstanceID>0</vssd:InstanceID>'
|
||||
echo ' <vssd:VirtualSystemIdentifier>vm</vssd:VirtualSystemIdentifier>'
|
||||
echo ' <vssd:VirtualSystemType>vmx-14</vssd:VirtualSystemType>'
|
||||
echo ' </System>'
|
||||
echo ' <Item>'
|
||||
echo ' <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>'
|
||||
echo ' <rasd:Description>Number of Virtual CPUs</rasd:Description>'
|
||||
echo ' <rasd:ElementName>Virtual CPU(s)</rasd:ElementName>'
|
||||
echo ' <rasd:InstanceID>1</rasd:InstanceID>'
|
||||
echo " <rasd:VirtualQuantity>$VM_VCPUS</rasd:VirtualQuantity>"
|
||||
echo ' <rasd:ResourceType>3</rasd:ResourceType>'
|
||||
echo ' </Item>'
|
||||
echo ' <Item>'
|
||||
echo ' <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>'
|
||||
echo ' <rasd:Description>Memory Size</rasd:Description>'
|
||||
echo ' <rasd:ElementName>Memory</rasd:ElementName>'
|
||||
echo ' <rasd:InstanceID>2</rasd:InstanceID>'
|
||||
echo " <rasd:VirtualQuantity>$VM_MEMORY</rasd:VirtualQuantity>"
|
||||
echo ' <rasd:ResourceType>4</rasd:ResourceType>'
|
||||
echo ' </Item>'
|
||||
echo ' <Item>'
|
||||
echo ' <rasd:Address>0</rasd:Address>'
|
||||
echo ' <rasd:Description>SCSI Controller</rasd:Description>'
|
||||
echo ' <rasd:ElementName>SCSI Controller 0</rasd:ElementName>'
|
||||
echo ' <rasd:InstanceID>10</rasd:InstanceID>'
|
||||
echo ' <rasd:ResourceSubType>lsilogic</rasd:ResourceSubType>'
|
||||
echo ' <rasd:ResourceType>6</rasd:ResourceType>'
|
||||
echo ' </Item>'
|
||||
} >> "$ovf_path"
|
||||
|
||||
for idx in "${!EXPORT_DISK_FILES[@]}"; do
|
||||
disk_id="vmdisk$((idx + 1))"
|
||||
echo ' <Item>' >> "$ovf_path"
|
||||
echo " <rasd:AddressOnParent>$idx</rasd:AddressOnParent>" >> "$ovf_path"
|
||||
echo ' <rasd:Description>Hard disk</rasd:Description>' >> "$ovf_path"
|
||||
echo " <rasd:ElementName>Hard disk $((idx + 1))</rasd:ElementName>" >> "$ovf_path"
|
||||
echo " <rasd:HostResource>ovf:/disk/$disk_id</rasd:HostResource>" >> "$ovf_path"
|
||||
echo " <rasd:InstanceID>$((200 + idx + 1))</rasd:InstanceID>" >> "$ovf_path"
|
||||
echo ' <rasd:Parent>10</rasd:Parent>' >> "$ovf_path"
|
||||
echo ' <rasd:ResourceType>17</rasd:ResourceType>' >> "$ovf_path"
|
||||
echo ' </Item>' >> "$ovf_path"
|
||||
done
|
||||
|
||||
if [[ "$NET_COUNT" -gt 0 ]]; then
|
||||
local n
|
||||
for n in $(seq 1 "$NET_COUNT"); do
|
||||
{
|
||||
echo ' <Item>'
|
||||
echo ' <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>'
|
||||
echo ' <rasd:Connection>VM Network</rasd:Connection>'
|
||||
echo " <rasd:ElementName>Ethernet adapter $n</rasd:ElementName>"
|
||||
echo " <rasd:InstanceID>$((300 + n))</rasd:InstanceID>"
|
||||
echo ' <rasd:ResourceType>10</rasd:ResourceType>'
|
||||
echo ' </Item>'
|
||||
} >> "$ovf_path"
|
||||
done
|
||||
fi
|
||||
|
||||
{
|
||||
echo ' </VirtualHardwareSection>'
|
||||
echo ' </VirtualSystem>'
|
||||
echo '</Envelope>'
|
||||
} >> "$ovf_path"
|
||||
}
|
||||
|
||||
generate_manifest() {
|
||||
local mf_path="$1"
|
||||
shift
|
||||
local files=("$@")
|
||||
: > "$mf_path"
|
||||
|
||||
local f hash
|
||||
for f in "${files[@]}"; do
|
||||
hash=$(sha1sum "$WORK_DIR/$f" | awk '{print $1}')
|
||||
echo "SHA1($f)= $hash" >> "$mf_path"
|
||||
done
|
||||
}
|
||||
|
||||
print_export_result() {
|
||||
local mode="$1"
|
||||
local path="$2"
|
||||
|
||||
echo ""
|
||||
msg_title "$(translate "Export Summary")"
|
||||
|
||||
msg_ok "$(translate "VM:") ${VMID} — ${VM_NAME}"
|
||||
msg_ok "$(translate "vCPUs:") ${VM_VCPUS} $(translate "Memory:") ${VM_MEMORY} MB $(translate "Disks exported:") ${DISK_COUNT}"
|
||||
echo ""
|
||||
|
||||
if [[ "$mode" == "ova" ]]; then
|
||||
local ova_size ova_sha1
|
||||
ova_size=$(stat -c%s "$path" 2>/dev/null || echo 0)
|
||||
ova_sha1=$(sha1sum "$path" 2>/dev/null | awk '{print $1}')
|
||||
msg_ok "$(translate "Format:") OVA — $(translate "single portable archive")"
|
||||
msg_ok "$(translate "File:") $path"
|
||||
msg_ok "$(translate "Size:") $(human_bytes "$ova_size") (${ova_size} $(translate "bytes"))"
|
||||
msg_ok "SHA1: ${ova_sha1}"
|
||||
else
|
||||
local fsz total_size=0 f
|
||||
msg_ok "$(translate "Format:") OVF — $(translate "descriptor + VMDK files")"
|
||||
msg_ok "$(translate "Directory:") $path"
|
||||
for f in "${EXPORT_DISK_FILES[@]}"; do
|
||||
fsz=$(stat -c%s "$path/$f" 2>/dev/null || echo 0)
|
||||
total_size=$((total_size + fsz))
|
||||
msg_info2 " ${f} [$(human_bytes "$fsz")]"
|
||||
done
|
||||
msg_ok "$(translate "Total size:") $(human_bytes "$total_size")"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
msg_ok "$(translate "Compatible with:") VMware ESXi 6.7+ (vmx-14) · VMware Workstation / Fusion · VirtualBox · Proxmox VE"
|
||||
msg_info2 "$(translate "Not portable:") $(translate "PCI passthrough, TPM state, cloud-init configuration, Proxmox hooks")"
|
||||
echo ""
|
||||
}
|
||||
|
||||
run_export() {
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Export VM to OVA or OVF")"
|
||||
|
||||
msg_ok "$(translate "VM selected:") $VMID ($VM_NAME)"
|
||||
msg_ok "$(translate "Export mode:") ${EXPORT_MODE^^}"
|
||||
msg_ok "$(translate "Destination:") $DEST_DIR"
|
||||
|
||||
local ts vm_safe base_name
|
||||
ts=$(date +%Y%m%d_%H%M%S)
|
||||
vm_safe=$(sanitize_name "$VM_NAME")
|
||||
base_name="${vm_safe}-${VMID}-${ts}"
|
||||
|
||||
WORK_DIR=$(mktemp -d "$DEST_DIR/.ovaovf-${base_name}-XXXXXX")
|
||||
if [[ ! -d "$WORK_DIR" ]]; then
|
||||
msg_error "$(translate "Could not create temporary working directory.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Working directory:") $WORK_DIR"
|
||||
|
||||
# Clean up temp dir on unexpected exit (Ctrl+C, unhandled error, etc.)
|
||||
trap 'rm -rf "$WORK_DIR" 2>/dev/null' EXIT
|
||||
|
||||
declare -ga EXPORT_DISK_FILES
|
||||
EXPORT_DISK_FILES=()
|
||||
|
||||
local i src dst disk_name
|
||||
for i in "${!DISK_SRCS[@]}"; do
|
||||
src="${DISK_SRCS[$i]}"
|
||||
disk_name="${base_name}-disk$((i + 1)).vmdk"
|
||||
dst="$WORK_DIR/$disk_name"
|
||||
|
||||
echo ""
|
||||
msg_info "$(translate "Converting disk") $((i + 1))/$DISK_COUNT: ${DISK_SLOTS[$i]}"
|
||||
msg_info2 "$(translate "Source:") $src"
|
||||
|
||||
if ! qemu-img convert -p -O vmdk -o subformat=streamOptimized "$src" "$dst"; then
|
||||
msg_error "$(translate "Disk conversion failed for") ${DISK_SLOTS[$i]}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
EXPORT_DISK_FILES+=("$disk_name")
|
||||
msg_ok "$(translate "Converted:") $disk_name"
|
||||
done
|
||||
|
||||
local ovf_file mf_file
|
||||
ovf_file="${base_name}.ovf"
|
||||
mf_file="${base_name}.mf"
|
||||
|
||||
msg_info "$(translate "Generating OVF descriptor...")"
|
||||
generate_ovf_descriptor "$WORK_DIR/$ovf_file"
|
||||
|
||||
msg_info "$(translate "Generating manifest...")"
|
||||
generate_manifest "$WORK_DIR/$mf_file" "$ovf_file" "${EXPORT_DISK_FILES[@]}"
|
||||
|
||||
if [[ "$EXPORT_MODE" == "ovf" ]]; then
|
||||
local final_dir="$DEST_DIR/${base_name}-ovf"
|
||||
rm -rf "$final_dir"
|
||||
trap - EXIT
|
||||
mv "$WORK_DIR" "$final_dir"
|
||||
|
||||
print_export_result "ovf" "$final_dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local ova_path="$DEST_DIR/${base_name}.ova"
|
||||
msg_info "$(translate "Packaging OVA file...")"
|
||||
|
||||
if ! tar -C "$WORK_DIR" -cf "$ova_path" "$ovf_file" "$mf_file" "${EXPORT_DISK_FILES[@]}"; then
|
||||
msg_error "$(translate "Failed to create OVA archive.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
trap - EXIT
|
||||
rm -rf "$WORK_DIR"
|
||||
|
||||
print_export_result "ova" "$ova_path"
|
||||
return 0
|
||||
}
|
||||
|
||||
main() {
|
||||
require_cmd dialog || exit 1
|
||||
require_cmd qm || exit 1
|
||||
require_cmd pvesm || exit 1
|
||||
require_cmd qemu-img || exit 1
|
||||
require_cmd tar || exit 1
|
||||
require_cmd sha1sum || exit 1
|
||||
|
||||
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
|
||||
|
||||
select_vm || exit 0
|
||||
ensure_vm_stopped || exit 0
|
||||
select_export_mode || exit 0
|
||||
select_destination_dir || exit 0
|
||||
|
||||
get_vm_metadata || {
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
|
||||
--msgbox "$(translate "Could not read VM configuration.")" 8 60
|
||||
exit 1
|
||||
}
|
||||
|
||||
collect_vm_disks || {
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No exportable disks")" \
|
||||
--msgbox "$(translate "No exportable VM disks were found (CD-ROM/cloud-init are excluded).")" 9 80
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_destination_space || exit 0
|
||||
|
||||
if ! dialog --backtitle "ProxMenux" --title "$(translate "Confirm export")" --yesno \
|
||||
"$(translate "VM:") $VMID ($VM_NAME)\n$(translate "Disks to export:") $DISK_COUNT\n$(translate "Format:") ${EXPORT_MODE^^}\n$(translate "Destination:") $DEST_DIR\n\n$(translate "Continue?")" 13 80; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if run_export; then
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to return...")"
|
||||
read -r
|
||||
exit 0
|
||||
else
|
||||
echo ""
|
||||
msg_error "$(translate "Export failed.")"
|
||||
msg_info2 "$(translate "Temporary working directory (if present):") $WORK_DIR"
|
||||
msg_success "$(translate "Press Enter to return...")"
|
||||
read -r
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
612
scripts/utilities/import_vm_ova_ovf.sh
Executable file
612
scripts/utilities/import_vm_ova_ovf.sh
Executable file
@@ -0,0 +1,612 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenux - Import VM from OVA or OVF
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||
# Version : 1.0
|
||||
# Last Updated: 10/04/2026
|
||||
# ==========================================================
|
||||
# Description:
|
||||
# Imports a virtual machine from an OVA or OVF package into Proxmox VE.
|
||||
# Compatible with exports from VMware ESXi, VMware Workstation/Fusion,
|
||||
# VirtualBox, and Proxmox itself (via export_vm_ova_ovf).
|
||||
#
|
||||
# What is imported:
|
||||
# - Disk images (VMDK converted to the target storage format)
|
||||
# - CPU and memory settings
|
||||
# - Number of network interfaces
|
||||
# - VM name and OS type hint
|
||||
#
|
||||
# What requires manual review after import:
|
||||
# - Network bridge assignment (vmbr0 assigned by default)
|
||||
# - NIC model (e1000 by default — change to VirtIO if guest supports it)
|
||||
# - Firmware (BIOS/UEFI — must match what the original VM used)
|
||||
# - VirtIO/qemu-guest-agent installation inside the guest (especially from ESXi)
|
||||
# - PCI passthrough, TPM, cloud-init, snapshots — not portable in OVF/OVA
|
||||
# ==========================================================
|
||||
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
|
||||
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
BACKTITLE="ProxMenux"
|
||||
UI_MENU_H=20
|
||||
UI_MENU_W=84
|
||||
UI_MENU_LIST_H=10
|
||||
|
||||
# Globals populated during the flow
|
||||
SOURCE_FILE=""
|
||||
OVF_FILE=""
|
||||
OVF_DIR=""
|
||||
WORK_DIR=""
|
||||
|
||||
OVF_VM_NAME=""
|
||||
OVF_VCPUS=1
|
||||
OVF_MEMORY_MB=1024
|
||||
OVF_DISK_FILES=()
|
||||
OVF_DISK_CAPACITIES=()
|
||||
OVF_NET_COUNT=0
|
||||
OVF_OS_TYPE="other"
|
||||
|
||||
NEW_VMID=""
|
||||
NEW_VM_NAME=""
|
||||
STORAGE=""
|
||||
BRIDGE="vmbr0"
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# HELPERS
|
||||
# -------------------------------------------------------
|
||||
|
||||
human_bytes() {
|
||||
local bytes="$1"
|
||||
local units=("B" "KB" "MB" "GB" "TB")
|
||||
local idx=0 value="$bytes"
|
||||
[[ -z "$value" || ! "$value" =~ ^[0-9]+$ ]] && { echo "N/A"; return; }
|
||||
while [[ "$value" -ge 1024 && "$idx" -lt 4 ]]; do
|
||||
value=$((value / 1024))
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
echo "${value}${units[$idx]}"
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# SELECT SOURCE FILE
|
||||
# -------------------------------------------------------
|
||||
|
||||
select_source_file() {
|
||||
local dump_dir="/var/lib/vz/dump"
|
||||
local iso_dir="/var/lib/vz/template/iso"
|
||||
local options=(
|
||||
"1" "$dump_dir"
|
||||
"2" "$iso_dir"
|
||||
"M" "$(translate "Manual path entry")"
|
||||
)
|
||||
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Import VM from OVA or OVF")" \
|
||||
--menu "$(translate "Where is the OVA/OVF file located?")" \
|
||||
14 82 4 "${options[@]}" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$choice" ]] || return 1
|
||||
|
||||
local search_dir=""
|
||||
case "$choice" in
|
||||
1) search_dir="$dump_dir" ;;
|
||||
2) search_dir="$iso_dir" ;;
|
||||
M)
|
||||
search_dir=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Custom Path")" \
|
||||
--inputbox "\n$(translate "Enter directory containing OVA/OVF files:")" \
|
||||
10 82 "/var/lib/vz/dump" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$search_dir" ]] || continue
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -d "$search_dir" ]]; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "Not found")" \
|
||||
--msgbox "$(translate "Directory does not exist:")\n$search_dir" 8 74
|
||||
continue
|
||||
fi
|
||||
|
||||
local file_opts=()
|
||||
while IFS= read -r f; do
|
||||
local fname size_h
|
||||
fname=$(basename "$f")
|
||||
size_h=$(du -sh "$f" 2>/dev/null | awk '{print $1}')
|
||||
file_opts+=("$f" "$fname [$size_h]")
|
||||
done < <(find "$search_dir" -maxdepth 2 \( -name "*.ova" -o -name "*.ovf" \) 2>/dev/null | sort)
|
||||
|
||||
if [[ ${#file_opts[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "No files found")" \
|
||||
--msgbox "$(translate "No .ova or .ovf files found in:")\n\n$search_dir" 10 74
|
||||
continue
|
||||
fi
|
||||
|
||||
local selected
|
||||
selected=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Select OVA/OVF file")" \
|
||||
--menu "$(translate "Select the file to import:")" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"${file_opts[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
[[ -n "$selected" ]] || continue
|
||||
|
||||
SOURCE_FILE="$selected"
|
||||
return 0
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# EXTRACT OVA / LOCATE OVF
|
||||
# -------------------------------------------------------
|
||||
|
||||
prepare_ovf() {
|
||||
local src="$SOURCE_FILE"
|
||||
local ext="${src##*.}"
|
||||
ext="${ext,,}"
|
||||
|
||||
if [[ "$ext" == "ova" ]]; then
|
||||
WORK_DIR=$(mktemp -d "/tmp/.proxmenux-import-XXXXXX")
|
||||
trap 'rm -rf "$WORK_DIR" 2>/dev/null' EXIT
|
||||
|
||||
msg_info "$(translate "Extracting OVA archive...")"
|
||||
if ! tar xf "$src" -C "$WORK_DIR" 2>/dev/null; then
|
||||
msg_error "$(translate "Failed to extract OVA file:") $src"
|
||||
return 1
|
||||
fi
|
||||
msg_ok "$(translate "Archive extracted.")"
|
||||
|
||||
OVF_FILE=$(find "$WORK_DIR" -maxdepth 2 -name "*.ovf" | head -1)
|
||||
if [[ -z "$OVF_FILE" ]]; then
|
||||
msg_error "$(translate "No .ovf descriptor found inside OVA.")"
|
||||
return 1
|
||||
fi
|
||||
OVF_DIR=$(dirname "$OVF_FILE")
|
||||
|
||||
elif [[ "$ext" == "ovf" ]]; then
|
||||
OVF_FILE="$src"
|
||||
OVF_DIR=$(dirname "$src")
|
||||
WORK_DIR=""
|
||||
|
||||
else
|
||||
msg_error "$(translate "Unsupported format. Only .ova and .ovf files are supported.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# PARSE OVF XML
|
||||
# -------------------------------------------------------
|
||||
|
||||
parse_ovf() {
|
||||
local ovf_file="$1"
|
||||
|
||||
local result
|
||||
result=$(awk '
|
||||
BEGIN {
|
||||
in_item=0; rt=""; qty=""
|
||||
file_count=0; cap_count=0; net_count=0
|
||||
name=""; vcpu="1"; mem="1024"; os=""
|
||||
}
|
||||
|
||||
/<[Nn]ame>/ {
|
||||
match($0, /<[Nn]ame>([^<]+)</, a)
|
||||
if (a[1] != "" && name == "") name = a[1]
|
||||
}
|
||||
|
||||
/[Ll]inux/ && /[Dd]escription|[Oo]perating/ { if (os == "") os="linux" }
|
||||
/[Ww]indows/ && /[Dd]escription|[Oo]perating/ { if (os == "") os="windows" }
|
||||
|
||||
/ovf:href=|href=/ {
|
||||
n = split($0, parts, /"/)
|
||||
for (i=1; i<=n; i++) {
|
||||
if (parts[i] ~ /\.(vmdk|qcow2|img|raw)$/) {
|
||||
files[file_count++] = parts[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/[Cc]apacity=/ {
|
||||
match($0, /[Cc]apacity="([0-9]+)"/, a)
|
||||
if (a[1]+0 > 0) caps[cap_count++] = a[1]
|
||||
}
|
||||
|
||||
/<Item>|<Item / { in_item=1; rt=""; qty="" }
|
||||
/<\/Item>/ {
|
||||
if (in_item) {
|
||||
if (rt=="3" && qty ~ /^[0-9]+$/) vcpu=qty
|
||||
if (rt=="4" && qty ~ /^[0-9]+$/) mem=qty
|
||||
if (rt=="10") net_count++
|
||||
}
|
||||
in_item=0
|
||||
}
|
||||
/ResourceType>/ {
|
||||
match($0, /ResourceType>([0-9]+)</, a); rt=a[1]
|
||||
}
|
||||
/VirtualQuantity>/ {
|
||||
match($0, /VirtualQuantity>([0-9]+)</, a); qty=a[1]
|
||||
}
|
||||
|
||||
END {
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
|
||||
if (name == "") name = "imported-vm"
|
||||
print "NAME=" name
|
||||
print "VCPU=" vcpu
|
||||
print "MEM=" mem
|
||||
print "NET=" net_count
|
||||
print "OS=" os
|
||||
for (i=0; i<file_count; i++) print "FILE=" files[i]
|
||||
for (i=0; i<cap_count; i++) print "CAP=" caps[i]
|
||||
}
|
||||
' "$ovf_file")
|
||||
|
||||
OVF_VM_NAME=$(echo "$result" | grep '^NAME=' | cut -d= -f2-)
|
||||
OVF_VCPUS=$(echo "$result" | grep '^VCPU=' | cut -d= -f2-)
|
||||
OVF_MEMORY_MB=$(echo "$result" | grep '^MEM=' | cut -d= -f2-)
|
||||
OVF_NET_COUNT=$(echo "$result" | grep '^NET=' | cut -d= -f2-)
|
||||
OVF_OS_TYPE=$(echo "$result" | grep '^OS=' | cut -d= -f2-)
|
||||
|
||||
OVF_DISK_FILES=()
|
||||
while IFS= read -r line; do
|
||||
OVF_DISK_FILES+=("${line#FILE=}")
|
||||
done < <(echo "$result" | grep '^FILE=')
|
||||
|
||||
OVF_DISK_CAPACITIES=()
|
||||
while IFS= read -r line; do
|
||||
OVF_DISK_CAPACITIES+=("${line#CAP=}")
|
||||
done < <(echo "$result" | grep '^CAP=')
|
||||
|
||||
[[ -z "$OVF_VM_NAME" ]] && OVF_VM_NAME="imported-vm"
|
||||
[[ ! "$OVF_VCPUS" =~ ^[0-9]+$ ]] && OVF_VCPUS=1
|
||||
[[ ! "$OVF_MEMORY_MB" =~ ^[0-9]+$ ]] && OVF_MEMORY_MB=1024
|
||||
[[ ! "$OVF_NET_COUNT" =~ ^[0-9]+$ ]] && OVF_NET_COUNT=0
|
||||
|
||||
case "$OVF_OS_TYPE" in
|
||||
linux) OVF_OS_TYPE="l26" ;;
|
||||
windows) OVF_OS_TYPE="win10" ;;
|
||||
*) OVF_OS_TYPE="other" ;;
|
||||
esac
|
||||
|
||||
[[ ${#OVF_DISK_FILES[@]} -gt 0 ]] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# SELECT IMPORT OPTIONS (dialogs — no terminal output)
|
||||
# -------------------------------------------------------
|
||||
|
||||
select_import_options() {
|
||||
# VMID
|
||||
local suggested_vmid
|
||||
suggested_vmid=$(pvesh get /cluster/nextid 2>/dev/null || echo "100")
|
||||
|
||||
while true; do
|
||||
NEW_VMID=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "VM ID")" \
|
||||
--inputbox "\n$(translate "Enter the VMID for the new VM:") ($(translate "suggested:") $suggested_vmid)" \
|
||||
10 72 "$suggested_vmid" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$NEW_VMID" ]] || return 1
|
||||
|
||||
if ! [[ "$NEW_VMID" =~ ^[0-9]+$ ]]; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "Invalid VMID")" \
|
||||
--msgbox "$(translate "VMID must be a number.")" 8 50
|
||||
continue
|
||||
fi
|
||||
|
||||
if qm status "$NEW_VMID" &>/dev/null; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "VMID in use")" \
|
||||
--msgbox "$(translate "VMID $NEW_VMID is already in use. Please choose another.")" 8 60
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
# VM Name
|
||||
NEW_VM_NAME=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "VM Name")" \
|
||||
--inputbox "\n$(translate "Enter name for the imported VM:")" \
|
||||
10 72 "$OVF_VM_NAME" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$NEW_VM_NAME" ]] || return 1
|
||||
|
||||
# Storage
|
||||
local storage_list storage_opts=()
|
||||
storage_list=$(pvesm status -content images 2>/dev/null | awk 'NR>1 {print $1}')
|
||||
if [[ -z "$storage_list" ]]; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "No storage")" \
|
||||
--msgbox "$(translate "No storage volumes available for VM images.")" 8 60
|
||||
return 1
|
||||
fi
|
||||
while IFS= read -r s; do
|
||||
storage_opts+=("$s" "")
|
||||
done <<< "$storage_list"
|
||||
|
||||
STORAGE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Select Storage")" \
|
||||
--menu "$(translate "Select storage for imported disk(s):")" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"${storage_opts[@]}" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$STORAGE" ]] || return 1
|
||||
|
||||
# Network bridge
|
||||
local bridge_opts=()
|
||||
while IFS= read -r br; do
|
||||
[[ -n "$br" ]] && bridge_opts+=("$br" "")
|
||||
done < <(ip link show type bridge 2>/dev/null | awk -F': ' '/^[0-9]+:/{print $2}' | sed 's/@.*//')
|
||||
|
||||
if [[ ${#bridge_opts[@]} -gt 1 ]]; then
|
||||
BRIDGE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Network Bridge")" \
|
||||
--menu "$(translate "Select bridge for network interface(s):")" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"${bridge_opts[@]}" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$BRIDGE" ]] || return 1
|
||||
elif [[ ${#bridge_opts[@]} -eq 1 ]]; then
|
||||
BRIDGE="${bridge_opts[0]}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# CONFIRM BEFORE IMPORT (dialog)
|
||||
# -------------------------------------------------------
|
||||
|
||||
confirm_import() {
|
||||
local disk_count="${#OVF_DISK_FILES[@]}"
|
||||
local disk_info="" i
|
||||
|
||||
for i in "${!OVF_DISK_FILES[@]}"; do
|
||||
local cap="${OVF_DISK_CAPACITIES[$i]:-0}"
|
||||
disk_info+="\n disk$((i+1)): ${OVF_DISK_FILES[$i]} ($(human_bytes "$cap"))"
|
||||
done
|
||||
|
||||
local msg
|
||||
msg="$(translate "New VM:") $NEW_VMID ($NEW_VM_NAME)\n"
|
||||
msg+="$(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB $(translate "OS type:") $OVF_OS_TYPE\n"
|
||||
msg+="$(translate "NICs:") $OVF_NET_COUNT $(translate "Bridge:") $BRIDGE\n"
|
||||
msg+="$(translate "Storage:") $STORAGE\n"
|
||||
msg+="$(translate "Disks to import:") $disk_count${disk_info}\n\n"
|
||||
msg+="$(translate "Continue?")"
|
||||
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Confirm Import")" \
|
||||
--yesno "$msg" 18 84 3>&1 1>&2 2>&3
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# RUN IMPORT (terminal output only — no dialogs)
|
||||
# -------------------------------------------------------
|
||||
|
||||
run_import() {
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Import VM from OVA or OVF")"
|
||||
|
||||
msg_ok "$(translate "VM:") $NEW_VMID ($NEW_VM_NAME)"
|
||||
msg_ok "$(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB $(translate "OS:") $OVF_OS_TYPE"
|
||||
msg_ok "$(translate "Storage:") $STORAGE $(translate "Bridge:") $BRIDGE $(translate "NICs:") $OVF_NET_COUNT"
|
||||
echo ""
|
||||
|
||||
# 1. Create VM shell
|
||||
msg_info "$(translate "Creating VM...")"
|
||||
if ! qm create "$NEW_VMID" \
|
||||
--name "$NEW_VM_NAME" \
|
||||
--memory "$OVF_MEMORY_MB" \
|
||||
--cores "$OVF_VCPUS" \
|
||||
--ostype "$OVF_OS_TYPE" \
|
||||
--scsihw lsi \
|
||||
--net0 "e1000,bridge=$BRIDGE" \
|
||||
&>/dev/null; then
|
||||
msg_error "$(translate "Failed to create VM") $NEW_VMID"
|
||||
return 1
|
||||
fi
|
||||
msg_ok "$(translate "VM shell created:") $NEW_VMID"
|
||||
|
||||
# Add extra NICs (net0 already created above)
|
||||
local n
|
||||
for n in $(seq 1 $((OVF_NET_COUNT - 1))); do
|
||||
qm set "$NEW_VMID" "--net${n}" "e1000,bridge=$BRIDGE" &>/dev/null || true
|
||||
done
|
||||
[[ "$OVF_NET_COUNT" -gt 1 ]] && msg_ok "$(translate "Network interfaces added:") $OVF_NET_COUNT"
|
||||
|
||||
# 2. Import disks
|
||||
local disk_count="${#OVF_DISK_FILES[@]}"
|
||||
local i disk_file src_path
|
||||
local TEMP_STATUS_FILE TEMP_DISK_FILE
|
||||
|
||||
for i in "${!OVF_DISK_FILES[@]}"; do
|
||||
disk_file="${OVF_DISK_FILES[$i]}"
|
||||
src_path="$OVF_DIR/$disk_file"
|
||||
|
||||
if [[ ! -f "$src_path" ]]; then
|
||||
msg_error "$(translate "Disk file not found:") $src_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
msg_info "$(translate "Importing disk") $((i + 1))/$disk_count: $disk_file"
|
||||
msg_info2 "$(translate "Source:") $src_path"
|
||||
|
||||
TEMP_STATUS_FILE=$(mktemp)
|
||||
TEMP_DISK_FILE=$(mktemp)
|
||||
|
||||
(
|
||||
qm importdisk "$NEW_VMID" "$src_path" "$STORAGE" 2>&1
|
||||
echo $? > "$TEMP_STATUS_FILE"
|
||||
) | while IFS= read -r line; do
|
||||
if [[ "$line" =~ transferred ]]; then
|
||||
local pct
|
||||
pct=$(echo "$line" | grep -oP "\d+\.\d+(?=%)")
|
||||
[[ -n "$pct" ]] && echo -ne "\r${TAB}${BL}- $(translate "Importing:") $disk_file -${CL} ${pct}%"
|
||||
elif [[ "$line" =~ successfully\ imported\ disk ]]; then
|
||||
echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE"
|
||||
fi
|
||||
done
|
||||
echo -ne "\n"
|
||||
|
||||
local import_status
|
||||
import_status=$(cat "$TEMP_STATUS_FILE" 2>/dev/null)
|
||||
rm -f "$TEMP_STATUS_FILE"
|
||||
[[ -z "$import_status" ]] && import_status=1
|
||||
|
||||
if [[ "$import_status" -ne 0 ]]; then
|
||||
msg_error "$(translate "Import failed for:") $disk_file"
|
||||
rm -f "$TEMP_DISK_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Locate the unused disk entry in VM config
|
||||
local unused_id unused_disk
|
||||
unused_id=$(qm config "$NEW_VMID" | grep -E '^unused[0-9]+:' | tail -1 | cut -d: -f1)
|
||||
unused_disk=$(qm config "$NEW_VMID" | grep -E '^unused[0-9]+:' | tail -1 | cut -d: -f2- | xargs)
|
||||
rm -f "$TEMP_DISK_FILE"
|
||||
|
||||
if [[ -z "$unused_disk" ]]; then
|
||||
msg_error "$(translate "Could not locate imported disk in VM config.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Attach to scsi slot i
|
||||
if ! qm set "$NEW_VMID" "--scsi${i}" "$unused_disk" &>/dev/null; then
|
||||
msg_error "$(translate "Failed to attach disk as scsi$i.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Remove the unused marker
|
||||
[[ -n "$unused_id" ]] && qm set "$NEW_VMID" --delete "$unused_id" &>/dev/null || true
|
||||
|
||||
msg_ok "$(translate "Disk attached as:") scsi${i} (${disk_file})"
|
||||
done
|
||||
|
||||
# 3. Set boot disk
|
||||
echo ""
|
||||
msg_info "$(translate "Configuring boot order...")"
|
||||
if qm set "$NEW_VMID" --boot c --bootdisk "scsi0" &>/dev/null; then
|
||||
msg_ok "$(translate "Boot disk:") scsi0"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# PRINT FINAL RESULT
|
||||
# -------------------------------------------------------
|
||||
|
||||
print_import_result() {
|
||||
local disk_count="${#OVF_DISK_FILES[@]}"
|
||||
|
||||
echo ""
|
||||
msg_title "$(translate "Import Summary")"
|
||||
|
||||
msg_ok "$(translate "VM imported successfully")"
|
||||
msg_ok "$(translate "VM ID:") $NEW_VMID $(translate "Name:") $NEW_VM_NAME"
|
||||
msg_ok "$(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB $(translate "Disks:") $disk_count"
|
||||
msg_ok "$(translate "Storage:") $STORAGE $(translate "Bridge:") $BRIDGE $(translate "NICs:") $OVF_NET_COUNT"
|
||||
echo ""
|
||||
|
||||
msg_ok "$(translate "To start the VM:") qm start $NEW_VMID"
|
||||
echo ""
|
||||
|
||||
msg_title "$(translate "Manual steps recommended after import")"
|
||||
msg_info2 "$(translate "Network :") $(translate "Verify bridge assignment and NIC model — change to VirtIO if guest drivers are available")"
|
||||
msg_info2 "$(translate "Firmware :") $(translate "Check BIOS/UEFI in Hardware > BIOS — must match what the original VM used")"
|
||||
msg_info2 "$(translate "Drivers :") $(translate "If imported from ESXi: install qemu-guest-agent inside the guest OS")"
|
||||
msg_info2 "$(translate "Display :") $(translate "Set Display > Graphic card (VGA, SPICE or VirtIO) to match the guest")"
|
||||
msg_info2 "$(translate "OS type :") $(translate "Verify Options > OS Type — currently set to:") $OVF_OS_TYPE"
|
||||
echo ""
|
||||
msg_info2 "$(translate "Not imported:") $(translate "PCI passthrough, TPM state, cloud-init, snapshots, Proxmox-specific hooks")"
|
||||
echo ""
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# MAIN
|
||||
# -------------------------------------------------------
|
||||
|
||||
main() {
|
||||
if ! command -v pveversion >/dev/null 2>&1; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "Error")" \
|
||||
--msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in dialog qm pvesm qemu-img tar; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "Missing dependency")" \
|
||||
--msgbox "$(translate "Required command not found:") $cmd" 8 60
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Step 1: pick the OVA/OVF file (dialog)
|
||||
select_source_file || exit 0
|
||||
|
||||
# Step 2: extract + parse (terminal output)
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Import VM from OVA or OVF")"
|
||||
|
||||
msg_ok "$(translate "Source:") $SOURCE_FILE"
|
||||
echo ""
|
||||
|
||||
prepare_ovf || {
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to return...")"
|
||||
read -r
|
||||
exit 1
|
||||
}
|
||||
|
||||
msg_info "$(translate "Parsing OVF descriptor...")"
|
||||
if ! parse_ovf "$OVF_FILE"; then
|
||||
msg_error "$(translate "Could not parse OVF file, or no disk image references found.")"
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to return...")"
|
||||
read -r
|
||||
exit 1
|
||||
fi
|
||||
msg_ok "$(translate "OVF parsed:")"
|
||||
msg_info2 " $(translate "Name:") $OVF_VM_NAME $(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB"
|
||||
msg_info2 " $(translate "Disks:") ${#OVF_DISK_FILES[@]} $(translate "NICs:") $OVF_NET_COUNT $(translate "OS hint:") $OVF_OS_TYPE"
|
||||
|
||||
# Clean screen before returning to dialogs
|
||||
show_proxmenux_logo
|
||||
|
||||
# Step 3: configure the new VM (dialogs)
|
||||
select_import_options || exit 0
|
||||
|
||||
# Step 4: confirm (dialog)
|
||||
confirm_import || exit 0
|
||||
|
||||
# Step 5: do the import (terminal output only)
|
||||
if run_import; then
|
||||
print_import_result
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
exit 0
|
||||
else
|
||||
echo ""
|
||||
msg_error "$(translate "Import failed. VM $NEW_VMID may be in partial state.")"
|
||||
msg_info2 "$(translate "To remove partial VM:") qm destroy $NEW_VMID --destroy-unreferenced-disks 1"
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to return...")"
|
||||
read -r
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -80,21 +80,21 @@ function select_disk_type() {
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(whiptail --backtitle "ProxMenux" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
||||
"1" "$(translate "Add virtual disk")" \
|
||||
"2" "$(translate "Add import disk")" \
|
||||
"3" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
||||
"a" "$(translate "Add virtual disk")" \
|
||||
"b" "$(translate "Add import disk")" \
|
||||
"c" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
||||
"r" "$(translate "Reset current storage selection")" \
|
||||
"d" "$(translate "[ Finish and continue ]")" \
|
||||
"d" "$(translate "──── [ Finish and continue ] ────")" \
|
||||
--ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
a)
|
||||
select_virtual_disk
|
||||
;;
|
||||
2)
|
||||
b)
|
||||
select_import_disk
|
||||
;;
|
||||
3)
|
||||
c)
|
||||
select_controller_nvme
|
||||
;;
|
||||
r)
|
||||
|
||||
@@ -58,7 +58,7 @@ function select_linux_iso() {
|
||||
--backtitle "ProxMenux" \
|
||||
--title "Opciones de instalación de Linux" \
|
||||
--menu "\nSeleccione el tipo de instalación de Linux:\n\n$header" \
|
||||
18 72 10 \
|
||||
20 70 10 \
|
||||
1 "$(printf '%-35s│ %s' 'Instalar con metodo tradicional' 'Desde ISO oficial')" \
|
||||
2 "$(printf '%-35s│ %s' 'Instalar con script Cloud-Init' 'Helper Scripts')" \
|
||||
3 "$(printf '%-35s│ %s' 'Instalar con ISO personal' 'Almacenamiento local')" \
|
||||
@@ -140,7 +140,7 @@ function select_linux_iso_official() {
|
||||
|
||||
CHOICE=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Official Linux Distributions")" \
|
||||
--menu "$(translate "Select the Linux distribution to install:")\n\n$HEADER_TEXT" 20 80 12 \
|
||||
--menu "$(translate "Select the Linux distribution to install:")\n\n$HEADER_TEXT" 20 70 12 \
|
||||
"${MENU_OPTIONS[@]}" \
|
||||
3>&1 1>&2 2>&3)
|
||||
|
||||
@@ -269,7 +269,7 @@ local OTHER_OPTIONS=(
|
||||
local choice
|
||||
choice=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(translate "Other Prebuilt Linux VMs")" \
|
||||
--menu "\n$(translate "Select one of the ready-to-run Linux VMs:")" 18 70 10 \
|
||||
--menu "\n$(translate "Select one of the ready-to-run Linux VMs:")" 20 70 10 \
|
||||
"${OTHER_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
if [[ $? -ne 0 || "$choice" == "4" ]]; then
|
||||
|
||||
@@ -51,7 +51,7 @@ function select_windows_iso() {
|
||||
--backtitle "ProxMenux" \
|
||||
--title "Opciones de instalación de Windows" \
|
||||
--menu "\nSeleccione el tipo de instalación de Windows:\n\n$header" \
|
||||
18 70 10 \
|
||||
20 70 10 \
|
||||
1 "$(printf '%-34s│ %s' 'Instalar con ISO UUP Dump' 'UUP Dump ISO creator')" \
|
||||
2 "$(printf '%-34s│ %s' 'Instalar con ISO personal' 'Almacenamiento local')" \
|
||||
3 "Volver al menú principal" \
|
||||
|
||||
@@ -51,6 +51,11 @@ if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh" ]]; then
|
||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh"
|
||||
fi
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/vm/disk_selector.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_LOCAL/vm/disk_selector.sh"
|
||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/vm/disk_selector.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_DEFAULT/vm/disk_selector.sh"
|
||||
fi
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh"
|
||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
|
||||
@@ -475,24 +480,24 @@ function select_disk_type() {
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(whiptail --backtitle "ProxMenuX" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
||||
"1" "$(translate "Add virtual disk")" \
|
||||
"2" "$(translate "Add import disk")" \
|
||||
"3" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
||||
"a" "$(translate "Add virtual disk")" \
|
||||
"b" "$(translate "Add import disk")" \
|
||||
"c" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
||||
"r" "$(translate "Reset current storage selection")" \
|
||||
"d" "$(translate "[ Finish and continue ]")" \
|
||||
"d" "$(translate "──── [ Finish and continue ] ────")" \
|
||||
--ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || {
|
||||
msg_warn "$(translate "Storage plan selection cancelled.")"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
a)
|
||||
select_virtual_disk
|
||||
;;
|
||||
2)
|
||||
b)
|
||||
select_import_disk
|
||||
;;
|
||||
3)
|
||||
c)
|
||||
select_controller_nvme
|
||||
;;
|
||||
r)
|
||||
@@ -575,50 +580,6 @@ function select_virtual_disk() {
|
||||
VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}")
|
||||
}
|
||||
|
||||
function select_import_disk() {
|
||||
msg_info "$(translate "Detecting available disks...")"
|
||||
_refresh_host_storage_cache
|
||||
|
||||
local FREE_DISKS=()
|
||||
local DISK INFO MODEL SIZE LABEL DESCRIPTION
|
||||
while read -r DISK; do
|
||||
[[ "$DISK" =~ /dev/zd ]] && continue
|
||||
_disk_is_host_system_used "$DISK" && continue
|
||||
|
||||
INFO=($(lsblk -dn -o MODEL,SIZE "$DISK"))
|
||||
MODEL="${INFO[@]::${#INFO[@]}-1}"
|
||||
SIZE="${INFO[-1]}"
|
||||
LABEL=""
|
||||
if _disk_used_in_guest_configs "$DISK"; then
|
||||
LABEL+=" [⚠ $(translate "In use by VM/LXC config")]"
|
||||
fi
|
||||
DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
|
||||
if _array_contains "$DISK" "${IMPORT_DISKS[@]}"; then
|
||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "ON")
|
||||
else
|
||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF")
|
||||
fi
|
||||
done < <(lsblk -dn -e 7,11 -o PATH)
|
||||
|
||||
stop_spinner
|
||||
if [[ ${#FREE_DISKS[@]} -eq 0 ]]; then
|
||||
whiptail --title "Error" --msgbox "$(translate "No importable disks available. System disks and protected disks are hidden.")" 9 70
|
||||
return 1
|
||||
fi
|
||||
|
||||
local selected
|
||||
selected=$(whiptail --title "Select Import Disks" --checklist \
|
||||
"$(translate "Select the disks you want to import (use spacebar to toggle):")" 20 78 10 \
|
||||
"${FREE_DISKS[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
IMPORT_DISKS=()
|
||||
local item
|
||||
for item in $(echo "$selected" | tr -d '"'); do
|
||||
IMPORT_DISKS+=("$item")
|
||||
done
|
||||
export IMPORT_DISKS
|
||||
}
|
||||
|
||||
function select_controller_nvme() {
|
||||
local VM_STORAGE_IOMMU_REBOOT_POLICY="defer"
|
||||
|
||||
@@ -747,7 +708,7 @@ function prompt_controller_conflict_policy() {
|
||||
shift
|
||||
local -a source_vms=("$@")
|
||||
local msg vmid vm_name st ob
|
||||
msg="$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
||||
msg="\n$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
||||
for vmid in "${source_vms[@]}"; do
|
||||
vm_name=$(_vm_name_by_id "$vmid")
|
||||
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
||||
@@ -757,7 +718,7 @@ function prompt_controller_conflict_policy() {
|
||||
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
||||
|
||||
local choice
|
||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 22 96 10 \
|
||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 20 80 10 \
|
||||
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
||||
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
||||
"3" "$(translate "Skip this device")" \
|
||||
@@ -1486,18 +1447,26 @@ if [[ "$GPU_WIZARD_APPLIED" == "yes" ]]; then
|
||||
echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")"
|
||||
fi
|
||||
local HOST_REBOOT_REQUIRED="no"
|
||||
local REBOOT_REASONS=""
|
||||
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||
HOST_REBOOT_REQUIRED="yes"
|
||||
msg_warn "$(translate "IOMMU was enabled during this wizard. Reboot the host to apply it.")"
|
||||
msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")"
|
||||
REBOOT_REASONS+="$(translate "IOMMU has been enabled on this system.")\n"
|
||||
fi
|
||||
if [[ "$GPU_WIZARD_REBOOT_REQUIRED" == "yes" ]]; then
|
||||
HOST_REBOOT_REQUIRED="yes"
|
||||
REBOOT_REASONS+="$(translate "GPU passthrough changes require a host reboot.")\n"
|
||||
fi
|
||||
if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
|
||||
if whiptail --title "$(translate "Reboot Recommended")" --yesno \
|
||||
"$(translate "A host reboot is required to apply passthrough changes before starting the VM.")\n\n$(translate "Do you want to reboot now?")" 11 78; then
|
||||
echo ""
|
||||
if whiptail --title "$(translate "Reboot Required")" --yesno \
|
||||
"\n${REBOOT_REASONS}\n$(translate "A host reboot is required before starting the VM. Reboot now?")" 13 78; then
|
||||
msg_warn "$(translate "Rebooting the system...")"
|
||||
reboot
|
||||
else
|
||||
echo ""
|
||||
msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")"
|
||||
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
|
||||
fi
|
||||
fi
|
||||
echo -e
|
||||
|
||||
@@ -118,7 +118,7 @@ function prompt_controller_conflict_policy() {
|
||||
shift
|
||||
local -a source_vms=("$@")
|
||||
local msg vmid vm_name st ob
|
||||
msg="$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
||||
msg="\n$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
||||
for vmid in "${source_vms[@]}"; do
|
||||
vm_name=$(_vm_name_by_id "$vmid")
|
||||
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
||||
@@ -128,7 +128,7 @@ function prompt_controller_conflict_policy() {
|
||||
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
||||
|
||||
local choice
|
||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 22 96 10 \
|
||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 20 80 10 \
|
||||
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
||||
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
||||
"3" "$(translate "Skip this device")" \
|
||||
@@ -554,6 +554,7 @@ fi
|
||||
if qm set "$VMID" --hostpci${hostpci_idx} "${pci},pcie=1" >/dev/null 2>&1; then
|
||||
msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx} → ${pci})"
|
||||
DISK_INFO+="<p>Controller/NVMe: ${pci}</p>"
|
||||
BOOT_ORDER="${BOOT_ORDER:+$BOOT_ORDER;}hostpci${hostpci_idx}"
|
||||
hostpci_idx=$((hostpci_idx + 1))
|
||||
else
|
||||
msg_error "$(translate "Failed to assign Controller/NVMe") (${pci})"
|
||||
@@ -769,18 +770,26 @@ if [[ "${WIZARD_ADD_GPU:-no}" == "yes" ]]; then
|
||||
echo -e
|
||||
fi
|
||||
local HOST_REBOOT_REQUIRED="no"
|
||||
local REBOOT_REASONS=""
|
||||
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||
HOST_REBOOT_REQUIRED="yes"
|
||||
msg_warn "$(translate "IOMMU was enabled during this wizard. Reboot the host to apply it.")"
|
||||
msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")"
|
||||
REBOOT_REASONS+="$(translate "IOMMU has been enabled on this system.")\n"
|
||||
fi
|
||||
if [[ "$GPU_WIZARD_REBOOT_REQUIRED" == "yes" ]]; then
|
||||
HOST_REBOOT_REQUIRED="yes"
|
||||
REBOOT_REASONS+="$(translate "GPU passthrough changes require a host reboot.")\n"
|
||||
fi
|
||||
if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
|
||||
if whiptail --title "$(translate "Reboot Recommended")" --yesno \
|
||||
"$(translate "A host reboot is required to apply passthrough changes before starting the VM.")\n\n$(translate "Do you want to reboot now?")" 11 78; then
|
||||
echo ""
|
||||
if whiptail --title "$(translate "Reboot Required")" --yesno \
|
||||
"\n${REBOOT_REASONS}\n$(translate "A host reboot is required before starting the VM. Reboot now?")" 13 78; then
|
||||
msg_warn "$(translate "Rebooting the system...")"
|
||||
reboot
|
||||
else
|
||||
echo ""
|
||||
msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")"
|
||||
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
|
||||
fi
|
||||
fi
|
||||
msg_success "$(translate "Press Enter to return to the main menu...")"
|
||||
@@ -807,10 +816,6 @@ elif [[ "$OS_TYPE" == "3" ]]; then
|
||||
echo -e
|
||||
fi
|
||||
|
||||
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||
msg_warn "$(translate "IOMMU was enabled during this wizard. Reboot the host to apply it.")"
|
||||
fi
|
||||
|
||||
msg_success "$(translate "Press Enter to return to the main menu...")"
|
||||
read -r
|
||||
bash "$LOCAL_SCRIPTS/menus/create_vm_menu.sh"
|
||||
|
||||
@@ -44,6 +44,11 @@ if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh" ]]; then
|
||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh"
|
||||
fi
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/vm/disk_selector.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_LOCAL/vm/disk_selector.sh"
|
||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/vm/disk_selector.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_DEFAULT/vm/disk_selector.sh"
|
||||
fi
|
||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then
|
||||
source "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh"
|
||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
|
||||
@@ -490,24 +495,24 @@ function select_disk_type() {
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(whiptail --backtitle "ProxMenuX" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
||||
"1" "$(translate "Add virtual disk")" \
|
||||
"2" "$(translate "Add import disk")" \
|
||||
"3" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
||||
"a" "$(translate "Add virtual disk")" \
|
||||
"b" "$(translate "Add import disk")" \
|
||||
"c" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
||||
"r" "$(translate "Reset current storage selection")" \
|
||||
"d" "$(translate "[ Finish and continue ]")" \
|
||||
"d" "$(translate "──── [ Finish and continue ] ────")" \
|
||||
--ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || {
|
||||
msg_warn "$(translate "Storage plan selection cancelled.")"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
a)
|
||||
select_virtual_disk
|
||||
;;
|
||||
2)
|
||||
b)
|
||||
select_import_disk
|
||||
;;
|
||||
3)
|
||||
c)
|
||||
select_controller_nvme
|
||||
;;
|
||||
r)
|
||||
@@ -590,49 +595,6 @@ function select_virtual_disk() {
|
||||
|
||||
}
|
||||
|
||||
function select_import_disk() {
|
||||
msg_info "$(translate "Detecting available disks...")"
|
||||
_refresh_host_storage_cache
|
||||
|
||||
local FREE_DISKS=()
|
||||
local DISK INFO MODEL SIZE LABEL DESCRIPTION
|
||||
while read -r DISK; do
|
||||
[[ "$DISK" =~ /dev/zd ]] && continue
|
||||
_disk_is_host_system_used "$DISK" && continue
|
||||
|
||||
INFO=($(lsblk -dn -o MODEL,SIZE "$DISK"))
|
||||
MODEL="${INFO[@]::${#INFO[@]}-1}"
|
||||
SIZE="${INFO[-1]}"
|
||||
LABEL=""
|
||||
if _disk_used_in_guest_configs "$DISK"; then
|
||||
LABEL+=" [⚠ $(translate "In use by VM/LXC config")]"
|
||||
fi
|
||||
DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
|
||||
if _array_contains "$DISK" "${IMPORT_DISKS[@]}"; then
|
||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "ON")
|
||||
else
|
||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF")
|
||||
fi
|
||||
done < <(lsblk -dn -e 7,11 -o PATH)
|
||||
stop_spinner
|
||||
if [[ ${#FREE_DISKS[@]} -eq 0 ]]; then
|
||||
whiptail --title "Error" --msgbox "$(translate "No importable disks available. System disks and protected disks are hidden.")" 9 70
|
||||
return 1
|
||||
fi
|
||||
|
||||
local selected
|
||||
selected=$(whiptail --title "Select Import Disks" --checklist \
|
||||
"$(translate "Select the disks you want to import (use spacebar to toggle):")" 20 78 10 \
|
||||
"${FREE_DISKS[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
IMPORT_DISKS=()
|
||||
local item
|
||||
for item in $(echo "$selected" | tr -d '"'); do
|
||||
IMPORT_DISKS+=("$item")
|
||||
done
|
||||
export IMPORT_DISKS
|
||||
}
|
||||
|
||||
function select_controller_nvme() {
|
||||
local VM_STORAGE_IOMMU_REBOOT_POLICY="defer"
|
||||
|
||||
@@ -761,7 +723,7 @@ function prompt_controller_conflict_policy() {
|
||||
shift
|
||||
local -a source_vms=("$@")
|
||||
local msg vmid vm_name st ob
|
||||
msg="$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
||||
msg="\n$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
||||
for vmid in "${source_vms[@]}"; do
|
||||
vm_name=$(_vm_name_by_id "$vmid")
|
||||
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
||||
@@ -771,7 +733,7 @@ function prompt_controller_conflict_policy() {
|
||||
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
||||
|
||||
local choice
|
||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 22 96 10 \
|
||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 20 80 10 \
|
||||
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
||||
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
||||
"3" "$(translate "Skip this device")" \
|
||||
@@ -1398,6 +1360,7 @@ function create_vm() {
|
||||
msg_ok "Configured controller/NVMe as hostpci${HOSTPCI_INDEX}: ${PCI_DEV}"
|
||||
DISK_INFO="${DISK_INFO}<p>Controller/NVMe: ${PCI_DEV}</p>"
|
||||
CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Controller/NVMe: ${PCI_DEV} (hostpci${HOSTPCI_INDEX})\n"
|
||||
BOOT_ORDER_LIST+=("hostpci${HOSTPCI_INDEX}")
|
||||
HOSTPCI_INDEX=$((HOSTPCI_INDEX + 1))
|
||||
else
|
||||
msg_error "Failed to configure controller/NVMe: ${PCI_DEV}"
|
||||
@@ -1511,18 +1474,26 @@ else
|
||||
echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")"
|
||||
fi
|
||||
local HOST_REBOOT_REQUIRED="no"
|
||||
local REBOOT_REASONS=""
|
||||
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||
HOST_REBOOT_REQUIRED="yes"
|
||||
msg_warn "$(translate "IOMMU was enabled during this wizard. Reboot the host to apply it.")"
|
||||
msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")"
|
||||
REBOOT_REASONS+="$(translate "IOMMU has been enabled on this system.")\n"
|
||||
fi
|
||||
if [[ "$GPU_WIZARD_REBOOT_REQUIRED" == "yes" ]]; then
|
||||
HOST_REBOOT_REQUIRED="yes"
|
||||
REBOOT_REASONS+="$(translate "GPU passthrough changes require a host reboot.")\n"
|
||||
fi
|
||||
if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
|
||||
if whiptail --title "$(translate "Reboot Recommended")" --yesno \
|
||||
"$(translate "A host reboot is required to apply passthrough changes before starting the VM.")\n\n$(translate "Do you want to reboot now?")" 11 78; then
|
||||
echo ""
|
||||
if whiptail --title "$(translate "Reboot Required")" --yesno \
|
||||
"\n${REBOOT_REASONS}\n$(translate "A host reboot is required before starting the VM. Reboot now?")" 13 78; then
|
||||
msg_warn "$(translate "Rebooting the system...")"
|
||||
reboot
|
||||
else
|
||||
echo ""
|
||||
msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")"
|
||||
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
|
||||
fi
|
||||
fi
|
||||
echo -e
|
||||
|
||||
Reference in New Issue
Block a user