How to Manually Convert an ESXi Legacy VM into an Hyperion VM

Written By Sebastian Sime (Super Administrator)

Updated at October 22nd, 2026

→ Applies to: Hyperion 9.x and above

PREREQUISITES
- Make sure the virtual machine is properly shut down before starting the conversion process.

IMPORTANT
Make sure the VM in ESXi is not configured with “Enable UEFI Secure Boot”.
Go to [ Advanced → Boot Options → Enable UEFI Secure Boot ] and make sure the option is unchecked.

 

Step 1. Connect to Hyperion appliance via SSH as admin

ssh admin@<your_ip_address_or_hostname>

 

Step 2. Become Super User

sudo su

 

Step 3. Install libguestfs tools (virt-win-reg)

dnf install -y libguestfs-tools libguestfs-winsupport

 

Step 4. Edit /tmp/convert-VM file

vi /tmp/convert-VM

IMPORTANT
Make sure to copy and paste the exact lines below.

#!/bin/bash
set -euo pipefail

# ----------------------------------------------------------------
# Global Configuration (Dynamic Detection)
# ----------------------------------------------------------------
PRIMARY_POOL=$(zfs list -H -o name | head -n1 | cut -d'/' -f1)

DEFAULT_VIRTIO_ISO="/${PRIMARY_POOL}/isos/virtio-win.iso"
DEFAULT_BROWSE_ROOT="/${PRIMARY_POOL}"
DEFAULT_NETWORK="VM Network"
MACHINE_Q35="pc-q35-rhel9.4.0"
# i440fx (PIIX3 + legacy IDE) — required for guests with no AHCI boot driver (Windows XP).
# q35 has no IDE controller, so XP cannot reach its boot disk there -> STOP 0x7B.
MACHINE_I440FX="pc-i440fx-rhel7.6.0"

# Global variables
VM_DIR="" VM_NAME="" VMDK_PATH=""
VMDK_LIST=()
EXTRA_VMDK_LIST=()
EXTRA_IMG_LIST=()
VMX_FILE=""
OS_TYPE="" OS_SUBTYPE=""
MEM_MB="4096" MEM_KIB="" VCPUS="2"
GUEST_OS=""
MACHINE_TYPE="" DISK_BUS="" DISK_DEV=""
CDROM_BUS="sata"
USB_MODEL="qemu-xhci" NIC_MODEL="virtio" WATCHDOG_MODEL="itco"
CLOCK_OFFSET="" VIDEO_MODEL="vga"
USE_UEFI=false
USE_TPM=false
VIRTIO_ISO=""
NET_NAME="$DEFAULT_NETWORK"
NEW_UUID="" NEW_DATASET="" NEW_MOUNTPOINT=""
OUT_IMG="" XML_PATH="" VM_UUID=""

# ----------------------------------------------------------------
# TUI — Detect available backend
# ----------------------------------------------------------------
TUI="plain"
command -v whiptail &>/dev/null && TUI="whiptail"
[[ "$TUI" == "plain" ]] && command -v dialog &>/dev/null && TUI="dialog"

_DLG_H=$(( $(tput lines 2>/dev/null || echo 25) - 3 ))
_DLG_W=$(( $(tput cols  2>/dev/null || echo 80) - 4 ))
(( _DLG_H < 16 )) && _DLG_H=16
(( _DLG_W < 60 )) && _DLG_W=60
(( _DLG_H > 24 )) && _DLG_H=24
(( _DLG_W > 78 )) && _DLG_W=78

# ----------------------------------------------------------------
# TUI — Base Primitives
# ----------------------------------------------------------------

tui_msg() {
    local title="$1" text="$2"
    if [[ "$TUI" != "plain" ]]; then
        "$TUI" --title "$title" --msgbox "$text" "$_DLG_H" "$_DLG_W"
    else
        echo -e "\n╔ $title ╗\n$text\n"
        read -r -p "[ENTER to continue]"
    fi
}

tui_input() {
    local title="$1" prompt="$2" default="${3:-}"
    local result
    if [[ "$TUI" != "plain" ]]; then
        result=$("$TUI" --title "$title" --inputbox "$prompt" \
            "$_DLG_H" "$_DLG_W" "$default" 3>&1 1>&2 2>&3) || true
        echo "${result:-$default}"
    else
        echo -e "\n[$title] $prompt" >&2
        [[ -n "$default" ]] && echo "(enter = $default)" >&2
        read -r result
        echo "${result:-$default}"
    fi
}

