data.go #2

  • //
  • guest/
  • perforce_software/
  • p4survey/
  • internal/
  • topology/
  • data.go
  • View
  • Commits
  • Open Download .zip Download (8 KB)
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
}
# Change User Description Committed
#2 33865 Robert Cowham Save enriched server info for compatibility with python version
#1 33863 Robert Cowham Sample topology implementation