master
go 314 lines 7.93 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package l2topology
4
5 import (
6 "strings"
7 "time"
8 )
9
10 func projectAdjacencyLinks(
11 adjacencies []Adjacency,
12 layer string,
13 collectedAt time.Time,
14 deviceByID map[string]Device,
15 ifIndexByDeviceName map[string]int,
16 ifaceByDeviceIndex map[string]Interface,
17 ) projectedLinks {
18 out := projectedLinks{
19 links: make([]Link, 0, len(adjacencies)),
20 }
21 if len(adjacencies) == 0 {
22 return out
23 }
24
25 pairs := make(map[string]*pairedLinkAccumulator)
26 pairOrder := make([]string, 0)
27
28 for _, adj := range adjacencies {
29 protocol := strings.ToLower(strings.TrimSpace(adj.Protocol))
30 link := adjacencyToTopologyLink(adj, protocol, layer, collectedAt, deviceByID, ifIndexByDeviceName, ifaceByDeviceIndex)
31
32 pairID := strings.TrimSpace(adj.Labels[adjacencyLabelPairID])
33 if pairID != "" {
34 acc := pairs[pairID]
35 if acc == nil {
36 acc = &pairedLinkAccumulator{}
37 pairs[pairID] = acc
38 pairOrder = append(pairOrder, pairID)
39 }
40
41 entry := &builtAdjacencyLink{
42 adj: adj,
43 protocol: protocol,
44 link: link,
45 }
46 acc.all = append(acc.all, entry)
47 continue
48 }
49
50 out.links = append(out.links, link)
51 incrementProjectedProtocolCounters(&out, protocol, false)
52 }
53
54 for _, pairID := range pairOrder {
55 acc := pairs[pairID]
56 if acc == nil {
57 continue
58 }
59
60 if left, right, ok := reversePairEntriesForBidirectionalMerge(acc.all); ok {
61 merged := left.link
62 merged.Direction = "bidirectional"
63 merged.Src = mergeEndpointIPHints(left.link.Src, right.link.Dst)
64 merged.Dst = mergeEndpointIPHints(right.link.Src, left.link.Dst)
65 merged.Metrics = buildPairedLinkMetrics(left.adj.Labels, right.adj.Labels)
66 out.links = append(out.links, merged)
67 incrementProjectedProtocolCounters(&out, left.protocol, true)
68 continue
69 }
70
71 backfillPairGroupMissingEndpointPorts(acc.all)
72 for _, entry := range acc.all {
73 if entry == nil {
74 continue
75 }
76 out.links = append(out.links, entry.link)
77 incrementProjectedProtocolCounters(&out, entry.protocol, false)
78 }
79 }
80
81 sortTopologyLinks(out.links)
82 return out
83 }
84
85 func reversePairEntriesForBidirectionalMerge(entries []*builtAdjacencyLink) (left, right *builtAdjacencyLink, ok bool) {
86 if len(entries) != 2 || entries[0] == nil || entries[1] == nil {
87 return nil, nil, false
88 }
89
90 a := entries[0]
91 b := entries[1]
92 aSrc := strings.TrimSpace(a.adj.SourceID)
93 aDst := strings.TrimSpace(a.adj.TargetID)
94 bSrc := strings.TrimSpace(b.adj.SourceID)
95 bDst := strings.TrimSpace(b.adj.TargetID)
96 if aSrc == "" || aDst == "" || bSrc == "" || bDst == "" {
97 return nil, nil, false
98 }
99 if aSrc != bDst || aDst != bSrc {
100 return nil, nil, false
101 }
102
103 if pairedEntryDeterministicKey(b) < pairedEntryDeterministicKey(a) {
104 a, b = b, a
105 }
106 return a, b, true
107 }
108
109 func pairedEntryDeterministicKey(entry *builtAdjacencyLink) string {
110 if entry == nil {
111 return ""
112 }
113 return strings.Join([]string{
114 strings.TrimSpace(entry.protocol),
115 strings.TrimSpace(entry.adj.SourceID),
116 strings.TrimSpace(entry.adj.SourcePort),
117 strings.TrimSpace(entry.adj.TargetID),
118 strings.TrimSpace(entry.adj.TargetPort),
119 }, keySep)
120 }
121
122 func backfillPairGroupMissingEndpointPorts(entries []*builtAdjacencyLink) {
123 if len(entries) < 2 {
124 return
125 }
126
127 directionToIndexes := make(map[string][]int, len(entries))
128 for i, entry := range entries {
129 if entry == nil {
130 continue
131 }
132 src := strings.TrimSpace(entry.adj.SourceID)
133 dst := strings.TrimSpace(entry.adj.TargetID)
134 if src == "" || dst == "" {
135 continue
136 }
137 key := src + keySep + dst
138 directionToIndexes[key] = append(directionToIndexes[key], i)
139 }
140
141 for i, entry := range entries {
142 if entry == nil {
143 continue
144 }
145 src := strings.TrimSpace(entry.adj.SourceID)
146 dst := strings.TrimSpace(entry.adj.TargetID)
147 if src == "" || dst == "" {
148 continue
149 }
150
151 reverseKey := dst + keySep + src
152 candidates := directionToIndexes[reverseKey]
153 if len(candidates) != 1 {
154 continue
155 }
156
157 reverseEntry := entries[candidates[0]]
158 if reverseEntry == nil || candidates[0] == i {
159 continue
160 }
161
162 entry.link.Src = backfillEndpointPortFromPeer(entry.link.Src, reverseEntry.link.Dst)
163 entry.link.Dst = backfillEndpointPortFromPeer(entry.link.Dst, reverseEntry.link.Src)
164 }
165 }
166
167 func endpointHasKnownCanonicalPort(endpoint LinkEndpoint) bool {
168 return strings.TrimSpace(topologyCanonicalPortName(endpoint.Attributes)) != ""
169 }
170
171 func backfillEndpointPortFromPeer(endpoint LinkEndpoint, peer LinkEndpoint) LinkEndpoint {
172 if endpointHasKnownCanonicalPort(endpoint) || !endpointHasKnownCanonicalPort(peer) {
173 return endpoint
174 }
175
176 attrs := cloneAnyMap(endpoint.Attributes)
177 if attrs == nil {
178 attrs = make(map[string]any)
179 }
180 peerAttrs := peer.Attributes
181 if len(peerAttrs) == 0 {
182 return endpoint
183 }
184
185 if topologyAttrInt(attrs, "if_index") <= 0 {
186 if ifIndex := topologyAttrInt(peerAttrs, "if_index"); ifIndex > 0 {
187 attrs["if_index"] = ifIndex
188 }
189 }
190
191 copyIfMissing := func(key string) {
192 if topologyAttrString(attrs, key) != "" {
193 return
194 }
195 if value := topologyAttrString(peerAttrs, key); value != "" {
196 attrs[key] = value
197 }
198 }
199
200 copyIfMissing("if_name")
201 copyIfMissing("if_descr")
202 copyIfMissing("if_alias")
203 copyIfMissing("port_id")
204 copyIfMissing("port_name")
205 copyIfMissing("bridge_port")
206 copyIfMissing("if_admin_status")
207 copyIfMissing("if_oper_status")
208
209 endpoint.Attributes = pruneTopologyAttributes(attrs)
210 return endpoint
211 }
212
213 func adjacencyToTopologyLink(
214 adj Adjacency,
215 protocol string,
216 layer string,
217 collectedAt time.Time,
218 deviceByID map[string]Device,
219 ifIndexByDeviceName map[string]int,
220 ifaceByDeviceIndex map[string]Interface,
221 ) Link {
222 src := adjacencySideToEndpoint(deviceByID[adj.SourceID], adj.SourcePort, ifIndexByDeviceName, ifaceByDeviceIndex)
223 dst := adjacencySideToEndpoint(deviceByID[adj.TargetID], adj.TargetPort, ifIndexByDeviceName, ifaceByDeviceIndex)
224 if rawAddress := strings.TrimSpace(adj.Labels["remote_address_raw"]); rawAddress != "" {
225 dst.Match.IPAddresses = uniqueTopologyStrings(append(dst.Match.IPAddresses, rawAddress))
226 }
227
228 link := Link{
229 Layer: layer,
230 Protocol: protocol,
231 LinkType: protocol,
232 Direction: "unidirectional",
233 Src: src,
234 Dst: dst,
235 DiscoveredAt: topologyTimePtr(collectedAt),
236 LastSeen: topologyTimePtr(collectedAt),
237 }
238 if len(adj.Labels) > 0 {
239 link.Metrics = mapStringStringToAny(adj.Labels)
240 }
241 return link
242 }
243
244 func buildPairedLinkMetrics(sourceLabels, targetLabels map[string]string) map[string]any {
245 metrics := make(map[string]any)
246
247 pairID := strings.TrimSpace(sourceLabels[adjacencyLabelPairID])
248 if pairID == "" {
249 pairID = strings.TrimSpace(targetLabels[adjacencyLabelPairID])
250 }
251 if pairID != "" {
252 metrics[adjacencyLabelPairID] = pairID
253 }
254
255 pairPass := strings.TrimSpace(sourceLabels[adjacencyLabelPairPass])
256 if pairPass == "" {
257 pairPass = strings.TrimSpace(targetLabels[adjacencyLabelPairPass])
258 }
259 if pairPass != "" {
260 metrics[adjacencyLabelPairPass] = pairPass
261 }
262 metrics["pair_consistent"] = true
263
264 for key, value := range sourceLabels {
265 key = strings.TrimSpace(key)
266 value = strings.TrimSpace(value)
267 if key == "" || value == "" || isPairLabelKey(key) {
268 continue
269 }
270 metrics["src_"+key] = value
271 }
272 for key, value := range targetLabels {
273 key = strings.TrimSpace(key)
274 value = strings.TrimSpace(value)
275 if key == "" || value == "" || isPairLabelKey(key) {
276 continue
277 }
278 metrics["dst_"+key] = value
279 }
280
281 if len(metrics) == 0 {
282 return nil
283 }
284 return metrics
285 }
286
287 func mergeEndpointIPHints(base, extra LinkEndpoint) LinkEndpoint {
288 if len(extra.Match.IPAddresses) == 0 {
289 return base
290 }
291 base.Match.IPAddresses = uniqueTopologyStrings(append(base.Match.IPAddresses, extra.Match.IPAddresses...))
292 return base
293 }
294
295 func isPairLabelKey(key string) bool {
296 return key == adjacencyLabelPairID || key == adjacencyLabelPairPass
297 }
298
299 func incrementProjectedProtocolCounters(out *projectedLinks, protocol string, bidirectional bool) {
300 if out == nil {
301 return
302 }
303 switch protocol {
304 case "lldp":
305 out.lldp++
306 case "cdp":
307 out.cdp++
308 }
309 if bidirectional {
310 out.bidirectionalCount++
311 return
312 }
313 out.unidirectionalCount++
314 }