Add ova2vzdump: OVA -> Proxmox vzdump (.vma.zst) converter

Dockerized converter that turns VirtualBox/VMware OVA appliances into
native Proxmox vzdump backups restorable via qmrestore, without needing
a PVE host.

- OVF parser (CPU/RAM/disks/NICs, SCSI/SATA/IDE controllers)
- Streaming OVA extractor
- qemu-server.conf generator with required #qmdump#map lines
- Orchestrator: qemu-img convert -> vma create -> zstd
- Click CLI: convert, create-test-ova, gui subcommands
- Flask web UI with Convert + Create-test-OVA tabs, SSE progress
- Dockerfile: Debian bookworm + pve-no-subscription for vma/qemu-img,
  pre-caches Alpine tiny raw image (sha512-verified) for bootable
  test-OVA generation
- End-to-end verified: generated OVA imports into VirtualBox, converted
  vzdump restores on Proxmox and the VM boots

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-21 12:50:04 +02:00
parent 5fe65d6b44
commit dcbc957334
17 changed files with 1761 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Thin CLI wrapper so folks can run the test-OVA builder without having
the package installed in their active env. Prefer `ova2vzdump create-test-ova`
when possible."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from ova2vzdump.test_ova import create_test_ova # noqa: E402
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("output", type=Path)
ap.add_argument("--name", default="test-vm")
ap.add_argument("--size-mib", type=int, default=64)
ap.add_argument("--bootable", action="store_true",
help="Package pre-cached bootable Alpine image")
ap.add_argument("--force", action="store_true",
help="Overwrite existing output file")
args = ap.parse_args()
r = create_test_ova(
output=args.output, vm_name=args.name, size_mib=args.size_mib,
bootable=args.bootable, force=args.force,
)
if r.reused:
print(f"reused existing {r.path} ({r.bytes} bytes) "
"(pass --force to rebuild)")
else:
kind = "bootable Alpine" if r.bootable else "empty stub"
print(f"wrote {r.path} ({r.bytes} bytes, {kind})")
if __name__ == "__main__":
main()