From fed279a0bdbcc22d71eb62d2c5627793de5c90d2 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 3 Oct 2026 02:23:26 -0400 Subject: [PATCH] Fix false LLDP_MISMATCH: compare the switch's name with the server's lldpd neighbour The switch port's LLDP entry describes the server, while the server's lldpctl describes the switch, so the two were never comparable and every link with lldpd running was flagged as cross-cabled. Pass the switch name in the port data and compare it with the server's neighbour instead; skip the check without it. --- app.py | 1 + diagnose.py | 13 +++++++------ tests/test_diagnose.py | 17 ++++++++++++++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 3de273d..d208004 100644 --- a/app.py +++ b/app.py @@ -436,6 +436,7 @@ def api_diagnose_start(): if pd.get('port_idx') == port_idx: port_data = dict(pd) port_data['name'] = pname + port_data['switch_name'] = switch_name break if not port_data: return jsonify({'error': f'Port {port_idx} not found on switch "{switch_name}"'}), 404 diff --git a/diagnose.py b/diagnose.py index e7ec513..b6e4300 100644 --- a/diagnose.py +++ b/diagnose.py @@ -530,15 +530,16 @@ class DiagnosticsRunner: add(warnings, 'KERNEL_EVENTS', f'{len(err_events)} recent kernel error event(s) for this interface in dmesg') - # LLDP validation + # LLDP validation: the neighbour the *server's* lldpd sees should be the switch + # that this port belongs to. (The switch port's own LLDP entry describes the server, + # so it must not be compared with the server's view of the switch.) if lldp.get('available'): - sw_lldp = switch_port_data.get('lldp') or {} - sw_system = (sw_lldp.get('system_name') or '').lower() + expected_switch = (switch_port_data.get('switch_name') or '').lower() srv_neighbor = (lldp.get('neighbor_system') or '').lower() - if sw_system and srv_neighbor and sw_system not in srv_neighbor and srv_neighbor not in sw_system: + if expected_switch and srv_neighbor and expected_switch not in srv_neighbor and srv_neighbor not in expected_switch: add(warnings, 'LLDP_MISMATCH', - f'LLDP mismatch: switch sees "{sw_lldp.get("system_name")}" but ' - f'server lldpctl sees "{lldp.get("neighbor_system")}" — cross-cabled port?') + f'LLDP mismatch: this port is on "{switch_port_data.get("switch_name")}" but the server\'s ' + f'lldpctl sees "{lldp.get("neighbor_system")}" — cross-cabled port?') else: add(info, 'LLDP_MISSING', 'lldpd not running on server — install lldpd for full path validation') diff --git a/tests/test_diagnose.py b/tests/test_diagnose.py index e70425d..0b924f2 100644 --- a/tests/test_diagnose.py +++ b/tests/test_diagnose.py @@ -473,12 +473,27 @@ class TestAnalyze: assert 'LLDP_MISSING' in codes def test_lldp_mismatch_is_warning(self): + # The server's lldpd sees a different switch than the one this port belongs to. sections = self._sections(lldpctl={'available': True, 'neighbor_system': 'wrong-switch'}) - switch_data = {'speed_mbps': 1000, 'lldp': {'system_name': 'core-sw-01'}} + switch_data = {'speed_mbps': 1000, 'switch_name': 'core-sw-01', 'lldp': {'system_name': 'server-a'}} result = DiagnosticsRunner.analyze(sections, switch_data) codes = [w['code'] for w in result['warnings']] assert 'LLDP_MISMATCH' in codes + def test_lldp_match_is_not_a_warning(self): + # Correct cabling: the switch sees the server, the server sees the switch. + sections = self._sections(lldpctl={'available': True, 'neighbor_system': 'core-sw-01'}) + switch_data = {'speed_mbps': 1000, 'switch_name': 'core-sw-01', 'lldp': {'system_name': 'server-a'}} + result = DiagnosticsRunner.analyze(sections, switch_data) + codes = [w['code'] for w in result['warnings']] + assert 'LLDP_MISMATCH' not in codes + + def test_lldp_without_switch_name_skips_check(self): + sections = self._sections(lldpctl={'available': True, 'neighbor_system': 'anything'}) + result = DiagnosticsRunner.analyze(sections, {'speed_mbps': 1000, 'lldp': {'system_name': 'server-a'}}) + codes = [w['code'] for w in result['warnings']] + assert 'LLDP_MISMATCH' not in codes + def test_healthy_link_no_issues(self): result = DiagnosticsRunner.analyze(self._sections(), {}) assert result['issues'] == []