After conversion completes, the user sees a form with VM Name and ID
prefilled, and three buttons:
- Clone (instant): qm clone --full via backend, takes seconds
- Copy (re-convert): shows CPU/RAM/Disk fields, re-runs full pipeline
- Clean Up & Finish: deletes staging, returns to start page
Backend: new /api/v1/clone endpoint → provisioner.clone_vm()
Frontend: new /session/clone proxy, api_client.clone_vm()
1. BIOS: provisioner now calls detect_efi() on the first disk when
auto_detect_boot=true. Detects EFI partition via guestfish.
Falls back to user's boot_type if detection inconclusive.
2. Reuse: 'Create Another VM' now shows an inline form asking only
for VM Name and ID, reusing the same disk image, format, and
boot settings from the completed job. No redirect needed.
Previously always showed indeterminate spinner (pct=0) because
total size was unknown. Now if the HEAD request returns
Content-Length, the progress bar fills proportionally.
The 10s HEAD request for Content-Length blocked the download endpoint
response. nginx proxy in front (bench.srv2.sechpoint.app:443 → :5000)
timed out waiting, returned empty response → 'data is null' JS error.
Now wget starts immediately, response returns instantly, and HEAD
runs in a daemon thread to populate content_length for ETA later.
The fill.style.width = pct + '%' line was lost during a previous edit,
so the progress bar element was found but never visually updated.
Events fired, label updated, but bar stayed at 0%.
Starlette's multipart parser (pure Python) is too slow for >1 GB uploads —
boundary scanning over gigabytes blocks the event loop indefinitely.
New /session/upload-raw endpoint:
- Receives raw binary body via request.stream() — no parsing overhead
- Metadata (filename, vmid, vm_name) passed in HTTP headers
- Async chunked write directly to staging — true zero-copy streaming
- Same progress logging as before
Frontend now sends File object directly via xhr.send(file)
instead of FormData — eliminates multipart encoding on the client too.
New /scp page with form for host, user, password, remote path.
Runs scp via sshpass in background with progress polling.
Files go to /mnt/converter/in/{session_id}/ for multi-user isolation.
- Added sshpass + openssh-client to frontend installer
- scp.html template with full form + progress bar
- POST /scp/start — launches background SCP via sshpass -e
- GET /scp/progress/{sid}/{file} — polls file size + speed
- 'SCP Pull (+10 GB)' button on start page
innerHTML reset in startSession was destroying #phase-label and
#progress-fill elements, then recreating them with same IDs. XHR
progress events fired asynchronously but getElementById might
return stale references or fail during the DOM update window.
Now #phase-label, #progress-fill, and #session-error are permanent
elements in the static HTML. startSession clears them via
textContent/style instead of innerHTML — no DOM destruction.
showError appends to #session-error instead of #session-status.
- HTTP middleware logs every incoming request (method, path, body size)
BEFORE handler runs — catches silent failures at the network/parser layer
- Changed upload handler from async to sync so FastAPI runs it
in a thread pool, preventing event loop blockage during large
file reads
Added loadstart handler to show 'Uploading...' spinner as soon as
upload begins, plus fallback label when lengthComputable is false
(shows bytes uploaded even without percentage).
Uploads already logged start, every-1-GiB progress, and completion.
Downloads only logged start — now also log:
- Completion with file size (Download complete: file.7z (1.9 GiB))
- Failure with wget exit code
- Periodic progress every ~1 GiB with speed and ETA
qemu-img convert is the longest step but previously had no progress
updates — bar stuck at 10% until conversion completed.
Now _convert_with_progress() runs qemu-img in background and polls
output file size every 2s, updating progress with GiB done/total.
Progress flow for single disk:
5% queued → 10% starting → 10-55% converting (real-time)
→ 60% shrinking → 70% creating VM → 75-85% importing
→ 95% configuring → 100% done
The job submission receives the original filename (e.g. Debian_13_VMG.7z)
but qemu-img convert needs the actual VMDK path inside the archive.
Now reuse extract_if_needed() + discover_disk() from converter module
to locate the real disk image before conversion.
Replace stub provisioner with real Proxmox integration:
- qemu-img convert: source → QCOW2 in /mnt/converter/out/{vmid}/
- virt-resize --shrink --resize-force: auto-shrink >30 GiB disks
- qm create: VM with cores, RAM, network, SCSI controller
- qm importdisk: import each QCOW2 into Proxmox storage
- qm set: attach disks, configure boot (OVMF/SeaBIOS), EFI disk, serial
- Auto-destroy existing VM with same ID before recreating
- Background thread execution with in-memory status tracking
- Full logging throughout the conversion pipeline
Archives like 64bit.7z that already contain a 64bit/ top-level dir
were extracted into STAGING_IN/64bit/, creating 64bit/64bit/ nesting.
Now extract into STAGING_IN root, then:
- If the archive produced a single dir matching the stem, use it
- Otherwise, collect scattered files into a stem-named subdirectory
Removes the need for nested path: 64bit/Debian.vmdk instead of
64bit/64bit/Debian.vmdk
Both frontend and backend now write rotating log files to
/mnt/converter/logs/ (shared between host and LXC):
/mnt/converter/logs/vm-bench.log (frontend)
/mnt/converter/logs/vm-bench-backend.log (backend)
- RotatingFileHandler: 10 MB per file, 5 backups
- Console handler still writes to systemd journal
- Logs/ directory is gitignored and auto-created on startup
- Install script creates logs/ directory
Backend had zero logging. Frontend logs were fine (logging.basicConfig
in app.py). Now both services write structured logs to systemd journal:
journalctl -u vm-bench -f # frontend (LXC)
journalctl -u vm-bench-backend -f # backend (srv2)
Root cause: innerHTML does not execute <script> tags in modern browsers.
The _analysis.html and polling.html fragments had inline <script> blocks
that were silently dropped, causing:
- Confirm form's submitJob() undefined → form submitted as normal GET
back to '/' (user saw 'New Conversion Session' instead of polling)
- Polling page's poll() never started → stuck on 'Waiting for backend...'
Fix:
- Stripped all <script> tags from _analysis.html and polling.html
- Moved all JS logic into index.html (executed on page load)
- initConfirmForm() attaches submit/change listeners after analysis HTML
is injected via innerHTML
- initPolling() attaches poll timer + reuse button handlers after
polling fragment is injected
- Polling fragment replaces step1/analysis content while keeping
base.html header/footer intact
- Reuse buttons now use onclick attached via JS (not inline)
guestfish needs more than 30s to probe a 7.5 GiB VMDK image
(mount filesystem, read /etc/os-release, check EFI partition).
Other endpoints keep the 30s default.
{job_id} in f-strings was interpreted as Python variable instead
of FastAPI path parameter. Double braces {{job_id}} produce
literal {job_id} in the route string.
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)