feat: add Proxmox multi-source support

Add encrypted VMware and Proxmox sources per customer profile, import QEMU and LXC guests, improve Proxmox permission diagnostics, and speed up VMware loading with bulk property collection.
This commit is contained in:
2026-07-24 11:17:01 +02:00
parent 16726b5fab
commit 9f09d0dfc7
5 changed files with 697 additions and 784 deletions
+4 -4
View File
@@ -1,11 +1,12 @@
# NetBox VM Import Desktop
Windows-Desktopanwendung zum Importieren und Synchronisieren virtueller Maschinen aus VMware ESXi/vSphere nach NetBox.
Windows-Desktopanwendung zum Importieren und Synchronisieren virtueller Maschinen aus VMware ESXi/vSphere und Proxmox VE nach NetBox.
## Funktionen
- Mehrere kundenbezogene VMware- und NetBox-Profile
- Mit Windows DPAPI verschlüsselte Passwörter und API-Token
- Mehrere VMware- und Proxmox-Quellen pro Kundenprofil
- QEMU/KVM-VMs und optional LXC-Container aus Proxmox VE
- Mit Windows DPAPI verschlüsselte Passwörter, API-Token und Token-Secrets
- Optionale Unterstützung selbstsignierter TLS-Zertifikate
- Synchronisierung von VMs, Hardwaredaten, Interfaces und IP-Adressen
- Übernahme primärer IPv4- und IPv6-Adressen
@@ -30,4 +31,3 @@ python app.py
```powershell
pyinstaller "NetBox VM Import.spec"
```
+635 -758
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+57 -21
View File
@@ -8,6 +8,7 @@ from pathlib import Path
APP_NAME = "NetBox VM Import"
SECRET_KEYS = ("password", "token_secret")
def config_path():
@@ -28,7 +29,7 @@ def _blob(data):
def protect(value):
"""Encrypt a string for the current Windows user using DPAPI."""
"""Encrypt a value for the current Windows user with DPAPI."""
if not value:
return ""
if sys.platform != "win32":
@@ -40,18 +41,16 @@ def protect(value):
):
raise ctypes.WinError()
try:
encrypted = ctypes.string_at(result.pbData, result.cbData)
return base64.b64encode(encrypted).decode("ascii")
return base64.b64encode(ctypes.string_at(result.pbData, result.cbData)).decode("ascii")
finally:
ctypes.windll.kernel32.LocalFree(result.pbData)
def unprotect(value):
"""Decrypt a DPAPI string for the current Windows user."""
"""Decrypt a DPAPI value for the current Windows user."""
if not value:
return ""
encrypted = base64.b64decode(value)
source, source_buffer = _blob(encrypted)
source, source_buffer = _blob(base64.b64decode(value))
result = _DataBlob()
if not ctypes.windll.crypt32.CryptUnprotectData(
ctypes.byref(source), None, None, None, None, 0, ctypes.byref(result)
@@ -74,25 +73,56 @@ class ProfileStore:
data = json.load(stream)
profiles = {}
for item in data.get("profiles", []):
name = item.get("name", "").strip()
for record in data.get("profiles", []):
name = record.get("name", "").strip()
if not name:
continue
profile = dict(item)
profile["esxi_password"] = unprotect(item.get("esxi_password_encrypted", ""))
profile["netbox_token"] = unprotect(item.get("netbox_token_encrypted", ""))
profile = {
"name": name,
"netbox_url": record.get("netbox_url", ""),
"netbox_token": unprotect(record.get("netbox_token_encrypted", "")),
"netbox_ignore_ssl": bool(record.get("netbox_ignore_ssl", record.get("ignore_ssl_errors", False))),
"sources": [],
}
for source_record in record.get("sources", []):
source = dict(source_record)
for key in SECRET_KEYS:
encrypted_key = f"{key}_encrypted"
if encrypted_key in source:
source[key] = unprotect(source.pop(encrypted_key))
profile["sources"].append(source)
# Migration from v2: one VMware source embedded in each profile.
if not profile["sources"] and record.get("esxi_host"):
profile["sources"].append({
"id": "vmware-legacy",
"type": "vmware",
"name": "VMware",
"enabled": True,
"host": record.get("esxi_host", ""),
"username": record.get("esxi_user", ""),
"password": unprotect(record.get("esxi_password_encrypted", "")),
"ignore_ssl": bool(record.get("ignore_ssl_errors", False)),
})
profiles[name] = profile
# Migration from the original single-profile, plaintext format.
# Migration from the first plaintext single-profile format.
if not profiles and any(key in data for key in ("esxi_host", "netbox_url")):
profiles["Standard"] = {
"name": "Standard",
"esxi_host": data.get("esxi_host", ""),
"esxi_user": data.get("esxi_user", ""),
"esxi_password": data.get("esxi_password", ""),
"netbox_url": data.get("netbox_url", ""),
"netbox_token": data.get("netbox_token", ""),
"ignore_ssl_errors": bool(data.get("ignore_ssl_errors", False)),
"netbox_ignore_ssl": bool(data.get("ignore_ssl_errors", False)),
"sources": [{
"id": "vmware-legacy",
"type": "vmware",
"name": "VMware",
"enabled": True,
"host": data.get("esxi_host", ""),
"username": data.get("esxi_user", ""),
"password": data.get("esxi_password", ""),
"ignore_ssl": bool(data.get("ignore_ssl_errors", False)),
}],
}
return profiles, data.get("active_profile", "")
@@ -100,16 +130,22 @@ class ProfileStore:
records = []
for name in sorted(profiles, key=str.casefold):
profile = profiles[name]
sources = []
for source_data in profile.get("sources", []):
source = {key: value for key, value in source_data.items() if key not in SECRET_KEYS}
for key in SECRET_KEYS:
if key in source_data:
source[f"{key}_encrypted"] = protect(source_data.get(key, ""))
sources.append(source)
records.append({
"name": name,
"esxi_host": profile.get("esxi_host", ""),
"esxi_user": profile.get("esxi_user", ""),
"esxi_password_encrypted": protect(profile.get("esxi_password", "")),
"netbox_url": profile.get("netbox_url", ""),
"netbox_token_encrypted": protect(profile.get("netbox_token", "")),
"ignore_ssl_errors": bool(profile.get("ignore_ssl_errors", False)),
"netbox_ignore_ssl": bool(profile.get("netbox_ignore_ssl", False)),
"sources": sources,
})
payload = {"version": 2, "active_profile": active_profile, "profiles": records}
payload = {"version": 3, "active_profile": active_profile, "profiles": records}
self.path.parent.mkdir(parents=True, exist_ok=True)
temp_path = self.path.with_suffix(".tmp")
with temp_path.open("w", encoding="utf-8") as stream:
+1 -1
View File
@@ -1,4 +1,4 @@
pyvmomi
pynetbox
requests
pyinstaller