#!/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