vm-bench/frontend/templates/configure.html
Claus Lohmar 4082250740 feat: VMware VMX config detection — parse .vmx files for CPU, RAM, firmware, OS type
- _parse_vmx_config reads numvcpus, memSize, firmware, guestOS from .vmx
- Configure page pre-fills CPU cores, RAM, boot type, VM name from VMX
- Banner shows detected config: 'VMware VMX config detected. Settings pre-filled'
- Works alongside OVF detection — OVF takes priority if both present
2026-07-27 10:29:38 +00:00

222 lines
No EOL
8.7 KiB
HTML

{% extends "base.html" %}
{% block content %}
<div class="pve-hero" style="padding-bottom:0.5rem;">
<h1 style="font-size:1.1rem;">VM Configuration</h1>
<p>Source: <span class="pve-code">{{ source_filename }}</span></p>
{% if ovf_path %}
<div class="pve-alert pve-alert-info" style="margin-top:0.5rem;">
<strong>OVA/OVF appliance detected.</strong> qm importovf will read the VM configuration
from the appliance descriptor. Disks are imported together — individual shrink is not available.
</div>
{% elif vmx_config and vmx_config|length > 0 %}
<div class="pve-alert pve-alert-info" style="margin-top:0.5rem;">
<strong>VMware VMX config detected.</strong> Settings pre-filled from {{ vmx_config.os_type or 'VM' }} configuration.
CPU: {{ vmx_config.cpu_cores or '?' }} cores, RAM: {{ vmx_config.ram_mb or '?' }} MB, Boot: {{ vmx_config.boot_type or 'auto' }}.
</div>
{% endif %}
</div>
<div class="pve-panel">
<div class="pve-panel-header">
<span class="pve-panel-title">VM Settings</span>
</div>
<div class="pve-panel-body">
<div class="pve-row">
<div class="pve-form-group">
<label>VM ID *</label>
<input type="number" id="config-vmid" class="pve-input" required
min="21000" max="21100" placeholder="e.g. 21050" value="{{ suggested_vmid }}">
</div>
<div class="pve-form-group">
<label>VM Name *</label>
<input type="text" id="config-vmname" class="pve-input" required
placeholder="e.g. my-vm" value="{{ vmx_config.os_type or vm_name or '' }}">
</div>
</div>
<div class="pve-row">
<div class="pve-form-group">
<label>Storage Pool</label>
{% if storage_pools %}
<select id="config-storage" class="pve-select">
{% for pool in storage_pools %}
<option value="{{ pool.name }}">{{ pool.name }} ({{ pool.type }})</option>
{% endfor %}
<option value="__custom__">Custom...</option>
</select>
<input type="text" id="config-storage-custom" class="pve-input pve-hidden"
placeholder="Enter pool name" style="margin-top:0.3rem;">
{% else %}
<input type="text" id="config-storage" class="pve-input">
{% endif %}
</div>
<div class="pve-form-group">
<label>Boot Detection</label>
<select id="config-bootdetect" class="pve-select">
<option value="auto" {% if not vmx_config.boot_type %}selected{% endif %}>Auto-detect (recommended)</option>
<option value="uefi" {% if vmx_config.boot_type == 'uefi' %}selected{% endif %}>UEFI (OVMF)</option>
<option value="legacy" {% if vmx_config.boot_type == 'legacy' %}selected{% endif %}>Legacy BIOS (SeaBIOS)</option>
</select>
</div>
</div>
<div class="pve-row">
<div class="pve-form-group">
<label>CPU Cores</label>
<input type="number" id="config-cores" class="pve-input" value="{{ vmx_config.cpu_cores or 2 }}" min="1">
</div>
<div class="pve-form-group">
<label>RAM (MB)</label>
<input type="number" id="config-ram" class="pve-input" value="{{ vmx_config.ram_mb or 4096 }}" min="512">
</div>
</div>
</div>
</div>
<div class="pve-panel">
<div class="pve-panel-header">
<span class="pve-panel-title">Disks Found</span>
</div>
<div class="pve-panel-body">
<table class="pve-table">
<thead>
<tr style="font-size:0.7rem;color:var(--pve-muted);">
<td style="width:40px;">Boot</td>
<td>Filename</td>
<td style="width:60px;">Format</td>
<td style="width:80px;">Size</td>
<td style="width:140px;">Resize</td>
</tr>
</thead>
<tbody id="disks-tbody">
{% for d in disks %}
<tr>
<td><input type="radio" name="boot_disk" value="{{ loop.index0 }}" {% if loop.first %}checked{% endif %}></td>
<td>{{ d.filename }}</td>
<td>{{ d.format }}</td>
<td>{{ d.size_gb }} GB</td>
<td>
<label style="display:flex;align-items:center;gap:0.3rem;font-size:0.72rem;white-space:nowrap;">
{% if ovf_path %}
<span class="pve-dim">handled by OVF</span>
{% else %}
<input type="checkbox" class="disk-shrink-cb" onchange="toggleShrink(this, {{ loop.index0 }})">
to <input type="number" class="pve-input disk-shrink-size" id="shrink-{{ loop.index0 }}"
value="{{ d.size_gb }}" disabled style="width:70px;padding:0.2rem 0.3rem;font-size:0.72rem;"> GB
{% endif %}
</label>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pve-alert pve-alert-info" style="margin-top:0.75rem;">
<strong>TIP:</strong> The disk with the operating system should be the boot disk.
Data disks will be attached as additional drives.
</div>
<button id="config-submit-btn" class="pve-btn pve-btn-primary" style="margin-top:0.75rem;" onclick="submitConfig()">Start Conversion</button>
<div id="config-submit-status" class="pve-hidden" style="margin-top:0.5rem;"></div>
</div>
</div>
<script>
var _analysisId = '{{ analysis_id }}';
var _sessionId = '{{ session_id }}';
var _ovfPath = '{{ ovf_path or "" }}';
function toggleShrink(cb, idx) {
var inp = document.getElementById('shrink-' + idx);
if (cb.checked) { inp.removeAttribute('disabled'); inp.focus(); }
else { inp.setAttribute('disabled', ''); }
}
(function(){
var ss = document.getElementById('config-storage');
var sc = document.getElementById('config-storage-custom');
if (ss && sc) {
ss.addEventListener('change', function() {
if (this.value === '__custom__') {
sc.classList.remove('pve-hidden'); sc.focus();
} else {
sc.classList.add('pve-hidden');
}
});
}
})();
async function submitConfig() {
var vmid = parseInt(document.getElementById('config-vmid').value) || 0;
var vmName = document.getElementById('config-vmname').value.trim();
if (!vmid || vmid < 21000 || vmid > 21100) { alert('VM ID must be 21000-21100.'); return; }
if (!vmName) { alert('Please enter a VM name.'); return; }
var storage = document.getElementById('config-storage');
var sc = document.getElementById('config-storage-custom');
var targetStorage = sc && !sc.classList.contains('pve-hidden') ? sc.value : (storage ? storage.value : '');
var bootDetect = document.getElementById('config-bootdetect').value;
var autoBoot = bootDetect === 'auto';
var bootType = autoBoot ? 'uefi' : bootDetect;
var cores = parseInt(document.getElementById('config-cores').value) || 2;
var ram = parseInt(document.getElementById('config-ram').value) || 4096;
var bootIdx = 0;
var radios = document.getElementsByName('boot_disk');
for (var i = 0; i < radios.length; i++) {
if (radios[i].checked) { bootIdx = parseInt(radios[i].value); break; }
}
var payload = {
vmid: vmid, vm_name: vmName, cpu_cores: cores, ram_mb: ram,
target_storage: targetStorage, auto_detect_boot: autoBoot, boot_type: bootType,
session_id: _sessionId, analysis_id: _analysisId,
boot_disk: null, additional_disks: []
};
if (_ovfPath) payload.ovf_path = _ovfPath;
var shrinkCbs = document.getElementsByClassName('disk-shrink-cb');
var diskFiles = [{% for d in disks %}{{ d|tojson }},{% endfor %}];
if (shrinkCbs.length === 0 && diskFiles.length > 0) {
payload.boot_disk = { disk_type: 'image_file', source_filename: diskFiles[0].filename, format: diskFiles[0].format };
for (var k = 1; k < diskFiles.length; k++) {
payload.additional_disks.push({ disk_type: 'image_file', source_filename: diskFiles[k].filename, format: diskFiles[k].format });
}
} else {
for (var i = 0; i < shrinkCbs.length; i++) {
var d = diskFiles[i];
var spec = { disk_type: 'image_file', source_filename: d.filename, format: d.format };
if (shrinkCbs[i].checked) {
var si = document.getElementById('shrink-' + i);
var sz = si ? parseInt(si.value) || null : null;
if (sz) spec.target_disk_size_gb = sz;
}
if (i === bootIdx) payload.boot_disk = spec;
else payload.additional_disks.push(spec);
}
}
var btn = document.getElementById('config-submit-btn');
var status = document.getElementById('config-submit-status');
btn.disabled = true;
status.classList.remove('pve-hidden');
status.innerHTML = '<div class="pve-spinner"></div> Submitting job...';
try {
var resp = await fetch('/session/configure/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
var data = await resp.json();
if (data.error) { throw new Error(data.error); }
window.location.href = '/session/status/' + data.job_id;
} catch (err) {
status.innerHTML = '<div class="pve-alert pve-alert-error">Failed: ' + err.message + '</div>';
btn.disabled = false;
}
}
</script>
{% endblock %}