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
This commit is contained in:
Claus Lohmar 2026-07-21 19:14:32 +00:00
parent affcde7afb
commit 9a6d2df78b

View file

@ -145,6 +145,43 @@ def _run(cmd: list[str], check: bool = True, timeout: int = 3600) -> subprocess.
return result
def _convert_with_progress(
job_id: str, source_path: Path, output_path: Path, format: str,
start_pct: int, end_pct: int,
):
"""Run qemu-img convert in background and update job progress
by polling the output file size against the source virtual size."""
# Get source virtual size for percentage estimation
info = _run(["qemu-img", "info", "--output=json", str(source_path)])
source_size = json.loads(info.stdout).get("virtual-size", 0)
if source_size == 0:
source_size = source_path.stat().st_size
proc = subprocess.Popen(
["qemu-img", "convert", "-f", format, "-O", "qcow2",
str(source_path), str(output_path)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
# Poll output file size and update progress
while proc.poll() is None:
time.sleep(2)
if output_path.exists():
current = output_path.stat().st_size
if source_size > 0:
ratio = min(current / source_size, 1.0)
pct = start_pct + int(ratio * (end_pct - start_pct))
gb_done = current / (1024**3)
gb_total = source_size / (1024**3)
_update_job(job_id, progress=pct,
message=f"Converting disk... {gb_done:.1f} / {gb_total:.1f} GiB")
if proc.returncode != 0:
raise RuntimeError(f"qemu-img convert failed (rc={proc.returncode})")
_update_job(job_id, progress=end_pct, message="Disk converted.")
def _process_job(job_id: str, req: JobSubmissionRequest):
try:
vmid = req.vmid
@ -171,13 +208,11 @@ def _process_job(job_id: str, req: JobSubmissionRequest):
logger.info("Disk %d: source=%s format=%s size=%.1f GiB",
idx, source_path, disk.format, disk.size_gb)
_run(["qemu-img", "convert",
"-f", disk.format,
"-O", "qcow2",
str(source_path), str(output_path)],
timeout=7200)
_update_job(job_id, progress=20 + (idx + 1) * 15,
message=f"Converted disk {idx + 1}/{len(disk_specs)}")
# Convert with real-time progress
_convert_with_progress(
job_id, source_path, output_path, disk.format,
start_pct=10 + idx * 10, end_pct=55 + idx * 10,
)
else:
size_gb = spec.size_gb or 32
logger.info("Creating empty disk %d: %d GiB", idx, size_gb)
@ -204,7 +239,7 @@ def _process_job(job_id: str, req: JobSubmissionRequest):
target_gb = int(round(current_gb)) + 1
if target_gb < current_gb:
_update_job(job_id, progress=65,
_update_job(job_id, progress=60,
message=f"Shrinking disk to {target_gb} GiB...")
logger.info("Shrinking disk: %.1f GiB → %d GiB via virt-resize", current_gb, target_gb)
try:
@ -276,6 +311,7 @@ def _process_job(job_id: str, req: JobSubmissionRequest):
f"{req.target_storage}:0,format=raw,size=4M"])
# ── Finalise ───────────────────────────────────────────────
_update_job(job_id, progress=95, message="Configuring VM...")
_run(["qm", "set", str(vmid), "--boot", "order=scsi0"])
_run(["qm", "set", str(vmid), "--serial0", "socket"])