tui_menu() {
    local title="$1" prompt="$2"; shift 2
    local -a items=("$@")
    local n=$(( ${#items[@]} / 2 ))
    local list_h=$(( _DLG_H - 8 ))
    (( list_h < 3 )) && list_h=3
    (( n < list_h )) && list_h=$n
    local result
    if [[ "$TUI" != "plain" ]]; then
        result=$("$TUI" --title "$title" --menu "$prompt" \
            "$_DLG_H" "$_DLG_W" "$list_h" "${items[@]}" 3>&1 1>&2 2>&3) || true
        echo "${result:-${items[0]}}"
    else
        echo -e "\n[$title] $prompt" >&2
        local i=0
        while [[ $i -lt ${#items[@]} ]]; do
            echo "  ${items[$i]}) ${items[$((i+1))]}" >&2
            i=$(( i + 2 ))
        done
        read -r -p "Choice [${items[0]}]: " result >&2 || true
        echo "${result:-${items[0]}}"
    fi
}

tui_yesno() {
    local title="$1" prompt="$2"
    if [[ "$TUI" != "plain" ]]; then
        "$TUI" --title "$title" --yesno "$prompt" "$_DLG_H" "$_DLG_W"
    else
        local yn
        read -r -p "[$title] $prompt [y/n]: " yn
        [[ "${yn,,}" =~ ^[sy] ]]
    fi
}

# ----------------------------------------------------------------
# TUI — Filesystem Browser
# ----------------------------------------------------------------
tui_browse() {
    local title="$1"
    local current_dir="${2:-/}"
    local filter="${3:-*}"

    current_dir="$(realpath "$current_dir" 2>/dev/null || echo "/")"
    [[ -d "$current_dir" ]] || current_dir="/"

    local list_h=$(( _DLG_H - 8 ))
    (( list_h < 4 )) && list_h=4

    while true; do
        local -a tags=() labels=() paths=()
        local idx=1

        tags+=("$idx"); labels+=("[ ✎  Type path manually ]"); paths+=("__TYPE__")
        idx=$(( idx + 1 ))

        if [[ "$current_dir" != "/" ]]; then
            local parent
            parent="$(dirname "$current_dir")"
            tags+=("$idx"); labels+=("[ ↑  .. ]   $parent"); paths+=("__UP__:$parent")
            idx=$(( idx + 1 ))
        fi

        while IFS= read -r d; do
            [[ -z "$d" ]] && continue
            tags+=("$idx")
            labels+=("[DIR]  $(basename "$d")/")
            paths+=("$d")
            idx=$(( idx + 1 ))
        done < <(find "$current_dir" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort)

        while IFS= read -r f; do
            [[ -z "$f" ]] && continue
            local fsize
            fsize="$(du -sh "$f" 2>/dev/null | cut -f1 || echo "?")"
            tags+=("$idx")
            labels+=("$(basename "$f")  [$fsize]")
            paths+=("$f")
            idx=$(( idx + 1 ))
        done < <(find "$current_dir" -maxdepth 1 -mindepth 1 \
                    -type f -name "$filter" 2>/dev/null | sort)

        local -a wt_items=()
        local i
        for (( i=0; i<${#tags[@]}; i++ )); do
            wt_items+=("${tags[$i]}" "${labels[$i]}")
        done

        local total=$(( idx - 1 ))
        local visible_h=$list_h
        (( total < visible_h )) && visible_h=$total
        (( visible_h < 1 )) && visible_h=1

        local choice=""
        if [[ "$TUI" != "plain" ]]; then
            choice=$("$TUI" \
                --title "$title" \
                --cancel-button "Cancel" \
                --menu "$current_dir" \
                "$_DLG_H" "$_DLG_W" "$visible_h" \
                "${wt_items[@]}" \
                3>&1 1>&2 2>&3) || { echo ""; return 0; }
        else
            echo -e "\n[$title]  $current_dir" >&2
            echo "──────────────────────────────────────────" >&2
            for (( i=0; i<${#tags[@]}; i++ )); do
                echo "  ${tags[$i]}) ${labels[$i]}" >&2
            done
            echo "  0) Cancel" >&2
            read -r -p "Choice: " choice || true
            if [[ "$choice" == "0" || -z "$choice" ]]; then
                echo ""; return 0
            fi
        fi

        [[ -z "$choice" ]] && { echo ""; return 0; }

        local chosen_path=""
        for (( i=0; i<${#tags[@]}; i++ )); do
            if [[ "${tags[$i]}" == "$choice" ]]; then
                chosen_path="${paths[$i]}"
                break
            fi
        done

        [[ -z "$chosen_path" ]] && continue

        case "$chosen_path" in
            __TYPE__)
                local typed
                typed=$(tui_input "$title" "Full path (file or directory):" "")
                if [[ -n "$typed" ]]; then
                    echo "$typed"
                    return 0
                fi
                ;;
            __UP__:*)
                current_dir="${chosen_path#__UP__:}"
                ;;
            *)
                if [[ -d "$chosen_path" ]]; then
                    current_dir="$chosen_path"
                else
                    echo "$chosen_path"
                    return 0
                fi
                ;;
        esac

        unset tags labels paths wt_items
        local -a tags=() labels=() paths=() wt_items=()
    done
}

pick_path() {
    local title="$1"
    local prompt="$2"
    local default="${3:-}"
    local start_dir="${4:-$DEFAULT_BROWSE_ROOT}"
    local filter="${5:-*}"

    [[ -d "$start_dir" ]] || start_dir="/"

    local method
    method=$(tui_menu "$title" "How do you want to select?" \
        "paste"  "Paste / type the path" \
        "browse" "Browse the filesystem")

    case "${method:-paste}" in
        paste)
            tui_input "$title" "$prompt" "$default"
            ;;
        browse)
            tui_browse "$title" "$start_dir" "$filter"
            ;;
    esac
}

# ----------------------------------------------------------------
# Logging
# ----------------------------------------------------------------
log()  { echo -e "\033[1;34m==> $*\033[0m"; }
warn() { echo -e "\033[1;33m[!] $*\033[0m"; }
die()  { echo -e "\033[1;31m[ERROR] $*\033[0m" >&2; exit 1; }

# ----------------------------------------------------------------
# Cleanup on error
# ----------------------------------------------------------------
_cleanup() {
    local code=$?
    [[ $code -eq 0 ]] && return
    warn "Script terminated with error (code $code)."
    if [[ -n "$NEW_DATASET" ]]; then
        warn "ZFS dataset created but migration incomplete: $NEW_DATASET"
        warn "To remove it: zfs destroy -r $NEW_DATASET"
    fi
}
trap _cleanup EXIT

# ----------------------------------------------------------------
# Utility
# ----------------------------------------------------------------
check_deps() {
    local miss=()
    for cmd in qemu-img zfs uuidgen; do
        command -v "$cmd" &>/dev/null || miss+=("$cmd")
    done
    [[ ${#miss[@]} -eq 0 ]] || die "Missing commands: ${miss[*]}"
}

sanitize_name() {
    echo "$1" | tr -cd '[:alnum:]_.-' | sed 's/^[^a-zA-Z]/_/'
}

# ================================================================
# STEP 1 — VMDK Selection / VM Directory
# ================================================================
step_input() {
    local input
    input=$(pick_path \
        "Select VM" \
        "ESXi VM DIRECTORY path or .vmdk FILE:" \
        "" \
        "$DEFAULT_BROWSE_ROOT" \
        "*.vmdk")

    [[ -n "$input" ]] || die "No path entered."

    if [[ -d "$input" ]]; then
        VM_DIR="$input"
        VM_NAME="$(sanitize_name "$(basename "$VM_DIR")")"
        mapfile -t VMDK_LIST < <(
            find "$VM_DIR" -maxdepth 1 -name "*.vmdk" \
            | grep -vE '(-flat|-delta|-[0-9]{6})\.vmdk$' \
            | sort || true
        )
        [[ ${#VMDK_LIST[@]} -gt 0 ]] \
            || die "No .vmdk descriptor found in $VM_DIR"
        VMDK_PATH="${VMDK_LIST[0]}"

    elif [[ -f "$input" && "$input" == *.vmdk ]]; then
        VMDK_PATH="$input"
        VM_DIR="$(dirname "$VMDK_PATH")"
        VM_NAME="$(sanitize_name "$(basename "$VMDK_PATH" .vmdk)")"
        VMDK_LIST=("$VMDK_PATH")
    else
        die "Enter a valid directory or an existing .vmdk file."
    fi
}

# ================================================================
# STEP 2 — Disk Selection (if multi-disk)
# ================================================================
step_select_vmdk() {
    [[ ${#VMDK_LIST[@]} -le 1 ]] && return

    local -a menu_opts=()
    local i=1
    for v in "${VMDK_LIST[@]}"; do
        menu_opts+=("$i" "$(basename "$v")")
        i=$(( i + 1 ))
    done

    local choice
    choice=$(tui_menu "Disk Selection" \
        "Found ${#VMDK_LIST[@]} disks. Select the main disk to migrate:" \
        "${menu_opts[@]}")

    [[ "$choice" =~ ^[0-9]+$ ]] && VMDK_PATH="${VMDK_LIST[$((choice-1))]}"
}

# ================================================================
# STEP 2b — Additional Disks
# ================================================================
step_add_disks() {
    EXTRA_VMDK_LIST=()

    while true; do
        local added="${#EXTRA_VMDK_LIST[@]}"
        local prompt_msg
        if [[ $added -eq 0 ]]; then
            prompt_msg="Does the VM have multiple disks?\nDo you want to add other VMDKs besides the main disk?"
        else
            local disk_list=""
            local j
            for j in "${!EXTRA_VMDK_LIST[@]}"; do
                disk_list="${disk_list}\n  Disk $((j+2)): $(basename "${EXTRA_VMDK_LIST[$j]}")"
            done
            prompt_msg="Additional disks already added: $added${disk_list}\n\nDo you want to add another VMDK disk?"
        fi

        tui_yesno "Additional Disks" "$prompt_msg" || break

        local extra_vmdk
        extra_vmdk=$(pick_path \
            "Additional Disk $((added + 2))" \
            "Additional VMDK path:" \
            "" \
            "$VM_DIR" \
            "*.vmdk")

        if [[ -f "$extra_vmdk" && "$extra_vmdk" == *.vmdk ]]; then
            EXTRA_VMDK_LIST+=("$extra_vmdk")
            tui_msg "Disk Added" "Disk $((added + 2)) added:\n$(basename "$extra_vmdk")"
        elif [[ -n "$extra_vmdk" ]]; then
            tui_msg "Error" "Invalid file or not found:\n${extra_vmdk}\n\nPlease enter an existing .vmdk file."
        fi
    done
}

# ================================================================
# STEP 3 — VMX File
# ================================================================
step_vmx() {
    VMX_FILE=""
    local found_vmx
    found_vmx="$(find "$VM_DIR" -maxdepth 1 -name "*.vmx" 2>/dev/null | head -n1 || true)"

    if [[ -n "$found_vmx" ]]; then
        local vmx_action
        vmx_action=$(tui_menu "VMX Detected" \
            "VMX file found: $(basename "$found_vmx")\n\nWhat do you want to do?" \
            "use"    "Use this VMX for auto-detection" \
            "browse" "Choose another VMX file (browse/paste)" \
            "skip"   "Ignore — configure OS / RAM / CPU manually")

        case "${vmx_action:-use}" in
            use)    VMX_FILE="$found_vmx" ;;
            browse) _pick_vmx ;;
            skip)   VMX_FILE="" ;;
        esac
    else
        if tui_yesno "VMX Not Found" \
            "No VMX in the VM directory.\nDo you want to indicate a .vmx file for auto-detection?"; then
            _pick_vmx
        fi
    fi
}

_pick_vmx() {
    local vmx_path
    vmx_path=$(pick_path \
        "Select VMX" \
        "Path to .vmx file:" \
        "" \
        "$VM_DIR" \
        "*.vmx")
    if [[ -f "$vmx_path" ]]; then
        VMX_FILE="$vmx_path"
    else
        warn "Invalid or missing VMX file — continuing without it."
        VMX_FILE=""
    fi
}

# ================================================================
# STEP 4 — OS Type and Hardware Parameters
# ================================================================
step_os_config() {
    GUEST_OS=""
    local detected_tag="linux"

    if [[ -n "$VMX_FILE" ]]; then
        GUEST_OS="$(grep -im1 '^guestOS' "$VMX_FILE" 2>/dev/null \
            | cut -d'"' -f2 || true)"
        local mem_vmx cpu_vmx
        mem_vmx="$(grep -im1 '^memsize'  "$VMX_FILE" 2>/dev/null \
            | cut -d'"' -f2 || true)"
        cpu_vmx="$(grep -im1 '^numvcpus' "$VMX_FILE" 2>/dev/null \
            | cut -d'"' -f2 || true)"
        [[ "$mem_vmx" =~ ^[0-9]+$ ]] && MEM_MB="$mem_vmx"
        [[ "$cpu_vmx" =~ ^[0-9]+$ ]] && VCPUS="$cpu_vmx"

        local g="${GUEST_OS,,}"
        if [[ "$g" =~ win ]]; then
            OS_TYPE="windows"
            if [[ "$g" =~ (longhorn|winnetenterprise|winnetserver|winnet[^0-9]|server2008[^r]) ]]; then
                detected_tag="win2008"
            elif [[ "$g" =~ (windows7server|win7server|2008r2|win2k8r2) ]]; then
                detected_tag="win2008r2"
            elif [[ "$g" =~ (windows11|win11) ]]; then
                detected_tag="win11"
            elif [[ "$g" =~ (windows2025|win2025|2025srv) ]]; then
                detected_tag="win2025"
            elif [[ "$g" =~ (winxp|whistler|xppro|winxphome|windowsxp) ]]; then
                detected_tag="winxp"
            elif [[ "$g" =~ (vista|windows7$|win7$|winvista) ]]; then
                detected_tag="windesktop"
            elif [[ "$g" =~ (windows9|win10) ]]; then
                detected_tag="win10"
            else
                detected_tag="win2012"
            fi
        elif [[ "$g" =~ freebsd|openbsd|netbsd ]]; then
            OS_TYPE="bsd"; detected_tag="bsd"
        else
            OS_TYPE="linux"; detected_tag="linux"
        fi
    fi

    local os_detected_hint=""
    if [[ -n "$GUEST_OS" ]]; then
        os_detected_hint="VMX detected: \"${GUEST_OS}\"  →  suggested: ${detected_tag}\n\n"
    else
        os_detected_hint="No VMX — select operating system manually.\n\n"
    fi

    local os_choice
    os_choice=$(tui_menu "Guest OS Type" \
        "${os_detected_hint}⚠  WARNING: Choose the CORRECT operating system.\nA wrong OS generates incorrect XML (disk, clock, drivers)." \
        "win2008"    "Windows Server 2008 (non R2)          [BIOS+SATA]" \
        "win2008r2"  "Windows Server 2008 R2                [BIOS+SATA]" \
        "win2012"    "Windows Server 2012 / 2016 / 2019 / 2022  [BIOS]" \
        "winxp"      "Windows XP                       [i440fx+IDE]" \
        "windesktop" "Windows Desktop 7 / 10              [BIOS+SATA]" \
        "win11"      "Windows 11                             [UEFI+TPM]" \
        "win2025"    "Windows Server 2025                    [UEFI+TPM]" \
        "linux"      "Linux (Generic — virtio drivers)" \
        "bsd"        "FreeBSD / OpenBSD / NetBSD")

    os_choice="${os_choice:-$detected_tag}"

    case "$os_choice" in
        win2008)     OS_TYPE="windows"; OS_SUBTYPE="win2008"    ;;
        win2008r2)   OS_TYPE="windows"; OS_SUBTYPE="win2008r2"  ;;
        win2012)     OS_TYPE="windows"; OS_SUBTYPE="win2012plus";;
        winxp)       OS_TYPE="windows"; OS_SUBTYPE="winxp"      ;;
        windesktop)  OS_TYPE="windows"; OS_SUBTYPE="desktop"    ;;
        win11)       OS_TYPE="windows"; OS_SUBTYPE="win11"      ;;
        win2025)     OS_TYPE="windows"; OS_SUBTYPE="win2025"    ;;
        linux)       OS_TYPE="linux";   OS_SUBTYPE="generic"    ;;
        bsd)         OS_TYPE="bsd";     OS_SUBTYPE="generic"    ;;
        *)           OS_TYPE="linux";   OS_SUBTYPE="generic"    ;;
    esac

    local os_label
    case "$OS_SUBTYPE" in
        win2008)     os_label="Windows Server 2008 (non R2) — BIOS + SATA + localtime" ;;
        win2008r2)   os_label="Windows Server 2008 R2       — BIOS + SATA + localtime" ;;
        win2012plus) os_label="Windows Server 2012+         — BIOS + SATA + localtime" ;;
        winxp)       os_label="Windows XP                   — i440fx + IDE + localtime" ;;
        desktop)     os_label="Windows Desktop 7/10         — BIOS + SATA + localtime" ;;
        win11)       os_label="Windows 11                   — UEFI + TPM  + localtime" ;;
        win2025)     os_label="Windows Server 2025          — UEFI + TPM  + localtime" ;;
        *)           os_label="Linux / BSD                  — virtio + utc"             ;;
    esac
    tui_yesno "Confirm OS" \
        "Selected OS:\n\n  ${os_label}\n\nIs this correct?" \
        || { tui_msg "Cancelled" "Restart the script and select the correct OS."; exit 1; }

    local name_in
    name_in=$(tui_input "VM Name" \
        "VM Name (used in XML and file names):" "$VM_NAME")
    [[ -n "$name_in" ]] && VM_NAME="$(sanitize_name "$name_in")"

    local mem_in
    mem_in=$(tui_input "RAM" "RAM amount in MB:" "$MEM_MB")
    [[ "$mem_in" =~ ^[0-9]+$ ]] && MEM_MB="$mem_in" \
        || warn "Invalid RAM value, using $MEM_MB MB."

    local cpu_in
    cpu_in=$(tui_input "vCPU" "Number of vCPUs:" "$VCPUS")
    [[ "$cpu_in" =~ ^[0-9]+$ ]] && VCPUS="$cpu_in" \
        || warn "Invalid vCPU value, using $VCPUS."

    MEM_KIB=$(( MEM_MB * 1024 ))
}

