Commit graph

57 commits

Author SHA1 Message Date
90bf8e4842 fix: remove unused out_dir from submit_job + Copy flow uses legacy fallback
1. submit_job() was creating empty tmp/out/{vmid}/ dirs via the old
   flat STAGING_OUT path. Removed — _process_job handles this via
   _staging_out() in the session-aware path.

2. Copy (re-convert) flow was hardcoding boot_type=uefi. Now uses
   auto_detect_boot=true with boot_type=legacy as fallback, matching
   the behavior of the original analyze→convert flow.
2026-07-23 10:52:20 +00:00
6617aca341 fix: purge all old staging path references — full code review cleanup
Bugs found and fixed:
- converter.py: STAGING_IN.iterdir() → extract_base.iterdir()
  (checked wrong dir when source was in session subdir)
- vm-bench.service: TMPDIR=/mnt/converter/in → /mnt/converter/tmp
  (old path poisoned all temp file writes)
- backend/app.py: removed unused STAGING_IN constant
- converter.py: removed unused STAGING_OUT constant
- install.sh: removed creation of old /in and /out dirs
2026-07-23 10:36:03 +00:00
1b145adea7 fix: extraction uses file's parent dir as base, not hardcoded STAGING_IN
extract_if_needed now extracts relative to the source file's parent
directory instead of /mnt/converter/tmp/in/. This correctly handles
files in tmp/{guid}/in/ paths.
2026-07-23 10:19:43 +00:00
7a024ca166 fix: correct staging path resolution — tmp/{guid}/in not tmp/in/{guid}
Frontend now passes '{guid}/in/filename' to backend.
Backend extract_if_needed uses STAGING_ROOT (tmp/) as base.
Provisioner staging helpers build tmp/{guid}/in + tmp/{guid}/out.
2026-07-23 10:14:19 +00:00
9dcde5d022 fix: flatten nested single-child directories after extraction
Archives like Debian_13_VMG.7z contain nested single-child dirs:
  Debian_13_VMG_LinuxVMImages.COM/Debian_13_VMG_LinuxVMImages.COM/

_flatten_nested() collapses these chains until files sit directly
in the parent, eliminating the double-nesting issue.
2026-07-23 10:09:25 +00:00
a28e2af674 refactor: move staging to /mnt/converter/tmp/{guid}/in + out 2026-07-23 10:07:57 +00:00
fc380f7d08 fix: add session_id to copyVM payload (Copy flow was missing it) 2026-07-23 09:51:30 +00:00
bc20691390 fix: prepend session GUID to filename when calling backend analyze
When files are stored in /mnt/converter/in/{guid}/, the backend
needs the full relative path. The analyze endpoint now constructs
'{guid}/{filename}' before passing to the backend API.
2026-07-23 09:46:28 +00:00
e53d05f7dc fix: add missing _staging_in helper + STAGING_IN constant 2026-07-23 09:36:50 +00:00
63659e4f93 feat: session GUID isolation for multi-user support
Each browser gets a UUID stored in localStorage, set as a cookie,
and attached to every API call. Files go to session-specific dirs:

  /mnt/converter/in/{guid}/    — uploads, downloads, SCP pulls
  /mnt/converter/out/{guid}/   — converted QCOW2s

Changes:
- base.html: generates UUID via crypto.randomUUID(), stores in
  localStorage + cookie, exposes as window.VM_BENCH_SID
- Frontend: all endpoints accept session_id, _staging() helper
  creates session-aware paths on demand
- JavaScript: session_id appended to all FormData, set as
  X-Session-ID header on raw uploads
- Backend models: JobSubmissionRequest.session_id field added
- Provisioner: _staging_in/_staging_out helpers, source path
  resolution uses session-aware directory
- Converter: extract_if_needed skips re-extraction if dir exists
2026-07-23 09:32:16 +00:00
7ded831354 fix: skip re-extraction if source directory already exists
When reusing a source image (Copy flow), the archive was already
extracted in /mnt/converter/in/. The extraction step failed with
'Destination path already exists' from 7z.

