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,17 +290,20 @@ 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
|
|
||||||
|
raw_os = _get("guestOS")
|
||||||
|
if raw_os:
|
||||||
mapping = {
|
mapping = {
|
||||||
"debian": "Debian", "ubuntu": "Ubuntu", "centos": "CentOS",
|
"debian": "Debian", "ubuntu": "Ubuntu", "centos": "CentOS",
|
||||||
"rhel": "RHEL", "fedora": "Fedora", "windows": "Windows",
|
"rhel": "RHEL", "fedora": "Fedora", "windows": "Windows",
|
||||||
|
|
@ -308,11 +311,41 @@ def _parse_vmx_guest_os(vmx_path: Path) -> Optional[str]:
|
||||||
"other26xlinux": "Linux 2.6.x", "other3xlinux": "Linux 3.x+",
|
"other26xlinux": "Linux 2.6.x", "other3xlinux": "Linux 3.x+",
|
||||||
"other4xlinux": "Linux 4.x+", "other5xlinux": "Linux 5.x+",
|
"other4xlinux": "Linux 4.x+", "other5xlinux": "Linux 5.x+",
|
||||||
}
|
}
|
||||||
raw_lower = raw.lower().replace("_", "").replace("-", "").replace(" ", "")
|
raw_lower = raw_os.lower().replace("_", "").replace("-", "").replace(" ", "")
|
||||||
for k, v in mapping.items():
|
for k, v in mapping.items():
|
||||||
if k in raw_lower:
|
if k in raw_lower:
|
||||||
return v
|
result["os_type"] = v
|
||||||
return raw.replace("-", " ").replace("_", " ").title()
|
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())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue