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.
This commit is contained in:
Claus Lohmar 2026-07-23 08:48:31 +00:00
parent 80c5e2ceda
commit 7e1303a217
3 changed files with 90 additions and 4 deletions

View file

@ -23,8 +23,9 @@ from models import (
JobStatusResponse,
JobStatus,
DiskType,
BootType,
)
from converter import extract_if_needed, discover_disk
from converter import extract_if_needed, discover_disk, detect_efi
logger = logging.getLogger("backend.provisioner")
@ -275,7 +276,21 @@ def _process_job(job_id: str, req: JobSubmissionRequest):
if not safe_name:
safe_name = f"vm-{vmid}"
bios = "ovmf" if req.boot_type.value == "uefi" else "seabios"
# ── Auto-detect boot type if requested ─────────────────────
final_boot = req.boot_type
if req.auto_detect_boot and disk_paths:
logger.info("Auto-detecting boot type...")
efi = detect_efi(disk_paths[0])
if efi is True:
final_boot = BootType.UEFI
logger.info(" EFI partition detected → UEFI (OVMF)")
elif efi is False:
final_boot = BootType.LEGACY
logger.info(" No EFI partition → Legacy BIOS (SeaBIOS)")
else:
logger.info(" EFI detection inconclusive — using %s", final_boot.value)
bios = "ovmf" if final_boot.value == "uefi" else "seabios"
_run(["qm", "create", str(vmid),
"--name", safe_name,
"--bios", bios,
@ -310,7 +325,7 @@ def _process_job(job_id: str, req: JobSubmissionRequest):
message=f"Imported disk {idx + 1}/{len(disk_paths)}")
# ── Add EFI disk if UEFI ──────────────────────────────────
if req.boot_type.value == "uefi":
if final_boot.value == "uefi":
logger.info("Adding EFI disk")
_run(["qm", "set", str(vmid), "--efidisk0",
f"{req.target_storage}:0,format=raw,size=4M"])

View file

@ -295,7 +295,9 @@ function initPolling() {
let completed = false;
// Reuse buttons
document.getElementById('btn-reuse-yes').onclick = () => reuseImage(jobId, vmid, sourceFilename, true);
document.getElementById('btn-reuse-yes').onclick = () => {
document.getElementById('reuse-form-wrap').classList.remove('hidden');
};
document.getElementById('btn-reuse-no').onclick = () => reuseImage(jobId, vmid, sourceFilename, false);
async function poll() {
@ -363,6 +365,52 @@ async function reuseImage(jobId, vmid, sourceFilename, keep) {
}
} catch (err) { alert('Cleanup failed: ' + err.message); }
}
async function submitReuse(e) {
e.preventDefault();
const newVmid = document.getElementById('reuse-vmid').value;
const newVmName = document.getElementById('reuse-vmname').value.trim();
const status = document.getElementById('reuse-submit-status');
status.classList.remove('hidden');
status.innerHTML = '<div class="spinner"></div> Submitting job...';
// Get original job data from the container
const container = document.getElementById('polling-container');
const sourceFilename = container.dataset.sourceFilename;
const jobId = container.dataset.jobId;
// Get the analysis data from the completed job's backend
let analysis;
try {
const resp = await fetch('/session/status/' + jobId);
analysis = await resp.json();
} catch (err) {
status.innerHTML = '<div class="error">Failed to fetch job data.</div>';
return;
}
// Build payload using same disk settings as first VM
const payload = new FormData();
payload.append('vmid', newVmid);
payload.append('vm_name', newVmName);
payload.append('source_filename', sourceFilename);
// Re-use disk format from original job or default
payload.append('disk_format', 'vmdk');
payload.append('cpu_cores', '2');
payload.append('ram_mb', '4096');
payload.append('target_storage', 'local-lvm');
payload.append('auto_detect_boot', 'true');
payload.append('boot_type', 'uefi');
try {
const resp = await fetch('/session/confirm', { method: 'POST', body: payload });
const html = await resp.text();
document.getElementById('polling-section').innerHTML = html;
initPolling();
} catch (err) {
status.innerHTML = '<div class="error">Failed: ' + err.message + '</div>';
}
}
</script>
{% endblock %}

View file

@ -19,6 +19,7 @@
<div id="error-block" class="error hidden"></div>
</div>
<!-- Reuse: ask whether to create another VM -->
<div id="reuse-section" class="panel hidden">
<h2>Conversion Complete</h2>
<p>Use the same source image to create another VM?</p>
@ -26,5 +27,27 @@
<button id="btn-reuse-yes" class="btn">Yes — Create Another VM</button>
<button id="btn-reuse-no" class="btn btn-secondary">No — Clean Up &amp; Finish</button>
</div>
<div id="reuse-form-wrap" class="hidden" style="margin-top:1rem;">
<hr style="border-color:var(--border); margin-bottom:1rem;">
<form id="reuse-form" onsubmit="submitReuse(event)">
<div class="row">
<div class="form-group">
<label>VM Name *</label>
<input type="text" id="reuse-vmname" name="vm_name" required
placeholder="e.g. second-vm">
</div>
<div class="form-group">
<label>VM ID *</label>
<input type="number" id="reuse-vmid" name="vmid" required
min="21000" max="21100" placeholder="21051">
</div>
</div>
<p class="dim" style="font-size:0.8rem;">
Same disk image, format, and boot settings as the first VM.
</p>
<button type="submit" class="btn" style="margin-top:0.5rem;">Start Conversion</button>
</form>
<div id="reuse-submit-status" class="hidden" style="margin-top:1rem;"></div>
</div>
</div>
</div>