# ================================================================
# STEP 5 — Machine type and disk bus
# ================================================================
step_hw_config() {
    USE_UEFI=false
    USE_TPM=false

    # Defaults (q35 guests): modern USB/NIC/watchdog
    CDROM_BUS="sata"
    USB_MODEL="qemu-xhci"; NIC_MODEL="virtio"; WATCHDOG_MODEL="itco"

    case "$OS_SUBTYPE" in
        winxp)
            # Windows XP: no AHCI driver and q35 has no IDE -> must use i440fx + IDE.
            # XP also lacks xHCI (VNC mouse dead) and virtio-net drivers.
            MACHINE_TYPE="$MACHINE_I440FX"
            DISK_BUS="ide"; DISK_DEV="hda"; CDROM_BUS="ide"
            USB_MODEL="piix3-uhci"      # USB 1.1, native XP -> absolute mouse works in VNC
            NIC_MODEL="rtl8139"         # native XP NIC driver (virtio has none)
            WATCHDOG_MODEL="i6300esb"   # itco is q35-only; i6300esb is the i440fx watchdog
            CLOCK_OFFSET="localtime"
            ;;
        win2008|win2008r2|win2012plus|desktop|win10)
            MACHINE_TYPE="$MACHINE_Q35"
            DISK_BUS="sata"; DISK_DEV="sda"
            CLOCK_OFFSET="localtime"
            ;;
        win11|win2025)
            MACHINE_TYPE="$MACHINE_Q35"
            DISK_BUS="sata"; DISK_DEV="sda"
            CLOCK_OFFSET="localtime"
            USE_UEFI=true
            USE_TPM=true
            ;;
        *)
            MACHINE_TYPE="$MACHINE_Q35"
            DISK_BUS="virtio"; DISK_DEV="vda"
            CLOCK_OFFSET="utc"
            ;;
    esac
    VIDEO_MODEL="vga"

    if [[ "$OS_SUBTYPE" == "winxp" ]]; then
        tui_msg "Machine Type" "Windows XP requires i440fx (legacy PIIX3 IDE).\nUsing: ${MACHINE_TYPE}\nq35 has no IDE controller -> XP would BSOD 0x7B."
    else
    local mt_choice
    mt_choice=$(tui_menu "Machine Type" \
"QEMU/KVM virtual machine type.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
pc-q35-rhel9.4.0  → default SynetoOS
pc-q35-rhel9.2.0  → previous Q35 version
Custom             → enter manually
pc-i440fx-rhel7.6.0 -> legacy guests (Windows XP, PIIX3 IDE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Default: pc-q35-rhel9.4.0" \
        "q35_94"  "pc-q35-rhel9.4.0     (default, modern guests)" \
        "q35_92"  "pc-q35-rhel9.2.0     (previous version)" \
        "i440fx"  "pc-i440fx-rhel7.6.0  (legacy: Windows XP / old OS)" \
        "custom"  "Custom               (enter manually)")

    case "${mt_choice:-q35_94}" in
        q35_94) MACHINE_TYPE="pc-q35-rhel9.4.0" ;;
        q35_92) MACHINE_TYPE="pc-q35-rhel9.2.0" ;;
        i440fx) MACHINE_TYPE="$MACHINE_I440FX" ;;
        custom)
            local custom_mt
            custom_mt=$(tui_input "Custom Machine Type" \
                "Enter QEMU machine type:" "$MACHINE_TYPE")
            [[ -n "$custom_mt" ]] && MACHINE_TYPE="$custom_mt"
            ;;
    esac
    fi

    # The watchdog depends on the final machine type (also when picked by hand):
    # itco exists only on q35, i440fx needs i6300esb. Mismatch -> virsh define fails
    # with "itco model of watchdog is only part of q35 machine".
    case "$MACHINE_TYPE" in
        pc-q35*|q35*) WATCHDOG_MODEL="itco" ;;
        *)            WATCHDOG_MODEL="i6300esb" ;;
    esac

    if $USE_UEFI; then
        tui_msg "UEFI Detected" "Boot: UEFI (Required for ${OS_SUBTYPE})\nUEFI automatically enabled."
    elif [[ "$OS_TYPE" == "windows" ]]; then
        if tui_yesno "UEFI" \
