| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package l2topology |
| 4 | |
| 5 | import ( |
| 6 | "strings" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | type graphBuilder struct { |
| 11 | result Result |
| 12 | opts GraphOptions |
| 13 | |
| 14 | schemaVersion string |
| 15 | source string |
| 16 | layer string |
| 17 | view string |
| 18 | collectedAt time.Time |
| 19 | |
| 20 | strategyConfig topologyInferenceStrategyConfig |
| 21 | |
| 22 | deviceByID map[string]Device |
| 23 | ifaceByDeviceIndex map[string]Interface |
| 24 | ifIndexByDeviceName map[string]int |
| 25 | bridgeLinks []bridgeBridgeLinkRecord |
| 26 | reporterAliases map[string][]string |
| 27 | ifaceSummaryByDevice map[string]topologyDeviceInterfaceSummary |
| 28 | |
| 29 | actors []Actor |
| 30 | actorIndex map[string]struct{} |
| 31 | actorMACIndex map[string]struct{} |
| 32 | |
| 33 | projectedAdjacencies projectedLinks |
| 34 | endpointActors builtEndpointActors |
| 35 | segmentProjection projectedSegments |
| 36 | |
| 37 | links []Link |
| 38 | segmentSuppressed int |
| 39 | unlinkedSuppressed int |
| 40 | linkCounts topologyLinkCounts |
| 41 | probableLinks int |
| 42 | stats map[string]any |
| 43 | } |
| 44 | |
| 45 | func newGraphBuilder(result Result, opts GraphOptions) *graphBuilder { |
| 46 | builder := &graphBuilder{ |
| 47 | result: result, |
| 48 | opts: opts, |
| 49 | } |
| 50 | |
| 51 | builder.schemaVersion = strings.TrimSpace(opts.SchemaVersion) |
| 52 | if builder.schemaVersion == "" { |
| 53 | builder.schemaVersion = "2.0" |
| 54 | } |
| 55 | |
| 56 | builder.source = strings.TrimSpace(opts.Source) |
| 57 | if builder.source == "" { |
| 58 | builder.source = "snmp" |
| 59 | } |
| 60 | |
| 61 | builder.layer = strings.TrimSpace(opts.Layer) |
| 62 | if builder.layer == "" { |
| 63 | builder.layer = "2" |
| 64 | } |
| 65 | |
| 66 | builder.view = strings.TrimSpace(opts.View) |
| 67 | if builder.view == "" { |
| 68 | builder.view = "summary" |
| 69 | } |
| 70 | |
| 71 | builder.collectedAt = opts.CollectedAt |
| 72 | if builder.collectedAt.IsZero() { |
| 73 | builder.collectedAt = result.CollectedAt |
| 74 | } |
| 75 | if builder.collectedAt.IsZero() { |
| 76 | builder.collectedAt = time.Now().UTC() |
| 77 | } |
| 78 | |
| 79 | builder.strategyConfig = topologyInferenceStrategyConfigFor(opts.InferenceStrategy) |
| 80 | return builder |
| 81 | } |
| 82 | |
| 83 | func (b *graphBuilder) prepareIndexes() { |
| 84 | b.deviceByID = make(map[string]Device, len(b.result.Devices)) |
| 85 | b.ifaceByDeviceIndex = make(map[string]Interface, len(b.result.Interfaces)) |
| 86 | b.ifIndexByDeviceName = make(map[string]int, len(b.result.Interfaces)) |
| 87 | |
| 88 | for _, dev := range b.result.Devices { |
| 89 | b.deviceByID[dev.ID] = dev |
| 90 | } |
| 91 | |
| 92 | for _, iface := range b.result.Interfaces { |
| 93 | if iface.IfIndex <= 0 { |
| 94 | continue |
| 95 | } |
| 96 | b.ifaceByDeviceIndex[deviceIfIndexKey(iface.DeviceID, iface.IfIndex)] = iface |
| 97 | for _, alias := range interfaceNameLookupAliases(iface.IfName, iface.IfDescr) { |
| 98 | b.ifIndexByDeviceName[deviceIfNameKey(iface.DeviceID, alias)] = iface.IfIndex |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | func (b *graphBuilder) collectBridgeTopologyInputs() { |
| 104 | b.bridgeLinks = collectBridgeLinkRecords(b.result.Adjacencies, b.ifIndexByDeviceName, b.strategyConfig) |
| 105 | b.reporterAliases = buildFDBReporterAliases(b.deviceByID, b.ifaceByDeviceIndex) |
| 106 | if b.strategyConfig.enableFDBPairwiseLinks { |
| 107 | b.bridgeLinks = mergeBridgeLinkRecordSets( |
| 108 | b.bridgeLinks, |
| 109 | inferFDBPairwiseBridgeLinks(b.result.Attachments, b.ifaceByDeviceIndex, b.reporterAliases), |
| 110 | ) |
| 111 | } |
| 112 | |
| 113 | deterministicTransitPortKeys := buildDeterministicTransitPortKeySet(b.result.Adjacencies, b.ifIndexByDeviceName) |
| 114 | discoveryDevicePairs := buildDeterministicDiscoveryDevicePairSet(b.result.Adjacencies) |
| 115 | b.bridgeLinks = suppressInferredBridgeLinksOnDeterministicDiscovery( |
| 116 | b.bridgeLinks, |
| 117 | deterministicTransitPortKeys, |
| 118 | discoveryDevicePairs, |
| 119 | ) |
| 120 | |
| 121 | b.ifaceSummaryByDevice = buildTopologyDeviceInterfaceSummaries( |
| 122 | b.result.Interfaces, |
| 123 | b.result.Attachments, |
| 124 | b.result.Adjacencies, |
| 125 | b.deviceByID, |
| 126 | b.ifIndexByDeviceName, |
| 127 | b.bridgeLinks, |
| 128 | b.reporterAliases, |
| 129 | ) |
| 130 | } |
| 131 | |
| 132 | func (b *graphBuilder) buildDeviceActors() { |
| 133 | b.actors = make([]Actor, 0, len(b.result.Devices)) |
| 134 | b.actorIndex = make(map[string]struct{}, len(b.result.Devices)*2) |
| 135 | b.actorMACIndex = make(map[string]struct{}, len(b.result.Devices)) |
| 136 | |
| 137 | for _, dev := range b.result.Devices { |
| 138 | actor := deviceToTopologyActor( |
| 139 | dev, |
| 140 | b.source, |
| 141 | b.layer, |
| 142 | b.opts.LocalDeviceID, |
| 143 | b.ifaceSummaryByDevice[dev.ID], |
| 144 | b.reporterAliases[dev.ID], |
| 145 | ) |
| 146 | keys := topologyMatchIdentityKeys(actor.Match) |
| 147 | if len(keys) == 0 { |
| 148 | continue |
| 149 | } |
| 150 | macKeys := topologyMatchHardwareIdentityKeys(actor.Match) |
| 151 | if len(macKeys) > 0 { |
| 152 | if topologyIdentityIndexOverlaps(b.actorMACIndex, macKeys) { |
| 153 | continue |
| 154 | } |
| 155 | addTopologyIdentityKeys(b.actorMACIndex, macKeys) |
| 156 | } else if topologyIdentityIndexOverlaps(b.actorIndex, keys) { |
| 157 | continue |
| 158 | } |
| 159 | addTopologyIdentityKeys(b.actorIndex, keys) |
| 160 | b.actors = append(b.actors, actor) |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | func (b *graphBuilder) projectAdjacencyTopology() { |
| 165 | b.projectedAdjacencies = projectAdjacencyLinks( |
| 166 | b.result.Adjacencies, |
| 167 | b.layer, |
| 168 | b.collectedAt, |
| 169 | b.deviceByID, |
| 170 | b.ifIndexByDeviceName, |
| 171 | b.ifaceByDeviceIndex, |
| 172 | ) |
| 173 | } |
| 174 | |
| 175 | func (b *graphBuilder) buildEndpointTopology() { |
| 176 | b.endpointActors = buildEndpointActors( |
| 177 | b.result.Attachments, |
| 178 | b.result.Enrichments, |
| 179 | b.ifaceByDeviceIndex, |
| 180 | b.source, |
| 181 | b.layer, |
| 182 | b.actorIndex, |
| 183 | b.actorMACIndex, |
| 184 | ) |
| 185 | b.actors = append(b.actors, b.endpointActors.actors...) |
| 186 | } |
| 187 | |
| 188 | func (b *graphBuilder) buildSegmentTopology() { |
| 189 | b.segmentProjection = projectSegmentTopology( |
| 190 | b.result.Attachments, |
| 191 | b.result.Adjacencies, |
| 192 | b.layer, |
| 193 | b.source, |
| 194 | b.collectedAt, |
| 195 | b.deviceByID, |
| 196 | b.ifaceByDeviceIndex, |
| 197 | b.ifIndexByDeviceName, |
| 198 | b.bridgeLinks, |
| 199 | b.reporterAliases, |
| 200 | b.endpointActors.matchByEndpointID, |
| 201 | b.endpointActors.labelsByEndpointID, |
| 202 | b.actorIndex, |
| 203 | b.opts.ProbabilisticConnectivity, |
| 204 | b.strategyConfig, |
| 205 | ) |
| 206 | annotateEndpointActorsWithDirectOwners( |
| 207 | b.actors, |
| 208 | b.endpointActors.matchByEndpointID, |
| 209 | b.segmentProjection.endpointDirectOwners, |
| 210 | b.deviceByID, |
| 211 | ) |
| 212 | b.actors = append(b.actors, b.segmentProjection.actors...) |
| 213 | } |
| 214 | |
| 215 | func (b *graphBuilder) finalizeGraph() { |
| 216 | sortTopologyActors(b.actors) |
| 217 | |
| 218 | b.links = make([]Link, 0, len(b.projectedAdjacencies.links)+len(b.segmentProjection.links)) |
| 219 | b.links = append(b.links, b.projectedAdjacencies.links...) |
| 220 | b.links = append(b.links, b.segmentProjection.links...) |
| 221 | sortTopologyLinks(b.links) |
| 222 | |
| 223 | b.actors, b.links, b.segmentSuppressed = pruneSegmentArtifacts(b.actors, b.links) |
| 224 | if b.opts.CollapseActorsByIP { |
| 225 | b.actors = collapseActorsByIP(b.actors) |
| 226 | } |
| 227 | if b.opts.EliminateNonIPInferred { |
| 228 | b.actors, b.links = eliminateNonIPInferredActors(b.actors, b.links) |
| 229 | } |
| 230 | if b.opts.CollapseActorsByIP { |
| 231 | b.actors, b.unlinkedSuppressed = pruneManagedOverlapUnlinkedEndpointActors( |
| 232 | b.actors, |
| 233 | b.links, |
| 234 | b.segmentProjection.suppressedManagedOverlapIDs, |
| 235 | ) |
| 236 | } |
| 237 | var additionalSegmentSuppressed int |
| 238 | b.actors, b.links, additionalSegmentSuppressed = pruneSegmentArtifacts(b.actors, b.links) |
| 239 | b.segmentSuppressed += additionalSegmentSuppressed |
| 240 | sortTopologyActors(b.actors) |
| 241 | sortTopologyLinks(b.links) |
| 242 | applyTopologyDisplayNames(b.actors, b.links, b.opts.ResolveDNSName) |
| 243 | assignTopologyActorIDsAndLinkEndpoints(b.actors, b.links) |
| 244 | enrichTopologyPortTablesWithLinkCounts(b.actors, b.links) |
| 245 | |
| 246 | b.linkCounts = summarizeTopologyLinks(b.links) |
| 247 | b.probableLinks = 0 |
| 248 | for _, link := range b.links { |
| 249 | if strings.EqualFold(strings.TrimSpace(link.State), "probable") { |
| 250 | b.probableLinks++ |
| 251 | continue |
| 252 | } |
| 253 | if strings.EqualFold(topologyMetricString(link.Metrics, "inference"), "probable") { |
| 254 | b.probableLinks++ |
| 255 | } |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | func (b *graphBuilder) buildStats() { |
| 260 | b.stats = cloneAnyMap(b.result.Stats) |
| 261 | if b.stats == nil { |
| 262 | b.stats = make(map[string]any) |
| 263 | } |
| 264 | |
| 265 | b.stats["devices_total"] = len(b.result.Devices) |
| 266 | b.stats["devices_discovered"] = discoveredDeviceCount(b.result.Devices, b.opts.LocalDeviceID) |
| 267 | b.stats["links_total"] = len(b.links) |
| 268 | b.stats["links_lldp"] = b.linkCounts.lldp |
| 269 | b.stats["links_cdp"] = b.linkCounts.cdp |
| 270 | b.stats["links_bidirectional"] = b.linkCounts.bidirectional |
| 271 | b.stats["links_unidirectional"] = b.linkCounts.unidirectional |
| 272 | b.stats["links_fdb"] = b.linkCounts.fdb |
| 273 | b.stats["links_fdb_endpoint_candidates"] = b.segmentProjection.endpointLinksCandidates |
| 274 | b.stats["links_fdb_endpoint_emitted"] = b.segmentProjection.endpointLinksEmitted |
| 275 | b.stats["links_fdb_endpoint_suppressed"] = b.segmentProjection.endpointLinksSuppressed |
| 276 | b.stats["endpoints_ambiguous_segments"] = b.segmentProjection.endpointsWithAmbiguousSegment |
| 277 | b.stats["links_arp"] = b.linkCounts.arp |
| 278 | b.stats["links_probable"] = b.probableLinks |
| 279 | b.stats["segments_suppressed"] = b.segmentSuppressed |
| 280 | b.stats["actors_total"] = len(b.actors) |
| 281 | b.stats["actors_unlinked_suppressed"] = b.unlinkedSuppressed |
| 282 | b.stats["endpoints_total"] = b.endpointActors.count |
| 283 | b.stats["inference_strategy"] = b.strategyConfig.id |
| 284 | } |
| 285 | |
| 286 | func (b *graphBuilder) graph() Graph { |
| 287 | return Graph{ |
| 288 | SchemaVersion: b.schemaVersion, |
| 289 | Source: b.source, |
| 290 | Layer: b.layer, |
| 291 | AgentID: b.opts.AgentID, |
| 292 | CollectedAt: b.collectedAt, |
| 293 | View: b.view, |
| 294 | Actors: b.actors, |
| 295 | Links: b.links, |
| 296 | Stats: b.stats, |
| 297 | } |
| 298 | } |