Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f09d0dfc7 |
@@ -1,11 +1,12 @@
|
|||||||
# NetBox VM Import Desktop
|
# 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
|
## Funktionen
|
||||||
|
|
||||||
- Mehrere kundenbezogene VMware- und NetBox-Profile
|
- Mehrere VMware- und Proxmox-Quellen pro Kundenprofil
|
||||||
- Mit Windows DPAPI verschlüsselte Passwörter und API-Token
|
- 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
|
- Optionale Unterstützung selbstsignierter TLS-Zertifikate
|
||||||
- Synchronisierung von VMs, Hardwaredaten, Interfaces und IP-Adressen
|
- Synchronisierung von VMs, Hardwaredaten, Interfaces und IP-Adressen
|
||||||
- Übernahme primärer IPv4- und IPv6-Adressen
|
- Übernahme primärer IPv4- und IPv6-Adressen
|
||||||
@@ -30,4 +31,3 @@ python app.py
|
|||||||
```powershell
|
```powershell
|
||||||
pyinstaller "NetBox VM Import.spec"
|
pyinstaller "NetBox VM Import.spec"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+57
-21
@@ -8,6 +8,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
|
|
||||||
APP_NAME = "NetBox VM Import"
|
APP_NAME = "NetBox VM Import"
|
||||||
|
SECRET_KEYS = ("password", "token_secret")
|
||||||
|
|
||||||
|
|
||||||
def config_path():
|
def config_path():
|
||||||
@@ -28,7 +29,7 @@ def _blob(data):
|
|||||||
|
|
||||||
|
|
||||||
def protect(value):
|
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:
|
if not value:
|
||||||
return ""
|
return ""
|
||||||
if sys.platform != "win32":
|
if sys.platform != "win32":
|
||||||
@@ -40,18 +41,16 @@ def protect(value):
|
|||||||
):
|
):
|
||||||
raise ctypes.WinError()
|
raise ctypes.WinError()
|
||||||
try:
|
try:
|
||||||
encrypted = ctypes.string_at(result.pbData, result.cbData)
|
return base64.b64encode(ctypes.string_at(result.pbData, result.cbData)).decode("ascii")
|
||||||
return base64.b64encode(encrypted).decode("ascii")
|
|
||||||
finally:
|
finally:
|
||||||
ctypes.windll.kernel32.LocalFree(result.pbData)
|
ctypes.windll.kernel32.LocalFree(result.pbData)
|
||||||
|
|
||||||
|
|
||||||
def unprotect(value):
|
def unprotect(value):
|
||||||
"""Decrypt a DPAPI string for the current Windows user."""
|
"""Decrypt a DPAPI value for the current Windows user."""
|
||||||
if not value:
|
if not value:
|
||||||
return ""
|
return ""
|
||||||
encrypted = base64.b64decode(value)
|
source, source_buffer = _blob(base64.b64decode(value))
|
||||||
source, source_buffer = _blob(encrypted)
|
|
||||||
result = _DataBlob()
|
result = _DataBlob()
|
||||||
if not ctypes.windll.crypt32.CryptUnprotectData(
|
if not ctypes.windll.crypt32.CryptUnprotectData(
|
||||||
ctypes.byref(source), None, None, None, None, 0, ctypes.byref(result)
|
ctypes.byref(source), None, None, None, None, 0, ctypes.byref(result)
|
||||||
@@ -74,25 +73,56 @@ class ProfileStore:
|
|||||||
data = json.load(stream)
|
data = json.load(stream)
|
||||||
|
|
||||||
profiles = {}
|
profiles = {}
|
||||||
for item in data.get("profiles", []):
|
for record in data.get("profiles", []):
|
||||||
name = item.get("name", "").strip()
|
name = record.get("name", "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
continue
|
continue
|
||||||
profile = dict(item)
|
profile = {
|
||||||
profile["esxi_password"] = unprotect(item.get("esxi_password_encrypted", ""))
|
"name": name,
|
||||||
profile["netbox_token"] = unprotect(item.get("netbox_token_encrypted", ""))
|
"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
|
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")):
|
if not profiles and any(key in data for key in ("esxi_host", "netbox_url")):
|
||||||
profiles["Standard"] = {
|
profiles["Standard"] = {
|
||||||
"name": "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_url": data.get("netbox_url", ""),
|
||||||
"netbox_token": data.get("netbox_token", ""),
|
"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", "")
|
return profiles, data.get("active_profile", "")
|
||||||
|
|
||||||
@@ -100,16 +130,22 @@ class ProfileStore:
|
|||||||
records = []
|
records = []
|
||||||
for name in sorted(profiles, key=str.casefold):
|
for name in sorted(profiles, key=str.casefold):
|
||||||
profile = profiles[name]
|
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({
|
records.append({
|
||||||
"name": name,
|
"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_url": profile.get("netbox_url", ""),
|
||||||
"netbox_token_encrypted": protect(profile.get("netbox_token", "")),
|
"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)
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
temp_path = self.path.with_suffix(".tmp")
|
temp_path = self.path.with_suffix(".tmp")
|
||||||
with temp_path.open("w", encoding="utf-8") as stream:
|
with temp_path.open("w", encoding="utf-8") as stream:
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
pyvmomi
|
pyvmomi
|
||||||
pynetbox
|
pynetbox
|
||||||
|
requests
|
||||||
pyinstaller
|
pyinstaller
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user