Update scripts

This commit is contained in:
MacRimi
2026-04-12 20:32:34 +02:00
parent 4fa4bbb08b
commit 4843fae0eb
47 changed files with 8313 additions and 3014 deletions

View File

@@ -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