master
go 850 lines 24.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package parity
4
5 import (
6 "encoding/hex"
7 "fmt"
8 "net/netip"
9 "sort"
10 "strconv"
11 "strings"
12
13 "github.com/netdata/netdata/go/plugins/pkg/l2topology"
14 )
15
16 // BuildOptions configures protocol extraction when converting walk fixtures
17 // into an engine result.
18 type BuildOptions struct {
19 EnableLLDP bool
20 EnableCDP bool
21 EnableBridge bool
22 EnableARP bool
23 }
24
25 // FixtureWalk is one device fixture with already-parsed walk records.
26 type FixtureWalk struct {
27 DeviceID string
28 Hostname string
29 Address string
30 Records []WalkRecord
31 }
32
33 // LoadScenarioWalks loads all walk fixtures referenced by a resolved scenario.
34 func LoadScenarioWalks(scenario ResolvedScenario) ([]FixtureWalk, error) {
35 walks := make([]FixtureWalk, 0, len(scenario.Fixtures))
36 for _, fixture := range scenario.Fixtures {
37 ds, err := LoadWalkFile(fixture.WalkFile)
38 if err != nil {
39 return nil, fmt.Errorf("load walk for fixture %q: %w", fixture.DeviceID, err)
40 }
41 walks = append(walks, FixtureWalk{
42 DeviceID: fixture.DeviceID,
43 Hostname: fixture.Hostname,
44 Address: fixture.Address,
45 Records: ds.Records,
46 })
47 }
48 return walks, nil
49 }
50
51 // BuildL2ResultFromWalks builds a deterministic layer-2 l2topology.Result from
52 // LLDP/CDP/FDB/ARP observations in walk fixtures.
53 func BuildL2ResultFromWalks(fixtures []FixtureWalk, opts BuildOptions) (l2topology.Result, error) {
54 if len(fixtures) == 0 {
55 return l2topology.Result{}, fmt.Errorf("at least one fixture is required")
56 }
57
58 observations := make([]l2topology.L2Observation, 0, len(fixtures))
59 for _, fx := range fixtures {
60 if strings.TrimSpace(fx.DeviceID) == "" {
61 return l2topology.Result{}, fmt.Errorf("fixture with empty device id")
62 }
63 observations = append(observations, parseFixture(fx).toObservation())
64 }
65
66 return l2topology.BuildL2ResultFromObservations(observations, l2topology.DiscoverOptions{
67 EnableLLDP: opts.EnableLLDP,
68 EnableCDP: opts.EnableCDP,
69 EnableBridge: opts.EnableBridge,
70 EnableARP: opts.EnableARP,
71 })
72 }
73
74 type parsedFixture struct {
75 deviceID string
76 hostname string
77 mgmtIP string
78 sysObjectID string
79 chassisID string
80 ifNameByIndex map[string]string
81 bridgePortToIfIndex map[string]string
82 portIDByNum map[string]string
83 portIDSubtypeByNum map[string]string
84 portDescByNum map[string]string
85 fdbEntries map[string]fdbObs
86 arpEntries map[string]arpObs
87 lldpRemotes map[string]lldpRemoteObs
88 cdpRemotes map[string]cdpRemoteObs
89 }
90
91 type lldpRemoteObs struct {
92 localPortNum string
93 remIndex string
94 chassisID string
95 sysName string
96 portID string
97 portIDSubtype string
98 portDesc string
99 mgmtIP string
100 }
101
102 type cdpRemoteObs struct {
103 ifIndex string
104 deviceIndex string
105 deviceID string
106 devicePort string
107 addressType string
108 address string
109 }
110
111 type fdbObs struct {
112 mac string
113 bridgePort string
114 status string
115 }
116
117 type arpObs struct {
118 ifIndex string
119 ip string
120 mac string
121 state string
122 addrType string
123 }
124
125 func parseFixture(f FixtureWalk) parsedFixture {
126 p := parsedFixture{
127 deviceID: strings.TrimSpace(f.DeviceID),
128 hostname: strings.TrimSpace(f.Hostname),
129 mgmtIP: strings.TrimSpace(f.Address),
130 ifNameByIndex: make(map[string]string),
131 bridgePortToIfIndex: make(map[string]string),
132 portIDByNum: make(map[string]string),
133 portIDSubtypeByNum: make(map[string]string),
134 portDescByNum: make(map[string]string),
135 fdbEntries: make(map[string]fdbObs),
136 arpEntries: make(map[string]arpObs),
137 lldpRemotes: make(map[string]lldpRemoteObs),
138 cdpRemotes: make(map[string]cdpRemoteObs),
139 }
140
141 for _, rec := range f.Records {
142 oid := normalizeOID(rec.OID)
143 value := strings.TrimSpace(rec.Value)
144
145 switch {
146 case oid == "1.3.6.1.2.1.1.2.0":
147 p.sysObjectID = value
148 case oid == "1.3.6.1.2.1.1.5.0":
149 if p.hostname == "" {
150 p.hostname = value
151 }
152 case oid == "1.0.8802.1.1.2.1.3.3.0":
153 p.hostname = value
154 case oid == "1.0.8802.1.1.2.1.3.2.0":
155 p.chassisID = normalizeHexToken(value)
156 case strings.HasPrefix(oid, "1.3.6.1.2.1.31.1.1.1.1."):
157 idx := strings.TrimPrefix(oid, "1.3.6.1.2.1.31.1.1.1.1.")
158 p.ifNameByIndex[idx] = value
159 case strings.HasPrefix(oid, "1.3.6.1.2.1.2.2.1.2."):
160 idx := strings.TrimPrefix(oid, "1.3.6.1.2.1.2.2.1.2.")
161 if _, ok := p.ifNameByIndex[idx]; !ok {
162 p.ifNameByIndex[idx] = value
163 }
164 case strings.HasPrefix(oid, "1.3.6.1.2.1.17.1.4.1.2."):
165 basePort := strings.TrimPrefix(oid, "1.3.6.1.2.1.17.1.4.1.2.")
166 basePort = strings.TrimSpace(basePort)
167 if basePort != "" {
168 p.bridgePortToIfIndex[basePort] = value
169 }
170 case strings.HasPrefix(oid, "1.3.6.1.2.1.17.4.3.1.2."):
171 if key, mac, ok := fdbIndexFromOID(oid, "1.3.6.1.2.1.17.4.3.1.2."); ok {
172 entry := p.fdbEntries[key]
173 if entry.mac == "" {
174 entry.mac = mac
175 }
176 entry.bridgePort = value
177 p.fdbEntries[key] = entry
178 }
179 case strings.HasPrefix(oid, "1.3.6.1.2.1.17.4.3.1.3."):
180 if key, mac, ok := fdbIndexFromOID(oid, "1.3.6.1.2.1.17.4.3.1.3."); ok {
181 entry := p.fdbEntries[key]
182 if entry.mac == "" {
183 entry.mac = mac
184 }
185 entry.status = value
186 p.fdbEntries[key] = entry
187 }
188 case strings.HasPrefix(oid, "1.3.6.1.2.1.4.22.1.2."):
189 if key, ifIndex, ip, ok := arpLegacyIndex(oid, "1.3.6.1.2.1.4.22.1.2."); ok {
190 entry := p.arpEntries[key]
191 entry.ifIndex = ifIndex
192 entry.ip = ip
193 entry.mac = value
194 entry.addrType = "ipv4"
195 p.arpEntries[key] = entry
196 }
197 case strings.HasPrefix(oid, "1.3.6.1.2.1.4.22.1.3."):
198 if key, ifIndex, ip, ok := arpLegacyIndex(oid, "1.3.6.1.2.1.4.22.1.3."); ok {
199 entry := p.arpEntries[key]
200 entry.ifIndex = ifIndex
201 entry.ip = ip
202 entry.addrType = "ipv4"
203 p.arpEntries[key] = entry
204 }
205 case strings.HasPrefix(oid, "1.3.6.1.2.1.4.22.1.4."):
206 if key, ifIndex, ip, ok := arpLegacyIndex(oid, "1.3.6.1.2.1.4.22.1.4."); ok {
207 entry := p.arpEntries[key]
208 entry.ifIndex = ifIndex
209 entry.ip = ip
210 entry.state = value
211 entry.addrType = "ipv4"
212 p.arpEntries[key] = entry
213 }
214 case strings.HasPrefix(oid, "1.0.8802.1.1.2.1.3.7.1.3."):
215 portNum := strings.TrimPrefix(oid, "1.0.8802.1.1.2.1.3.7.1.3.")
216 p.portIDByNum[portNum] = value
217 case strings.HasPrefix(oid, "1.0.8802.1.1.2.1.3.7.1.2."):
218 portNum := strings.TrimPrefix(oid, "1.0.8802.1.1.2.1.3.7.1.2.")
219 p.portIDSubtypeByNum[portNum] = value
220 case strings.HasPrefix(oid, "1.0.8802.1.1.2.1.3.7.1.4."):
221 portNum := strings.TrimPrefix(oid, "1.0.8802.1.1.2.1.3.7.1.4.")
222 p.portDescByNum[portNum] = value
223 case strings.HasPrefix(oid, "1.3.6.1.4.1.6527.3.1.2.59.3.1.1.4."):
224 if localPort, ok := nokiaLocalPortIndex(oid, "1.3.6.1.4.1.6527.3.1.2.59.3.1.1.4."); ok {
225 if portID := normalizeNokiaPortLabel(value); portID != "" {
226 if _, exists := p.portIDByNum[localPort]; !exists {
227 p.portIDByNum[localPort] = portID
228 }
229 }
230 if strings.TrimSpace(value) != "" {
231 if _, exists := p.portDescByNum[localPort]; !exists {
232 p.portDescByNum[localPort] = strings.TrimSpace(value)
233 }
234 }
235 }
236 case strings.HasPrefix(oid, "1.0.8802.1.1.2.1.4.1.1.5."):
237 if localPort, remIndex, ok := lldpRemoteIndex(oid, "1.0.8802.1.1.2.1.4.1.1.5."); ok {
238 entry := p.lldpRemote(localPort, remIndex)
239 entry.chassisID = normalizeHexToken(value)
240 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
241 }
242 case strings.HasPrefix(oid, "1.0.8802.1.1.2.1.4.1.1.7."):
243 if localPort, remIndex, ok := lldpRemoteIndex(oid, "1.0.8802.1.1.2.1.4.1.1.7."); ok {
244 entry := p.lldpRemote(localPort, remIndex)
245 entry.portID = value
246 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
247 }
248 case strings.HasPrefix(oid, "1.0.8802.1.1.2.1.4.1.1.6."):
249 if localPort, remIndex, ok := lldpRemoteIndex(oid, "1.0.8802.1.1.2.1.4.1.1.6."); ok {
250 entry := p.lldpRemote(localPort, remIndex)
251 entry.portIDSubtype = value
252 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
253 }
254 case strings.HasPrefix(oid, "1.0.8802.1.1.2.1.4.1.1.8."):
255 if localPort, remIndex, ok := lldpRemoteIndex(oid, "1.0.8802.1.1.2.1.4.1.1.8."); ok {
256 entry := p.lldpRemote(localPort, remIndex)
257 entry.portDesc = value
258 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
259 }
260 case strings.HasPrefix(oid, "1.0.8802.1.1.2.1.4.1.1.9."):
261 if localPort, remIndex, ok := lldpRemoteIndex(oid, "1.0.8802.1.1.2.1.4.1.1.9."); ok {
262 entry := p.lldpRemote(localPort, remIndex)
263 entry.sysName = value
264 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
265 }
266 case strings.HasPrefix(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.1.1.5."):
267 if localPort, remIndex, ok := nokiaLldpRemoteIndex(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.1.1.5."); ok {
268 entry := p.lldpRemote(localPort, remIndex)
269 entry.chassisID = normalizeHexToken(value)
270 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
271 }
272 case strings.HasPrefix(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.1.1.7."):
273 if localPort, remIndex, ok := nokiaLldpRemoteIndex(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.1.1.7."); ok {
274 entry := p.lldpRemote(localPort, remIndex)
275 entry.portID = value
276 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
277 }
278 case strings.HasPrefix(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.1.1.6."):
279 if localPort, remIndex, ok := nokiaLldpRemoteIndex(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.1.1.6."); ok {
280 entry := p.lldpRemote(localPort, remIndex)
281 entry.portIDSubtype = value
282 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
283 }
284 case strings.HasPrefix(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.1.1.8."):
285 if localPort, remIndex, ok := nokiaLldpRemoteIndex(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.1.1.8."); ok {
286 entry := p.lldpRemote(localPort, remIndex)
287 entry.portDesc = value
288 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
289 }
290 case strings.HasPrefix(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.1.1.9."):
291 if localPort, remIndex, ok := nokiaLldpRemoteIndex(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.1.1.9."); ok {
292 entry := p.lldpRemote(localPort, remIndex)
293 entry.sysName = value
294 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
295 }
296 case strings.HasPrefix(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.2.1.3."):
297 if localPort, remIndex, mgmtIP, ok := nokiaLldpRemoteMgmtIndex(oid, "1.3.6.1.4.1.6527.3.1.2.59.4.2.1.3."); ok {
298 entry := p.lldpRemote(localPort, remIndex)
299 entry.mgmtIP = mgmtIP
300 p.lldpRemotes[lldpRemoteKey(localPort, remIndex)] = entry
301 }
302 case strings.HasPrefix(oid, "1.3.6.1.4.1.9.9.23.1.2.1.1.6."):
303 if ifIndex, devIndex, ok := cdpRemoteIndex(oid, "1.3.6.1.4.1.9.9.23.1.2.1.1.6."); ok {
304 entry := p.cdpRemote(ifIndex, devIndex)
305 entry.deviceID = value
306 p.cdpRemotes[cdpRemoteKey(ifIndex, devIndex)] = entry
307 }
308 case strings.HasPrefix(oid, "1.3.6.1.4.1.9.9.23.1.2.1.1.7."):
309 if ifIndex, devIndex, ok := cdpRemoteIndex(oid, "1.3.6.1.4.1.9.9.23.1.2.1.1.7."); ok {
310 entry := p.cdpRemote(ifIndex, devIndex)
311 entry.devicePort = value
312 p.cdpRemotes[cdpRemoteKey(ifIndex, devIndex)] = entry
313 }
314 case strings.HasPrefix(oid, "1.3.6.1.4.1.9.9.23.1.2.1.1.4."):
315 if ifIndex, devIndex, ok := cdpRemoteIndex(oid, "1.3.6.1.4.1.9.9.23.1.2.1.1.4."); ok {
316 entry := p.cdpRemote(ifIndex, devIndex)
317 entry.address = value
318 p.cdpRemotes[cdpRemoteKey(ifIndex, devIndex)] = entry
319 }
320 case strings.HasPrefix(oid, "1.3.6.1.4.1.9.9.23.1.2.1.1.3."):
321 if ifIndex, devIndex, ok := cdpRemoteIndex(oid, "1.3.6.1.4.1.9.9.23.1.2.1.1.3."); ok {
322 entry := p.cdpRemote(ifIndex, devIndex)
323 entry.addressType = value
324 p.cdpRemotes[cdpRemoteKey(ifIndex, devIndex)] = entry
325 }
326 }
327 }
328
329 if p.hostname == "" {
330 p.hostname = p.deviceID
331 }
332 return p
333 }
334
335 func (p parsedFixture) toObservation() l2topology.L2Observation {
336 obs := l2topology.L2Observation{
337 DeviceID: p.deviceID,
338 Hostname: p.hostname,
339 ManagementIP: p.mgmtIP,
340 SysObjectID: p.sysObjectID,
341 ChassisID: p.chassisID,
342 Interfaces: p.toInterfaces(),
343 BridgePorts: p.toBridgePorts(),
344 FDBEntries: p.toFDBEntries(),
345 ARPNDEntries: p.toARPNDEntries(),
346 LLDPRemotes: p.toLLDPRemotes(),
347 CDPRemotes: p.toCDPRemotes(),
348 }
349 return obs
350 }
351
352 func (p parsedFixture) toInterfaces() []l2topology.ObservedInterface {
353 if len(p.ifNameByIndex) == 0 {
354 return nil
355 }
356 out := make([]l2topology.ObservedInterface, 0, len(p.ifNameByIndex))
357 for idx, name := range p.ifNameByIndex {
358 n, err := strconv.Atoi(idx)
359 if err != nil {
360 continue
361 }
362 ifName := strings.TrimSpace(name)
363 if ifName == "" {
364 continue
365 }
366 out = append(out, l2topology.ObservedInterface{IfIndex: n, IfName: ifName, IfDescr: ifName})
367 }
368 sort.Slice(out, func(i, j int) bool {
369 if out[i].IfIndex != out[j].IfIndex {
370 return out[i].IfIndex < out[j].IfIndex
371 }
372 return out[i].IfName < out[j].IfName
373 })
374 return out
375 }
376
377 func (p parsedFixture) toBridgePorts() []l2topology.BridgePortObservation {
378 if len(p.bridgePortToIfIndex) == 0 {
379 return nil
380 }
381 out := make([]l2topology.BridgePortObservation, 0, len(p.bridgePortToIfIndex))
382 for basePort, ifIndexRaw := range p.bridgePortToIfIndex {
383 ifIndex, err := strconv.Atoi(strings.TrimSpace(ifIndexRaw))
384 if err != nil || ifIndex <= 0 {
385 continue
386 }
387 basePort = strings.TrimSpace(basePort)
388 if basePort == "" {
389 continue
390 }
391 out = append(out, l2topology.BridgePortObservation{
392 BasePort: basePort,
393 IfIndex: ifIndex,
394 })
395 }
396 sort.Slice(out, func(i, j int) bool {
397 a, b := out[i], out[j]
398 if a.BasePort != b.BasePort {
399 return a.BasePort < b.BasePort
400 }
401 return a.IfIndex < b.IfIndex
402 })
403 return out
404 }
405
406 func (p parsedFixture) toFDBEntries() []l2topology.FDBObservation {
407 if len(p.fdbEntries) == 0 {
408 return nil
409 }
410 out := make([]l2topology.FDBObservation, 0, len(p.fdbEntries))
411 for _, entry := range p.fdbEntries {
412 mac := normalizeHexToken(entry.mac)
413 if mac == "" {
414 continue
415 }
416 out = append(out, l2topology.FDBObservation{
417 MAC: mac,
418 BridgePort: strings.TrimSpace(entry.bridgePort),
419 Status: strings.TrimSpace(entry.status),
420 })
421 }
422 sort.Slice(out, func(i, j int) bool {
423 a, b := out[i], out[j]
424 if a.BridgePort != b.BridgePort {
425 return a.BridgePort < b.BridgePort
426 }
427 if a.MAC != b.MAC {
428 return a.MAC < b.MAC
429 }
430 return a.Status < b.Status
431 })
432 return out
433 }
434
435 func (p parsedFixture) toARPNDEntries() []l2topology.ARPNDObservation {
436 if len(p.arpEntries) == 0 {
437 return nil
438 }
439 out := make([]l2topology.ARPNDObservation, 0, len(p.arpEntries))
440 for _, entry := range p.sortedARPEntries() {
441 ifIndex, err := strconv.Atoi(strings.TrimSpace(entry.ifIndex))
442 if err != nil {
443 ifIndex = 0
444 }
445 out = append(out, l2topology.ARPNDObservation{
446 Protocol: "arp",
447 IfIndex: ifIndex,
448 IfName: p.ifNameByIndex[entry.ifIndex],
449 IP: strings.TrimSpace(entry.ip),
450 MAC: normalizeHexToken(entry.mac),
451 State: strings.TrimSpace(entry.state),
452 AddrType: strings.TrimSpace(entry.addrType),
453 })
454 }
455 return out
456 }
457
458 func (p parsedFixture) toLLDPRemotes() []l2topology.LLDPRemoteObservation {
459 if len(p.lldpRemotes) == 0 {
460 return nil
461 }
462 obs := make([]l2topology.LLDPRemoteObservation, 0, len(p.lldpRemotes))
463 for _, remote := range p.sortedLLDPRemotes() {
464 obs = append(obs, l2topology.LLDPRemoteObservation{
465 LocalPortNum: remote.localPortNum,
466 RemoteIndex: remote.remIndex,
467 LocalPortID: p.portIDByNum[remote.localPortNum],
468 LocalPortIDSubtype: p.portIDSubtypeByNum[remote.localPortNum],
469 LocalPortDesc: p.portDescByNum[remote.localPortNum],
470 ChassisID: remote.chassisID,
471 SysName: remote.sysName,
472 PortID: remote.portID,
473 PortIDSubtype: remote.portIDSubtype,
474 PortDesc: remote.portDesc,
475 ManagementIP: remote.mgmtIP,
476 })
477 }
478 return obs
479 }
480
481 func (p parsedFixture) toCDPRemotes() []l2topology.CDPRemoteObservation {
482 if len(p.cdpRemotes) == 0 {
483 return nil
484 }
485 obs := make([]l2topology.CDPRemoteObservation, 0, len(p.cdpRemotes))
486 for _, remote := range p.sortedCDPRemotes() {
487 ifIndex, err := strconv.Atoi(remote.ifIndex)
488 if err != nil {
489 ifIndex = 0
490 }
491 localName := p.ifNameByIndex[remote.ifIndex]
492 obs = append(obs, l2topology.CDPRemoteObservation{
493 LocalIfIndex: ifIndex,
494 LocalIfName: localName,
495 DeviceIndex: remote.deviceIndex,
496 DeviceID: remote.deviceID,
497 DevicePort: remote.devicePort,
498 Address: remote.address,
499 })
500 }
501 return obs
502 }
503
504 func (p parsedFixture) lldpRemote(localPort, remIndex string) lldpRemoteObs {
505 key := lldpRemoteKey(localPort, remIndex)
506 entry := p.lldpRemotes[key]
507 if entry.localPortNum == "" {
508 entry.localPortNum = localPort
509 entry.remIndex = remIndex
510 }
511 return entry
512 }
513
514 func (p parsedFixture) cdpRemote(ifIndex, devIndex string) cdpRemoteObs {
515 key := cdpRemoteKey(ifIndex, devIndex)
516 entry := p.cdpRemotes[key]
517 if entry.ifIndex == "" {
518 entry.ifIndex = ifIndex
519 entry.deviceIndex = devIndex
520 }
521 return entry
522 }
523
524 func (p parsedFixture) sortedLLDPRemotes() []lldpRemoteObs {
525 out := make([]lldpRemoteObs, 0, len(p.lldpRemotes))
526 for _, rem := range p.lldpRemotes {
527 if rem.chassisID == "" && rem.sysName == "" {
528 continue
529 }
530 out = append(out, rem)
531 }
532 sort.Slice(out, func(i, j int) bool {
533 a, b := out[i], out[j]
534 if a.localPortNum != b.localPortNum {
535 return a.localPortNum < b.localPortNum
536 }
537 if a.remIndex != b.remIndex {
538 return a.remIndex < b.remIndex
539 }
540 if a.sysName != b.sysName {
541 return a.sysName < b.sysName
542 }
543 if a.chassisID != b.chassisID {
544 return a.chassisID < b.chassisID
545 }
546 if a.portID != b.portID {
547 return a.portID < b.portID
548 }
549 if a.portIDSubtype != b.portIDSubtype {
550 return a.portIDSubtype < b.portIDSubtype
551 }
552 if a.portDesc != b.portDesc {
553 return a.portDesc < b.portDesc
554 }
555 return a.mgmtIP < b.mgmtIP
556 })
557 return out
558 }
559
560 func (p parsedFixture) sortedCDPRemotes() []cdpRemoteObs {
561 out := make([]cdpRemoteObs, 0, len(p.cdpRemotes))
562 for _, rem := range p.cdpRemotes {
563 if rem.deviceID == "" && rem.address == "" {
564 continue
565 }
566 out = append(out, rem)
567 }
568 sort.Slice(out, func(i, j int) bool {
569 a, b := out[i], out[j]
570 if a.ifIndex != b.ifIndex {
571 return a.ifIndex < b.ifIndex
572 }
573 if a.deviceIndex != b.deviceIndex {
574 return a.deviceIndex < b.deviceIndex
575 }
576 return a.deviceID < b.deviceID
577 })
578 return out
579 }
580
581 func (p parsedFixture) sortedARPEntries() []arpObs {
582 out := make([]arpObs, 0, len(p.arpEntries))
583 for _, entry := range p.arpEntries {
584 if strings.TrimSpace(entry.ip) == "" && strings.TrimSpace(entry.mac) == "" {
585 continue
586 }
587 out = append(out, entry)
588 }
589 sort.Slice(out, func(i, j int) bool {
590 a, b := out[i], out[j]
591 if a.ifIndex != b.ifIndex {
592 return a.ifIndex < b.ifIndex
593 }
594 if a.ip != b.ip {
595 return a.ip < b.ip
596 }
597 if a.mac != b.mac {
598 return a.mac < b.mac
599 }
600 return a.state < b.state
601 })
602 return out
603 }
604
605 func normalizeHexToken(v string) string {
606 if decoded := decodeHexBytes(v); len(decoded) > 0 {
607 if len(decoded) == 6 {
608 parts := make([]string, 0, 6)
609 for _, b := range decoded {
610 parts = append(parts, fmt.Sprintf("%02x", b))
611 }
612 return strings.Join(parts, ":")
613 }
614 if asIP := decodeHexIP(v); asIP != "" {
615 return asIP
616 }
617 }
618 return strings.TrimSpace(v)
619 }
620
621 func decodeHexIP(v string) string {
622 bs := decodeHexBytes(v)
623 if len(bs) == 4 {
624 addr, ok := netip.AddrFromSlice(bs)
625 if ok {
626 return addr.Unmap().String()
627 }
628 }
629 if len(bs) == 16 {
630 addr, ok := netip.AddrFromSlice(bs)
631 if ok {
632 return addr.String()
633 }
634 }
635 return ""
636 }
637
638 func decodeHexBytes(v string) []byte {
639 clean := strings.ToLower(strings.TrimSpace(v))
640 clean = strings.TrimPrefix(clean, "0x")
641 if clean == "" {
642 return nil
643 }
644
645 if strings.ContainsAny(clean, ":-. \t") {
646 parts := strings.FieldsFunc(clean, func(r rune) bool {
647 return r == ':' || r == '-' || r == '.' || r == ' ' || r == '\t'
648 })
649 if len(parts) == 0 {
650 return nil
651 }
652
653 out := make([]byte, 0, len(parts))
654 for _, part := range parts {
655 part = strings.TrimSpace(part)
656 if part == "" {
657 continue
658 }
659 if len(part) > 2 {
660 return nil
661 }
662 if len(part) == 1 {
663 part = "0" + part
664 }
665 b, err := hex.DecodeString(part)
666 if err != nil || len(b) != 1 {
667 return nil
668 }
669 out = append(out, b[0])
670 }
671 if len(out) == 0 {
672 return nil
673 }
674 return out
675 }
676
677 if len(clean)%2 == 1 {
678 clean = "0" + clean
679 }
680 bs, err := hex.DecodeString(clean)
681 if err != nil {
682 return nil
683 }
684 return bs
685 }
686
687 func lldpRemoteIndex(oid, prefix string) (localPort string, remIndex string, ok bool) {
688 suffix := strings.TrimPrefix(oid, prefix)
689 suffix = strings.TrimPrefix(suffix, ".")
690 parts := strings.Split(suffix, ".")
691 if len(parts) < 2 {
692 return "", "", false
693 }
694 return parts[len(parts)-2], parts[len(parts)-1], true
695 }
696
697 func nokiaLocalPortIndex(oid, prefix string) (localPort string, ok bool) {
698 suffix := strings.TrimPrefix(oid, prefix)
699 suffix = strings.TrimPrefix(suffix, ".")
700 parts := strings.Split(suffix, ".")
701 if len(parts) < 2 {
702 return "", false
703 }
704 localPort = strings.TrimSpace(parts[0])
705 if localPort == "" {
706 return "", false
707 }
708 return localPort, true
709 }
710
711 func normalizeNokiaPortLabel(v string) string {
712 raw := strings.TrimSpace(v)
713 if raw == "" {
714 return ""
715 }
716 parts := strings.SplitN(raw, ",", 2)
717 label := strings.TrimSpace(parts[0])
718 if label != "" {
719 return label
720 }
721 return raw
722 }
723
724 func nokiaLldpRemoteIndex(oid, prefix string) (localPort string, remIndex string, ok bool) {
725 suffix := strings.TrimPrefix(oid, prefix)
726 suffix = strings.TrimPrefix(suffix, ".")
727 parts := strings.Split(suffix, ".")
728 if len(parts) < 3 {
729 return "", "", false
730 }
731
732 localPos := len(parts) - 2
733 if len(parts) >= 4 {
734 localPos = len(parts) - 3
735 }
736
737 localPort = strings.TrimSpace(parts[localPos])
738 remIndex = strings.TrimSpace(parts[len(parts)-1])
739 if localPort == "" || remIndex == "" {
740 return "", "", false
741 }
742 return localPort, remIndex, true
743 }
744
745 func nokiaLldpRemoteMgmtIndex(oid, prefix string) (localPort string, remIndex string, mgmtIP string, ok bool) {
746 suffix := strings.TrimPrefix(oid, prefix)
747 suffix = strings.TrimPrefix(suffix, ".")
748 parts := strings.Split(suffix, ".")
749 if len(parts) < 10 {
750 return "", "", "", false
751 }
752
753 localPort = strings.TrimSpace(parts[1])
754 remIndex = strings.TrimSpace(parts[3])
755 if localPort == "" || remIndex == "" {
756 return "", "", "", false
757 }
758
759 if strings.TrimSpace(parts[len(parts)-5]) != "4" {
760 return "", "", "", false
761 }
762
763 octets := make([]byte, 0, 4)
764 for _, part := range parts[len(parts)-4:] {
765 n, err := strconv.Atoi(strings.TrimSpace(part))
766 if err != nil || n < 0 || n > 255 {
767 return "", "", "", false
768 }
769 octets = append(octets, byte(n))
770 }
771
772 addr, addrOK := netip.AddrFromSlice(octets)
773 if !addrOK {
774 return "", "", "", false
775 }
776 return localPort, remIndex, addr.Unmap().String(), true
777 }
778
779 func cdpRemoteIndex(oid, prefix string) (ifIndex string, deviceIndex string, ok bool) {
780 suffix := strings.TrimPrefix(oid, prefix)
781 suffix = strings.TrimPrefix(suffix, ".")
782 parts := strings.Split(suffix, ".")
783 if len(parts) < 2 {
784 return "", "", false
785 }
786 return parts[len(parts)-2], parts[len(parts)-1], true
787 }
788
789 func fdbIndexFromOID(oid, prefix string) (key string, mac string, ok bool) {
790 suffix := strings.TrimPrefix(oid, prefix)
791 suffix = strings.TrimPrefix(suffix, ".")
792 parts := strings.Split(suffix, ".")
793 if len(parts) != 6 {
794 return "", "", false
795 }
796
797 octets := make([]byte, 0, 6)
798 for _, part := range parts {
799 n, err := strconv.Atoi(strings.TrimSpace(part))
800 if err != nil || n < 0 || n > 255 {
801 return "", "", false
802 }
803 octets = append(octets, byte(n))
804 }
805
806 macParts := make([]string, 0, 6)
807 for _, octet := range octets {
808 macParts = append(macParts, fmt.Sprintf("%02x", octet))
809 }
810
811 return strings.Join(parts, "."), strings.Join(macParts, ":"), true
812 }
813
814 func arpLegacyIndex(oid, prefix string) (key string, ifIndex string, ip string, ok bool) {
815 suffix := strings.TrimPrefix(oid, prefix)
816 suffix = strings.TrimPrefix(suffix, ".")
817 parts := strings.Split(suffix, ".")
818 if len(parts) != 5 {
819 return "", "", "", false
820 }
821
822 ifIndex = strings.TrimSpace(parts[0])
823 if ifIndex == "" {
824 return "", "", "", false
825 }
826
827 octets := make([]byte, 0, 4)
828 for _, part := range parts[1:] {
829 n, err := strconv.Atoi(strings.TrimSpace(part))
830 if err != nil || n < 0 || n > 255 {
831 return "", "", "", false
832 }
833 octets = append(octets, byte(n))
834 }
835 addr, addrOK := netip.AddrFromSlice(octets)
836 if !addrOK {
837 return "", "", "", false
838 }
839 ip = addr.Unmap().String()
840
841 return ifIndex + "|" + ip, ifIndex, ip, true
842 }
843
844 func lldpRemoteKey(localPort, remIndex string) string {
845 return localPort + ":" + remIndex
846 }
847
848 func cdpRemoteKey(ifIndex, devIndex string) string {
849 return ifIndex + ":" + devIndex
850 }