| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package l2topology |
| 4 | |
| 5 | import ( |
| 6 | "sort" |
| 7 | "strings" |
| 8 | ) |
| 9 | |
| 10 | func (b *segmentProjectionBuilder) emitLinks() { |
| 11 | sort.Strings(b.segmentIDs) |
| 12 | probableOnlyAnchorPortIDBySegment := b.buildProbableOnlyAnchorPortIDBySegment() |
| 13 | segmentsWithAnyLinks := make(map[string]struct{}) |
| 14 | |
| 15 | for _, segmentID := range b.segmentIDs { |
| 16 | segment := b.segmentByID[segmentID] |
| 17 | if segment == nil { |
| 18 | continue |
| 19 | } |
| 20 | segmentEndpoint := LinkEndpoint{ |
| 21 | Match: b.segmentMatchByID[segmentID], |
| 22 | Attributes: map[string]any{ |
| 23 | "segment_id": segmentID, |
| 24 | }, |
| 25 | } |
| 26 | |
| 27 | portIDs := make([]string, 0, len(segment.ports)) |
| 28 | for portID := range segment.ports { |
| 29 | portIDs = append(portIDs, portID) |
| 30 | } |
| 31 | sort.Strings(portIDs) |
| 32 | probableOnlyAnchorPortID := probableOnlyAnchorPortIDBySegment[segmentID] |
| 33 | for _, portID := range portIDs { |
| 34 | if probableOnlyAnchorPortID != "" && portID != probableOnlyAnchorPortID { |
| 35 | continue |
| 36 | } |
| 37 | port := segment.ports[portID] |
| 38 | device, ok := b.deviceByID[port.deviceID] |
| 39 | if !ok { |
| 40 | continue |
| 41 | } |
| 42 | localPort := bridgePortDisplay(port) |
| 43 | if localPort == "" { |
| 44 | continue |
| 45 | } |
| 46 | edgeKey := segmentID + keySep + portID |
| 47 | if _, seen := b.deviceSegmentEdgeSeen[edgeKey]; seen { |
| 48 | continue |
| 49 | } |
| 50 | b.deviceSegmentEdgeSeen[edgeKey] = struct{}{} |
| 51 | |
| 52 | metrics := map[string]any{ |
| 53 | "bridge_domain": segmentID, |
| 54 | } |
| 55 | if segment.portIdentityKey(port) == segment.portIdentityKey(segment.designatedPort) { |
| 56 | metrics["designated"] = true |
| 57 | } |
| 58 | b.out.links = append(b.out.links, Link{ |
| 59 | Layer: b.layer, |
| 60 | Protocol: "bridge", |
| 61 | LinkType: "bridge", |
| 62 | Direction: "bidirectional", |
| 63 | Src: adjacencySideToEndpoint(device, localPort, b.ifIndexByDeviceName, b.ifaceByDeviceIndex), |
| 64 | Dst: segmentEndpoint, |
| 65 | DiscoveredAt: topologyTimePtr(b.collectedAt), |
| 66 | LastSeen: topologyTimePtr(b.collectedAt), |
| 67 | Metrics: metrics, |
| 68 | }) |
| 69 | b.out.linksFdb++ |
| 70 | b.out.bidirectionalCount++ |
| 71 | segmentsWithAnyLinks[segmentID] = struct{}{} |
| 72 | } |
| 73 | |
| 74 | allowedEndpoints := b.allowedEndpointBySegment[segmentID] |
| 75 | if len(allowedEndpoints) == 0 { |
| 76 | continue |
| 77 | } |
| 78 | endpointSet := make(map[string]struct{}, len(segment.endpointIDs)+len(allowedEndpoints)) |
| 79 | for endpointID := range segment.endpointIDs { |
| 80 | endpointSet[endpointID] = struct{}{} |
| 81 | } |
| 82 | for endpointID := range allowedEndpoints { |
| 83 | endpointSet[endpointID] = struct{}{} |
| 84 | } |
| 85 | endpointIDs := sortedTopologySet(endpointSet) |
| 86 | for _, endpointID := range endpointIDs { |
| 87 | if _, ok := allowedEndpoints[endpointID]; !ok { |
| 88 | continue |
| 89 | } |
| 90 | |
| 91 | endpointMatch, ok := b.endpointMatchByID[endpointID] |
| 92 | if !ok { |
| 93 | endpointMatch = endpointMatchFromID(endpointID) |
| 94 | if len(topologyMatchIdentityKeys(endpointMatch)) == 0 { |
| 95 | continue |
| 96 | } |
| 97 | } |
| 98 | overlappingDeviceIDs := endpointMatchOverlappingKnownDeviceIDs(endpointMatch, b.deviceIdentityByID) |
| 99 | if len(overlappingDeviceIDs) > 0 { |
| 100 | matchedManagedDeviceIDs := make([]string, 0, len(overlappingDeviceIDs)) |
| 101 | for _, overlapID := range overlappingDeviceIDs { |
| 102 | if _, ok := b.deviceByID[overlapID]; ok { |
| 103 | matchedManagedDeviceIDs = append(matchedManagedDeviceIDs, overlapID) |
| 104 | } |
| 105 | } |
| 106 | if len(matchedManagedDeviceIDs) > 0 { |
| 107 | if len(matchedManagedDeviceIDs) == 1 { |
| 108 | matchedDeviceID := matchedManagedDeviceIDs[0] |
| 109 | if segmentContainsDevice(segment, matchedDeviceID) { |
| 110 | if b.out.suppressedManagedOverlapIDs == nil { |
| 111 | b.out.suppressedManagedOverlapIDs = make(map[string]struct{}) |
| 112 | } |
| 113 | b.out.suppressedManagedOverlapIDs[normalizeFDBEndpointID(endpointID)] = struct{}{} |
| 114 | b.out.endpointLinksSuppressed++ |
| 115 | continue |
| 116 | } |
| 117 | if matchedDevice, ok := b.deviceByID[matchedDeviceID]; ok { |
| 118 | edgeKey := segmentID + "|managed-device|" + matchedDeviceID |
| 119 | if _, seen := b.endpointSegmentEdgeSeen[edgeKey]; !seen { |
| 120 | b.endpointSegmentEdgeSeen[edgeKey] = struct{}{} |
| 121 | b.out.links = append(b.out.links, Link{ |
| 122 | Layer: b.layer, |
| 123 | Protocol: "fdb", |
| 124 | LinkType: "fdb", |
| 125 | Direction: "bidirectional", |
| 126 | Src: segmentEndpoint, |
| 127 | Dst: adjacencySideToEndpoint(matchedDevice, "", b.ifIndexByDeviceName, b.ifaceByDeviceIndex), |
| 128 | DiscoveredAt: topologyTimePtr(b.collectedAt), |
| 129 | LastSeen: topologyTimePtr(b.collectedAt), |
| 130 | Metrics: map[string]any{ |
| 131 | "bridge_domain": segmentID, |
| 132 | "attachment_mode": "managed_device_overlap", |
| 133 | }, |
| 134 | }) |
| 135 | b.out.linksFdb++ |
| 136 | b.out.bidirectionalCount++ |
| 137 | b.out.endpointLinksEmitted++ |
| 138 | segmentsWithAnyLinks[segmentID] = struct{}{} |
| 139 | } |
| 140 | if b.out.suppressedManagedOverlapIDs == nil { |
| 141 | b.out.suppressedManagedOverlapIDs = make(map[string]struct{}) |
| 142 | } |
| 143 | b.out.suppressedManagedOverlapIDs[normalizeFDBEndpointID(endpointID)] = struct{}{} |
| 144 | continue |
| 145 | } |
| 146 | } |
| 147 | if b.out.suppressedManagedOverlapIDs == nil { |
| 148 | b.out.suppressedManagedOverlapIDs = make(map[string]struct{}) |
| 149 | } |
| 150 | b.out.suppressedManagedOverlapIDs[normalizeFDBEndpointID(endpointID)] = struct{}{} |
| 151 | b.out.endpointLinksSuppressed++ |
| 152 | continue |
| 153 | } |
| 154 | if !b.probabilisticConnectivity { |
| 155 | b.out.endpointLinksSuppressed++ |
| 156 | continue |
| 157 | } |
| 158 | b.allowEndpoint(segmentID, endpointID, true, "probable_segment") |
| 159 | } |
| 160 | |
| 161 | if owner, hasOwner := b.out.endpointDirectOwners[endpointID]; hasOwner && |
| 162 | strings.EqualFold(strings.TrimSpace(owner.source), "single_port_mac") { |
| 163 | device, ok := b.deviceByID[owner.port.deviceID] |
| 164 | if ok { |
| 165 | localPort := bridgePortDisplay(owner.port) |
| 166 | if localPort != "" { |
| 167 | edgeKey := "direct" + keySep + bridgePortObservationVLANKey(owner.port) + keySep + endpointID |
| 168 | if _, seen := b.endpointSegmentEdgeSeen[edgeKey]; !seen { |
| 169 | b.endpointSegmentEdgeSeen[edgeKey] = struct{}{} |
| 170 | metrics := map[string]any{ |
| 171 | "attachment_mode": "direct", |
| 172 | } |
| 173 | linkState := "" |
| 174 | if probableSet := b.probableEndpointBySegment[segmentID]; len(probableSet) > 0 { |
| 175 | if _, isProbable := probableSet[endpointID]; isProbable { |
| 176 | metrics["attachment_mode"] = "probable_direct" |
| 177 | metrics["inference"] = "probable" |
| 178 | metrics["confidence"] = "low" |
| 179 | linkState = "probable" |
| 180 | } |
| 181 | } |
| 182 | b.out.links = append(b.out.links, Link{ |
| 183 | Layer: b.layer, |
| 184 | Protocol: "fdb", |
| 185 | LinkType: "fdb", |
| 186 | Direction: "bidirectional", |
| 187 | Src: adjacencySideToEndpoint(device, localPort, b.ifIndexByDeviceName, b.ifaceByDeviceIndex), |
| 188 | Dst: LinkEndpoint{Match: endpointMatch}, |
| 189 | DiscoveredAt: topologyTimePtr(b.collectedAt), |
| 190 | LastSeen: topologyTimePtr(b.collectedAt), |
| 191 | State: linkState, |
| 192 | Metrics: metrics, |
| 193 | }) |
| 194 | b.out.linksFdb++ |
| 195 | b.out.bidirectionalCount++ |
| 196 | b.out.endpointLinksEmitted++ |
| 197 | continue |
| 198 | } |
| 199 | } |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | edgeKey := segmentID + keySep + endpointID |
| 204 | if _, seen := b.endpointSegmentEdgeSeen[edgeKey]; seen { |
| 205 | continue |
| 206 | } |
| 207 | b.endpointSegmentEdgeSeen[edgeKey] = struct{}{} |
| 208 | |
| 209 | metrics := map[string]any{ |
| 210 | "bridge_domain": segmentID, |
| 211 | } |
| 212 | linkState := "" |
| 213 | if probableSet := b.probableEndpointBySegment[segmentID]; len(probableSet) > 0 { |
| 214 | if _, isProbable := probableSet[endpointID]; isProbable { |
| 215 | probableMode := "" |
| 216 | if modes := b.probableAttachmentModes[segmentID]; len(modes) > 0 { |
| 217 | probableMode = strings.TrimSpace(modes[endpointID]) |
| 218 | } |
| 219 | if probableMode == "" { |
| 220 | probableMode = "probable_segment" |
| 221 | } |
| 222 | metrics["attachment_mode"] = probableMode |
| 223 | metrics["inference"] = "probable" |
| 224 | metrics["confidence"] = "low" |
| 225 | linkState = "probable" |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | b.out.links = append(b.out.links, Link{ |
| 230 | Layer: b.layer, |
| 231 | Protocol: "fdb", |
| 232 | LinkType: "fdb", |
| 233 | Direction: "bidirectional", |
| 234 | Src: segmentEndpoint, |
| 235 | Dst: LinkEndpoint{Match: endpointMatch}, |
| 236 | DiscoveredAt: topologyTimePtr(b.collectedAt), |
| 237 | LastSeen: topologyTimePtr(b.collectedAt), |
| 238 | State: linkState, |
| 239 | Metrics: metrics, |
| 240 | }) |
| 241 | b.out.linksFdb++ |
| 242 | b.out.bidirectionalCount++ |
| 243 | b.out.endpointLinksEmitted++ |
| 244 | segmentsWithAnyLinks[segmentID] = struct{}{} |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | b.pruneSegmentsWithoutLinks(segmentsWithAnyLinks) |
| 249 | } |
| 250 | |
| 251 | func (b *segmentProjectionBuilder) pruneSegmentsWithoutLinks(segmentsWithAnyLinks map[string]struct{}) { |
| 252 | if len(segmentsWithAnyLinks) >= len(b.segmentIDs) { |
| 253 | return |
| 254 | } |
| 255 | |
| 256 | filteredActors := make([]Actor, 0, len(b.out.actors)) |
| 257 | for _, actor := range b.out.actors { |
| 258 | segmentID := topologyAttrString(actor.Attributes, "segment_id") |
| 259 | if segmentID == "" { |
| 260 | continue |
| 261 | } |
| 262 | if _, ok := segmentsWithAnyLinks[segmentID]; ok { |
| 263 | filteredActors = append(filteredActors, actor) |
| 264 | } |
| 265 | } |
| 266 | b.out.actors = filteredActors |
| 267 | |
| 268 | filteredLinks := make([]Link, 0, len(b.out.links)) |
| 269 | b.out.linksFdb = 0 |
| 270 | b.out.bidirectionalCount = 0 |
| 271 | b.out.endpointLinksEmitted = 0 |
| 272 | for _, link := range b.out.links { |
| 273 | segmentID := topologyMetricString(link.Metrics, "bridge_domain") |
| 274 | if segmentID != "" { |
| 275 | if _, ok := segmentsWithAnyLinks[segmentID]; !ok { |
| 276 | continue |
| 277 | } |
| 278 | } |
| 279 | filteredLinks = append(filteredLinks, link) |
| 280 | b.out.linksFdb++ |
| 281 | if strings.EqualFold(strings.TrimSpace(link.Direction), "bidirectional") { |
| 282 | b.out.bidirectionalCount++ |
| 283 | } |
| 284 | if strings.EqualFold(strings.TrimSpace(link.Protocol), "fdb") { |
| 285 | b.out.endpointLinksEmitted++ |
| 286 | } |
| 287 | } |
| 288 | b.out.links = filteredLinks |
| 289 | } |