diff --git a/README.md b/README.md index abc9d18..5bab27f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Windows-Desktopanwendung zum Importieren und Synchronisieren virtueller Maschine - 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 - Optionale Unterstützung selbstsignierter TLS-Zertifikate - Synchronisierung von VMs, Hardwaredaten, Interfaces und IP-Adressen - Übernahme primärer IPv4- und IPv6-Adressen diff --git a/app.py b/app.py index 710e5ba..ea3410b 100644 --- a/app.py +++ b/app.py @@ -134,6 +134,47 @@ class SourceDialog(tk.Toplevel): self.destroy() +class TenantConfirmationDialog(tk.Toplevel): + """Require explicit tenant confirmation before changing NetBox VMs.""" + + def __init__(self, parent, cluster_name, tenants, default_tenant=""): + super().__init__(parent) + self.title("Mandant bestätigen") + self.geometry("500x260") + self.resizable(False, False) + self.transient(parent) + self.grab_set() + self.result = None + self.tenant_name = tk.StringVar(value=default_tenant) + + body = ttk.Frame(self, padding=22) + body.pack(fill="both", expand=True) + ttk.Label(body, text="Mandant für die Synchronisierung", font=("Segoe UI Semibold", 14)).pack(anchor="w") + ttk.Label( + body, + text=f"Cluster: {cluster_name}\n\nDer Mandant des Clusters wurde vorausgewählt. Bitte bestätigen oder einen anderen Mandanten auswählen.", + wraplength=450, + justify="left", + ).pack(anchor="w", pady=(8, 14)) + self.combo = ttk.Combobox(body, textvariable=self.tenant_name, values=tenants, state="readonly") + self.combo.pack(fill="x") + + buttons = ttk.Frame(body) + buttons.pack(anchor="e", pady=(20, 0)) + ttk.Button(buttons, text="Abbrechen", command=self.destroy).pack(side="left", padx=4) + ttk.Button(buttons, text="Mandant 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 accept(self): + tenant = self.tenant_name.get().strip() + if not tenant: + messagebox.showwarning("Mandant fehlt", "Bitte einen Mandanten auswählen.", parent=self) + return + self.result = tenant + self.destroy() + + class NetBoxVMImporter: def __init__(self, root): self.root = root @@ -146,6 +187,7 @@ class NetBoxVMImporter: self.profiles = {} self.current_sources = [] self.cluster_map = {} + self.cluster_tenants = {} self.tenant_map = {} self.platforms = [] self.profile_name = tk.StringVar() @@ -245,7 +287,8 @@ class NetBoxVMImporter: workspace.add(target_card, weight=1) workspace.add(vm_card, weight=4) self.cluster_combo = self.add_combo_field(target_card, 0, "Cluster") - self.tenant_combo = self.add_combo_field(target_card, 1, "Tenant") + self.tenant_combo = self.add_combo_field(target_card, 1, "Mandant") + self.cluster_combo.bind("<>", self.on_cluster_selected) target_card.columnconfigure(0, weight=1) vm_columns = ("name", "source", "kind", "status", "cpu", "memory") @@ -487,10 +530,39 @@ class NetBoxVMImporter: tenants = list(self.nb.tenancy.tenants.all()) self.cluster_map = {item.name: item.id for item in clusters} self.tenant_map = {item.name: item.id for item in tenants} + self.cluster_tenants = {} + for cluster in clusters: + tenant = getattr(cluster, "tenant", None) + if tenant: + tenant_name = getattr(tenant, "name", None) + tenant_id = getattr(tenant, "id", None) + if isinstance(tenant, dict): + tenant_name = tenant.get("name") + tenant_id = tenant.get("id") + if tenant_name and tenant_id: + self.cluster_tenants[cluster.name] = { + "name": tenant_name, + "id": tenant_id, + } + self.tenant_map.setdefault(tenant_name, tenant_id) self.cluster_combo["values"] = sorted(self.cluster_map, key=str.casefold) self.tenant_combo["values"] = sorted(self.tenant_map, key=str.casefold) self.platforms = list(self.nb.dcim.platforms.all()) + def on_cluster_selected(self, _event=None): + cluster_name = self.cluster_combo.get() + cluster_tenant = self.cluster_tenants.get(cluster_name) + if cluster_tenant: + self.tenant_combo.set(cluster_tenant["name"]) + self.status_text.set( + f"Mandant „{cluster_tenant['name']}“ vom Cluster „{cluster_name}“ übernommen" + ) + else: + self.tenant_combo.set("") + self.status_text.set( + f"Cluster „{cluster_name}“ hat keinen Mandanten – Auswahl vor dem Sync erforderlich" + ) + def load_vmware_source(self, source): self.log(f"Verbinde zu VMware: {source['name']} …") context = ssl._create_unverified_context() if source.get("ignore_ssl") else None @@ -753,11 +825,26 @@ class NetBoxVMImporter: if not selected: messagebox.showwarning("Hinweis", "Keine VMs ausgewählt.") return - cluster_id = self.cluster_map.get(self.cluster_combo.get()) - tenant_id = self.tenant_map.get(self.tenant_combo.get()) + cluster_name = self.cluster_combo.get() + cluster_id = self.cluster_map.get(cluster_name) if not cluster_id: messagebox.showerror("Fehler", "Bitte einen NetBox-Cluster auswählen.") return + inherited_tenant = self.cluster_tenants.get(cluster_name, {}).get("name", "") + default_tenant = self.tenant_combo.get() or inherited_tenant + confirmation = TenantConfirmationDialog( + self.root, + cluster_name, + sorted(self.tenant_map, key=str.casefold), + default_tenant, + ) + if confirmation.result is None: + self.log("Synchronisierung durch Benutzer abgebrochen") + return + tenant_name = confirmation.result + tenant_id = self.tenant_map[tenant_name] + self.tenant_combo.set(tenant_name) + self.log(f"Mandant bestätigt: {tenant_name}") failures = [] for item_id in selected: vm_data = self.vms[int(item_id)] diff --git a/dist/NetBox VM Import.exe b/dist/NetBox VM Import.exe index 052ae05..71af94a 100644 Binary files a/dist/NetBox VM Import.exe and b/dist/NetBox VM Import.exe differ diff --git a/netbox_vm_import.ico b/netbox_vm_import.ico index 4f67afa..04f774c 100644 Binary files a/netbox_vm_import.ico and b/netbox_vm_import.ico differ diff --git a/netbox_vm_import.png b/netbox_vm_import.png index cf9283f..9f660ed 100644 Binary files a/netbox_vm_import.png and b/netbox_vm_import.png differ