→ 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 python3-hivex
rpm -Uvh --nodeps --force \
https://dl.rockylinux.org/stg/rocky/9/AppStream/x86_64/os/Packages/h/hivex-1.3.24-1.el9.x86_64.rpm \
https://dl.rockylinux.org/stg/rocky/9/AppStream/x86_64/os/Packages/h/hivex-libs-1.3.24-1.el9.x86_64.rpm \
https://dl.rockylinux.org/stg/rocky/9/CRB/x86_64/kickstart/Packages/p/python3-hivex-1.3.24-1.el9.x86_64.rpm
Step 4. Edit /tmp/convert-VM file
vi /tmp/convert-VMIMPORTANT
Make sure to copy and paste the exact lines below.#!/bin/bash # ================================================================ # vmdk2img.sh — VMDK → qcow2 Migration for SynetoOS / Hyperion # ================================================================ 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" # 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="" 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|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]" \ "windesktop" "Windows Desktop XP / 7 / 10 [BIOS]" \ "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";; 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" ;; desktop) os_label="Windows Desktop XP/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 case "$OS_SUBTYPE" in 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" 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 NOTE: i440fx is NOT supported on SynetoOS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Default: pc-q35-rhel9.4.0" \ "q35_94" "pc-q35-rhel9.4.0 (default, recommended)" \ "q35_92" "pc-q35-rhel9.2.0 (previous version)" \ "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" ;; 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 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" ]]; 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 local miss=() python3 -c "import hivex" &>/dev/null 2>&1 || miss+=("python3-hivex") command -v ntfs-3g &>/dev/null || miss+=("ntfs-3g") if [[ ${#miss[@]} -gt 0 ]]; then tui_msg "Missing Dependencies" \ "Missing packages: ${miss[*]} Install with: dnf install -y ${miss[*]} Then re-apply patch manually." return 0 fi log "Applying Windows boot patch..." } # ================================================================ # 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..." # ---------------------------------------------------------------- # 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" 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 # sata: main=sda, extras=sdb sdc..., cdrom=after extras local sata_idx=1 for extra_img in "${EXTRA_IMG_LIST[@]}"; do local edev="sd${_ALPHA[$sata_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='sata'/> <serial>${VM_NAME}_$(( sata_idx + 1 ))</serial> </disk>" sata_idx=$(( sata_idx + 1 )) done CDROM_DEV="sd${_ALPHA[$sata_idx]}" 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='sata'/> <readonly/> </disk>" else CDROM_XML=" <disk type='file' device='cdrom'> <driver name='qemu' type='raw'/> <target dev='${CDROM_DEV}' bus='sata'/> <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>" # ---------------------------------------------------------------- # 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 mode='host-model' check='partial'> <topology sockets='1' dies='1' clusters='1' cores='${VCPUS}' threads='1'/> </cpu> <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='qemu-xhci' ports='15'/> <controller type='sata' index='0'/> <interface type='network'> <source network='${NET_NAME}'/> <model type='virtio'/> <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='itco' 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" 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_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

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

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-winISO 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”)