"Boot firmware: BIOS legacy (default for $OS_SUBTYPE)
Do you want to enable UEFI instead of BIOS?
(Check VMX: firmware=\"efi\" → yes, firmware=\"bios\" → no)
If in doubt: No."; then
            USE_UEFI=true
        fi
    fi

    if $USE_TPM; then
        tui_msg "TPM 2.0 Detected" "Emulated TPM 2.0 enabled (Required for ${OS_SUBTYPE})"
    elif $USE_UEFI; then
        if tui_yesno "TPM 2.0" \
"Do you want to add an emulated TPM 2.0?
Only needed if original VM used it (BitLocker, Win 11, Srv 2025).
If in doubt: No."; then
            USE_TPM=true
        fi
    fi

    if [[ "$OS_TYPE" == "windows" && "$OS_SUBTYPE" != "winxp" ]]; then
        if tui_yesno "Disk Bus" \
"Current disk bus: $DISK_BUS (device: $DISK_DEV)
Do you want to change it?
SATA   → safe default, no extra drivers needed
VirtIO → better performance, only if drivers already installed
If in doubt: No (leave SATA)."; then
            local bus_choice
            bus_choice=$(tui_menu "Disk Bus" \
                "Select disk bus:" \
                "sata"   "SATA / AHCI  — safe, always works on Windows" \
                "virtio" "VirtIO       — fast, only if drivers already installed")
            case "${bus_choice:-sata}" in
                sata)   DISK_BUS="sata";   DISK_DEV="sda" ;;
                virtio) DISK_BUS="virtio"; DISK_DEV="vda" ;;
            esac
        fi
    fi
}

# ================================================================
# STEP 6 — Network
# ================================================================
step_network() {
    local net_in
    net_in=$(tui_input "Network" \
        "Libvirt network name (source network in XML):" "$NET_NAME")
    [[ -n "$net_in" ]] && NET_NAME="$net_in"
}

# ================================================================
# STEP 7 — VirtIO ISO (Windows only)
# ================================================================
step_virtio() {
    VIRTIO_ISO=""
    [[ "$OS_TYPE" != "windows" ]] && return

    if tui_yesno "VirtIO Driver" \
        "Do you want to mount the virtio-win ISO as a CD-ROM?\n(needed to install VirtIO drivers on Windows after migration)"; then

        local iso_path
        iso_path=$(pick_path \
            "Select VirtIO ISO" \
            "VirtIO ISO path:" \
            "$DEFAULT_VIRTIO_ISO" \
            "$(dirname "$DEFAULT_VIRTIO_ISO")" \
            "*.iso")

        if [[ -f "$iso_path" ]]; then
            VIRTIO_ISO="$iso_path"
        else
            warn "ISO not found: ${iso_path:-<empty>} — proceeding without it."
        fi
    fi
}

