diff --git a/scripts/import-disk-image.sh b/scripts/import-disk-image.sh index 3b42217..a1b5136 100644 --- a/scripts/import-disk-image.sh +++ b/scripts/import-disk-image.sh @@ -10,21 +10,23 @@ # Last Updated: 28/01/2025 # ========================================================== # Description: -# 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 +# 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 simplifies the process of managing virtual disk -# imports, making it easier to integrate pre-existing disk images -# into Proxmox virtual machines without manual configuration. +# The goal of this script is to simplify the process of assigning +# physical disks to Proxmox VMs, reducing manual configurations +# and preventing potential errors. # ========================================================== + # Configuration ============================================ REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main" BASE_DIR="/usr/local/share/proxmenux" @@ -38,209 +40,214 @@ load_language initialize_cache # ========================================================== -# Path where disk images are stored -IMAGES_DIR="/var/lib/vz/template/images/" -# Ensure directory exists -if [ ! -d "$IMAGES_DIR" ]; then - mkdir -p "$IMAGES_DIR" - chmod 755 "$IMAGES_DIR" - msg_info "Created missing directory: $IMAGES_DIR" +# 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]*$//') 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 - - - -# Initial setup -if ! [ -d "$IMAGES_DIR" ]; then - msg_info "$(translate 'Creating images directory')" - mkdir -p "$IMAGES_DIR" - msg_ok "$(translate 'Images directory created')" -fi - -# 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 - - - -# 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')" +if [ -z "$physical_disk" ]; then + echo "$(translate "Could not determine the physical disk.")" >&2 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) +msg_ok "$(translate "System physical disk identified"): $physical_disk. $(translate "This disk will not be shown.")" + +# Display list of available VMs +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 + exit 1 +fi + +# 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) if [ -z "$VMID" ]; then - msg_error "$(translate 'No VM selected')" + whiptail --title "$(translate "Error")" --msgbox "$(translate "No VM was selected.")" 8 40 exit 1 fi +VMID=$(echo "$VMID" | tr -d '"') - -# 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')" +# 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 exit 1 fi +clear +msg_ok "$(translate "VM selected successfully.")" - -# 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')" +# 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 exit 1 fi -# 4. Import each selected image -for IMAGE in $SELECTED_IMAGES; do +msg_info "$(translate "Detecting available disks...")" - # Remove quotes from selected image - IMAGE=$(echo "$IMAGE" | tr -d '"') +# 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) - # 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) +msg_ok "$(translate "Available disks detected.")" - if [ -z "$INTERFACE" ]; then - msg_error "$(translate 'No interface type selected for') $IMAGE" - continue +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 fi - FULL_PATH="$IMAGES_DIR/$IMAGE" + if $CONTINUE; then + INDEX=0 + while qm config "$VMID" | grep -q "${INTERFACE}${INDEX}"; do + ((INDEX++)) + done - # Show initial message - msg_info "$(translate 'Importing image:')" + # Perform the assignment + RESULT=$(qm set "$VMID" -${INTERFACE}${INDEX} "$DISK" 2>&1) - # 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" + 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" fi + SUCCESS_MESSAGES+="$MESSAGE\\n\\n" + ((DISKS_ADDED++)) else - msg_error "$(translate 'Could not find the imported disk')" + ERROR_MESSAGES+="$(translate "Could not add disk") $DISK_INFO $(translate "to VM") $VMID.\\n$(translate "Error:") $RESULT\\n\\n" fi - else - msg_error "$(translate 'Could not import') $IMAGE" fi done -msg_ok "$(translate 'All selected images have been processed')" -sleep 2 +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