diff --git a/README.md b/README.md index 163ea27..da5955f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Windows-Desktopanwendung zum Synchronisieren virtueller Maschinen sowie HPE-/Aru - Gemeinsame, synchronisierte Kundenprofil- und NetBox-Zielansicht in beiden Tabs - Verschlüsselte SSH-Zugänge pro bestehendem NetBox-Switch - SSH-Erkennung für Aruba CX, ArubaOS-Switch/ProCurve und HPE Comware +- Durchsuchbare, nach Mandant filterbare Auswahl vorhandener NetBox-Geräte +- Automatische Aktivierung von global deaktiviertem LLDP auf unterstützten Switches - Synchronisierung von Switch-Schnittstellen und optionalen LLDP-Verbindungen als NetBox-Kabel ## Verwendung diff --git a/app.py b/app.py index 9fcc01d..903f4c3 100644 --- a/app.py +++ b/app.py @@ -141,7 +141,7 @@ class SwitchSourceDialog(tk.Toplevel): def __init__(self, parent, devices, switch=None): super().__init__(parent) self.title("Switch-Zugang") - self.geometry("560x500") + self.geometry("620x560") self.resizable(False, False) self.transient(parent) self.grab_set() @@ -151,30 +151,54 @@ class SwitchSourceDialog(tk.Toplevel): self.enabled = tk.BooleanVar(value=switch.get("enabled", True)) self.platform = tk.StringVar(value=switch.get("platform", "aruba_cx")) self.device_map = {} + self.device_options = [] - device_values = [] selected_device = "" + selected_tenant = "Alle Mandanten" for device in devices: site = getattr(getattr(device, "site", None), "name", "") - display = f"{device.name} · {site}" if site else device.name + tenant = getattr(getattr(device, "tenant", None), "name", "") or "Ohne Mandant" + parts = [device.name] + if site: + parts.append(site) + parts.append(tenant) + display = " · ".join(parts) + option = { + "display": display, + "device_id": device.id, + "device_name": device.name, + "tenant": tenant, + } + self.device_options.append(option) self.device_map[display] = (device.id, device.name) - device_values.append(display) if device.id == switch.get("device_id"): selected_device = display + selected_tenant = tenant body = ttk.Frame(self, padding=22) body.pack(fill="both", expand=True) body.columnconfigure(1, weight=1) - ttk.Label(body, text="NetBox-Gerät").grid(row=0, column=0, sticky="w", padx=(0, 12), pady=7) + tenants = ["Alle Mandanten"] + sorted( + {option["tenant"] for option in self.device_options}, + key=str.casefold, + ) + ttk.Label(body, text="Mandant").grid(row=0, column=0, sticky="w", padx=(0, 12), pady=7) + self.tenant_filter = tk.StringVar(value=selected_tenant) + tenant_combo = ttk.Combobox(body, textvariable=self.tenant_filter, values=tenants, state="readonly") + tenant_combo.grid(row=0, column=1, sticky="ew", pady=7) + tenant_combo.bind("<>", self.on_tenant_changed) + + ttk.Label(body, text="NetBox-Gerät suchen").grid(row=1, column=0, sticky="w", padx=(0, 12), pady=7) self.device_selection = tk.StringVar(value=selected_device) - ttk.Combobox( + self.device_combo = ttk.Combobox( body, textvariable=self.device_selection, - values=device_values, - state="readonly", - ).grid(row=0, column=1, sticky="ew", pady=7) + values=[], + ) + self.device_combo.grid(row=1, column=1, sticky="ew", pady=7) + self.device_combo.bind("", self.filter_devices) - ttk.Label(body, text="Plattform").grid(row=1, column=0, sticky="w", padx=(0, 12), pady=7) + ttk.Label(body, text="Plattform").grid(row=2, column=0, sticky="w", padx=(0, 12), pady=7) platform_map = {label: key for key, label in PLATFORMS.items()} self.platform_map = platform_map platform_display = tk.StringVar(value=PLATFORMS[self.platform.get()]) @@ -184,28 +208,51 @@ class SwitchSourceDialog(tk.Toplevel): textvariable=platform_display, values=list(platform_map), state="readonly", - ).grid(row=1, column=1, sticky="ew", pady=7) + ).grid(row=2, column=1, sticky="ew", pady=7) - self.host_entry = self.add_entry(body, 2, "Host / IP", switch.get("host", "")) - self.port_entry = self.add_entry(body, 3, "SSH-Port", str(switch.get("port", 22))) - self.username_entry = self.add_entry(body, 4, "Benutzer", switch.get("username", "")) - self.password_entry = self.add_entry(body, 5, "Passwort", switch.get("password", ""), secret=True) + self.host_entry = self.add_entry(body, 3, "Host / IP", switch.get("host", "")) + self.port_entry = self.add_entry(body, 4, "SSH-Port", str(switch.get("port", 22))) + self.username_entry = self.add_entry(body, 5, "Benutzer", switch.get("username", "")) + self.password_entry = self.add_entry(body, 6, "Passwort", switch.get("password", ""), secret=True) ttk.Checkbutton(body, text="Switch aktivieren", variable=self.enabled).grid( - row=6, column=0, columnspan=2, sticky="w", pady=(12, 4) + row=7, column=0, columnspan=2, sticky="w", pady=(12, 4) ) ttk.Label( body, text="Das Passwort wird im Kundenprofil mit Windows DPAPI verschlüsselt gespeichert.", foreground="#52606d", wraplength=500, - ).grid(row=7, column=0, columnspan=2, sticky="w", pady=(12, 0)) + ).grid(row=8, column=0, columnspan=2, sticky="w", pady=(12, 0)) buttons = ttk.Frame(body) - buttons.grid(row=8, column=0, columnspan=2, sticky="e", pady=(22, 0)) + buttons.grid(row=9, column=0, columnspan=2, sticky="e", pady=(22, 0)) ttk.Button(buttons, text="Abbrechen", command=self.destroy).pack(side="left", padx=4) ttk.Button(buttons, text="Übernehmen", command=self.accept, style="Primary.TButton").pack(side="left", padx=4) + self.update_device_values() self.protocol("WM_DELETE_WINDOW", self.destroy) self.wait_window(self) + def matching_device_options(self, query=""): + tenant = self.tenant_filter.get() + query = query.strip().casefold() + return [ + option for option in self.device_options + if (tenant == "Alle Mandanten" or option["tenant"] == tenant) + and (not query or query in option["display"].casefold()) + ] + + def update_device_values(self, query=""): + self.device_combo["values"] = [option["display"] for option in self.matching_device_options(query)] + + def filter_devices(self, _event=None): + self.update_device_values(self.device_selection.get()) + + def on_tenant_changed(self, _event=None): + current = self.device_selection.get() + allowed = {option["display"] for option in self.matching_device_options()} + if current not in allowed: + self.device_selection.set("") + self.update_device_values() + @staticmethod def add_entry(parent, row, label, value, secret=False): ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=7) @@ -1348,6 +1395,8 @@ class NetBoxVMImporter: self.switch_log(f"Lese {switch['name']} ({switch['host']}) aus …") snapshot = discover_switch(switch) self.switch_snapshots[switch["id"]] = snapshot + if snapshot.get("lldp_enabled_automatically"): + self.switch_log(f"{switch['name']}: LLDP war deaktiviert und wurde automatisch aktiviert") self.switch_log( f"{switch['name']}: {len(snapshot['interfaces'])} Schnittstellen, " f"{len(snapshot['neighbors'])} LLDP-Nachbarn" diff --git a/dist/NetBox VM Import.exe b/dist/NetBox VM Import.exe index 83ba753..871828b 100644 Binary files a/dist/NetBox VM Import.exe and b/dist/NetBox VM Import.exe differ diff --git a/switch_sync.py b/switch_sync.py index eda71af..b34865e 100644 --- a/switch_sync.py +++ b/switch_sync.py @@ -18,6 +18,24 @@ COMMANDS = { "hpe_comware": ("screen-length disable", "display interface brief", "display lldp neighbor-information verbose"), } +LLDP_CONTROL = { + "aruba_cx": { + "status": "show lldp configuration", + "disabled": r"LLDP Enabled\s*:\s*No", + "enable": ("configure terminal", "lldp", "end"), + }, + "aruba_aos": { + "status": "show lldp config", + "disabled": r"LLDP (?:Enabled|Run)\s*:\s*(?:No|False)|LLDP operation.*disabled", + "enable": ("configure terminal", "lldp run", "exit"), + }, + "hpe_comware": { + "status": "display lldp status", + "disabled": r"Global status of LLDP\s*:\s*Disable", + "enable": ("system-view", "lldp global enable", "return"), + }, +} + ANSI_ESCAPE = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") MAC_PATTERN = re.compile(r"(?i)\b(?:[0-9a-f]{2}[:-]){5}[0-9a-f]{2}\b|\b[0-9a-f]{4}(?:-[0-9a-f]{4}){2}\b") @@ -63,11 +81,42 @@ def run_switch_commands(config): ) channel = client.invoke_shell(width=240, height=1000) _read_available(channel, timeout=3) - outputs = [] - for command in COMMANDS[platform]: - channel.send(command + "\n") - outputs.append(_read_available(channel)) - return {"interfaces": outputs[1], "neighbors": outputs[2]} + pager_command, interface_command, neighbor_command = COMMANDS[platform] + channel.send(pager_command + "\n") + _read_available(channel) + + control = LLDP_CONTROL[platform] + channel.send(control["status"] + "\n") + lldp_status = _read_available(channel) + lldp_enabled_automatically = bool(re.search(control["disabled"], lldp_status, flags=re.I)) + if lldp_enabled_automatically: + enable_output = [] + for command in control["enable"]: + channel.send(command + "\n") + enable_output.append(_read_available(channel)) + combined = "\n".join(enable_output) + if platform == "hpe_comware" and re.search(r"(?i)unrecognized|invalid|wrong parameter", combined): + channel.send("system-view\n") + _read_available(channel) + channel.send("lldp enable\n") + combined = _read_available(channel) + channel.send("return\n") + combined += "\n" + _read_available(channel) + if re.search(r"(?i)permission denied|authorization failed|access denied", combined): + raise PermissionError("LLDP konnte mangels Konfigurationsrechten nicht aktiviert werden.") + if re.search(r"(?i)unrecognized|invalid input|unknown command|wrong parameter", combined): + raise RuntimeError("Der LLDP-Aktivierungsbefehl wird von diesem Switch nicht unterstützt.") + time.sleep(2) + + channel.send(interface_command + "\n") + interfaces = _read_available(channel) + channel.send(neighbor_command + "\n") + neighbors = _read_available(channel) + return { + "interfaces": interfaces, + "neighbors": neighbors, + "lldp_enabled_automatically": lldp_enabled_automatically, + } finally: client.close() @@ -225,7 +274,12 @@ def discover_switch(config): local = interface_map.get(neighbor["local_port"].casefold()) if local: local["neighbor"] = neighbor - return {"interfaces": interfaces, "neighbors": neighbors, "raw": outputs} + return { + "interfaces": interfaces, + "neighbors": neighbors, + "raw": outputs, + "lldp_enabled_automatically": outputs.get("lldp_enabled_automatically", False), + } def _netbox_interface_type(speed_mbps):