# ================================================================
# STEP 8 — Summary and Confirmation
# ================================================================
step_summary() {
    local virtio_line=""
    [[ -n "$VIRTIO_ISO" ]] \
        && virtio_line=$'\n'"VirtIO ISO:   $(basename "$VIRTIO_ISO") (bus: sata / sdb)"

    local extra_disks_line=""
    if [[ ${#EXTRA_VMDK_LIST[@]} -gt 0 ]]; then
        local j
        for j in "${!EXTRA_VMDK_LIST[@]}"; do
            extra_disks_line="${extra_disks_line}"$'\n'"Disk $((j+2)):      $(basename "${EXTRA_VMDK_LIST[$j]}")"
        done
    fi

    local uefi_line="BIOS legacy"
    $USE_UEFI && uefi_line="UEFI (firmware='efi')"
    local tpm_line="no"
    $USE_TPM  && tpm_line="yes (tpm-crb, emulator v2.0)"

    local summary
    summary=$(cat <<RECAP
CONVERSION SUMMARY
────────────────────────────────────────────
VM Name:      $VM_NAME
OS:           $OS_TYPE / $OS_SUBTYPE
Disk 1:      $(basename "$VMDK_PATH")${extra_disks_line}
RAM:          $MEM_MB MB  ($MEM_KIB KiB)
vCPU:         $VCPUS
Machine:      $MACHINE_TYPE
Disk bus:     ${DISK_BUS}  (dev: ${DISK_DEV})
Boot:         $uefi_line
TPM 2.0:      $tpm_line
Clock:        $CLOCK_OFFSET
Network:      $NET_NAME
VMX used:    ${VMX_FILE:-<none>}${virtio_line}
────────────────────────────────────────────
Press Yes to start conversion.
RECAP
)
    tui_yesno "Confirm Conversion" "$summary" \
        || { tui_msg "Cancelled" "Conversion cancelled by user."; exit 0; }
}

# ================================================================
# STEP 9 — ZFS Dataset Creation + Syneto Properties
# ================================================================
step_zfs() {
    local ACTUAL_POOL
    ACTUAL_POOL=$(zfs list -H -o name | head -n1 | cut -d'/' -f1)

    # Detect machine identity for eu:syneto:creation
    local HOSTNAME POOL_GUID MACHINE_ID
    HOSTNAME="$(hostname -s 2>/dev/null || hostname)"
    POOL_GUID="$(zpool get -Hpo value guid "${ACTUAL_POOL}" 2>/dev/null || echo "0")"
    MACHINE_ID="$(cat /etc/machine-id 2>/dev/null | tr -d '[:space:]' || echo "")"

    NEW_UUID="$(uuidgen)"
    NEW_DATASET="${ACTUAL_POOL}/syn-volumes/${NEW_UUID}"
    NEW_MOUNTPOINT="/${ACTUAL_POOL}/syn-volumes/${NEW_UUID}"

    log "Creating ZFS dataset on pool [${ACTUAL_POOL}]: $NEW_DATASET"
    zfs create -p "$NEW_DATASET"

    # ----------------------------------------------------------------
    # Syneto ZFS properties — required for GUI to recognise the VM
    # as LOCAL (internal) and not external
    # ----------------------------------------------------------------
    log "Setting Syneto ZFS properties..."

    zfs set eu:syneto:type=LOCAL "${NEW_DATASET}"

    local creation_json
    creation_json="{\"hostname\": \"${HOSTNAME}\", \"poolName\": \"${ACTUAL_POOL}\", \"poolGuid\": ${POOL_GUID}, \"machineId\": \"${MACHINE_ID}\"}"
    zfs set "eu:syneto:creation=${creation_json}" "${NEW_DATASET}"

    zfs set 'volume:labels={}' "${NEW_DATASET}"

    log "Dataset ready: $NEW_MOUNTPOINT"
    log "  eu:syneto:type     = LOCAL"
    log "  eu:syneto:creation = ${creation_json}"
    log "  volume:labels      = {}"
}

# ================================================================
# STEP 10 — Conversion VMDK → qcow2
# ================================================================
step_convert() {
    EXTRA_IMG_LIST=()
    local total_disks=$(( 1 + ${#EXTRA_VMDK_LIST[@]} ))

    OUT_IMG="${NEW_MOUNTPOINT}/${VM_NAME}_1.img"
    log "Converting disk 1/${total_disks}:"
    log "  From:  $VMDK_PATH"
    log "  To:    $OUT_IMG"
    echo ""
    qemu-img convert -p -f vmdk -O qcow2 "$VMDK_PATH" "$OUT_IMG"
    echo ""

    local i
    for i in "${!EXTRA_VMDK_LIST[@]}"; do
        local n=$(( i + 2 ))
        local extra_img="${NEW_MOUNTPOINT}/${VM_NAME}_${n}.img"
        log "Converting disk ${n}/${total_disks}:"
        log "  From:  ${EXTRA_VMDK_LIST[$i]}"
        log "  To:    $extra_img"
        echo ""
        qemu-img convert -p -f vmdk -O qcow2 "${EXTRA_VMDK_LIST[$i]}" "$extra_img"
        echo ""
        EXTRA_IMG_LIST+=("$extra_img")
    done

    log "Conversion complete (${total_disks} disk(s))."
}

# ================================================================
# STEP 10b — Fix BSOD 0x0000007B (INACCESSIBLE_BOOT_DEVICE)
# ================================================================
step_fix_win_boot() {
    [[ "$OS_TYPE" != "windows" ]] && return

    tui_yesno "Fix BSOD 0x0000007B" \
"RECOMMENDED for VMs migrated from VMware / ESXi.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STOP: 0x0000007B = INACCESSIBLE_BOOT_DEVICE

Cause: original VM used IDE or SCSI
controllers on VMware. KVM uses SATA/AHCI.
Windows doesn't load AHCI at boot → BSOD.

The patch enables drivers offline:
  msahci   (AHCI — Windows 2008/Vista)
  storahci (AHCI — Windows 2008 R2 / 7+)
  iaStorV  (Intel Storage AHCI)
  pciide   (PCI IDE — generic fallback)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Apply patch?" || return 0

    if ! command -v virt-win-reg &>/dev/null; then
        tui_msg "Missing Dependencies" \
"virt-win-reg not found.
Install:  dnf install -y libguestfs-tools libguestfs-winsupport
Then re-run the boot fix."
        return 0
    fi

    export LIBGUESTFS_BACKEND=direct

    # Offline hives expose ControlSet00N (not CurrentControlSet). Detect the active one.
    local cur cs
    cur="$(virt-win-reg "$OUT_IMG" 'HKLM\SYSTEM\Select' 2>/dev/null \
            | sed -n 's/.*"Current"=dword:0*\([0-9a-fA-F]\+\).*/\1/p' | head -n1)" || true
    [[ -n "$cur" ]] && cur=$((16#$cur)) || cur=1
    cs="$(printf 'ControlSet%03d' "$cur")"
    log "Active ControlSet: $cs"

    local reg="/tmp/${VM_NAME}-winboot.reg"
    local ide_class='{4D36E96A-E325-11CE-BFC1-08002BE10318}'

    if [[ "$DISK_BUS" == "ide" ]]; then
        # Windows XP on i440fx: bind the PIIX3 IDE controller (PCI 8086:7010) to a boot
        # driver and enable the IDE storage stack at boot (the mergeide fix).
        cat > "$reg" <<'REOF'
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Control\CriticalDeviceDatabase\pci#ven_8086&dev_7010]
"Service"="intelide"
"ClassGUID"="@@IDECLASS@@"

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Control\CriticalDeviceDatabase\*pnp0600]
"Service"="atapi"
"ClassGUID"="@@IDECLASS@@"

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Services\pciide]
"Start"=dword:00000000

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Services\intelide]
"Start"=dword:00000000

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Services\atapi]
"Start"=dword:00000000

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Control\CrashControl]
"AutoReboot"=dword:00000000
REOF
    else
        # q35 + SATA/AHCI (Windows 7 / 2008 R2 / 2012+): enable AHCI drivers at boot.
        cat > "$reg" <<'REOF'
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Services\storahci]
"Start"=dword:00000000

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Services\storahci\StartOverride]
"0"=dword:00000000

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Services\msahci]
"Start"=dword:00000000

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Services\msahci\StartOverride]
"0"=dword:00000000

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Services\iaStorV]
"Start"=dword:00000000

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Services\pciide]
"Start"=dword:00000000

[HKEY_LOCAL_MACHINE\SYSTEM\@@CS@@\Control\CrashControl]
"AutoReboot"=dword:00000000
REOF
    fi

    sed -i "s/@@CS@@/${cs}/g; s|@@IDECLASS@@|${ide_class}|g" "$reg"

    log "Applying Windows boot patch to $OUT_IMG ($cs)..."
    if virt-win-reg --merge "$OUT_IMG" "$reg"; then
        log "Boot driver patch applied ($cs)."
    else
        warn "virt-win-reg merge failed — apply the boot fix manually."
    fi
    rm -f "$reg"
}

