feat: filter switch devices and enable LLDP

Add tenant-aware searchable NetBox device selection and conditionally enable global LLDP on Aruba CX, ArubaOS-Switch, and HPE Comware before discovery.
This commit is contained in:
2026-07-30 09:56:42 +02:00
parent 08add7982a
commit 47e1d3499c
4 changed files with 129 additions and 24 deletions
+2
View File
@@ -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
+67 -18
View File
@@ -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("<<ComboboxSelected>>", 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("<KeyRelease>", 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"
BIN
View File
Binary file not shown.
+60 -6
View File
@@ -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):