#!/usr/bin/env bash # qcow2-to-ova.sh — Convert a QEMU qcow2 disk image to a VMware-compatible OVA # without requiring ovftool. # # Usage: qcow2-to-ova.sh # # Example: # qcow2-to-ova.sh ../output/qemu/zroc-appliance-1.0.0 \ # ../output/zroc-appliance-1.0.0-ubuntu-24.04-amd64.ova \ # zroc-appliance 1.0.0 set -euo pipefail QEMU_VM_PATH="$1" # path to the qcow2 file (without extension) or directory OVA_OUT="$2" # destination .ova file VM_NAME="$3" # display name inside the OVF VM_VERSION="$4" # version string # ── Locate the qcow2 ────────────────────────────────────────────────────────── if [[ -f "${QEMU_VM_PATH}.qcow2" ]]; then QCOW2="${QEMU_VM_PATH}.qcow2" elif [[ -d "$QEMU_VM_PATH" ]]; then QCOW2=$(find "$QEMU_VM_PATH" -name "*.qcow2" | head -1) else # Packer QEMU plugin writes (no extension) as the output file QCOW2="$QEMU_VM_PATH" fi if [[ -z "$QCOW2" || ! -f "$QCOW2" ]]; then echo "ERROR: could not find qcow2 image at ${QEMU_VM_PATH}" >&2 exit 1 fi echo "==> [qcow2-to-ova] Source qcow2: $QCOW2" echo "==> [qcow2-to-ova] Output OVA: $OVA_OUT" WORK_DIR=$(mktemp -d) trap 'rm -rf "$WORK_DIR"' EXIT VMDK_NAME="${VM_NAME}-disk1.vmdk" OVF_NAME="${VM_NAME}.ovf" MF_NAME="${VM_NAME}.mf" # ── 1. Convert qcow2 → stream-optimised VMDK ───────────────────────────────── echo "==> [qcow2-to-ova] Converting qcow2 → VMDK (stream-optimised)…" qemu-img convert \ -f qcow2 \ -O vmdk \ -o subformat=streamOptimized,adapter_type=lsilogic,compat6 \ "$QCOW2" \ "${WORK_DIR}/${VMDK_NAME}" DISK_SIZE_BYTES=$(qemu-img info --output=json "${WORK_DIR}/${VMDK_NAME}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['virtual-size'])") DISK_SIZE_GB=$(( (DISK_SIZE_BYTES + 1073741823) / 1073741824 )) DISK_FILE_BYTES=$(stat -c%s "${WORK_DIR}/${VMDK_NAME}") echo "==> [qcow2-to-ova] VMDK: virtual=${DISK_SIZE_GB}GB, file=${DISK_FILE_BYTES} bytes" # ── 2. Generate OVF descriptor ──────────────────────────────────────────────── echo "==> [qcow2-to-ova] Generating OVF descriptor…" cat > "${WORK_DIR}/${OVF_NAME}" << OVFEOF Virtual disk information The list of logical networks VM Network zROC Observability Console Appliance v${VM_VERSION} ${VM_NAME} A human-readable annotation zROC Appliance v${VM_VERSION} — https://github.com/recklessop/zroc The kind of installed guest operating system Ubuntu Linux (64-bit) Virtual hardware requirements Virtual Hardware Family 0 ${VM_NAME} vmx-19 hertz * 10^6 Number of virtual CPUs 4 virtual CPU(s) 1 3 4 byte * 2^20 Memory Size 8192 MB of memory 2 4 8192 0 SCSI Controller SCSI Controller 0 3 lsilogic 6 0 Hard Disk 1 ovf:/disk/vmdisk1 4 3 17 7 true VM Network VmxNet3 ethernet adapter Network Adapter 1 5 VmxNet3 10 OVFEOF # ── 3. Generate manifest (.mf) with SHA256 checksums ───────────────────────── echo "==> [qcow2-to-ova] Generating manifest…" OVF_SHA=$(sha256sum "${WORK_DIR}/${OVF_NAME}" | awk '{print $1}') VMDK_SHA=$(sha256sum "${WORK_DIR}/${VMDK_NAME}" | awk '{print $1}') cat > "${WORK_DIR}/${MF_NAME}" << MFEOF SHA256(${OVF_NAME})= ${OVF_SHA} SHA256(${VMDK_NAME})= ${VMDK_SHA} MFEOF # ── 4. Package as OVA (tar, OVF first per spec) ─────────────────────────────── echo "==> [qcow2-to-ova] Packaging OVA…" mkdir -p "$(dirname "$OVA_OUT")" tar -C "$WORK_DIR" \ --format=ustar \ -cf "$OVA_OUT" \ "${OVF_NAME}" \ "${VMDK_NAME}" \ "${MF_NAME}" OVA_SIZE=$(du -sh "$OVA_OUT" | cut -f1) OVA_SHA=$(sha256sum "$OVA_OUT" | awk '{print $1}') echo "==> [qcow2-to-ova] OVA complete: $OVA_OUT ($OVA_SIZE)" echo "==> [qcow2-to-ova] SHA256: $OVA_SHA" echo "$OVA_SHA $(basename "$OVA_OUT")" > "${OVA_OUT}.sha256" echo "==> [qcow2-to-ova] Done"