9 Commits
Author SHA1 Message Date
MrBlake 5d1d7758c0 feat: review detected networks in one import plan
Show a combined network and VRF dialog only when selected VMs introduce missing prefixes. Allow a different existing or newly named VRF per network and cache existing prefixes to prevent repeated prompts.
2026-07-30 08:44:06 +02:00
MrBlake 5d1b39c702 feat: add batch VRF and network management
Add a dedicated tenant-aware window for creating multiple VRFs and prefixes. Reuse existing covering prefixes across tenant VRFs to avoid repeated prompts on later imports.
2026-07-30 08:32:10 +02:00
MrBlake fcc9f3d3a1 feat: create VRFs from prefix dialog
Offer tenant-scoped VRF creation directly while confirming a missing prefix. Use the newly created VRF consistently for both the prefix and its IP address.
2026-07-29 16:09:29 +02:00
MrBlake c76c0fc089 feat: allow custom and per-prefix VRF selection
Let users name automatically created tenant VRFs and override the preselected VRF for each new prefix. Keep IP addresses and prefixes in the same selected VRF.
2026-07-29 16:03:27 +02:00
MrBlake 744aaeafe5 feat: add tenant-aware VRF selection
Allow selecting an existing tenant VRF or automatically creating one per tenant and cluster, then scope synchronized IP addresses and prefixes to the selected VRF.
2026-07-29 15:48:12 +02:00
MrBlake 988efcf3a7 feat: confirm tenant-aware prefix creation
Propagate tenants to IPv4 and IPv6 prefixes, detect VMware prefix lengths, require confirmation before creating missing subnets, and provide canonical subnet-size selection for new prefixes.
2026-07-29 14:02:00 +02:00
MrBlake ec40bded81 fix: propagate tenant to synchronized IP addresses
Assign the confirmed cluster tenant to every created or updated IP address and include the affected address in NetBox synchronization errors.
2026-07-29 13:41:33 +02:00
MrBlake a37477e4ec feat: confirm cluster tenant before VM sync
Inherit the NetBox cluster tenant, require explicit confirmation or selection before synchronization, and replace the application artwork with a crisp multi-resolution Windows icon.
2026-07-29 13:27:16 +02:00
MrBlake 9f09d0dfc7 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.
2026-07-24 11:17:01 +02:00
7 changed files with 1675 additions and 778 deletions
+10 -4
View File
@@ -1,11 +1,18 @@
# 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
- Automatische Übernahme des Cluster-Mandanten mit verbindlicher Bestätigung vor dem Sync
- Mandantenzuweisung für IPv4-/IPv6-Adressen und erkannte NetBox-Präfixe
- Gemeinsame Importmaske für alle neu erkannten Netze mit Subnetzgröße und vorausgefüllter VRF-Auswahl
- Mehrere neue VRFs direkt in der Netzwerkmaske anlegen; Namensfelder erscheinen nur bei Bedarf
- Auswahl vorhandener Mandanten-VRFs oder automatische VRF-Erstellung pro Cluster mit frei wählbarem Namen
- Wiederverwendung bereits vorhandener, zur IP passender Präfixe bei erneuten Imports
- Optionale Unterstützung selbstsignierter TLS-Zertifikate
- Synchronisierung von VMs, Hardwaredaten, Interfaces und IP-Adressen
- Übernahme primärer IPv4- und IPv6-Adressen
@@ -30,4 +37,3 @@ python app.py
```powershell
pyinstaller "NetBox VM Import.spec"
```
+1607 -752
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 735 KiB

After

Width:  |  Height:  |  Size: 416 KiB

+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