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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+60
-6
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user