From 40822507406b4a463314aa48f41d3c27a75e3120 Mon Sep 17 00:00:00 2001 From: Claus Lohmar Date: Mon, 27 Jul 2026 10:29:38 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20VMware=20VMX=20config=20detection=20?= =?UTF-8?q?=E2=80=94=20parse=20.vmx=20files=20for=20CPU,=20RAM,=20firmware?= =?UTF-8?q?,=20OS=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _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 --- backend/converter.py | 84 ++++++++++++++++++++++--------- backend/models.py | 1 + frontend/app.py | 3 +- frontend/templates/configure.html | 17 ++++--- 4 files changed, 75 insertions(+), 30 deletions(-) diff --git a/backend/converter.py b/backend/converter.py index 8248d1b..b536b5e 100644 --- a/backend/converter.py +++ b/backend/converter.py @@ -290,29 +290,62 @@ def _find_sidecar(base: Path, ext: str) -> Optional[Path]: return None -def _parse_vmx_guest_os(vmx_path: Path) -> Optional[str]: - """Parse VMware .vmx file for guestOS field.""" +def _parse_vmx_config(vmx_path: Path) -> dict: + """Parse VMware .vmx file for all useful VM configuration.""" + result = {} try: text = vmx_path.read_text(errors="ignore") except OSError: - return None - m = re.search(r'guestOS\s*=\s*"([^"]+)"', text, re.IGNORECASE) - if not m: - return None - raw = m.group(1) - # Map common VMware guestOS values - mapping = { - "debian": "Debian", "ubuntu": "Ubuntu", "centos": "CentOS", - "rhel": "RHEL", "fedora": "Fedora", "windows": "Windows", - "other": "Other Linux", "other-64": "Other Linux (64-bit)", - "other26xlinux": "Linux 2.6.x", "other3xlinux": "Linux 3.x+", - "other4xlinux": "Linux 4.x+", "other5xlinux": "Linux 5.x+", - } - raw_lower = raw.lower().replace("_", "").replace("-", "").replace(" ", "") - for k, v in mapping.items(): - if k in raw_lower: - return v - return raw.replace("-", " ").replace("_", " ").title() + return result + + def _get(key: str) -> Optional[str]: + m = re.search(rf'{key}\s*=\s*"([^"]*)"', text, re.IGNORECASE) + return m.group(1) if m else None + + raw_os = _get("guestOS") + if raw_os: + mapping = { + "debian": "Debian", "ubuntu": "Ubuntu", "centos": "CentOS", + "rhel": "RHEL", "fedora": "Fedora", "windows": "Windows", + "other": "Other Linux", "other-64": "Other Linux (64-bit)", + "other26xlinux": "Linux 2.6.x", "other3xlinux": "Linux 3.x+", + "other4xlinux": "Linux 4.x+", "other5xlinux": "Linux 5.x+", + } + raw_lower = raw_os.lower().replace("_", "").replace("-", "").replace(" ", "") + for k, v in mapping.items(): + 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 ovf_path = None + vmx_config = {} for dirpath, _, filenames in os.walk(source if source.is_dir() else source.parent): for fname in filenames: - if fname.lower().endswith(".ovf"): + if fname.lower().endswith(".ovf") and not ovf_path: ovf_path = str(Path(dirpath) / fname) logger.info("OVF descriptor found: %s", ovf_path) - break - if ovf_path: + if fname.lower().endswith(".vmx"): + 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 result = AnalyzeResponse( @@ -442,6 +479,7 @@ def _process_analysis(analysis_id: str, vmid: int, source_filename: str): efi_detectable=efi, all_disks=all_disks_data, ovf_path=ovf_path, + vmx_config=vmx_config, ) _update_analysis(analysis_id, "completed", "Analysis complete", result=result.model_dump()) diff --git a/backend/models.py b/backend/models.py index f50b658..d3b1dce 100644 --- a/backend/models.py +++ b/backend/models.py @@ -109,6 +109,7 @@ class AnalyzeResponse(BaseModel): efi_detectable: Optional[bool] = None all_disks: list = [] ovf_path: Optional[str] = None + vmx_config: dict = {} # --------------------------------------------------------------------------- diff --git a/frontend/app.py b/frontend/app.py index c7d73c9..45700b0 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -386,12 +386,13 @@ async def session_configure( suggested_vmid = vmid if vmid else "" ovf_path = analysis.get("ovf_path") + vmx_config = analysis.get("vmx_config", {}) return render("configure.html", request=request, analysis_id=analysis_id, source_filename=source_filename, session_id=session_id, vm_name=name, storage_pools=pools, disks=disks, suggested_vmid=suggested_vmid, - ovf_path=ovf_path) + ovf_path=ovf_path, vmx_config=vmx_config) @app.post("/session/configure/submit") diff --git a/frontend/templates/configure.html b/frontend/templates/configure.html index 373e372..a0fc32e 100644 --- a/frontend/templates/configure.html +++ b/frontend/templates/configure.html @@ -9,6 +9,11 @@ OVA/OVF appliance detected. qm importovf will read the VM configuration from the appliance descriptor. Disks are imported together — individual shrink is not available. + {% elif vmx_config and vmx_config|length > 0 %} +
+ VMware VMX config detected. 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' }}. +
{% endif %} @@ -27,7 +32,7 @@
+ placeholder="e.g. my-vm" value="{{ vmx_config.os_type or vm_name or '' }}">
@@ -50,9 +55,9 @@
@@ -60,11 +65,11 @@
- +
- +