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:
parent
5c8d5ea4b4
commit
4082250740
4 changed files with 75 additions and 30 deletions
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ class AnalyzeResponse(BaseModel):
|
|||
efi_detectable: Optional[bool] = None
|
||||
all_disks: list = []
|
||||
ovf_path: Optional[str] = None
|
||||
vmx_config: dict = {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@
|
|||
<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>
|
||||
|
||||
|
|
@ -27,7 +32,7 @@
|
|||
<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="{{ vm_name or '' }}">
|
||||
placeholder="e.g. my-vm" value="{{ vmx_config.os_type or vm_name or '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -50,9 +55,9 @@
|
|||
<div class="pve-form-group">
|
||||
<label>Boot Detection</label>
|
||||
<select id="config-bootdetect" class="pve-select">
|
||||
<option value="auto" selected>Auto-detect (recommended)</option>
|
||||
<option value="uefi">UEFI (OVMF)</option>
|
||||
<option value="legacy">Legacy BIOS (SeaBIOS)</option>
|
||||
<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>
|
||||
|
|
@ -60,11 +65,11 @@
|
|||
<div class="pve-row">
|
||||
<div class="pve-form-group">
|
||||
<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 class="pve-form-group">
|
||||
<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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue