Large-file handling: - Set TMPDIR=/mnt/converter/in in service to spool uploads to shared storage instead of 24 GB LXC rootfs (critical for >24GB) - Chunked upload streaming (8 MiB) with progress logging every 1 GiB - Pre-flight disk space check via Content-Length header - Clean up partial files on upload/download failure - Download timeout extended to 7200s (2 hours) for 88 GB images - Switched wget from --show-progress to --progress=dot:giga (compact output, won't fill memory on large transfers) - uvicorn --timeout-keep-alive 300 on both frontend and backend VM name: - Added vm_name field to initial session form (step 1) - Falls back to auto-generated 'os_type-vmid' if left blank - Pre-filled & editable in confirm form (step 2) |
||
|---|---|---|
| backend | ||
| frontend | ||
| .gitignore | ||
| open-api.yaml | ||
| README.md | ||
VM Bench — Proxmox Image Conversion Frontend
Web GUI and REST API for converting VMware disk images (VMDK, VHD, etc.) into Proxmox-compatible QCOW2 disks and provisioning VMs with automatic OS and boot type detection.
Architecture
Browser vm-bench LXC Proxmox Host (srv2)
(this container)
──→ :5000 ──→ frontend/app.py ──→ :9000 backend/app.py
│
├─ qemu-img (format, size)
├─ guestfish (OS, EFI detection)
├─ 7z/unzip (archive extraction)
└─ qm create (VM provisioning)
- Frontend (
vm-benchLXC) — FastAPI + Jinja2 web UI on port 5000 - Backend (
srv2Proxmox host) — FastAPI REST API on port 9000 - Shared storage —
/mnt/converter/in(staging) and/mnt/converter/out(output)
Directory Layout
/mnt/converter/
├── frontend/ # Web UI (runs on vm-bench LXC)
│ ├── app.py # FastAPI app, routes, session handling
│ ├── api_client.py # Typed REST client → backend
│ ├── install.sh # Frontend installer (system deps + service)
│ ├── vm-bench.service # Systemd unit for the frontend
│ ├── requirements.txt
│ ├── templates/ # Jinja2 HTML templates
│ │ ├── base.html
│ │ ├── index.html # New session form
│ │ ├── _analysis.html # Analysis result + confirm form
│ │ └── polling.html # Progress bar + job status
│ └── static/
│ └── proxmox.css
├── backend/ # REST API (deploys to Proxmox host srv2)
│ ├── app.py # FastAPI app, routes
│ ├── models.py # Pydantic request/response models
│ ├── converter.py # Archive extraction, disk probing, OS/EFI detection
│ ├── provisioner.py # VM provisioning (qm create/importdisk)
│ ├── install.sh # Full deployment script (backend + LXC creation)
│ ├── vm-bench-backend.service
│ └── requirements.txt
├── open-api.yaml # API spec (single source of truth)
├── in/ # Shared staging directory (gitignored)
└── out/ # Shared output directory (gitignored)
Features
- File upload or URL download — ingest
.vmdk,.vhd,.7z,.zip,.tar.gzarchives - Automatic archive extraction — 7z, unzip, tar, gunzip
- Disk analysis — detects format, virtual size, guest OS, EFI bootability
- VM provisioning — creates Proxmox VM with automatic disk import and boot config
- Progress polling — real-time job status with progress bar
- Staging reuse — keep source files to create multiple VMs from one image
Prerequisites
Proxmox host (srv2)
The host must be a Proxmox VE node with:
pct— LXC container management (built-in)git— to clone the repository (install script handles this)- Internet access to download container templates
LXC container (vm-bench)
Created automatically by the install script. Requires:
- 3 CPU cores, 6 GB RAM, 24 GB disk, 2 GB swap
- Debian 12 template (downloaded by script)
- Network bridge
vmbr0
Deployment
All deployment is driven by a single script on the Proxmox host. It creates the
directory tree, clones the repo, installs the backend, and provisions the
vm-bench LXC container ready for the frontend.
Step 1: Clone & deploy (on srv2)
# Create the shared directory and clone the repo
mkdir -p /mnt/converter
cd /mnt/converter
git clone https://git.lohmar.co.uk/cclohmar/vm-bench.git .
# Run the installer — handles everything interactively
bash backend/install.sh
The script will:
- Create the
/mnt/converter/directory tree - Clone or update the git repository
- Install system packages (qemu-img, guestfish, archive tools, etc.)
- Install Python dependencies and start the backend systemd service
- Prompt for container ID + network settings
- Create and configure the
vm-benchLXC container with:- 3 cores, 6 GB RAM, 24 GB rootfs, 2 GB swap
- Bind mount:
/mnt/convertershared with host - Unprivileged mode off, nesting enabled
- AppArmor unconfined, cgroup device access allowed
- Optionally start the container
Example prompts:
Container ID (e.g. 20020): 20020
IP address/CIDR [10.2.0.20/16]:
Gateway [10.2.0.2]:
Bridge [vmbr0]:
MAC address [BC:24:11:80:5A:B1]:
Verify the backend is running:
curl http://127.0.0.1:9000/api/v1/health
# → {"status":"ok"}
Step 2: Install frontend (inside the LXC)
After the container is created and started, enter it:
pct enter 20020 # use your container ID
Inside the container, run the frontend installer:
bash /mnt/converter/frontend/install.sh
This installs Python, pip, wget, curl, Python dependencies, and starts the
vm-bench systemd service on port 5000.
Verify:
curl http://127.0.0.1:5000
# → HTML response (the web UI)
Open http://<container-ip>:5000 in a browser.
Container LXC Config (created by install script)
| Setting | Value |
|---|---|
| Arch | amd64 |
| Cores | 3 |
| Memory | 6144 MB |
| Swap | 2048 MB |
| Rootfs | local-lvm:24G |
| OS type | debian |
| Unprivileged | 0 (disabled) |
| Features | nesting=1 |
| Network | vmbr0, firewall enabled, veth |
| Bind mount | mp0: /mnt/converter,mp=/mnt/converter |
| AppArmor | unconfined |
| Cgroup | devices.allow: a, mount.auto: proc:rw sys:rw cgroup:rw |
Re-deploying / updating
The install scripts are idempotent — safe to re-run:
# On srv2: update backend
cd /mnt/converter && git pull
bash backend/install.sh # skips LXC creation if container exists
# Inside LXC: update frontend
cd /mnt/converter && git pull
bash frontend/install.sh
API Reference
All endpoints are under /api/v1/. Full specification in open-api.yaml.
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/health |
Health check |
POST |
/api/v1/analyze |
Probe source image (format, size, OS, EFI) |
POST |
/api/v1/jobs |
Submit conversion + provisioning job |
GET |
/api/v1/jobs/{id} |
Poll job status and progress |
POST |
/api/v1/jobs/{id}/cleanup |
Delete or preserve staging files |
Example: Analyze a source image
curl -X POST http://10.2.0.2:9000/api/v1/analyze \
-H "Content-Type: application/json" \
-d '{"vmid": 21050, "source_filename": "64bit/Debian 12.11.0 (64bit).vmdk"}'
# Response:
# {
# "vmid": 21050,
# "filename": "Debian 12.11.0 (64bit).vmdk",
# "disk_format": "vmdk",
# "disk_size_gb": 7.5,
# "os_type": "Debian",
# "efi_detectable": true
# }
Example: Submit a conversion job
curl -X POST http://10.2.0.2:9000/api/v1/jobs \
-H "Content-Type: application/json" \
-d '{
"vmid": 21050,
"vm_name": "debian-test",
"boot_disk": {
"disk_type": "image_file",
"source_filename": "64bit/Debian 12.11.0 (64bit).vmdk",
"format": "vmdk"
},
"cpu_cores": 2,
"ram_mb": 4096,
"target_storage": "local-lvm"
}'
# Response:
# { "job_id": "job_21050_1737480000", "vmid": 21050, "status": "queued", ... }
Usage Flow
- Open the web UI → new session form
- Upload or download a source image → file lands in
/mnt/converter/in/ - Backend analyses the image → shows format, size, OS, EFI status
- Configure VM settings → name, CPU, RAM, storage, boot type
- Submit job → backend converts, shrinks (if needed), creates VM
- Poll progress → real-time status bar updates every 2 seconds
- Reuse or cleanup → create another VM from same source, or delete staging files
Configuration
| Variable | Default | Description |
|---|---|---|
BACKEND_URL |
http://10.2.0.2:9000 |
Backend API base URL (set in systemd unit) |
VM_ID_MIN |
21000 |
Minimum allowed Proxmox VM ID |
VM_ID_MAX |
21100 |
Maximum allowed Proxmox VM ID |
Development
Source of truth: open-api.yaml — always derive models from this file.
Backend tool requirements (on srv2):
qemu-img— disk format and size detectionguestfish/virt-inspector— OS type and EFI boot detection7z,unzip,tar,gunzip,bunzip2,xz— archive extraction
Provisioner note: backend/provisioner.py is currently a stub with simulated
progress. Replace with real qm create, qm importdisk, and qm set commands
when deploying to the Proxmox host.
Staging files: /mnt/converter/in/ and /mnt/converter/out/ are gitignored —
they contain user data, not code.