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
This commit is contained in:
Claus Lohmar 2026-07-27 10:29:38 +00:00
parent 5c8d5ea4b4
commit 4082250740
4 changed files with 75 additions and 30 deletions

View file

@ -290,29 +290,62 @@ def _find_sidecar(base: Path, ext: str) -> Optional[Path]:
return None return None
def _parse_vmx_guest_os(vmx_path: Path) -> Optional[str]: def _parse_vmx_config(vmx_path: Path) -> dict:
"""Parse VMware .vmx file for guestOS field.""" """Parse VMware .vmx file for all useful VM configuration."""
result = {}
try: try:
text = vmx_path.read_text(errors="ignore") text = vmx_path.read_text(errors="ignore")
except OSError: except OSError:
return None return result
m = re.search(r'guestOS\s*=\s*"([^"]+)"', text, re.IGNORECASE)
if not m: def _get(key: str) -> Optional[str]:
return None m = re.search(rf'{key}\s*=\s*"([^"]*)"', text, re.IGNORECASE)
raw = m.group(1) return m.group(1) if m else None
# Map common VMware guestOS values
mapping = { raw_os = _get("guestOS")
"debian": "Debian", "ubuntu": "Ubuntu", "centos": "CentOS", if raw_os:
"rhel": "RHEL", "fedora": "Fedora", "windows": "Windows", mapping = {
"other": "Other Linux", "other-64": "Other Linux (64-bit)", "debian": "Debian", "ubuntu": "Ubuntu", "centos": "CentOS",
"other26xlinux": "Linux 2.6.x", "other3xlinux": "Linux 3.x+", "rhel": "RHEL", "fedora": "Fedora", "windows": "Windows",
"other4xlinux": "Linux 4.x+", "other5xlinux": "Linux 5.x+", "other": "Other Linux", "other-64": "Other Linux (64-bit)",
} "other26xlinux": "Linux 2.6.x", "other3xlinux": "Linux 3.x+",
raw_lower = raw.lower().replace("_", "").replace("-", "").replace(" ", "") "other4xlinux": "Linux 4.x+", "other5xlinux": "Linux 5.x+",
for k, v in mapping.items(): }
if k in raw_lower: raw_lower = raw_os.lower().replace("_", "").replace("-", "").replace(" ", "")
return v for k, v in mapping.items():
return raw.replace("-", " ").replace("_", " ").title() if k in raw_lower:
result["os_type"] = v
break
if "os_type" not in result:
result["os_type"] = raw_os.replace("-", " ").replace("_", " ").title()
cores = _get("numvcpus")
if cores:
try:
result["cpu_cores"] = int(cores)
except ValueError:
pass
mem = _get("memSize")
if mem:
try:
result["ram_mb"] = int(mem)
except ValueError:
pass
fw = _get("firmware")
if fw and fw.lower() == "efi":
result["boot_type"] = "uefi"
elif fw and fw.lower() == "bios":
result["boot_type"] = "legacy"
return result
def _parse_vmx_guest_os(vmx_path: Path) -> Optional[str]:
"""Parse VMware .vmx file for guestOS field (backward-compat)."""
cfg = _parse_vmx_config(vmx_path)
return cfg.get("os_type")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -424,13 +457,17 @@ def _process_analysis(analysis_id: str, vmid: int, source_filename: str):
# Detect OVF for qm importovf # Detect OVF for qm importovf
ovf_path = None ovf_path = None
vmx_config = {}
for dirpath, _, filenames in os.walk(source if source.is_dir() else source.parent): for dirpath, _, filenames in os.walk(source if source.is_dir() else source.parent):
for fname in filenames: for fname in filenames:
if fname.lower().endswith(".ovf"): if fname.lower().endswith(".ovf") and not ovf_path:
ovf_path = str(Path(dirpath) / fname) ovf_path = str(Path(dirpath) / fname)
logger.info("OVF descriptor found: %s", ovf_path) logger.info("OVF descriptor found: %s", ovf_path)
break if fname.lower().endswith(".vmx"):
if ovf_path: vmx_path = Path(dirpath) / fname
vmx_config = _parse_vmx_config(vmx_path)
logger.info("VMX config found: %s%s", fname, vmx_config)
if ovf_path and vmx_config:
break break
result = AnalyzeResponse( result = AnalyzeResponse(
@ -442,6 +479,7 @@ def _process_analysis(analysis_id: str, vmid: int, source_filename: str):
efi_detectable=efi, efi_detectable=efi,
all_disks=all_disks_data, all_disks=all_disks_data,
ovf_path=ovf_path, ovf_path=ovf_path,
vmx_config=vmx_config,
) )
_update_analysis(analysis_id, "completed", "Analysis complete", result=result.model_dump()) _update_analysis(analysis_id, "completed", "Analysis complete", result=result.model_dump())