# ================================================================
# STEP 10c — Fix legacy Linux guests: /dev/sdX -> UUID (boot + fstab)
# ================================================================
# Old guests (e.g. Ubuntu 8.04, grub legacy) hard-code root=/dev/sda1 and
# /dev/sdaN in fstab. On KVM the disk is virtio (/dev/vda) -> the initramfs
# cannot find the root and drops to a (initramfs) shell. Replacing the device
# names with filesystem UUIDs makes the guest independent of the bus/name.
# Originals are saved next to the files as *.bak-convert-VM-<timestamp>.
step_fix_linux_boot() {
    [[ "$OS_TYPE" != "linux" ]] && return

    tui_yesno "Fix Linux boot (device names)" \
"RECOMMENDED for old Linux VMs migrated from VMware / ESXi.

Legacy guests hard-code /dev/sda1 in grub (root=) and in
/etc/fstab. On KVM the disk may appear as /dev/vda, so the
kernel stops at the (initramfs) prompt: root not found.

The patch replaces /dev/sdXN with UUID=... in:
  /boot/grub/menu.lst   (root=)
  /etc/fstab            (first column)
Backups: *.bak-convert-VM-<timestamp> in the same folders.
Apply patch?" || return 0

    if ! command -v guestfish &>/dev/null; then
        tui_msg "Missing Dependencies" \
"guestfish not found.
Install:  dnf install -y libguestfs-tools
Then fix root=/dev/sdXN and fstab manually (use UUID=...)."
        return 0
    fi

    export LIBGUESTFS_BACKEND=direct
    local work; work="$(mktemp -d /tmp/${VM_NAME}-linuxfix.XXXXXX)"
    local bak; bak="bak-convert-VM-$(date +%Y%m%d%H%M%S)"

    # Map /dev/sdXN -> UUID for every filesystem found on the image
    declare -A uuid_of=()
    local -a swap_uuids=()
    local rootdev="" line dev u
    rootdev="$(guestfish --ro -a "$OUT_IMG" run : inspect-os 2>/dev/null | head -n1)" || true
    if [[ -z "$rootdev" ]]; then
        warn "Cannot detect the Linux root filesystem - skipping boot fix."
        rm -rf "$work"; return 0
    fi
    while IFS= read -r line; do
        dev="${line%%:*}"
        [[ "$dev" =~ ^/dev/[sh]d[a-z][0-9]+$ ]] || continue
        u="$(guestfish --ro -a "$OUT_IMG" run : vfs-uuid "$dev" 2>/dev/null | head -n1)" || true
        [[ -n "$u" ]] && uuid_of["$dev"]="$u"
        [[ "${line#*: }" == "swap" && -n "$u" ]] && swap_uuids+=("$u")
    done < <(guestfish --ro -a "$OUT_IMG" run : list-filesystems 2>/dev/null)

    if [[ ${#uuid_of[@]} -eq 0 ]]; then
        warn "No filesystem UUIDs found - skipping boot fix."
        rm -rf "$work"; return 0
    fi

    log "Linux root: $rootdev - patching /etc/fstab and /boot/grub/menu.lst"
    guestfish -a "$OUT_IMG" <<GFEOF || warn "guestfish read failed"
run
mount $rootdev /
-download /etc/fstab $work/fstab
-download /boot/grub/menu.lst $work/menu.lst
GFEOF

    local d
    for d in "${!uuid_of[@]}"; do
        [[ -f "$work/fstab"    ]] && sed -i "s#^${d}\([[:space:]]\)#UUID=${uuid_of[$d]}\1#" "$work/fstab"
        [[ -f "$work/menu.lst" ]] && sed -i "s#root=${d}\([[:space:]]\|\$\)#root=UUID=${uuid_of[$d]}\1#g" "$work/menu.lst"
    done

    # fstab swap entry pointing to a device that no longer exists (e.g. /dev/sda5
    # while the swap is /dev/sda2): use the UUID of the only swap on the image.
    if [[ ${#swap_uuids[@]} -eq 1 && -f "$work/fstab" ]]; then
        sed -i "s|^/dev/[sh]d[a-z][0-9]*\([[:space:]].*[[:space:]]swap[[:space:]]\)|UUID=${swap_uuids[0]}\1|" "$work/fstab"
    fi

    {
        echo "run"
        echo "mount $rootdev /"
        if [[ -f "$work/fstab" ]]; then
            echo "cp-a /etc/fstab /etc/fstab.$bak"
            echo "upload $work/fstab /etc/fstab"
        fi
        if [[ -f "$work/menu.lst" ]]; then
            echo "cp-a /boot/grub/menu.lst /boot/grub/menu.lst.$bak"
            echo "upload $work/menu.lst /boot/grub/menu.lst"
        fi
    } > "$work/apply.fish"

    if guestfish -a "$OUT_IMG" -f "$work/apply.fish"; then
        log "Linux boot patch applied (UUID in fstab/menu.lst)."
    else
        warn "guestfish write failed - fix root=/dev/sdXN and fstab manually."
    fi
    rm -rf "$work"
}

# ================================================================
# STEP 10d — Disable VMware Tools leftovers (Windows guests from ESXi)
# ================================================================
# VMware Tools components still installed in the guest (Winlogon notify DLL
# VMUpgradeAtShutdownWXP.dll, VMTools service, vmci/vsock drivers...) talk to the
# host with VMware-only instructions. On KVM they can crash Winlogon at logon or
# shutdown: STOP c000021a, status 0xc0000096 (privileged instruction).
# Only services/keys that really exist are touched. The original keys are
# exported to <dataset>/<VM>-vmwtools-keys-backup.reg before any change.
step_fix_vmware_tools() {
    [[ "$OS_TYPE" != "windows" ]] && return

    tui_yesno "Disable VMware Tools" \
"RECOMMENDED for Windows VMs migrated from VMware / ESXi.

Leftover VMware Tools (VMTools service, vmci/vsock drivers,
Winlogon notify VMUpgradeAtShutdown) can crash Winlogon on KVM:
STOP c000021a / 0xc0000096 at logon or shutdown.

The patch (offline, only for items that exist):
  - sets Start=4 (disabled) on VMware services/drivers
  - removes the Winlogon Notify entries of VMware Tools
Original keys are exported to a .reg file in the VM folder.
Apply patch?" || return 0

    if ! command -v virt-win-reg &>/dev/null; then
        tui_msg "Missing Dependencies" \
"virt-win-reg not found.
Install:  dnf install -y libguestfs-tools libguestfs-winsupport
Then disable the VMware Tools services manually."
        return 0
    fi

    export LIBGUESTFS_BACKEND=direct

    local cur cs
    cur="$(virt-win-reg "$OUT_IMG" 'HKLM\SYSTEM\Select' 2>/dev/null \
            | sed -n 's/.*"Current"=dword:0*\([0-9a-fA-F]\+\).*/\1/p' | head -n1)" || true
    [[ -n "$cur" ]] && cur=$((16#$cur)) || cur=1
    cs="$(printf 'ControlSet%03d' "$cur")"

    local reg="/tmp/${VM_NAME}-vmwtools.reg"
    local bak="${NEW_MOUNTPOINT}/${VM_NAME}-vmwtools-keys-backup.reg"
    local notify='SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify'
    local svc key found=0
    : > "$bak"
    printf 'Windows Registry Editor Version 5.00\r\n\r\n' > "$reg"

    for svc in VMTools VMMEMCTL vmci vsock vmdebug vmmouse vmx_svga vmhgfs vmrawdsk vmvss vmusb vmware-tools; do
        key="SYSTEM\\${cs}\\Services\\${svc}"
        if virt-win-reg "$OUT_IMG" "HKLM\\${key}" >> "$bak" 2>/dev/null; then
            printf '[HKEY_LOCAL_MACHINE\\%s]\r\n"Start"=dword:00000004\r\n\r\n' "$key" >> "$reg"
            found=$(( found + 1 ))
        fi
    done
    for svc in VMUpgradeAtShutdown TPSvc; do
        key="${notify}\\${svc}"
        if virt-win-reg "$OUT_IMG" "HKLM\\${key}" >> "$bak" 2>/dev/null; then
            printf '[-HKEY_LOCAL_MACHINE\\%s]\r\n\r\n' "$key" >> "$reg"
            found=$(( found + 1 ))
        fi
    done

    if [[ $found -eq 0 ]]; then
        log "No VMware Tools leftovers found - nothing to disable."
        rm -f "$reg" "$bak"
        return 0
    fi

    log "Disabling ${found} VMware Tools item(s) ($cs). Backup: $bak"
    if virt-win-reg --merge "$OUT_IMG" "$reg"; then
        log "VMware Tools leftovers disabled."
    else
        warn "virt-win-reg merge failed - disable the VMware Tools services manually."
    fi
    rm -f "$reg"
}

# ================================================================
# STEP 11 — Generate XML + Register in Libvirt
# ================================================================
step_register_libvirt() {
    XML_PATH="${NEW_MOUNTPOINT}/${VM_NAME}.xml"
    VM_UUID="$(uuidgen)"

    log "Generating Libvirt XML configuration..."

    # Last guard: refuse to write an XML that libvirt will reject at define time.
    if [[ "$WATCHDOG_MODEL" == "itco" && "$MACHINE_TYPE" != *q35* ]]; then
        die "Watchdog itco requires a q35 machine type (got ${MACHINE_TYPE}). Use i6300esb on i440fx."
    fi

    # ----------------------------------------------------------------
    # libosinfo OS ID mapping
    # ----------------------------------------------------------------
    local LIBOSINFO_ID
    case "$OS_SUBTYPE" in
        win2008)     LIBOSINFO_ID="http://microsoft.com/win/2k8"    ;;
        win2008r2)   LIBOSINFO_ID="http://microsoft.com/win/2k8r2"  ;;
        win2012plus) LIBOSINFO_ID="http://microsoft.com/win/2k12"   ;;
        desktop)     LIBOSINFO_ID="http://microsoft.com/win/10"     ;;
        win11)       LIBOSINFO_ID="http://microsoft.com/win/11"     ;;
        win2025)     LIBOSINFO_ID="http://microsoft.com/win/2k25"   ;;
        bsd)         LIBOSINFO_ID="http://freebsd.org/freebsd/13"   ;;
        *)           LIBOSINFO_ID="http://libosinfo.org/linux/2016" ;;
    esac

    # ----------------------------------------------------------------
    # Device letter helpers
    # ----------------------------------------------------------------
    local -a _ALPHA=( a b c d e f g h i j k l m n o p q r s t u v w x y z )

    local CDROM_DEV=""
    local EXTRA_DISKS_XML=""
    local CDROM_XML=""
    local SYNETO_CDROM_CONNECT="false"

    if [[ "$DISK_BUS" == "virtio" ]]; then
        # cdrom at sda (sata), main disk at vda, extras at vdb vdc ...
        CDROM_DEV="sda"; CDROM_BUS="sata"
        local vdisk_idx=1
        for extra_img in "${EXTRA_IMG_LIST[@]}"; do
            local edev="vd${_ALPHA[$vdisk_idx]}"
            EXTRA_DISKS_XML+="
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2' cache='none' io='threads' discard='unmap'/>
      <source file='${extra_img}'/>
      <target dev='${edev}' bus='virtio'/>
      <serial>${VM_NAME}_$(( vdisk_idx + 1 ))</serial>
    </disk>"
            vdisk_idx=$(( vdisk_idx + 1 ))
        done
    else
        # letter-bus (sata or ide): main=<pfx>a, extras next, cdrom after.
        # sata -> sd prefix ; ide (Windows XP) -> hd prefix.
        local _pfx="sd"; [[ "$DISK_BUS" == "ide" ]] && _pfx="hd"
        local disk_idx=1
        for extra_img in "${EXTRA_IMG_LIST[@]}"; do
            local edev="${_pfx}${_ALPHA[$disk_idx]}"
            EXTRA_DISKS_XML+="
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2' cache='none' io='threads' discard='unmap'/>
      <source file='${extra_img}'/>
      <target dev='${edev}' bus='${DISK_BUS}'/>
      <serial>${VM_NAME}_$(( disk_idx + 1 ))</serial>
    </disk>"
            disk_idx=$(( disk_idx + 1 ))
        done
        CDROM_DEV="${_pfx}${_ALPHA[$disk_idx]}"
        CDROM_BUS="$DISK_BUS"
    fi

    # ----------------------------------------------------------------
    # Cdrom XML
    # ----------------------------------------------------------------
    if [[ -n "$VIRTIO_ISO" ]]; then
        SYNETO_CDROM_CONNECT="true"
        CDROM_XML="
    <disk type='file' device='cdrom'>
      <driver name='qemu' type='raw'/>
      <source file='${VIRTIO_ISO}'/>
      <target dev='${CDROM_DEV}' bus='${CDROM_BUS}'/>
      <readonly/>
    </disk>"
    else
        CDROM_XML="
    <disk type='file' device='cdrom'>
      <driver name='qemu' type='raw'/>
      <target dev='${CDROM_DEV}' bus='${CDROM_BUS}'/>
      <readonly/>
    </disk>"
    fi

    # ----------------------------------------------------------------
    # OS firmware block
    # ----------------------------------------------------------------
    local OS_OPEN_TAG="<os>"
    $USE_UEFI && OS_OPEN_TAG="<os firmware='efi'>"

    local BOOT_ENTRIES="    <boot dev='hd'/>"
    [[ -n "$VIRTIO_ISO" ]] && BOOT_ENTRIES+="
    <boot dev='cdrom'/>"

    # ----------------------------------------------------------------
    # TPM block
    # ----------------------------------------------------------------
    local TPM_XML=""
    $USE_TPM && TPM_XML="
    <tpm model='tpm-crb'>
      <backend type='emulator' version='2.0'/>
    </tpm>"

    # ----------------------------------------------------------------
    # CPU block. Windows XP crashes (STOP c000021a / 0xc0000096) with the
    # host-model CPU (Cascadelake + ~150 feature flags): use a legacy model
    # without the hypervisor bit. Everything else keeps host-model.
    # ----------------------------------------------------------------
    local CPU_XML
    if [[ "$OS_SUBTYPE" == "winxp" ]]; then
        CPU_XML="  <cpu mode='custom' match='exact' check='none'>
    <model fallback='allow'>core2duo</model>
    <feature policy='disable' name='hypervisor'/>
    <topology sockets='1' dies='1' clusters='1' cores='${VCPUS}' threads='1'/>
  </cpu>"
    else
        CPU_XML="  <cpu mode='host-model' check='partial'>
    <topology sockets='1' dies='1' clusters='1' cores='${VCPUS}' threads='1'/>
  </cpu>"
    fi

    # ----------------------------------------------------------------
    # Write XML
    # ----------------------------------------------------------------
    cat > "$XML_PATH" <<EOF
<domain type='kvm'>
  <name>${VM_NAME}</name>
  <uuid>${VM_UUID}</uuid>
  <metadata xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
    <libosinfo:libosinfo>
      <libosinfo:os id="${LIBOSINFO_ID}"/>
    </libosinfo:libosinfo>
    <syneto:metadata xmlns:syneto="https://syneto.eu" xmlns:metadata="https://syneto.eu">
        <syneto:conf_file_path>${XML_PATH}</syneto:conf_file_path>
        <syneto:cdroms>
                <syneto:cdrom connect_at_power_on="${SYNETO_CDROM_CONNECT}">${CDROM_DEV}</syneto:cdrom>
        </syneto:cdroms>
    </syneto:metadata>
  </metadata>
  <memory unit='KiB'>${MEM_KIB}</memory>
  <currentMemory unit='KiB'>${MEM_KIB}</currentMemory>
  <vcpu placement='static'>${VCPUS}</vcpu>
  <sysinfo type='smbios'>
    <bios>
      <entry name='vendor'>Syneto</entry>
    </bios>
    <system>
      <entry name='manufacturer'>Syneto</entry>
      <entry name='product'>Hyperion</entry>
      <entry name='version'>6.0</entry>
      <entry name='serial'>${VM_UUID}</entry>
    </system>
    <baseBoard>
      <entry name='manufacturer'>Syneto</entry>
      <entry name='product'>Hyperion</entry>
      <entry name='version'>6.0</entry>
      <entry name='serial'>${VM_UUID}</entry>
    </baseBoard>
    <chassis>
      <entry name='manufacturer'>Syneto</entry>
      <entry name='version'>6.0</entry>
      <entry name='serial'>${VM_UUID}</entry>
    </chassis>
    <oemStrings>
      <entry>SynetoOS:6.0</entry>
    </oemStrings>
  </sysinfo>
  ${OS_OPEN_TAG}
    <type arch='x86_64' machine='${MACHINE_TYPE}'>hvm</type>
${BOOT_ENTRIES}
    <bootmenu enable='yes'/>
    <smbios mode='sysinfo'/>
  </os>
  <features>
    <acpi/>
    <apic/>
  </features>
${CPU_XML}
  <clock offset='${CLOCK_OFFSET}'>
    <timer name='rtc' tickpolicy='catchup'/>
    <timer name='pit' tickpolicy='delay'/>
    <timer name='hpet' present='no'/>
  </clock>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>destroy</on_crash>
  <pm>
    <suspend-to-mem enabled='no'/>
    <suspend-to-disk enabled='no'/>
  </pm>
  <devices>
    <emulator>/usr/libexec/qemu-kvm</emulator>${CDROM_XML}
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2' cache='none' io='threads' discard='unmap'/>
      <source file='${OUT_IMG}'/>
      <target dev='${DISK_DEV}' bus='${DISK_BUS}'/>
      <serial>${VM_NAME}_1</serial>
    </disk>${EXTRA_DISKS_XML}
    <controller type='virtio-serial' index='0'/>
    <controller type='usb' index='0' model='${USB_MODEL}'/>
    <controller type='sata' index='0'/>
    <interface type='network'>
      <source network='${NET_NAME}'/>
      <model type='${NIC_MODEL}'/>
      <link state='up'/>
    </interface>
    <serial type='pty'>
      <target type='isa-serial' port='0'>
        <model name='isa-serial'/>
      </target>
    </serial>
    <console type='pty'>
      <target type='serial' port='0'/>
    </console>
    <channel type='unix'>
      <target type='virtio' name='org.qemu.guest_agent.0'/>
      <address type='virtio-serial' controller='0' bus='0' port='1'/>
    </channel>
    <input type='tablet' bus='usb'>
      <address type='usb' bus='0' port='1'/>
    </input>
    <input type='mouse' bus='ps2'/>
    <input type='keyboard' bus='ps2'/>
    <graphics type='vnc' port='-1' autoport='yes' listen='0.0.0.0'>
      <listen type='address' address='0.0.0.0'/>
    </graphics>
    <audio id='1' type='none'/>
    <video>
      <model type='${VIDEO_MODEL}' vram='131072' heads='1' primary='yes'/>
    </video>${TPM_XML}
    <watchdog model='${WATCHDOG_MODEL}' action='reset'/>
    <memballoon model='virtio' autodeflate='on' freePageReporting='on'>
      <stats period='5'/>
    </memballoon>
    <rng model='virtio'>
      <backend model='random'>/dev/urandom</backend>
    </rng>
  </devices>
</domain>
EOF

    log "XML written to: $XML_PATH"
    log "Registering VM with libvirt..."
    virsh define "$XML_PATH"

    # ----------------------------------------------------------------
    # Register the libvirt storage POOL for this dataset.
    # Without it Minerva cannot map the VM to managed storage and shows
    # it as "unregistered" / duplicated (external) in the GUI. Healthy
    # VMs all have a dir pool named "<VM> (1)" targeting their dataset.
    # ----------------------------------------------------------------
    local POOL_NAME="${VM_NAME} (1)"
    if virsh pool-info "$POOL_NAME" &>/dev/null; then
        log "Storage pool '$POOL_NAME' already exists."
    else
        log "Creating storage pool '$POOL_NAME' -> ${NEW_MOUNTPOINT}"
        local pool_xml="/tmp/${VM_NAME}-pool.xml"
        cat > "$pool_xml" <<POOLEOF
<pool type='dir'>
  <name>${POOL_NAME}</name>
  <target>
    <path>${NEW_MOUNTPOINT}</path>
    <permissions><mode>0755</mode><owner>0</owner><group>0</group></permissions>
  </target>
</pool>
POOLEOF
        virsh pool-define "$pool_xml"
        virsh pool-autostart "$POOL_NAME"
        virsh pool-start "$POOL_NAME"
        rm -f "$pool_xml"
    fi

    log "VM '${VM_NAME}' registered successfully!"
}

