mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-23 12:10:38 +00:00
361 lines
12 KiB
Bash
361 lines
12 KiB
Bash
#!/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.3
|
|
# Last Updated: 12/04/2026
|
|
# ==========================================================
|
|
# Description:
|
|
# 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 ============================================
|
|
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 ============================================
|
|
|
|
|
|
_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)
|
|
[[ -d "$path" ]] && echo "$path" && return 0
|
|
done
|
|
echo "/var/lib/vz/template/iso"
|
|
}
|
|
|
|
|
|
# ==========================================================
|
|
# 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
|
|
|
|
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 type _rest; do
|
|
STORAGE_OPTIONS+=("$storage" "$type")
|
|
done < <(pvesm status -content images 2>/dev/null | awk 'NR>1')
|
|
stop_spinner
|
|
|
|
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 [[ ${#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"
|
|
|
|
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
|
|
|
|
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 IFS= read -r img; do
|
|
IMAGE_OPTIONS+=("$img" "" "OFF")
|
|
done <<< "$IMAGES"
|
|
|
|
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
|
|
|
|
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.
|
|
# ==========================================================
|
|
|
|
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 ""
|
|
|
|
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"
|
|
|
|
if [[ ! -f "$FULL_PATH" ]]; then
|
|
msg_error "$(translate 'Image file not found:') $FULL_PATH"
|
|
FAILED=$((FAILED + 1))
|
|
continue
|
|
fi
|
|
|
|
# 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
|
|
|
|
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_success "$(translate 'Press Enter to return to menu...')"
|
|
read -r
|