diff --git a/scripts/menus/sm.sh b/scripts/menus/sm.sh new file mode 100644 index 00000000..d8c726b5 --- /dev/null +++ b/scripts/menus/sm.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# ========================================================== +# ProxMenu - A menu-driven script for Proxmox VE management +# ========================================================== +# Author : MacRimi +# Copyright : (c) 2024 MacRimi +# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE) +# Version : 1.0 +# Last Updated: 28/01/2025 +# ========================================================== + + +# Configuration ============================================ +REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main" +BASE_DIR="/usr/local/share/proxmenux" +UTILS_FILE="$BASE_DIR/utils.sh" +VENV_PATH="/opt/googletrans-env" + +if [[ -f "$UTILS_FILE" ]]; then + source "$UTILS_FILE" +fi +load_language +initialize_cache +# ========================================================== + + +while true; do + OPTION=$(whiptail --title "$(translate "Disk and Storage Manager Menu")" --menu "$(translate "Select an option:")" 20 70 10 \ + "1" "$(translate "Add Disk Passthrough to a VM")" \ + "2" "$(translate "Add Disk Passthrough to a CT")" \ + "3" "$(translate "Import Disk Image to a VM")" \ + "4" "$(translate "Mount point to CT")" \ + "5" "$(translate "Mount disk on HOST")" \ + "6" "$(translate "Unmount disk from HOST")" \ + "7" "$(translate "Format disk")" \ + "8" "$(translate "Return to Main Menu")" 3>&1 1>&2 2>&3) + + case $OPTION in + 1) + msg_info2 "$(translate "Running script: Add Disk Passthrough to a VM")..." + bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough-vm.sh") + ;; + 2) + msg_info2 "$(translate "Running script: Add Disk Passthrough to a CT")..." + bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough.sh") + ;; + 3) + msg_info2 "$(translate "Running script: Import Disk Image to a VM")..." + bash <(curl -s "$REPO_URL/scripts/storage/import-disk-image.sh") + ;; + 4) + msg_info2 "$(translate "Running script: Mount point to CT")..." + bash <(curl -s "$REPO_URL/scripts/storage/mount-point-to-ct.sh") + ;; + 5) + msg_info2 "$(translate "Running script: Mount disk on HOST")..." + bash <(curl -s "$REPO_URL/scripts/storage/mount-disk-on-host.sh") + ;; + 6) + msg_info2 "$(translate "Running script: Unmount disk from HOST")..." + bash <(curl -s "$REPO_URL/scripts/storage/unmount-disk-from-host.sh") + ;; + 7) + msg_info2 "$(translate "Running script: Format disk")..." + bash <(curl -s "$REPO_URL/scripts/storage/format-disk.sh") + ;; + 8) + exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") + ;; + *) + exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh") + ;; + esac +done + diff --git a/scripts/storage/disk-passthrough.sh b/scripts/storage/disk-passthrough.sh new file mode 100644 index 00000000..0ed180e0 --- /dev/null +++ b/scripts/storage/disk-passthrough.sh @@ -0,0 +1,367 @@ +#!/bin/bash + +# ========================================================== +# ProxMenu - A menu-driven script for Proxmox VE management +# ========================================================== +# Author : MacRimi +# Copyright : (c) 2024 MacRimi +# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE) +# Version : 1.0 +# 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. +# +# 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" +UTILS_FILE="$BASE_DIR/utils.sh" +VENV_PATH="/opt/googletrans-env" + +if [[ -f "$UTILS_FILE" ]]; then + source "$UTILS_FILE" +fi +load_language +initialize_cache +# ========================================================== + + + +get_disk_info() { + local disk=$1 + MODEL=$(lsblk -dn -o MODEL "$disk" | xargs) + SIZE=$(lsblk -dn -o SIZE "$disk" | xargs) + echo "$MODEL" "$SIZE" +} + + +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 + + +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 + whiptail --title "$(translate "Error")" --msgbox "$(translate "No VM was selected.")" 8 40 + exit 1 +fi + +VMID=$(echo "$VMID" | tr -d '"') + + +msg_ok "$(translate "VM selected successfully.")" + + +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 + + +########################################## + +msg_info "$(translate "Detecting available disks...")" + +USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}') +MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}') + +ZFS_DISKS="" +ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror') + +for entry in $ZFS_RAW; do + + path="" + if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then + if [ -e "/dev/disk/by-id/$entry" ]; then + path=$(readlink -f "/dev/disk/by-id/$entry") + fi + elif [[ "$entry" == /dev/* ]]; then + path="$entry" + fi + + + if [ -n "$path" ]; then + base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null) + if [ -n "$base_disk" ]; then + ZFS_DISKS+="/dev/$base_disk"$'\n' + fi + fi +done + +ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u) + + +is_disk_in_use() { + local disk="$1" + + + while read -r part fstype; do + case "$fstype" in + zfs_member|linux_raid_member) + return 0 ;; + esac + + if echo "$MOUNTED_DISKS" | grep -q "/dev/$part"; then + return 0 + fi + done < <(lsblk -ln -o NAME,FSTYPE "$disk" | tail -n +2) + + + if echo "$USED_DISKS" | grep -q "$disk" || echo "$ZFS_DISKS" | grep -q "$disk"; then + return 0 + fi + + return 1 +} + + + + +FREE_DISKS=() + +LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u) +RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u) + +while read -r DISK; do + + [[ "$DISK" =~ /dev/zd ]] && continue + + INFO=($(get_disk_info "$DISK")) + MODEL="${INFO[@]::${#INFO[@]}-1}" + SIZE="${INFO[-1]}" + LABEL="" + SHOW_DISK=true + + IS_MOUNTED=false + IS_RAID=false + IS_ZFS=false + IS_LVM=false + + while read -r part fstype; do + [[ "$fstype" == "zfs_member" ]] && IS_ZFS=true + [[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true + [[ "$fstype" == "LVM2_member" ]] && IS_LVM=true + if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then + IS_MOUNTED=true + fi + done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2) + + REAL_PATH=$(readlink -f "$DISK") + if echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then + IS_MOUNTED=true + fi + + + + USED_BY="" + REAL_PATH=$(readlink -f "$DISK") + CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null) + + if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then + USED_BY="⚠ $(translate "In use")" + else + for SYMLINK in /dev/disk/by-id/*; do + if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then + if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then + USED_BY="⚠ $(translate "In use")" + break + fi + fi + done + fi + + + + + if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then + if grep -q "active raid" /proc/mdstat; then + SHOW_DISK=false + fi + fi + + + if $IS_ZFS; then + SHOW_DISK=false + fi + + + if $IS_MOUNTED; then + SHOW_DISK=false + fi + + + if qm config "$VMID" | grep -vE '^\s*#|^description:' | grep -q "$DISK"; then + SHOW_DISK=false + fi + + if $SHOW_DISK; then + [[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]" + [[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID" + [[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM" + [[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS" + + DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL") + FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF") + fi +done < <(lsblk -dn -e 7,11 -o PATH) + + + +if [ "${#FREE_DISKS[@]}" -eq 0 ]; then + cleanup + whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks available for this VM.")" 8 40 + clear + exit 1 +fi + +msg_ok "$(translate "Available disks detected.")" + + + +###################################################### + + + + +MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1) +TOTAL_WIDTH=$((MAX_WIDTH + 20)) + +if [ $TOTAL_WIDTH -lt 50 ]; then + TOTAL_WIDTH=50 +fi + + +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) + +if [ -z "$SELECTED" ]; then + whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks were selected.")" 10 64 + clear + exit 1 +fi + +msg_ok "$(translate "Disks selected successfully.")" + + +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")" + +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") + + ASSIGNED_TO="" + RUNNING_VMS="" + RUNNING_CTS="" + + + while read -r VM_ID VM_NAME; do + if [[ "$VM_ID" =~ ^[0-9]+$ ]] && qm config "$VM_ID" | grep -q "$DISK"; then + ASSIGNED_TO+="VM $VM_ID $VM_NAME\n" + VM_STATUS=$(qm status "$VM_ID" | awk '{print $2}') + if [ "$VM_STATUS" == "running" ]; then + RUNNING_VMS+="VM $VM_ID $VM_NAME\n" + fi + fi + done < <(qm list | awk 'NR>1 {print $1, $2}') + + + while read -r CT_ID CT_NAME; do + if [[ "$CT_ID" =~ ^[0-9]+$ ]] && pct config "$CT_ID" | grep -q "$DISK"; then + ASSIGNED_TO+="CT $CT_ID $CT_NAME\n" + CT_STATUS=$(pct status "$CT_ID" | awk '{print $2}') + if [ "$CT_STATUS" == "running" ]; then + RUNNING_CTS+="CT $CT_ID $CT_NAME\n" + fi + fi + done < <(pct list | awk 'NR>1 {print $1, $2}') + + if [ -n "$RUNNING_VMS" ] || [ -n "$RUNNING_CTS" ]; then + ERROR_MESSAGES+="$(translate "The disk") $DISK_INFO $(translate "is currently in use by the following running VM(s) or CT(s):")\\n$RUNNING_VMS$RUNNING_CTS\\n\\n$(translate "You cannot add this disk while the VM or CT is running.")\\n$(translate "Please shut it down first and run this script again to add the disk.")\\n\\n" + continue + fi + + if [ -n "$ASSIGNED_TO" ]; then + cleanup + whiptail --title "$(translate "Disk Already Assigned")" --yesno "$(translate "The disk") $DISK_INFO $(translate "is already assigned to the following VM(s) or CT(s):")\\n$ASSIGNED_TO\\n\\n$(translate "Do you want to continue anyway?")" 15 70 + if [ $? -ne 0 ]; then + sleep 1 + exec "$0" + fi + fi + + + INDEX=0 + while qm config "$VMID" | grep -q "${INTERFACE}${INDEX}"; do + ((INDEX++)) + done + + RESULT=$(qm set "$VMID" -${INTERFACE}${INDEX} "$DISK" 2>&1) + + 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\\n$(translate "WARNING: This disk is also assigned to the following VM(s):")\\n$ASSIGNED_TO" + MESSAGE+="\\n$(translate "Make sure not to start VMs that share this disk at the same time to avoid data corruption.")" + 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" + fi +done + +msg_ok "$(translate "Disk processing completed.")" + + + +if [ -n "$SUCCESS_MESSAGES" ]; then + MSG_LINES=$(echo "$SUCCESS_MESSAGES" | wc -l) + whiptail --title "$(translate "Successful Operations")" --msgbox "$SUCCESS_MESSAGES" 16 70 +fi + +if [ -n "$ERROR_MESSAGES" ]; then + whiptail --title "$(translate "Warnings and Errors")" --msgbox "$ERROR_MESSAGES" 16 70 +fi + + + +exit 0 diff --git a/scripts/storage/disk-passthrough_ct.sh b/scripts/storage/disk-passthrough_ct.sh new file mode 100644 index 00000000..df9f81d8 --- /dev/null +++ b/scripts/storage/disk-passthrough_ct.sh @@ -0,0 +1,531 @@ +#!/bin/bash + +# ========================================================== +# ProxMenu - A menu-driven script for Proxmox VE management +# ========================================================== +# Author : MacRimi +# Copyright : (c) 2024 MacRimi +# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE) +# Version : 1.0 +# Last Updated: 28/01/2025 +# ========================================================== +# Description: +# This script allows users to assign physical disks to existing +# Proxmox containers (CTs) through an interactive menu. +# - Detects the system disk and excludes it from selection. +# - Lists all available CTs for the user to choose from. +# - Identifies and displays unassigned physical disks. +# - Allows the user to select multiple disks and attach them to a CT. +# - Configures the selected disks for the CT and verifies the assignment. +# ========================================================== + + +# Configuration ============================================ +REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main" +BASE_DIR="/usr/local/share/proxmenux" +UTILS_FILE="$BASE_DIR/utils.sh" +VENV_PATH="/opt/googletrans-env" + +if [[ -f "$UTILS_FILE" ]]; then + source "$UTILS_FILE" +fi +load_language +initialize_cache +# ========================================================== + + + +get_disk_info() { + local disk=$1 + MODEL=$(lsblk -dn -o MODEL "$disk" | xargs) + SIZE=$(lsblk -dn -o SIZE "$disk" | xargs) + echo "$MODEL" "$SIZE" +} + + + +CT_LIST=$(pct list | awk 'NR>1 {print $1, $3}') +if [ -z "$CT_LIST" ]; then + whiptail --title "$(translate "Error")" --msgbox "$(translate "No CTs available in the system.")" 8 40 + exit 1 +fi + + +CTID=$(whiptail --title "$(translate "Select CT")" --menu "$(translate "Select the CT to which you want to add disks:")" 15 60 8 $CT_LIST 3>&1 1>&2 2>&3) + +if [ -z "$CTID" ]; then + whiptail --title "$(translate "Error")" --msgbox "$(translate "No CT was selected.")" 8 40 + exit 1 +fi + +CTID=$(echo "$CTID" | tr -d '"') + +msg_ok "$(translate "CT selected successfully.")" + + + + +CT_STATUS=$(pct status "$CTID" | awk '{print $2}') +if [ "$CT_STATUS" != "running" ]; then + msg_info "$(translate "Starting CT") $CTID..." + pct start "$CTID" + sleep 2 + if [ "$(pct status "$CTID" | awk '{print $2}')" != "running" ]; then + msg_error "$(translate "Failed to start the CT.")" + exit 1 + fi + msg_ok "$(translate "CT started successfully.")" +fi + + + + +CONF_FILE="/etc/pve/lxc/$CTID.conf" + +if grep -q '^unprivileged: 1' "$CONF_FILE"; then + if whiptail --title "$(translate "Privileged Container")" \ + --yesno "$(translate "The selected container is unprivileged. A privileged container is required for direct device passthrough.")\\n\\n$(translate "Do you want to convert it to a privileged container now?")" 12 70; then + + msg_info "$(translate "Stopping container") $CTID..." + pct shutdown "$CTID" & + for i in {1..10}; do + sleep 1 + if [ "$(pct status "$CTID" | awk '{print $2}')" != "running" ]; then + break + fi + done + + if [ "$(pct status "$CTID" | awk '{print $2}')" == "running" ]; then + msg_error "$(translate "Failed to stop the container.")" + exit 1 + fi + + msg_ok "$(translate "Container stopped.")" + + cp "$CONF_FILE" "$CONF_FILE.bak" + sed -i '/^unprivileged: 1/d' "$CONF_FILE" + echo "unprivileged: 0" >> "$CONF_FILE" + + msg_ok "$(translate "Container successfully converted to privileged.")" + + msg_info "$(translate "Starting container") $CTID..." + pct start "$CTID" + sleep 2 + if [ "$(pct status "$CTID" | awk '{print $2}')" != "running" ]; then + msg_error "$(translate "Failed to start the container.")" + exit 1 + fi + msg_ok "$(translate "Container started successfully.")" + + else + whiptail --title "$(translate "Aborted")" \ + --msgbox "$(translate "Operation cancelled. Cannot continue with an unprivileged container.")" 10 60 + exit 1 + fi +fi + + + + + +########################################## + + + + + +msg_info "$(translate "Detecting available disks...")" + +USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}') +MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}') + +ZFS_DISKS="" +ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror') + +for entry in $ZFS_RAW; do + path="" + if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then + if [ -e "/dev/disk/by-id/$entry" ]; then + path=$(readlink -f "/dev/disk/by-id/$entry") + fi + elif [[ "$entry" == /dev/* ]]; then + path="$entry" + fi + + if [ -n "$path" ]; then + base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null) + if [ -n "$base_disk" ]; then + ZFS_DISKS+="/dev/$base_disk"$'\n' + fi + fi +done + + +ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u) + +is_disk_in_use() { + local disk="$1" + + while read -r part fstype; do + case "$fstype" in + zfs_member|linux_raid_member) + return 0 ;; + esac + + if echo "$MOUNTED_DISKS" | grep -q "/dev/$part"; then + return 0 + fi + done < <(lsblk -ln -o NAME,FSTYPE "$disk" | tail -n +2) + + if echo "$USED_DISKS" | grep -q "$disk" || echo "$ZFS_DISKS" | grep -q "$disk"; then + return 0 + fi + + return 1 +} + +FREE_DISKS=() + +LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u) +RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u) + +while read -r DISK; do + [[ "$DISK" =~ /dev/zd ]] && continue + + INFO=($(get_disk_info "$DISK")) + MODEL="${INFO[@]::${#INFO[@]}-1}" + SIZE="${INFO[-1]}" + LABEL="" + SHOW_DISK=true + + IS_MOUNTED=false + IS_RAID=false + IS_ZFS=false + IS_LVM=false + + while read -r part fstype; do + [[ "$fstype" == "zfs_member" ]] && IS_ZFS=true + [[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true + [[ "$fstype" == "LVM2_member" ]] && IS_LVM=true + if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then + IS_MOUNTED=true + fi + done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2) + + REAL_PATH=$(readlink -f "$DISK") + if echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then + IS_MOUNTED=true + fi + + + USED_BY="" + REAL_PATH=$(readlink -f "$DISK") + CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null) + + if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then + USED_BY="⚠ $(translate "In use")" + else + for SYMLINK in /dev/disk/by-id/*; do + if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then + if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then + USED_BY="⚠ $(translate "In use")" + break + fi + fi + done + fi + + + + if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then + if grep -q "active raid" /proc/mdstat; then + SHOW_DISK=false + fi + fi + + if $IS_ZFS; then + SHOW_DISK=false + fi + + if $IS_MOUNTED; then + SHOW_DISK=false + fi + + if pct config "$CTID" | grep -vE '^\s*#|^description:' | grep -q "$DISK"; then + SHOW_DISK=false + fi + + if $SHOW_DISK; then + [[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]" + [[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID" + [[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM" + [[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS" + + DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL") + FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF") + fi +done < <(lsblk -dn -e 7,11 -o PATH) + +if [ "${#FREE_DISKS[@]}" -eq 0 ]; then + cleanup + whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks available for this CT.")" 8 40 + clear + exit 1 +fi + +msg_ok "$(translate "Available disks detected.")" + + + + + +###################################################### + + + + + +MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1) +TOTAL_WIDTH=$((MAX_WIDTH + 20)) + +if [ $TOTAL_WIDTH -lt 50 ]; then + TOTAL_WIDTH=50 +fi + +SELECTED=$(whiptail --title "$(translate "Select Disks")" --radiolist \ + "$(translate "Select the disks you want to add:")" 20 $TOTAL_WIDTH 10 "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3) + +if [ -z "$SELECTED" ]; then + whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks were selected.")" 10 64 + clear + exit 1 +fi + +msg_ok "$(translate "Disks selected successfully.")" + +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") + + ASSIGNED_TO="" + RUNNING_CTS="" + RUNNING_VMS="" + + # Comprobar CTs + while read -r CT_ID CT_NAME; do + if [[ "$CT_ID" =~ ^[0-9]+$ ]] && pct config "$CT_ID" | grep -q "$DISK"; then + ASSIGNED_TO+="CT $CT_ID $CT_NAME\n" + CT_STATUS=$(pct status "$CT_ID" | awk '{print $2}') + if [ "$CT_STATUS" == "running" ]; then + RUNNING_CTS+="CT $CT_ID $CT_NAME\n" + fi + fi + done < <(pct list | awk 'NR>1 {print $1, $3}') + + # Comprobar VMs + while read -r VM_ID VM_NAME; do + if [[ "$VM_ID" =~ ^[0-9]+$ ]] && qm config "$VM_ID" | grep -q "$DISK"; then + ASSIGNED_TO+="VM $VM_ID $VM_NAME\n" + VM_STATUS=$(qm status "$VM_ID" | awk '{print $2}') + if [ "$VM_STATUS" == "running" ]; then + RUNNING_VMS+="VM $VM_ID $VM_NAME\n" + fi + fi + done < <(qm list | awk 'NR>1 {print $1, $2}') + + if [ -n "$RUNNING_CTS" ] || [ -n "$RUNNING_VMS" ]; then + ERROR_MESSAGES+="$(translate "The disk") $DISK_INFO $(translate "is in use by the following running VM(s) or CT(s):")\\n$RUNNING_CTS$RUNNING_VMS\\n\\n" + continue + fi + + if [ -n "$ASSIGNED_TO" ]; then + cleanup + whiptail --title "$(translate "Disk Already Assigned")" --yesno "$(translate "The disk") $DISK_INFO $(translate "is already assigned to the following VM(s) or CT(s):")\\n$ASSIGNED_TO\\n\\n$(translate "Do you want to continue anyway?")" 15 70 + if [ $? -ne 0 ]; then + sleep 1 + exec "$0" + fi + fi + + cleanup + + + + + if lsblk "$DISK" | grep -q "raid" || grep -q "${DISK##*/}" /proc/mdstat; then + whiptail --title "$(translate "RAID Detected")" --msgbox "$(translate "The disk") $DISK_INFO $(translate "appears to be part of a") RAID. $(translate "For security reasons, the system cannot format it.")\\n\\n$(translate "If you are sure you want to use it, please remove the") RAID metadata $(translate "or format it manually using external tools.")\\n\\n$(translate "After that, run this script again to add it.")" 18 70 + exit + fi + + + + + MOUNT_POINT=$(whiptail --title "$(translate "Mount Point")" --inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/disk_passthrough):")" 10 60 "/mnt/disk_passthrough" 3>&1 1>&2 2>&3) + + if [ -z "$MOUNT_POINT" ]; then + whiptail --title "$(translate "Error")" --msgbox "$(translate "No mount point was specified.")" 8 40 + continue + fi + + msg_ok "$(translate "Mount point specified: $MOUNT_POINT")" + + + + + + + PARTITION=$(lsblk -rno NAME "$DISK" | awk -v disk="$(basename "$DISK")" '$1 != disk {print $1; exit}') + SKIP_FORMAT=false + + if [ -n "$PARTITION" ]; then + PARTITION="/dev/$PARTITION" + CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs) + + if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then + SKIP_FORMAT=true + msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION." + else + whiptail --title "$(translate "Unsupported Filesystem")" --yesno "$(translate "The partition") $PARTITION $(translate "has an unsupported filesystem ($CURRENT_FS).\\nDo you want to format it?")" 10 70 + if [ $? -ne 0 ]; then + continue + fi + fi + else + + CURRENT_FS=$(lsblk -no FSTYPE "$DISK" | xargs) + + if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then + SKIP_FORMAT=true + PARTITION="$DISK" + msg_ok "$(translate "Detected filesystem") $CURRENT_FS $(translate "directly on disk") $DISK.)" + else + + whiptail --title "$(translate "No Valid Partitions")" --yesno "$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70 + if [ $? -ne 0 ]; then + continue + fi + + echo -e "$(translate "Creating partition table and partition...")" + parted -s "$DISK" mklabel gpt + parted -s "$DISK" mkpart primary 0% 100% + sleep 2 + partprobe "$DISK" + sleep 2 + + PARTITION=$(lsblk -rno NAME "$DISK" | awk -v disk="$(basename "$DISK")" '$1 != disk {print $1; exit}') + if [ -n "$PARTITION" ]; then + PARTITION="/dev/$PARTITION" + else + whiptail --title "$(translate "Partition Error")" --msgbox "$(translate "Failed to create partition on disk") $DISK_INFO." 8 70 + continue + fi + fi + fi + + + + + + if [ "$SKIP_FORMAT" != true ]; then + CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs) + if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then + SKIP_FORMAT=true + msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION. $(translate "Skipping format.")" + else + + FORMAT_TYPE=$(whiptail --title "$(translate "Select Format Type")" --menu "$(translate "Select the filesystem type for") $DISK_INFO:" 15 60 6 \ + "ext4" "$(translate "Extended Filesystem 4 (recommended)")" \ + "xfs" "$(translate "XFS Filesystem")" \ + "btrfs" "$(translate "Btrfs Filesystem")" 3>&1 1>&2 2>&3) + + if [ -z "$FORMAT_TYPE" ]; then + whiptail --title "$(translate "Format Cancelled")" --msgbox "$(translate "Format operation cancelled. The disk will not be added.")" 8 60 + continue + fi + + whiptail --title "$(translate "WARNING")" --yesno "$(translate "WARNING: This operation will FORMAT the disk") $DISK_INFO $(translate "with") $FORMAT_TYPE.\\n\\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\\n\\n$(translate "Are you sure you want to continue")" 15 70 + if [ $? -ne 0 ]; then + whiptail --title "$(translate "Format Cancelled")" --msgbox "$(translate "Format operation cancelled. The disk will not be added.")" 8 60 + continue + fi + fi + fi + + + + + + if [ "$SKIP_FORMAT" != true ]; then + echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..." + + case "$FORMAT_TYPE" in + "ext4") mkfs.ext4 -F "$PARTITION" ;; + "xfs") mkfs.xfs -f "$PARTITION" ;; + "btrfs") mkfs.btrfs -f "$PARTITION" ;; + esac + + if [ $? -ne 0 ]; then + whiptail --title "$(translate "Format Failed")" --msgbox "$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE.\\n\\n$(translate "The disk may be in use by the system or have hardware issues.")" 12 70 + continue + else + msg_ok "$(translate "Partition") $PARTITION $(translate "successfully formatted with") $FORMAT_TYPE." + partprobe "$DISK" + sleep 2 + fi + fi + + + + + INDEX=0 + while pct config "$CTID" | grep -q "mp${INDEX}:"; do + ((INDEX++)) + done + + + + + ############################################################################## + + RESULT=$(pct set "$CTID" -mp${INDEX} "$PARTITION,mp=$MOUNT_POINT,backup=0,ro=0,acl=1" 2>&1) + + pct exec "$CTID" -- chmod -R 775 "$MOUNT_POINT" + + ############################################################################## + + + + + if [ $? -eq 0 ]; then + MESSAGE="$(translate "The disk") $DISK_INFO $(translate "has been successfully added to CT") $CTID $(translate "as a mount point at") $MOUNT_POINT." + if [ -n "$ASSIGNED_TO" ]; then + MESSAGE+="\\n\\n$(translate "WARNING: This disk is also assigned to the following CT(s):")\\n$ASSIGNED_TO" + MESSAGE+="\\n$(translate "Make sure not to start CTs that share this disk at the same time to avoid data corruption.")" + fi + SUCCESS_MESSAGES+="$MESSAGE\\n\\n" + ((DISKS_ADDED++)) + else + ERROR_MESSAGES+="$(translate "Could not add disk") $DISK_INFO $(translate "to CT") $CTID.\\n$(translate "Error:") $RESULT\\n\\n" + fi +done + + + +msg_ok "$(translate "Disk processing completed.")" + +if [ -n "$SUCCESS_MESSAGES" ]; then + MSG_LINES=$(echo "$SUCCESS_MESSAGES" | wc -l) + whiptail --title "$(translate "Successful Operations")" --msgbox "$SUCCESS_MESSAGES" 16 70 +fi + +if [ -n "$ERROR_MESSAGES" ]; then + whiptail --title "$(translate "Warnings and Errors")" --msgbox "$ERROR_MESSAGES" 16 70 +fi + +exit 0 diff --git a/scripts/storage/import-disk-image.sh b/scripts/storage/import-disk-image.sh new file mode 100644 index 00000000..a3f4aeab --- /dev/null +++ b/scripts/storage/import-disk-image.sh @@ -0,0 +1,241 @@ +#!/bin/bash + +# ========================================================== +# ProxMenu - A menu-driven script for Proxmox VE management +# ========================================================== +# Author : MacRimi +# Copyright : (c) 2024 MacRimi +# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE) +# Version : 1.0 +# Last Updated: 28/01/2025 +# ========================================================== +# 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) 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. +# ========================================================== + +# Configuration ============================================ +REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main" +BASE_DIR="/usr/local/share/proxmenux" +UTILS_FILE="$BASE_DIR/utils.sh" +VENV_PATH="/opt/googletrans-env" + +if [[ -f "$UTILS_FILE" ]]; then + source "$UTILS_FILE" +fi +load_language +initialize_cache +# ========================================================== + +# Path where disk images are stored +IMAGES_DIR="/var/lib/vz/template/images/" + + +# Initial setup +if [ ! -d "$IMAGES_DIR" ]; then + msg_info "$(translate 'Creating images directory')" + mkdir -p "$IMAGES_DIR" + chmod 755 "$IMAGES_DIR" + msg_ok "$(translate 'Images directory created:') $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 + whiptail --title "$(translate 'No Images Found')" \ + --msgbox "$(translate 'No images available for import in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk')\n\n$(translate 'Please add some images and try again.')" 15 60 + exit 1 +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.')" 15 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')" + 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 + # msg_error "$(translate 'No VM selected')" + 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')" + +# 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 + + + +# 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 +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 + +# 4. Import each selected image +for IMAGE in $SELECTED_IMAGES; do + + # Remove quotes from selected image + IMAGE=$(echo "$IMAGE" | tr -d '"') + + # 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 [ -z "$INTERFACE" ]; then + msg_error "$(translate 'No interface type selected for') $IMAGE" + continue + fi + + FULL_PATH="$IMAGES_DIR/$IMAGE" + + # Show initial message + msg_info "$(translate 'Importing image:')" + + # 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 + else + msg_error "$(translate 'Could not find the imported disk')" + fi + else + msg_error "$(translate 'Could not import') $IMAGE" + fi +done + +msg_ok "$(translate 'All selected images have been processed')" +sleep 2