View file

@ -109,6 +109,7 @@ class AnalyzeResponse(BaseModel):
efi_detectable: Optional[bool] = None efi_detectable: Optional[bool] = None
all_disks: list = [] all_disks: list = []
ovf_path: Optional[str] = None ovf_path: Optional[str] = None
vmx_config: dict = {}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -386,12 +386,13 @@ async def session_configure(
suggested_vmid = vmid if vmid else "" suggested_vmid = vmid if vmid else ""
ovf_path = analysis.get("ovf_path") ovf_path = analysis.get("ovf_path")
vmx_config = analysis.get("vmx_config", {})
return render("configure.html", request=request, return render("configure.html", request=request,
analysis_id=analysis_id, source_filename=source_filename, analysis_id=analysis_id, source_filename=source_filename,
session_id=session_id, vm_name=name, storage_pools=pools, session_id=session_id, vm_name=name, storage_pools=pools,
disks=disks, suggested_vmid=suggested_vmid, disks=disks, suggested_vmid=suggested_vmid,
ovf_path=ovf_path) ovf_path=ovf_path, vmx_config=vmx_config)
@app.post("/session/configure/submit") @app.post("/session/configure/submit")

View file

@ -9,6 +9,11 @@
<strong>OVA/OVF appliance detected.</strong> qm importovf will read the VM configuration <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. from the appliance descriptor. Disks are imported together — individual shrink is not available.
</div> </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 %} {% endif %}
</div> </div>
@ -27,7 +32,7 @@
<div class="pve-form-group"> <div class="pve-form-group">
<label>VM Name *</label> <label>VM Name *</label>
<input type="text" id="config-vmname" class="pve-input" required <input type="text" id="config-vmname" class="pve-input" required
placeholder="e.g. my-vm" value="{{ vm_name or '' }}"> placeholder="e.g. my-vm" value="{{ vmx_config.os_type or vm_name or '' }}">
</div> </div>
</div> </div>
@ -50,9 +55,9 @@
<div class="pve-form-group"> <div class="pve-form-group">
<label>Boot Detection</label> <label>Boot Detection</label>
<select id="config-bootdetect" class="pve-select"> <select id="config-bootdetect" class="pve-select">
<option value="auto" selected>Auto-detect (recommended)</option> <option value="auto" {% if not vmx_config.boot_type %}selected{% endif %}>Auto-detect (recommended)</option>
<option value="uefi">UEFI (OVMF)</option> <option value="uefi" {% if vmx_config.boot_type == 'uefi' %}selected{% endif %}>UEFI (OVMF)</option>
<option value="legacy">Legacy BIOS (SeaBIOS)</option> <option value="legacy" {% if vmx_config.boot_type == 'legacy' %}selected{% endif %}>Legacy BIOS (SeaBIOS)</option>
</select> </select>
</div> </div>
</div> </div>
@ -60,11 +65,11 @@
<div class="pve-row"> <div class="pve-row">
<div class="pve-form-group"> <div class="pve-form-group">
<label>CPU Cores</label> <label>CPU Cores</label>
<input type="number" id="config-cores" class="pve-input" value="2" min="1"> <input type="number" id="config-cores" class="pve-input" value="{{ vmx_config.cpu_cores or 2 }}" min="1">
</div> </div>
<div class="pve-form-group"> <div class="pve-form-group">
<label>RAM (MB)</label> <label>RAM (MB)</label>
<input type="number" id="config-ram" class="pve-input" value="4096" min="512"> <input type="number" id="config-ram" class="pve-input" value="{{ vmx_config.ram_mb or 4096 }}" min="512">
</div> </div>
</div> </div>