Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d1d7758c0 | ||
|
|
5d1b39c702 | ||
|
|
fcc9f3d3a1 | ||
|
|
c76c0fc089 | ||
|
|
744aaeafe5 | ||
|
|
988efcf3a7 | ||
|
|
ec40bded81 |
@@ -8,6 +8,11 @@ Windows-Desktopanwendung zum Importieren und Synchronisieren virtueller Maschine
|
|||||||
- QEMU/KVM-VMs und optional LXC-Container aus Proxmox VE
|
- QEMU/KVM-VMs und optional LXC-Container aus Proxmox VE
|
||||||
- Mit Windows DPAPI verschlüsselte Passwörter, API-Token und Token-Secrets
|
- Mit Windows DPAPI verschlüsselte Passwörter, API-Token und Token-Secrets
|
||||||
- Automatische Übernahme des Cluster-Mandanten mit verbindlicher Bestätigung vor dem Sync
|
- 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
|
- 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
|
||||||
|
|||||||
@@ -175,6 +175,546 @@ class TenantConfirmationDialog(tk.Toplevel):
|
|||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
class VRFSelectionDialog(tk.Toplevel):
|
||||||
|
"""Select a tenant VRF or request automatic creation."""
|
||||||
|
|
||||||
|
AUTO_CREATE = "➕ Neue VRF automatisch erstellen"
|
||||||
|
|
||||||
|
def __init__(self, parent, tenant_name, cluster_name, vrfs):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.title("VRF auswählen")
|
||||||
|
self.geometry("540x355")
|
||||||
|
self.resizable(False, False)
|
||||||
|
self.transient(parent)
|
||||||
|
self.grab_set()
|
||||||
|
self.result = None
|
||||||
|
self.vrf_map = {}
|
||||||
|
|
||||||
|
values = []
|
||||||
|
for vrf in vrfs:
|
||||||
|
rd = getattr(vrf, "rd", None)
|
||||||
|
display = f"{vrf.name} (RD: {rd})" if rd else vrf.name
|
||||||
|
self.vrf_map[display] = vrf.id
|
||||||
|
values.append(display)
|
||||||
|
values.append(self.AUTO_CREATE)
|
||||||
|
default = values[0] if vrfs else self.AUTO_CREATE
|
||||||
|
self.selection = tk.StringVar(value=default)
|
||||||
|
self.new_vrf_name = tk.StringVar(value=f"{tenant_name} - {cluster_name}")
|
||||||
|
|
||||||
|
body = ttk.Frame(self, padding=22)
|
||||||
|
body.pack(fill="both", expand=True)
|
||||||
|
ttk.Label(body, text="VRF für IP-Adressen", font=("Segoe UI Semibold", 14)).pack(anchor="w")
|
||||||
|
ttk.Label(
|
||||||
|
body,
|
||||||
|
text=(
|
||||||
|
f"Mandant: {tenant_name}\nCluster: {cluster_name}\n\n"
|
||||||
|
"Bitte eine vorhandene VRF dieses Mandanten auswählen oder automatisch eine neue VRF erstellen lassen."
|
||||||
|
),
|
||||||
|
wraplength=490,
|
||||||
|
justify="left",
|
||||||
|
).pack(anchor="w", pady=(8, 14))
|
||||||
|
selection_combo = ttk.Combobox(body, textvariable=self.selection, values=values, state="readonly")
|
||||||
|
selection_combo.pack(fill="x")
|
||||||
|
selection_combo.bind("<<ComboboxSelected>>", self.update_name_field)
|
||||||
|
self.name_frame = ttk.Frame(body)
|
||||||
|
ttk.Label(self.name_frame, text="Name der neuen VRF").pack(anchor="w", pady=(14, 4))
|
||||||
|
ttk.Entry(self.name_frame, textvariable=self.new_vrf_name).pack(fill="x")
|
||||||
|
self.update_name_field()
|
||||||
|
|
||||||
|
buttons = ttk.Frame(body)
|
||||||
|
buttons.pack(anchor="e", pady=(22, 0))
|
||||||
|
ttk.Button(buttons, text="Abbrechen", command=self.destroy).pack(side="left", padx=4)
|
||||||
|
ttk.Button(buttons, text="VRF bestätigen", command=self.accept, style="Primary.TButton").pack(side="left", padx=4)
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||||
|
self.wait_window(self)
|
||||||
|
|
||||||
|
def update_name_field(self, _event=None):
|
||||||
|
if self.selection.get() == self.AUTO_CREATE:
|
||||||
|
self.name_frame.pack(fill="x")
|
||||||
|
else:
|
||||||
|
self.name_frame.pack_forget()
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
selected = self.selection.get()
|
||||||
|
if selected == self.AUTO_CREATE:
|
||||||
|
vrf_name = self.new_vrf_name.get().strip()
|
||||||
|
if not vrf_name:
|
||||||
|
messagebox.showwarning("VRF-Name fehlt", "Bitte einen Namen für die neue VRF eingeben.", parent=self)
|
||||||
|
return
|
||||||
|
self.result = {"create": True, "id": None, "name": vrf_name}
|
||||||
|
else:
|
||||||
|
self.result = {"create": False, "id": self.vrf_map[selected], "name": None}
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
class PrefixCreationDialog(tk.Toplevel):
|
||||||
|
"""Ask explicitly before creating a missing IPv4 or IPv6 prefix."""
|
||||||
|
|
||||||
|
AUTO_CREATE = "➕ Neue VRF anlegen"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent,
|
||||||
|
address,
|
||||||
|
detected_prefix=True,
|
||||||
|
vrfs=None,
|
||||||
|
default_vrf_id=None,
|
||||||
|
tenant_name="Mandant",
|
||||||
|
):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.title("Subnetz anlegen?")
|
||||||
|
self.geometry("560x455")
|
||||||
|
self.resizable(False, False)
|
||||||
|
self.transient(parent)
|
||||||
|
self.grab_set()
|
||||||
|
self.result = None
|
||||||
|
self.vrf_map = {}
|
||||||
|
|
||||||
|
vrf_values = []
|
||||||
|
for vrf in vrfs or []:
|
||||||
|
rd = getattr(vrf, "rd", None)
|
||||||
|
display = f"{vrf.name} (RD: {rd})" if rd else vrf.name
|
||||||
|
self.vrf_map[display] = vrf.id
|
||||||
|
vrf_values.append(display)
|
||||||
|
vrf_values.append(self.AUTO_CREATE)
|
||||||
|
default_vrf = next(
|
||||||
|
(display for display, vrf_id in self.vrf_map.items() if vrf_id == default_vrf_id),
|
||||||
|
self.AUTO_CREATE,
|
||||||
|
)
|
||||||
|
self.vrf_selection = tk.StringVar(value=default_vrf)
|
||||||
|
self.new_vrf_name = tk.StringVar(value=f"{tenant_name} - VRF")
|
||||||
|
|
||||||
|
interface = ipaddress.ip_interface(address)
|
||||||
|
self.host_address = interface.ip
|
||||||
|
if interface.version == 4:
|
||||||
|
self.choices = [f"/{length}" for length in range(8, 25)]
|
||||||
|
default_length = max(8, min(interface.network.prefixlen, 24))
|
||||||
|
else:
|
||||||
|
common = [32, 40, 48, 52, 56, 60, 64, 80, 96, 112, 120, 124, 126, 127, 128]
|
||||||
|
if interface.network.prefixlen not in common:
|
||||||
|
common.append(interface.network.prefixlen)
|
||||||
|
self.choices = [f"/{length}" for length in sorted(set(common))]
|
||||||
|
default_length = interface.network.prefixlen
|
||||||
|
self.prefix_length = tk.StringVar(value=f"/{default_length}")
|
||||||
|
self.preview_text = tk.StringVar()
|
||||||
|
|
||||||
|
body = ttk.Frame(self, padding=22)
|
||||||
|
body.pack(fill="both", expand=True)
|
||||||
|
ttk.Label(body, text="Fehlendes Präfix", font=("Segoe UI Semibold", 14)).pack(anchor="w")
|
||||||
|
source_text = "vom Hypervisor erkannt" if detected_prefix else "aus der IP-Adresse geschätzt"
|
||||||
|
ttk.Label(
|
||||||
|
body,
|
||||||
|
text=(
|
||||||
|
f"IP-Adresse: {self.host_address}\n"
|
||||||
|
f"Präfixlänge: {source_text}\n\n"
|
||||||
|
"NetBox enthält noch kein exakt passendes Präfix. Bitte die gewünschte Subnetzgröße auswählen."
|
||||||
|
),
|
||||||
|
wraplength=470,
|
||||||
|
justify="left",
|
||||||
|
).pack(anchor="w", pady=(8, 14))
|
||||||
|
|
||||||
|
row = ttk.Frame(body)
|
||||||
|
row.pack(fill="x")
|
||||||
|
ttk.Label(row, text="Subnetzgröße").pack(side="left")
|
||||||
|
combo = ttk.Combobox(row, textvariable=self.prefix_length, values=self.choices, state="readonly", width=10)
|
||||||
|
combo.pack(side="left", padx=(12, 0))
|
||||||
|
combo.bind("<<ComboboxSelected>>", self.update_preview)
|
||||||
|
|
||||||
|
vrf_row = ttk.Frame(body)
|
||||||
|
vrf_row.pack(fill="x", pady=(12, 0))
|
||||||
|
ttk.Label(vrf_row, text="VRF").pack(side="left")
|
||||||
|
vrf_combo = ttk.Combobox(
|
||||||
|
vrf_row,
|
||||||
|
textvariable=self.vrf_selection,
|
||||||
|
values=vrf_values,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
vrf_combo.pack(side="left", fill="x", expand=True, padx=(12, 0))
|
||||||
|
vrf_combo.bind("<<ComboboxSelected>>", self.update_vrf_fields)
|
||||||
|
|
||||||
|
new_vrf_row = ttk.Frame(body)
|
||||||
|
new_vrf_row.pack(fill="x", pady=(10, 0))
|
||||||
|
ttk.Label(new_vrf_row, text="Neue VRF").pack(side="left")
|
||||||
|
self.new_vrf_entry = ttk.Entry(new_vrf_row, textvariable=self.new_vrf_name)
|
||||||
|
self.new_vrf_entry.pack(side="left", fill="x", expand=True, padx=(12, 0))
|
||||||
|
self.new_vrf_name.trace_add("write", self.update_preview)
|
||||||
|
ttk.Label(body, textvariable=self.preview_text, foreground="#334e68", font=("Segoe UI Semibold", 10)).pack(
|
||||||
|
anchor="w", pady=(14, 0)
|
||||||
|
)
|
||||||
|
if interface.version == 4:
|
||||||
|
ttk.Label(
|
||||||
|
body,
|
||||||
|
text="IPv4-Präfixe werden als kanonische Netzadresse mit .0 am Ende angelegt.",
|
||||||
|
foreground="#52606d",
|
||||||
|
).pack(anchor="w", pady=(6, 0))
|
||||||
|
|
||||||
|
buttons = ttk.Frame(body)
|
||||||
|
buttons.pack(anchor="e", pady=(22, 0))
|
||||||
|
ttk.Button(buttons, text="Nicht anlegen", command=self.destroy).pack(side="left", padx=4)
|
||||||
|
ttk.Button(buttons, text="Präfix anlegen", command=self.accept, style="Primary.TButton").pack(side="left", padx=4)
|
||||||
|
self.update_vrf_fields()
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||||
|
self.wait_window(self)
|
||||||
|
|
||||||
|
def selected_network(self):
|
||||||
|
length = int(self.prefix_length.get().lstrip("/"))
|
||||||
|
return ipaddress.ip_network(f"{self.host_address}/{length}", strict=False)
|
||||||
|
|
||||||
|
def update_vrf_fields(self, _event=None):
|
||||||
|
is_new = self.vrf_selection.get() == self.AUTO_CREATE
|
||||||
|
self.new_vrf_entry.configure(state="normal" if is_new else "disabled")
|
||||||
|
self.update_preview()
|
||||||
|
|
||||||
|
def update_preview(self, *_args):
|
||||||
|
vrf_name = self.vrf_selection.get() or "keine VRF"
|
||||||
|
if vrf_name == self.AUTO_CREATE:
|
||||||
|
vrf_name = self.new_vrf_name.get().strip() or "neue VRF ohne Namen"
|
||||||
|
self.preview_text.set(f"Wird angelegt: {self.selected_network()} · VRF: {vrf_name}")
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
network = self.selected_network()
|
||||||
|
selected_vrf = self.vrf_selection.get()
|
||||||
|
if selected_vrf == self.AUTO_CREATE:
|
||||||
|
vrf_name = self.new_vrf_name.get().strip()
|
||||||
|
if not vrf_name:
|
||||||
|
messagebox.showwarning("VRF-Name fehlt", "Bitte einen Namen für die neue VRF eingeben.", parent=self)
|
||||||
|
return
|
||||||
|
vrf_id = None
|
||||||
|
create_vrf = True
|
||||||
|
elif selected_vrf in self.vrf_map:
|
||||||
|
vrf_name = None
|
||||||
|
vrf_id = self.vrf_map[selected_vrf]
|
||||||
|
create_vrf = False
|
||||||
|
else:
|
||||||
|
messagebox.showwarning("VRF fehlt", "Bitte eine VRF für das Präfix auswählen.", parent=self)
|
||||||
|
return
|
||||||
|
if network.version == 4 and str(network.network_address).split(".")[-1] != "0":
|
||||||
|
messagebox.showerror(
|
||||||
|
"Ungültige Netzadresse",
|
||||||
|
"Das IPv4-Präfix muss mit einer .0-Netzadresse beginnen.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.result = {
|
||||||
|
"network": str(network),
|
||||||
|
"vrf_id": vrf_id,
|
||||||
|
"create_vrf": create_vrf,
|
||||||
|
"vrf_name": vrf_name,
|
||||||
|
}
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
class DetectedNetworksDialog(tk.Toplevel):
|
||||||
|
"""Review all newly detected networks and their VRFs in one import plan."""
|
||||||
|
|
||||||
|
AUTO_CREATE = "➕ Neue VRF anlegen"
|
||||||
|
|
||||||
|
def __init__(self, parent, networks, vrfs, default_vrf_id, tenant_name):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.title("Neue Netzwerke prüfen")
|
||||||
|
self.geometry("1060x620")
|
||||||
|
self.minsize(900, 480)
|
||||||
|
self.transient(parent)
|
||||||
|
self.grab_set()
|
||||||
|
self.result = None
|
||||||
|
self.rows = []
|
||||||
|
self.vrf_map = {}
|
||||||
|
|
||||||
|
vrf_values = []
|
||||||
|
for vrf in vrfs:
|
||||||
|
rd = getattr(vrf, "rd", None)
|
||||||
|
display = f"{vrf.name} (RD: {rd})" if rd else vrf.name
|
||||||
|
self.vrf_map[display] = vrf.id
|
||||||
|
vrf_values.append(display)
|
||||||
|
vrf_values.append(self.AUTO_CREATE)
|
||||||
|
default_vrf = next(
|
||||||
|
(display for display, vrf_id in self.vrf_map.items() if vrf_id == default_vrf_id),
|
||||||
|
vrf_values[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
body = ttk.Frame(self, padding=20)
|
||||||
|
body.pack(fill="both", expand=True)
|
||||||
|
ttk.Label(body, text="Neue Netzwerke erkannt", font=("Segoe UI Semibold", 16)).pack(anchor="w")
|
||||||
|
ttk.Label(
|
||||||
|
body,
|
||||||
|
text=(
|
||||||
|
"Nur noch nicht vorhandene Netze werden angezeigt. Subnetzgröße und VRF können pro Netz geändert "
|
||||||
|
"werden. Bei ‚Neue VRF anlegen‘ erscheint ein Namensfeld in derselben Zeile."
|
||||||
|
),
|
||||||
|
wraplength=980,
|
||||||
|
foreground="#52606d",
|
||||||
|
).pack(anchor="w", pady=(5, 14))
|
||||||
|
|
||||||
|
header = ttk.Frame(body)
|
||||||
|
header.pack(fill="x", padx=(2, 20))
|
||||||
|
ttk.Label(header, text="Anlegen", width=8).grid(row=0, column=0, sticky="w")
|
||||||
|
ttk.Label(header, text="Erkannte IP", width=21).grid(row=0, column=1, sticky="w")
|
||||||
|
ttk.Label(header, text="Größe", width=9).grid(row=0, column=2, sticky="w")
|
||||||
|
ttk.Label(header, text="Netzwerk", width=25).grid(row=0, column=3, sticky="w")
|
||||||
|
ttk.Label(header, text="VRF", width=34).grid(row=0, column=4, sticky="w")
|
||||||
|
|
||||||
|
container = ttk.Frame(body)
|
||||||
|
container.pack(fill="both", expand=True, pady=(4, 10))
|
||||||
|
canvas = tk.Canvas(container, borderwidth=0, highlightthickness=0)
|
||||||
|
scrollbar = ttk.Scrollbar(container, orient="vertical", command=canvas.yview)
|
||||||
|
self.rows_frame = ttk.Frame(canvas)
|
||||||
|
self.rows_frame.bind(
|
||||||
|
"<Configure>",
|
||||||
|
lambda _event: canvas.configure(scrollregion=canvas.bbox("all")),
|
||||||
|
)
|
||||||
|
window_id = canvas.create_window((0, 0), window=self.rows_frame, anchor="nw")
|
||||||
|
canvas.bind("<Configure>", lambda event: canvas.itemconfigure(window_id, width=event.width))
|
||||||
|
canvas.configure(yscrollcommand=scrollbar.set)
|
||||||
|
canvas.pack(side="left", fill="both", expand=True)
|
||||||
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
|
for index, item in enumerate(networks):
|
||||||
|
self.add_network_row(index, item, vrf_values, default_vrf, tenant_name)
|
||||||
|
|
||||||
|
buttons = ttk.Frame(body)
|
||||||
|
buttons.pack(fill="x")
|
||||||
|
ttk.Button(buttons, text="Import abbrechen", command=self.destroy).pack(side="left")
|
||||||
|
ttk.Button(buttons, text="Ohne neue Netze fortfahren", command=self.skip_all).pack(side="right", padx=(8, 0))
|
||||||
|
ttk.Button(buttons, text="Auswahl übernehmen", command=self.accept, style="Primary.TButton").pack(side="right")
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||||
|
self.wait_window(self)
|
||||||
|
|
||||||
|
def add_network_row(self, index, item, vrf_values, default_vrf, tenant_name):
|
||||||
|
row_frame = ttk.Frame(self.rows_frame, padding=(4, 7))
|
||||||
|
row_frame.pack(fill="x")
|
||||||
|
enabled = tk.BooleanVar(value=True)
|
||||||
|
prefix_length = tk.StringVar(value=f"/{item['suggested_prefixlen']}")
|
||||||
|
network_text = tk.StringVar()
|
||||||
|
vrf_selection = tk.StringVar(value=default_vrf)
|
||||||
|
new_vrf_name = tk.StringVar(value=f"{tenant_name} - {item['suggested_network'].network_address}")
|
||||||
|
|
||||||
|
ttk.Checkbutton(row_frame, variable=enabled).grid(row=0, column=0, sticky="w", padx=(0, 12))
|
||||||
|
ttk.Label(row_frame, text=str(item["host_address"]), width=21).grid(row=0, column=1, sticky="w")
|
||||||
|
choices = self.prefix_choices(item["host_address"], item["detected_prefixlen"])
|
||||||
|
size_combo = ttk.Combobox(row_frame, textvariable=prefix_length, values=choices, state="readonly", width=7)
|
||||||
|
size_combo.grid(row=0, column=2, sticky="w", padx=(0, 12))
|
||||||
|
ttk.Label(row_frame, textvariable=network_text, width=25).grid(row=0, column=3, sticky="w")
|
||||||
|
vrf_combo = ttk.Combobox(
|
||||||
|
row_frame,
|
||||||
|
textvariable=vrf_selection,
|
||||||
|
values=vrf_values,
|
||||||
|
state="readonly",
|
||||||
|
width=34,
|
||||||
|
)
|
||||||
|
vrf_combo.grid(row=0, column=4, sticky="ew")
|
||||||
|
row_frame.columnconfigure(4, weight=1)
|
||||||
|
|
||||||
|
name_frame = ttk.Frame(row_frame)
|
||||||
|
ttk.Label(name_frame, text="Name der neuen VRF:").pack(side="left")
|
||||||
|
ttk.Entry(name_frame, textvariable=new_vrf_name).pack(side="left", fill="x", expand=True, padx=(8, 0))
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"frame": row_frame,
|
||||||
|
"enabled": enabled,
|
||||||
|
"host_address": item["host_address"],
|
||||||
|
"prefix_length": prefix_length,
|
||||||
|
"network_text": network_text,
|
||||||
|
"vrf_selection": vrf_selection,
|
||||||
|
"new_vrf_name": new_vrf_name,
|
||||||
|
"name_frame": name_frame,
|
||||||
|
}
|
||||||
|
self.rows.append(row)
|
||||||
|
|
||||||
|
size_combo.bind("<<ComboboxSelected>>", lambda _event, current=row: self.update_row(current))
|
||||||
|
vrf_combo.bind("<<ComboboxSelected>>", lambda _event, current=row: self.update_row(current))
|
||||||
|
self.update_row(row)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def prefix_choices(host_address, detected_prefixlen):
|
||||||
|
host = ipaddress.ip_address(host_address)
|
||||||
|
if host.version == 4:
|
||||||
|
return [f"/{length}" for length in range(8, 25)]
|
||||||
|
common = {32, 40, 48, 52, 56, 60, 64, 80, 96, 112, 120, 124, 126, 127, 128, detected_prefixlen}
|
||||||
|
return [f"/{length}" for length in sorted(common)]
|
||||||
|
|
||||||
|
def update_row(self, row):
|
||||||
|
length = int(row["prefix_length"].get().lstrip("/"))
|
||||||
|
network = ipaddress.ip_network(f"{row['host_address']}/{length}", strict=False)
|
||||||
|
row["network_text"].set(str(network))
|
||||||
|
if row["vrf_selection"].get() == self.AUTO_CREATE:
|
||||||
|
row["name_frame"].grid(row=1, column=4, sticky="ew", pady=(7, 0))
|
||||||
|
else:
|
||||||
|
row["name_frame"].grid_remove()
|
||||||
|
|
||||||
|
def skip_all(self):
|
||||||
|
self.result = []
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
decisions = {}
|
||||||
|
for row in self.rows:
|
||||||
|
if not row["enabled"].get():
|
||||||
|
continue
|
||||||
|
network = row["network_text"].get()
|
||||||
|
selected_vrf = row["vrf_selection"].get()
|
||||||
|
if selected_vrf == self.AUTO_CREATE:
|
||||||
|
vrf_name = row["new_vrf_name"].get().strip()
|
||||||
|
if not vrf_name:
|
||||||
|
messagebox.showwarning("VRF-Name fehlt", f"Bitte für {network} einen VRF-Namen eingeben.", parent=self)
|
||||||
|
return
|
||||||
|
decision = {"network": network, "vrf_id": None, "create_vrf": True, "vrf_name": vrf_name}
|
||||||
|
else:
|
||||||
|
decision = {
|
||||||
|
"network": network,
|
||||||
|
"vrf_id": self.vrf_map[selected_vrf],
|
||||||
|
"create_vrf": False,
|
||||||
|
"vrf_name": None,
|
||||||
|
}
|
||||||
|
existing = decisions.get(network)
|
||||||
|
if existing and existing != decision:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Netzwerk doppelt",
|
||||||
|
f"{network} wurde mit unterschiedlichen VRFs ausgewählt.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
decisions[network] = decision
|
||||||
|
self.result = list(decisions.values())
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkManagementDialog(tk.Toplevel):
|
||||||
|
"""Create multiple tenant VRFs and prefixes in one dedicated window."""
|
||||||
|
|
||||||
|
def __init__(self, parent, importer, default_tenant=""):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.importer = importer
|
||||||
|
self.title("VRFs & Netzwerke")
|
||||||
|
self.geometry("720x650")
|
||||||
|
self.minsize(650, 590)
|
||||||
|
self.transient(parent)
|
||||||
|
self.grab_set()
|
||||||
|
self.tenant_name = tk.StringVar(value=default_tenant)
|
||||||
|
self.vrf_selection = tk.StringVar()
|
||||||
|
self.vrf_map = {}
|
||||||
|
self.status_text = tk.StringVar(value="Bereit")
|
||||||
|
|
||||||
|
body = ttk.Frame(self, padding=22)
|
||||||
|
body.pack(fill="both", expand=True)
|
||||||
|
ttk.Label(body, text="VRFs & Netzwerke", font=("Segoe UI Semibold", 16)).pack(anchor="w")
|
||||||
|
ttk.Label(
|
||||||
|
body,
|
||||||
|
text="Mehrere Einträge jeweils zeilenweise erfassen und gesammelt in NetBox anlegen.",
|
||||||
|
foreground="#52606d",
|
||||||
|
).pack(anchor="w", pady=(4, 16))
|
||||||
|
|
||||||
|
tenant_row = ttk.Frame(body)
|
||||||
|
tenant_row.pack(fill="x")
|
||||||
|
ttk.Label(tenant_row, text="Mandant", width=14).pack(side="left")
|
||||||
|
tenant_combo = ttk.Combobox(
|
||||||
|
tenant_row,
|
||||||
|
textvariable=self.tenant_name,
|
||||||
|
values=sorted(importer.tenant_map, key=str.casefold),
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
tenant_combo.pack(side="left", fill="x", expand=True)
|
||||||
|
tenant_combo.bind("<<ComboboxSelected>>", self.refresh_vrfs)
|
||||||
|
|
||||||
|
vrf_box = ttk.LabelFrame(body, text="Mehrere VRFs anlegen", padding=12)
|
||||||
|
vrf_box.pack(fill="both", expand=True, pady=(16, 10))
|
||||||
|
ttk.Label(vrf_box, text="Ein VRF-Name pro Zeile:").pack(anchor="w")
|
||||||
|
self.vrf_names = tk.Text(vrf_box, height=6, font=("Segoe UI", 10))
|
||||||
|
self.vrf_names.pack(fill="both", expand=True, pady=(6, 10))
|
||||||
|
ttk.Button(vrf_box, text="Alle VRFs anlegen", command=self.create_vrfs, style="Primary.TButton").pack(
|
||||||
|
anchor="e"
|
||||||
|
)
|
||||||
|
|
||||||
|
prefix_box = ttk.LabelFrame(body, text="Mehrere Netzwerke anlegen", padding=12)
|
||||||
|
prefix_box.pack(fill="both", expand=True, pady=(0, 10))
|
||||||
|
vrf_row = ttk.Frame(prefix_box)
|
||||||
|
vrf_row.pack(fill="x")
|
||||||
|
ttk.Label(vrf_row, text="Ziel-VRF", width=14).pack(side="left")
|
||||||
|
self.vrf_combo = ttk.Combobox(vrf_row, textvariable=self.vrf_selection, state="readonly")
|
||||||
|
self.vrf_combo.pack(side="left", fill="x", expand=True)
|
||||||
|
ttk.Label(prefix_box, text="Ein Präfix pro Zeile, z. B. 10.10.100.0/24:").pack(anchor="w", pady=(12, 0))
|
||||||
|
self.prefixes = tk.Text(prefix_box, height=6, font=("Consolas", 10))
|
||||||
|
self.prefixes.pack(fill="both", expand=True, pady=(6, 10))
|
||||||
|
ttk.Button(
|
||||||
|
prefix_box,
|
||||||
|
text="Alle Netzwerke anlegen",
|
||||||
|
command=self.create_prefixes,
|
||||||
|
style="Primary.TButton",
|
||||||
|
).pack(anchor="e")
|
||||||
|
|
||||||
|
footer = ttk.Frame(body)
|
||||||
|
footer.pack(fill="x", pady=(4, 0))
|
||||||
|
ttk.Label(footer, textvariable=self.status_text, foreground="#334e68").pack(side="left")
|
||||||
|
ttk.Button(footer, text="Schließen", command=self.destroy).pack(side="right")
|
||||||
|
|
||||||
|
if not self.tenant_name.get() and importer.tenant_map:
|
||||||
|
self.tenant_name.set(sorted(importer.tenant_map, key=str.casefold)[0])
|
||||||
|
self.refresh_vrfs()
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||||
|
self.wait_window(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def text_lines(widget):
|
||||||
|
return list(dict.fromkeys(line.strip() for line in widget.get("1.0", "end").splitlines() if line.strip()))
|
||||||
|
|
||||||
|
def refresh_vrfs(self, _event=None):
|
||||||
|
tenant_name = self.tenant_name.get()
|
||||||
|
tenant_id = self.importer.tenant_map.get(tenant_name)
|
||||||
|
vrfs = self.importer.tenant_vrfs(tenant_id) if tenant_id else []
|
||||||
|
self.vrf_map = {}
|
||||||
|
values = []
|
||||||
|
for vrf in vrfs:
|
||||||
|
rd = getattr(vrf, "rd", None)
|
||||||
|
display = f"{vrf.name} (RD: {rd})" if rd else vrf.name
|
||||||
|
self.vrf_map[display] = vrf.id
|
||||||
|
values.append(display)
|
||||||
|
self.vrf_combo["values"] = values
|
||||||
|
if self.vrf_selection.get() not in self.vrf_map:
|
||||||
|
self.vrf_selection.set(values[0] if values else "")
|
||||||
|
|
||||||
|
def create_vrfs(self):
|
||||||
|
names = self.text_lines(self.vrf_names)
|
||||||
|
tenant_name = self.tenant_name.get()
|
||||||
|
if not tenant_name or not names:
|
||||||
|
messagebox.showwarning("Angaben fehlen", "Bitte Mandant und mindestens einen VRF-Namen angeben.", parent=self)
|
||||||
|
return
|
||||||
|
created, reused, errors = self.importer.create_tenant_vrfs_batch(tenant_name, names)
|
||||||
|
self.status_text.set(f"VRFs: {created} angelegt, {reused} bereits vorhanden")
|
||||||
|
self.refresh_vrfs()
|
||||||
|
if not errors:
|
||||||
|
self.vrf_names.delete("1.0", "end")
|
||||||
|
else:
|
||||||
|
messagebox.showwarning("VRFs teilweise angelegt", "\n".join(errors), parent=self)
|
||||||
|
|
||||||
|
def create_prefixes(self):
|
||||||
|
networks = self.text_lines(self.prefixes)
|
||||||
|
tenant_name = self.tenant_name.get()
|
||||||
|
vrf_id = self.vrf_map.get(self.vrf_selection.get())
|
||||||
|
if not tenant_name or not vrf_id or not networks:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Angaben fehlen",
|
||||||
|
"Bitte Mandant, Ziel-VRF und mindestens ein Netzwerk angeben.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
created, reused, canonical, errors = self.importer.create_prefixes_batch(
|
||||||
|
tenant_name,
|
||||||
|
vrf_id,
|
||||||
|
networks,
|
||||||
|
)
|
||||||
|
self.status_text.set(f"Netzwerke: {created} angelegt, {reused} bereits vorhanden")
|
||||||
|
if canonical:
|
||||||
|
self.prefixes.delete("1.0", "end")
|
||||||
|
self.prefixes.insert("1.0", "\n".join(canonical))
|
||||||
|
if errors:
|
||||||
|
messagebox.showwarning("Netzwerke teilweise angelegt", "\n".join(errors), parent=self)
|
||||||
|
else:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Netzwerke verarbeitet",
|
||||||
|
f"{created} Netzwerk(e) angelegt, {reused} bereits vorhanden.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NetBoxVMImporter:
|
class NetBoxVMImporter:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
@@ -189,6 +729,7 @@ class NetBoxVMImporter:
|
|||||||
self.cluster_map = {}
|
self.cluster_map = {}
|
||||||
self.cluster_tenants = {}
|
self.cluster_tenants = {}
|
||||||
self.tenant_map = {}
|
self.tenant_map = {}
|
||||||
|
self.vrfs = []
|
||||||
self.platforms = []
|
self.platforms = []
|
||||||
self.profile_name = tk.StringVar()
|
self.profile_name = tk.StringVar()
|
||||||
self.netbox_ignore_ssl = tk.BooleanVar(value=False)
|
self.netbox_ignore_ssl = tk.BooleanVar(value=False)
|
||||||
@@ -528,6 +1069,7 @@ class NetBoxVMImporter:
|
|||||||
self.log("Lade NetBox-Zieldaten …")
|
self.log("Lade NetBox-Zieldaten …")
|
||||||
clusters = list(self.nb.virtualization.clusters.all())
|
clusters = list(self.nb.virtualization.clusters.all())
|
||||||
tenants = list(self.nb.tenancy.tenants.all())
|
tenants = list(self.nb.tenancy.tenants.all())
|
||||||
|
self.vrfs = list(self.nb.ipam.vrfs.all())
|
||||||
self.cluster_map = {item.name: item.id for item in clusters}
|
self.cluster_map = {item.name: item.id for item in clusters}
|
||||||
self.tenant_map = {item.name: item.id for item in tenants}
|
self.tenant_map = {item.name: item.id for item in tenants}
|
||||||
self.cluster_tenants = {}
|
self.cluster_tenants = {}
|
||||||
@@ -621,7 +1163,7 @@ class NetBoxVMImporter:
|
|||||||
mac = getattr(net, "macAddress", None)
|
mac = getattr(net, "macAddress", None)
|
||||||
if not mac:
|
if not mac:
|
||||||
continue
|
continue
|
||||||
ips = [ip for ip in (getattr(net, "ipAddress", None) or []) if self.valid_guest_ip(ip)]
|
ips = self.vmware_guest_ips(net)
|
||||||
interfaces.append({"name": getattr(net, "device", None) or f"NIC-{index}", "mac": mac, "ips": ips})
|
interfaces.append({"name": getattr(net, "device", None) or f"NIC-{index}", "mac": mac, "ips": ips})
|
||||||
return {
|
return {
|
||||||
"name": properties["name"],
|
"name": properties["name"],
|
||||||
@@ -636,6 +1178,29 @@ class NetBoxVMImporter:
|
|||||||
"interfaces": interfaces,
|
"interfaces": interfaces,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def vmware_guest_ips(self, net):
|
||||||
|
"""Prefer VMware-reported prefix lengths and retain legacy IP fallback."""
|
||||||
|
ips = []
|
||||||
|
ip_config = getattr(net, "ipConfig", None)
|
||||||
|
for entry in (getattr(ip_config, "ipAddress", None) or []):
|
||||||
|
value = getattr(entry, "ipAddress", None)
|
||||||
|
prefix = getattr(entry, "prefixLength", None)
|
||||||
|
if value and self.valid_guest_ip(value):
|
||||||
|
ips.append(f"{value}/{prefix}" if prefix is not None else value)
|
||||||
|
|
||||||
|
known_addresses = {
|
||||||
|
str(ipaddress.ip_interface(value).ip if "/" in value else ipaddress.ip_address(value))
|
||||||
|
for value in ips
|
||||||
|
}
|
||||||
|
for value in (getattr(net, "ipAddress", None) or []):
|
||||||
|
if not self.valid_guest_ip(value):
|
||||||
|
continue
|
||||||
|
normalized = str(ipaddress.ip_address(value))
|
||||||
|
if normalized not in known_addresses:
|
||||||
|
ips.append(value)
|
||||||
|
known_addresses.add(normalized)
|
||||||
|
return ips
|
||||||
|
|
||||||
def proxmox_session(self, source):
|
def proxmox_session(self, source):
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
session.verify = not source.get("ignore_ssl")
|
session.verify = not source.get("ignore_ssl")
|
||||||
@@ -845,11 +1410,21 @@ class NetBoxVMImporter:
|
|||||||
tenant_id = self.tenant_map[tenant_name]
|
tenant_id = self.tenant_map[tenant_name]
|
||||||
self.tenant_combo.set(tenant_name)
|
self.tenant_combo.set(tenant_name)
|
||||||
self.log(f"Mandant bestätigt: {tenant_name}")
|
self.log(f"Mandant bestätigt: {tenant_name}")
|
||||||
|
vrf_id = self.select_or_create_vrf(tenant_name, tenant_id, cluster_name)
|
||||||
|
if vrf_id is None:
|
||||||
|
self.log("Synchronisierung ohne VRF-Auswahl abgebrochen")
|
||||||
|
return
|
||||||
|
self.synced_prefixes = set()
|
||||||
|
prefix_plans = self.prepare_network_import(selected, tenant_name, tenant_id, vrf_id)
|
||||||
|
if prefix_plans is None:
|
||||||
|
self.log("Synchronisierung in der Netzwerkprüfung abgebrochen")
|
||||||
|
return
|
||||||
|
self.prefix_plans = prefix_plans
|
||||||
failures = []
|
failures = []
|
||||||
for item_id in selected:
|
for item_id in selected:
|
||||||
vm_data = self.vms[int(item_id)]
|
vm_data = self.vms[int(item_id)]
|
||||||
try:
|
try:
|
||||||
self.sync_vm(vm_data, cluster_id, tenant_id)
|
self.sync_vm(vm_data, cluster_id, tenant_id, vrf_id)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
failures.append(f"{vm_data['name']}: {error}")
|
failures.append(f"{vm_data['name']}: {error}")
|
||||||
self.log(f"FEHLER {vm_data['name']}: {error}")
|
self.log(f"FEHLER {vm_data['name']}: {error}")
|
||||||
@@ -859,14 +1434,210 @@ class NetBoxVMImporter:
|
|||||||
messagebox.showinfo("Fertig", "VM-Import und Synchronisierung abgeschlossen.")
|
messagebox.showinfo("Fertig", "VM-Import und Synchronisierung abgeschlossen.")
|
||||||
self.log("Synchronisierung abgeschlossen")
|
self.log("Synchronisierung abgeschlossen")
|
||||||
|
|
||||||
def sync_vm(self, source_vm, cluster_id, tenant_id):
|
def select_or_create_vrf(self, tenant_name, tenant_id, cluster_name):
|
||||||
|
tenant_vrfs = self.tenant_vrfs(tenant_id)
|
||||||
|
dialog = VRFSelectionDialog(
|
||||||
|
self.root,
|
||||||
|
tenant_name,
|
||||||
|
cluster_name,
|
||||||
|
sorted(tenant_vrfs, key=lambda item: item.name.casefold()),
|
||||||
|
)
|
||||||
|
if dialog.result is None:
|
||||||
|
return None
|
||||||
|
if not dialog.result["create"]:
|
||||||
|
vrf_id = dialog.result["id"]
|
||||||
|
selected = next(vrf for vrf in tenant_vrfs if vrf.id == vrf_id)
|
||||||
|
self.log(f"Vorhandene VRF ausgewählt: {selected.name}")
|
||||||
|
return vrf_id
|
||||||
|
|
||||||
|
vrf_name = dialog.result["name"]
|
||||||
|
try:
|
||||||
|
return self.get_or_create_tenant_vrf(vrf_name, tenant_id)
|
||||||
|
except Exception as error:
|
||||||
|
messagebox.showerror("VRF konnte nicht erstellt werden", str(error))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def tenant_vrfs(self, tenant_id):
|
||||||
|
return sorted(
|
||||||
|
(
|
||||||
|
vrf for vrf in self.vrfs
|
||||||
|
if getattr(getattr(vrf, "tenant", None), "id", None) == tenant_id
|
||||||
|
),
|
||||||
|
key=lambda item: item.name.casefold(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def prepare_network_import(self, selected_items, tenant_name, tenant_id, default_vrf_id):
|
||||||
|
detected = {}
|
||||||
|
for item_id in selected_items:
|
||||||
|
vm_data = self.vms[int(item_id)]
|
||||||
|
for interface in vm_data.get("interfaces", []):
|
||||||
|
for ip_value in dict.fromkeys(interface.get("ips", [])):
|
||||||
|
address = self.ip_with_prefix(ip_value)
|
||||||
|
if not address:
|
||||||
|
continue
|
||||||
|
parsed = ipaddress.ip_interface(address)
|
||||||
|
if parsed.version == 4:
|
||||||
|
suggested_prefixlen = 24
|
||||||
|
else:
|
||||||
|
suggested_prefixlen = parsed.network.prefixlen
|
||||||
|
suggested_network = ipaddress.ip_network(
|
||||||
|
f"{parsed.ip}/{suggested_prefixlen}",
|
||||||
|
strict=False,
|
||||||
|
)
|
||||||
|
key = str(suggested_network)
|
||||||
|
detected.setdefault(
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
"host_address": parsed.ip,
|
||||||
|
"host_addresses": set(),
|
||||||
|
"detected_prefixlen": parsed.network.prefixlen,
|
||||||
|
"suggested_prefixlen": suggested_prefixlen,
|
||||||
|
"suggested_network": suggested_network,
|
||||||
|
},
|
||||||
|
)["host_addresses"].add(parsed.ip)
|
||||||
|
|
||||||
|
tenant_vrf_ids = {vrf.id for vrf in self.tenant_vrfs(tenant_id)}
|
||||||
|
self.preexisting_prefixes = []
|
||||||
|
missing = []
|
||||||
|
for item in detected.values():
|
||||||
|
group_is_missing = False
|
||||||
|
for host in item["host_addresses"]:
|
||||||
|
covering = self.find_covering_prefixes(host, tenant_vrf_ids)
|
||||||
|
default_matches = [match for match in covering if match[3] == default_vrf_id]
|
||||||
|
matching_vrfs = {match[3] for match in covering}
|
||||||
|
reusable = None
|
||||||
|
if default_matches:
|
||||||
|
reusable = max(default_matches, key=lambda match: match[0])
|
||||||
|
elif len(matching_vrfs) == 1:
|
||||||
|
reusable = max(covering, key=lambda match: match[0])
|
||||||
|
if reusable:
|
||||||
|
_, prefix, existing_network, existing_vrf_id = reusable
|
||||||
|
cached = (prefix, existing_network, existing_vrf_id)
|
||||||
|
if cached not in self.preexisting_prefixes:
|
||||||
|
self.preexisting_prefixes.append(cached)
|
||||||
|
else:
|
||||||
|
group_is_missing = True
|
||||||
|
if group_is_missing:
|
||||||
|
missing.append(item)
|
||||||
|
|
||||||
|
if not missing:
|
||||||
|
self.log("Keine neuen Netzwerke erkannt")
|
||||||
|
return []
|
||||||
|
|
||||||
|
dialog = DetectedNetworksDialog(
|
||||||
|
self.root,
|
||||||
|
missing,
|
||||||
|
self.tenant_vrfs(tenant_id),
|
||||||
|
default_vrf_id,
|
||||||
|
tenant_name,
|
||||||
|
)
|
||||||
|
if dialog.result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for decision in dialog.result:
|
||||||
|
if decision.get("create_vrf"):
|
||||||
|
decision["vrf_id"] = self.get_or_create_tenant_vrf(decision["vrf_name"], tenant_id)
|
||||||
|
decision["create_vrf"] = False
|
||||||
|
self.log(f"Netzwerkplan bestätigt: {len(dialog.result)} neue(s) Netzwerk(e)")
|
||||||
|
return dialog.result
|
||||||
|
|
||||||
|
def open_network_manager(self):
|
||||||
|
if self.nb is None or not self.tenant_map:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Keine NetBox-Verbindung",
|
||||||
|
"Bitte zuerst mit NetBox verbinden und die Daten laden.",
|
||||||
|
parent=self.root,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
NetworkManagementDialog(self.root, self, self.tenant_combo.get())
|
||||||
|
|
||||||
|
def create_tenant_vrfs_batch(self, tenant_name, names):
|
||||||
|
tenant_id = self.tenant_map[tenant_name]
|
||||||
|
created = 0
|
||||||
|
reused = 0
|
||||||
|
errors = []
|
||||||
|
for name in names:
|
||||||
|
exists = any(vrf.name == name for vrf in self.tenant_vrfs(tenant_id))
|
||||||
|
try:
|
||||||
|
self.get_or_create_tenant_vrf(name, tenant_id)
|
||||||
|
if exists:
|
||||||
|
reused += 1
|
||||||
|
else:
|
||||||
|
created += 1
|
||||||
|
except Exception as error:
|
||||||
|
errors.append(f"{name}: {error}")
|
||||||
|
self.log(f"FEHLER VRF {name}: {error}")
|
||||||
|
return created, reused, errors
|
||||||
|
|
||||||
|
def find_prefixes_in_vrf(self, network, vrf_id):
|
||||||
|
return [
|
||||||
|
prefix for prefix in self.nb.ipam.prefixes.filter(prefix=str(network))
|
||||||
|
if getattr(getattr(prefix, "vrf", None), "id", None) == vrf_id
|
||||||
|
]
|
||||||
|
|
||||||
|
def create_prefixes_batch(self, tenant_name, vrf_id, values):
|
||||||
|
tenant_id = self.tenant_map[tenant_name]
|
||||||
|
created = 0
|
||||||
|
reused = 0
|
||||||
|
canonical = []
|
||||||
|
errors = []
|
||||||
|
for value in values:
|
||||||
|
try:
|
||||||
|
network = ipaddress.ip_network(value, strict=False)
|
||||||
|
if network.version == 4 and str(network.network_address).split(".")[-1] != "0":
|
||||||
|
raise ValueError("IPv4-Netzadresse muss mit .0 enden")
|
||||||
|
network_text = str(network)
|
||||||
|
candidates = self.find_prefixes_in_vrf(network_text, vrf_id)
|
||||||
|
if len(candidates) > 1:
|
||||||
|
raise ValueError("mehrere identische Präfixe in dieser VRF gefunden")
|
||||||
|
if candidates:
|
||||||
|
candidates[0].update({"tenant": tenant_id})
|
||||||
|
reused += 1
|
||||||
|
else:
|
||||||
|
self.nb.ipam.prefixes.create({
|
||||||
|
"prefix": network_text,
|
||||||
|
"status": "active",
|
||||||
|
"tenant": tenant_id,
|
||||||
|
"vrf": vrf_id,
|
||||||
|
})
|
||||||
|
created += 1
|
||||||
|
canonical.append(network_text)
|
||||||
|
self.log(f"Netzwerk verarbeitet: {network_text}")
|
||||||
|
except Exception as error:
|
||||||
|
errors.append(f"{value}: {error}")
|
||||||
|
self.log(f"FEHLER Netzwerk {value}: {error}")
|
||||||
|
return created, reused, canonical, errors
|
||||||
|
|
||||||
|
def get_or_create_tenant_vrf(self, vrf_name, tenant_id):
|
||||||
|
existing = next(
|
||||||
|
(
|
||||||
|
vrf for vrf in self.vrfs
|
||||||
|
if vrf.name == vrf_name
|
||||||
|
and getattr(getattr(vrf, "tenant", None), "id", None) == tenant_id
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
self.log(f"Vorhandene VRF verwendet: {vrf_name}")
|
||||||
|
return existing.id
|
||||||
|
|
||||||
|
vrf = self.nb.ipam.vrfs.create({
|
||||||
|
"name": vrf_name,
|
||||||
|
"tenant": tenant_id,
|
||||||
|
"enforce_unique": True,
|
||||||
|
"description": "Automatisch erstellt durch NetBox VM Import",
|
||||||
|
})
|
||||||
|
self.vrfs.append(vrf)
|
||||||
|
self.log(f"VRF erstellt: {vrf_name}")
|
||||||
|
return vrf.id
|
||||||
|
|
||||||
|
def sync_vm(self, source_vm, cluster_id, tenant_id, vrf_id):
|
||||||
self.log(f"Synchronisiere {source_vm['name']} ({source_vm['source']}) …")
|
self.log(f"Synchronisiere {source_vm['name']} ({source_vm['source']}) …")
|
||||||
payload = {
|
payload = {
|
||||||
"name": source_vm["name"], "cluster": cluster_id, "status": source_vm["status"],
|
"name": source_vm["name"], "cluster": cluster_id, "status": source_vm["status"],
|
||||||
"vcpus": source_vm["vcpus"], "memory": source_vm["memory"], "disk": source_vm["disk"],
|
"vcpus": source_vm["vcpus"], "memory": source_vm["memory"], "disk": source_vm["disk"],
|
||||||
|
"tenant": tenant_id,
|
||||||
}
|
}
|
||||||
if tenant_id:
|
|
||||||
payload["tenant"] = tenant_id
|
|
||||||
platform_id = self.get_platform_id(source_vm.get("guest_os"))
|
platform_id = self.get_platform_id(source_vm.get("guest_os"))
|
||||||
if platform_id:
|
if platform_id:
|
||||||
payload["platform"] = platform_id
|
payload["platform"] = platform_id
|
||||||
@@ -898,16 +1669,31 @@ class NetBoxVMImporter:
|
|||||||
address = self.ip_with_prefix(ip_value)
|
address = self.ip_with_prefix(ip_value)
|
||||||
if not address:
|
if not address:
|
||||||
continue
|
continue
|
||||||
|
effective_vrf_id = self.sync_prefix_tenant(
|
||||||
|
address,
|
||||||
|
tenant_id,
|
||||||
|
prefix_is_detected="/" in ip_value,
|
||||||
|
default_vrf_id=vrf_id,
|
||||||
|
)
|
||||||
ip_payload = {
|
ip_payload = {
|
||||||
"address": address, "status": "active",
|
"address": address, "status": "active",
|
||||||
|
"tenant": tenant_id,
|
||||||
|
"vrf": effective_vrf_id,
|
||||||
"assigned_object_type": "virtualization.vminterface",
|
"assigned_object_type": "virtualization.vminterface",
|
||||||
"assigned_object_id": netbox_interface.id,
|
"assigned_object_id": netbox_interface.id,
|
||||||
}
|
}
|
||||||
ip_object = self.nb.ipam.ip_addresses.get(address=address)
|
try:
|
||||||
if ip_object:
|
host_address = str(ipaddress.ip_interface(address).ip)
|
||||||
ip_object.update(ip_payload)
|
ip_object = self.nb.ipam.ip_addresses.get(
|
||||||
else:
|
address=host_address,
|
||||||
ip_object = self.nb.ipam.ip_addresses.create(ip_payload)
|
vrf_id=effective_vrf_id,
|
||||||
|
)
|
||||||
|
if ip_object:
|
||||||
|
ip_object.update(ip_payload)
|
||||||
|
else:
|
||||||
|
ip_object = self.nb.ipam.ip_addresses.create(ip_payload)
|
||||||
|
except Exception as error:
|
||||||
|
raise RuntimeError(f"IP-Adresse {address}: {error}") from error
|
||||||
version = ipaddress.ip_interface(address).version
|
version = ipaddress.ip_interface(address).version
|
||||||
if version == 4 and not primary_ipv4:
|
if version == 4 and not primary_ipv4:
|
||||||
primary_ipv4 = ip_object.id
|
primary_ipv4 = ip_object.id
|
||||||
@@ -922,6 +1708,111 @@ class NetBoxVMImporter:
|
|||||||
if primary:
|
if primary:
|
||||||
netbox_vm.update(primary)
|
netbox_vm.update(primary)
|
||||||
|
|
||||||
|
def find_covering_prefixes(self, host_address, allowed_vrf_ids):
|
||||||
|
host = ipaddress.ip_address(host_address)
|
||||||
|
host_prefix = f"{host}/{host.max_prefixlen}"
|
||||||
|
try:
|
||||||
|
candidates = list(self.nb.ipam.prefixes.filter(contains_or_equals=host_prefix))
|
||||||
|
except Exception as error:
|
||||||
|
self.log(f"Hinweis: Präfixsuche für {host} nicht möglich: {error}")
|
||||||
|
return []
|
||||||
|
matches = []
|
||||||
|
for prefix in candidates:
|
||||||
|
prefix_vrf_id = getattr(getattr(prefix, "vrf", None), "id", None)
|
||||||
|
if prefix_vrf_id not in allowed_vrf_ids:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
parsed = ipaddress.ip_network(str(prefix.prefix))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if host in parsed:
|
||||||
|
matches.append((parsed.prefixlen, prefix, str(parsed), prefix_vrf_id))
|
||||||
|
return matches
|
||||||
|
|
||||||
|
def sync_prefix_tenant(self, address, tenant_id, prefix_is_detected, default_vrf_id):
|
||||||
|
"""Assign a tenant and apply the previously confirmed network import plan."""
|
||||||
|
if not hasattr(self, "synced_prefixes"):
|
||||||
|
self.synced_prefixes = set()
|
||||||
|
if not hasattr(self, "prefix_plans"):
|
||||||
|
self.prefix_plans = []
|
||||||
|
parsed_interface = ipaddress.ip_interface(address)
|
||||||
|
host_address = parsed_interface.ip
|
||||||
|
network = str(parsed_interface.network)
|
||||||
|
vrf_id = default_vrf_id
|
||||||
|
|
||||||
|
for existing_prefix, existing_network, existing_vrf_id in getattr(self, "preexisting_prefixes", []):
|
||||||
|
if host_address in ipaddress.ip_network(existing_network):
|
||||||
|
existing_prefix.update({"tenant": tenant_id})
|
||||||
|
self.synced_prefixes.add((existing_network, existing_vrf_id))
|
||||||
|
self.log(f"Vorhandenes Netzwerk wiederverwendet: {existing_network}")
|
||||||
|
return existing_vrf_id
|
||||||
|
|
||||||
|
for cached_network, cached_vrf in self.synced_prefixes:
|
||||||
|
if host_address in ipaddress.ip_network(cached_network):
|
||||||
|
return cached_vrf
|
||||||
|
|
||||||
|
cache_key = (network, vrf_id)
|
||||||
|
candidates = self.find_prefixes_in_vrf(network, vrf_id)
|
||||||
|
|
||||||
|
if len(candidates) > 1:
|
||||||
|
self.log(
|
||||||
|
f"Präfix {network} nicht geändert: mehrere passende VRFs gefunden"
|
||||||
|
)
|
||||||
|
return vrf_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
if candidates:
|
||||||
|
candidates[0].update({"tenant": tenant_id})
|
||||||
|
self.log(f"Mandant am Präfix aktualisiert: {network}")
|
||||||
|
else:
|
||||||
|
tenant_vrf_ids = {vrf.id for vrf in self.tenant_vrfs(tenant_id)}
|
||||||
|
covering = self.find_covering_prefixes(host_address, tenant_vrf_ids)
|
||||||
|
default_matches = [item for item in covering if item[3] == vrf_id]
|
||||||
|
matching_vrfs = {item[3] for item in covering}
|
||||||
|
reusable = None
|
||||||
|
if default_matches:
|
||||||
|
reusable = max(default_matches, key=lambda item: item[0])
|
||||||
|
elif len(matching_vrfs) == 1:
|
||||||
|
reusable = max(covering, key=lambda item: item[0])
|
||||||
|
if reusable:
|
||||||
|
_, existing_prefix, existing_network, existing_vrf_id = reusable
|
||||||
|
existing_prefix.update({"tenant": tenant_id})
|
||||||
|
self.synced_prefixes.add((existing_network, existing_vrf_id))
|
||||||
|
self.log(f"Vorhandenes Netzwerk wiederverwendet: {existing_network}")
|
||||||
|
return existing_vrf_id
|
||||||
|
matching_plans = [
|
||||||
|
decision for decision in self.prefix_plans
|
||||||
|
if host_address in ipaddress.ip_network(decision["network"])
|
||||||
|
]
|
||||||
|
if not matching_plans:
|
||||||
|
self.log(f"Präfix für {host_address} nicht angelegt (Netzwerkplan)")
|
||||||
|
return vrf_id
|
||||||
|
decision = max(
|
||||||
|
matching_plans,
|
||||||
|
key=lambda item: ipaddress.ip_network(item["network"]).prefixlen,
|
||||||
|
)
|
||||||
|
network = decision["network"]
|
||||||
|
vrf_id = decision["vrf_id"]
|
||||||
|
cache_key = (network, vrf_id)
|
||||||
|
selected_candidates = self.find_prefixes_in_vrf(network, vrf_id)
|
||||||
|
if len(selected_candidates) > 1:
|
||||||
|
self.log(f"Präfix {network} nicht geändert: mehrere passende VRFs gefunden")
|
||||||
|
return vrf_id
|
||||||
|
if selected_candidates:
|
||||||
|
selected_candidates[0].update({"tenant": tenant_id})
|
||||||
|
self.log(f"Mandant am Präfix aktualisiert: {network}")
|
||||||
|
else:
|
||||||
|
prefix_payload = {"prefix": network, "status": "active", "tenant": tenant_id}
|
||||||
|
if vrf_id is not None:
|
||||||
|
prefix_payload["vrf"] = vrf_id
|
||||||
|
self.nb.ipam.prefixes.create(prefix_payload)
|
||||||
|
self.log(f"Präfix nach Bestätigung erstellt: {network}")
|
||||||
|
except Exception as error:
|
||||||
|
raise RuntimeError(f"Präfix {network}: {error}") from error
|
||||||
|
|
||||||
|
self.synced_prefixes.add(cache_key)
|
||||||
|
return vrf_id
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Reference in New Issue
Block a user