# ================================================================
# MAIN EXECUTION
# ================================================================
check_deps
step_input
step_select_vmdk
step_add_disks
step_vmx
step_os_config
step_hw_config
step_network
step_virtio
step_summary
step_zfs
step_convert
step_fix_win_boot
step_fix_linux_boot
step_fix_vmware_tools
step_register_libvirt

log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "MIGRATION COMPLETE!"
log "VM Name:    $VM_NAME"
log "Dataset:    $NEW_DATASET"
log "Disk image: $OUT_IMG"
log "XML:        $XML_PATH"
log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

Save and EXIT

:wq

 

Step 5.  Give permissions to /tmp/convert-VM file

chmod +x /tmp/convert-VM

 

Step 6.  Run the script

/tmp/convert-VM

 

Step 7. Enter the path to the VM directory or VMDK file, or browse to its location, then click Enter

The VM to convert can be selected by entering its path directly or by browsing to its location

 

Step 8. Select the VMDK file, then click Enter

 

Step 9. Confirm whether the VM has additional disks to include in the import

 

Step 10. Choose how to configure the VM resources, then press Enter

  • use: Applies the settings from the detected VMX file automatically 
  • browse: Select a different VMX file to source the VM configuration from 
  • skip: Ignore the VMX file and configure OS, RAM, and CPU manually later

 

