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
}