Files
weave-scope/report/topology.go

312 lines
9.2 KiB
Go

package report
import (
"fmt"
"log"
"net"
"reflect"
)
const localUnknown = "localUnknown"
// Topology describes a specific view of a network. It consists of nodes and
// edges, represented by Adjacency, and metadata about those nodes and edges,
// represented by EdgeMetadatas and NodeMetadatas respectively.
type Topology struct {
Adjacency
EdgeMetadatas
NodeMetadatas
}
// Adjacency is an adjacency-list encoding of the topology. Keys are node IDs,
// as produced by the relevant MappingFunc for the topology.
type Adjacency map[string]IDList
// EdgeMetadatas collect metadata about each edge in a topology. Keys are a
// concatenation of node IDs.
type EdgeMetadatas map[string]EdgeMetadata
// NodeMetadatas collect metadata about each node in a topology. Keys are node
// IDs.
type NodeMetadatas map[string]NodeMetadata
// EdgeMetadata describes a superset of the metadata that probes can
// conceivably (and usefully) collect about an edge between two nodes in any
// topology.
type EdgeMetadata struct {
WithBytes bool `json:"with_bytes,omitempty"`
BytesIngress uint `json:"bytes_ingress,omitempty"` // dst -> src
BytesEgress uint `json:"bytes_egress,omitempty"` // src -> dst
WithConnCountTCP bool `json:"with_conn_count_tcp,omitempty"`
MaxConnCountTCP uint `json:"max_conn_count_tcp,omitempty"`
}
// NodeMetadata describes a superset of the metadata that probes can collect
// about a given node in a given topology. Right now it's a weakly-typed map,
// which should probably change (see comment on type MapFunc).
type NodeMetadata map[string]string
// Copy returns a value copy, useful for tests.
func (nm NodeMetadata) Copy() NodeMetadata {
cp := make(NodeMetadata, len(nm))
for k, v := range nm {
cp[k] = v
}
return cp
}
// Merge merges two node metadata maps together. In case of conflict, the
// other (right-hand) side wins. Always reassign the result of merge to the
// destination. Merge is defined on the value-type, but node metadata map is
// itself a reference type, so if you want to maintain immutability, use copy.
func (nm NodeMetadata) Merge(other NodeMetadata) NodeMetadata {
for k, v := range other {
nm[k] = v // other takes precedence
}
return nm
}
// NewTopology gives you a Topology.
func NewTopology() Topology {
return Topology{
Adjacency: map[string]IDList{},
EdgeMetadatas: map[string]EdgeMetadata{},
NodeMetadatas: map[string]NodeMetadata{},
}
}
// RenderBy transforms a given Topology into a set of RenderableNodes, which
// the UI will render collectively as a graph. Note that a RenderableNode will
// always be rendered with other nodes, and therefore contains limited detail.
//
// RenderBy takes a a MapFunc, which defines how to group and label nodes. Npdes
// with the same mapped IDs will be merged.
func (t Topology) RenderBy(mapFunc MapFunc, pseudoFunc PseudoFunc) RenderableNodes {
nodes := RenderableNodes{}
// Build a set of RenderableNodes for all non-pseudo probes, and an
// addressID to nodeID lookup map. Multiple addressIDs can map to the same
// RenderableNodes.
address2mapped := map[string]string{}
for nodeID, metadata := range t.NodeMetadatas {
mapped, ok := mapFunc(nodeID, metadata)
if !ok {
continue
}
// mapped.ID needs not be unique over all addressIDs. If not, we just overwrite
// the existing data, on the assumption that the MapFunc returns the same
// data.
nodes[mapped.ID] = RenderableNode{
ID: mapped.ID,
LabelMajor: mapped.Major,
LabelMinor: mapped.Minor,
Rank: mapped.Rank,
Pseudo: false,
Origins: IDList{nodeID},
Metadata: AggregateMetadata{}, // later
}
address2mapped[nodeID] = mapped.ID
}
// Walk the graph and make connections.
for src, dsts := range t.Adjacency {
var (
srcNodeID, ok1 = ParseAdjacencyID(src)
srcOriginHostID, _, ok2 = ParseNodeID(srcNodeID)
srcRenderableID = address2mapped[srcNodeID] // must exist
srcRenderableNode = nodes[srcRenderableID] // must exist
)
if !ok1 || !ok2 {
log.Printf("bad adjacency ID %q", src)
continue
}
for _, dstNodeID := range dsts {
dstRenderableID, ok := address2mapped[dstNodeID]
if !ok {
pseudoNode, ok := pseudoFunc(srcNodeID, srcRenderableNode, dstNodeID)
if !ok {
continue
}
dstRenderableID = pseudoNode.ID
nodes[dstRenderableID] = RenderableNode{
ID: pseudoNode.ID,
LabelMajor: pseudoNode.Major,
LabelMinor: pseudoNode.Minor,
Pseudo: true,
Metadata: AggregateMetadata{}, // populated below - or not?
}
address2mapped[dstNodeID] = dstRenderableID
}
srcRenderableNode.Adjacency = srcRenderableNode.Adjacency.Add(dstRenderableID)
srcRenderableNode.Origins = srcRenderableNode.Origins.Add(MakeHostNodeID(srcOriginHostID))
srcRenderableNode.Origins = srcRenderableNode.Origins.Add(srcNodeID)
edgeID := MakeEdgeID(srcNodeID, dstNodeID)
if md, ok := t.EdgeMetadatas[edgeID]; ok {
srcRenderableNode.Metadata.Merge(md.Transform())
}
}
nodes[srcRenderableID] = srcRenderableNode
}
return nodes
}
// EdgeMetadata gives the metadata of an edge from the perspective of the
// srcRenderableID. Since an edgeID can have multiple edges on the address
// level, it uses the supplied mapping function to translate address IDs to
// renderable node (mapped) IDs.
func (t Topology) EdgeMetadata(mapFunc MapFunc, srcRenderableID, dstRenderableID string) EdgeMetadata {
metadata := EdgeMetadata{}
for edgeID, edgeMeta := range t.EdgeMetadatas {
src, dst, ok := ParseEdgeID(edgeID)
if !ok {
log.Printf("bad edge ID %q", edgeID)
continue
}
if src != TheInternet {
mapped, _ := mapFunc(src, t.NodeMetadatas[src])
src = mapped.ID
}
if dst != TheInternet {
mapped, _ := mapFunc(dst, t.NodeMetadatas[dst])
dst = mapped.ID
}
if src == srcRenderableID && dst == dstRenderableID {
metadata.Flatten(edgeMeta)
}
}
return metadata
}
// Squash squashes all non-local nodes in the topology to a super-node called
// the Internet.
// We rely on the values in the t.Adjacency lists being valid keys in
// t.NodeMetadata (or t.Adjacency).
func (t Topology) Squash(f IDAddresser, localNets []*net.IPNet) Topology {
isRemote := func(id string) bool {
if _, ok := t.NodeMetadatas[id]; ok {
return false // it is a node, cannot possibly be remote
}
if _, ok := t.Adjacency[MakeAdjacencyID(id)]; ok {
return false // it is in our adjacency list, cannot possibly be remote
}
if ip := f(id); ip != nil && netsContain(localNets, ip) {
return false // it is in our local nets, so it is not remote
}
return true
}
for srcID, dstIDs := range t.Adjacency {
newDstIDs := make(IDList, 0, len(dstIDs))
for _, dstID := range dstIDs {
if isRemote(dstID) {
dstID = TheInternet
}
newDstIDs = newDstIDs.Add(dstID)
}
t.Adjacency[srcID] = newDstIDs
}
return t
}
func netsContain(nets []*net.IPNet, ip net.IP) bool {
for _, net := range nets {
if net.Contains(ip) {
return true
}
}
return false
}
// Diff is returned by TopoDiff. It represents the changes between two
// RenderableNode maps.
type Diff struct {
Add []RenderableNode `json:"add"`
Update []RenderableNode `json:"update"`
Remove []string `json:"remove"`
}
// TopoDiff gives you the diff to get from A to B.
func TopoDiff(a, b RenderableNodes) Diff {
diff := Diff{}
notSeen := map[string]struct{}{}
for k := range a {
notSeen[k] = struct{}{}
}
for k, node := range b {
if _, ok := a[k]; !ok {
diff.Add = append(diff.Add, node)
} else if !reflect.DeepEqual(node, a[k]) {
diff.Update = append(diff.Update, node)
}
delete(notSeen, k)
}
// leftover keys
for k := range notSeen {
diff.Remove = append(diff.Remove, k)
}
return diff
}
// ByID is a sort interface for a RenderableNode slice.
type ByID []RenderableNode
func (r ByID) Len() int { return len(r) }
func (r ByID) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ByID) Less(i, j int) bool { return r[i].ID < r[j].ID }
// Validate checks the topology for various inconsistencies.
func (t Topology) Validate() error {
// Check all edge metadata keys must have the appropriate entries in adjacencies & node metadata
for edgeID := range t.EdgeMetadatas {
srcNodeID, dstNodeID, ok := ParseEdgeID(edgeID)
if !ok {
return fmt.Errorf("Invalid edge id: %s", edgeID)
}
if _, ok := t.NodeMetadatas[srcNodeID]; !ok {
return fmt.Errorf("Source node missing for edge id: %s", edgeID)
}
adjs, ok := t.Adjacency[MakeAdjacencyID(srcNodeID)]
if !ok {
return fmt.Errorf("Adjancey entries for missing for node id: %s (from edge %s)", srcNodeID, edgeID)
}
if !adjs.Contains(dstNodeID) {
return fmt.Errorf("Adjancey entry missing for edge id: %s", edgeID)
}
}
// Check all adjancency keys has entries in NodeMetadata
for adjID := range t.Adjacency {
nodeID, ok := ParseAdjacencyID(adjID)
if !ok {
return fmt.Errorf("Invalid adjacency id: %s", adjID)
}
if _, ok := t.NodeMetadatas[nodeID]; !ok {
return fmt.Errorf("Source node missing for adjancency id: %s", adjID)
}
}
// Check all node metadata keys are parse-able (ie, contain a scope)
for nodeID := range t.NodeMetadatas {
if _, _, ok := ParseNodeID(nodeID); !ok {
return fmt.Errorf("Invalid node id: %s", nodeID)
}
}
return nil
}