| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package l2topology |
| 4 | |
| 5 | import ( |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | type topologyDisplayNameResolver struct { |
| 10 | lookup func(ip string) string |
| 11 | cache map[string]string |
| 12 | } |
| 13 | |
| 14 | type topologyDisplayName struct { |
| 15 | name string |
| 16 | source string |
| 17 | } |
| 18 | |
| 19 | func applyTopologyDisplayNames(actors []Actor, links []Link, lookup func(ip string) string) { |
| 20 | resolver := topologyDisplayNameResolver{ |
| 21 | lookup: lookup, |
| 22 | cache: make(map[string]string), |
| 23 | } |
| 24 | |
| 25 | deviceDisplayByID := make(map[string]string, len(actors)) |
| 26 | displayByMatchKey := make(map[string]string, len(actors)) |
| 27 | |
| 28 | // First pass: materialize display names for non-segment actors so segment naming can reuse them. |
| 29 | for i := range actors { |
| 30 | if actors[i].ActorType == "segment" { |
| 31 | continue |
| 32 | } |
| 33 | display := topologyActorDisplayName(actors[i], nil, &resolver) |
| 34 | if display.name == "" { |
| 35 | display = topologyFallbackActorDisplayName(actors[i]) |
| 36 | } |
| 37 | topologySetActorDisplay(&actors[i], display) |
| 38 | if matchKey := canonicalTopologyMatchKey(actors[i].Match); matchKey != "" { |
| 39 | displayByMatchKey[matchKey] = display.name |
| 40 | } |
| 41 | if IsDeviceActorType(actors[i].ActorType) { |
| 42 | if deviceID := topologyActorDeviceID(actors[i]); deviceID != "" { |
| 43 | deviceDisplayByID[deviceID] = display.name |
| 44 | } |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | // Second pass: segment display names depend on finalized device display names. |
| 49 | for i := range actors { |
| 50 | if actors[i].ActorType != "segment" { |
| 51 | continue |
| 52 | } |
| 53 | display := topologyActorDisplayName(actors[i], deviceDisplayByID, &resolver) |
| 54 | if display.name == "" { |
| 55 | display = topologyFallbackActorDisplayName(actors[i]) |
| 56 | } |
| 57 | topologySetActorDisplay(&actors[i], display) |
| 58 | if matchKey := canonicalTopologyMatchKey(actors[i].Match); matchKey != "" { |
| 59 | displayByMatchKey[matchKey] = display.name |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | for i := range links { |
| 64 | src := topologyEndpointDisplayName(links[i].Src, displayByMatchKey, &resolver) |
| 65 | if src.name == "" { |
| 66 | src = topologyDisplayName{name: "[unset]", source: "fallback"} |
| 67 | } |
| 68 | srcPortName := topologySetEndpointDisplayAndCanonicalPortName(&links[i].Src, src) |
| 69 | |
| 70 | dst := topologyEndpointDisplayName(links[i].Dst, displayByMatchKey, &resolver) |
| 71 | if dst.name == "" { |
| 72 | dst = topologyDisplayName{name: "[unset]", source: "fallback"} |
| 73 | } |
| 74 | dstPortName := topologySetEndpointDisplayAndCanonicalPortName(&links[i].Dst, dst) |
| 75 | |
| 76 | linkName := topologyCanonicalLinkName(src.name, srcPortName, dst.name, dstPortName) |
| 77 | if links[i].Metrics == nil { |
| 78 | links[i].Metrics = make(map[string]any) |
| 79 | } |
| 80 | links[i].Metrics["display_name"] = linkName |
| 81 | links[i].Metrics["src_port_name"] = srcPortName |
| 82 | links[i].Metrics["dst_port_name"] = dstPortName |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | func topologySetActorDisplay(actor *Actor, display topologyDisplayName) { |
| 87 | if actor == nil { |
| 88 | return |
| 89 | } |
| 90 | labels := cloneStringMap(actor.Labels) |
| 91 | if labels == nil { |
| 92 | labels = make(map[string]string) |
| 93 | } |
| 94 | labels["display_name"] = display.name |
| 95 | if display.source != "" { |
| 96 | labels["display_source"] = display.source |
| 97 | } |
| 98 | actor.Labels = labels |
| 99 | |
| 100 | attrs := cloneAnyMap(actor.Attributes) |
| 101 | if attrs == nil { |
| 102 | attrs = make(map[string]any) |
| 103 | } |
| 104 | attrs["display_name"] = display.name |
| 105 | if display.source != "" { |
| 106 | attrs["display_source"] = display.source |
| 107 | } |
| 108 | actor.Attributes = pruneTopologyAttributes(attrs) |
| 109 | } |
| 110 | |
| 111 | func topologySetEndpointDisplayAndCanonicalPortName(endpoint *LinkEndpoint, display topologyDisplayName) string { |
| 112 | if endpoint == nil { |
| 113 | return "" |
| 114 | } |
| 115 | attrs := cloneAnyMap(endpoint.Attributes) |
| 116 | if attrs == nil { |
| 117 | attrs = make(map[string]any) |
| 118 | } |
| 119 | attrs["display_name"] = display.name |
| 120 | if display.source != "" { |
| 121 | attrs["display_source"] = display.source |
| 122 | } |
| 123 | name := topologyCanonicalPortName(attrs) |
| 124 | attrs["port_name"] = name |
| 125 | endpoint.Attributes = pruneTopologyAttributes(attrs) |
| 126 | return name |
| 127 | } |
| 128 | |
| 129 | func topologyEndpointDisplayName(endpoint LinkEndpoint, actorDisplayByMatch map[string]string, resolver *topologyDisplayNameResolver) topologyDisplayName { |
| 130 | if key := canonicalTopologyMatchKey(endpoint.Match); key != "" { |
| 131 | if name := strings.TrimSpace(actorDisplayByMatch[key]); name != "" { |
| 132 | return topologyDisplayName{name: name, source: "actor"} |
| 133 | } |
| 134 | } |
| 135 | return topologyDisplayNameFromMatch(endpoint.Match, resolver) |
| 136 | } |
| 137 | |
| 138 | func topologyActorDisplayName(actor Actor, deviceDisplayByID map[string]string, resolver *topologyDisplayNameResolver) topologyDisplayName { |
| 139 | if actor.ActorType == "segment" { |
| 140 | if name := topologySegmentDisplayName(actor, deviceDisplayByID); name != "" { |
| 141 | return topologyDisplayName{name: name, source: "segment"} |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | display := topologyDisplayNameFromMatch(actor.Match, resolver) |
| 146 | if display.name != "" { |
| 147 | return display |
| 148 | } |
| 149 | |
| 150 | if segmentID := topologyAttrString(actor.Attributes, "segment_id"); segmentID != "" { |
| 151 | return topologyDisplayName{name: topologyCompactSegmentID(segmentID), source: "segment_id"} |
| 152 | } |
| 153 | return topologyDisplayName{} |
| 154 | } |
| 155 | |
| 156 | func topologyFallbackActorDisplayName(actor Actor) topologyDisplayName { |
| 157 | if matchKey := canonicalTopologyMatchKey(actor.Match); matchKey != "" { |
| 158 | return topologyDisplayName{name: matchKey, source: "fallback_match"} |
| 159 | } |
| 160 | if segmentID := topologyAttrString(actor.Attributes, "segment_id"); segmentID != "" { |
| 161 | return topologyDisplayName{name: topologyCompactSegmentID(segmentID), source: "segment_id"} |
| 162 | } |
| 163 | actorType := strings.TrimSpace(actor.ActorType) |
| 164 | if actorType == "" { |
| 165 | actorType = "actor" |
| 166 | } |
| 167 | return topologyDisplayName{name: actorType + ":[unset]", source: "fallback"} |
| 168 | } |
| 169 | |
| 170 | func topologyActorDeviceID(actor Actor) string { |
| 171 | return topologyAttrString(actor.Attributes, "device_id") |
| 172 | } |
| 173 | |
| 174 | func topologyDisplayNameFromMatch(match Match, resolver *topologyDisplayNameResolver) topologyDisplayName { |
| 175 | if dns := topologyMatchPreferredDNSName(match, resolver); dns != "" { |
| 176 | return topologyDisplayName{name: dns, source: "dns"} |
| 177 | } |
| 178 | if sysName := topologyMatchPreferredSysName(match); sysName != "" { |
| 179 | return topologyDisplayName{name: sysName, source: "sys_name"} |
| 180 | } |
| 181 | if hostname := topologyMatchPreferredHostname(match); hostname != "" { |
| 182 | return topologyDisplayName{name: hostname, source: "hostname"} |
| 183 | } |
| 184 | if ip := topologyMatchPreferredIP(match); ip != "" { |
| 185 | return topologyDisplayName{name: ip, source: "ip"} |
| 186 | } |
| 187 | if mac := topologyMatchPreferredMAC(match); mac != "" { |
| 188 | return topologyDisplayName{name: mac, source: "mac"} |
| 189 | } |
| 190 | return topologyDisplayName{} |
| 191 | } |
| 192 | |
| 193 | func topologyMatchPreferredDNSName(match Match, resolver *topologyDisplayNameResolver) string { |
| 194 | candidates := make(map[string]struct{}) |
| 195 | for _, value := range match.DNSNames { |
| 196 | if normalized := normalizeDNSName(value); normalized != "" { |
| 197 | candidates[normalized] = struct{}{} |
| 198 | } |
| 199 | } |
| 200 | for _, value := range match.IPAddresses { |
| 201 | if resolver == nil { |
| 202 | continue |
| 203 | } |
| 204 | if ip := normalizeTopologyIP(value); ip != "" { |
| 205 | if resolved := resolver.resolve(ip); resolved != "" { |
| 206 | candidates[resolved] = struct{}{} |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | names := sortedTopologySet(candidates) |
| 211 | if len(names) == 0 { |
| 212 | return "" |
| 213 | } |
| 214 | return names[0] |
| 215 | } |
| 216 | |
| 217 | func topologyMatchPreferredSysName(match Match) string { |
| 218 | return strings.TrimSpace(match.SysName) |
| 219 | } |
| 220 | |
| 221 | func topologyMatchPreferredHostname(match Match) string { |
| 222 | hostnames := uniqueTopologyStrings(match.Hostnames) |
| 223 | if len(hostnames) == 0 { |
| 224 | return "" |
| 225 | } |
| 226 | return hostnames[0] |
| 227 | } |
| 228 | |
| 229 | func topologyMatchPreferredIP(match Match) string { |
| 230 | ips := make([]string, 0, len(match.IPAddresses)) |
| 231 | for _, value := range match.IPAddresses { |
| 232 | if ip := normalizeTopologyIP(value); ip != "" { |
| 233 | ips = append(ips, ip) |
| 234 | } |
| 235 | } |
| 236 | ips = uniqueTopologyStrings(ips) |
| 237 | if len(ips) == 0 { |
| 238 | return "" |
| 239 | } |
| 240 | return ips[0] |
| 241 | } |
| 242 | |
| 243 | func topologyMatchPreferredMAC(match Match) string { |
| 244 | macs := make([]string, 0, len(match.MacAddresses)+len(match.ChassisIDs)) |
| 245 | for _, value := range match.MacAddresses { |
| 246 | if mac := normalizeMAC(value); mac != "" { |
| 247 | macs = append(macs, mac) |
| 248 | } |
| 249 | } |
| 250 | for _, value := range match.ChassisIDs { |
| 251 | if mac := normalizeMAC(value); mac != "" { |
| 252 | macs = append(macs, mac) |
| 253 | } |
| 254 | } |
| 255 | macs = uniqueTopologyStrings(macs) |
| 256 | if len(macs) == 0 { |
| 257 | return "" |
| 258 | } |
| 259 | return macs[0] |
| 260 | } |
| 261 | |
| 262 | func normalizeDNSName(name string) string { |
| 263 | name = strings.TrimSpace(name) |
| 264 | name = strings.TrimSuffix(name, ".") |
| 265 | if name == "" { |
| 266 | return "" |
| 267 | } |
| 268 | return strings.ToLower(name) |
| 269 | } |
| 270 | |
| 271 | func (r *topologyDisplayNameResolver) resolve(ip string) string { |
| 272 | if r == nil || r.lookup == nil { |
| 273 | return "" |
| 274 | } |
| 275 | ip = normalizeTopologyIP(ip) |
| 276 | if ip == "" { |
| 277 | return "" |
| 278 | } |
| 279 | if name, ok := r.cache[ip]; ok { |
| 280 | return name |
| 281 | } |
| 282 | name := normalizeDNSName(r.lookup(ip)) |
| 283 | r.cache[ip] = name |
| 284 | return name |
| 285 | } |