diff --git a/scripts/import-disk-image.sh b/scripts/import-disk-image.sh index a1b5136..39990e1 100644 --- a/scripts/import-disk-image.sh +++ b/scripts/import-disk-image.sh @@ -10,23 +10,21 @@ # Last Updated: 28/01/2025 # ========================================================== # Description: -# This script allows users to assign physical disks to existing -# Proxmox virtual machines (VMs) through an interactive menu. -# - Detects the system disk and excludes it from selection. -# - Lists all available VMs for the user to choose from. -# - Identifies and displays unassigned physical disks. -# - Allows the user to select multiple disks and attach them to a VM. -# - Supports interface types: SATA, SCSI, VirtIO, and IDE. -# - 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. +# This script automates the process of importing disk images +# into existing Proxmox virtual machines (VMs). It: +# - Scans the storage directory for compatible disk images (.img, .qcow2, .vmdk) +# - Allows the user to select a target VM for disk import +# - Lists available Proxmox storage volumes for disk placement +# - Enables users to select one or multiple disk images for import +# - Assigns a suitable disk interface (SATA, SCSI, VirtIO, IDE) +# - Handles SSD emulation and bootable disk configuration (optional) +# - Ensures disks are properly attached to the VM and configured # -# The goal of this script is to simplify the process of assigning -# physical disks to Proxmox VMs, reducing manual configurations -# and preventing potential errors. +# This script simplifies the process of managing virtual disk +# imports, making it easier to integrate pre-existing disk images +# into Proxmox virtual machines without manual configuration. # ========================================================== - # Configuration ============================================ REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main" BASE_DIR="/usr/local/share/proxmenux" @@ -40,214 +38,208 @@ load_language initialize_cache # ========================================================== +# Path where disk images are stored +IMAGES_DIR="/var/lib/vz/template/images/" -# Function to identify the physical disk where Proxmox is installed -get_physical_disk() { - local lv_path=$1 - local pv_name - pv_name=$(pvs --noheadings -o pv_name 2>/dev/null | grep -v "/dev/mapper" | head -n1 | tr -d ' ') || true - if [ -z "$pv_name" ]; then - echo "$(translate "Could not determine the physical disk. Is LVM installed?")" >&2 - return 1 - fi - echo "$pv_name" | sed 's/[0-9]*$//' -} - -# Function to get detailed disk information -get_disk_info() { - local disk=$1 - lsblk -ndo NAME,MODEL,SIZE "$disk" | awk '{print $1 " " $2 " " $3}' -} - -# Detect the root partition and associated physical disk -root_device=$(findmnt -n -o SOURCE / 2>/dev/null) || { echo "$(translate "Could not determine the root device.")" >&2; exit 1; } -if [[ $root_device == /dev/mapper/* ]]; then - physical_disk=$(get_physical_disk "$root_device") -else - physical_disk=$(echo "$root_device" | sed 's/[0-9]*$//') +# Ensure directory exists +if [ ! -d "$IMAGES_DIR" ]; then + mkdir -p "$IMAGES_DIR" + chmod 755 "$IMAGES_DIR" + msg_info "Created missing directory: $IMAGES_DIR" +fi +# Check if there are any images in the directory +IMAGES=$(ls -A "$IMAGES_DIR" | grep -E "\.(img|qcow2|vmdk)$") +if [ -z "$IMAGES" ]; then + msg_error "$(translate 'No images available for import in') $IMAGES_DIR" + echo -e "${YW}$(translate 'Supported formats: .img, .qcow2, .vmdk')${CL}" + echo -e "${YW}$(translate 'Please add some images and try again.')${CL}" + exit 0 fi -if [ -z "$physical_disk" ]; then - echo "$(translate "Could not determine the physical disk.")" >&2 - exit 1 + + +# Initial setup +if ! [ -d "$IMAGES_DIR" ]; then + msg_info "$(translate 'Creating images directory')" + mkdir -p "$IMAGES_DIR" + msg_ok "$(translate 'Images directory created')" fi -msg_ok "$(translate "System physical disk identified"): $physical_disk. $(translate "This disk will not be shown.")" +# Display initial message +whiptail --title "$(translate 'Import Disk Image')" --msgbox "$(translate 'Make sure the disk images you want to import are located in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk.')" 12 60 -# Display list of available VMs -VM_LIST=$(qm list | awk 'NR>1 {print $1, $2}') + + +# 1. Select VM +msg_info "$(translate 'Getting VM list')" +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 + msg_error "$(translate 'No VMs available in the system')" exit 1 fi +msg_ok "$(translate 'VM list obtained')" -# Select VM -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) +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 - whiptail --title "$(translate "Error")" --msgbox "$(translate "No VM was selected.")" 8 40 + msg_error "$(translate 'No VM selected')" exit 1 fi -VMID=$(echo "$VMID" | tr -d '"') -# Verify that VMID is a number -if ! [[ "$VMID" =~ ^[0-9]+$ ]]; then - whiptail --title "$(translate "Error")" --msgbox "$(translate "The selected VM ID is not valid.")" 8 40 + +# 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')" + +# Create an array of storage options for whiptail +STORAGE_OPTIONS=() +while read -r storage; do + STORAGE_OPTIONS+=("$storage" "") +done <<< "$STORAGE_LIST" + +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 [ -z "$STORAGE" ]; then + msg_error "$(translate 'No storage selected')" exit 1 fi -clear -msg_ok "$(translate "VM selected successfully.")" -# Check if the VM is powered on -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 + +# 3. Select disk images +msg_info "$(translate 'Scanning disk images')" +if [ -z "$IMAGES" ]; then + msg_error "$(translate 'No compatible disk images found in') $IMAGES_DIR" + exit 0 +fi +msg_ok "$(translate 'Disk images found')" + +IMAGE_OPTIONS=() +while 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) + +if [ -z "$SELECTED_IMAGES" ]; then + msg_error "$(translate 'No images selected')" exit 1 fi -msg_info "$(translate "Detecting available disks...")" +# 4. Import each selected image +for IMAGE in $SELECTED_IMAGES; do -# Detect free disks, excluding the system disk and those already assigned to the selected VM -FREE_DISKS=() -while read -r LINE; do - DISK=$(echo "$LINE" | awk '{print $1}') - if [[ "/dev/$DISK" != "$physical_disk" ]] && ! qm config "$VMID" | grep -q "/dev/$DISK"; then - DESCRIPTION=$(echo "$LINE" | awk '{$1=""; print $0}' | xargs) - FREE_DISKS+=("/dev/$DISK" "$DESCRIPTION" "OFF") - fi -done < <(lsblk -d -n -e 7,11 -o NAME,MODEL,SIZE) + # Remove quotes from selected image + IMAGE=$(echo "$IMAGE" | tr -d '"') -msg_ok "$(translate "Available disks detected.")" + # 5. Select interface type for each 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 [ "${#FREE_DISKS[@]}" -eq 0 ]; then - whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks available for this VM.")" 8 40 - clear - exit 1 -fi - -# Calculate maximum content length -MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1) -TOTAL_WIDTH=$((MAX_WIDTH + 20)) # Add additional margin - -# Set a reasonable minimum width -if [ $TOTAL_WIDTH -lt 70 ]; then - TOTAL_WIDTH=70 -fi - -# Display menu to select free disks with dynamically calculated width -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) - -# Check if disks were selected -if [ -z "$SELECTED" ]; then - whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks were selected.")" 10 $TOTAL_WIDTH - clear - exit 1 -fi - -msg_ok "$(translate "Disks selected successfully.")" - -# Select interface type once for all disks -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")" \ - "virtio" "$(translate "Add as VirtIO")" \ - "ide" "$(translate "Add as IDE")" 3>&1 1>&2 2>&3) - -if [ -z "$INTERFACE" ]; then - whiptail --title "$(translate "Error")" --msgbox "$(translate "No interface type was selected for the disks.")" 8 40 - clear - exit 1 -fi - -msg_ok "$(translate "Interface type selected: $INTERFACE")" - -# Verify selected disks -DISKS_ADDED=0 -ERROR_MESSAGES="" -SUCCESS_MESSAGES="" - -msg_info "$(translate "Processing selected disks...")" - -for DISK in $SELECTED; do - DISK=$(echo "$DISK" | tr -d '"') - DISK_INFO=$(get_disk_info "$DISK") - - # Check if the disk is already assigned to another VM - ASSIGNED_TO="" - while read -r VM_ID VM_NAME; do - if [[ "$VM_ID" =~ ^[0-9]+$ ]] && qm config "$VM_ID" | grep -q "$DISK"; then - ASSIGNED_TO+="$VM_ID $VM_NAME\n" - fi - done < <(qm list | awk 'NR>1 {print $1, $2}') - - CONTINUE=true - if [ -n "$ASSIGNED_TO" ]; then - RUNNING_VMS="" - while read -r VM_ID VM_NAME; do - if [[ "$VM_ID" =~ ^[0-9]+$ ]] && [ "$(qm status "$VM_ID" | awk '{print $2}')" == "running" ]; then - RUNNING_VMS+="$VM_ID $VM_NAME\n" - fi - done < <(echo -e "$ASSIGNED_TO") - - if [ -n "$RUNNING_VMS" ]; then - ERROR_MESSAGES+="$(translate "The disk") $DISK_INFO $(translate "is in use by the following running VM(s):")\\n$RUNNING_VMS\\n\\n" - CONTINUE=false - fi + if [ -z "$INTERFACE" ]; then + msg_error "$(translate 'No interface type selected for') $IMAGE" + continue fi - if $CONTINUE; then - INDEX=0 - while qm config "$VMID" | grep -q "${INTERFACE}${INDEX}"; do - ((INDEX++)) - done + FULL_PATH="$IMAGES_DIR/$IMAGE" - # Perform the assignment - RESULT=$(qm set "$VMID" -${INTERFACE}${INDEX} "$DISK" 2>&1) + # Show initial message + msg_info "$(translate 'Importing image:')" - 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$(translate "WARNING: This disk is also assigned to the following VM(s):")\n$ASSIGNED_TO" - MESSAGE+="$(translate "Make sure not to power on VMs that share this disk simultaneously to avoid data corruption.")\n" + # Temporary file to capture the imported disk + TEMP_DISK_FILE=$(mktemp) + + + # Execute the command and process its output in real-time + qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1 | while read -r line; do + if [[ "$line" =~ transferred ]]; then + + # Extract the progress percentage + PERCENT=$(echo "$line" | grep -oP "\(\d+\.\d+%\)" | tr -d '()%') + + # Show progress with custom format without translation + echo -ne "\r${TAB}${YW}-$(translate 'Importing image:') $IMAGE-${CL} ${PERCENT}%" + + elif [[ "$line" =~ successfully\ imported\ disk ]]; then + + # Extract the imported disk name and save it to the temporary file + echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE" + fi + done + echo -ne "\n" + + + IMPORT_STATUS=${PIPESTATUS[0]} # Capture the exit status of the main command + + if [ $IMPORT_STATUS -eq 0 ]; then + msg_ok "$(translate 'Image imported successfully')" + + # Read the imported disk from the temporary file + IMPORTED_DISK=$(cat "$TEMP_DISK_FILE") + rm -f "$TEMP_DISK_FILE" # Delete the temporary file + + if [ -n "$IMPORTED_DISK" ]; then + + # Find the next available disk slot + EXISTING_DISKS=$(qm config "$VMID" | grep -oP "${INTERFACE}\d+" | sort -n) + if [ -z "$EXISTING_DISKS" ]; then + + # If there are no existing disks, start from 0 + NEXT_SLOT=0 + else + # If there are existing disks, take the last one and add 1 + LAST_SLOT=$(echo "$EXISTING_DISKS" | tail -n1 | sed "s/${INTERFACE}//") + NEXT_SLOT=$((LAST_SLOT + 1)) + fi + + + # Ask if SSD emulation is desired (only for non-VirtIO interfaces) + 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')" + + # Configure the disk in the VM + 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}" + + # Ask if the disk should be bootable + 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" fi - SUCCESS_MESSAGES+="$MESSAGE\\n\\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 find the imported disk')" fi + else + msg_error "$(translate 'Could not import') $IMAGE" fi done -msg_ok "$(translate "Disk processing completed.")" - -# Display success messages -if [ -n "$SUCCESS_MESSAGES" ]; then - - -MSG_LINES=$(echo "$SUCCESS_MESSAGES" | wc -l) - - - - whiptail --title "$(translate "Successful Operations")" --scrolltext --msgbox "$SUCCESS_MESSAGES" 20 70 - - - -fi - -# Display error or warning messages if any -if [ -n "$ERROR_MESSAGES" ]; then - whiptail --title "$(translate "Warnings and Errors")" --scrolltext --msgbox "$ERROR_MESSAGES" 20 70 -fi - -# Operation completed message -if [ $DISKS_ADDED -gt 0 ]; then - whiptail --title "$(translate "Operation Completed")" --msgbox "$(translate "$DISKS_ADDED disk(s) were successfully added to VM") $VMID." 8 60 -else - whiptail --title "$(translate "Information")" --msgbox "$(translate "No disks were added to VM") $VMID." 8 60 -fi - -clear -exit 0 +msg_ok "$(translate 'All selected images have been processed')" +sleep 2