Move the port inspector onto device pages
Lint / JS (eslint) (push) Successful in 1m14s
Lint / Python (flake8) (push) Successful in 2m12s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m3s
Test / Python Tests (pytest) (push) Successful in 1m11s
Lint / Python (flake8) (pull_request) Successful in 1m1s
Lint / JS (eslint) (pull_request) Successful in 10s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 56s
Test / Python Tests (pytest) (pull_request) Successful in 1m5s
Lint / JS (eslint) (push) Successful in 1m14s
Lint / Python (flake8) (push) Successful in 2m12s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m3s
Test / Python Tests (pytest) (push) Successful in 1m11s
Lint / Python (flake8) (pull_request) Successful in 1m1s
Lint / JS (eslint) (pull_request) Successful in 10s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 56s
Test / Python Tests (pytest) (pull_request) Successful in 1m5s
Replace the all-switches Inspector page with a per-device front panel, port table and detail panel. /inspector now redirects to the dashboard, and /device/<name>#port-<n> links directly to a port. Restore neighbor names after UniFi's LLDP change: read local_port_idx and resolve neighbors by MAC using Prometheus NIC data, UniFi devices and the wired client table. This also brings back Run Diagnostics on server ports, which depends on a named neighbor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+75
@@ -112,6 +112,77 @@ def build_index(snapshot: dict, link_stats: dict, modem: Optional[dict]) -> List
|
||||
return sorted(devices.values(), key=lambda d: d['name'].lower())
|
||||
|
||||
|
||||
_MAC_RE = re.compile(r'^[0-9a-f]{2}(:[0-9a-f]{2}){5}$')
|
||||
|
||||
|
||||
def _norm_mac(value: Optional[str]) -> str:
|
||||
mac = (value or '').strip().lower().replace('-', ':')
|
||||
return mac if _MAC_RE.match(mac) else ''
|
||||
|
||||
|
||||
def resolve_lldp_neighbors(
|
||||
switches: dict,
|
||||
host_macs: Dict[str, tuple],
|
||||
device_macs: Dict[str, str],
|
||||
host_ips: Dict[str, str],
|
||||
) -> None:
|
||||
"""Fill in LLDP neighbor names in place.
|
||||
|
||||
UniFi's API reports a neighbor's MAC, port ID, and sometimes management IPs, but no
|
||||
system name. Names come from what Gandalf already knows:
|
||||
|
||||
host_macs: {mac: (host, interface)} from node_network_info
|
||||
device_macs: {mac: device name} from UniFi
|
||||
host_ips: {ip: host}
|
||||
"""
|
||||
for sw in (switches or {}).values():
|
||||
for port in (sw.get('ports') or {}).values():
|
||||
lldp = port.get('lldp')
|
||||
if not lldp or lldp.get('system_name'):
|
||||
continue
|
||||
port_mac = _norm_mac(lldp.get('port_id'))
|
||||
chassis_mac = _norm_mac(lldp.get('chassis_id'))
|
||||
if port_mac in host_macs:
|
||||
# The port ID names the exact NIC, so it also gives the interface.
|
||||
lldp['system_name'], lldp['port_id'] = host_macs[port_mac]
|
||||
elif chassis_mac in host_macs:
|
||||
lldp['system_name'] = host_macs[chassis_mac][0]
|
||||
elif chassis_mac in device_macs or port_mac in device_macs:
|
||||
lldp['system_name'] = device_macs.get(chassis_mac) or device_macs[port_mac]
|
||||
else:
|
||||
lldp['system_name'] = next(
|
||||
(host_ips[ip] for ip in lldp.get('mgmt_ips') or [] if ip in host_ips), '',
|
||||
)
|
||||
|
||||
|
||||
def apply_client_locations(
|
||||
switches: dict,
|
||||
clients: List[dict],
|
||||
host_macs: Dict[str, tuple],
|
||||
device_macs: Dict[str, str],
|
||||
) -> None:
|
||||
"""Name server neighbors from UniFi's wired client table, where LLDP gave no name.
|
||||
|
||||
This finds servers that don't run lldpd. Uplink ports are skipped because they
|
||||
carry traffic from devices further downstream.
|
||||
"""
|
||||
for client in clients or []:
|
||||
mac = _norm_mac(client.get('mac'))
|
||||
switch = device_macs.get(_norm_mac(client.get('sw_mac')))
|
||||
if mac not in host_macs or switch not in (switches or {}):
|
||||
continue
|
||||
for port in (switches[switch].get('ports') or {}).values():
|
||||
if port.get('port_idx') != client.get('sw_port') or port.get('is_uplink'):
|
||||
continue
|
||||
if (port.get('lldp') or {}).get('system_name'):
|
||||
continue
|
||||
host, iface = host_macs[mac]
|
||||
port['lldp'] = {
|
||||
'system_name': host, 'port_id': iface, 'chassis_id': mac,
|
||||
'port_desc': '', 'mgmt_ips': [], 'source': 'unifi-clients',
|
||||
}
|
||||
|
||||
|
||||
def uplinks_for_host(name: str, switches: dict) -> List[dict]:
|
||||
"""Find the switch ports whose LLDP neighbor is this host."""
|
||||
found = []
|
||||
@@ -148,6 +219,10 @@ def device_detail(name: str, snapshot: dict, link_stats: dict, modem: Optional[d
|
||||
pass # the page renders the shared WAN / Fiber panel from /api/modem
|
||||
else:
|
||||
detail['ports'] = (switches.get(name) or {}).get('ports') or {}
|
||||
# The port panel compares each port with the server on the other end
|
||||
hosts = link_stats.get('hosts') or {}
|
||||
neighbors = set(_port_neighbors(detail['ports']))
|
||||
detail['neighbor_hosts'] = {h: ifaces for h, ifaces in hosts.items() if h in neighbors}
|
||||
detail['info'] = (link_stats.get('unifi_devices') or {}).get(name) or {}
|
||||
detail['updated'] = link_stats.get('updated')
|
||||
return detail
|
||||
|
||||
Reference in New Issue
Block a user