Now extract_if_needed checks if the expected output directory already
contains disk images, and returns it directly without re-extracting.
2026-07-23 09:28:02 +00:00
111d0d1331 chore: remove debug console.log from toggleSourceInput 2026-07-23 09:22:06 +00:00
fd4f8744d2 style: light Proxmox VE theme — white panels, blue header, light badges
Rewrote to match actual Proxmox VE light interface:
- #f0f0f0 page background, white panels
- Dark blue header (#1b3a5c) with white text
- Light gray panel headers (#f8f8f8)
- Bootstrap-style alert colors (yellow/blue/green/red)
- Orange accent throughout
2026-07-23 09:21:07 +00:00
272feefd36 debug: add console.log to toggleSourceInput 2026-07-23 09:19:36 +00:00
b2dbfe81f9 fix: custom select dropdown arrow visible on dark background
Native browser select arrows don't adapt to dark backgrounds.
Added appearance:none with a light SVG chevron via background-image.
2026-07-23 09:17:42 +00:00
4bebbd9da9 fix: add cache-busting ?v=2 to CSS link (nginx was caching old stylesheet) 2026-07-23 09:11:28 +00:00
844d86c105 style: Proxmox VE-inspired dark theme — pve-* class naming
Rewrote proxmox.css with consistent pve- prefix matching the Proxmox
VE design language: panels with orange-accent headers, clean tables,
orange progress bars, green/red/orange badges, subtle shadows.

Updated all templates (base, index, _analysis, polling, scp)
to use pve-btn, pve-input, pve-select, pve-panel, pve-progress,
pve-badge, pve-alert, pve-spinner, pve-hidden, pve-dim, etc.
2026-07-23 09:09:29 +00:00
492924f346 fix: remove dangling } from regex cleanup of submitReuse 2026-07-23 09:05:11 +00:00
9eb0f15261 feat: Clone (qm clone) + Copy (re-convert) buttons on completion
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()
2026-07-23 09:02:57 +00:00
7e1303a217 fix: auto-detect BIOS boot type + simplified reuse form
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.
2026-07-23 08:48:31 +00:00
80c5e2ceda fix: sanitize VM name + use qm disk import (newer Proxmox syntax)
- VM name now sanitized: spaces→hyphens, lowercase, strip invalid chars
- qm importdisk → qm disk import (newer Proxmox VE command)
2026-07-23 08:25:46 +00:00
334883dd26 fix: show download progress bar percentage when content_length is known
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.
2026-07-23 08:12:21 +00:00
50e969f95e fix: restore download logic to session_upload — was embedded in upload_raw 2026-07-22 12:25:38 +00:00
adfb68fe6b fix: move HEAD request out of critical path — non-blocking background thread
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.
2026-07-22 12:16:45 +00:00
c31ad3409a fix: restore missing progress bar width update in setPhase + cleanup debug logs
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%.
2026-07-22 11:49:08 +00:00
0f69e725a5 debug: add progress/loadstart console.logs 2026-07-22 09:16:12 +00:00
4428d91feb debug: add console.log to handleUpload entry point 2026-07-22 09:10:54 +00:00
f3477dc23b fix: raw streaming upload endpoint — bypasses multipart parser for large files
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.
2026-07-22 09:02:05 +00:00
837e674195 feat: SCP Pull page for large files from remote servers
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
2026-07-22 05:52:01 +00:00
1183059d8b fix: add no-cache headers + remove debug console.logs
- Cache-Control/Pragma/Expires headers prevent browser caching
  of JS-heavy pages (fixes stale code after updates)
- Removed debug console.log statements from upload handlers
2026-07-22 05:24:21 +00:00
6519fc1cf7 fix: use permanent DOM elements for progress instead of innerHTML reset
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.
2026-07-22 05:19:13 +00:00
d8d301a13f debug: add request-logging middleware + make upload handler sync
- 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
2026-07-22 05:12:21 +00:00
f032c546e1 debug: add console.log to upload handlers to diagnose missing progress 2026-07-21 19:46:08 +00:00
5ff0635aa5 fix: show upload phase immediately + fallback for non-computable progress
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).
2026-07-21 19:43:04 +00:00
5174939939 fix: add download progress/completion logging to frontend
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
2026-07-21 19:40:03 +00:00
6ba7944bb2 fix: add unrar-free, use 7z for .zip extraction (more reliable)
- Added unrar-free package to backend installer
- Added .rar to archive extension detection
- Changed .zip extraction from unzip → 7z (x -y)
  7z handles .zip, .7z, and other formats with one tool
- Added unrar handler for .rar files
2026-07-21 19:37:27 +00:00
5e786533b1 fix: add /usr/sbin to PATH in service files (qm, virt-resize, etc.)
Proxmox tools (qm, pct) and libguestfs tools (virt-resize,
guestfish) live in /usr/sbin which wasn't in the service PATH.
2026-07-21 19:24:13 +00:00
9a6d2df78b fix: real-time progress during qemu-img convert, smoother percentages
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
2026-07-21 19:14:32 +00:00
affcde7afb fix: provisioner now extracts archives and discovers disks before conversion
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.
2026-07-21 19:11:19 +00:00
56131d6346 feat: real Proxmox VM provisioning — qemu-img, virt-resize, qm commands
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
2026-07-21 19:05:07 +00:00
7808b16946 fix: prevent double-nested directory on archive extraction
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
2026-07-21 18:58:13 +00:00
0e7f8c81f3 feat: shared file-based logging to /mnt/converter/logs/
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
2026-07-21 18:52:58 +00:00
42d3cdf1fe fix: add logging to backend — analyze, job submit, status, cleanup
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)
2026-07-21 18:46:12 +00:00
989807f29e fix: innerHTML script execution bug — confirm form and polling now work
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)
2026-07-21 18:43:16 +00:00
267b64ec1b docs: explain disk sizing, virtual vs actual size, and auto-shrink behavior 2026-07-21 18:35:15 +00:00
ebe175afcf fix: bump analyze timeout to 300s for 87 GB+ disk images 2026-07-21 18:26:57 +00:00
71792756ea fix: increase analyze API timeout to 120s for large disk probing
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.
2026-07-21 18:24:26 +00:00
10199f896b fix: escape f-string braces in backend route decorators
{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.
2026-07-21 18:17:41 +00:00
af26da7b64 feat: two-phase upload/download with real-time progress + smart speed timeout
Split monolithic /session/start into two endpoints:
- POST /session/upload  — phase 1: file acquisition only, returns JSON
- POST /session/analyze — phase 2: backend analysis, returns HTML
- GET  /session/progress/{filename} — poll download progress

Uploads:
- Browser-native progress bar via XMLHttpRequest (real % + GiB)

Downloads:
- wget runs in background (Popen), frontend polls /session/progress
- HEAD request gets Content-Length before download starts
- Real-time speed (MB/s) and ETA displayed in the UI
- Smart timeout: after 30s, if ETA > 1 hour, kills download and
  suggests manual download to laptop + File Upload instead
- Absolute safety net at 4 hours

UI: clear phase transitions — 'Downloading... 2.3 GiB (4.5 MB/s) ~12 min'
→ 'Step 2/2: Analysing source image...' → result
2026-07-21 18:10:52 +00:00
509eb97b57 fix: large-file support + user-defined VM name
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)
2026-07-21 17:59:31 +00:00