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
|
- Gemeinsame, synchronisierte Kundenprofil- und NetBox-Zielansicht in beiden Tabs
|
||||||
- Verschlüsselte SSH-Zugänge pro bestehendem NetBox-Switch
|
- Verschlüsselte SSH-Zugänge pro bestehendem NetBox-Switch
|
||||||
- SSH-Erkennung für Aruba CX, ArubaOS-Switch/ProCurve und HPE Comware
|
- 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
|
- Synchronisierung von Switch-Schnittstellen und optionalen LLDP-Verbindungen als NetBox-Kabel
|
||||||
|
|
||||||
## Verwendung
|
## Verwendung
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class SwitchSourceDialog(tk.Toplevel):
|
|||||||
def __init__(self, parent, devices, switch=None):
|
def __init__(self, parent, devices, switch=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.title("Switch-Zugang")
|
self.title("Switch-Zugang")
|
||||||
self.geometry("560x500")
|
self.geometry("620x560")
|
||||||
self.resizable(False, False)
|
self.resizable(False, False)
|
||||||
self.transient(parent)
|
self.transient(parent)
|
||||||
self.grab_set()
|
self.grab_set()
|
||||||
@@ -151,30 +151,54 @@ class SwitchSourceDialog(tk.Toplevel):
|
|||||||
self.enabled = tk.BooleanVar(value=switch.get("enabled", True))
|
self.enabled = tk.BooleanVar(value=switch.get("enabled", True))
|
||||||
self.platform = tk.StringVar(value=switch.get("platform", "aruba_cx"))
|
self.platform = tk.StringVar(value=switch.get("platform", "aruba_cx"))
|
||||||
self.device_map = {}
|
self.device_map = {}
|
||||||
|
self.device_options = []
|
||||||
|
|
||||||
device_values = []
|
|
||||||
selected_device = ""
|
selected_device = ""
|
||||||
|
selected_tenant = "Alle Mandanten"
|
||||||
for device in devices:
|
for device in devices:
|
||||||
site = getattr(getattr(device, "site", None), "name", "")
|
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)
|
self.device_map[display] = (device.id, device.name)
|
||||||
device_values.append(display)
|
|
||||||
if device.id == switch.get("device_id"):
|
if device.id == switch.get("device_id"):
|
||||||
selected_device = display
|
selected_device = display
|
||||||
|
selected_tenant = tenant
|
||||||
|
|
||||||
body = ttk.Frame(self, padding=22)
|
body = ttk.Frame(self, padding=22)
|
||||||
body.pack(fill="both", expand=True)
|
body.pack(fill="both", expand=True)
|
||||||
body.columnconfigure(1, weight=1)
|
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)
|
self.device_selection = tk.StringVar(value=selected_device)
|
||||||
ttk.Combobox(
|
self.device_combo = ttk.Combobox(
|
||||||
body,
|
body,
|
||||||
textvariable=self.device_selection,
|
textvariable=self.device_selection,
|
||||||
values=device_values,
|
values=[],
|
||||||
state="readonly",
|
)
|
||||||
).grid(row=0, column=1, sticky="ew", pady=7)
|
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()}
|
platform_map = {label: key for key, label in PLATFORMS.items()}
|
||||||
self.platform_map = platform_map
|
self.platform_map = platform_map
|
||||||
platform_display = tk.StringVar(value=PLATFORMS[self.platform.get()])
|
platform_display = tk.StringVar(value=PLATFORMS[self.platform.get()])
|
||||||
@@ -184,28 +208,51 @@ class SwitchSourceDialog(tk.Toplevel):
|
|||||||
textvariable=platform_display,
|
textvariable=platform_display,
|
||||||
values=list(platform_map),
|
values=list(platform_map),
|
||||||
state="readonly",
|
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.host_entry = self.add_entry(body, 3, "Host / IP", switch.get("host", ""))
|
||||||
self.port_entry = self.add_entry(body, 3, "SSH-Port", str(switch.get("port", 22)))
|
self.port_entry = self.add_entry(body, 4, "SSH-Port", str(switch.get("port", 22)))
|
||||||
self.username_entry = self.add_entry(body, 4, "Benutzer", switch.get("username", ""))
|
self.username_entry = self.add_entry(body, 5, "Benutzer", switch.get("username", ""))
|
||||||
self.password_entry = self.add_entry(body, 5, "Passwort", switch.get("password", ""), secret=True)
|
self.password_entry = self.add_entry(body, 6, "Passwort", switch.get("password", ""), secret=True)
|
||||||
ttk.Checkbutton(body, text="Switch aktivieren", variable=self.enabled).grid(
|
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(
|
ttk.Label(
|
||||||
body,
|
body,
|
||||||
text="Das Passwort wird im Kundenprofil mit Windows DPAPI verschlüsselt gespeichert.",
|
text="Das Passwort wird im Kundenprofil mit Windows DPAPI verschlüsselt gespeichert.",
|
||||||
foreground="#52606d",
|
foreground="#52606d",
|
||||||
wraplength=500,
|
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 = 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="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)
|
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.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||||
self.wait_window(self)
|
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
|
@staticmethod
|
||||||
def add_entry(parent, row, label, value, secret=False):
|
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)
|
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 …")
|
self.switch_log(f"Lese {switch['name']} ({switch['host']}) aus …")
|
||||||
snapshot = discover_switch(switch)
|
snapshot = discover_switch(switch)
|
||||||
self.switch_snapshots[switch["id"]] = snapshot
|
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(
|
self.switch_log(
|
||||||
f"{switch['name']}: {len(snapshot['interfaces'])} Schnittstellen, "
|
f"{switch['name']}: {len(snapshot['interfaces'])} Schnittstellen, "
|
||||||
f"{len(snapshot['neighbors'])} LLDP-Nachbarn"
|
f"{len(snapshot['neighbors'])} LLDP-Nachbarn"
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+59
-5
@@ -18,6 +18,24 @@ COMMANDS = {
|
|||||||
"hpe_comware": ("screen-length disable", "display interface brief", "display lldp neighbor-information verbose"),
|
"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-?]*[ -/]*[@-~])")
|
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")
|
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)
|
channel = client.invoke_shell(width=240, height=1000)
|
||||||
_read_available(channel, timeout=3)
|
_read_available(channel, timeout=3)
|
||||||
outputs = []
|
pager_command, interface_command, neighbor_command = COMMANDS[platform]
|
||||||
for command in 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")
|
channel.send(command + "\n")
|
||||||
outputs.append(_read_available(channel))
|
enable_output.append(_read_available(channel))
|
||||||
return {"interfaces": outputs[1], "neighbors": outputs[2]}
|
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:
|
finally:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
@@ -225,7 +274,12 @@ def discover_switch(config):
|
|||||||
local = interface_map.get(neighbor["local_port"].casefold())
|
local = interface_map.get(neighbor["local_port"].casefold())
|
||||||
if local:
|
if local:
|
||||||
local["neighbor"] = neighbor
|
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):
|
def _netbox_interface_type(speed_mbps):
|
||||||
|
|||||||
Reference in New Issue
Block a user