mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 00:46:21 +00:00
Update scripts
This commit is contained in:
628
scripts/utilities/export_vm_ova_ovf.sh
Executable file
628
scripts/utilities/export_vm_ova_ovf.sh
Executable file
@@ -0,0 +1,628 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenux - Export VM to OVA or OVF
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||
# Version : 1.0
|
||||
# Last Updated: 07/04/2026
|
||||
# ==========================================================
|
||||
|
||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
require_cmd() {
|
||||
local cmd="$1"
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Missing dependency")" \
|
||||
--msgbox "$(translate "Required command not found:") $cmd" 8 60
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
human_bytes() {
|
||||
local bytes="$1"
|
||||
local units=("B" "KB" "MB" "GB" "TB" "PB")
|
||||
local idx=0
|
||||
local value="$bytes"
|
||||
|
||||
[[ -z "$value" || ! "$value" =~ ^[0-9]+$ ]] && { echo "N/A"; return; }
|
||||
|
||||
while [[ "$value" -ge 1024 && "$idx" -lt 5 ]]; do
|
||||
value=$((value / 1024))
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
|
||||
echo "${value}${units[$idx]}"
|
||||
}
|
||||
|
||||
sanitize_name() {
|
||||
local raw="$1"
|
||||
local out
|
||||
out=$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-zA-Z0-9._-]/_/g' | sed 's/__*/_/g' | sed 's/^_\+//;s/_\+$//')
|
||||
[[ -z "$out" ]] && out="vm"
|
||||
echo "$out"
|
||||
}
|
||||
|
||||
xml_escape() {
|
||||
local s="$1"
|
||||
s=${s//&/&}
|
||||
s=${s//</<}
|
||||
s=${s//>/>}
|
||||
s=${s//\"/"}
|
||||
s=${s//\'/'}
|
||||
echo "$s"
|
||||
}
|
||||
|
||||
validate_destination_dir() {
|
||||
local dir="$1"
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Directory error")" \
|
||||
--msgbox "$(translate "Destination directory does not exist:")\n$dir" 8 74
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -w "$dir" ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Permission error")" \
|
||||
--msgbox "$(translate "Destination directory is not writable:")\n$dir" 8 70
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
select_vm() {
|
||||
local options=()
|
||||
local line vmid name status
|
||||
|
||||
while read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
vmid=$(echo "$line" | awk '{print $1}')
|
||||
name=$(echo "$line" | awk '{print $2}')
|
||||
status=$(echo "$line" | awk '{print $3}')
|
||||
[[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
|
||||
[[ -z "$name" ]] && name="vm-${vmid}"
|
||||
options+=("$vmid" "$name [$status]")
|
||||
done < <(qm list 2>/dev/null)
|
||||
|
||||
if [[ ${#options[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No VMs found")" \
|
||||
--msgbox "$(translate "No virtual machines were found on this host.")" 8 60
|
||||
return 1
|
||||
fi
|
||||
|
||||
VMID=$(dialog --backtitle "ProxMenux" --title "$(translate "Export VM to OVA or OVF")" \
|
||||
--menu "$(translate "Select VM to export:")" 20 80 12 \
|
||||
"${options[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
[[ -n "$VMID" ]] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_vm_stopped() {
|
||||
local status
|
||||
status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||
|
||||
if [[ "$status" == "stopped" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! dialog --backtitle "ProxMenux" --title "$(translate "VM is running")" --yesno \
|
||||
"$(translate "For a consistent export, the VM should be stopped.")\n\n$(translate "Do you want ProxMenux to stop it now?")" 10 70; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
qm shutdown "$VMID" --timeout 120 >/dev/null 2>&1 || true
|
||||
|
||||
local i
|
||||
for i in $(seq 1 60); do
|
||||
status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||
[[ "$status" == "stopped" ]] && return 0
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if dialog --backtitle "ProxMenux" --title "$(translate "Shutdown timeout")" --yesno \
|
||||
"$(translate "Graceful shutdown timed out.")\n\n$(translate "Force stop VM now?")" 10 60; then
|
||||
qm stop "$VMID" >/dev/null 2>&1 || true
|
||||
sleep 2
|
||||
status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||
[[ "$status" == "stopped" ]] && return 0
|
||||
fi
|
||||
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Cannot continue")" \
|
||||
--msgbox "$(translate "VM is still running. Export cancelled.")" 8 60
|
||||
return 1
|
||||
}
|
||||
|
||||
select_export_mode() {
|
||||
EXPORT_MODE=$(dialog --backtitle "ProxMenux" --title "$(translate "Export Format")" \
|
||||
--menu "$(translate "Select export format:")" 14 70 4 \
|
||||
"ova" "$(translate "OVA (single portable file)")" \
|
||||
"ovf" "$(translate "OVF (descriptor + VMDK files)")" \
|
||||
3>&1 1>&2 2>&3)
|
||||
[[ -n "$EXPORT_MODE" ]] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
select_destination_dir() {
|
||||
local dump_dir="/var/lib/vz/dump"
|
||||
local iso_dir="/var/lib/vz/template/iso"
|
||||
local options=(
|
||||
"1" "$dump_dir [$(translate "recommended")]"
|
||||
"2" "$iso_dir [$(translate "recommended")]"
|
||||
"M" "$(translate "Manual path entry")"
|
||||
)
|
||||
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(dialog --backtitle "ProxMenux" --title "$(translate "Destination Directory")" \
|
||||
--menu "$(translate "Select where to export VM files (OVA/OVF + temporary workspace):")" \
|
||||
16 84 8 "${options[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
[[ -n "$choice" ]] || return 1
|
||||
|
||||
case "$choice" in
|
||||
M)
|
||||
DEST_DIR=$(dialog --backtitle "ProxMenux" --title "$(translate "Manual destination path")" \
|
||||
--inputbox "$(translate "Enter destination directory for exported file(s):")" \
|
||||
10 90 "/var/lib/vz/dump" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$DEST_DIR" ]] || continue
|
||||
if [[ ! -d "$DEST_DIR" ]]; then
|
||||
if dialog --backtitle "ProxMenux" --title "$(translate "Create directory")" \
|
||||
--yesno "$(translate "The selected directory does not exist:")\n$DEST_DIR\n\n$(translate "Do you want to create it now?")" \
|
||||
11 80; then
|
||||
if ! mkdir -p "$DEST_DIR" 2>/dev/null; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Directory error")" \
|
||||
--msgbox "$(translate "Could not create destination directory:")\n$DEST_DIR" 8 74
|
||||
continue
|
||||
fi
|
||||
else
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
validate_destination_dir "$DEST_DIR" || continue
|
||||
return 0
|
||||
;;
|
||||
1)
|
||||
DEST_DIR="$dump_dir"
|
||||
validate_destination_dir "$DEST_DIR" || continue
|
||||
return 0
|
||||
;;
|
||||
2)
|
||||
DEST_DIR="$iso_dir"
|
||||
validate_destination_dir "$DEST_DIR" || continue
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
get_vm_metadata() {
|
||||
VM_CONF=$(qm config "$VMID" 2>/dev/null) || return 1
|
||||
|
||||
VM_NAME=$(echo "$VM_CONF" | awk -F': ' '/^name:/{print $2; exit}')
|
||||
[[ -z "$VM_NAME" ]] && VM_NAME="vm-${VMID}"
|
||||
|
||||
VM_MEMORY=$(echo "$VM_CONF" | awk -F': ' '/^memory:/{print $2; exit}')
|
||||
[[ -z "$VM_MEMORY" ]] && VM_MEMORY=1024
|
||||
|
||||
VM_CORES=$(echo "$VM_CONF" | awk -F': ' '/^cores:/{print $2; exit}')
|
||||
VM_SOCKETS=$(echo "$VM_CONF" | awk -F': ' '/^sockets:/{print $2; exit}')
|
||||
[[ -z "$VM_CORES" ]] && VM_CORES=1
|
||||
[[ -z "$VM_SOCKETS" ]] && VM_SOCKETS=1
|
||||
VM_VCPUS=$((VM_CORES * VM_SOCKETS))
|
||||
|
||||
VM_OSTYPE=$(echo "$VM_CONF" | awk -F': ' '/^ostype:/{print $2; exit}')
|
||||
case "$VM_OSTYPE" in
|
||||
l26|l24) VM_OS_DESC="Linux" ;;
|
||||
win11|win10|win8|win7|w2k8|w2k12|w2k16|w2k19|w2k22|wxp|w2k|w2k3)
|
||||
VM_OS_DESC="Windows"
|
||||
;;
|
||||
*) VM_OS_DESC="Other" ;;
|
||||
esac
|
||||
|
||||
NET_COUNT=$(echo "$VM_CONF" | grep -E '^net[0-9]+:' | wc -l)
|
||||
}
|
||||
|
||||
get_virtual_size_bytes() {
|
||||
local src="$1"
|
||||
local bytes=""
|
||||
|
||||
bytes=$(qemu-img info "$src" 2>/dev/null | sed -n 's/.*virtual size:.*(\([0-9]\+\) bytes).*/\1/p' | head -1)
|
||||
if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
|
||||
echo "$bytes"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -b "$src" ]]; then
|
||||
bytes=$(blockdev --getsize64 "$src" 2>/dev/null || true)
|
||||
if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
|
||||
echo "$bytes"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
bytes=$(stat -c%s "$src" 2>/dev/null || true)
|
||||
if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
|
||||
echo "$bytes"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "0"
|
||||
return 0
|
||||
}
|
||||
|
||||
collect_vm_disks() {
|
||||
DISK_COUNT=0
|
||||
unset DISK_SLOTS DISK_SRCS DISK_VSIZES
|
||||
declare -ga DISK_SLOTS DISK_SRCS DISK_VSIZES
|
||||
|
||||
local line slot value source src
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^(scsi|sata|virtio|ide)[0-9]+: ]]; then
|
||||
slot="${line%%:*}"
|
||||
value="${line#*: }"
|
||||
|
||||
[[ "$value" == *"media=cdrom"* ]] && continue
|
||||
[[ "$value" == *"cloudinit"* ]] && continue
|
||||
|
||||
source="${value%%,*}"
|
||||
[[ -z "$source" || "$source" == "none" ]] && continue
|
||||
|
||||
src=""
|
||||
if [[ "$source" == /dev/* || "$source" == /* ]]; then
|
||||
src="$source"
|
||||
elif [[ "$source" == *:* ]]; then
|
||||
src=$(pvesm path "$source" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [[ -z "$src" || ! -e "$src" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
DISK_SLOTS+=("$slot")
|
||||
DISK_SRCS+=("$src")
|
||||
DISK_VSIZES+=("$(get_virtual_size_bytes "$src")")
|
||||
DISK_COUNT=$((DISK_COUNT + 1))
|
||||
fi
|
||||
done <<< "$VM_CONF"
|
||||
|
||||
[[ "$DISK_COUNT" -gt 0 ]] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
check_destination_space() {
|
||||
local total=0
|
||||
local i
|
||||
for i in "${DISK_VSIZES[@]}"; do
|
||||
[[ "$i" =~ ^[0-9]+$ ]] && total=$((total + i))
|
||||
done
|
||||
|
||||
local factor=120
|
||||
[[ "$EXPORT_MODE" == "ova" ]] && factor=220
|
||||
REQUIRED_BYTES=$((total * factor / 100))
|
||||
|
||||
AVAILABLE_BYTES=$(df -PB1 "$DEST_DIR" | awk 'NR==2{print $4}')
|
||||
[[ "$AVAILABLE_BYTES" =~ ^[0-9]+$ ]] || AVAILABLE_BYTES=0
|
||||
|
||||
if [[ "$AVAILABLE_BYTES" -lt "$REQUIRED_BYTES" ]]; then
|
||||
if ! dialog --backtitle "ProxMenux" --title "$(translate "Low free space warning")" --yesno \
|
||||
"$(translate "Estimated required free space:") $(human_bytes "$REQUIRED_BYTES") ($REQUIRED_BYTES bytes)\n$(translate "Current free space:") $(human_bytes "$AVAILABLE_BYTES") ($AVAILABLE_BYTES bytes)\n\n$(translate "Do you want to continue anyway?")" 13 90; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
generate_ovf_descriptor() {
|
||||
local ovf_path="$1"
|
||||
local vm_name_xml os_desc_xml
|
||||
vm_name_xml=$(xml_escape "$VM_NAME")
|
||||
os_desc_xml=$(xml_escape "$VM_OS_DESC")
|
||||
|
||||
{
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
echo '<Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">'
|
||||
echo ' <References>'
|
||||
} > "$ovf_path"
|
||||
|
||||
local idx file_id disk_id file_name file_size capacity
|
||||
for idx in "${!EXPORT_DISK_FILES[@]}"; do
|
||||
file_id="file$((idx + 1))"
|
||||
file_name="${EXPORT_DISK_FILES[$idx]}"
|
||||
file_size=$(stat -c%s "$WORK_DIR/$file_name")
|
||||
echo " <File ovf:id=\"$file_id\" ovf:href=\"$file_name\" ovf:size=\"$file_size\"/>" >> "$ovf_path"
|
||||
done
|
||||
|
||||
{
|
||||
echo ' </References>'
|
||||
echo ' <DiskSection>'
|
||||
echo ' <Info>Virtual disk information</Info>'
|
||||
} >> "$ovf_path"
|
||||
|
||||
for idx in "${!EXPORT_DISK_FILES[@]}"; do
|
||||
file_id="file$((idx + 1))"
|
||||
disk_id="vmdisk$((idx + 1))"
|
||||
capacity="${DISK_VSIZES[$idx]}"
|
||||
[[ -z "$capacity" || "$capacity" -le 0 ]] && capacity=$(stat -c%s "$WORK_DIR/${EXPORT_DISK_FILES[$idx]}")
|
||||
echo " <Disk ovf:diskId=\"$disk_id\" ovf:fileRef=\"$file_id\" ovf:capacity=\"$capacity\" ovf:capacityAllocationUnits=\"byte\" ovf:format=\"http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized\"/>" >> "$ovf_path"
|
||||
done
|
||||
|
||||
{
|
||||
echo ' </DiskSection>'
|
||||
echo " <VirtualSystem ovf:id=\"$(sanitize_name "$VM_NAME")\">"
|
||||
echo ' <Info>A virtual machine</Info>'
|
||||
echo " <Name>$vm_name_xml</Name>"
|
||||
echo ' <OperatingSystemSection ovf:id="94">'
|
||||
echo ' <Info>Guest operating system</Info>'
|
||||
echo " <Description>$os_desc_xml</Description>"
|
||||
echo ' </OperatingSystemSection>'
|
||||
echo ' <VirtualHardwareSection>'
|
||||
echo ' <Info>Virtual hardware requirements</Info>'
|
||||
echo ' <System>'
|
||||
echo ' <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>'
|
||||
echo ' <vssd:InstanceID>0</vssd:InstanceID>'
|
||||
echo ' <vssd:VirtualSystemIdentifier>vm</vssd:VirtualSystemIdentifier>'
|
||||
echo ' <vssd:VirtualSystemType>vmx-14</vssd:VirtualSystemType>'
|
||||
echo ' </System>'
|
||||
echo ' <Item>'
|
||||
echo ' <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>'
|
||||
echo ' <rasd:Description>Number of Virtual CPUs</rasd:Description>'
|
||||
echo ' <rasd:ElementName>Virtual CPU(s)</rasd:ElementName>'
|
||||
echo ' <rasd:InstanceID>1</rasd:InstanceID>'
|
||||
echo " <rasd:VirtualQuantity>$VM_VCPUS</rasd:VirtualQuantity>"
|
||||
echo ' <rasd:ResourceType>3</rasd:ResourceType>'
|
||||
echo ' </Item>'
|
||||
echo ' <Item>'
|
||||
echo ' <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>'
|
||||
echo ' <rasd:Description>Memory Size</rasd:Description>'
|
||||
echo ' <rasd:ElementName>Memory</rasd:ElementName>'
|
||||
echo ' <rasd:InstanceID>2</rasd:InstanceID>'
|
||||
echo " <rasd:VirtualQuantity>$VM_MEMORY</rasd:VirtualQuantity>"
|
||||
echo ' <rasd:ResourceType>4</rasd:ResourceType>'
|
||||
echo ' </Item>'
|
||||
echo ' <Item>'
|
||||
echo ' <rasd:Address>0</rasd:Address>'
|
||||
echo ' <rasd:Description>SCSI Controller</rasd:Description>'
|
||||
echo ' <rasd:ElementName>SCSI Controller 0</rasd:ElementName>'
|
||||
echo ' <rasd:InstanceID>10</rasd:InstanceID>'
|
||||
echo ' <rasd:ResourceSubType>lsilogic</rasd:ResourceSubType>'
|
||||
echo ' <rasd:ResourceType>6</rasd:ResourceType>'
|
||||
echo ' </Item>'
|
||||
} >> "$ovf_path"
|
||||
|
||||
for idx in "${!EXPORT_DISK_FILES[@]}"; do
|
||||
disk_id="vmdisk$((idx + 1))"
|
||||
echo ' <Item>' >> "$ovf_path"
|
||||
echo " <rasd:AddressOnParent>$idx</rasd:AddressOnParent>" >> "$ovf_path"
|
||||
echo ' <rasd:Description>Hard disk</rasd:Description>' >> "$ovf_path"
|
||||
echo " <rasd:ElementName>Hard disk $((idx + 1))</rasd:ElementName>" >> "$ovf_path"
|
||||
echo " <rasd:HostResource>ovf:/disk/$disk_id</rasd:HostResource>" >> "$ovf_path"
|
||||
echo " <rasd:InstanceID>$((200 + idx + 1))</rasd:InstanceID>" >> "$ovf_path"
|
||||
echo ' <rasd:Parent>10</rasd:Parent>' >> "$ovf_path"
|
||||
echo ' <rasd:ResourceType>17</rasd:ResourceType>' >> "$ovf_path"
|
||||
echo ' </Item>' >> "$ovf_path"
|
||||
done
|
||||
|
||||
if [[ "$NET_COUNT" -gt 0 ]]; then
|
||||
local n
|
||||
for n in $(seq 1 "$NET_COUNT"); do
|
||||
{
|
||||
echo ' <Item>'
|
||||
echo ' <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>'
|
||||
echo ' <rasd:Connection>VM Network</rasd:Connection>'
|
||||
echo " <rasd:ElementName>Ethernet adapter $n</rasd:ElementName>"
|
||||
echo " <rasd:InstanceID>$((300 + n))</rasd:InstanceID>"
|
||||
echo ' <rasd:ResourceType>10</rasd:ResourceType>'
|
||||
echo ' </Item>'
|
||||
} >> "$ovf_path"
|
||||
done
|
||||
fi
|
||||
|
||||
{
|
||||
echo ' </VirtualHardwareSection>'
|
||||
echo ' </VirtualSystem>'
|
||||
echo '</Envelope>'
|
||||
} >> "$ovf_path"
|
||||
}
|
||||
|
||||
generate_manifest() {
|
||||
local mf_path="$1"
|
||||
shift
|
||||
local files=("$@")
|
||||
: > "$mf_path"
|
||||
|
||||
local f hash
|
||||
for f in "${files[@]}"; do
|
||||
hash=$(sha1sum "$WORK_DIR/$f" | awk '{print $1}')
|
||||
echo "SHA1($f)= $hash" >> "$mf_path"
|
||||
done
|
||||
}
|
||||
|
||||
print_export_result() {
|
||||
local mode="$1"
|
||||
local path="$2"
|
||||
|
||||
echo ""
|
||||
msg_title "$(translate "Export Summary")"
|
||||
|
||||
msg_ok "$(translate "VM:") ${VMID} — ${VM_NAME}"
|
||||
msg_ok "$(translate "vCPUs:") ${VM_VCPUS} $(translate "Memory:") ${VM_MEMORY} MB $(translate "Disks exported:") ${DISK_COUNT}"
|
||||
echo ""
|
||||
|
||||
if [[ "$mode" == "ova" ]]; then
|
||||
local ova_size ova_sha1
|
||||
ova_size=$(stat -c%s "$path" 2>/dev/null || echo 0)
|
||||
ova_sha1=$(sha1sum "$path" 2>/dev/null | awk '{print $1}')
|
||||
msg_ok "$(translate "Format:") OVA — $(translate "single portable archive")"
|
||||
msg_ok "$(translate "File:") $path"
|
||||
msg_ok "$(translate "Size:") $(human_bytes "$ova_size") (${ova_size} $(translate "bytes"))"
|
||||
msg_ok "SHA1: ${ova_sha1}"
|
||||
else
|
||||
local fsz total_size=0 f
|
||||
msg_ok "$(translate "Format:") OVF — $(translate "descriptor + VMDK files")"
|
||||
msg_ok "$(translate "Directory:") $path"
|
||||
for f in "${EXPORT_DISK_FILES[@]}"; do
|
||||
fsz=$(stat -c%s "$path/$f" 2>/dev/null || echo 0)
|
||||
total_size=$((total_size + fsz))
|
||||
msg_info2 " ${f} [$(human_bytes "$fsz")]"
|
||||
done
|
||||
msg_ok "$(translate "Total size:") $(human_bytes "$total_size")"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
msg_ok "$(translate "Compatible with:") VMware ESXi 6.7+ (vmx-14) · VMware Workstation / Fusion · VirtualBox · Proxmox VE"
|
||||
msg_info2 "$(translate "Not portable:") $(translate "PCI passthrough, TPM state, cloud-init configuration, Proxmox hooks")"
|
||||
echo ""
|
||||
}
|
||||
|
||||
run_export() {
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Export VM to OVA or OVF")"
|
||||
|
||||
msg_ok "$(translate "VM selected:") $VMID ($VM_NAME)"
|
||||
msg_ok "$(translate "Export mode:") ${EXPORT_MODE^^}"
|
||||
msg_ok "$(translate "Destination:") $DEST_DIR"
|
||||
|
||||
local ts vm_safe base_name
|
||||
ts=$(date +%Y%m%d_%H%M%S)
|
||||
vm_safe=$(sanitize_name "$VM_NAME")
|
||||
base_name="${vm_safe}-${VMID}-${ts}"
|
||||
|
||||
WORK_DIR=$(mktemp -d "$DEST_DIR/.ovaovf-${base_name}-XXXXXX")
|
||||
if [[ ! -d "$WORK_DIR" ]]; then
|
||||
msg_error "$(translate "Could not create temporary working directory.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
msg_ok "$(translate "Working directory:") $WORK_DIR"
|
||||
|
||||
# Clean up temp dir on unexpected exit (Ctrl+C, unhandled error, etc.)
|
||||
trap 'rm -rf "$WORK_DIR" 2>/dev/null' EXIT
|
||||
|
||||
declare -ga EXPORT_DISK_FILES
|
||||
EXPORT_DISK_FILES=()
|
||||
|
||||
local i src dst disk_name
|
||||
for i in "${!DISK_SRCS[@]}"; do
|
||||
src="${DISK_SRCS[$i]}"
|
||||
disk_name="${base_name}-disk$((i + 1)).vmdk"
|
||||
dst="$WORK_DIR/$disk_name"
|
||||
|
||||
echo ""
|
||||
msg_info "$(translate "Converting disk") $((i + 1))/$DISK_COUNT: ${DISK_SLOTS[$i]}"
|
||||
msg_info2 "$(translate "Source:") $src"
|
||||
|
||||
if ! qemu-img convert -p -O vmdk -o subformat=streamOptimized "$src" "$dst"; then
|
||||
msg_error "$(translate "Disk conversion failed for") ${DISK_SLOTS[$i]}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
EXPORT_DISK_FILES+=("$disk_name")
|
||||
msg_ok "$(translate "Converted:") $disk_name"
|
||||
done
|
||||
|
||||
local ovf_file mf_file
|
||||
ovf_file="${base_name}.ovf"
|
||||
mf_file="${base_name}.mf"
|
||||
|
||||
msg_info "$(translate "Generating OVF descriptor...")"
|
||||
generate_ovf_descriptor "$WORK_DIR/$ovf_file"
|
||||
|
||||
msg_info "$(translate "Generating manifest...")"
|
||||
generate_manifest "$WORK_DIR/$mf_file" "$ovf_file" "${EXPORT_DISK_FILES[@]}"
|
||||
|
||||
if [[ "$EXPORT_MODE" == "ovf" ]]; then
|
||||
local final_dir="$DEST_DIR/${base_name}-ovf"
|
||||
rm -rf "$final_dir"
|
||||
trap - EXIT
|
||||
mv "$WORK_DIR" "$final_dir"
|
||||
|
||||
print_export_result "ovf" "$final_dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local ova_path="$DEST_DIR/${base_name}.ova"
|
||||
msg_info "$(translate "Packaging OVA file...")"
|
||||
|
||||
if ! tar -C "$WORK_DIR" -cf "$ova_path" "$ovf_file" "$mf_file" "${EXPORT_DISK_FILES[@]}"; then
|
||||
msg_error "$(translate "Failed to create OVA archive.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
trap - EXIT
|
||||
rm -rf "$WORK_DIR"
|
||||
|
||||
print_export_result "ova" "$ova_path"
|
||||
return 0
|
||||
}
|
||||
|
||||
main() {
|
||||
require_cmd dialog || exit 1
|
||||
require_cmd qm || exit 1
|
||||
require_cmd pvesm || exit 1
|
||||
require_cmd qemu-img || exit 1
|
||||
require_cmd tar || exit 1
|
||||
require_cmd sha1sum || exit 1
|
||||
|
||||
if ! command -v pveversion >/dev/null 2>&1; then
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
|
||||
--msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
|
||||
exit 1
|
||||
fi
|
||||
|
||||
select_vm || exit 0
|
||||
ensure_vm_stopped || exit 0
|
||||
select_export_mode || exit 0
|
||||
select_destination_dir || exit 0
|
||||
|
||||
get_vm_metadata || {
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
|
||||
--msgbox "$(translate "Could not read VM configuration.")" 8 60
|
||||
exit 1
|
||||
}
|
||||
|
||||
collect_vm_disks || {
|
||||
dialog --backtitle "ProxMenux" --title "$(translate "No exportable disks")" \
|
||||
--msgbox "$(translate "No exportable VM disks were found (CD-ROM/cloud-init are excluded).")" 9 80
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_destination_space || exit 0
|
||||
|
||||
if ! dialog --backtitle "ProxMenux" --title "$(translate "Confirm export")" --yesno \
|
||||
"$(translate "VM:") $VMID ($VM_NAME)\n$(translate "Disks to export:") $DISK_COUNT\n$(translate "Format:") ${EXPORT_MODE^^}\n$(translate "Destination:") $DEST_DIR\n\n$(translate "Continue?")" 13 80; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if run_export; then
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to return...")"
|
||||
read -r
|
||||
exit 0
|
||||
else
|
||||
echo ""
|
||||
msg_error "$(translate "Export failed.")"
|
||||
msg_info2 "$(translate "Temporary working directory (if present):") $WORK_DIR"
|
||||
msg_success "$(translate "Press Enter to return...")"
|
||||
read -r
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
612
scripts/utilities/import_vm_ova_ovf.sh
Executable file
612
scripts/utilities/import_vm_ova_ovf.sh
Executable file
@@ -0,0 +1,612 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenux - Import VM from OVA or OVF
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||
# Version : 1.0
|
||||
# Last Updated: 10/04/2026
|
||||
# ==========================================================
|
||||
# Description:
|
||||
# Imports a virtual machine from an OVA or OVF package into Proxmox VE.
|
||||
# Compatible with exports from VMware ESXi, VMware Workstation/Fusion,
|
||||
# VirtualBox, and Proxmox itself (via export_vm_ova_ovf).
|
||||
#
|
||||
# What is imported:
|
||||
# - Disk images (VMDK converted to the target storage format)
|
||||
# - CPU and memory settings
|
||||
# - Number of network interfaces
|
||||
# - VM name and OS type hint
|
||||
#
|
||||
# What requires manual review after import:
|
||||
# - Network bridge assignment (vmbr0 assigned by default)
|
||||
# - NIC model (e1000 by default — change to VirtIO if guest supports it)
|
||||
# - Firmware (BIOS/UEFI — must match what the original VM used)
|
||||
# - VirtIO/qemu-guest-agent installation inside the guest (especially from ESXi)
|
||||
# - PCI passthrough, TPM, cloud-init, snapshots — not portable in OVF/OVA
|
||||
# ==========================================================
|
||||
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
|
||||
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
BACKTITLE="ProxMenux"
|
||||
UI_MENU_H=20
|
||||
UI_MENU_W=84
|
||||
UI_MENU_LIST_H=10
|
||||
|
||||
# Globals populated during the flow
|
||||
SOURCE_FILE=""
|
||||
OVF_FILE=""
|
||||
OVF_DIR=""
|
||||
WORK_DIR=""
|
||||
|
||||
OVF_VM_NAME=""
|
||||
OVF_VCPUS=1
|
||||
OVF_MEMORY_MB=1024
|
||||
OVF_DISK_FILES=()
|
||||
OVF_DISK_CAPACITIES=()
|
||||
OVF_NET_COUNT=0
|
||||
OVF_OS_TYPE="other"
|
||||
|
||||
NEW_VMID=""
|
||||
NEW_VM_NAME=""
|
||||
STORAGE=""
|
||||
BRIDGE="vmbr0"
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# HELPERS
|
||||
# -------------------------------------------------------
|
||||
|
||||
human_bytes() {
|
||||
local bytes="$1"
|
||||
local units=("B" "KB" "MB" "GB" "TB")
|
||||
local idx=0 value="$bytes"
|
||||
[[ -z "$value" || ! "$value" =~ ^[0-9]+$ ]] && { echo "N/A"; return; }
|
||||
while [[ "$value" -ge 1024 && "$idx" -lt 4 ]]; do
|
||||
value=$((value / 1024))
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
echo "${value}${units[$idx]}"
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# SELECT SOURCE FILE
|
||||
# -------------------------------------------------------
|
||||
|
||||
select_source_file() {
|
||||
local dump_dir="/var/lib/vz/dump"
|
||||
local iso_dir="/var/lib/vz/template/iso"
|
||||
local options=(
|
||||
"1" "$dump_dir"
|
||||
"2" "$iso_dir"
|
||||
"M" "$(translate "Manual path entry")"
|
||||
)
|
||||
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Import VM from OVA or OVF")" \
|
||||
--menu "$(translate "Where is the OVA/OVF file located?")" \
|
||||
14 82 4 "${options[@]}" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$choice" ]] || return 1
|
||||
|
||||
local search_dir=""
|
||||
case "$choice" in
|
||||
1) search_dir="$dump_dir" ;;
|
||||
2) search_dir="$iso_dir" ;;
|
||||
M)
|
||||
search_dir=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Custom Path")" \
|
||||
--inputbox "\n$(translate "Enter directory containing OVA/OVF files:")" \
|
||||
10 82 "/var/lib/vz/dump" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$search_dir" ]] || continue
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -d "$search_dir" ]]; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "Not found")" \
|
||||
--msgbox "$(translate "Directory does not exist:")\n$search_dir" 8 74
|
||||
continue
|
||||
fi
|
||||
|
||||
local file_opts=()
|
||||
while IFS= read -r f; do
|
||||
local fname size_h
|
||||
fname=$(basename "$f")
|
||||
size_h=$(du -sh "$f" 2>/dev/null | awk '{print $1}')
|
||||
file_opts+=("$f" "$fname [$size_h]")
|
||||
done < <(find "$search_dir" -maxdepth 2 \( -name "*.ova" -o -name "*.ovf" \) 2>/dev/null | sort)
|
||||
|
||||
if [[ ${#file_opts[@]} -eq 0 ]]; then
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "No files found")" \
|
||||
--msgbox "$(translate "No .ova or .ovf files found in:")\n\n$search_dir" 10 74
|
||||
continue
|
||||
fi
|
||||
|
||||
local selected
|
||||
selected=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Select OVA/OVF file")" \
|
||||
--menu "$(translate "Select the file to import:")" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"${file_opts[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
[[ -n "$selected" ]] || continue
|
||||
|
||||
SOURCE_FILE="$selected"
|
||||
return 0
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# EXTRACT OVA / LOCATE OVF
|
||||
# -------------------------------------------------------
|
||||
|
||||
prepare_ovf() {
|
||||
local src="$SOURCE_FILE"
|
||||
local ext="${src##*.}"
|
||||
ext="${ext,,}"
|
||||
|
||||
if [[ "$ext" == "ova" ]]; then
|
||||
WORK_DIR=$(mktemp -d "/tmp/.proxmenux-import-XXXXXX")
|
||||
trap 'rm -rf "$WORK_DIR" 2>/dev/null' EXIT
|
||||
|
||||
msg_info "$(translate "Extracting OVA archive...")"
|
||||
if ! tar xf "$src" -C "$WORK_DIR" 2>/dev/null; then
|
||||
msg_error "$(translate "Failed to extract OVA file:") $src"
|
||||
return 1
|
||||
fi
|
||||
msg_ok "$(translate "Archive extracted.")"
|
||||
|
||||
OVF_FILE=$(find "$WORK_DIR" -maxdepth 2 -name "*.ovf" | head -1)
|
||||
if [[ -z "$OVF_FILE" ]]; then
|
||||
msg_error "$(translate "No .ovf descriptor found inside OVA.")"
|
||||
return 1
|
||||
fi
|
||||
OVF_DIR=$(dirname "$OVF_FILE")
|
||||
|
||||
elif [[ "$ext" == "ovf" ]]; then
|
||||
OVF_FILE="$src"
|
||||
OVF_DIR=$(dirname "$src")
|
||||
WORK_DIR=""
|
||||
|
||||
else
|
||||
msg_error "$(translate "Unsupported format. Only .ova and .ovf files are supported.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# PARSE OVF XML
|
||||
# -------------------------------------------------------
|
||||
|
||||
parse_ovf() {
|
||||
local ovf_file="$1"
|
||||
|
||||
local result
|
||||
result=$(awk '
|
||||
BEGIN {
|
||||
in_item=0; rt=""; qty=""
|
||||
file_count=0; cap_count=0; net_count=0
|
||||
name=""; vcpu="1"; mem="1024"; os=""
|
||||
}
|
||||
|
||||
/<[Nn]ame>/ {
|
||||
match($0, /<[Nn]ame>([^<]+)</, a)
|
||||
if (a[1] != "" && name == "") name = a[1]
|
||||
}
|
||||
|
||||
/[Ll]inux/ && /[Dd]escription|[Oo]perating/ { if (os == "") os="linux" }
|
||||
/[Ww]indows/ && /[Dd]escription|[Oo]perating/ { if (os == "") os="windows" }
|
||||
|
||||
/ovf:href=|href=/ {
|
||||
n = split($0, parts, /"/)
|
||||
for (i=1; i<=n; i++) {
|
||||
if (parts[i] ~ /\.(vmdk|qcow2|img|raw)$/) {
|
||||
files[file_count++] = parts[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/[Cc]apacity=/ {
|
||||
match($0, /[Cc]apacity="([0-9]+)"/, a)
|
||||
if (a[1]+0 > 0) caps[cap_count++] = a[1]
|
||||
}
|
||||
|
||||
/<Item>|<Item / { in_item=1; rt=""; qty="" }
|
||||
/<\/Item>/ {
|
||||
if (in_item) {
|
||||
if (rt=="3" && qty ~ /^[0-9]+$/) vcpu=qty
|
||||
if (rt=="4" && qty ~ /^[0-9]+$/) mem=qty
|
||||
if (rt=="10") net_count++
|
||||
}
|
||||
in_item=0
|
||||
}
|
||||
/ResourceType>/ {
|
||||
match($0, /ResourceType>([0-9]+)</, a); rt=a[1]
|
||||
}
|
||||
/VirtualQuantity>/ {
|
||||
match($0, /VirtualQuantity>([0-9]+)</, a); qty=a[1]
|
||||
}
|
||||
|
||||
END {
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
|
||||
if (name == "") name = "imported-vm"
|
||||
print "NAME=" name
|
||||
print "VCPU=" vcpu
|
||||
print "MEM=" mem
|
||||
print "NET=" net_count
|
||||
print "OS=" os
|
||||
for (i=0; i<file_count; i++) print "FILE=" files[i]
|
||||
for (i=0; i<cap_count; i++) print "CAP=" caps[i]
|
||||
}
|
||||
' "$ovf_file")
|
||||
|
||||
OVF_VM_NAME=$(echo "$result" | grep '^NAME=' | cut -d= -f2-)
|
||||
OVF_VCPUS=$(echo "$result" | grep '^VCPU=' | cut -d= -f2-)
|
||||
OVF_MEMORY_MB=$(echo "$result" | grep '^MEM=' | cut -d= -f2-)
|
||||
OVF_NET_COUNT=$(echo "$result" | grep '^NET=' | cut -d= -f2-)
|
||||
OVF_OS_TYPE=$(echo "$result" | grep '^OS=' | cut -d= -f2-)
|
||||
|
||||
OVF_DISK_FILES=()
|
||||
while IFS= read -r line; do
|
||||
OVF_DISK_FILES+=("${line#FILE=}")
|
||||
done < <(echo "$result" | grep '^FILE=')
|
||||
|
||||
OVF_DISK_CAPACITIES=()
|
||||
while IFS= read -r line; do
|
||||
OVF_DISK_CAPACITIES+=("${line#CAP=}")
|
||||
done < <(echo "$result" | grep '^CAP=')
|
||||
|
||||
[[ -z "$OVF_VM_NAME" ]] && OVF_VM_NAME="imported-vm"
|
||||
[[ ! "$OVF_VCPUS" =~ ^[0-9]+$ ]] && OVF_VCPUS=1
|
||||
[[ ! "$OVF_MEMORY_MB" =~ ^[0-9]+$ ]] && OVF_MEMORY_MB=1024
|
||||
[[ ! "$OVF_NET_COUNT" =~ ^[0-9]+$ ]] && OVF_NET_COUNT=0
|
||||
|
||||
case "$OVF_OS_TYPE" in
|
||||
linux) OVF_OS_TYPE="l26" ;;
|
||||
windows) OVF_OS_TYPE="win10" ;;
|
||||
*) OVF_OS_TYPE="other" ;;
|
||||
esac
|
||||
|
||||
[[ ${#OVF_DISK_FILES[@]} -gt 0 ]] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# SELECT IMPORT OPTIONS (dialogs — no terminal output)
|
||||
# -------------------------------------------------------
|
||||
|
||||
select_import_options() {
|
||||
# VMID
|
||||
local suggested_vmid
|
||||
suggested_vmid=$(pvesh get /cluster/nextid 2>/dev/null || echo "100")
|
||||
|
||||
while true; do
|
||||
NEW_VMID=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "VM ID")" \
|
||||
--inputbox "\n$(translate "Enter the VMID for the new VM:") ($(translate "suggested:") $suggested_vmid)" \
|
||||
10 72 "$suggested_vmid" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$NEW_VMID" ]] || return 1
|
||||
|
||||
if ! [[ "$NEW_VMID" =~ ^[0-9]+$ ]]; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "Invalid VMID")" \
|
||||
--msgbox "$(translate "VMID must be a number.")" 8 50
|
||||
continue
|
||||
fi
|
||||
|
||||
if qm status "$NEW_VMID" &>/dev/null; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "VMID in use")" \
|
||||
--msgbox "$(translate "VMID $NEW_VMID is already in use. Please choose another.")" 8 60
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
# VM Name
|
||||
NEW_VM_NAME=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "VM Name")" \
|
||||
--inputbox "\n$(translate "Enter name for the imported VM:")" \
|
||||
10 72 "$OVF_VM_NAME" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$NEW_VM_NAME" ]] || return 1
|
||||
|
||||
# Storage
|
||||
local storage_list storage_opts=()
|
||||
storage_list=$(pvesm status -content images 2>/dev/null | awk 'NR>1 {print $1}')
|
||||
if [[ -z "$storage_list" ]]; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "No storage")" \
|
||||
--msgbox "$(translate "No storage volumes available for VM images.")" 8 60
|
||||
return 1
|
||||
fi
|
||||
while IFS= read -r s; do
|
||||
storage_opts+=("$s" "")
|
||||
done <<< "$storage_list"
|
||||
|
||||
STORAGE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Select Storage")" \
|
||||
--menu "$(translate "Select storage for imported disk(s):")" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"${storage_opts[@]}" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$STORAGE" ]] || return 1
|
||||
|
||||
# Network bridge
|
||||
local bridge_opts=()
|
||||
while IFS= read -r br; do
|
||||
[[ -n "$br" ]] && bridge_opts+=("$br" "")
|
||||
done < <(ip link show type bridge 2>/dev/null | awk -F': ' '/^[0-9]+:/{print $2}' | sed 's/@.*//')
|
||||
|
||||
if [[ ${#bridge_opts[@]} -gt 1 ]]; then
|
||||
BRIDGE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Network Bridge")" \
|
||||
--menu "$(translate "Select bridge for network interface(s):")" \
|
||||
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||
"${bridge_opts[@]}" 3>&1 1>&2 2>&3)
|
||||
[[ -n "$BRIDGE" ]] || return 1
|
||||
elif [[ ${#bridge_opts[@]} -eq 1 ]]; then
|
||||
BRIDGE="${bridge_opts[0]}"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# CONFIRM BEFORE IMPORT (dialog)
|
||||
# -------------------------------------------------------
|
||||
|
||||
confirm_import() {
|
||||
local disk_count="${#OVF_DISK_FILES[@]}"
|
||||
local disk_info="" i
|
||||
|
||||
for i in "${!OVF_DISK_FILES[@]}"; do
|
||||
local cap="${OVF_DISK_CAPACITIES[$i]:-0}"
|
||||
disk_info+="\n disk$((i+1)): ${OVF_DISK_FILES[$i]} ($(human_bytes "$cap"))"
|
||||
done
|
||||
|
||||
local msg
|
||||
msg="$(translate "New VM:") $NEW_VMID ($NEW_VM_NAME)\n"
|
||||
msg+="$(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB $(translate "OS type:") $OVF_OS_TYPE\n"
|
||||
msg+="$(translate "NICs:") $OVF_NET_COUNT $(translate "Bridge:") $BRIDGE\n"
|
||||
msg+="$(translate "Storage:") $STORAGE\n"
|
||||
msg+="$(translate "Disks to import:") $disk_count${disk_info}\n\n"
|
||||
msg+="$(translate "Continue?")"
|
||||
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title "$(translate "Confirm Import")" \
|
||||
--yesno "$msg" 18 84 3>&1 1>&2 2>&3
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# RUN IMPORT (terminal output only — no dialogs)
|
||||
# -------------------------------------------------------
|
||||
|
||||
run_import() {
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Import VM from OVA or OVF")"
|
||||
|
||||
msg_ok "$(translate "VM:") $NEW_VMID ($NEW_VM_NAME)"
|
||||
msg_ok "$(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB $(translate "OS:") $OVF_OS_TYPE"
|
||||
msg_ok "$(translate "Storage:") $STORAGE $(translate "Bridge:") $BRIDGE $(translate "NICs:") $OVF_NET_COUNT"
|
||||
echo ""
|
||||
|
||||
# 1. Create VM shell
|
||||
msg_info "$(translate "Creating VM...")"
|
||||
if ! qm create "$NEW_VMID" \
|
||||
--name "$NEW_VM_NAME" \
|
||||
--memory "$OVF_MEMORY_MB" \
|
||||
--cores "$OVF_VCPUS" \
|
||||
--ostype "$OVF_OS_TYPE" \
|
||||
--scsihw lsi \
|
||||
--net0 "e1000,bridge=$BRIDGE" \
|
||||
&>/dev/null; then
|
||||
msg_error "$(translate "Failed to create VM") $NEW_VMID"
|
||||
return 1
|
||||
fi
|
||||
msg_ok "$(translate "VM shell created:") $NEW_VMID"
|
||||
|
||||
# Add extra NICs (net0 already created above)
|
||||
local n
|
||||
for n in $(seq 1 $((OVF_NET_COUNT - 1))); do
|
||||
qm set "$NEW_VMID" "--net${n}" "e1000,bridge=$BRIDGE" &>/dev/null || true
|
||||
done
|
||||
[[ "$OVF_NET_COUNT" -gt 1 ]] && msg_ok "$(translate "Network interfaces added:") $OVF_NET_COUNT"
|
||||
|
||||
# 2. Import disks
|
||||
local disk_count="${#OVF_DISK_FILES[@]}"
|
||||
local i disk_file src_path
|
||||
local TEMP_STATUS_FILE TEMP_DISK_FILE
|
||||
|
||||
for i in "${!OVF_DISK_FILES[@]}"; do
|
||||
disk_file="${OVF_DISK_FILES[$i]}"
|
||||
src_path="$OVF_DIR/$disk_file"
|
||||
|
||||
if [[ ! -f "$src_path" ]]; then
|
||||
msg_error "$(translate "Disk file not found:") $src_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
msg_info "$(translate "Importing disk") $((i + 1))/$disk_count: $disk_file"
|
||||
msg_info2 "$(translate "Source:") $src_path"
|
||||
|
||||
TEMP_STATUS_FILE=$(mktemp)
|
||||
TEMP_DISK_FILE=$(mktemp)
|
||||
|
||||
(
|
||||
qm importdisk "$NEW_VMID" "$src_path" "$STORAGE" 2>&1
|
||||
echo $? > "$TEMP_STATUS_FILE"
|
||||
) | while IFS= read -r line; do
|
||||
if [[ "$line" =~ transferred ]]; then
|
||||
local pct
|
||||
pct=$(echo "$line" | grep -oP "\d+\.\d+(?=%)")
|
||||
[[ -n "$pct" ]] && echo -ne "\r${TAB}${BL}- $(translate "Importing:") $disk_file -${CL} ${pct}%"
|
||||
elif [[ "$line" =~ successfully\ imported\ disk ]]; then
|
||||
echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE"
|
||||
fi
|
||||
done
|
||||
echo -ne "\n"
|
||||
|
||||
local import_status
|
||||
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 "Import failed for:") $disk_file"
|
||||
rm -f "$TEMP_DISK_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Locate the unused disk entry in VM config
|
||||
local unused_id unused_disk
|
||||
unused_id=$(qm config "$NEW_VMID" | grep -E '^unused[0-9]+:' | tail -1 | cut -d: -f1)
|
||||
unused_disk=$(qm config "$NEW_VMID" | grep -E '^unused[0-9]+:' | tail -1 | cut -d: -f2- | xargs)
|
||||
rm -f "$TEMP_DISK_FILE"
|
||||
|
||||
if [[ -z "$unused_disk" ]]; then
|
||||
msg_error "$(translate "Could not locate imported disk in VM config.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Attach to scsi slot i
|
||||
if ! qm set "$NEW_VMID" "--scsi${i}" "$unused_disk" &>/dev/null; then
|
||||
msg_error "$(translate "Failed to attach disk as scsi$i.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Remove the unused marker
|
||||
[[ -n "$unused_id" ]] && qm set "$NEW_VMID" --delete "$unused_id" &>/dev/null || true
|
||||
|
||||
msg_ok "$(translate "Disk attached as:") scsi${i} (${disk_file})"
|
||||
done
|
||||
|
||||
# 3. Set boot disk
|
||||
echo ""
|
||||
msg_info "$(translate "Configuring boot order...")"
|
||||
if qm set "$NEW_VMID" --boot c --bootdisk "scsi0" &>/dev/null; then
|
||||
msg_ok "$(translate "Boot disk:") scsi0"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# PRINT FINAL RESULT
|
||||
# -------------------------------------------------------
|
||||
|
||||
print_import_result() {
|
||||
local disk_count="${#OVF_DISK_FILES[@]}"
|
||||
|
||||
echo ""
|
||||
msg_title "$(translate "Import Summary")"
|
||||
|
||||
msg_ok "$(translate "VM imported successfully")"
|
||||
msg_ok "$(translate "VM ID:") $NEW_VMID $(translate "Name:") $NEW_VM_NAME"
|
||||
msg_ok "$(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB $(translate "Disks:") $disk_count"
|
||||
msg_ok "$(translate "Storage:") $STORAGE $(translate "Bridge:") $BRIDGE $(translate "NICs:") $OVF_NET_COUNT"
|
||||
echo ""
|
||||
|
||||
msg_ok "$(translate "To start the VM:") qm start $NEW_VMID"
|
||||
echo ""
|
||||
|
||||
msg_title "$(translate "Manual steps recommended after import")"
|
||||
msg_info2 "$(translate "Network :") $(translate "Verify bridge assignment and NIC model — change to VirtIO if guest drivers are available")"
|
||||
msg_info2 "$(translate "Firmware :") $(translate "Check BIOS/UEFI in Hardware > BIOS — must match what the original VM used")"
|
||||
msg_info2 "$(translate "Drivers :") $(translate "If imported from ESXi: install qemu-guest-agent inside the guest OS")"
|
||||
msg_info2 "$(translate "Display :") $(translate "Set Display > Graphic card (VGA, SPICE or VirtIO) to match the guest")"
|
||||
msg_info2 "$(translate "OS type :") $(translate "Verify Options > OS Type — currently set to:") $OVF_OS_TYPE"
|
||||
echo ""
|
||||
msg_info2 "$(translate "Not imported:") $(translate "PCI passthrough, TPM state, cloud-init, snapshots, Proxmox-specific hooks")"
|
||||
echo ""
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------
|
||||
# MAIN
|
||||
# -------------------------------------------------------
|
||||
|
||||
main() {
|
||||
if ! command -v pveversion >/dev/null 2>&1; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "Error")" \
|
||||
--msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in dialog qm pvesm qemu-img tar; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
dialog --backtitle "$BACKTITLE" --title "$(translate "Missing dependency")" \
|
||||
--msgbox "$(translate "Required command not found:") $cmd" 8 60
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Step 1: pick the OVA/OVF file (dialog)
|
||||
select_source_file || exit 0
|
||||
|
||||
# Step 2: extract + parse (terminal output)
|
||||
show_proxmenux_logo
|
||||
msg_title "$(translate "Import VM from OVA or OVF")"
|
||||
|
||||
msg_ok "$(translate "Source:") $SOURCE_FILE"
|
||||
echo ""
|
||||
|
||||
prepare_ovf || {
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to return...")"
|
||||
read -r
|
||||
exit 1
|
||||
}
|
||||
|
||||
msg_info "$(translate "Parsing OVF descriptor...")"
|
||||
if ! parse_ovf "$OVF_FILE"; then
|
||||
msg_error "$(translate "Could not parse OVF file, or no disk image references found.")"
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to return...")"
|
||||
read -r
|
||||
exit 1
|
||||
fi
|
||||
msg_ok "$(translate "OVF parsed:")"
|
||||
msg_info2 " $(translate "Name:") $OVF_VM_NAME $(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB"
|
||||
msg_info2 " $(translate "Disks:") ${#OVF_DISK_FILES[@]} $(translate "NICs:") $OVF_NET_COUNT $(translate "OS hint:") $OVF_OS_TYPE"
|
||||
|
||||
# Clean screen before returning to dialogs
|
||||
show_proxmenux_logo
|
||||
|
||||
# Step 3: configure the new VM (dialogs)
|
||||
select_import_options || exit 0
|
||||
|
||||
# Step 4: confirm (dialog)
|
||||
confirm_import || exit 0
|
||||
|
||||
# Step 5: do the import (terminal output only)
|
||||
if run_import; then
|
||||
print_import_result
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
exit 0
|
||||
else
|
||||
echo ""
|
||||
msg_error "$(translate "Import failed. VM $NEW_VMID may be in partial state.")"
|
||||
msg_info2 "$(translate "To remove partial VM:") qm destroy $NEW_VMID --destroy-unreferenced-disks 1"
|
||||
echo ""
|
||||
msg_success "$(translate "Press Enter to return...")"
|
||||
read -r
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user