fix: improve Aruba 6100 neighbor discovery
Wait for complete CLI output on large switches, merge AOS-CX LLDP and CDP neighbors, and expose resizable logs plus raw SSH diagnostics.
This commit is contained in:
@@ -22,9 +22,11 @@ Windows-Desktopanwendung zum Synchronisieren virtueller Maschinen sowie HPE-/Aru
|
|||||||
- 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
|
- Durchsuchbare, nach Mandant filterbare Auswahl vorhandener NetBox-Geräte
|
||||||
- Automatische Aktivierung von global deaktiviertem LLDP auf unterstützten Switches
|
- 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-/CDP-Verbindungen als NetBox-Kabel
|
||||||
- Auslesen und Synchronisieren vorhandener IPv4-/IPv6-Adressen auf Switch-Schnittstellen
|
- Auslesen und Synchronisieren vorhandener IPv4-/IPv6-Adressen auf Switch-Schnittstellen
|
||||||
- Spezieller LLDP-Detailparser mit Tabellen-Fallback für Aruba-6100-Switches
|
- Robustes, vollständiges Einlesen großer SSH-Ausgaben von 48-Port-Switches
|
||||||
|
- Spezieller LLDP-Detailparser und ergänzender CDP-Fallback für Aruba-6100-Switches mit AOS-CX 10.13
|
||||||
|
- Skalierbares Switch-Protokoll sowie separates Fenster für Protokoll und SSH-Rohdaten
|
||||||
|
|
||||||
## Verwendung
|
## Verwendung
|
||||||
|
|
||||||
|
|||||||
@@ -1026,8 +1026,7 @@ class NetBoxVMImporter:
|
|||||||
main = ttk.Frame(parent, padding=18)
|
main = ttk.Frame(parent, padding=18)
|
||||||
main.pack(fill="both", expand=True)
|
main.pack(fill="both", expand=True)
|
||||||
main.columnconfigure(0, weight=1)
|
main.columnconfigure(0, weight=1)
|
||||||
main.rowconfigure(3, weight=2)
|
main.rowconfigure(3, weight=1)
|
||||||
main.rowconfigure(5, weight=2)
|
|
||||||
|
|
||||||
header = ttk.Frame(main)
|
header = ttk.Frame(main)
|
||||||
header.grid(row=0, column=0, sticky="ew", pady=(0, 12))
|
header.grid(row=0, column=0, sticky="ew", pady=(0, 12))
|
||||||
@@ -1093,14 +1092,15 @@ class NetBoxVMImporter:
|
|||||||
style="Primary.TButton",
|
style="Primary.TButton",
|
||||||
).pack(side="right")
|
).pack(side="right")
|
||||||
|
|
||||||
result_card = ttk.LabelFrame(main, text="Schnittstellen & LLDP-Nachbarn", style="Card.TLabelframe")
|
switch_workspace = ttk.Panedwindow(main, orient="vertical")
|
||||||
result_card.grid(row=3, column=0, sticky="nsew", pady=(0, 10))
|
switch_workspace.grid(row=3, column=0, sticky="nsew", pady=(0, 10))
|
||||||
|
result_card = ttk.LabelFrame(switch_workspace, text="Schnittstellen & LLDP/CDP-Nachbarn", style="Card.TLabelframe")
|
||||||
result_columns = ("switch", "interface", "state", "speed", "ips", "neighbor", "remote_port")
|
result_columns = ("switch", "interface", "state", "speed", "ips", "neighbor", "remote_port")
|
||||||
self.switch_result_tree = ttk.Treeview(result_card, columns=result_columns, show="headings")
|
self.switch_result_tree = ttk.Treeview(result_card, columns=result_columns, show="headings")
|
||||||
result_headings = {
|
result_headings = {
|
||||||
"switch": "Switch", "interface": "Schnittstelle", "state": "Status",
|
"switch": "Switch", "interface": "Schnittstelle", "state": "Status",
|
||||||
"speed": "Geschwindigkeit", "ips": "IP-Adressen",
|
"speed": "Geschwindigkeit", "ips": "IP-Adressen",
|
||||||
"neighbor": "LLDP-Nachbar", "remote_port": "Nachbar-Port",
|
"neighbor": "LLDP/CDP-Nachbar", "remote_port": "Nachbar-Port",
|
||||||
}
|
}
|
||||||
result_widths = {
|
result_widths = {
|
||||||
"switch": 150, "interface": 140, "state": 70, "speed": 100,
|
"switch": 150, "interface": 140, "state": 70, "speed": 100,
|
||||||
@@ -1114,18 +1114,24 @@ class NetBoxVMImporter:
|
|||||||
self.switch_result_tree.pack(side="left", fill="both", expand=True)
|
self.switch_result_tree.pack(side="left", fill="both", expand=True)
|
||||||
result_scroll.pack(side="right", fill="y")
|
result_scroll.pack(side="right", fill="y")
|
||||||
|
|
||||||
|
log_card = ttk.LabelFrame(switch_workspace, text="Switch-Protokoll", style="Card.TLabelframe")
|
||||||
|
self.switch_log_text = tk.Text(
|
||||||
|
log_card, height=6, borderwidth=0, bg="#f7f9fb", foreground="#243b53", font=("Consolas", 9), state="disabled"
|
||||||
|
)
|
||||||
|
log_scroll = ttk.Scrollbar(log_card, orient="vertical", command=self.switch_log_text.yview)
|
||||||
|
self.switch_log_text.configure(yscrollcommand=log_scroll.set)
|
||||||
|
self.switch_log_text.pack(side="left", fill="both", expand=True)
|
||||||
|
log_scroll.pack(side="right", fill="y")
|
||||||
|
switch_workspace.add(result_card, weight=4)
|
||||||
|
switch_workspace.add(log_card, weight=1)
|
||||||
|
|
||||||
actions = ttk.Frame(main)
|
actions = ttk.Frame(main)
|
||||||
actions.grid(row=4, column=0, sticky="ew", pady=(0, 10))
|
actions.grid(row=4, column=0, sticky="ew", pady=(0, 10))
|
||||||
self.create_neighbor_cables = tk.BooleanVar(value=True)
|
self.create_neighbor_cables = tk.BooleanVar(value=True)
|
||||||
ttk.Checkbutton(actions, text="LLDP-Verbindungen als Kabel anlegen", variable=self.create_neighbor_cables).pack(side="left")
|
ttk.Checkbutton(actions, text="LLDP/CDP-Verbindungen als Kabel anlegen", variable=self.create_neighbor_cables).pack(side="left")
|
||||||
ttk.Button(actions, text="Nach NetBox synchronisieren", command=self.sync_switches, style="Primary.TButton").pack(side="right")
|
ttk.Button(actions, text="Nach NetBox synchronisieren", command=self.sync_switches, style="Primary.TButton").pack(side="right")
|
||||||
|
ttk.Button(actions, text="SSH-Rohdaten", command=self.open_switch_raw_window).pack(side="right", padx=(0, 8))
|
||||||
log_card = ttk.LabelFrame(main, text="Switch-Protokoll", style="Card.TLabelframe")
|
ttk.Button(actions, text="Protokoll öffnen", command=self.open_switch_log_window).pack(side="right", padx=(0, 8))
|
||||||
log_card.grid(row=5, column=0, sticky="nsew")
|
|
||||||
self.switch_log_text = tk.Text(
|
|
||||||
log_card, height=8, borderwidth=0, bg="#f7f9fb", foreground="#243b53", font=("Consolas", 9), state="disabled"
|
|
||||||
)
|
|
||||||
self.switch_log_text.pack(fill="both", expand=True)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_field(parent, row, label, secret=False, variable=None):
|
def add_field(parent, row, label, secret=False, variable=None):
|
||||||
@@ -1305,6 +1311,61 @@ class NetBoxVMImporter:
|
|||||||
self.status_text.set(text)
|
self.status_text.set(text)
|
||||||
self.root.update()
|
self.root.update()
|
||||||
|
|
||||||
|
def _open_switch_text_window(self, title, content):
|
||||||
|
window = tk.Toplevel(self.root)
|
||||||
|
window.title(title)
|
||||||
|
window.geometry("1000x650")
|
||||||
|
window.minsize(700, 400)
|
||||||
|
icon = self.resource_path("netbox_vm_import.ico")
|
||||||
|
if icon.exists():
|
||||||
|
try:
|
||||||
|
window.iconbitmap(str(icon))
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
frame = ttk.Frame(window, padding=10)
|
||||||
|
frame.pack(fill="both", expand=True)
|
||||||
|
frame.rowconfigure(0, weight=1)
|
||||||
|
frame.columnconfigure(0, weight=1)
|
||||||
|
text_widget = tk.Text(frame, wrap="none", font=("Consolas", 9), bg="#f7f9fb", foreground="#243b53")
|
||||||
|
vertical = ttk.Scrollbar(frame, orient="vertical", command=text_widget.yview)
|
||||||
|
horizontal = ttk.Scrollbar(frame, orient="horizontal", command=text_widget.xview)
|
||||||
|
text_widget.configure(yscrollcommand=vertical.set, xscrollcommand=horizontal.set)
|
||||||
|
text_widget.grid(row=0, column=0, sticky="nsew")
|
||||||
|
vertical.grid(row=0, column=1, sticky="ns")
|
||||||
|
horizontal.grid(row=1, column=0, sticky="ew")
|
||||||
|
text_widget.insert("1.0", content)
|
||||||
|
text_widget.configure(state="disabled")
|
||||||
|
return window
|
||||||
|
|
||||||
|
def open_switch_log_window(self):
|
||||||
|
content = self.switch_log_text.get("1.0", "end-1c") or "Noch keine Protokolleinträge vorhanden."
|
||||||
|
self._open_switch_text_window("Switch-Protokoll", content)
|
||||||
|
|
||||||
|
def open_switch_raw_window(self):
|
||||||
|
selected_ids = set(self.switch_tree.selection())
|
||||||
|
snapshots = [
|
||||||
|
(switch, self.switch_snapshots.get(switch["id"]))
|
||||||
|
for switch in self.current_switches
|
||||||
|
if (not selected_ids or switch["id"] in selected_ids) and self.switch_snapshots.get(switch["id"])
|
||||||
|
]
|
||||||
|
if not snapshots:
|
||||||
|
messagebox.showinfo("SSH-Rohdaten", "Bitte zuerst mindestens einen Switch auslesen.", parent=self.root)
|
||||||
|
return
|
||||||
|
sections = []
|
||||||
|
labels = {
|
||||||
|
"interfaces": "SCHNITTSTELLEN",
|
||||||
|
"ipv4_interfaces": "IPV4-SCHNITTSTELLEN",
|
||||||
|
"ipv6_interfaces": "IPV6-SCHNITTSTELLEN",
|
||||||
|
"neighbors": "LLDP-NACHBARN",
|
||||||
|
"cdp_neighbors": "CDP-NACHBARN",
|
||||||
|
}
|
||||||
|
for switch, snapshot in snapshots:
|
||||||
|
sections.append(f"{'=' * 20} {switch['name']} ({switch['host']}) {'=' * 20}")
|
||||||
|
raw = snapshot.get("raw", {})
|
||||||
|
for key, label in labels.items():
|
||||||
|
sections.extend((f"\n--- {label} ---", str(raw.get(key, "") or "(keine Ausgabe)")))
|
||||||
|
self._open_switch_text_window("SSH-Rohdaten", "\n".join(sections))
|
||||||
|
|
||||||
def refresh_switch_tree(self):
|
def refresh_switch_tree(self):
|
||||||
if not hasattr(self, "switch_tree"):
|
if not hasattr(self, "switch_tree"):
|
||||||
return
|
return
|
||||||
@@ -1404,8 +1465,13 @@ class NetBoxVMImporter:
|
|||||||
self.switch_log(
|
self.switch_log(
|
||||||
f"{switch['name']}: {len(snapshot['interfaces'])} Schnittstellen, "
|
f"{switch['name']}: {len(snapshot['interfaces'])} Schnittstellen, "
|
||||||
f"{len(snapshot.get('ip_addresses', []))} IP-Adressen, "
|
f"{len(snapshot.get('ip_addresses', []))} IP-Adressen, "
|
||||||
f"{len(snapshot['neighbors'])} LLDP-Nachbarn"
|
f"{len(snapshot.get('lldp_neighbors', []))} LLDP- und "
|
||||||
|
f"{len(snapshot.get('cdp_neighbors', []))} CDP-Nachbarn"
|
||||||
)
|
)
|
||||||
|
if not snapshot["interfaces"]:
|
||||||
|
self.switch_log(f"WARNUNG {switch['name']}: Keine Schnittstellen erkannt – bitte SSH-Rohdaten prüfen")
|
||||||
|
if not snapshot["neighbors"]:
|
||||||
|
self.switch_log(f"WARNUNG {switch['name']}: Keine LLDP/CDP-Nachbarn erkannt – bitte SSH-Rohdaten prüfen")
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
failures.append(f"{switch['name']}: {error}")
|
failures.append(f"{switch['name']}: {error}")
|
||||||
self.switch_log(f"FEHLER {switch['name']}: {error}")
|
self.switch_log(f"FEHLER {switch['name']}: {error}")
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+73
-8
@@ -53,7 +53,7 @@ def _clean_output(value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _read_available(channel, quiet_seconds=0.35, timeout=12):
|
def _read_available(channel, quiet_seconds=0.8, timeout=20):
|
||||||
chunks = []
|
chunks = []
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
last_data = started
|
last_data = started
|
||||||
@@ -61,7 +61,14 @@ def _read_available(channel, quiet_seconds=0.35, timeout=12):
|
|||||||
if channel.recv_ready():
|
if channel.recv_ready():
|
||||||
chunks.append(channel.recv(65535).decode("utf-8", errors="replace"))
|
chunks.append(channel.recv(65535).decode("utf-8", errors="replace"))
|
||||||
last_data = time.monotonic()
|
last_data = time.monotonic()
|
||||||
elif time.monotonic() - last_data >= quiet_seconds:
|
# Interactive AOS-CX/AOS-S/Comware commands are complete when the
|
||||||
|
# device prompt returns. This avoids truncating output at a short
|
||||||
|
# pause while also avoiding a fixed delay after every command.
|
||||||
|
if re.search(r"(?m)^\s*[^\r\n]+[>#]\s*$", "".join(chunks)):
|
||||||
|
break
|
||||||
|
elif chunks and time.monotonic() - last_data >= quiet_seconds:
|
||||||
|
break
|
||||||
|
elif not chunks and time.monotonic() - started >= min(timeout, 5):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
@@ -87,7 +94,7 @@ def run_switch_commands(config):
|
|||||||
banner_timeout=12,
|
banner_timeout=12,
|
||||||
)
|
)
|
||||||
channel = client.invoke_shell(width=240, height=1000)
|
channel = client.invoke_shell(width=240, height=1000)
|
||||||
_read_available(channel, timeout=3)
|
_read_available(channel, quiet_seconds=0.5, timeout=5)
|
||||||
pager_command, interface_command, neighbor_command = COMMANDS[platform]
|
pager_command, interface_command, neighbor_command = COMMANDS[platform]
|
||||||
channel.send(pager_command + "\n")
|
channel.send(pager_command + "\n")
|
||||||
_read_available(channel)
|
_read_available(channel)
|
||||||
@@ -116,21 +123,36 @@ def run_switch_commands(config):
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
channel.send(interface_command + "\n")
|
channel.send(interface_command + "\n")
|
||||||
interfaces = _read_available(channel)
|
interfaces = _read_available(channel, quiet_seconds=1.2, timeout=30)
|
||||||
ip_outputs = []
|
ip_outputs = []
|
||||||
for command in IP_COMMANDS[platform]:
|
for command in IP_COMMANDS[platform]:
|
||||||
channel.send(command + "\n")
|
channel.send(command + "\n")
|
||||||
ip_outputs.append(_read_available(channel))
|
ip_outputs.append(_read_available(channel, quiet_seconds=1.0, timeout=25))
|
||||||
channel.send(neighbor_command + "\n")
|
channel.send(neighbor_command + "\n")
|
||||||
neighbors = _read_available(channel)
|
neighbors = _read_available(channel, quiet_seconds=1.5, timeout=35)
|
||||||
if platform == "aruba_cx" and re.search(r"(?i)invalid input|unknown command|unrecognized", neighbors):
|
if platform == "aruba_cx" and re.search(r"(?i)invalid input|unknown command|unrecognized", neighbors):
|
||||||
channel.send("show lldp neighbor-info\n")
|
channel.send("show lldp neighbor-info\n")
|
||||||
neighbors = _read_available(channel)
|
neighbors = _read_available(channel, quiet_seconds=1.5, timeout=35)
|
||||||
|
cdp_neighbors = ""
|
||||||
|
if platform == "aruba_cx":
|
||||||
|
channel.send("show cdp neighbor-info\n")
|
||||||
|
cdp_summary = _read_available(channel, quiet_seconds=1.2, timeout=25)
|
||||||
|
cdp_ports = []
|
||||||
|
for line in cdp_summary.splitlines():
|
||||||
|
match = re.match(r"^\s*(\d+/\d+/\d+)\s+\S+", line)
|
||||||
|
if match and match.group(1) not in cdp_ports:
|
||||||
|
cdp_ports.append(match.group(1))
|
||||||
|
details = []
|
||||||
|
for port in cdp_ports:
|
||||||
|
channel.send(f"show cdp neighbor-info {port}\n")
|
||||||
|
details.append(_read_available(channel, quiet_seconds=1.0, timeout=20))
|
||||||
|
cdp_neighbors = "\n".join([cdp_summary, *details])
|
||||||
return {
|
return {
|
||||||
"interfaces": interfaces,
|
"interfaces": interfaces,
|
||||||
"ipv4_interfaces": ip_outputs[0],
|
"ipv4_interfaces": ip_outputs[0],
|
||||||
"ipv6_interfaces": ip_outputs[1],
|
"ipv6_interfaces": ip_outputs[1],
|
||||||
"neighbors": neighbors,
|
"neighbors": neighbors,
|
||||||
|
"cdp_neighbors": cdp_neighbors,
|
||||||
"lldp_enabled_automatically": lldp_enabled_automatically,
|
"lldp_enabled_automatically": lldp_enabled_automatically,
|
||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
@@ -322,6 +344,37 @@ def parse_neighbors(output):
|
|||||||
return neighbors
|
return neighbors
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cdp_neighbors(output):
|
||||||
|
"""Parse detailed AOS-CX CDP records into the common neighbor shape."""
|
||||||
|
clean = _clean_output(output)
|
||||||
|
starts = list(re.finditer(r"(?im)^\s*Local Port\s*:\s*\S+", clean))
|
||||||
|
neighbors = []
|
||||||
|
for index, start in enumerate(starts):
|
||||||
|
end = starts[index + 1].start() if index + 1 < len(starts) else len(clean)
|
||||||
|
block = clean[start.start():end]
|
||||||
|
local_port = _field(block, (r"^\s*Local Port\s*:\s*(\S+)",))
|
||||||
|
system_name = _field(block, (
|
||||||
|
r"^\s*Device ID\s*:\s*(.+)$",
|
||||||
|
r"^\s*System Name\s*:\s*(.+)$",
|
||||||
|
))
|
||||||
|
remote_port = _field(block, (
|
||||||
|
r"^\s*Neighbor Port-ID\s*:\s*(.+)$",
|
||||||
|
r"^\s*Port ID\s*:\s*(.+)$",
|
||||||
|
))
|
||||||
|
management_ip = _field(block, (
|
||||||
|
r"^\s*Address\s*:\s*([0-9a-fA-F:.]+)",
|
||||||
|
r"^\s*Management Address\s*:\s*([0-9a-fA-F:.]+)",
|
||||||
|
))
|
||||||
|
if local_port and (system_name or remote_port or management_ip):
|
||||||
|
neighbors.append({
|
||||||
|
"local_port": local_port,
|
||||||
|
"system_name": system_name,
|
||||||
|
"remote_port": remote_port,
|
||||||
|
"management_ip": management_ip,
|
||||||
|
})
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
|
||||||
def _parse_neighbor_table(output):
|
def _parse_neighbor_table(output):
|
||||||
neighbors = []
|
neighbors = []
|
||||||
for raw_line in output.splitlines():
|
for raw_line in output.splitlines():
|
||||||
@@ -360,7 +413,17 @@ def discover_switch(config):
|
|||||||
outputs.get("ipv6_interfaces", ""),
|
outputs.get("ipv6_interfaces", ""),
|
||||||
config["platform"],
|
config["platform"],
|
||||||
)
|
)
|
||||||
neighbors = parse_neighbors(outputs["neighbors"])
|
lldp_neighbors = parse_neighbors(outputs["neighbors"])
|
||||||
|
cdp_neighbors = parse_cdp_neighbors(outputs.get("cdp_neighbors", ""))
|
||||||
|
# LLDP normally contains richer data. CDP fills ports for which CX did not
|
||||||
|
# return an LLDP record, without duplicating a physical local interface.
|
||||||
|
neighbors = list(lldp_neighbors)
|
||||||
|
occupied_ports = {_interface_key(item["local_port"]) for item in neighbors}
|
||||||
|
for neighbor in cdp_neighbors:
|
||||||
|
key = _interface_key(neighbor["local_port"])
|
||||||
|
if key not in occupied_ports:
|
||||||
|
neighbors.append(neighbor)
|
||||||
|
occupied_ports.add(key)
|
||||||
interface_map = {_interface_key(interface["name"]): interface for interface in interfaces}
|
interface_map = {_interface_key(interface["name"]): interface for interface in interfaces}
|
||||||
for item in interface_ips:
|
for item in interface_ips:
|
||||||
key = _interface_key(item["interface"])
|
key = _interface_key(item["interface"])
|
||||||
@@ -388,6 +451,8 @@ def discover_switch(config):
|
|||||||
"interfaces": interfaces,
|
"interfaces": interfaces,
|
||||||
"ip_addresses": interface_ips,
|
"ip_addresses": interface_ips,
|
||||||
"neighbors": neighbors,
|
"neighbors": neighbors,
|
||||||
|
"lldp_neighbors": lldp_neighbors,
|
||||||
|
"cdp_neighbors": cdp_neighbors,
|
||||||
"raw": outputs,
|
"raw": outputs,
|
||||||
"lldp_enabled_automatically": outputs.get("lldp_enabled_automatically", False),
|
"lldp_enabled_automatically": outputs.get("lldp_enabled_automatically", False),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user