Step 11. Select the OS version, then press Enter

 

Step 12. Choose the VM name, then press Enter

 

Step 13. Choose the RAM amount in MB, then press Enter

 

Step 14. Choose the vCPU number, then press Enter

 

Step 15. Select the recommended machine type, then press Enter

For Windows XP, the machine type and disk bus are set automatically (i440fx + IDE), so the "Machine type" and "Disk bus" steps are skipped.

 

Step 16. Choose the boot firmware

Select Yes for UEFI or No for BIOS

 

Step 17. Choose whether to add TPM 2.0 support 

Select Yes to enable emulated TPM 2.0 support, or No to skip it.

 

Step 18. Choose the disk bus type, then press Enter

For Windows XP, the machine type and disk bus are set automatically (i440fx + IDE), so the "Machine type" and "Disk bus" steps are skipped.

 

Step 19. Specify the VM network name will connect to, then press Enter

The default value is VM Network

 

Step 20 (Optional). Choose whether to mount the virtio-win ISO as a CD-ROM for VirtIO driver installation

Select Yes to mount the virtio-win ISO as a CD-ROM for VirtIO driver installation, or No to skip it.
The ISO can also be mounted at a later stage. If selected, the path to the VirtIO ISO will be required.

 

Step 21. Review the conversion summary, if all parameters are correct, click Yes to proceed

 

Step 22 (Optional). Choose whether to apply the BSOD fix

Select Yes to apply the BSOD fix, or No to skip it.
The BSOD fix is recommended for all VMs migrated from VMware/ESXi. 
After the conversion, the VM will be visible in the SynetoOS GUI within 10–15 minutes.

 

Step 23. Register the VM (“How to Register a VM in SynetoOS 6”)