package topology import ( "net" "sort" "strings" ) // Server is the normalized topology server record written by // p4_topology_diagram.py into saved topology JSON. type Server struct { ServerID string `json:"serverid"` Service string `json:"service"` ServerType string `json:"server_type"` DisplayName string `json:"display_name"` Address string `json:"address"` Port string `json:"port"` Host string `json:"host"` IP string `json:"ip"` Version string `json:"version"` Target string `json:"target"` TargetSource string `json:"target_source"` HostTarget string `json:"host_target"` Participates bool `json:"participates_in_topology"` Notes []string `json:"notes"` } // ParseZtagRecords converts P4 tagged output into individual records. func ParseZtagRecords(output string) []map[string]string { records := make([]map[string]string, 0) current := make(map[string]string) flush := func() { if len(current) > 0 { records = append(records, current) current = make(map[string]string) } } for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) if line == "" { flush() continue } if !strings.HasPrefix(line, "... ") { continue } parts := strings.SplitN(strings.TrimPrefix(line, "... "), " ", 2) if len(parts) != 2 { continue } if _, exists := current[parts[0]]; exists { flush() } current[parts[0]] = strings.TrimSpace(parts[1]) } flush() return records } func hostFromP4Address(address string) string { address = strings.TrimPrefix(strings.TrimSpace(address), "ssl:") host, _, err := net.SplitHostPort(address) if err != nil { return "" } return strings.Trim(host, "[]") } // Hosts returns the sorted, unique hosts referenced by P4 server records. func Hosts(records ...[]map[string]string) []string { hostSet := make(map[string]struct{}) for _, recordSet := range records { for _, record := range recordSet { for _, key := range []string{"Address", "address", "ServerAddress", "serverAddress", "TargetAddress", "targetAddress"} { if host := hostFromP4Address(record[key]); host != "" { hostSet[host] = struct{}{} } } } } hosts := make([]string, 0, len(hostSet)) for host := range hostSet { hosts = append(hosts, host) } sort.Strings(hosts) return hosts } // NormalizeServers produces the enriched servers array used by the Python // topology JSON format from P4 tagged command output. func NormalizeServers(serverRecords, topologyRecords []map[string]string, nslookup map[string]string) []Server { servers := make([]Server, 0, len(serverRecords)) byID := make(map[string]int) for _, record := range serverRecords { serverType := recordValue(record, "Services", "Service") if serverType == "" { serverType = "unknown" } if serverType == "git-connector" { continue } id := recordValue(record, "ServerID", "Name") if id == "" { continue } address := recordValue(record, "Address", "ExternalAddress") servers = append(servers, serverFromAddress(id, recordValue(record, "Type"), serverType, address)) byID[id] = len(servers) - 1 } addPseudoServers(&servers, byID, topologyRecords) byID = serverIndexes(servers) addresses := make(map[string]string) for _, server := range servers { if server.Address != "" && !isLoopbackAddress(server.Address) { addresses[server.Address] = server.ServerID } } for index := range servers { server := &servers[index] if server.Host == "" { if address := ownAddress(server.ServerID, topologyRecords); address != "" { server.Address = address server.Host = hostFromP4Address(address) server.Port = portFromP4Address(address) } } if server.Host != "" { server.IP = nslookupIP(nslookup[server.Host]) } if server.ServerType == "commit-server" { server.TargetSource = "root" continue } if record, ok := topologyFor(server.ServerID, topologyRecords); ok { server.Target = recordValue(record, "TargetServerID", "ParentServerID") if _, known := byID[server.Target]; server.Target == "" || !known { if isLoopbackAddress(recordValue(record, "TargetDestAddress")) { server.Target = addresses[recordValue(record, "TargetAddress")] } } server.Version = recordValue(record, "Version") server.TargetSource = "topology" server.Participates = true } } return servers } func serverFromAddress(id, service, serverType, address string) Server { return Server{ServerID: id, Service: valueOr(service, "server"), ServerType: serverType, Address: address, Port: portFromP4Address(address), Host: hostFromP4Address(address), Notes: []string{}} } func addPseudoServers(servers *[]Server, byID map[string]int, records []map[string]string) { selected := make(map[string]map[string]string) for _, record := range records { serverType := recordValue(record, "Type") target := recordValue(record, "TargetServerID") address := recordValue(record, "ServerAddress") if (serverType != "broker" && serverType != "proxy") || target == "" || address == "" { continue } if _, ok := byID[target]; !ok { continue } key := serverType + "\x00" + target if current, ok := selected[key]; !ok || isLoopbackAddress(recordValue(current, "ServerAddress")) { selected[key] = record } } for _, record := range selected { address := recordValue(record, "ServerAddress") host := hostFromP4Address(address) id := recordValue(record, "Type") + "-" + sanitizeID(host) if _, exists := byID[id]; exists { continue } server := serverFromAddress(id, "pseudo", recordValue(record, "Type"), address) server.DisplayName = address server.Target = recordValue(record, "TargetServerID") server.TargetSource = "topology" server.Version = recordValue(record, "Version") server.Participates = true server.Notes = []string{server.ServerType + " for " + server.Target} if isLoopbackAddress(recordValue(record, "TargetAddress")) { server.HostTarget = server.Target } *servers = append(*servers, server) byID[id] = len(*servers) - 1 } } func recordValue(record map[string]string, keys ...string) string { for _, key := range keys { if value := strings.TrimSpace(record[key]); value != "" { return value } } return "" } func portFromP4Address(address string) string { _, port, err := net.SplitHostPort(strings.TrimPrefix(address, "ssl:")) if err != nil { return "" } return port } func isLoopbackAddress(address string) bool { host := hostFromP4Address(address) return net.ParseIP(host) != nil && net.ParseIP(host).IsLoopback() } func sanitizeID(value string) string { return strings.Trim(strings.Map(func(character rune) rune { if character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' || character >= '0' && character <= '9' { return character } return '-' }, value), "-") } func serverIndexes(servers []Server) map[string]int { indexes := make(map[string]int, len(servers)) for index, server := range servers { indexes[server.ServerID] = index } return indexes } func topologyFor(id string, records []map[string]string) (map[string]string, bool) { for _, record := range records { if recordValue(record, "ServerID") == id { return record, true } } return nil, false } func ownAddress(id string, records []map[string]string) string { if record, ok := topologyFor(id, records); ok { address := recordValue(record, "ServerAddress") if !isLoopbackAddress(address) { return address } } return "" } func nslookupIP(output string) string { nameSeen := false candidate := "" for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Name:") { nameSeen = true continue } if strings.HasPrefix(line, "Address:") { value := strings.TrimSpace(strings.TrimPrefix(line, "Address:")) if strings.Contains(value, "#") || net.ParseIP(value) == nil { continue } if nameSeen { return value } candidate = value } } return candidate } func valueOr(value, fallback string) string { if value == "" { return fallback } return value }