Skip to content

Commit e875fef

Browse files
peelmanCopilot
andauthored
Add FDB table support for Nokia TiMOS devices (librenms#18713)
* Add FDB table support for Nokia TiMOS devices Implements FDB (Forwarding Database) table discovery for Nokia TiMOS devices using TIMETRA-SERV-MIB::tlsFdbInfoTable. Features: - Decodes TmnxEncapVal for VLAN ID extraction (supports dot1q and QinQ) - Only processes SAP-local entries (tlsFdbLocale = sap) - Auto-creates VLANs in database when not already present - Uses selective column walking for optimal performance The implementation walks only 3 required OIDs (tlsFdbLocale, tlsFdbPortId, tlsFdbEncapValue) rather than the full table entry, reducing discovery time by ~2.7x on large tables (388s → 144s in testing). * StyleCI Changes * Reactor Changes * add timos fdb-table test data * Typo in license... Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * find and replace fail * The prevailing preference is to have this enabled by default * Per @PipoCanaja; remove adding Vlans in FdbTable, add VlanDiscovery for Timos * Add Test data for 7750 VLANs module * refactor(timos): use parent VLAN discovery with SAP fallback Simplify discoverVlans() and discoverVlanPorts() to leverage the base OS class Q-BRIDGE-MIB implementation, falling back to Nokia SAP-based discovery only when standard MIBs return no results. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 6163f4f commit e875fef

7 files changed

Lines changed: 13595 additions & 1 deletion

File tree

LibreNMS/OS/Timos.php

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
use App\Models\MplsService;
4040
use App\Models\MplsTunnelArHop;
4141
use App\Models\MplsTunnelCHop;
42+
use App\Models\PortVlan;
4243
use App\Models\Transceiver;
44+
use App\Models\Vlan;
4345
use Illuminate\Support\Collection;
4446
use LibreNMS\Device\WirelessSensor;
4547
use LibreNMS\Enum\WirelessSensorType;
@@ -52,13 +54,14 @@
5254
use LibreNMS\Interfaces\Discovery\Sensors\WirelessRssiDiscovery;
5355
use LibreNMS\Interfaces\Discovery\Sensors\WirelessSnrDiscovery;
5456
use LibreNMS\Interfaces\Discovery\TransceiverDiscovery;
57+
use LibreNMS\Interfaces\Discovery\VlanDiscovery;
5558
use LibreNMS\Interfaces\Polling\MplsPolling;
5659
use LibreNMS\OS;
5760
use LibreNMS\RRD\RrdDefinition;
5861
use LibreNMS\Util\IP;
5962
use SnmpQuery;
6063

61-
class Timos extends OS implements MplsDiscovery, MplsPolling, TransceiverDiscovery, WirelessPowerDiscovery, WirelessSnrDiscovery, WirelessRsrqDiscovery, WirelessRssiDiscovery, WirelessRsrpDiscovery, WirelessChannelDiscovery
64+
class Timos extends OS implements MplsDiscovery, MplsPolling, TransceiverDiscovery, VlanDiscovery, WirelessPowerDiscovery, WirelessSnrDiscovery, WirelessRsrqDiscovery, WirelessRssiDiscovery, WirelessRsrpDiscovery, WirelessChannelDiscovery
6265
{
6366
public function discoverOS(Device $device): void
6467
{
@@ -943,6 +946,101 @@ public function discoverEntityPhysical(): Collection
943946
return $inventory;
944947
}
945948

949+
/**
950+
* Discover VLANs on Nokia TiMOS devices
951+
*
952+
* Uses parent Q-BRIDGE-MIB discovery first, then falls back to
953+
* extracting VLAN IDs from SAP encapsulation values.
954+
*
955+
* @return Collection of Vlan objects
956+
*/
957+
public function discoverVlans(): Collection
958+
{
959+
// Try standard Q-BRIDGE-MIB discovery from parent
960+
$vlans = parent::discoverVlans();
961+
if ($vlans->isNotEmpty()) {
962+
return $vlans;
963+
}
964+
965+
// Fallback: Extract VLANs from SAP encapsulation values
966+
return SnmpQuery::hideMib()->walk('TIMETRA-SAP-MIB::sapBaseInfoTable')
967+
->mapTable(function ($data, $svcId, $sapPortId, $sapEncapValue) {
968+
$vlanNumber = $this->extractVlanFromEncapValue($sapEncapValue);
969+
970+
if ($vlanNumber < 1 || $vlanNumber > 4094) {
971+
return null;
972+
}
973+
974+
return new Vlan([
975+
'vlan_vlan' => $vlanNumber,
976+
'vlan_name' => "VLAN $vlanNumber",
977+
]);
978+
})->filter()->unique('vlan_vlan')->values();
979+
}
980+
981+
/**
982+
* Discover VLAN-Port associations on Nokia TiMOS devices
983+
*
984+
* Uses parent Q-BRIDGE-MIB discovery first, then falls back to
985+
* extracting port membership from SAP encapsulation values.
986+
*
987+
* @param Collection $vlans Collection of discovered Vlan objects
988+
* @return Collection of PortVlan objects
989+
*/
990+
public function discoverVlanPorts($vlans): Collection
991+
{
992+
// Try standard Q-BRIDGE-MIB discovery from parent
993+
$portVlans = parent::discoverVlanPorts($vlans);
994+
if ($portVlans->isNotEmpty()) {
995+
return $portVlans;
996+
}
997+
998+
// Fallback: Extract VLAN-Port associations from SAP encapsulation values
999+
return SnmpQuery::hideMib()->walk('TIMETRA-SAP-MIB::sapBaseInfoTable')
1000+
->mapTable(function ($data, $svcId, $sapPortId, $sapEncapValue) use ($vlans) {
1001+
$vlanNumber = $this->extractVlanFromEncapValue($sapEncapValue);
1002+
1003+
if ($vlanNumber < 1 || $vlanNumber > 4094) {
1004+
return null;
1005+
}
1006+
1007+
$vlan = $vlans->firstWhere('vlan_vlan', $vlanNumber);
1008+
if (! $vlan) {
1009+
return null;
1010+
}
1011+
1012+
$portId = PortCache::getIdFromIfIndex((int) $sapPortId, $this->getDevice());
1013+
if (! $portId) {
1014+
return null;
1015+
}
1016+
1017+
return new PortVlan([
1018+
'port_id' => $portId,
1019+
'vlan' => $vlanNumber,
1020+
'state' => 'tagged',
1021+
]);
1022+
})->filter()->unique(fn ($item) => $item->port_id . '-' . $item->vlan)->values();
1023+
}
1024+
1025+
/**
1026+
* Extract outer VLAN ID from TmnxEncapVal
1027+
*
1028+
* TmnxEncapVal uses:
1029+
* - Lower 12 bits for dot1q (simple VLAN)
1030+
* - Lower 16 bits for outer VLAN in QinQ
1031+
* - Upper 16 bits for inner VLAN in QinQ
1032+
*
1033+
* @param int|string $encapVal The encoded encapsulation value
1034+
* @return int The VLAN ID (outer VLAN for QinQ)
1035+
*/
1036+
private function extractVlanFromEncapValue($encapVal): int
1037+
{
1038+
$encapVal = (int) $encapVal;
1039+
1040+
// Extract lower 12 bits for VLAN ID
1041+
return $encapVal & 0x0FFF;
1042+
}
1043+
9461044
private function parseIpField(array $data, string $ngField): ?string
9471045
{
9481046
if (isset($data[$ngField])) {
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<?php
2+
3+
/**
4+
* timos.inc.php
5+
*
6+
* Discover FDB data with TIMETRA-SERV-MIB for Nokia TiMOS devices
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU General Public License as published by
10+
* the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
16+
* GNU General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU General Public License
19+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
*
21+
* @link https://www.librenms.org
22+
*
23+
* @copyright 2025 LibreNMS Contributors
24+
* @author LibreNMS Contributors
25+
*/
26+
27+
use App\Facades\PortCache;
28+
use App\Models\Vlan;
29+
use Illuminate\Support\Facades\Log;
30+
use LibreNMS\Util\Mac;
31+
32+
/*
33+
* Nokia TiMOS devices use TIMETRA-SERV-MIB::tlsFdbInfoTable for FDB information.
34+
*
35+
* The table is indexed by:
36+
* - svcId (service ID)
37+
* - tlsFdbMacAddr (MAC address)
38+
*
39+
* Key OIDs:
40+
* - tlsFdbMacAddr: The MAC address
41+
* - tlsFdbLocale: Where the MAC is located (sap, sdp, cpm, endpoint, vxlan, evpnMpls, blackhole)
42+
* - tlsFdbPortId: The port ID when tlsFdbLocale is 'sap' (this is the TmnxPortID which equals ifIndex)
43+
* - tlsFdbEncapValue: The encapsulation value (encoded per TIMETRA-TC-MIB::TmnxEncapVal)
44+
*
45+
* TmnxEncapVal encoding (32-bit):
46+
* - nullEncap: 0
47+
* - dot1qEncap: lower 12 bits = VLAN ID (0x0FFF mask)
48+
* - qinqEncap: lower 16 bits = outer VLAN, upper 16 bits = inner VLAN
49+
*
50+
* We only process entries where tlsFdbLocale is 'sap' (1) as these are the local learned MACs.
51+
*
52+
* Nokia SAP identifier format: ServiceID:PortId:EncapValue (e.g., 100:1/1/1:500)
53+
*/
54+
55+
/**
56+
* Decode TmnxEncapVal to extract VLAN ID(s)
57+
*
58+
* @param int|string $encapVal The encoded encapsulation value
59+
* @return array Array with 'outer' and optionally 'inner' VLAN IDs
60+
*
61+
* @see TIMETRA-TC-MIB::TmnxEncapVal
62+
*/
63+
function decodeNokiaEncapValue($encapVal): array
64+
{
65+
$encapVal = (int) $encapVal;
66+
67+
// Null encapsulation
68+
if ($encapVal == 0) {
69+
return ['outer' => 0, 'inner' => null];
70+
}
71+
72+
// Check for QinQ: if upper 16 bits have a value (ignoring special bits)
73+
$innerVlan = ($encapVal >> 16) & 0x0FFF; // Upper 12 bits of upper 16 bits
74+
$outerVlan = $encapVal & 0x0FFF; // Lower 12 bits
75+
76+
if ($innerVlan > 0) {
77+
// QinQ encapsulation
78+
return ['outer' => $outerVlan, 'inner' => $innerVlan];
79+
}
80+
81+
// Simple dot1q encapsulation - VLAN is in lower 12 bits
82+
return ['outer' => $outerVlan, 'inner' => null];
83+
}
84+
85+
/**
86+
* Format TmnxEncapVal for display (Nokia-friendly format)
87+
*
88+
* @param int|string $encapVal The encoded encapsulation value
89+
* @return string Formatted encap value (e.g., "500" or "100.200" for QinQ)
90+
*/
91+
function formatNokiaEncapValue($encapVal): string
92+
{
93+
$decoded = decodeNokiaEncapValue($encapVal);
94+
95+
if ($decoded['inner'] !== null) {
96+
// QinQ format: outer.inner
97+
return $decoded['outer'] . '.' . $decoded['inner'];
98+
}
99+
100+
if ($decoded['outer'] == 4095) {
101+
return '*'; // Wildcard
102+
}
103+
104+
return (string) $decoded['outer'];
105+
}
106+
107+
// Walk only the required FDB columns for best performance
108+
// Testing showed: 5 columns = 388s, 3 columns = 144s, full table entry = 10+ min
109+
// The selective approach is fastest because tlsFdbInfoEntry has 25+ columns we don't need
110+
$fdbTable = SnmpQuery::hideMib()->walk([
111+
'TIMETRA-SERV-MIB::tlsFdbLocale',
112+
'TIMETRA-SERV-MIB::tlsFdbPortId',
113+
'TIMETRA-SERV-MIB::tlsFdbEncapValue',
114+
])->table(2);
115+
116+
if (! empty($fdbTable)) {
117+
// Count SAP entries for progress indication
118+
$sapCount = 0;
119+
foreach ($fdbTable as $svcId => $macEntries) {
120+
foreach ($macEntries as $entry) {
121+
$locale = $entry['tlsFdbLocale'] ?? null;
122+
if ($locale === 'sap' || $locale === '1' || $locale === 1) {
123+
$sapCount++;
124+
}
125+
}
126+
}
127+
echo "TIMETRA-SERV-MIB: $sapCount SAP entries" . PHP_EOL;
128+
129+
foreach ($fdbTable as $svcId => $macEntries) {
130+
foreach ($macEntries as $macIndex => $entry) {
131+
// Only process entries with tlsFdbLocale = 'sap' (1)
132+
// sap(1), sdp(2), cpm(3), endpoint(4), vxlan(5), evpnMpls(6), blackhole(7)
133+
$locale = $entry['tlsFdbLocale'] ?? null;
134+
if ($locale !== 'sap' && $locale !== '1' && $locale !== 1) {
135+
continue;
136+
}
137+
138+
// Get the port ID (this is the TmnxPortID which is the same as ifIndex for physical ports)
139+
$portId = $entry['tlsFdbPortId'] ?? null;
140+
if (empty($portId) || $portId == 0) {
141+
Log::debug("Skipping MAC $macIndex in svc $svcId - no valid port ID\n");
142+
continue;
143+
}
144+
145+
// Get the encapsulation value (VLAN ID)
146+
$encapValue = $entry['tlsFdbEncapValue'] ?? 0;
147+
148+
// Parse the MAC address - the index format contains the MAC address
149+
// Format: svcId.macAddr where macAddr is 6 octets separated by dots
150+
$mac_address = Mac::parse($macIndex)->hex();
151+
if (strlen($mac_address) != 12) {
152+
Log::debug("MAC address parsing failed for $macIndex\n");
153+
continue;
154+
}
155+
156+
// Get the port_id from the ifIndex (TmnxPortID equals ifIndex for physical ports)
157+
$port_id = PortCache::getIdFromIfIndex($portId, $device['device_id']);
158+
159+
if (! $port_id) {
160+
Log::debug("Could not find port for TmnxPortID $portId (MAC: $mac_address)\n");
161+
continue;
162+
}
163+
164+
// Decode the encapsulation value to get VLAN ID(s)
165+
// TmnxEncapVal is encoded: dot1q uses lower 12 bits, QinQ uses upper/lower 16 bits
166+
$decodedEncap = decodeNokiaEncapValue($encapValue);
167+
$vlanNumber = $decodedEncap['outer']; // Use outer VLAN for FDB lookup
168+
169+
// Skip if no valid VLAN (null encap or wildcard)
170+
if ($vlanNumber == 0 || $vlanNumber == 4095) {
171+
Log::debug("Skipping MAC $mac_address - no valid VLAN (encap: $encapValue, decoded: $vlanNumber)\n");
172+
continue;
173+
}
174+
175+
// Use VLAN ID from vlans_dict if available, otherwise use VLAN number directly
176+
// This allows FDB entries to be added even if VLAN discovery module is disabled
177+
$vlan_id = $vlans_dict[$vlanNumber] ?? $vlanNumber;
178+
179+
// Nokia SAP format: ServiceID:Port:EncapValue (formatted for display)
180+
$formattedEncap = formatNokiaEncapValue($encapValue);
181+
$sapIdentifier = "$svcId:$portId:$formattedEncap";
182+
183+
$insert[$vlan_id][$mac_address]['port_id'] = $port_id;
184+
Log::debug("SAP $sapIdentifier mac $mac_address vlan $vlanNumber port ($portId) $port_id\n");
185+
}
186+
}
187+
}
188+
189+
unset($fdbTable);

resources/definitions/os_detection/timos.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ discovery:
1212
sysObjectID:
1313
- .1.3.6.1.4.1.6527.
1414
discovery_modules:
15+
fdb-table: true
1516
bgp-peers: true
1617
vrf: true
1718
cisco-vrf-lite: false

tests/data/timos_7750-vlans.json

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"os": {
3+
"discovery": {
4+
"devices": [
5+
{
6+
"sysName": "<private>",
7+
"sysObjectID": ".1.3.6.1.4.1.6527.1.3.23",
8+
"sysDescr": "TiMOS-C-23.10.R1 cpm/x86hops64 Nokia 7250 IXR Copyright (c) 2000-2023 Nokia.\nAll rights reserved. All use subject to applicable license agreements.\r\nBuilt on Thu Oct 26 20:12:19 UTC 2023 by builder in /builds/2310B/R1/panos/main/sros",
9+
"sysContact": "<private>",
10+
"version": "23.10.R1",
11+
"hardware": "7250 IXR-s",
12+
"features": null,
13+
"location": "<private>",
14+
"os": "timos",
15+
"type": "network",
16+
"serial": "NK223835632",
17+
"icon": "nokia.svg"
18+
}
19+
]
20+
},
21+
"poller": "matches discovery"
22+
},
23+
"vlans": {
24+
"discovery": {
25+
"vlans": [
26+
{
27+
"vlan_vlan": 10,
28+
"vlan_domain": null,
29+
"vlan_name": "VLAN 10",
30+
"vlan_type": null,
31+
"vlan_state": 1
32+
},
33+
{
34+
"vlan_vlan": 20,
35+
"vlan_domain": null,
36+
"vlan_name": "VLAN 20",
37+
"vlan_type": null,
38+
"vlan_state": 1
39+
},
40+
{
41+
"vlan_vlan": 100,
42+
"vlan_domain": null,
43+
"vlan_name": "VLAN 100",
44+
"vlan_type": null,
45+
"vlan_state": 1
46+
},
47+
{
48+
"vlan_vlan": 200,
49+
"vlan_domain": null,
50+
"vlan_name": "VLAN 200",
51+
"vlan_type": null,
52+
"vlan_state": 1
53+
},
54+
{
55+
"vlan_vlan": 500,
56+
"vlan_domain": null,
57+
"vlan_name": "VLAN 500",
58+
"vlan_type": null,
59+
"vlan_state": 1
60+
},
61+
{
62+
"vlan_vlan": 1000,
63+
"vlan_domain": null,
64+
"vlan_name": "VLAN 1000",
65+
"vlan_type": null,
66+
"vlan_state": 1
67+
}
68+
]
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)