| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package l2topology |
| 4 | |
| 5 | import ( |
| 6 | "sort" |
| 7 | "strings" |
| 8 | ) |
| 9 | |
| 10 | func pruneSegmentArtifacts(actors []Actor, links []Link) ([]Actor, []Link, int) { |
| 11 | if len(actors) == 0 || len(links) == 0 { |
| 12 | return actors, links, 0 |
| 13 | } |
| 14 | |
| 15 | segmentKeys := make(map[string]struct{}) |
| 16 | segmentOrder := make([]string, 0) |
| 17 | for _, actor := range actors { |
| 18 | if !strings.EqualFold(strings.TrimSpace(actor.ActorType), "segment") { |
| 19 | continue |
| 20 | } |
| 21 | key := canonicalTopologyMatchKey(actor.Match) |
| 22 | if key == "" { |
| 23 | continue |
| 24 | } |
| 25 | if _, seen := segmentKeys[key]; seen { |
| 26 | continue |
| 27 | } |
| 28 | segmentKeys[key] = struct{}{} |
| 29 | segmentOrder = append(segmentOrder, key) |
| 30 | } |
| 31 | if len(segmentKeys) == 0 { |
| 32 | return actors, links, 0 |
| 33 | } |
| 34 | sort.Strings(segmentOrder) |
| 35 | |
| 36 | discoveryPairs := make(map[string]struct{}) |
| 37 | for _, link := range links { |
| 38 | protocol := strings.ToLower(strings.TrimSpace(link.Protocol)) |
| 39 | if protocol != "lldp" && protocol != "cdp" { |
| 40 | continue |
| 41 | } |
| 42 | src := canonicalTopologyMatchKey(link.Src.Match) |
| 43 | dst := canonicalTopologyMatchKey(link.Dst.Match) |
| 44 | if src == "" || dst == "" { |
| 45 | continue |
| 46 | } |
| 47 | if _, srcSegment := segmentKeys[src]; srcSegment { |
| 48 | continue |
| 49 | } |
| 50 | if _, dstSegment := segmentKeys[dst]; dstSegment { |
| 51 | continue |
| 52 | } |
| 53 | if pair := topologyUndirectedPairKey(src, dst); pair != "" { |
| 54 | discoveryPairs[pair] = struct{}{} |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | suppressed := make(map[string]struct{}) |
| 59 | for { |
| 60 | changed := false |
| 61 | neighborsBySegment := make(map[string]map[string]struct{}) |
| 62 | for _, link := range links { |
| 63 | src := canonicalTopologyMatchKey(link.Src.Match) |
| 64 | dst := canonicalTopologyMatchKey(link.Dst.Match) |
| 65 | if src == "" || dst == "" { |
| 66 | continue |
| 67 | } |
| 68 | if _, srcSuppressed := suppressed[src]; srcSuppressed { |
| 69 | continue |
| 70 | } |
| 71 | if _, dstSuppressed := suppressed[dst]; dstSuppressed { |
| 72 | continue |
| 73 | } |
| 74 | |
| 75 | _, srcSegment := segmentKeys[src] |
| 76 | _, dstSegment := segmentKeys[dst] |
| 77 | |
| 78 | if srcSegment && !dstSegment { |
| 79 | neighbors := neighborsBySegment[src] |
| 80 | if neighbors == nil { |
| 81 | neighbors = make(map[string]struct{}) |
| 82 | neighborsBySegment[src] = neighbors |
| 83 | } |
| 84 | neighbors[dst] = struct{}{} |
| 85 | } |
| 86 | if dstSegment && !srcSegment { |
| 87 | neighbors := neighborsBySegment[dst] |
| 88 | if neighbors == nil { |
| 89 | neighbors = make(map[string]struct{}) |
| 90 | neighborsBySegment[dst] = neighbors |
| 91 | } |
| 92 | neighbors[src] = struct{}{} |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | for _, segmentKey := range segmentOrder { |
| 97 | if _, alreadySuppressed := suppressed[segmentKey]; alreadySuppressed { |
| 98 | continue |
| 99 | } |
| 100 | |
| 101 | neighbors := neighborsBySegment[segmentKey] |
| 102 | if len(neighbors) < 2 { |
| 103 | suppressed[segmentKey] = struct{}{} |
| 104 | changed = true |
| 105 | continue |
| 106 | } |
| 107 | |
| 108 | if len(neighbors) == 2 { |
| 109 | pairValues := make([]string, 0, 2) |
| 110 | for neighbor := range neighbors { |
| 111 | pairValues = append(pairValues, neighbor) |
| 112 | } |
| 113 | if len(pairValues) == 2 { |
| 114 | if pair := topologyUndirectedPairKey(pairValues[0], pairValues[1]); pair != "" { |
| 115 | if _, found := discoveryPairs[pair]; found { |
| 116 | suppressed[segmentKey] = struct{}{} |
| 117 | changed = true |
| 118 | } |
| 119 | } |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | if !changed { |
| 125 | break |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | if len(suppressed) == 0 { |
| 130 | return actors, links, 0 |
| 131 | } |
| 132 | |
| 133 | filteredActors := make([]Actor, 0, len(actors)) |
| 134 | for _, actor := range actors { |
| 135 | key := canonicalTopologyMatchKey(actor.Match) |
| 136 | if key == "" { |
| 137 | filteredActors = append(filteredActors, actor) |
| 138 | continue |
| 139 | } |
| 140 | if _, isSuppressed := suppressed[key]; isSuppressed && strings.EqualFold(strings.TrimSpace(actor.ActorType), "segment") { |
| 141 | continue |
| 142 | } |
| 143 | filteredActors = append(filteredActors, actor) |
| 144 | } |
| 145 | |
| 146 | filteredLinks := make([]Link, 0, len(links)) |
| 147 | for _, link := range links { |
| 148 | src := canonicalTopologyMatchKey(link.Src.Match) |
| 149 | dst := canonicalTopologyMatchKey(link.Dst.Match) |
| 150 | if src != "" { |
| 151 | if _, srcSuppressed := suppressed[src]; srcSuppressed { |
| 152 | continue |
| 153 | } |
| 154 | } |
| 155 | if dst != "" { |
| 156 | if _, dstSuppressed := suppressed[dst]; dstSuppressed { |
| 157 | continue |
| 158 | } |
| 159 | } |
| 160 | filteredLinks = append(filteredLinks, link) |
| 161 | } |
| 162 | |
| 163 | return filteredActors, filteredLinks, len(suppressed) |
| 164 | } |
| 165 | |
| 166 | func topologyUndirectedPairKey(left, right string) string { |
| 167 | left = strings.TrimSpace(left) |
| 168 | right = strings.TrimSpace(right) |
| 169 | if left == "" || right == "" { |
| 170 | return "" |
| 171 | } |
| 172 | if left <= right { |
| 173 | return left + keySep + right |
| 174 | } |
| 175 | return right + keySep + left |
| 176 | } |
| 177 | |
| 178 | type topologyLinkCounts struct { |
| 179 | lldp int |
| 180 | cdp int |
| 181 | fdb int |
| 182 | arp int |
| 183 | bidirectional int |
| 184 | unidirectional int |
| 185 | } |
| 186 | |
| 187 | func summarizeTopologyLinks(links []Link) topologyLinkCounts { |
| 188 | var counts topologyLinkCounts |
| 189 | for _, link := range links { |
| 190 | switch strings.ToLower(strings.TrimSpace(link.Protocol)) { |
| 191 | case "lldp": |
| 192 | counts.lldp++ |
| 193 | case "cdp": |
| 194 | counts.cdp++ |
| 195 | case "bridge", "fdb": |
| 196 | counts.fdb++ |
| 197 | case "arp": |
| 198 | counts.arp++ |
| 199 | } |
| 200 | |
| 201 | switch strings.ToLower(strings.TrimSpace(link.Direction)) { |
| 202 | case "bidirectional": |
| 203 | counts.bidirectional++ |
| 204 | case "unidirectional": |
| 205 | counts.unidirectional++ |
| 206 | } |
| 207 | } |
| 208 | return counts |
| 209 | } |
| 210 | |
| 211 | func pruneManagedOverlapUnlinkedEndpointActors( |
| 212 | actors []Actor, |
| 213 | links []Link, |
| 214 | suppressedEndpointIDs map[string]struct{}, |
| 215 | ) ([]Actor, int) { |
| 216 | if len(actors) == 0 || len(suppressedEndpointIDs) == 0 { |
| 217 | return actors, 0 |
| 218 | } |
| 219 | |
| 220 | suppressedIdentityKeys := make(map[string]struct{}) |
| 221 | for endpointID := range suppressedEndpointIDs { |
| 222 | endpointID = normalizeFDBEndpointID(endpointID) |
| 223 | if endpointID == "" { |
| 224 | continue |
| 225 | } |
| 226 | match := endpointMatchFromID(endpointID) |
| 227 | for _, key := range topologyMatchIdentityKeys(match) { |
| 228 | key = strings.TrimSpace(key) |
| 229 | if key == "" { |
| 230 | continue |
| 231 | } |
| 232 | suppressedIdentityKeys[key] = struct{}{} |
| 233 | } |
| 234 | } |
| 235 | if len(suppressedIdentityKeys) == 0 { |
| 236 | return actors, 0 |
| 237 | } |
| 238 | |
| 239 | linkedIdentityKeys := make(map[string]struct{}, len(links)*2) |
| 240 | for _, link := range links { |
| 241 | for _, key := range topologyMatchIdentityKeys(link.Src.Match) { |
| 242 | key = strings.TrimSpace(key) |
| 243 | if key == "" { |
| 244 | continue |
| 245 | } |
| 246 | linkedIdentityKeys[key] = struct{}{} |
| 247 | } |
| 248 | for _, key := range topologyMatchIdentityKeys(link.Dst.Match) { |
| 249 | key = strings.TrimSpace(key) |
| 250 | if key == "" { |
| 251 | continue |
| 252 | } |
| 253 | linkedIdentityKeys[key] = struct{}{} |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | filtered := make([]Actor, 0, len(actors)) |
| 258 | suppressedCount := 0 |
| 259 | for _, actor := range actors { |
| 260 | if !strings.EqualFold(strings.TrimSpace(actor.ActorType), "endpoint") { |
| 261 | filtered = append(filtered, actor) |
| 262 | continue |
| 263 | } |
| 264 | |
| 265 | actorKeys := topologyMatchIdentityKeys(actor.Match) |
| 266 | if !topologyIdentityKeysOverlap(actorKeys, suppressedIdentityKeys) { |
| 267 | filtered = append(filtered, actor) |
| 268 | continue |
| 269 | } |
| 270 | // Keep endpoint actors that still participate in at least one emitted link. |
| 271 | if topologyIdentityKeysOverlap(actorKeys, linkedIdentityKeys) { |
| 272 | filtered = append(filtered, actor) |
| 273 | continue |
| 274 | } |
| 275 | suppressedCount++ |
| 276 | } |
| 277 | return filtered, suppressedCount |
| 278 | } |