diff --git a/src/go/pkg/l2topology/engine.go b/src/go/pkg/l2topology/engine.go deleted file mode 100644 index 13a77180fae3bf..00000000000000 --- a/src/go/pkg/l2topology/engine.go +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import ( - "context" - "errors" -) - -var ( - ErrNotImplemented = errors.New("topology engine not implemented") - ErrInvalidRequest = errors.New("invalid topology discovery request") -) - -// Engine executes topology discovery from different seed inputs. -type Engine interface { - DiscoverByCIDRs(ctx context.Context, req CIDRRequest) (Result, error) - DiscoverByDevices(ctx context.Context, req DeviceRequest) (Result, error) -} diff --git a/src/go/pkg/l2topology/engine_test.go b/src/go/pkg/l2topology/engine_test.go deleted file mode 100644 index f4a3fdd44399f6..00000000000000 --- a/src/go/pkg/l2topology/engine_test.go +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNoopEngine_DiscoverByCIDRs(t *testing.T) { - eng := NoopEngine{} - _, err := eng.DiscoverByCIDRs(context.Background(), CIDRRequest{}) - require.ErrorIs(t, err, ErrNotImplemented) -} - -func TestNoopEngine_DiscoverByDevices(t *testing.T) { - eng := NoopEngine{} - _, err := eng.DiscoverByDevices(context.Background(), DeviceRequest{}) - require.ErrorIs(t, err, ErrNotImplemented) -} diff --git a/src/go/pkg/l2topology/l2_pipeline.go b/src/go/pkg/l2topology/l2_pipeline.go index 3a30ccdcefea43..4024f013efb40a 100644 --- a/src/go/pkg/l2topology/l2_pipeline.go +++ b/src/go/pkg/l2topology/l2_pipeline.go @@ -2,7 +2,7 @@ package l2topology -import "fmt" +import "errors" const ( fdbStatusLearned = "learned" @@ -15,7 +15,7 @@ const ( // DiscoverOptions.CollectedAt explicitly. func BuildL2ResultFromObservations(observations []L2Observation, opts DiscoverOptions) (Result, error) { if len(observations) == 0 { - return Result{}, fmt.Errorf("%w: at least one observation is required", ErrInvalidRequest) + return Result{}, errors.New("at least one observation is required") } if !opts.EnableLLDP && !opts.EnableCDP && !opts.EnableBridge && !opts.EnableARP && !opts.EnableSTP { opts.EnableLLDP = true diff --git a/src/go/pkg/l2topology/l2_pipeline_test.go b/src/go/pkg/l2topology/l2_pipeline_test.go index c46405f4968928..14e6c58ede1eb8 100644 --- a/src/go/pkg/l2topology/l2_pipeline_test.go +++ b/src/go/pkg/l2topology/l2_pipeline_test.go @@ -1546,7 +1546,6 @@ func TestBuildL2ResultFromObservations_AnnotatesPairMetadata(t *testing.T) { func TestBuildL2ResultFromObservations_ErrorsOnEmptyInput(t *testing.T) { _, err := BuildL2ResultFromObservations(nil, DiscoverOptions{EnableLLDP: true}) require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidRequest) } func findDeviceByHostname(devices []Device, hostname string) *Device { diff --git a/src/go/pkg/l2topology/mac_oui_lookup.go b/src/go/pkg/l2topology/mac_oui_lookup.go index cf177243e0d13b..87ca70dd70858b 100644 --- a/src/go/pkg/l2topology/mac_oui_lookup.go +++ b/src/go/pkg/l2topology/mac_oui_lookup.go @@ -7,6 +7,8 @@ import ( "sort" "strings" "sync" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) //go:embed mac_oui_vendors.tsv @@ -114,7 +116,7 @@ func lookupTopologyVendorByMACInIndex(index topologyOUIVendorIndex, mac string) return "", "" } -func inferTopologyVendorFromMatch(match Match) (vendor string, prefix string) { +func inferTopologyVendorFromMatch(match graph.Match) (vendor string, prefix string) { candidates := make(map[string]struct{}, len(match.MacAddresses)+len(match.ChassisIDs)) for _, value := range match.MacAddresses { if mac := normalizeMAC(value); mac != "" { diff --git a/src/go/pkg/l2topology/mac_oui_lookup_test.go b/src/go/pkg/l2topology/mac_oui_lookup_test.go index 3f5d702b836bbe..9ec1527fc66828 100644 --- a/src/go/pkg/l2topology/mac_oui_lookup_test.go +++ b/src/go/pkg/l2topology/mac_oui_lookup_test.go @@ -3,9 +3,11 @@ package l2topology import ( - "github.com/stretchr/testify/require" "strings" "testing" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" + "github.com/stretchr/testify/require" ) func TestBuildTopologyOUIVendorIndex_IgnoresInvalidLinesAndKeepsFirstDuplicate(t *testing.T) { @@ -47,7 +49,7 @@ func TestLookupTopologyVendorByMACInIndex_PrefersLongestPrefixAndNormalizesMAC(t } func TestInferTopologyVendorFromMatch_UsesDeterministicCandidateOrder(t *testing.T) { - match := Match{ + match := graph.Match{ MacAddresses: []string{ "28:6f:b9:00:00:22", "08:ea:44:11:22:33", diff --git a/src/go/pkg/l2topology/node_topology.go b/src/go/pkg/l2topology/node_topology.go deleted file mode 100644 index b4713f262d0a21..00000000000000 --- a/src/go/pkg/l2topology/node_topology.go +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import "net/netip" - -// NodeTopologyEntity mirrors the minimum node fields used by Enlinkd node topology logic. -type NodeTopologyEntity struct { - ID int - Label string - SysObject string - SysName string - Address netip.Addr -} - -// IPInterfaceTopologyEntity mirrors the minimum IP interface fields used by Enlinkd node topology logic. -type IPInterfaceTopologyEntity struct { - ID int - NodeID int - IPAddress netip.Addr - NetMask netip.Addr - IsManaged bool - IsSnmpPrimary bool - IfIndex int - SnmpInterfaceID int -} - -// SnmpInterfaceTopologyEntity mirrors the minimum SNMP interface fields used by Enlinkd node topology logic. -type SnmpInterfaceTopologyEntity struct { - ID int - NodeID int - IfIndex int - IfName string - IfDescr string -} diff --git a/src/go/pkg/l2topology/node_topology_router.go b/src/go/pkg/l2topology/node_topology_router.go deleted file mode 100644 index 5b7818f59f6de5..00000000000000 --- a/src/go/pkg/l2topology/node_topology_router.go +++ /dev/null @@ -1,318 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import ( - "sort" - "strconv" - "strings" -) - -// NetworkRouterTopology mirrors Enlinkd NetworkRouterTopologyUpdater graph payload. -type NetworkRouterTopology struct { - Vertices []NetworkRouterVertex - Edges []NetworkRouterEdge - DefaultVertex string -} - -// NetworkRouterVertex is one node or subnet vertex in the network-router topology. -type NetworkRouterVertex struct { - ID string - Label string - Address string - IconKey string - NodeID int - ToolTip string - IsSubnet bool -} - -// NetworkRouterPort is one endpoint port in the network-router topology. -type NetworkRouterPort struct { - ID string - Vertex string - IPID int - IfIndex int - IfName string - Addr string - ToolTip string -} - -// NetworkRouterEdge is one link in the network-router topology. -type NetworkRouterEdge struct { - ID string - SourcePort NetworkRouterPort - TargetPort NetworkRouterPort -} - -// BuildNetworkRouterTopology ports NetworkRouterTopologyUpdater.buildTopology(). -func BuildNetworkRouterTopology(service *NodeTopologyService, ipv4prefix, ipv6prefix int) NetworkRouterTopology { - if service == nil { - return NetworkRouterTopology{} - } - - result := NetworkRouterTopology{} - vertexByID := make(map[string]NetworkRouterVertex) - edgeByID := make(map[string]NetworkRouterEdge) - - nodeMap := make(map[int]NodeTopologyEntity) - for _, node := range service.FindAllNode() { - nodeMap[node.ID] = node - } - ipPrimaryMap := getIPPrimaryMap(service.FindAllIP()) - ipTable := getIPInterfaceTable(service.FindAllIP()) - snmpByID := make(map[int]SnmpInterfaceTopologyEntity) - for _, snmp := range service.FindAllSnmp() { - snmpByID[snmp.ID] = snmp - } - - addVertex := func(v NetworkRouterVertex) { - if strings.TrimSpace(v.ID) == "" { - return - } - if _, exists := vertexByID[v.ID]; exists { - return - } - vertexByID[v.ID] = v - } - addEdge := func(e NetworkRouterEdge) { - if strings.TrimSpace(e.ID) == "" { - return - } - if _, exists := edgeByID[e.ID]; exists { - return - } - edgeByID[e.ID] = e - } - - for _, node := range service.FindAllNode() { - primary := ipPrimaryMap[node.ID] - addVertex(createNodeVertex(node, primary)) - } - - for _, subnet := range service.FindAllLegalPointToPointSubNetwork() { - if subnet == nil { - continue - } - nodeIDs := subnet.NodeIDs() - if len(nodeIDs) < 2 { - continue - } - sourceNodeID := nodeIDs[0] - targetNodeID := nodeIDs[1] - - source, sourceOK := nodeMap[sourceNodeID] - target, targetOK := nodeMap[targetNodeID] - if !sourceOK || !targetOK { - continue - } - - sourceIP, sourceIPOK := firstIPInSubnet(ipTable[sourceNodeID], subnet) - targetIP, targetIPOK := firstIPInSubnet(ipTable[targetNodeID], subnet) - if !sourceIPOK || !targetIPOK { - continue - } - - sourcePort := createNodePort(createNodeVertex(source, ipPrimaryMap[source.ID]), sourceIP, snmpByID[sourceIP.SnmpInterfaceID]) - targetPort := createNodePort(createNodeVertex(target, ipPrimaryMap[target.ID]), targetIP, snmpByID[targetIP.SnmpInterfaceID]) - addEdge(NetworkRouterEdge{ - ID: sourcePort.Vertex + keySep + sourcePort.ID + "->" + targetPort.Vertex + keySep + targetPort.ID, - SourcePort: sourcePort, - TargetPort: targetPort, - }) - } - - for _, subnet := range service.FindSubNetworkByNetworkPrefixLessThen(ipv4prefix, ipv6prefix) { - if subnet == nil { - continue - } - networkVertex := createNetworkVertex(subnet) - addVertex(networkVertex) - for _, targetNodeID := range subnet.NodeIDs() { - targetNode, ok := nodeMap[targetNodeID] - if !ok { - continue - } - targetIP, found := firstIPInSubnet(ipTable[targetNodeID], subnet) - if !found { - continue - } - targetVertex := createNodeVertex(targetNode, ipPrimaryMap[targetNodeID]) - sourcePort := createNetworkPort(networkVertex, targetIP) - targetPort := createNodePort(targetVertex, targetIP, snmpByID[targetIP.SnmpInterfaceID]) - addEdge(NetworkRouterEdge{ - ID: sourcePort.Vertex + keySep + sourcePort.ID + "->" + targetPort.Vertex + keySep + targetPort.ID, - SourcePort: sourcePort, - TargetPort: targetPort, - }) - } - } - - if len(ipPrimaryMap) > 0 { - nodeIDs := make([]int, 0, len(ipPrimaryMap)) - for nodeID := range ipPrimaryMap { - nodeIDs = append(nodeIDs, nodeID) - } - sort.Ints(nodeIDs) - for _, nodeID := range nodeIDs { - if nodeID <= 0 { - continue - } - result.DefaultVertex = strconv.Itoa(nodeID) - break - } - } - - result.Vertices = sortedNetworkRouterVertices(vertexByID) - result.Edges = sortedNetworkRouterEdges(edgeByID) - return result -} - -func createNodeVertex(node NodeTopologyEntity, primary IPInterfaceTopologyEntity) NetworkRouterVertex { - address := "" - if primary.IPAddress.IsValid() { - address = primary.IPAddress.String() - } else if node.Address.IsValid() { - address = node.Address.String() - } - label := strings.TrimSpace(node.Label) - if label == "" { - label = strconv.Itoa(node.ID) - } - vertex := NetworkRouterVertex{ - ID: strconv.Itoa(node.ID), - Label: label, - Address: address, - IconKey: "node", - NodeID: node.ID, - } - vertex.ToolTip = "Node: " + label - if address != "" { - vertex.ToolTip += " (" + address + ")" - } - return vertex -} - -func createNodePort(vertex NetworkRouterVertex, ip IPInterfaceTopologyEntity, snmp SnmpInterfaceTopologyEntity) NetworkRouterPort { - port := NetworkRouterPort{ - ID: ip.IPAddress.String(), - Vertex: vertex.ID, - IPID: ip.ID, - Addr: ip.IPAddress.String(), - } - if snmp.ID > 0 { - port.IfIndex = snmp.IfIndex - port.IfName = snmp.IfName - } - port.ToolTip = "Port " + port.Addr - if port.IfName != "" { - port.ToolTip += " (" + port.IfName + ")" - } - return port -} - -func createNetworkPort(vertex NetworkRouterVertex, target IPInterfaceTopologyEntity) NetworkRouterPort { - addr := "to: " + target.IPAddress.String() - return NetworkRouterPort{ - ID: vertex.ID + "to:" + target.IPAddress.String(), - Vertex: vertex.ID, - IPID: target.ID, - Addr: addr, - ToolTip: "Port " + addr, - } -} - -func createNetworkVertex(network *SubNetwork) NetworkRouterVertex { - cidr := "" - nodeIDs := "" - if network != nil { - cidr = network.CIDR() - nodeIDValues := network.NodeIDs() - parts := make([]string, 0, len(nodeIDValues)) - for _, nodeID := range nodeIDValues { - parts = append(parts, strconv.Itoa(nodeID)) - } - nodeIDs = strings.Join(parts, ",") - } - return NetworkRouterVertex{ - ID: cidr, - Label: cidr, - Address: cidr, - IconKey: "cloud", - ToolTip: "SubNetwork: " + cidr + ", Nodeids:[" + nodeIDs + "]", - IsSubnet: true, - } -} - -func getIPPrimaryMap(ips []IPInterfaceTopologyEntity) map[int]IPInterfaceTopologyEntity { - primary := make(map[int]IPInterfaceTopologyEntity) - for _, ip := range ips { - if ip.NodeID <= 0 || !ip.IPAddress.IsValid() { - continue - } - current, exists := primary[ip.NodeID] - if !exists { - primary[ip.NodeID] = ip - continue - } - if ip.IsSnmpPrimary { - primary[ip.NodeID] = ip - continue - } - primary[ip.NodeID] = current - } - return primary -} - -func getIPInterfaceTable(ips []IPInterfaceTopologyEntity) map[int][]IPInterfaceTopologyEntity { - table := make(map[int][]IPInterfaceTopologyEntity) - for _, ip := range ips { - if ip.NodeID <= 0 || !ip.IPAddress.IsValid() { - continue - } - table[ip.NodeID] = append(table[ip.NodeID], ip) - } - for nodeID := range table { - sort.Slice(table[nodeID], func(i, j int) bool { - if table[nodeID][i].ID != table[nodeID][j].ID { - return table[nodeID][i].ID < table[nodeID][j].ID - } - return compareAddr(table[nodeID][i].IPAddress, table[nodeID][j].IPAddress) < 0 - }) - } - return table -} - -func firstIPInSubnet(ips []IPInterfaceTopologyEntity, subnet *SubNetwork) (IPInterfaceTopologyEntity, bool) { - for _, ip := range ips { - if subnet.IsInRange(ip.IPAddress) { - return ip, true - } - } - return IPInterfaceTopologyEntity{}, false -} - -func sortedNetworkRouterVertices(values map[string]NetworkRouterVertex) []NetworkRouterVertex { - keys := make([]string, 0, len(values)) - for key := range values { - keys = append(keys, key) - } - sort.Strings(keys) - result := make([]NetworkRouterVertex, 0, len(keys)) - for _, key := range keys { - result = append(result, values[key]) - } - return result -} - -func sortedNetworkRouterEdges(values map[string]NetworkRouterEdge) []NetworkRouterEdge { - keys := make([]string, 0, len(values)) - for key := range values { - keys = append(keys, key) - } - sort.Strings(keys) - result := make([]NetworkRouterEdge, 0, len(keys)) - for _, key := range keys { - result = append(result, values[key]) - } - return result -} diff --git a/src/go/pkg/l2topology/node_topology_service.go b/src/go/pkg/l2topology/node_topology_service.go deleted file mode 100644 index 09fcf0f5f43894..00000000000000 --- a/src/go/pkg/l2topology/node_topology_service.go +++ /dev/null @@ -1,357 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import "sort" - -// NodeTopologyService ports Enlinkd NodeTopologyServiceImpl logic. -type NodeTopologyService struct { - nodes []NodeTopologyEntity - ips []IPInterfaceTopologyEntity - snmp []SnmpInterfaceTopologyEntity -} - -// NewNodeTopologyService builds a deterministic node topology service snapshot. -func NewNodeTopologyService(nodes []NodeTopologyEntity, ips []IPInterfaceTopologyEntity, snmp []SnmpInterfaceTopologyEntity) *NodeTopologyService { - out := &NodeTopologyService{ - nodes: append([]NodeTopologyEntity(nil), nodes...), - ips: append([]IPInterfaceTopologyEntity(nil), ips...), - snmp: append([]SnmpInterfaceTopologyEntity(nil), snmp...), - } - - sort.Slice(out.nodes, func(i, j int) bool { - if out.nodes[i].ID != out.nodes[j].ID { - return out.nodes[i].ID < out.nodes[j].ID - } - return out.nodes[i].Label < out.nodes[j].Label - }) - sort.Slice(out.ips, func(i, j int) bool { - if out.ips[i].ID != out.ips[j].ID { - return out.ips[i].ID < out.ips[j].ID - } - if out.ips[i].NodeID != out.ips[j].NodeID { - return out.ips[i].NodeID < out.ips[j].NodeID - } - return out.ips[i].IPAddress.String() < out.ips[j].IPAddress.String() - }) - sort.Slice(out.snmp, func(i, j int) bool { - if out.snmp[i].ID != out.snmp[j].ID { - return out.snmp[i].ID < out.snmp[j].ID - } - if out.snmp[i].NodeID != out.snmp[j].NodeID { - return out.snmp[i].NodeID < out.snmp[j].NodeID - } - return out.snmp[i].IfIndex < out.snmp[j].IfIndex - }) - return out -} - -// FindAllNode returns all node entities. -func (s *NodeTopologyService) FindAllNode() []NodeTopologyEntity { - if s == nil { - return nil - } - return append([]NodeTopologyEntity(nil), s.nodes...) -} - -// FindAllIP returns all IP interface entities. -func (s *NodeTopologyService) FindAllIP() []IPInterfaceTopologyEntity { - if s == nil { - return nil - } - return append([]IPInterfaceTopologyEntity(nil), s.ips...) -} - -// FindAllSnmp returns all SNMP interface entities. -func (s *NodeTopologyService) FindAllSnmp() []SnmpInterfaceTopologyEntity { - if s == nil { - return nil - } - return append([]SnmpInterfaceTopologyEntity(nil), s.snmp...) -} - -// FindAllSubNetwork ports NodeTopologyServiceImpl.findAllSubNetwork(). -func (s *NodeTopologyService) FindAllSubNetwork() []*SubNetwork { - if s == nil { - return nil - } - byKey := make(map[string]*SubNetwork) - keys := make([]string, 0) - - for _, ip := range s.ips { - if !ip.IsManaged || !ip.IPAddress.IsValid() || !ip.NetMask.IsValid() { - continue - } - network, ok := NetworkAddress(ip.IPAddress, ip.NetMask) - if !ok { - continue - } - key := subnetKey(network, ip.NetMask) - subnet := byKey[key] - if subnet == nil { - created, err := NewSubNetwork(ip.NodeID, ip.IPAddress, ip.NetMask) - if err != nil { - continue - } - byKey[key] = created - keys = append(keys, key) - continue - } - subnet.Add(ip.NodeID, ip.IPAddress) - } - - for _, ip := range s.ips { - if !ip.IsManaged || !ip.IPAddress.IsValid() || ip.NetMask.IsValid() { - continue - } - for _, key := range keys { - subnet := byKey[key] - if subnet == nil { - continue - } - subnet.Add(ip.NodeID, ip.IPAddress) - } - } - - sorted := sortedSubnetworkKeys(byKey) - result := make([]*SubNetwork, 0, len(sorted)) - for _, key := range sorted { - subnet := byKey[key] - if subnet == nil { - continue - } - result = append(result, subnet.clone()) - } - return result -} - -// FindAllLegalSubNetwork ports NodeTopologyServiceImpl.findAllLegalSubNetwork(). -func (s *NodeTopologyService) FindAllLegalSubNetwork() []*SubNetwork { - all := s.FindAllSubNetwork() - if len(all) == 0 { - return nil - } - result := make([]*SubNetwork, 0, len(all)) - for _, subnet := range all { - if subnet == nil || subnet.HasDuplicatedAddress() { - continue - } - if InSameNetwork(subnet.Network(), loopbackAddrIPv4, subnet.Netmask()) { - continue - } - result = append(result, subnet) - } - return result -} - -// FindSubNetworkByNetworkPrefixLessThen ports NodeTopologyServiceImpl.findSubNetworkByNetworkPrefixLessThen(). -func (s *NodeTopologyService) FindSubNetworkByNetworkPrefixLessThen(ipv4prefix, ipv6prefix int) []*SubNetwork { - legal := s.FindAllLegalSubNetwork() - if len(legal) == 0 { - return nil - } - result := make([]*SubNetwork, 0, len(legal)) - for _, subnet := range legal { - if subnet == nil { - continue - } - prefix := subnet.NetworkPrefix() - if subnet.IsIPv4Subnetwork() { - if prefix < ipv4prefix { - result = append(result, subnet) - } - continue - } - if prefix < ipv6prefix { - result = append(result, subnet) - } - } - return result -} - -// FindAllPointToPointSubNetwork ports NodeTopologyServiceImpl.findAllPointToPointSubNetwork(). -func (s *NodeTopologyService) FindAllPointToPointSubNetwork() []*SubNetwork { - all := s.FindAllSubNetwork() - if len(all) == 0 { - return nil - } - result := make([]*SubNetwork, 0, len(all)) - for _, subnet := range all { - if subnet == nil { - continue - } - if IsPointToPointMask(subnet.Netmask()) { - result = append(result, subnet) - } - } - return result -} - -// FindAllLegalPointToPointSubNetwork ports NodeTopologyServiceImpl.findAllLegalPointToPointSubNetwork(). -func (s *NodeTopologyService) FindAllLegalPointToPointSubNetwork() []*SubNetwork { - legal := s.FindAllLegalSubNetwork() - if len(legal) == 0 { - return nil - } - result := make([]*SubNetwork, 0, len(legal)) - for _, subnet := range legal { - if subnet == nil { - continue - } - if IsPointToPointMask(subnet.Netmask()) && len(subnet.NodeIDs()) == 2 { - result = append(result, subnet) - } - } - return result -} - -// FindAllLoopbacks ports NodeTopologyServiceImpl.findAllLoopbacks(). -func (s *NodeTopologyService) FindAllLoopbacks() []*SubNetwork { - all := s.FindAllSubNetwork() - if len(all) == 0 { - return nil - } - result := make([]*SubNetwork, 0, len(all)) - for _, subnet := range all { - if subnet == nil { - continue - } - if IsLoopbackMask(subnet.Netmask()) { - result = append(result, subnet) - } - } - return result -} - -// FindAllLegalLoopbacks ports NodeTopologyServiceImpl.findAllLegalLoopbacks(). -func (s *NodeTopologyService) FindAllLegalLoopbacks() []*SubNetwork { - all := s.FindAllSubNetwork() - if len(all) == 0 { - return nil - } - result := make([]*SubNetwork, 0, len(all)) - for _, subnet := range all { - if subnet == nil { - continue - } - if IsLoopbackMask(subnet.Netmask()) && len(subnet.NodeIDs()) == 1 { - result = append(result, subnet) - } - } - return result -} - -// GetNodeIDPriorityMap ports NodeTopologyServiceImpl.getNodeidPriorityMap(). -func (s *NodeTopologyService) GetNodeIDPriorityMap() map[int]int { - priorityMap := make(map[int]int) - legal := s.FindAllLegalSubNetwork() - remaining := make(map[string]*SubNetwork) - for _, subnet := range legal { - if subnet == nil || len(subnet.NodeIDs()) <= 1 { - continue - } - remaining[subnet.key()] = subnet - } - - priority := 0 - for len(remaining) > 0 { - start := getNextSubnetwork(remaining) - if start == nil { - break - } - delete(remaining, start.key()) - for _, nodeID := range start.NodeIDs() { - priorityMap[nodeID] = priority - } - priority = getConnectedSubnets(start, remaining, priorityMap, priority+1) - } - return priorityMap -} - -func getNextSubnetwork(subnets map[string]*SubNetwork) *SubNetwork { - var selected *SubNetwork - for _, subnet := range subnets { - if subnet == nil { - continue - } - if selected == nil { - selected = subnet - continue - } - selectedSize := len(selected.NodeIDs()) - subnetSize := len(subnet.NodeIDs()) - if selectedSize < subnetSize { - selected = subnet - continue - } - if selectedSize != subnetSize { - continue - } - if compareAddr(selected.Network(), subnet.Network()) > 0 { - selected = subnet - } - } - return selected -} - -func getConnectedSubnets(starting *SubNetwork, subnetworks map[string]*SubNetwork, priorityMap map[int]int, priority int) int { - if starting == nil || len(subnetworks) == 0 { - return priority - } - - downlevels := make([]*SubNetwork, 0) - for _, subnet := range subnetworks { - if subnet == nil { - continue - } - if hasNodeIntersection(starting, subnet) { - downlevels = append(downlevels, subnet) - } - } - for _, subnet := range downlevels { - delete(subnetworks, subnet.key()) - } - - for _, subnet := range downlevels { - if subnet == nil { - continue - } - addingNodes := make([]int, 0) - for _, nodeID := range subnet.NodeIDs() { - if _, exists := priorityMap[nodeID]; exists { - continue - } - addingNodes = append(addingNodes, nodeID) - } - if len(addingNodes) == 0 { - continue - } - for _, nodeID := range addingNodes { - priorityMap[nodeID] = priority - } - priority++ - } - - if len(downlevels) > 0 && len(subnetworks) > 0 { - for _, level := range downlevels { - priority = getConnectedSubnets(level, subnetworks, priorityMap, priority) - } - } - return priority -} - -func hasNodeIntersection(left, right *SubNetwork) bool { - if left == nil || right == nil { - return false - } - rightIDs := make(map[int]struct{}, len(right.nodeInterfaceMap)) - for nodeID := range right.nodeInterfaceMap { - rightIDs[nodeID] = struct{}{} - } - for nodeID := range left.nodeInterfaceMap { - if _, ok := rightIDs[nodeID]; ok { - return true - } - } - return false -} diff --git a/src/go/pkg/l2topology/node_topology_subnet.go b/src/go/pkg/l2topology/node_topology_subnet.go deleted file mode 100644 index 4b7af5e540c337..00000000000000 --- a/src/go/pkg/l2topology/node_topology_subnet.go +++ /dev/null @@ -1,353 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import ( - "fmt" - "net/netip" - "sort" - "strconv" -) - -var ( - pointToPointMaskIPv4 = netip.MustParseAddr("255.255.255.252") - pointToPointMaskIPv6 = netip.MustParseAddr("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe") - loopbackMaskIPv4 = netip.MustParseAddr("255.255.255.255") - loopbackMaskIPv6 = netip.MustParseAddr("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") - loopbackAddrIPv4 = netip.MustParseAddr("127.0.0.1") -) - -// SubNetwork mirrors Enlinkd SubNetwork behavior for node/IP membership. -type SubNetwork struct { - network netip.Addr - netmask netip.Addr - nodeInterfaceMap map[int]map[netip.Addr]struct{} -} - -// NewSubNetwork creates a subnet from one managed IP interface. -func NewSubNetwork(nodeID int, ip, netmask netip.Addr) (*SubNetwork, error) { - if nodeID <= 0 { - return nil, fmt.Errorf("node id is required") - } - if !ip.IsValid() { - return nil, fmt.Errorf("ip is required") - } - if !netmask.IsValid() { - return nil, fmt.Errorf("netmask is required") - } - network, ok := NetworkAddress(ip, netmask) - if !ok { - return nil, fmt.Errorf("cannot build network from ip %q and netmask %q", ip, netmask) - } - s := &SubNetwork{ - network: network, - netmask: netmask, - nodeInterfaceMap: map[int]map[netip.Addr]struct{}{}, - } - s.nodeInterfaceMap[nodeID] = map[netip.Addr]struct{}{ip.Unmap(): {}} - return s, nil -} - -// Network returns the network address. -func (s *SubNetwork) Network() netip.Addr { - if s == nil { - return netip.Addr{} - } - return s.network -} - -// Netmask returns the subnet mask. -func (s *SubNetwork) Netmask() netip.Addr { - if s == nil { - return netip.Addr{} - } - return s.netmask -} - -// CIDR returns network/prefix format. -func (s *SubNetwork) CIDR() string { - if s == nil || !s.network.IsValid() || !s.netmask.IsValid() { - return "" - } - prefix, err := MaskToCIDRPrefix(s.netmask) - if err != nil { - return "" - } - return s.network.String() + "/" + strconv.Itoa(prefix) -} - -// NetworkPrefix returns the CIDR prefix for the mask. -func (s *SubNetwork) NetworkPrefix() int { - if s == nil { - return 0 - } - prefix, err := MaskToCIDRPrefix(s.netmask) - if err != nil { - return 0 - } - return prefix -} - -// IsIPv4Subnetwork reports if the subnet uses IPv4. -func (s *SubNetwork) IsIPv4Subnetwork() bool { - return s != nil && s.network.IsValid() && s.network.Is4() -} - -// NodeIDs returns sorted node IDs in the subnet. -func (s *SubNetwork) NodeIDs() []int { - if s == nil || len(s.nodeInterfaceMap) == 0 { - return nil - } - ids := make([]int, 0, len(s.nodeInterfaceMap)) - for nodeID := range s.nodeInterfaceMap { - ids = append(ids, nodeID) - } - sort.Ints(ids) - return ids -} - -// Add adds one node/IP membership if the address is in range. -func (s *SubNetwork) Add(nodeID int, ip netip.Addr) bool { - if s == nil || nodeID <= 0 || !ip.IsValid() || !s.IsInRange(ip) { - return false - } - ip = ip.Unmap() - if _, ok := s.nodeInterfaceMap[nodeID]; !ok { - s.nodeInterfaceMap[nodeID] = map[netip.Addr]struct{}{} - } - if _, exists := s.nodeInterfaceMap[nodeID][ip]; exists { - return false - } - s.nodeInterfaceMap[nodeID][ip] = struct{}{} - return true -} - -// Remove removes one node/IP membership. -func (s *SubNetwork) Remove(nodeID int, ip netip.Addr) bool { - if s == nil || nodeID <= 0 || !ip.IsValid() { - return false - } - ip = ip.Unmap() - ips, ok := s.nodeInterfaceMap[nodeID] - if !ok { - return false - } - if _, exists := ips[ip]; !exists { - return false - } - delete(ips, ip) - if len(ips) == 0 { - delete(s.nodeInterfaceMap, nodeID) - } - return true -} - -// IsInRange reports if ip belongs to this subnet. -func (s *SubNetwork) IsInRange(ip netip.Addr) bool { - if s == nil || !ip.IsValid() || !s.network.IsValid() || !s.netmask.IsValid() { - return false - } - return InSameNetwork(ip.Unmap(), s.network, s.netmask) -} - -// HasDuplicatedAddress reports true when the same address exists under multiple entries. -func (s *SubNetwork) HasDuplicatedAddress() bool { - if s == nil { - return false - } - seen := make(map[netip.Addr]struct{}) - for _, addresses := range s.nodeInterfaceMap { - for addr := range addresses { - if _, ok := seen[addr]; ok { - return true - } - seen[addr] = struct{}{} - } - } - return false -} - -func (s *SubNetwork) clone() *SubNetwork { - if s == nil { - return nil - } - out := &SubNetwork{ - network: s.network, - netmask: s.netmask, - nodeInterfaceMap: make(map[int]map[netip.Addr]struct{}, len(s.nodeInterfaceMap)), - } - for nodeID, ips := range s.nodeInterfaceMap { - copySet := make(map[netip.Addr]struct{}, len(ips)) - for ip := range ips { - copySet[ip] = struct{}{} - } - out.nodeInterfaceMap[nodeID] = copySet - } - return out -} - -func (s *SubNetwork) key() string { - if s == nil { - return "" - } - return subnetKey(s.network, s.netmask) -} - -func subnetKey(network, netmask netip.Addr) string { - if !network.IsValid() || !netmask.IsValid() { - return "" - } - // Unmap IPv4-mapped IPv6 addresses so that ::ffff:10.0.0.0 and 10.0.0.0 - // produce the same key. - network = network.Unmap() - netmask = netmask.Unmap() - return network.String() + keySep + netmask.String() -} - -func sortedSubnetworkKeys(subnets map[string]*SubNetwork) []string { - keys := make([]string, 0, len(subnets)) - for key := range subnets { - keys = append(keys, key) - } - sort.Strings(keys) - return keys -} - -func compareAddr(a, b netip.Addr) int { - ab := addrBytes(a) - bb := addrBytes(b) - if len(ab) != len(bb) { - if len(ab) < len(bb) { - return -1 - } - return 1 - } - for i := range ab { - if ab[i] < bb[i] { - return -1 - } - if ab[i] > bb[i] { - return 1 - } - } - return 0 -} - -// IsPointToPointMask ports InetAddressUtils.isPointToPointMask(). -func IsPointToPointMask(mask netip.Addr) bool { - mask = mask.Unmap() - return mask == pointToPointMaskIPv4 || mask == pointToPointMaskIPv6 -} - -// IsLoopbackMask ports InetAddressUtils.isLoopbackMask(). -func IsLoopbackMask(mask netip.Addr) bool { - mask = mask.Unmap() - return mask == loopbackMaskIPv4 || mask == loopbackMaskIPv6 -} - -// InSameNetwork ports InetAddressUtils.inSameNetwork(). -func InSameNetwork(addr1, addr2, mask netip.Addr) bool { - addr1 = addr1.Unmap() - addr2 = addr2.Unmap() - mask = mask.Unmap() - if !addr1.IsValid() || !addr2.IsValid() || !mask.IsValid() { - return false - } - if addr1.Is4() != addr2.Is4() || addr1.Is4() != mask.Is4() { - return false - } - - ab := addrBytes(addr1) - bb := addrBytes(addr2) - mb := addrBytes(mask) - if len(ab) != len(bb) || len(ab) != len(mb) { - return false - } - for i := range ab { - if (ab[i] & mb[i]) != (bb[i] & mb[i]) { - return false - } - } - return true -} - -// NetworkAddress returns ip&mask for matching IP families. -func NetworkAddress(ip, mask netip.Addr) (netip.Addr, bool) { - ip = ip.Unmap() - mask = mask.Unmap() - if !ip.IsValid() || !mask.IsValid() || ip.Is4() != mask.Is4() { - return netip.Addr{}, false - } - ib := addrBytes(ip) - mb := addrBytes(mask) - if len(ib) != len(mb) { - return netip.Addr{}, false - } - out := make([]byte, len(ib)) - for i := range ib { - out[i] = ib[i] & mb[i] - } - addr, ok := netip.AddrFromSlice(out) - if !ok { - return netip.Addr{}, false - } - return addr.Unmap(), true -} - -// MaskToCIDRPrefix ports InetAddressUtils.convertInetAddressMaskToCidr(). -func MaskToCIDRPrefix(mask netip.Addr) (int, error) { - mask = mask.Unmap() - if !mask.IsValid() { - return 0, fmt.Errorf("invalid mask") - } - foundZero := false - cidr := 0 - for _, value := range addrBytes(mask) { - k := int(value) - if foundZero && k != 0 { - return 0, fmt.Errorf("invalid mask %q", mask) - } - switch k { - case 255: - cidr += 8 - case 254: - cidr += 7 - foundZero = true - case 252: - cidr += 6 - foundZero = true - case 248: - cidr += 5 - foundZero = true - case 240: - cidr += 4 - foundZero = true - case 224: - cidr += 3 - foundZero = true - case 192: - cidr += 2 - foundZero = true - case 128: - cidr += 1 - foundZero = true - case 0: - foundZero = true - default: - return 0, fmt.Errorf("invalid mask %q", mask) - } - } - return cidr, nil -} - -func addrBytes(addr netip.Addr) []byte { - if !addr.IsValid() { - return nil - } - if addr.Is4() { - a := addr.As4() - return a[:] - } - a := addr.As16() - return a[:] -} diff --git a/src/go/pkg/l2topology/node_topology_test.go b/src/go/pkg/l2topology/node_topology_test.go deleted file mode 100644 index 0167a1b167b205..00000000000000 --- a/src/go/pkg/l2topology/node_topology_test.go +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import ( - "net/netip" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestSubNetworkLifecycleAndHelpers(t *testing.T) { - subnet, err := NewSubNetwork(2, netip.MustParseAddr("192.0.2.2"), netip.MustParseAddr("255.255.255.252")) - require.NoError(t, err) - require.Equal(t, netip.MustParseAddr("192.0.2.0"), subnet.Network()) - require.Equal(t, netip.MustParseAddr("255.255.255.252"), subnet.Netmask()) - require.Equal(t, "192.0.2.0/30", subnet.CIDR()) - require.Equal(t, 30, subnet.NetworkPrefix()) - require.True(t, subnet.IsIPv4Subnetwork()) - require.Equal(t, []int{2}, subnet.NodeIDs()) - - require.True(t, subnet.IsInRange(netip.MustParseAddr("192.0.2.1"))) - require.False(t, subnet.IsInRange(netip.MustParseAddr("192.0.2.8"))) - - require.True(t, subnet.Add(1, netip.MustParseAddr("192.0.2.1"))) - require.False(t, subnet.Add(1, netip.MustParseAddr("192.0.2.1"))) - require.False(t, subnet.Add(3, netip.MustParseAddr("192.0.2.8"))) - require.Equal(t, []int{1, 2}, subnet.NodeIDs()) - - require.True(t, subnet.Add(3, netip.MustParseAddr("192.0.2.1"))) - require.True(t, subnet.HasDuplicatedAddress()) - require.True(t, subnet.Remove(3, netip.MustParseAddr("192.0.2.1"))) - require.False(t, subnet.HasDuplicatedAddress()) - require.False(t, subnet.Remove(3, netip.MustParseAddr("192.0.2.1"))) - - cloned := subnet.clone() - require.NotNil(t, cloned) - require.True(t, cloned.Remove(1, netip.MustParseAddr("192.0.2.1"))) - require.Equal(t, []int{1, 2}, subnet.NodeIDs()) - require.Equal(t, []int{2}, cloned.NodeIDs()) - - require.Equal(t, "192.0.2.0\x00255.255.255.252", subnet.key()) - require.Equal(t, "192.0.2.0\x00255.255.255.252", subnetKey(subnet.Network(), subnet.Netmask())) - require.Equal(t, []string{"a", "b"}, sortedSubnetworkKeys(map[string]*SubNetwork{ - "b": subnet, - "a": cloned, - })) - - require.Equal(t, -1, compareAddr(netip.MustParseAddr("10.0.0.1"), netip.MustParseAddr("10.0.0.2"))) - require.Equal(t, 1, compareAddr(netip.MustParseAddr("2001:db8::2"), netip.MustParseAddr("2001:db8::1"))) - require.Equal(t, 0, compareAddr(netip.MustParseAddr("10.0.0.1"), netip.MustParseAddr("10.0.0.1"))) - - require.True(t, IsPointToPointMask(netip.MustParseAddr("255.255.255.252"))) - require.True(t, IsPointToPointMask(netip.MustParseAddr("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe"))) - require.True(t, IsLoopbackMask(netip.MustParseAddr("255.255.255.255"))) - require.True(t, InSameNetwork( - netip.MustParseAddr("10.0.0.1"), - netip.MustParseAddr("10.0.0.2"), - netip.MustParseAddr("255.255.255.252"), - )) - require.False(t, InSameNetwork( - netip.MustParseAddr("10.0.0.1"), - netip.MustParseAddr("10.0.0.5"), - netip.MustParseAddr("255.255.255.252"), - )) - - network, ok := NetworkAddress(netip.MustParseAddr("10.0.0.2"), netip.MustParseAddr("255.255.255.252")) - require.True(t, ok) - require.Equal(t, netip.MustParseAddr("10.0.0.0"), network) - - prefix, err := MaskToCIDRPrefix(netip.MustParseAddr("255.255.255.252")) - require.NoError(t, err) - require.Equal(t, 30, prefix) - _, err = MaskToCIDRPrefix(netip.MustParseAddr("255.0.255.0")) - require.Error(t, err) -} - -func TestNodeTopologyServiceAndRouterTopology(t *testing.T) { - service := NewNodeTopologyService( - []NodeTopologyEntity{ - {ID: 3, Label: "node-3", Address: netip.MustParseAddr("192.168.1.3")}, - {ID: 1, Label: "node-1", Address: netip.MustParseAddr("10.0.0.1")}, - {ID: 2, Label: "node-2", Address: netip.MustParseAddr("10.0.0.2")}, - }, - []IPInterfaceTopologyEntity{ - {ID: 5, NodeID: 2, IPAddress: netip.MustParseAddr("192.168.1.2"), NetMask: netip.MustParseAddr("255.255.255.0"), IsManaged: true, SnmpInterfaceID: 103}, - {ID: 2, NodeID: 1, IPAddress: netip.MustParseAddr("10.0.0.1"), NetMask: netip.MustParseAddr("255.255.255.252"), IsManaged: true, IsSnmpPrimary: true, SnmpInterfaceID: 101}, - {ID: 6, NodeID: 3, IPAddress: netip.MustParseAddr("127.0.0.1"), NetMask: netip.MustParseAddr("255.255.255.255"), IsManaged: true}, - {ID: 4, NodeID: 3, IPAddress: netip.MustParseAddr("192.168.1.3"), NetMask: netip.MustParseAddr("255.255.255.0"), IsManaged: true, IsSnmpPrimary: true, SnmpInterfaceID: 104}, - {ID: 3, NodeID: 2, IPAddress: netip.MustParseAddr("10.0.0.2"), NetMask: netip.MustParseAddr("255.255.255.252"), IsManaged: true, IsSnmpPrimary: true, SnmpInterfaceID: 102}, - {ID: 1, NodeID: 1, IPAddress: netip.MustParseAddr("10.10.10.1"), IsManaged: false}, - }, - []SnmpInterfaceTopologyEntity{ - {ID: 104, NodeID: 3, IfIndex: 4, IfName: "eth4"}, - {ID: 101, NodeID: 1, IfIndex: 1, IfName: "eth1"}, - {ID: 103, NodeID: 2, IfIndex: 3, IfName: "eth3"}, - {ID: 102, NodeID: 2, IfIndex: 2, IfName: "eth2"}, - }, - ) - - nodes := service.FindAllNode() - require.Equal(t, []int{1, 2, 3}, []int{nodes[0].ID, nodes[1].ID, nodes[2].ID}) - - ips := service.FindAllIP() - require.Equal(t, []int{1, 2, 3, 4, 5, 6}, []int{ips[0].ID, ips[1].ID, ips[2].ID, ips[3].ID, ips[4].ID, ips[5].ID}) - - snmp := service.FindAllSnmp() - require.Equal(t, []int{101, 102, 103, 104}, []int{snmp[0].ID, snmp[1].ID, snmp[2].ID, snmp[3].ID}) - - allSubnets := service.FindAllSubNetwork() - require.Len(t, allSubnets, 3) - require.Equal(t, []string{"10.0.0.0/30", "127.0.0.1/32", "192.168.1.0/24"}, []string{ - allSubnets[0].CIDR(), - allSubnets[1].CIDR(), - allSubnets[2].CIDR(), - }) - - legal := service.FindAllLegalSubNetwork() - require.Len(t, legal, 2) - require.Equal(t, []string{"10.0.0.0/30", "192.168.1.0/24"}, []string{legal[0].CIDR(), legal[1].CIDR()}) - - ptp := service.FindAllPointToPointSubNetwork() - require.Len(t, ptp, 1) - require.Equal(t, "10.0.0.0/30", ptp[0].CIDR()) - - legalPTP := service.FindAllLegalPointToPointSubNetwork() - require.Len(t, legalPTP, 1) - require.Equal(t, []int{1, 2}, legalPTP[0].NodeIDs()) - - loopbacks := service.FindAllLoopbacks() - require.Len(t, loopbacks, 1) - require.Equal(t, "127.0.0.1/32", loopbacks[0].CIDR()) - - legalLoopbacks := service.FindAllLegalLoopbacks() - require.Len(t, legalLoopbacks, 1) - require.Equal(t, []int{3}, legalLoopbacks[0].NodeIDs()) - - multibet := service.FindSubNetworkByNetworkPrefixLessThen(30, 126) - require.Len(t, multibet, 1) - require.Equal(t, "192.168.1.0/24", multibet[0].CIDR()) - - require.Equal(t, map[int]int{ - 1: 0, - 2: 0, - 3: 1, - }, service.GetNodeIDPriorityMap()) - - router := BuildNetworkRouterTopology(service, 30, 126) - require.Equal(t, "1", router.DefaultVertex) - require.Len(t, router.Vertices, 4) - require.Len(t, router.Edges, 3) - - require.Equal(t, []string{"1", "192.168.1.0/24", "2", "3"}, []string{ - router.Vertices[0].ID, - router.Vertices[1].ID, - router.Vertices[2].ID, - router.Vertices[3].ID, - }) - - require.Equal(t, []string{ - "1\x0010.0.0.1->2\x0010.0.0.2", - "192.168.1.0/24\x00192.168.1.0/24to:192.168.1.2->2\x00192.168.1.2", - "192.168.1.0/24\x00192.168.1.0/24to:192.168.1.3->3\x00192.168.1.3", - }, []string{ - router.Edges[0].ID, - router.Edges[1].ID, - router.Edges[2].ID, - }) - require.Equal(t, "1", router.Edges[0].SourcePort.Vertex) - require.Equal(t, "2", router.Edges[0].TargetPort.Vertex) - require.Equal(t, "eth1", router.Edges[0].SourcePort.IfName) - require.Equal(t, "eth2", router.Edges[0].TargetPort.IfName) - require.Equal(t, "192.168.1.0/24", router.Edges[1].SourcePort.Vertex) - require.Equal(t, "2", router.Edges[1].TargetPort.Vertex) - require.Equal(t, "", router.Edges[1].SourcePort.IfName) - require.Equal(t, "eth3", router.Edges[1].TargetPort.IfName) - require.Equal(t, "192.168.1.0/24", router.Edges[2].SourcePort.Vertex) - require.Equal(t, "3", router.Edges[2].TargetPort.Vertex) -} diff --git a/src/go/pkg/l2topology/noop.go b/src/go/pkg/l2topology/noop.go deleted file mode 100644 index 684cf95c74958d..00000000000000 --- a/src/go/pkg/l2topology/noop.go +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import "context" - -// NoopEngine is a temporary placeholder that satisfies Engine. -type NoopEngine struct{} - -func (NoopEngine) DiscoverByCIDRs(context.Context, CIDRRequest) (Result, error) { - return Result{}, ErrNotImplemented -} - -func (NoopEngine) DiscoverByDevices(context.Context, DeviceRequest) (Result, error) { - return Result{}, ErrNotImplemented -} diff --git a/src/go/pkg/l2topology/parity/node_topology_parity_test.go b/src/go/pkg/l2topology/parity/node_topology_parity_test.go deleted file mode 100644 index 15d788c8e7fb28..00000000000000 --- a/src/go/pkg/l2topology/parity/node_topology_parity_test.go +++ /dev/null @@ -1,345 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -//go:build snmp_topology_fixtures - -package parity - -import ( - "encoding/json" - "net/netip" - "os" - "path/filepath" - "sort" - "strings" - "testing" - - "github.com/netdata/netdata/go/plugins/pkg/l2topology" - "github.com/stretchr/testify/require" -) - -type nodeTopologyFixtureDocument struct { - SchemaVersion int `json:"schema_version"` - Scenarios map[string]nodeTopologyScenario `json:"scenarios"` -} - -type nodeTopologyScenario struct { - BuilderClass string `json:"builder_class"` - Getters []string `json:"getters"` - Nodes []nodeTopologyFixtureNode `json:"nodes"` - IPs []nodeTopologyFixtureIP `json:"ips"` - SNMP []nodeTopologyFixtureSNMP `json:"snmp"` - Expect nodeTopologyFixtureExpectations `json:"expect"` -} - -type nodeTopologyFixtureNode struct { - ID int `json:"id"` - Label string `json:"label"` - SysObject string `json:"sys_object"` - SysName string `json:"sys_name"` - Address string `json:"address"` -} - -type nodeTopologyFixtureIP struct { - ID int `json:"id"` - NodeID int `json:"node_id"` - IPAddress string `json:"ip_address"` - Netmask string `json:"netmask"` - IsManaged bool `json:"is_managed"` - IsSnmpPrimary bool `json:"is_snmp_primary"` - IfIndex int `json:"if_index"` - SnmpInterfaceID int `json:"snmp_interface_id"` -} - -type nodeTopologyFixtureSNMP struct { - ID int `json:"id"` - NodeID int `json:"node_id"` - IfIndex int `json:"if_index"` - IfName string `json:"if_name"` - IfDescr string `json:"if_descr"` -} - -type nodeTopologyFixtureExpectations struct { - Nodes int `json:"nodes"` - IPs *int `json:"ips"` - Subnets *int `json:"subnets"` - LegalSubnets *int `json:"legal_subnets"` - PTPSubnets *int `json:"ptp_subnets"` - LegalPTPSubnets *int `json:"legal_ptp_subnets"` - Loopbacks *int `json:"loopbacks"` - LegalLoopbacks *int `json:"legal_loopbacks"` - MultiBet *int `json:"multibet"` - PrioritySize *int `json:"priority_size"` - PriorityRule nodeTopologyPriorityRule `json:"priority_rule"` - NLinksFinal *int `json:"nlinks_final"` - TopologyVertices *int `json:"topology_vertices"` - TopologyEdges *int `json:"topology_edges"` - CIDRNodeSizes []nodeTopologyCIDRExpectation `json:"cidr_node_sizes"` -} - -type nodeTopologyPriorityRule struct { - Kind string `json:"kind"` - Value *int `json:"value"` -} - -type nodeTopologyCIDRExpectation struct { - CIDR string `json:"cidr"` - NodeIDs int `json:"node_ids"` -} - -func loadNodeTopologyFixtureDocument(t *testing.T) nodeTopologyFixtureDocument { - t.Helper() - path := filepath.Join("../../../testdata/snmp/enlinkd", "node_topology", "scenarios.json") - data, err := os.ReadFile(path) - require.NoError(t, err) - - var doc nodeTopologyFixtureDocument - require.NoError(t, json.Unmarshal(data, &doc)) - require.Equal(t, 1, doc.SchemaVersion) - require.NotEmpty(t, doc.Scenarios) - return doc -} - -func scenarioToService(t *testing.T, scenario nodeTopologyScenario) *l2topology.NodeTopologyService { - t.Helper() - - nodes := make([]l2topology.NodeTopologyEntity, 0, len(scenario.Nodes)) - for _, node := range scenario.Nodes { - entry := l2topology.NodeTopologyEntity{ - ID: node.ID, - Label: node.Label, - SysObject: node.SysObject, - SysName: node.SysName, - } - if ip, ok := parseOptionalAddr(node.Address); ok { - entry.Address = ip - } - nodes = append(nodes, entry) - } - - ips := make([]l2topology.IPInterfaceTopologyEntity, 0, len(scenario.IPs)) - for _, ip := range scenario.IPs { - entry := l2topology.IPInterfaceTopologyEntity{ - ID: ip.ID, - NodeID: ip.NodeID, - IsManaged: ip.IsManaged, - IsSnmpPrimary: ip.IsSnmpPrimary, - IfIndex: ip.IfIndex, - SnmpInterfaceID: ip.SnmpInterfaceID, - } - parsedIP, ok := parseOptionalAddr(ip.IPAddress) - require.Truef(t, ok, "invalid fixture IP address %q", ip.IPAddress) - entry.IPAddress = parsedIP - if mask, ok := parseOptionalAddr(ip.Netmask); ok { - entry.NetMask = mask - } - ips = append(ips, entry) - } - - snmp := make([]l2topology.SnmpInterfaceTopologyEntity, 0, len(scenario.SNMP)) - for _, iface := range scenario.SNMP { - snmp = append(snmp, l2topology.SnmpInterfaceTopologyEntity{ - ID: iface.ID, - NodeID: iface.NodeID, - IfIndex: iface.IfIndex, - IfName: iface.IfName, - IfDescr: iface.IfDescr, - }) - } - - return l2topology.NewNodeTopologyService(nodes, ips, snmp) -} - -func parseOptionalAddr(value string) (netip.Addr, bool) { - value = stringsTrimSpace(value) - if value == "" { - return netip.Addr{}, false - } - addr, err := netip.ParseAddr(value) - if err != nil { - return netip.Addr{}, false - } - return addr.Unmap(), true -} - -func runNodeTopologyScenarioParity(t *testing.T, scenarioName string) { - t.Helper() - doc := loadNodeTopologyFixtureDocument(t) - scenario, ok := doc.Scenarios[scenarioName] - require.Truef(t, ok, "missing scenario %q", scenarioName) - - service := scenarioToService(t, scenario) - expect := scenario.Expect - - require.Len(t, service.FindAllNode(), expect.Nodes) - if expect.IPs != nil { - require.Len(t, service.FindAllIP(), *expect.IPs) - } - - subnets := service.FindAllSubNetwork() - if expect.Subnets != nil { - require.Len(t, subnets, *expect.Subnets) - } - - legalSubnets := service.FindAllLegalSubNetwork() - if expect.LegalSubnets != nil { - require.Len(t, legalSubnets, *expect.LegalSubnets) - } - - ptpSubnets := service.FindAllPointToPointSubNetwork() - if expect.PTPSubnets != nil { - require.Len(t, ptpSubnets, *expect.PTPSubnets) - } - - legalPTPSubnets := service.FindAllLegalPointToPointSubNetwork() - if expect.LegalPTPSubnets != nil { - require.Len(t, legalPTPSubnets, *expect.LegalPTPSubnets) - } - - loopbacks := service.FindAllLoopbacks() - if expect.Loopbacks != nil { - require.Len(t, loopbacks, *expect.Loopbacks) - } - - legalLoopbacks := service.FindAllLegalLoopbacks() - if expect.LegalLoopbacks != nil { - require.Len(t, legalLoopbacks, *expect.LegalLoopbacks) - } - - multibet := service.FindSubNetworkByNetworkPrefixLessThen(30, 126) - if expect.MultiBet != nil { - require.Len(t, multibet, *expect.MultiBet) - } - - priority := service.GetNodeIDPriorityMap() - if expect.PrioritySize != nil { - require.Len(t, priority, *expect.PrioritySize) - } - switch expect.PriorityRule.Kind { - case "all_zero": - for _, value := range priority { - require.Equal(t, 0, value) - } - case "lt": - require.NotNil(t, expect.PriorityRule.Value) - for _, value := range priority { - require.Less(t, value, *expect.PriorityRule.Value) - } - } - - if len(expect.CIDRNodeSizes) > 0 { - actual := make(map[string]int) - for _, subnet := range legalSubnets { - actual[subnet.CIDR()] = len(subnet.NodeIDs()) - } - for _, expectedCIDR := range expect.CIDRNodeSizes { - require.Equalf(t, expectedCIDR.NodeIDs, actual[expectedCIDR.CIDR], "cidr %s", expectedCIDR.CIDR) - } - } - - topology := l2topology.BuildNetworkRouterTopology(service, 30, 126) - if expect.TopologyVertices != nil { - require.Len(t, topology.Vertices, *expect.TopologyVertices) - } else if expect.MultiBet != nil { - require.Len(t, topology.Vertices, expect.Nodes+*expect.MultiBet) - } - - if expect.TopologyEdges != nil { - require.Len(t, topology.Edges, *expect.TopologyEdges) - } else if expect.NLinksFinal != nil { - require.Len(t, topology.Edges, *expect.NLinksFinal) - } -} - -func TestNodeTopologyServiceIT_nms0001SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms0001SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms0002SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms0002SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms003SubnetworkTests(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms003SubnetworkTests") -} -func TestNodeTopologyServiceIT_nms007SubnetworkTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms007SubnetworkTest") -} -func TestNodeTopologyServiceIT_nms101SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms101SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms102SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms102SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms0123SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms0123SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms1055SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms1055SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms4005SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms4005SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms4930SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms4930SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms6802SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms6802SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms7467SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms7467SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms7563SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms7563SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms7777DWSubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms7777DWSubnetworksTest") -} -func TestNodeTopologyServiceIT_nms7918SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms7918SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms8000SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms8000SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms10205aSubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms10205aSubnetworksTest") -} -func TestNodeTopologyServiceIT_nms10205bSubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms10205bSubnetworksTest") -} -func TestNodeTopologyServiceIT_nms13593SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms13593SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms13637SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms13637SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms13923SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms13923SubnetworksTest") -} -func TestNodeTopologyServiceIT_nms17216SubnetworksTest(t *testing.T) { - runNodeTopologyScenarioParity(t, "nms17216SubnetworksTest") -} - -func TestNms0001EnIT_testLinkdNetworkTopologyUpdater(t *testing.T) { - doc := loadNodeTopologyFixtureDocument(t) - scenario, ok := doc.Scenarios["nms0001EnIT_testLinkdNetworkTopologyUpdater"] - require.True(t, ok) - - service := scenarioToService(t, scenario) - topology := l2topology.BuildNetworkRouterTopology(service, 30, 126) - require.NotEmpty(t, topology.Vertices) - require.Len(t, topology.Vertices, *scenario.Expect.TopologyVertices) - require.Len(t, topology.Edges, *scenario.Expect.TopologyEdges) -} - -func TestNodeTopologyFixtureCoverage(t *testing.T) { - doc := loadNodeTopologyFixtureDocument(t) - scenarios := make([]string, 0, len(doc.Scenarios)) - for name := range doc.Scenarios { - scenarios = append(scenarios, name) - } - sort.Strings(scenarios) - - require.Contains(t, scenarios, "nms0001SubnetworksTest") - require.Contains(t, scenarios, "nms17216SubnetworksTest") - require.Contains(t, scenarios, "nms0001EnIT_testLinkdNetworkTopologyUpdater") -} - -func stringsTrimSpace(value string) string { return strings.TrimSpace(value) } diff --git a/src/go/pkg/l2topology/runtime.go b/src/go/pkg/l2topology/runtime.go deleted file mode 100644 index 352540e7594328..00000000000000 --- a/src/go/pkg/l2topology/runtime.go +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import ( - "context" - "fmt" - "time" -) - -// ObservationProvider gathers normalized L2 observations for discovery requests. -type ObservationProvider interface { - ObserveByCIDRs(ctx context.Context, req CIDRRequest) ([]L2Observation, error) - ObserveByDevices(ctx context.Context, req DeviceRequest) ([]L2Observation, error) -} - -// RuntimeEngine executes discovery using a concrete observation provider. -type RuntimeEngine struct { - provider ObservationProvider -} - -// NewRuntimeEngine constructs a concrete engine backed by the given provider. -func NewRuntimeEngine(provider ObservationProvider) (*RuntimeEngine, error) { - if provider == nil { - return nil, fmt.Errorf("%w: observation provider is required", ErrInvalidRequest) - } - return &RuntimeEngine{provider: provider}, nil -} - -func (e *RuntimeEngine) DiscoverByCIDRs(ctx context.Context, req CIDRRequest) (Result, error) { - if e == nil || e.provider == nil { - return Result{}, fmt.Errorf("%w: observation provider is not configured", ErrInvalidRequest) - } - if err := validateCIDRRequest(req); err != nil { - return Result{}, err - } - - req.Options = ensureCollectedAt(req.Options) - - observations, err := e.provider.ObserveByCIDRs(ctx, req) - if err != nil { - return Result{}, fmt.Errorf("observe cidr discovery request: %w", err) - } - if len(observations) == 0 { - return emptyResult(req.Options.CollectedAt), nil - } - - result, err := BuildL2ResultFromObservations(observations, req.Options) - if err != nil { - return Result{}, fmt.Errorf("build l2 result from cidr discovery request: %w", err) - } - return result, nil -} - -func (e *RuntimeEngine) DiscoverByDevices(ctx context.Context, req DeviceRequest) (Result, error) { - if e == nil || e.provider == nil { - return Result{}, fmt.Errorf("%w: observation provider is not configured", ErrInvalidRequest) - } - if err := validateDeviceRequest(req); err != nil { - return Result{}, err - } - - req.Options = ensureCollectedAt(req.Options) - - observations, err := e.provider.ObserveByDevices(ctx, req) - if err != nil { - return Result{}, fmt.Errorf("observe device discovery request: %w", err) - } - if len(observations) == 0 { - return emptyResult(req.Options.CollectedAt), nil - } - - result, err := BuildL2ResultFromObservations(observations, req.Options) - if err != nil { - return Result{}, fmt.Errorf("build l2 result from device discovery request: %w", err) - } - return result, nil -} - -func validateCIDRRequest(req CIDRRequest) error { - if len(req.CIDRs) == 0 { - return fmt.Errorf("%w: cidrs are required", ErrInvalidRequest) - } - for i := range req.CIDRs { - if !req.CIDRs[i].IsValid() { - return fmt.Errorf("%w: cidrs[%d] has invalid prefix", ErrInvalidRequest, i) - } - } - return nil -} - -func validateDeviceRequest(req DeviceRequest) error { - if len(req.Devices) == 0 { - return fmt.Errorf("%w: devices are required", ErrInvalidRequest) - } - for i := range req.Devices { - if !req.Devices[i].Address.IsValid() { - return fmt.Errorf("%w: devices[%d] has invalid address", ErrInvalidRequest, i) - } - } - return nil -} - -func ensureCollectedAt(opts DiscoverOptions) DiscoverOptions { - if opts.CollectedAt.IsZero() { - opts.CollectedAt = time.Now().UTC() - } else { - opts.CollectedAt = opts.CollectedAt.UTC() - } - return opts -} - -func emptyResult(collectedAt time.Time) Result { - if !collectedAt.IsZero() { - collectedAt = collectedAt.UTC() - } - - return Result{ - CollectedAt: collectedAt, - Stats: newL2ResultStats(), - } -} diff --git a/src/go/pkg/l2topology/runtime_test.go b/src/go/pkg/l2topology/runtime_test.go deleted file mode 100644 index f63fcfcf72abf4..00000000000000 --- a/src/go/pkg/l2topology/runtime_test.go +++ /dev/null @@ -1,256 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import ( - "context" - "errors" - "net/netip" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -type fakeObservationProvider struct { - cidrObs []L2Observation - deviceObs []L2Observation - cidrErr error - deviceErr error - cidrReqs []CIDRRequest - deviceReqs []DeviceRequest -} - -func (p *fakeObservationProvider) ObserveByCIDRs(_ context.Context, req CIDRRequest) ([]L2Observation, error) { - p.cidrReqs = append(p.cidrReqs, req) - if p.cidrErr != nil { - return nil, p.cidrErr - } - return p.cidrObs, nil -} - -func (p *fakeObservationProvider) ObserveByDevices(_ context.Context, req DeviceRequest) ([]L2Observation, error) { - p.deviceReqs = append(p.deviceReqs, req) - if p.deviceErr != nil { - return nil, p.deviceErr - } - return p.deviceObs, nil -} - -func TestNewRuntimeEngine_RequiresProvider(t *testing.T) { - _, err := NewRuntimeEngine(nil) - require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidRequest) -} - -func TestRuntimeEngine_DiscoverByDevices_BuildsResult(t *testing.T) { - provider := &fakeObservationProvider{ - deviceObs: []L2Observation{ - { - DeviceID: "switch-a", - Hostname: "switch-a.example.net", - ManagementIP: "10.0.0.1", - LLDPRemotes: []LLDPRemoteObservation{ - { - LocalPortNum: "8", - LocalPortID: "Gi0/0", - SysName: "switch-b.example.net", - PortID: "Gi0/1", - }, - }, - }, - { - DeviceID: "switch-b", - Hostname: "switch-b.example.net", - }, - }, - } - eng, err := NewRuntimeEngine(provider) - require.NoError(t, err) - - req := DeviceRequest{ - Devices: []DeviceTarget{{Address: netip.MustParseAddr("10.0.0.1")}}, - Options: DiscoverOptions{EnableLLDP: true}, - } - - result, err := eng.DiscoverByDevices(context.Background(), req) - require.NoError(t, err) - require.Len(t, provider.deviceReqs, 1) - require.Len(t, result.Adjacencies, 1) - require.Equal(t, "lldp", result.Adjacencies[0].Protocol) - require.Equal(t, "switch-a", result.Adjacencies[0].SourceID) - require.Equal(t, "switch-b", result.Adjacencies[0].TargetID) -} - -func TestRuntimeEngine_DiscoverByDevices_PropagatesCollectedAt(t *testing.T) { - sourceZone := time.FixedZone("UTC+02", 2*60*60) - collectedAt := time.Date(2026, time.April, 2, 3, 4, 5, 0, sourceZone) - expectedCollectedAt := collectedAt.UTC() - provider := &fakeObservationProvider{ - deviceObs: []L2Observation{ - { - DeviceID: "switch-a", - Hostname: "switch-a.example.net", - }, - }, - } - eng, err := NewRuntimeEngine(provider) - require.NoError(t, err) - - req := DeviceRequest{ - Devices: []DeviceTarget{{Address: netip.MustParseAddr("10.0.0.1")}}, - Options: DiscoverOptions{CollectedAt: collectedAt}, - } - - result, err := eng.DiscoverByDevices(context.Background(), req) - require.NoError(t, err) - require.Len(t, provider.deviceReqs, 1) - require.Equal(t, expectedCollectedAt, provider.deviceReqs[0].Options.CollectedAt) - require.Equal(t, expectedCollectedAt, result.CollectedAt) -} - -func TestRuntimeEngine_DiscoverByCIDRs_InvalidRequest(t *testing.T) { - eng, err := NewRuntimeEngine(&fakeObservationProvider{}) - require.NoError(t, err) - - _, err = eng.DiscoverByCIDRs(context.Background(), CIDRRequest{}) - require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidRequest) -} - -func TestRuntimeEngine_DiscoverByCIDRs_InvalidPrefix(t *testing.T) { - provider := &fakeObservationProvider{} - eng, err := NewRuntimeEngine(provider) - require.NoError(t, err) - - _, err = eng.DiscoverByCIDRs(context.Background(), CIDRRequest{ - CIDRs: []netip.Prefix{{}}, - }) - require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidRequest) - require.ErrorContains(t, err, "cidrs[0] has invalid prefix") - require.Empty(t, provider.cidrReqs) -} - -func TestRuntimeEngine_DiscoverByDevices_InvalidRequest(t *testing.T) { - eng, err := NewRuntimeEngine(&fakeObservationProvider{}) - require.NoError(t, err) - - _, err = eng.DiscoverByDevices(context.Background(), DeviceRequest{}) - require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidRequest) - require.ErrorContains(t, err, "devices are required") -} - -func TestRuntimeEngine_DiscoverByDevices_InvalidAddress(t *testing.T) { - eng, err := NewRuntimeEngine(&fakeObservationProvider{}) - require.NoError(t, err) - - _, err = eng.DiscoverByDevices(context.Background(), DeviceRequest{ - Devices: []DeviceTarget{{Address: netip.Addr{}}}, - }) - require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidRequest) -} - -func TestRuntimeEngine_DiscoverByCIDRs_ProviderError(t *testing.T) { - providerErr := errors.New("provider failed") - provider := &fakeObservationProvider{cidrErr: providerErr} - eng, err := NewRuntimeEngine(provider) - require.NoError(t, err) - - _, err = eng.DiscoverByCIDRs(context.Background(), CIDRRequest{ - CIDRs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")}, - }) - require.Error(t, err) - require.ErrorIs(t, err, providerErr) -} - -func TestRuntimeEngine_EmptyObservationsReturnEmptyResult(t *testing.T) { - provider := &fakeObservationProvider{} - eng, err := NewRuntimeEngine(provider) - require.NoError(t, err) - - sourceZone := time.FixedZone("UTC-03", -3*60*60) - collectedAt := time.Date(2026, time.April, 2, 3, 4, 5, 0, sourceZone) - expectedCollectedAt := collectedAt.UTC() - - result, err := eng.DiscoverByCIDRs(context.Background(), CIDRRequest{ - CIDRs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")}, - Options: DiscoverOptions{CollectedAt: collectedAt}, - }) - require.NoError(t, err) - require.Len(t, provider.cidrReqs, 1) - require.Equal(t, expectedCollectedAt, provider.cidrReqs[0].Options.CollectedAt) - require.Equal(t, expectedCollectedAt, result.CollectedAt) - require.Empty(t, result.Devices) - require.Empty(t, result.Adjacencies) - require.Equal(t, 0, result.Stats["links_total"]) - require.Equal(t, 0, result.Stats["identity_alias_endpoints_mapped"]) - require.Equal(t, 0, result.Stats["identity_alias_endpoints_ambiguous_mac"]) - require.Equal(t, 0, result.Stats["identity_alias_ips_merged"]) - require.Equal(t, 0, result.Stats["identity_alias_ips_conflict_skipped"]) -} - -func TestRuntimeEngine_EmptyResultStatsSchemaMatchesPipelineResult(t *testing.T) { - emptyStats := emptyResult(time.Time{}).Stats - pipelineResult, err := BuildL2ResultFromObservations([]L2Observation{{DeviceID: "switch-a"}}, DiscoverOptions{}) - require.NoError(t, err) - - require.ElementsMatch(t, statsKeys(pipelineResult.Stats), statsKeys(emptyStats)) -} - -func TestRuntimeEngine_DiscoverByDevices_NilReceiver(t *testing.T) { - var eng *RuntimeEngine - _, err := eng.DiscoverByDevices(context.Background(), DeviceRequest{ - Devices: []DeviceTarget{{Address: netip.MustParseAddr("10.0.0.1")}}, - }) - require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidRequest) -} - -func TestRuntimeEngine_DiscoverByDevices_NilProvider(t *testing.T) { - eng := &RuntimeEngine{} - _, err := eng.DiscoverByDevices(context.Background(), DeviceRequest{ - Devices: []DeviceTarget{{Address: netip.MustParseAddr("10.0.0.1")}}, - }) - require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidRequest) -} - -func statsKeys(stats map[string]any) []string { - keys := make([]string, 0, len(stats)) - for key := range stats { - keys = append(keys, key) - } - return keys -} - -func TestRuntimeEngine_DiscoverByCIDRs_NilReceiver(t *testing.T) { - var eng *RuntimeEngine - _, err := eng.DiscoverByCIDRs(context.Background(), CIDRRequest{ - CIDRs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")}, - }) - require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidRequest) -} - -func TestRuntimeEngine_DiscoverByCIDRs_NilProvider(t *testing.T) { - eng := &RuntimeEngine{} - _, err := eng.DiscoverByCIDRs(context.Background(), CIDRRequest{ - CIDRs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")}, - }) - require.Error(t, err) - require.ErrorIs(t, err, ErrInvalidRequest) -} - -func TestEnsureCollectedAt_DefaultsZeroToUTCNow(t *testing.T) { - before := time.Now().UTC() - opts := ensureCollectedAt(DiscoverOptions{}) - after := time.Now().UTC() - - require.False(t, opts.CollectedAt.IsZero()) - require.Equal(t, time.UTC, opts.CollectedAt.Location()) - require.False(t, opts.CollectedAt.Before(before)) - require.False(t, opts.CollectedAt.After(after)) -} diff --git a/src/go/pkg/l2topology/topology_adapter.go b/src/go/pkg/l2topology/topology_adapter.go index e9574ee145a73a..3808b80a70096a 100644 --- a/src/go/pkg/l2topology/topology_adapter.go +++ b/src/go/pkg/l2topology/topology_adapter.go @@ -6,6 +6,8 @@ import ( "net/netip" "strings" "time" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) // GraphOptions controls conversion from Result to the internal graph projection. @@ -54,8 +56,8 @@ type endpointActorAccumulator struct { } type projectedSegments struct { - actors []Actor - links []Link + actors []graph.Actor + links []graph.Link linksFdb int bidirectionalCount int endpointLinksCandidates int @@ -220,7 +222,7 @@ func topologyInferenceStrategyConfigFor(strategy string) topologyInferenceStrate } // ToGraph converts an engine result to the internal graph projection. -func ToGraph(result Result, opts GraphOptions) Graph { +func ToGraph(result Result, opts GraphOptions) graph.Graph { builder := newGraphBuilder(result, opts) builder.prepareIndexes() builder.collectBridgeTopologyInputs() diff --git a/src/go/pkg/l2topology/topology_adapter_actor_collapse.go b/src/go/pkg/l2topology/topology_adapter_actor_collapse.go index e86771b79b1fd0..194c08d9f95f72 100644 --- a/src/go/pkg/l2topology/topology_adapter_actor_collapse.go +++ b/src/go/pkg/l2topology/topology_adapter_actor_collapse.go @@ -5,9 +5,11 @@ package l2topology import ( "sort" "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) -func collapseActorsByIP(actors []Actor) []Actor { +func collapseActorsByIP(actors []graph.Actor) []graph.Actor { if len(actors) <= 1 { return actors } @@ -96,7 +98,7 @@ func collapseActorsByIP(actors []Actor) []Actor { actors[rep] = merged } - out := make([]Actor, 0, len(actors)) + out := make([]graph.Actor, 0, len(actors)) for idx, actor := range actors { if !keep[idx] { continue @@ -106,12 +108,12 @@ func collapseActorsByIP(actors []Actor) []Actor { return out } -func eliminateNonIPInferredActors(actors []Actor, links []Link) ([]Actor, []Link) { +func eliminateNonIPInferredActors(actors []graph.Actor, links []graph.Link) ([]graph.Actor, []graph.Link) { if len(actors) == 0 { return actors, links } removedIdentityKeys := make(map[string]struct{}) - filteredActors := make([]Actor, 0, len(actors)) + filteredActors := make([]graph.Actor, 0, len(actors)) for _, actor := range actors { if topologyActorIsInferred(actor) && len(normalizedTopologyActorIPs(actor)) == 0 { for _, key := range topologyMatchIdentityKeys(actor.Match) { @@ -125,7 +127,7 @@ func eliminateNonIPInferredActors(actors []Actor, links []Link) ([]Actor, []Link return actors, links } - filteredLinks := make([]Link, 0, len(links)) + filteredLinks := make([]graph.Link, 0, len(links)) for _, link := range links { srcKeys := topologyMatchIdentityKeys(link.Src.Match) dstKeys := topologyMatchIdentityKeys(link.Dst.Match) @@ -152,7 +154,7 @@ func topologyIdentityKeysOverlap(keys []string, set map[string]struct{}) bool { return false } -func normalizedTopologyActorIPs(actor Actor) []string { +func normalizedTopologyActorIPs(actor graph.Actor) []string { if len(actor.Match.IPAddresses) == 0 { return nil } @@ -173,7 +175,7 @@ func normalizedTopologyActorIPs(actor Actor) []string { return out } -func compareTopologyActorCollapsePriority(left, right Actor) int { +func compareTopologyActorCollapsePriority(left, right graph.Actor) int { leftDevice := IsDeviceActorType(left.ActorType) rightDevice := IsDeviceActorType(right.ActorType) if leftDevice != rightDevice { @@ -195,7 +197,7 @@ func compareTopologyActorCollapsePriority(left, right Actor) int { return strings.Compare(leftKey, rightKey) } -func mergeTopologyActorMatch(base, other Match) Match { +func mergeTopologyActorMatch(base, other graph.Match) graph.Match { base.ChassisIDs = mergeTopologyStringLists(base.ChassisIDs, other.ChassisIDs) base.MacAddresses = mergeTopologyStringLists(base.MacAddresses, other.MacAddresses) base.IPAddresses = mergeTopologyStringLists(base.IPAddresses, other.IPAddresses) @@ -272,7 +274,7 @@ func mergeTopologyActorAttributes(base, extra map[string]any) map[string]any { return base } -func topologyActorIsInferred(actor Actor) bool { +func topologyActorIsInferred(actor graph.Actor) bool { if strings.EqualFold(strings.TrimSpace(actor.ActorType), "endpoint") { return true } diff --git a/src/go/pkg/l2topology/topology_adapter_builder.go b/src/go/pkg/l2topology/topology_adapter_builder.go index c9c132ea9652ee..f74e1f57465456 100644 --- a/src/go/pkg/l2topology/topology_adapter_builder.go +++ b/src/go/pkg/l2topology/topology_adapter_builder.go @@ -5,6 +5,8 @@ package l2topology import ( "strings" "time" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) type graphBuilder struct { @@ -26,7 +28,7 @@ type graphBuilder struct { reporterAliases map[string][]string ifaceSummaryByDevice map[string]topologyDeviceInterfaceSummary - actors []Actor + actors []graph.Actor actorIndex map[string]struct{} actorMACIndex map[string]struct{} @@ -34,7 +36,7 @@ type graphBuilder struct { endpointActors builtEndpointActors segmentProjection projectedSegments - links []Link + links []graph.Link segmentSuppressed int unlinkedSuppressed int linkCounts topologyLinkCounts @@ -130,7 +132,7 @@ func (b *graphBuilder) collectBridgeTopologyInputs() { } func (b *graphBuilder) buildDeviceActors() { - b.actors = make([]Actor, 0, len(b.result.Devices)) + b.actors = make([]graph.Actor, 0, len(b.result.Devices)) b.actorIndex = make(map[string]struct{}, len(b.result.Devices)*2) b.actorMACIndex = make(map[string]struct{}, len(b.result.Devices)) @@ -215,7 +217,7 @@ func (b *graphBuilder) buildSegmentTopology() { func (b *graphBuilder) finalizeGraph() { sortTopologyActors(b.actors) - b.links = make([]Link, 0, len(b.projectedAdjacencies.links)+len(b.segmentProjection.links)) + b.links = make([]graph.Link, 0, len(b.projectedAdjacencies.links)+len(b.segmentProjection.links)) b.links = append(b.links, b.projectedAdjacencies.links...) b.links = append(b.links, b.segmentProjection.links...) sortTopologyLinks(b.links) @@ -283,8 +285,8 @@ func (b *graphBuilder) buildStats() { b.stats["inference_strategy"] = b.strategyConfig.id } -func (b *graphBuilder) graph() Graph { - return Graph{ +func (b *graphBuilder) graph() graph.Graph { + return graph.Graph{ SchemaVersion: b.schemaVersion, Source: b.source, Layer: b.layer, diff --git a/src/go/pkg/l2topology/topology_adapter_device_actor.go b/src/go/pkg/l2topology/topology_adapter_device_actor.go index 2974870f423cf6..9b39669084c6c0 100644 --- a/src/go/pkg/l2topology/topology_adapter_device_actor.go +++ b/src/go/pkg/l2topology/topology_adapter_device_actor.go @@ -4,6 +4,8 @@ package l2topology import ( "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) func topologyDeviceInferred(dev Device) bool { @@ -18,8 +20,8 @@ func topologyDeviceInferred(dev Device) bool { } } -func buildDeviceActorMatch(dev Device, reporterAliases []string) Match { - match := Match{ +func buildDeviceActorMatch(dev Device, reporterAliases []string) graph.Match { + match := graph.Match{ SysObjectID: strings.TrimSpace(dev.SysObject), SysName: strings.TrimSpace(dev.Hostname), } @@ -69,7 +71,7 @@ func buildDeviceActorAttributes( dev Device, localDeviceID string, ifaceSummary topologyDeviceInterfaceSummary, - match Match, + match graph.Match, ) map[string]any { discovered := strings.TrimSpace(localDeviceID) == "" || dev.ID != localDeviceID diff --git a/src/go/pkg/l2topology/topology_adapter_device_endpoint.go b/src/go/pkg/l2topology/topology_adapter_device_endpoint.go index e6ea37d208c8fb..e7891cb681d2a0 100644 --- a/src/go/pkg/l2topology/topology_adapter_device_endpoint.go +++ b/src/go/pkg/l2topology/topology_adapter_device_endpoint.go @@ -5,9 +5,11 @@ package l2topology import ( "strconv" "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) -func adjacencySideToEndpoint(dev Device, port string, ifIndexByDeviceName map[string]int, ifaceByDeviceIndex map[string]Interface) LinkEndpoint { +func adjacencySideToEndpoint(dev Device, port string, ifIndexByDeviceName map[string]int, ifaceByDeviceIndex map[string]Interface) graph.LinkEndpoint { match := buildDeviceActorMatch(dev, nil) port = strings.TrimSpace(port) @@ -56,7 +58,7 @@ func adjacencySideToEndpoint(dev Device, port string, ifIndexByDeviceName map[st } } - return LinkEndpoint{ + return graph.LinkEndpoint{ Match: match, Attributes: pruneTopologyAttributes(attrs), } diff --git a/src/go/pkg/l2topology/topology_adapter_devices.go b/src/go/pkg/l2topology/topology_adapter_devices.go index e1f6eb0d5b5454..ed157aecf4ed04 100644 --- a/src/go/pkg/l2topology/topology_adapter_devices.go +++ b/src/go/pkg/l2topology/topology_adapter_devices.go @@ -2,17 +2,19 @@ package l2topology +import "github.com/netdata/netdata/go/plugins/pkg/topology/graph" + func deviceToTopologyActor( dev Device, source, layer, localDeviceID string, ifaceSummary topologyDeviceInterfaceSummary, reporterAliases []string, -) Actor { +) graph.Actor { match := buildDeviceActorMatch(dev, reporterAliases) attrs := buildDeviceActorAttributes(dev, localDeviceID, ifaceSummary, match) tables := buildDeviceActorTables(ifaceSummary) - return Actor{ + return graph.Actor{ ActorType: resolveDeviceActorType(dev.Labels), Layer: layer, Source: source, diff --git a/src/go/pkg/l2topology/topology_adapter_display.go b/src/go/pkg/l2topology/topology_adapter_display.go index 05b536b78f57ad..de5c09c0ca8a3d 100644 --- a/src/go/pkg/l2topology/topology_adapter_display.go +++ b/src/go/pkg/l2topology/topology_adapter_display.go @@ -4,6 +4,8 @@ package l2topology import ( "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) type topologyDisplayNameResolver struct { @@ -16,7 +18,7 @@ type topologyDisplayName struct { source string } -func applyTopologyDisplayNames(actors []Actor, links []Link, lookup func(ip string) string) { +func applyTopologyDisplayNames(actors []graph.Actor, links []graph.Link, lookup func(ip string) string) { resolver := topologyDisplayNameResolver{ lookup: lookup, cache: make(map[string]string), @@ -83,7 +85,7 @@ func applyTopologyDisplayNames(actors []Actor, links []Link, lookup func(ip stri } } -func topologySetActorDisplay(actor *Actor, display topologyDisplayName) { +func topologySetActorDisplay(actor *graph.Actor, display topologyDisplayName) { if actor == nil { return } @@ -108,7 +110,7 @@ func topologySetActorDisplay(actor *Actor, display topologyDisplayName) { actor.Attributes = pruneTopologyAttributes(attrs) } -func topologySetEndpointDisplayAndCanonicalPortName(endpoint *LinkEndpoint, display topologyDisplayName) string { +func topologySetEndpointDisplayAndCanonicalPortName(endpoint *graph.LinkEndpoint, display topologyDisplayName) string { if endpoint == nil { return "" } @@ -126,7 +128,7 @@ func topologySetEndpointDisplayAndCanonicalPortName(endpoint *LinkEndpoint, disp return name } -func topologyEndpointDisplayName(endpoint LinkEndpoint, actorDisplayByMatch map[string]string, resolver *topologyDisplayNameResolver) topologyDisplayName { +func topologyEndpointDisplayName(endpoint graph.LinkEndpoint, actorDisplayByMatch map[string]string, resolver *topologyDisplayNameResolver) topologyDisplayName { if key := canonicalTopologyMatchKey(endpoint.Match); key != "" { if name := strings.TrimSpace(actorDisplayByMatch[key]); name != "" { return topologyDisplayName{name: name, source: "actor"} @@ -135,7 +137,7 @@ func topologyEndpointDisplayName(endpoint LinkEndpoint, actorDisplayByMatch map[ return topologyDisplayNameFromMatch(endpoint.Match, resolver) } -func topologyActorDisplayName(actor Actor, deviceDisplayByID map[string]string, resolver *topologyDisplayNameResolver) topologyDisplayName { +func topologyActorDisplayName(actor graph.Actor, deviceDisplayByID map[string]string, resolver *topologyDisplayNameResolver) topologyDisplayName { if actor.ActorType == "segment" { if name := topologySegmentDisplayName(actor, deviceDisplayByID); name != "" { return topologyDisplayName{name: name, source: "segment"} @@ -153,7 +155,7 @@ func topologyActorDisplayName(actor Actor, deviceDisplayByID map[string]string, return topologyDisplayName{} } -func topologyFallbackActorDisplayName(actor Actor) topologyDisplayName { +func topologyFallbackActorDisplayName(actor graph.Actor) topologyDisplayName { if matchKey := canonicalTopologyMatchKey(actor.Match); matchKey != "" { return topologyDisplayName{name: matchKey, source: "fallback_match"} } @@ -167,11 +169,11 @@ func topologyFallbackActorDisplayName(actor Actor) topologyDisplayName { return topologyDisplayName{name: actorType + ":[unset]", source: "fallback"} } -func topologyActorDeviceID(actor Actor) string { +func topologyActorDeviceID(actor graph.Actor) string { return topologyAttrString(actor.Attributes, "device_id") } -func topologyDisplayNameFromMatch(match Match, resolver *topologyDisplayNameResolver) topologyDisplayName { +func topologyDisplayNameFromMatch(match graph.Match, resolver *topologyDisplayNameResolver) topologyDisplayName { if dns := topologyMatchPreferredDNSName(match, resolver); dns != "" { return topologyDisplayName{name: dns, source: "dns"} } @@ -190,7 +192,7 @@ func topologyDisplayNameFromMatch(match Match, resolver *topologyDisplayNameReso return topologyDisplayName{} } -func topologyMatchPreferredDNSName(match Match, resolver *topologyDisplayNameResolver) string { +func topologyMatchPreferredDNSName(match graph.Match, resolver *topologyDisplayNameResolver) string { candidates := make(map[string]struct{}) for _, value := range match.DNSNames { if normalized := normalizeDNSName(value); normalized != "" { @@ -214,11 +216,11 @@ func topologyMatchPreferredDNSName(match Match, resolver *topologyDisplayNameRes return names[0] } -func topologyMatchPreferredSysName(match Match) string { +func topologyMatchPreferredSysName(match graph.Match) string { return strings.TrimSpace(match.SysName) } -func topologyMatchPreferredHostname(match Match) string { +func topologyMatchPreferredHostname(match graph.Match) string { hostnames := uniqueTopologyStrings(match.Hostnames) if len(hostnames) == 0 { return "" @@ -226,7 +228,7 @@ func topologyMatchPreferredHostname(match Match) string { return hostnames[0] } -func topologyMatchPreferredIP(match Match) string { +func topologyMatchPreferredIP(match graph.Match) string { ips := make([]string, 0, len(match.IPAddresses)) for _, value := range match.IPAddresses { if ip := normalizeTopologyIP(value); ip != "" { @@ -240,7 +242,7 @@ func topologyMatchPreferredIP(match Match) string { return ips[0] } -func topologyMatchPreferredMAC(match Match) string { +func topologyMatchPreferredMAC(match graph.Match) string { macs := make([]string, 0, len(match.MacAddresses)+len(match.ChassisIDs)) for _, value := range match.MacAddresses { if mac := normalizeMAC(value); mac != "" { diff --git a/src/go/pkg/l2topology/topology_adapter_display_segment.go b/src/go/pkg/l2topology/topology_adapter_display_segment.go index a3df3b95bf382d..678b8eafca413f 100644 --- a/src/go/pkg/l2topology/topology_adapter_display_segment.go +++ b/src/go/pkg/l2topology/topology_adapter_display_segment.go @@ -4,6 +4,8 @@ package l2topology import ( "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) type topologySegmentPortRef struct { @@ -13,7 +15,7 @@ type topologySegmentPortRef struct { bridgePort string } -func topologySegmentDisplayName(actor Actor, deviceDisplayByID map[string]string) string { +func topologySegmentDisplayName(actor graph.Actor, deviceDisplayByID map[string]string) string { attrs := actor.Attributes if len(attrs) == 0 { return "" diff --git a/src/go/pkg/l2topology/topology_adapter_endpoints.go b/src/go/pkg/l2topology/topology_adapter_endpoints.go index 32a89b7912f348..01de6c5bab4061 100644 --- a/src/go/pkg/l2topology/topology_adapter_endpoints.go +++ b/src/go/pkg/l2topology/topology_adapter_endpoints.go @@ -7,12 +7,14 @@ import ( "sort" "strconv" "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) type builtEndpointActors struct { - actors []Actor + actors []graph.Actor count int - matchByEndpointID map[string]Match + matchByEndpointID map[string]graph.Match labelsByEndpointID map[string]map[string]string } @@ -90,7 +92,7 @@ func buildEndpointActors( if len(accumulators) == 0 { return builtEndpointActors{ - matchByEndpointID: map[string]Match{}, + matchByEndpointID: map[string]graph.Match{}, labelsByEndpointID: map[string]map[string]string{}, } } @@ -101,9 +103,9 @@ func buildEndpointActors( } sort.Strings(keys) - actors := make([]Actor, 0, len(keys)) + actors := make([]graph.Actor, 0, len(keys)) endpointCount := 0 - matchByEndpointID := make(map[string]Match, len(keys)) + matchByEndpointID := make(map[string]graph.Match, len(keys)) labelsByEndpointID := make(map[string]map[string]string, len(keys)) for _, endpointID := range keys { acc := accumulators[endpointID] @@ -111,7 +113,7 @@ func buildEndpointActors( continue } - match := Match{} + match := graph.Match{} if acc.mac != "" { match.ChassisIDs = []string{acc.mac} match.MacAddresses = []string{acc.mac} @@ -143,7 +145,7 @@ func buildEndpointActors( attrs["vendor_derived_confidence"] = "low" attrs["vendor_derived_match_prefix"] = derivedPrefix } - actor := Actor{ + actor := graph.Actor{ ActorType: "endpoint", Layer: layer, Source: source, diff --git a/src/go/pkg/l2topology/topology_adapter_identity.go b/src/go/pkg/l2topology/topology_adapter_identity.go index 60603cd1f3e901..7006b7b018ea38 100644 --- a/src/go/pkg/l2topology/topology_adapter_identity.go +++ b/src/go/pkg/l2topology/topology_adapter_identity.go @@ -6,6 +6,8 @@ import ( "sort" "strconv" "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) var interfaceNameLookupSanitizer = strings.NewReplacer( @@ -171,7 +173,7 @@ func buildDeviceIdentityKeySetByID( return out } -func topologyMatchIdentityKeys(match Match) []string { +func topologyMatchIdentityKeys(match graph.Match) []string { seen := make(map[string]struct{}, 8) add := func(kind, value string) { value = strings.TrimSpace(value) @@ -232,7 +234,7 @@ func topologyMatchIdentityKeys(match Match) []string { return keys } -func topologyMatchHardwareIdentityKeys(match Match) []string { +func topologyMatchHardwareIdentityKeys(match graph.Match) []string { seen := make(map[string]struct{}, len(match.MacAddresses)+len(match.ChassisIDs)) add := func(value string) { if mac := normalizeMAC(value); mac != "" { @@ -259,7 +261,7 @@ func topologyMatchHardwareIdentityKeys(match Match) []string { } func endpointMatchOverlappingKnownDeviceIDs( - endpointMatch Match, + endpointMatch graph.Match, deviceIdentityByID map[string]topologyIdentityKeySet, ) []string { if len(deviceIdentityByID) == 0 { diff --git a/src/go/pkg/l2topology/topology_adapter_identity_assignment.go b/src/go/pkg/l2topology/topology_adapter_identity_assignment.go index d9e0704581dd95..25b0fcfa43bb3e 100644 --- a/src/go/pkg/l2topology/topology_adapter_identity_assignment.go +++ b/src/go/pkg/l2topology/topology_adapter_identity_assignment.go @@ -6,6 +6,8 @@ import ( "fmt" "sort" "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) type topologyMatchLookup struct { @@ -14,16 +16,16 @@ type topologyMatchLookup struct { } type topologyActorSortEntry struct { - actor Actor + actor graph.Actor key string } type topologyLinkSortEntry struct { - link Link + link graph.Link key string } -func canonicalTopologyMatchKey(match Match) string { +func canonicalTopologyMatchKey(match graph.Match) string { if key := canonicalTopologyPrimaryMACKey(match); key != "" { return "mac:" + key } @@ -48,7 +50,7 @@ func canonicalTopologyMatchKey(match Match) string { return "" } -func assignTopologyActorIDsAndLinkEndpoints(actors []Actor, links []Link) { +func assignTopologyActorIDsAndLinkEndpoints(actors []graph.Actor, links []graph.Link) { if len(actors) == 0 { return } @@ -93,7 +95,7 @@ func assignTopologyActorIDsAndLinkEndpoints(actors []Actor, links []Link) { } } -func newTopologyMatchLookup(match Match) topologyMatchLookup { +func newTopologyMatchLookup(match graph.Match) topologyMatchLookup { return topologyMatchLookup{ canonical: canonicalTopologyMatchKey(match), identityKeys: topologyMatchIdentityKeys(match), @@ -129,7 +131,7 @@ func resolveTopologyEndpointActorID(lookup topologyMatchLookup, byCanonicalMatch return "" } -func enrichTopologyPortTablesWithLinkCounts(actors []Actor, links []Link) { +func enrichTopologyPortTablesWithLinkCounts(actors []graph.Actor, links []graph.Link) { type actorPort struct { actorID string portName string @@ -172,7 +174,7 @@ func enrichTopologyPortTablesWithLinkCounts(actors []Actor, links []Link) { } } -func canonicalTopologyPrimaryMACKey(match Match) string { +func canonicalTopologyPrimaryMACKey(match graph.Match) string { set := make(map[string]struct{}, len(match.MacAddresses)+len(match.ChassisIDs)) for _, value := range match.MacAddresses { if mac := normalizeMAC(value); mac != "" { @@ -266,7 +268,7 @@ func canonicalTopologyStringListKey(values []string) string { return strings.Join(out, ",") } -func topologyLinkSortKey(link Link) string { +func topologyLinkSortKey(link graph.Link) string { return strings.Join([]string{ link.Protocol, link.Direction, @@ -282,7 +284,7 @@ func topologyLinkSortKey(link Link) string { }, keySep) } -func topologyActorSortKey(actor Actor) string { +func topologyActorSortKey(actor graph.Actor) string { return strings.Join([]string{ actor.ActorType, canonicalTopologyMatchKey(actor.Match), @@ -302,7 +304,7 @@ func topologyAttrKey(attrs map[string]any, key string) string { return fmt.Sprint(value) } -func sortTopologyActors(actors []Actor) { +func sortTopologyActors(actors []graph.Actor) { if len(actors) < 2 { return } @@ -324,7 +326,7 @@ func sortTopologyActors(actors []Actor) { } } -func sortTopologyLinks(links []Link) { +func sortTopologyLinks(links []graph.Link) { if len(links) < 2 { return } diff --git a/src/go/pkg/l2topology/topology_adapter_identity_assignment_test.go b/src/go/pkg/l2topology/topology_adapter_identity_assignment_test.go index c5ce243417e3b4..69c97201e25051 100644 --- a/src/go/pkg/l2topology/topology_adapter_identity_assignment_test.go +++ b/src/go/pkg/l2topology/topology_adapter_identity_assignment_test.go @@ -3,8 +3,10 @@ package l2topology import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" + "github.com/stretchr/testify/require" ) func TestCanonicalTopologyKeyHelpers_NormalizeDeterministically(t *testing.T) { @@ -19,12 +21,12 @@ func TestCanonicalTopologyKeyHelpers_NormalizeDeterministically(t *testing.T) { } func TestAssignTopologyActorIDsAndLinkEndpoints_IsDeterministic(t *testing.T) { - actors := []Actor{ + actors := []graph.Actor{ { ActorType: "device", Layer: "l2", Source: "snmp", - Match: Match{ + Match: graph.Match{ MacAddresses: []string{"00:11:22:33:44:55"}, Hostnames: []string{"switch-a"}, }, @@ -33,7 +35,7 @@ func TestAssignTopologyActorIDsAndLinkEndpoints_IsDeterministic(t *testing.T) { ActorType: "device", Layer: "l2", Source: "snmp", - Match: Match{ + Match: graph.Match{ MacAddresses: []string{"00-11-22-33-44-55"}, Hostnames: []string{"switch-a-duplicate"}, }, @@ -42,7 +44,7 @@ func TestAssignTopologyActorIDsAndLinkEndpoints_IsDeterministic(t *testing.T) { ActorType: "endpoint", Layer: "l2", Source: "derived", - Match: Match{ + Match: graph.Match{ IPAddresses: []string{"10.0.0.2"}, }, }, @@ -50,22 +52,22 @@ func TestAssignTopologyActorIDsAndLinkEndpoints_IsDeterministic(t *testing.T) { ActorType: "segment", Layer: "l2", Source: "derived", - Match: Match{}, + Match: graph.Match{}, }, } - links := []Link{ + links := []graph.Link{ { Protocol: "lldp", Direction: "outbound", State: "up", - Src: LinkEndpoint{ - Match: Match{MacAddresses: []string{"00:11:22:33:44:55"}}, + Src: graph.LinkEndpoint{ + Match: graph.Match{MacAddresses: []string{"00:11:22:33:44:55"}}, Attributes: map[string]any{ "if_name": "eth0", }, }, - Dst: LinkEndpoint{ - Match: Match{IPAddresses: []string{"10.0.0.2"}}, + Dst: graph.LinkEndpoint{ + Match: graph.Match{IPAddresses: []string{"10.0.0.2"}}, Attributes: map[string]any{ "if_name": "eth9", }, @@ -75,14 +77,14 @@ func TestAssignTopologyActorIDsAndLinkEndpoints_IsDeterministic(t *testing.T) { Protocol: "lldp", Direction: "outbound", State: "up", - Src: LinkEndpoint{ - Match: Match{IPAddresses: []string{"10.0.0.2"}}, + Src: graph.LinkEndpoint{ + Match: graph.Match{IPAddresses: []string{"10.0.0.2"}}, Attributes: map[string]any{ "if_name": "eth9", }, }, - Dst: LinkEndpoint{ - Match: Match{MacAddresses: []string{"00:11:22:33:44:55"}}, + Dst: graph.LinkEndpoint{ + Match: graph.Match{MacAddresses: []string{"00:11:22:33:44:55"}}, Attributes: map[string]any{ "if_name": "eth0", }, @@ -117,7 +119,7 @@ func TestAssignTopologyActorIDsAndLinkEndpoints_IsDeterministic(t *testing.T) { } func TestEnrichTopologyPortTablesWithLinkCounts_AddsCountsToMatchingPorts(t *testing.T) { - actors := []Actor{ + actors := []graph.Actor{ { ActorID: "device-a", Tables: map[string][]map[string]any{ @@ -136,18 +138,18 @@ func TestEnrichTopologyPortTablesWithLinkCounts_AddsCountsToMatchingPorts(t *tes }, }, } - links := []Link{ + links := []graph.Link{ { SrcActorID: "device-a", DstActorID: "device-b", - Src: LinkEndpoint{Attributes: map[string]any{"if_name": "eth0"}}, - Dst: LinkEndpoint{Attributes: map[string]any{"if_name": "xe-0/0/0"}}, + Src: graph.LinkEndpoint{Attributes: map[string]any{"if_name": "eth0"}}, + Dst: graph.LinkEndpoint{Attributes: map[string]any{"if_name": "xe-0/0/0"}}, }, { SrcActorID: "device-a", DstActorID: "device-b", - Src: LinkEndpoint{Attributes: map[string]any{"if_name": "eth0"}}, - Dst: LinkEndpoint{Attributes: map[string]any{"if_name": "xe-0/0/0"}}, + Src: graph.LinkEndpoint{Attributes: map[string]any{"if_name": "eth0"}}, + Dst: graph.LinkEndpoint{Attributes: map[string]any{"if_name": "xe-0/0/0"}}, }, } diff --git a/src/go/pkg/l2topology/topology_adapter_projection.go b/src/go/pkg/l2topology/topology_adapter_projection.go index 63d49aa8da626b..9bc3387f756fd5 100644 --- a/src/go/pkg/l2topology/topology_adapter_projection.go +++ b/src/go/pkg/l2topology/topology_adapter_projection.go @@ -2,10 +2,12 @@ package l2topology +import "github.com/netdata/netdata/go/plugins/pkg/topology/graph" + type builtAdjacencyLink struct { adj Adjacency protocol string - link Link + link graph.Link } type pairedLinkAccumulator struct { @@ -13,7 +15,7 @@ type pairedLinkAccumulator struct { } type projectedLinks struct { - links []Link + links []graph.Link lldp int cdp int bidirectionalCount int diff --git a/src/go/pkg/l2topology/topology_adapter_projection_pairs.go b/src/go/pkg/l2topology/topology_adapter_projection_pairs.go index bebfde771b112f..654e8e421d7fdd 100644 --- a/src/go/pkg/l2topology/topology_adapter_projection_pairs.go +++ b/src/go/pkg/l2topology/topology_adapter_projection_pairs.go @@ -5,6 +5,8 @@ package l2topology import ( "strings" "time" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) func projectAdjacencyLinks( @@ -16,7 +18,7 @@ func projectAdjacencyLinks( ifaceByDeviceIndex map[string]Interface, ) projectedLinks { out := projectedLinks{ - links: make([]Link, 0, len(adjacencies)), + links: make([]graph.Link, 0, len(adjacencies)), } if len(adjacencies) == 0 { return out @@ -164,11 +166,11 @@ func backfillPairGroupMissingEndpointPorts(entries []*builtAdjacencyLink) { } } -func endpointHasKnownCanonicalPort(endpoint LinkEndpoint) bool { +func endpointHasKnownCanonicalPort(endpoint graph.LinkEndpoint) bool { return strings.TrimSpace(topologyCanonicalPortName(endpoint.Attributes)) != "" } -func backfillEndpointPortFromPeer(endpoint LinkEndpoint, peer LinkEndpoint) LinkEndpoint { +func backfillEndpointPortFromPeer(endpoint graph.LinkEndpoint, peer graph.LinkEndpoint) graph.LinkEndpoint { if endpointHasKnownCanonicalPort(endpoint) || !endpointHasKnownCanonicalPort(peer) { return endpoint } @@ -218,14 +220,14 @@ func adjacencyToTopologyLink( deviceByID map[string]Device, ifIndexByDeviceName map[string]int, ifaceByDeviceIndex map[string]Interface, -) Link { +) graph.Link { src := adjacencySideToEndpoint(deviceByID[adj.SourceID], adj.SourcePort, ifIndexByDeviceName, ifaceByDeviceIndex) dst := adjacencySideToEndpoint(deviceByID[adj.TargetID], adj.TargetPort, ifIndexByDeviceName, ifaceByDeviceIndex) if rawAddress := strings.TrimSpace(adj.Labels["remote_address_raw"]); rawAddress != "" { dst.Match.IPAddresses = uniqueTopologyStrings(append(dst.Match.IPAddresses, rawAddress)) } - link := Link{ + link := graph.Link{ Layer: layer, Protocol: protocol, LinkType: protocol, @@ -284,7 +286,7 @@ func buildPairedLinkMetrics(sourceLabels, targetLabels map[string]string) map[st return metrics } -func mergeEndpointIPHints(base, extra LinkEndpoint) LinkEndpoint { +func mergeEndpointIPHints(base, extra graph.LinkEndpoint) graph.LinkEndpoint { if len(extra.Match.IPAddresses) == 0 { return base } diff --git a/src/go/pkg/l2topology/topology_adapter_pruning.go b/src/go/pkg/l2topology/topology_adapter_pruning.go index 6c2320c2ab78fb..6a089a476fca2f 100644 --- a/src/go/pkg/l2topology/topology_adapter_pruning.go +++ b/src/go/pkg/l2topology/topology_adapter_pruning.go @@ -5,9 +5,11 @@ package l2topology import ( "sort" "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) -func pruneSegmentArtifacts(actors []Actor, links []Link) ([]Actor, []Link, int) { +func pruneSegmentArtifacts(actors []graph.Actor, links []graph.Link) ([]graph.Actor, []graph.Link, int) { if len(actors) == 0 || len(links) == 0 { return actors, links, 0 } @@ -130,7 +132,7 @@ func pruneSegmentArtifacts(actors []Actor, links []Link) ([]Actor, []Link, int) return actors, links, 0 } - filteredActors := make([]Actor, 0, len(actors)) + filteredActors := make([]graph.Actor, 0, len(actors)) for _, actor := range actors { key := canonicalTopologyMatchKey(actor.Match) if key == "" { @@ -143,7 +145,7 @@ func pruneSegmentArtifacts(actors []Actor, links []Link) ([]Actor, []Link, int) filteredActors = append(filteredActors, actor) } - filteredLinks := make([]Link, 0, len(links)) + filteredLinks := make([]graph.Link, 0, len(links)) for _, link := range links { src := canonicalTopologyMatchKey(link.Src.Match) dst := canonicalTopologyMatchKey(link.Dst.Match) @@ -184,7 +186,7 @@ type topologyLinkCounts struct { unidirectional int } -func summarizeTopologyLinks(links []Link) topologyLinkCounts { +func summarizeTopologyLinks(links []graph.Link) topologyLinkCounts { var counts topologyLinkCounts for _, link := range links { switch strings.ToLower(strings.TrimSpace(link.Protocol)) { @@ -209,10 +211,10 @@ func summarizeTopologyLinks(links []Link) topologyLinkCounts { } func pruneManagedOverlapUnlinkedEndpointActors( - actors []Actor, - links []Link, + actors []graph.Actor, + links []graph.Link, suppressedEndpointIDs map[string]struct{}, -) ([]Actor, int) { +) ([]graph.Actor, int) { if len(actors) == 0 || len(suppressedEndpointIDs) == 0 { return actors, 0 } @@ -254,7 +256,7 @@ func pruneManagedOverlapUnlinkedEndpointActors( } } - filtered := make([]Actor, 0, len(actors)) + filtered := make([]graph.Actor, 0, len(actors)) suppressedCount := 0 for _, actor := range actors { if !strings.EqualFold(strings.TrimSpace(actor.ActorType), "endpoint") { diff --git a/src/go/pkg/l2topology/topology_adapter_regression_test.go b/src/go/pkg/l2topology/topology_adapter_regression_test.go index 2ee1705966447f..850065fc2c075e 100644 --- a/src/go/pkg/l2topology/topology_adapter_regression_test.go +++ b/src/go/pkg/l2topology/topology_adapter_regression_test.go @@ -3,8 +3,10 @@ package l2topology import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" + "github.com/stretchr/testify/require" ) func TestBackfillPairGroupMissingEndpointPortsCopiesPeerInterfaceAttributes(t *testing.T) { @@ -14,9 +16,9 @@ func TestBackfillPairGroupMissingEndpointPortsCopiesPeerInterfaceAttributes(t *t SourceID: "device-a", TargetID: "device-b", }, - link: Link{ - Src: LinkEndpoint{Attributes: map[string]any{}}, - Dst: LinkEndpoint{Attributes: map[string]any{}}, + link: graph.Link{ + Src: graph.LinkEndpoint{Attributes: map[string]any{}}, + Dst: graph.LinkEndpoint{Attributes: map[string]any{}}, }, }, { @@ -24,13 +26,13 @@ func TestBackfillPairGroupMissingEndpointPortsCopiesPeerInterfaceAttributes(t *t SourceID: "device-b", TargetID: "device-a", }, - link: Link{ - Src: LinkEndpoint{Attributes: map[string]any{ + link: graph.Link{ + Src: graph.LinkEndpoint{Attributes: map[string]any{ "if_index": 2, "if_name": "Gi0/2", "port_id": "Gi0/2", }}, - Dst: LinkEndpoint{Attributes: map[string]any{ + Dst: graph.LinkEndpoint{Attributes: map[string]any{ "if_index": 1, "if_name": "Gi0/1", "port_id": "Gi0/1", @@ -56,9 +58,9 @@ func TestBackfillPairGroupMissingEndpointPortsSkipsAmbiguousReverseCandidates(t SourceID: "device-a", TargetID: "device-b", }, - link: Link{ - Src: LinkEndpoint{Attributes: map[string]any{}}, - Dst: LinkEndpoint{Attributes: map[string]any{}}, + link: graph.Link{ + Src: graph.LinkEndpoint{Attributes: map[string]any{}}, + Dst: graph.LinkEndpoint{Attributes: map[string]any{}}, }, }, { @@ -66,11 +68,11 @@ func TestBackfillPairGroupMissingEndpointPortsSkipsAmbiguousReverseCandidates(t SourceID: "device-b", TargetID: "device-a", }, - link: Link{ - Src: LinkEndpoint{Attributes: map[string]any{ + link: graph.Link{ + Src: graph.LinkEndpoint{Attributes: map[string]any{ "if_name": "Gi0/2", }}, - Dst: LinkEndpoint{Attributes: map[string]any{ + Dst: graph.LinkEndpoint{Attributes: map[string]any{ "if_name": "Gi0/1", }}, }, @@ -80,11 +82,11 @@ func TestBackfillPairGroupMissingEndpointPortsSkipsAmbiguousReverseCandidates(t SourceID: "device-b", TargetID: "device-a", }, - link: Link{ - Src: LinkEndpoint{Attributes: map[string]any{ + link: graph.Link{ + Src: graph.LinkEndpoint{Attributes: map[string]any{ "if_name": "Gi0/22", }}, - Dst: LinkEndpoint{Attributes: map[string]any{ + Dst: graph.LinkEndpoint{Attributes: map[string]any{ "if_name": "Gi0/11", }}, }, @@ -98,12 +100,12 @@ func TestBackfillPairGroupMissingEndpointPortsSkipsAmbiguousReverseCandidates(t } func TestBackfillEndpointPortFromPeerPreservesExistingCanonicalPort(t *testing.T) { - endpoint := LinkEndpoint{ + endpoint := graph.LinkEndpoint{ Attributes: map[string]any{ "if_name": "Gi0/10", }, } - peer := LinkEndpoint{ + peer := graph.LinkEndpoint{ Attributes: map[string]any{ "if_index": 7, "if_name": "Gi0/7", @@ -122,7 +124,7 @@ func TestSegmentProjectionBuilderPruneSegmentsWithoutLinksRemovesEmptySegments(t builder := &segmentProjectionBuilder{ segmentIDs: []string{"segment-a", "segment-b"}, out: projectedSegments{ - actors: []Actor{ + actors: []graph.Actor{ { ActorID: "segment-a", ActorType: "segment", @@ -138,7 +140,7 @@ func TestSegmentProjectionBuilderPruneSegmentsWithoutLinksRemovesEmptySegments(t }, }, }, - links: []Link{ + links: []graph.Link{ { Protocol: "fdb", Direction: "bidirectional", diff --git a/src/go/pkg/l2topology/topology_adapter_reporter_hints.go b/src/go/pkg/l2topology/topology_adapter_reporter_hints.go index e9eff9ea7cf8bb..06aad0e7802895 100644 --- a/src/go/pkg/l2topology/topology_adapter_reporter_hints.go +++ b/src/go/pkg/l2topology/topology_adapter_reporter_hints.go @@ -6,10 +6,12 @@ import ( "sort" "strconv" "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) func collectTopologyEndpointIDs( - endpointMatchByID map[string]Match, + endpointMatchByID map[string]graph.Match, endpointLabelsByID map[string]map[string]string, endpointSegmentCandidates map[string][]string, rawFDBObservations fdbReporterObservation, diff --git a/src/go/pkg/l2topology/topology_adapter_segment_actor.go b/src/go/pkg/l2topology/topology_adapter_segment_actor.go index 817772035ff108..4320b45b1fe4c9 100644 --- a/src/go/pkg/l2topology/topology_adapter_segment_actor.go +++ b/src/go/pkg/l2topology/topology_adapter_segment_actor.go @@ -6,9 +6,11 @@ import ( "sort" "strconv" "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) -func buildBridgeSegmentActor(segmentID string, segment *bridgeDomainSegment, layer string, source string) (Match, Actor) { +func buildBridgeSegmentActor(segmentID string, segment *bridgeDomainSegment, layer string, source string) (graph.Match, graph.Actor) { parentDevices := make(map[string]struct{}) ifNames := make(map[string]struct{}) ifIndexes := make(map[string]struct{}) @@ -34,7 +36,7 @@ func buildBridgeSegmentActor(segmentID string, segment *bridgeDomainSegment, lay } } - match := Match{ + match := graph.Match{ Hostnames: []string{"segment:" + segmentID}, } @@ -58,7 +60,7 @@ func buildBridgeSegmentActor(segmentID string, segment *bridgeDomainSegment, lay } } - actor := Actor{ + actor := graph.Actor{ ActorType: "segment", Layer: layer, Source: source, @@ -72,36 +74,36 @@ func buildBridgeSegmentActor(segmentID string, segment *bridgeDomainSegment, lay return match, actor } -func endpointMatchFromID(endpointID string) Match { +func endpointMatchFromID(endpointID string) graph.Match { kind, value, ok := strings.Cut(strings.TrimSpace(endpointID), ":") if !ok { - return Match{} + return graph.Match{} } switch strings.ToLower(strings.TrimSpace(kind)) { case "mac": mac := normalizeMAC(value) if mac == "" { - return Match{} + return graph.Match{} } - return Match{ + return graph.Match{ ChassisIDs: []string{mac}, MacAddresses: []string{mac}, } case "ip": addr := normalizeTopologyIP(value) if addr == "" { - return Match{} + return graph.Match{} } - return Match{ + return graph.Match{ IPAddresses: []string{addr}, } } - return Match{} + return graph.Match{} } func annotateEndpointActorsWithDirectOwners( - actors []Actor, - endpointMatchByID map[string]Match, + actors []graph.Actor, + endpointMatchByID map[string]graph.Match, owners map[string]fdbEndpointOwner, deviceByID map[string]Device, ) { diff --git a/src/go/pkg/l2topology/topology_adapter_segments.go b/src/go/pkg/l2topology/topology_adapter_segments.go index 3931dd708cd5ce..fc1b4ff9319fd2 100644 --- a/src/go/pkg/l2topology/topology_adapter_segments.go +++ b/src/go/pkg/l2topology/topology_adapter_segments.go @@ -6,6 +6,8 @@ import ( "sort" "strings" "time" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) func projectSegmentTopology( @@ -19,7 +21,7 @@ func projectSegmentTopology( ifIndexByDeviceName map[string]int, bridgeLinks []bridgeBridgeLinkRecord, reporterAliases map[string][]string, - endpointMatchByID map[string]Match, + endpointMatchByID map[string]graph.Match, endpointLabelsByID map[string]map[string]string, actorIndex map[string]struct{}, probabilisticConnectivity bool, diff --git a/src/go/pkg/l2topology/topology_adapter_segments_builder.go b/src/go/pkg/l2topology/topology_adapter_segments_builder.go index e66941c39f4c59..6d3207c4d25816 100644 --- a/src/go/pkg/l2topology/topology_adapter_segments_builder.go +++ b/src/go/pkg/l2topology/topology_adapter_segments_builder.go @@ -4,6 +4,8 @@ package l2topology import ( "time" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) type segmentProjectionBuilder struct { @@ -17,14 +19,14 @@ type segmentProjectionBuilder struct { ifIndexByDeviceName map[string]int bridgeLinks []bridgeBridgeLinkRecord reporterAliases map[string][]string - endpointMatchByID map[string]Match + endpointMatchByID map[string]graph.Match endpointLabelsByID map[string]map[string]string actorIndex map[string]struct{} probabilisticConnectivity bool strategyConfig topologyInferenceStrategyConfig out projectedSegments segmentIDs []string - segmentMatchByID map[string]Match + segmentMatchByID map[string]graph.Match segmentByID map[string]*bridgeDomainSegment deviceSegmentEdgeSeen map[string]struct{} endpointSegmentEdgeSeen map[string]struct{} @@ -62,7 +64,7 @@ func newSegmentProjectionBuilder( ifIndexByDeviceName map[string]int, bridgeLinks []bridgeBridgeLinkRecord, reporterAliases map[string][]string, - endpointMatchByID map[string]Match, + endpointMatchByID map[string]graph.Match, endpointLabelsByID map[string]map[string]string, actorIndex map[string]struct{}, probabilisticConnectivity bool, @@ -85,8 +87,8 @@ func newSegmentProjectionBuilder( probabilisticConnectivity: probabilisticConnectivity, strategyConfig: strategyConfig, out: projectedSegments{ - actors: make([]Actor, 0), - links: make([]Link, 0), + actors: make([]graph.Actor, 0), + links: make([]graph.Link, 0), }, } } diff --git a/src/go/pkg/l2topology/topology_adapter_segments_builder_emit.go b/src/go/pkg/l2topology/topology_adapter_segments_builder_emit.go index 5ba8024abe5967..aea40c48f6d231 100644 --- a/src/go/pkg/l2topology/topology_adapter_segments_builder_emit.go +++ b/src/go/pkg/l2topology/topology_adapter_segments_builder_emit.go @@ -5,6 +5,8 @@ package l2topology import ( "sort" "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) func (b *segmentProjectionBuilder) emitLinks() { @@ -17,7 +19,7 @@ func (b *segmentProjectionBuilder) emitLinks() { if segment == nil { continue } - segmentEndpoint := LinkEndpoint{ + segmentEndpoint := graph.LinkEndpoint{ Match: b.segmentMatchByID[segmentID], Attributes: map[string]any{ "segment_id": segmentID, @@ -55,7 +57,7 @@ func (b *segmentProjectionBuilder) emitLinks() { if segment.portIdentityKey(port) == segment.portIdentityKey(segment.designatedPort) { metrics["designated"] = true } - b.out.links = append(b.out.links, Link{ + b.out.links = append(b.out.links, graph.Link{ Layer: b.layer, Protocol: "bridge", LinkType: "bridge", @@ -118,7 +120,7 @@ func (b *segmentProjectionBuilder) emitLinks() { edgeKey := segmentID + "|managed-device|" + matchedDeviceID if _, seen := b.endpointSegmentEdgeSeen[edgeKey]; !seen { b.endpointSegmentEdgeSeen[edgeKey] = struct{}{} - b.out.links = append(b.out.links, Link{ + b.out.links = append(b.out.links, graph.Link{ Layer: b.layer, Protocol: "fdb", LinkType: "fdb", @@ -179,13 +181,13 @@ func (b *segmentProjectionBuilder) emitLinks() { linkState = "probable" } } - b.out.links = append(b.out.links, Link{ + b.out.links = append(b.out.links, graph.Link{ Layer: b.layer, Protocol: "fdb", LinkType: "fdb", Direction: "bidirectional", Src: adjacencySideToEndpoint(device, localPort, b.ifIndexByDeviceName, b.ifaceByDeviceIndex), - Dst: LinkEndpoint{Match: endpointMatch}, + Dst: graph.LinkEndpoint{Match: endpointMatch}, DiscoveredAt: topologyTimePtr(b.collectedAt), LastSeen: topologyTimePtr(b.collectedAt), State: linkState, @@ -226,13 +228,13 @@ func (b *segmentProjectionBuilder) emitLinks() { } } - b.out.links = append(b.out.links, Link{ + b.out.links = append(b.out.links, graph.Link{ Layer: b.layer, Protocol: "fdb", LinkType: "fdb", Direction: "bidirectional", Src: segmentEndpoint, - Dst: LinkEndpoint{Match: endpointMatch}, + Dst: graph.LinkEndpoint{Match: endpointMatch}, DiscoveredAt: topologyTimePtr(b.collectedAt), LastSeen: topologyTimePtr(b.collectedAt), State: linkState, @@ -253,7 +255,7 @@ func (b *segmentProjectionBuilder) pruneSegmentsWithoutLinks(segmentsWithAnyLink return } - filteredActors := make([]Actor, 0, len(b.out.actors)) + filteredActors := make([]graph.Actor, 0, len(b.out.actors)) for _, actor := range b.out.actors { segmentID := topologyAttrString(actor.Attributes, "segment_id") if segmentID == "" { @@ -265,7 +267,7 @@ func (b *segmentProjectionBuilder) pruneSegmentsWithoutLinks(segmentsWithAnyLink } b.out.actors = filteredActors - filteredLinks := make([]Link, 0, len(b.out.links)) + filteredLinks := make([]graph.Link, 0, len(b.out.links)) b.out.linksFdb = 0 b.out.bidirectionalCount = 0 b.out.endpointLinksEmitted = 0 diff --git a/src/go/pkg/l2topology/topology_adapter_segments_builder_init.go b/src/go/pkg/l2topology/topology_adapter_segments_builder_init.go index 74d5019922dfcc..c881e47f85163c 100644 --- a/src/go/pkg/l2topology/topology_adapter_segments_builder_init.go +++ b/src/go/pkg/l2topology/topology_adapter_segments_builder_init.go @@ -6,6 +6,8 @@ import ( "sort" "strconv" "strings" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" ) func (b *segmentProjectionBuilder) initializeSegments() bool { @@ -25,7 +27,7 @@ func (b *segmentProjectionBuilder) initializeSegments() bool { return false } - b.segmentMatchByID = make(map[string]Match) + b.segmentMatchByID = make(map[string]graph.Match) b.segmentByID = make(map[string]*bridgeDomainSegment) for _, domain := range model.domains { if domain == nil { diff --git a/src/go/pkg/l2topology/topology_adapter_test.go b/src/go/pkg/l2topology/topology_adapter_test.go index 50a20a37017c9b..28539287a548ee 100644 --- a/src/go/pkg/l2topology/topology_adapter_test.go +++ b/src/go/pkg/l2topology/topology_adapter_test.go @@ -3,12 +3,14 @@ package l2topology import ( - "github.com/stretchr/testify/require" "net/netip" "slices" "strings" "testing" "time" + + "github.com/netdata/netdata/go/plugins/pkg/topology/graph" + "github.com/stretchr/testify/require" ) func TestToGraph_ProjectsResult(t *testing.T) { @@ -840,10 +842,10 @@ func TestToGraph_DeduplicatesEndpointActorOverlappingManagedDevice(t *testing.T) } func TestCanonicalTopologyMatchKey_NormalizesEquivalentMACRepresentations(t *testing.T) { - raw := Match{ + raw := graph.Match{ ChassisIDs: []string{"7049a26572cd"}, } - colon := Match{ + colon := graph.Match{ ChassisIDs: []string{"70:49:A2:65:72:CD"}, } @@ -2191,7 +2193,7 @@ func TestToGraph_DisplayNamesPreferDNSThenIPThenMAC(t *testing.T) { } func TestTopologyDisplayNameFromMatch_PrefersSysNameBeforeIP(t *testing.T) { - display := topologyDisplayNameFromMatch(Match{ + display := topologyDisplayNameFromMatch(graph.Match{ SysName: "MikroTik-router", IPAddresses: []string{"10.20.4.1"}, }, &topologyDisplayNameResolver{ @@ -2204,7 +2206,7 @@ func TestTopologyDisplayNameFromMatch_PrefersSysNameBeforeIP(t *testing.T) { } func TestTopologyDisplayNameFromMatch_PrefersHostnameBeforeIPWhenSysNameMissing(t *testing.T) { - display := topologyDisplayNameFromMatch(Match{ + display := topologyDisplayNameFromMatch(graph.Match{ Hostnames: []string{"nova"}, IPAddresses: []string{"10.20.4.22"}, }, &topologyDisplayNameResolver{ @@ -2518,36 +2520,36 @@ func TestToGraph_KeepsChassisPlaceholderDevicesAsDevices(t *testing.T) { } func TestPruneSegmentArtifacts_SuppressesLLDPDuplicateSegmentPath(t *testing.T) { - actors := []Actor{ + actors := []graph.Actor{ { ActorType: "device", - Match: Match{IPAddresses: []string{"10.0.0.1"}, SysName: "switch-a"}, + Match: graph.Match{IPAddresses: []string{"10.0.0.1"}, SysName: "switch-a"}, }, { ActorType: "device", - Match: Match{IPAddresses: []string{"10.0.0.2"}, SysName: "switch-b"}, + Match: graph.Match{IPAddresses: []string{"10.0.0.2"}, SysName: "switch-b"}, }, { ActorType: "segment", - Match: Match{Hostnames: []string{"segment:dup"}}, + Match: graph.Match{Hostnames: []string{"segment:dup"}}, }, } - links := []Link{ + links := []graph.Link{ { Protocol: "lldp", - Src: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.0.1"}}}, - Dst: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.0.2"}}}, + Src: graph.LinkEndpoint{Match: graph.Match{IPAddresses: []string{"10.0.0.1"}}}, + Dst: graph.LinkEndpoint{Match: graph.Match{IPAddresses: []string{"10.0.0.2"}}}, }, { Protocol: "bridge", - Src: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.0.1"}}}, - Dst: LinkEndpoint{Match: Match{Hostnames: []string{"segment:dup"}}}, + Src: graph.LinkEndpoint{Match: graph.Match{IPAddresses: []string{"10.0.0.1"}}}, + Dst: graph.LinkEndpoint{Match: graph.Match{Hostnames: []string{"segment:dup"}}}, }, { Protocol: "bridge", - Src: LinkEndpoint{Match: Match{Hostnames: []string{"segment:dup"}}}, - Dst: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.0.2"}}}, + Src: graph.LinkEndpoint{Match: graph.Match{Hostnames: []string{"segment:dup"}}}, + Dst: graph.LinkEndpoint{Match: graph.Match{IPAddresses: []string{"10.0.0.2"}}}, }, } @@ -2559,36 +2561,36 @@ func TestPruneSegmentArtifacts_SuppressesLLDPDuplicateSegmentPath(t *testing.T) } func TestPruneSegmentArtifacts_SuppressesCDPDuplicateSegmentPath(t *testing.T) { - actors := []Actor{ + actors := []graph.Actor{ { ActorType: "device", - Match: Match{IPAddresses: []string{"10.0.1.1"}, SysName: "switch-a"}, + Match: graph.Match{IPAddresses: []string{"10.0.1.1"}, SysName: "switch-a"}, }, { ActorType: "device", - Match: Match{IPAddresses: []string{"10.0.1.2"}, SysName: "switch-b"}, + Match: graph.Match{IPAddresses: []string{"10.0.1.2"}, SysName: "switch-b"}, }, { ActorType: "segment", - Match: Match{Hostnames: []string{"segment:dup-cdp"}}, + Match: graph.Match{Hostnames: []string{"segment:dup-cdp"}}, }, } - links := []Link{ + links := []graph.Link{ { Protocol: "cdp", - Src: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.1.1"}}}, - Dst: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.1.2"}}}, + Src: graph.LinkEndpoint{Match: graph.Match{IPAddresses: []string{"10.0.1.1"}}}, + Dst: graph.LinkEndpoint{Match: graph.Match{IPAddresses: []string{"10.0.1.2"}}}, }, { Protocol: "bridge", - Src: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.1.1"}}}, - Dst: LinkEndpoint{Match: Match{Hostnames: []string{"segment:dup-cdp"}}}, + Src: graph.LinkEndpoint{Match: graph.Match{IPAddresses: []string{"10.0.1.1"}}}, + Dst: graph.LinkEndpoint{Match: graph.Match{Hostnames: []string{"segment:dup-cdp"}}}, }, { Protocol: "bridge", - Src: LinkEndpoint{Match: Match{Hostnames: []string{"segment:dup-cdp"}}}, - Dst: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.1.2"}}}, + Src: graph.LinkEndpoint{Match: graph.Match{Hostnames: []string{"segment:dup-cdp"}}}, + Dst: graph.LinkEndpoint{Match: graph.Match{IPAddresses: []string{"10.0.1.2"}}}, }, } @@ -2600,22 +2602,22 @@ func TestPruneSegmentArtifacts_SuppressesCDPDuplicateSegmentPath(t *testing.T) { } func TestPruneSegmentArtifacts_SuppressesSegmentsWithSingleNeighbor(t *testing.T) { - actors := []Actor{ + actors := []graph.Actor{ { ActorType: "device", - Match: Match{IPAddresses: []string{"10.0.0.1"}, SysName: "router-a"}, + Match: graph.Match{IPAddresses: []string{"10.0.0.1"}, SysName: "router-a"}, }, { ActorType: "segment", - Match: Match{Hostnames: []string{"segment:orphan"}}, + Match: graph.Match{Hostnames: []string{"segment:orphan"}}, }, } - links := []Link{ + links := []graph.Link{ { Protocol: "bridge", - Src: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.0.1"}}}, - Dst: LinkEndpoint{Match: Match{Hostnames: []string{"segment:orphan"}}}, + Src: graph.LinkEndpoint{Match: graph.Match{IPAddresses: []string{"10.0.0.1"}}}, + Dst: graph.LinkEndpoint{Match: graph.Match{Hostnames: []string{"segment:orphan"}}}, }, } @@ -2890,8 +2892,8 @@ func findNeighborByProtocol(neighbors []map[string]any, protocol string) map[str return nil } -func findFDBLinksByEndpointMAC(links []Link, mac string) []Link { - out := make([]Link, 0) +func findFDBLinksByEndpointMAC(links []graph.Link, mac string) []graph.Link { + out := make([]graph.Link, 0) for _, link := range links { if link.Protocol != "fdb" { continue @@ -2903,8 +2905,8 @@ func findFDBLinksByEndpointMAC(links []Link, mac string) []Link { return out } -func findFDBLinksByEndpointIP(links []Link, ip string) []Link { - out := make([]Link, 0) +func findFDBLinksByEndpointIP(links []graph.Link, ip string) []graph.Link { + out := make([]graph.Link, 0) for _, link := range links { if link.Protocol != "fdb" { continue @@ -2916,7 +2918,7 @@ func findFDBLinksByEndpointIP(links []Link, ip string) []Link { return out } -func topologyLinkSignatures(links []Link) map[string]struct{} { +func topologyLinkSignatures(links []graph.Link) map[string]struct{} { out := make(map[string]struct{}, len(links)) for _, link := range links { srcKey := canonicalTopologyMatchKey(link.Src.Match) @@ -2937,8 +2939,8 @@ func topologyLinkSignatures(links []Link) map[string]struct{} { return out } -func findFDBLinksByDstSysName(links []Link, sysName string) []Link { - out := make([]Link, 0) +func findFDBLinksByDstSysName(links []graph.Link, sysName string) []graph.Link { + out := make([]graph.Link, 0) for _, link := range links { if link.Protocol != "fdb" { continue @@ -2951,7 +2953,7 @@ func findFDBLinksByDstSysName(links []Link, sysName string) []Link { return out } -func findActorByMatch(actors []Actor, match Match) *Actor { +func findActorByMatch(actors []graph.Actor, match graph.Match) *graph.Actor { target := canonicalTopologyMatchKey(match) if target == "" { return nil @@ -2964,7 +2966,7 @@ func findActorByMatch(actors []Actor, match Match) *Actor { return nil } -func findActorBySysName(actors []Actor, sysName string) *Actor { +func findActorBySysName(actors []graph.Actor, sysName string) *graph.Actor { for i := range actors { if actors[i].Match.SysName == sysName { return &actors[i] @@ -2973,7 +2975,7 @@ func findActorBySysName(actors []Actor, sysName string) *Actor { return nil } -func findActorByMAC(actors []Actor, mac string) *Actor { +func findActorByMAC(actors []graph.Actor, mac string) *graph.Actor { for i := range actors { if slices.Contains(actors[i].Match.MacAddresses, mac) { return &actors[i] @@ -2982,7 +2984,7 @@ func findActorByMAC(actors []Actor, mac string) *Actor { return nil } -func findActorByIP(actors []Actor, ip string) *Actor { +func findActorByIP(actors []graph.Actor, ip string) *graph.Actor { for i := range actors { if slices.Contains(actors[i].Match.IPAddresses, ip) { return &actors[i] @@ -2991,7 +2993,7 @@ func findActorByIP(actors []Actor, ip string) *Actor { return nil } -func findActorByType(actors []Actor, actorType string) *Actor { +func findActorByType(actors []graph.Actor, actorType string) *graph.Actor { for i := range actors { if actors[i].ActorType == actorType { return &actors[i] @@ -3000,7 +3002,7 @@ func findActorByType(actors []Actor, actorType string) *Actor { return nil } -func findLinkByProtocol(links []Link, protocol string) *Link { +func findLinkByProtocol(links []graph.Link, protocol string) *graph.Link { for i := range links { if links[i].Protocol == protocol { return &links[i] diff --git a/src/go/pkg/l2topology/topology_updater.go b/src/go/pkg/l2topology/topology_updater.go deleted file mode 100644 index 9211dcb962c82e..00000000000000 --- a/src/go/pkg/l2topology/topology_updater.go +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import "sync" - -// TopologyBuilderFunc builds the current topology snapshot. -type TopologyBuilderFunc func() (NetworkRouterTopology, error) - -// TopologyUpdater ports non-routing runtime semantics from Enlinkd TopologyUpdater. -type TopologyUpdater struct { - mu sync.RWMutex - - topology NetworkRouterTopology - builder TopologyBuilderFunc - parseFn func() bool - refreshFn func() - hasRun bool - forceRun bool - runFailed bool - lastFailed error -} - -// NewTopologyUpdater builds an updater. parseFn and refreshFn are optional. -func NewTopologyUpdater(builder TopologyBuilderFunc, parseFn func() bool, refreshFn func()) *TopologyUpdater { - if builder == nil { - return &TopologyUpdater{} - } - return &TopologyUpdater{ - builder: builder, - parseFn: parseFn, - refreshFn: refreshFn, - topology: NetworkRouterTopology{}, - } -} - -// RunSchedulable executes one discovery cycle. -func (u *TopologyUpdater) RunSchedulable() { - if u == nil || u.builder == nil { - return - } - - u.mu.Lock() - hasRun := u.hasRun - forceRun := u.forceRun - if forceRun { - u.forceRun = false - } - u.mu.Unlock() - - if !hasRun { - topology, err := u.builder() - u.mu.Lock() - defer u.mu.Unlock() - if err != nil { - u.runFailed = true - u.lastFailed = err - return - } - u.topology = CloneNetworkRouterTopology(topology) - u.hasRun = true - u.runFailed = false - u.lastFailed = nil - return - } - - parseUpdates := false - if u.parseFn != nil { - parseUpdates = u.parseFn() - } - if !parseUpdates && !forceRun { - return - } - if u.refreshFn != nil { - u.refreshFn() - } - - topology, err := u.builder() - u.mu.Lock() - defer u.mu.Unlock() - if err != nil { - u.runFailed = true - u.lastFailed = err - return - } - u.topology = CloneNetworkRouterTopology(topology) - u.runFailed = false - u.lastFailed = nil -} - -// GetTopology returns a non-blocking clone of the current topology. -func (u *TopologyUpdater) GetTopology() NetworkRouterTopology { - if u == nil { - return NetworkRouterTopology{} - } - u.mu.RLock() - topology := CloneNetworkRouterTopology(u.topology) - u.mu.RUnlock() - return topology -} - -// ForceRun requests recomputation on the next RunSchedulable call. -func (u *TopologyUpdater) ForceRun() { - if u == nil { - return - } - u.mu.Lock() - u.forceRun = true - u.mu.Unlock() -} - -// HasRun reports if the first successful run completed. -func (u *TopologyUpdater) HasRun() bool { - if u == nil { - return false - } - u.mu.RLock() - defer u.mu.RUnlock() - return u.hasRun -} - -// LastError returns the last builder error, if any. -func (u *TopologyUpdater) LastError() error { - if u == nil { - return nil - } - u.mu.RLock() - defer u.mu.RUnlock() - return u.lastFailed -} - -// CloneNetworkRouterTopology deep-copies topology structures. -func CloneNetworkRouterTopology(topology NetworkRouterTopology) NetworkRouterTopology { - out := NetworkRouterTopology{ - Vertices: append([]NetworkRouterVertex(nil), topology.Vertices...), - Edges: append([]NetworkRouterEdge(nil), topology.Edges...), - DefaultVertex: topology.DefaultVertex, - } - return out -} diff --git a/src/go/pkg/l2topology/topology_updater_test.go b/src/go/pkg/l2topology/topology_updater_test.go deleted file mode 100644 index 46d55192b47677..00000000000000 --- a/src/go/pkg/l2topology/topology_updater_test.go +++ /dev/null @@ -1,203 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package l2topology - -import ( - "errors" - "strconv" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -// Port of TopologyUpdaterIT.verifyGetTopologyAccessWhileDiscoveryInProgressDoesNotBlock. -func TestTopologyUpdater_GetTopologyDoesNotBlockDuringDiscovery(t *testing.T) { - var buildCalls atomic.Int64 - updater := NewTopologyUpdater(func() (NetworkRouterTopology, error) { - buildCalls.Add(1) - time.Sleep(1 * time.Second) - return NetworkRouterTopology{ - Vertices: []NetworkRouterVertex{ - {ID: "v1", Label: "Vertex 1"}, - {ID: "v2", Label: "Vertex 2"}, - }, - }, nil - }, nil, nil) - - discoveryDone := make(chan struct{}) - go func() { - updater.RunSchedulable() - close(discoveryDone) - }() - - type topoResult struct { - vertices int - edges int - } - - getCurrent := func() <-chan topoResult { - done := make(chan topoResult, 1) - go func() { - current := updater.GetTopology() - done <- topoResult{ - vertices: len(current.Vertices), - edges: len(current.Edges), - } - }() - return done - } - - select { - case <-time.After(1 * time.Second): - t.Fatalf("get topology timed out while discovery was running") - case result := <-getCurrent(): - require.Equal(t, 0, result.vertices) - require.Equal(t, 0, result.edges) - } - - select { - case <-time.After(3 * time.Second): - t.Fatalf("discovery did not complete") - case <-discoveryDone: - } - - select { - case <-time.After(1 * time.Second): - t.Fatalf("get topology timed out after discovery") - case result := <-getCurrent(): - require.Equal(t, 2, result.vertices) - require.Equal(t, 0, result.edges) - } - - require.Equal(t, int64(1), buildCalls.Load()) -} - -func TestTopologyUpdaterFirstRunFailureAndRecovery(t *testing.T) { - var calls int - boom := errors.New("boom") - - updater := NewTopologyUpdater(func() (NetworkRouterTopology, error) { - calls++ - if calls == 1 { - return NetworkRouterTopology{}, boom - } - return NetworkRouterTopology{ - Vertices: []NetworkRouterVertex{{ID: "node-1", Label: "node-1"}}, - DefaultVertex: "node-1", - }, nil - }, nil, nil) - - require.False(t, updater.HasRun()) - require.Nil(t, updater.LastError()) - - updater.RunSchedulable() - - require.False(t, updater.HasRun()) - require.ErrorIs(t, updater.LastError(), boom) - require.Empty(t, updater.GetTopology().Vertices) - - updater.RunSchedulable() - - require.True(t, updater.HasRun()) - require.NoError(t, updater.LastError()) - topology := updater.GetTopology() - require.Equal(t, "node-1", topology.DefaultVertex) - require.Len(t, topology.Vertices, 1) - - topology.Vertices[0].ID = "mutated" - require.Equal(t, "node-1", updater.GetTopology().Vertices[0].ID) -} - -func TestTopologyUpdaterForceRunAndParseUpdatesTriggerRefresh(t *testing.T) { - var ( - builderCalls int - parseCalls int - refreshCalls int - parseUpdates bool - ) - - updater := NewTopologyUpdater( - func() (NetworkRouterTopology, error) { - builderCalls++ - vertexID := strconv.Itoa(builderCalls) - return NetworkRouterTopology{ - Vertices: []NetworkRouterVertex{{ID: vertexID, Label: "node-" + vertexID}}, - DefaultVertex: vertexID, - }, nil - }, - func() bool { - parseCalls++ - return parseUpdates - }, - func() { - refreshCalls++ - }, - ) - - updater.RunSchedulable() - require.True(t, updater.HasRun()) - require.Equal(t, 1, builderCalls) - require.Equal(t, 0, parseCalls) - require.Equal(t, 0, refreshCalls) - require.Equal(t, "1", updater.GetTopology().DefaultVertex) - - updater.RunSchedulable() - require.Equal(t, 1, builderCalls) - require.Equal(t, 1, parseCalls) - require.Equal(t, 0, refreshCalls) - require.Equal(t, "1", updater.GetTopology().DefaultVertex) - - updater.ForceRun() - updater.RunSchedulable() - require.Equal(t, 2, builderCalls) - require.Equal(t, 2, parseCalls) - require.Equal(t, 1, refreshCalls) - require.Equal(t, "2", updater.GetTopology().DefaultVertex) - - parseUpdates = true - updater.RunSchedulable() - require.Equal(t, 3, builderCalls) - require.Equal(t, 3, parseCalls) - require.Equal(t, 2, refreshCalls) - require.Equal(t, "3", updater.GetTopology().DefaultVertex) - require.NoError(t, updater.LastError()) -} - -func TestTopologyUpdaterRefreshFailureKeepsLastSuccessfulTopology(t *testing.T) { - var ( - builderCalls int - parseUpdates bool - ) - boom := errors.New("refresh failed") - - updater := NewTopologyUpdater( - func() (NetworkRouterTopology, error) { - builderCalls++ - if builderCalls == 2 { - return NetworkRouterTopology{}, boom - } - vertexID := strconv.Itoa(builderCalls) - return NetworkRouterTopology{ - Vertices: []NetworkRouterVertex{{ID: vertexID, Label: "node-" + vertexID}}, - DefaultVertex: vertexID, - }, nil - }, - func() bool { - return parseUpdates - }, - nil, - ) - - updater.RunSchedulable() - require.True(t, updater.HasRun()) - require.Equal(t, "1", updater.GetTopology().DefaultVertex) - require.NoError(t, updater.LastError()) - - parseUpdates = true - updater.RunSchedulable() - require.True(t, updater.HasRun()) - require.ErrorIs(t, updater.LastError(), boom) - require.Equal(t, "1", updater.GetTopology().DefaultVertex) -} diff --git a/src/go/pkg/topology/graph/doc.go b/src/go/pkg/topology/graph/doc.go new file mode 100644 index 00000000000000..de1e47a280d9e8 --- /dev/null +++ b/src/go/pkg/topology/graph/doc.go @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// Package graph defines neutral, multi-layer topology graph DTOs. +package graph diff --git a/src/go/pkg/l2topology/graph_types.go b/src/go/pkg/topology/graph/graph_types.go similarity index 99% rename from src/go/pkg/l2topology/graph_types.go rename to src/go/pkg/topology/graph/graph_types.go index 125cfbd52c84fd..2335ea11610303 100644 --- a/src/go/pkg/l2topology/graph_types.go +++ b/src/go/pkg/topology/graph/graph_types.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -package l2topology +package graph import "time" diff --git a/src/go/pkg/topology/graph/graph_types_test.go b/src/go/pkg/topology/graph/graph_types_test.go new file mode 100644 index 00000000000000..f79b869b3975bd --- /dev/null +++ b/src/go/pkg/topology/graph/graph_types_test.go @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package graph + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestGraphJSONShape(t *testing.T) { + tm := time.Date(2026, time.June, 20, 8, 9, 10, 0, time.UTC) + payload := Graph{ + SchemaVersion: "2.0", + Source: "snmp", + Layer: "3", + AgentID: "agent-id", + CollectedAt: tm, + View: "summary", + IPPolicy: &IPPolicy{ + PublicAllowlist: []string{"10.0.0.0/8"}, + LiveTopN: &LiveTopN{Enabled: true, Limit: 10, SortBy: "bytes"}, + }, + Actors: []Actor{{ + ActorID: "actor-a", + ActorType: "router", + Layer: "3", + Source: "snmp", + Match: Match{ + ChassisIDs: []string{"chassis-a"}, + MacAddresses: []string{"00:11:22:33:44:55"}, + IPAddresses: []string{"10.0.0.1"}, + Hostnames: []string{"router-a"}, + DNSNames: []string{"router-a.example.net"}, + SysObjectID: "1.3.6.1.4.1.8072.3.2.10", + SysName: "router-a", + NetdataNodeID: "node-id", + NetdataMachineGUID: "machine-guid", + CloudInstanceID: "instance-id", + CloudAccountID: "account-id", + ContainerIDs: []string{"container-id"}, + PodNames: []string{"pod-a"}, + NamespaceIDs: []string{"namespace-a"}, + }, + ParentMatch: &Match{SysName: "parent"}, + Attributes: map[string]any{"role": "core"}, + Derived: map[string]any{"score": float64(1)}, + Labels: map[string]string{"site": "lab"}, + Tables: map[string][]map[string]any{"ports": {{"if_name": "eth0"}}}, + }}, + Links: []Link{{ + Layer: "3", + Protocol: "ospf", + LinkType: "ospf_adjacency", + Direction: "bidirectional", + State: "up", + SrcActorID: "actor-a", + DstActorID: "actor-b", + Src: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.0.1"}}, Attributes: map[string]any{"if_name": "eth0"}}, + Dst: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.0.2"}}, Attributes: map[string]any{"if_name": "eth1"}}, + DiscoveredAt: &tm, + LastSeen: &tm, + Metrics: map[string]any{"cost": float64(10)}, + }}, + Flows: []Flow{{ + Timestamp: tm, + DurationSec: 60, + Exporter: &FlowExporter{IP: "10.0.0.1", Name: "router-a", SamplingRate: 1000, FlowVersion: "v9"}, + Src: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.0.10"}}}, + Dst: LinkEndpoint{Match: Match{IPAddresses: []string{"10.0.0.20"}}}, + Key: map[string]any{"protocol": "tcp"}, + Metrics: map[string]any{"bytes": float64(42)}, + }}, + Stats: map[string]any{"links": float64(1)}, + Metrics: map[string]any{"refresh": float64(1)}, + } + + bs, err := json.Marshal(payload) + require.NoError(t, err) + + require.JSONEq(t, `{ + "schema_version":"2.0", + "source":"snmp", + "layer":"3", + "agent_id":"agent-id", + "collected_at":"2026-06-20T08:09:10Z", + "view":"summary", + "ip_policy":{ + "public_allowlist":["10.0.0.0/8"], + "live_top_n":{"enabled":true,"limit":10,"sort_by":"bytes"} + }, + "actors":[{ + "actor_id":"actor-a", + "actor_type":"router", + "layer":"3", + "source":"snmp", + "match":{ + "chassis_ids":["chassis-a"], + "mac_addresses":["00:11:22:33:44:55"], + "ip_addresses":["10.0.0.1"], + "hostnames":["router-a"], + "dns_names":["router-a.example.net"], + "sys_object_id":"1.3.6.1.4.1.8072.3.2.10", + "sys_name":"router-a", + "netdata_node_id":"node-id", + "netdata_machine_guid":"machine-guid", + "cloud_instance_id":"instance-id", + "cloud_account_id":"account-id", + "container_ids":["container-id"], + "pod_names":["pod-a"], + "namespace_ids":["namespace-a"] + }, + "parent_match":{"sys_name":"parent"}, + "attributes":{"role":"core"}, + "derived":{"score":1}, + "labels":{"site":"lab"}, + "tables":{"ports":[{"if_name":"eth0"}]} + }], + "links":[{ + "layer":"3", + "protocol":"ospf", + "link_type":"ospf_adjacency", + "direction":"bidirectional", + "state":"up", + "src_actor_id":"actor-a", + "dst_actor_id":"actor-b", + "src":{"match":{"ip_addresses":["10.0.0.1"]},"attributes":{"if_name":"eth0"}}, + "dst":{"match":{"ip_addresses":["10.0.0.2"]},"attributes":{"if_name":"eth1"}}, + "discovered_at":"2026-06-20T08:09:10Z", + "last_seen":"2026-06-20T08:09:10Z", + "metrics":{"cost":10} + }], + "flows":[{ + "timestamp":"2026-06-20T08:09:10Z", + "duration_sec":60, + "exporter":{"ip":"10.0.0.1","name":"router-a","sampling_rate":1000,"flow_version":"v9"}, + "src":{"match":{"ip_addresses":["10.0.0.10"]}}, + "dst":{"match":{"ip_addresses":["10.0.0.20"]}}, + "key":{"protocol":"tcp"}, + "metrics":{"bytes":42} + }], + "stats":{"links":1}, + "metrics":{"refresh":1} + }`, string(bs)) +} diff --git a/src/go/pkg/topology/netaddr/netaddr.go b/src/go/pkg/topology/netaddr/netaddr.go new file mode 100644 index 00000000000000..d0f212ea1634ca --- /dev/null +++ b/src/go/pkg/topology/netaddr/netaddr.go @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// Package netaddr contains topology network-address helpers. +package netaddr + +import ( + "fmt" + "net/netip" +) + +// NetworkAddress returns ip&mask for matching IP families. +func NetworkAddress(ip, mask netip.Addr) (netip.Addr, bool) { + ip = ip.Unmap() + mask = mask.Unmap() + if !ip.IsValid() || !mask.IsValid() || ip.Is4() != mask.Is4() { + return netip.Addr{}, false + } + ib := addrBytes(ip) + mb := addrBytes(mask) + if len(ib) != len(mb) { + return netip.Addr{}, false + } + out := make([]byte, len(ib)) + for i := range ib { + out[i] = ib[i] & mb[i] + } + addr, ok := netip.AddrFromSlice(out) + if !ok { + return netip.Addr{}, false + } + return addr.Unmap(), true +} + +// MaskToCIDRPrefix converts a contiguous IP mask to CIDR prefix length. +func MaskToCIDRPrefix(mask netip.Addr) (int, error) { + mask = mask.Unmap() + if !mask.IsValid() { + return 0, fmt.Errorf("invalid mask") + } + foundZero := false + cidr := 0 + for _, value := range addrBytes(mask) { + k := int(value) + if foundZero && k != 0 { + return 0, fmt.Errorf("invalid mask %q", mask) + } + switch k { + case 255: + cidr += 8 + case 254: + cidr += 7 + foundZero = true + case 252: + cidr += 6 + foundZero = true + case 248: + cidr += 5 + foundZero = true + case 240: + cidr += 4 + foundZero = true + case 224: + cidr += 3 + foundZero = true + case 192: + cidr += 2 + foundZero = true + case 128: + cidr += 1 + foundZero = true + case 0: + foundZero = true + default: + return 0, fmt.Errorf("invalid mask %q", mask) + } + } + return cidr, nil +} + +func addrBytes(addr netip.Addr) []byte { + if !addr.IsValid() { + return nil + } + if addr.Is4() { + a := addr.As4() + return a[:] + } + a := addr.As16() + return a[:] +} diff --git a/src/go/pkg/topology/netaddr/netaddr_test.go b/src/go/pkg/topology/netaddr/netaddr_test.go new file mode 100644 index 00000000000000..6160a4800772c5 --- /dev/null +++ b/src/go/pkg/topology/netaddr/netaddr_test.go @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package netaddr + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNetworkAddress(t *testing.T) { + tests := map[string]struct { + ip string + mask string + want string + wantOK bool + }{ + "ipv4": { + ip: "10.0.0.2", + mask: "255.255.255.252", + want: "10.0.0.0", + wantOK: true, + }, + "ipv4 mapped": { + ip: "::ffff:10.0.0.2", + mask: "::ffff:255.255.255.252", + want: "10.0.0.0", + wantOK: true, + }, + "family mismatch": { + ip: "10.0.0.2", + mask: "ffff:ffff:ffff:ffff::", + wantOK: false, + }, + "ipv6": { + ip: "2001:db8::2", + mask: "ffff:ffff:ffff:ffff::", + want: "2001:db8::", + wantOK: true, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + network, ok := NetworkAddress(netip.MustParseAddr(tt.ip), netip.MustParseAddr(tt.mask)) + require.Equal(t, tt.wantOK, ok) + if tt.wantOK { + require.Equal(t, tt.want, network.String()) + } + }) + } +} + +func TestMaskToCIDRPrefix(t *testing.T) { + tests := map[string]struct { + mask string + want int + wantErr bool + }{ + "ipv4 /30": { + mask: "255.255.255.252", + want: 30, + }, + "ipv4 /0": { + mask: "0.0.0.0", + want: 0, + }, + "ipv6 /64": { + mask: "ffff:ffff:ffff:ffff::", + want: 64, + }, + "non-contiguous": { + mask: "255.0.255.0", + wantErr: true, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + prefix, err := MaskToCIDRPrefix(netip.MustParseAddr(tt.mask)) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, prefix) + }) + } +} diff --git a/src/go/plugin/go.d/collector/snmp_topology/func_topology_v1_ospf_neighbors.go b/src/go/plugin/go.d/collector/snmp_topology/func_topology_v1_ospf_neighbors.go index e7907d67cee534..e755131b5f6a0d 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/func_topology_v1_ospf_neighbors.go +++ b/src/go/plugin/go.d/collector/snmp_topology/func_topology_v1_ospf_neighbors.go @@ -27,8 +27,8 @@ func buildSNMPTopologyV1OSPFNeighborsTable( for i, row := range rows { actorRefs[i] = row.actorRef remoteActors[i] = nullableActorRef(actorIndex, row.values["remote_actor_id"]) - localRouterIDs[i] = nullableStringRef(stringsDict, normalizeOSPFRouterID(topologyV1ScalarLabelValue(row.values["local_router_id"]))) - neighborRouterIDs[i] = nullableStringRef(stringsDict, normalizeOSPFRouterID(topologyV1ScalarLabelValue(row.values["neighbor_router_id"]))) + localRouterIDs[i] = nullableStringRef(stringsDict, normalizeTopologyRouterID(topologyV1ScalarLabelValue(row.values["local_router_id"]))) + neighborRouterIDs[i] = nullableStringRef(stringsDict, normalizeTopologyRouterID(topologyV1ScalarLabelValue(row.values["neighbor_router_id"]))) neighborIPs[i] = nullableStringRef(stringsDict, normalizeNonUnspecifiedIPAddress(topologyV1ScalarLabelValue(row.values["neighbor_ip"]))) states[i] = nullableStringRef(stringsDict, normalizeOSPFNeighborState(topologyV1ScalarLabelValue(row.values["state"]))) localIPs[i] = nullableStringRef(stringsDict, normalizeNonUnspecifiedIPAddress(topologyV1ScalarLabelValue(row.values["local_ip"]))) diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_actor_tables.go b/src/go/plugin/go.d/collector/snmp_topology/topology_actor_tables.go new file mode 100644 index 00000000000000..f7a0e91f6459fc --- /dev/null +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_actor_tables.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package snmptopology + +import "strings" + +func attachTopologyActorTableRows(data *topologyData, tableName string, rowsByActor map[string][]map[string]any, sortRows func([]map[string]any)) { + if data == nil || tableName == "" || len(rowsByActor) == 0 { + return + } + for i := range data.Actors { + actor := &data.Actors[i] + rows := rowsByActor[strings.TrimSpace(actor.ActorID)] + if len(rows) == 0 { + continue + } + if sortRows != nil { + sortRows(rows) + } + if actor.Tables == nil { + actor.Tables = make(map[string][]map[string]any) + } + actor.Tables[tableName] = rows + } +} diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_bgp_links.go b/src/go/plugin/go.d/collector/snmp_topology/topology_bgp_links.go index ab76b6dfd4c839..65aed9c45511cd 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_bgp_links.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_bgp_links.go @@ -23,8 +23,7 @@ type topologyBGPEnrichmentStats struct { func applyTopologyBGPAdjacencyEnrichment(data *topologyData, aggregate topologyObservationAggregate) topologyBGPEnrichmentStats { var stats topologyBGPEnrichmentStats if data == nil || len(aggregate.bgpPeers) == 0 { - recordTopologyBGPEnrichmentStats(data, stats) - return stats + return finishTopologyBGPAdjacencyEnrichment(data, stats) } resolver := newTopologyBGPActorResolver(data, aggregate) @@ -71,10 +70,14 @@ func applyTopologyBGPAdjacencyEnrichment(data *topologyData, aggregate topologyO stats.emittedLinks++ } - attachTopologyBGPPeerRows(data, peerRowsByActor) + attachTopologyActorTableRows(data, "bgp_peers", peerRowsByActor, sortTopologyBGPPeerRows) sort.Slice(data.Links, func(i, j int) bool { return topologyLinkSortKey(data.Links[i]) < topologyLinkSortKey(data.Links[j]) }) + return finishTopologyBGPAdjacencyEnrichment(data, stats) +} + +func finishTopologyBGPAdjacencyEnrichment(data *topologyData, stats topologyBGPEnrichmentStats) topologyBGPEnrichmentStats { recordTopologyBGPEnrichmentStats(data, stats) recomputeTopologyLinkStats(data) return stats @@ -93,7 +96,7 @@ func newTopologyBGPActorResolver(data *topologyData, aggregate topologyObservati for _, row := range aggregate.l3Interfaces { ref, ok := resolver.l3.resolveDeviceID(row.DeviceID) if ok { - resolver.l3.addUniqueIP(row.IP, ref) + resolver.l3.addUniqueIPAddress(row.IP, ref) } } for _, row := range aggregate.bgpPeers { @@ -102,7 +105,7 @@ func newTopologyBGPActorResolver(data *topologyData, aggregate topologyObservati continue } resolver.addUniqueIdentifier(row.LocalIdentifier, ref) - resolver.l3.addUniqueIP(row.LocalIP, ref) + resolver.l3.addUniqueIPAddress(row.LocalIP, ref) } return resolver } @@ -115,13 +118,7 @@ func (r topologyBGPActorResolver) resolveBGPPeer(row topologyBGPPeer) (topologyL if ref, ok := r.byIdentifier[normalizeBGPRouterID(row.PeerIdentifier)]; ok && ref.actorID != "" { return ref, true } - if ref, ok := r.l3.byRouterID[normalizeOSPFRouterID(row.PeerIdentifier)]; ok && ref.actorID != "" { - return ref, true - } - if ref, ok := r.l3.byIP[normalizeNonUnspecifiedIPAddress(row.NeighborIP)]; ok && ref.actorID != "" { - return ref, true - } - return topologyL3ActorRef{}, false + return r.l3.resolveRouterEndpoint(row.PeerIdentifier, row.NeighborIP) } func (r topologyBGPActorResolver) addUniqueIdentifier(identifier string, ref topologyL3ActorRef) { @@ -251,24 +248,6 @@ func topologyBGPPeerActorRow(row topologyBGPPeer) map[string]any { return out } -func attachTopologyBGPPeerRows(data *topologyData, rowsByActor map[string][]map[string]any) { - if data == nil || len(rowsByActor) == 0 { - return - } - for i := range data.Actors { - actor := &data.Actors[i] - rows := rowsByActor[strings.TrimSpace(actor.ActorID)] - if len(rows) == 0 { - continue - } - sortTopologyBGPPeerRows(rows) - if actor.Tables == nil { - actor.Tables = make(map[string][]map[string]any) - } - actor.Tables["bgp_peers"] = rows - } -} - func existingTopologyBGPLinkKeys(links []topologyLink) map[string]struct{} { seen := make(map[string]struct{}) for _, link := range links { @@ -317,19 +296,3 @@ func recordTopologyBGPEnrichmentStats(data *topologyData, stats topologyBGPEnric data.Stats["bgp_adjacency_suppressed_self_actor"] = stats.suppressedSelfActor data.Stats["bgp_adjacency_suppressed_duplicate_link"] = stats.suppressedDuplicateLink } - -func recomputeTopologyBGPVisibleLinkStats(data *topologyData) { - if data == nil || data.Stats == nil { - return - } - if _, ok := data.Stats["bgp_adjacency_emitted_links"]; !ok { - return - } - count := 0 - for _, link := range data.Links { - if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(link.LinkType, link.Protocol)), topologyBGPAdjacencyLinkType) { - count++ - } - } - data.Stats["bgp_adjacency_visible_links"] = count -} diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_bgp_peers.go b/src/go/plugin/go.d/collector/snmp_topology/topology_bgp_peers.go index b2163b2be1be60..dc947d1e14b9b7 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_bgp_peers.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_bgp_peers.go @@ -115,17 +115,6 @@ func (c *topologyCache) snapshotBGPPeers(localDeviceID string) []topologyBGPPeer return rows } -func normalizeBGPRouterID(value string) string { - value = strings.TrimSpace(value) - if value == "" { - return "" - } - if ip := normalizeNonUnspecifiedIPAddress(value); ip != "" { - return ip - } - return value -} - func topologyBGPAdminStatus(row ddsnmp.BGPRow) string { if !row.Admin.Enabled.Has { return "" diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_cache_profile_tags.go b/src/go/plugin/go.d/collector/snmp_topology/topology_cache_profile_tags.go index 34276dc7a2d9e4..4b462d0b8b6a7a 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_cache_profile_tags.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_cache_profile_tags.go @@ -64,7 +64,7 @@ func (c *topologyCache) applyOSPFProfileTags(tags map[string]string) { if c == nil || len(tags) == 0 { return } - if v := normalizeOSPFRouterID(tags[tagOSPFRouterID]); v != "" { + if v := normalizeTopologyRouterID(tags[tagOSPFRouterID]); v != "" { c.localDevice.OSPFRouterID = v c.localDevice.Labels = ensureLabels(c.localDevice.Labels) c.localDevice.Labels[tagOSPFRouterID] = v diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_l3_actor_resolver.go b/src/go/plugin/go.d/collector/snmp_topology/topology_l3_actor_resolver.go index ba49911e7f545b..3f32a4f6f804c0 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_l3_actor_resolver.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_l3_actor_resolver.go @@ -46,7 +46,7 @@ func newTopologyL3ActorResolver(data *topologyData, snapshots []topologyObservat resolver.addUniqueDeviceID(deviceID, ref) } for _, ip := range normalizedMatchIPs(actor.Match) { - resolver.addUniqueIP(ip, ref) + resolver.addUniqueIPAddress(ip, ref) } for _, routerID := range topologyL3ActorRouterIDs(actor) { resolver.addUniqueRouterID(routerID, ref) @@ -83,10 +83,7 @@ func (r topologyL3ActorResolver) resolve(row topologyL3Interface) (topologyL3Act if ref, ok := r.byActorID[strings.TrimSpace(row.DeviceID)]; ok && ref.actorID != "" { return ref, true } - if ref, ok := r.byIP[normalizeIPAddress(row.IP)]; ok && ref.actorID != "" { - return ref, true - } - return topologyL3ActorRef{}, false + return r.resolveIPAddress(row.IP) } func (r topologyL3ActorResolver) resolveDeviceID(deviceID string) (topologyL3ActorRef, bool) { @@ -99,11 +96,29 @@ func (r topologyL3ActorResolver) resolveDeviceID(deviceID string) (topologyL3Act return topologyL3ActorRef{}, false } -func (r topologyL3ActorResolver) resolveOSPFNeighbor(row topologyOSPFNeighbor) (topologyL3ActorRef, bool) { - if ref, ok := r.byRouterID[normalizeOSPFRouterID(row.NeighborRouterID)]; ok && ref.actorID != "" { +func (r topologyL3ActorResolver) resolveRouterEndpoint(routerID, ip string) (topologyL3ActorRef, bool) { + if ref, ok := r.resolveRouterID(routerID); ok { + return ref, true + } + return r.resolveNonUnspecifiedIPAddress(ip) +} + +func (r topologyL3ActorResolver) resolveRouterID(routerID string) (topologyL3ActorRef, bool) { + if ref, ok := r.byRouterID[normalizeTopologyRouterID(routerID)]; ok && ref.actorID != "" { return ref, true } - if ref, ok := r.byIP[normalizeNonUnspecifiedIPAddress(row.NeighborIP)]; ok && ref.actorID != "" { + return topologyL3ActorRef{}, false +} + +func (r topologyL3ActorResolver) resolveIPAddress(ip string) (topologyL3ActorRef, bool) { + if ref, ok := r.byIP[normalizeIPAddress(ip)]; ok && ref.actorID != "" { + return ref, true + } + return topologyL3ActorRef{}, false +} + +func (r topologyL3ActorResolver) resolveNonUnspecifiedIPAddress(ip string) (topologyL3ActorRef, bool) { + if ref, ok := r.byIP[normalizeNonUnspecifiedIPAddress(ip)]; ok && ref.actorID != "" { return ref, true } return topologyL3ActorRef{}, false @@ -124,7 +139,7 @@ func (r topologyL3ActorResolver) addUniqueDeviceID(deviceID string, ref topology } } -func (r topologyL3ActorResolver) addUniqueIP(ip string, ref topologyL3ActorRef) { +func (r topologyL3ActorResolver) addUniqueIPAddress(ip string, ref topologyL3ActorRef) { ip = normalizeIPAddress(ip) if ip == "" || ref.actorID == "" { return @@ -140,7 +155,7 @@ func (r topologyL3ActorResolver) addUniqueIP(ip string, ref topologyL3ActorRef) } func (r topologyL3ActorResolver) addUniqueRouterID(routerID string, ref topologyL3ActorRef) { - routerID = normalizeOSPFRouterID(routerID) + routerID = normalizeTopologyRouterID(routerID) if routerID == "" || ref.actorID == "" { return } @@ -156,10 +171,10 @@ func (r topologyL3ActorResolver) addUniqueRouterID(routerID string, ref topology func topologyL3ActorRouterIDs(actor topologyActor) []string { values := make([]string, 0, 2) - if routerID := normalizeOSPFRouterID(anyStringValue(actor.Attributes[tagOSPFRouterID])); routerID != "" { + if routerID := normalizeTopologyRouterID(anyStringValue(actor.Attributes[tagOSPFRouterID])); routerID != "" { values = append(values, routerID) } - if routerID := normalizeOSPFRouterID(actor.Labels[tagOSPFRouterID]); routerID != "" { + if routerID := normalizeTopologyRouterID(actor.Labels[tagOSPFRouterID]); routerID != "" { values = append(values, routerID) } return values diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_l3_links.go b/src/go/plugin/go.d/collector/snmp_topology/topology_l3_links.go index f57a19458366ca..c237def165932c 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_l3_links.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_l3_links.go @@ -21,15 +21,13 @@ type topologyL3EnrichmentStats struct { func applyTopologyL3SubnetEnrichment(data *topologyData, aggregate topologyObservationAggregate) topologyL3EnrichmentStats { var stats topologyL3EnrichmentStats if data == nil || len(aggregate.l3Interfaces) == 0 { - recordTopologyL3EnrichmentStats(data, stats) - return stats + return finishTopologyL3SubnetEnrichment(data, stats) } adjacencies, subnetStats := buildTopologyL3SubnetAdjacencies(aggregate.l3Interfaces) stats.subnetStats = subnetStats if len(adjacencies) == 0 { - recordTopologyL3EnrichmentStats(data, stats) - return stats + return finishTopologyL3SubnetEnrichment(data, stats) } resolver := newTopologyL3ActorResolver(data, aggregate.snapshots) @@ -63,6 +61,10 @@ func applyTopologyL3SubnetEnrichment(data *topologyData, aggregate topologyObser sort.Slice(data.Links, func(i, j int) bool { return topologyLinkSortKey(data.Links[i]) < topologyLinkSortKey(data.Links[j]) }) + return finishTopologyL3SubnetEnrichment(data, stats) +} + +func finishTopologyL3SubnetEnrichment(data *topologyData, stats topologyL3EnrichmentStats) topologyL3EnrichmentStats { recordTopologyL3EnrichmentStats(data, stats) recomputeTopologyLinkStats(data) return stats @@ -170,5 +172,4 @@ func recordTopologyL3EnrichmentStats(data *topologyData, stats topologyL3Enrichm data.Stats["l3_subnet_suppressed_unresolved_actor"] = stats.suppressedUnresolvedActor data.Stats["l3_subnet_suppressed_self_actor"] = stats.suppressedSelfActor data.Stats["l3_subnet_suppressed_duplicate_link"] = stats.suppressedDuplicateLink - recomputeTopologyL3VisibleLinkStats(data) } diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_l3_subnets.go b/src/go/plugin/go.d/collector/snmp_topology/topology_l3_subnets.go index 8bb8cf6df2cf92..841d03a0bbe0d3 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_l3_subnets.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_l3_subnets.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - topologyengine "github.com/netdata/netdata/go/plugins/pkg/l2topology" + "github.com/netdata/netdata/go/plugins/pkg/topology/netaddr" ) type topologyL3SubnetAdjacency struct { @@ -118,11 +118,11 @@ func topologyL3SubnetGroupForInterface(row topologyL3Interface) (*topologyL3Subn if err != nil || !netmask.Is4() { return nil, false } - network, ok := topologyengine.NetworkAddress(ip, netmask) + network, ok := netaddr.NetworkAddress(ip, netmask) if !ok { return nil, false } - prefix, err := topologyengine.MaskToCIDRPrefix(netmask) + prefix, err := netaddr.MaskToCIDRPrefix(netmask) if err != nil { return nil, false } diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_local_actor_attrs.go b/src/go/plugin/go.d/collector/snmp_topology/topology_local_actor_attrs.go index f38f3b36ecc31d..1fb0e86ca6f320 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_local_actor_attrs.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_local_actor_attrs.go @@ -40,7 +40,7 @@ func populateLocalActorAttributes(attrs map[string]any, local topologyDevice) ma if model := strings.TrimSpace(local.Model); model != "" { attrs["model"] = model } - if routerID := normalizeOSPFRouterID(local.OSPFRouterID); routerID != "" { + if routerID := normalizeTopologyRouterID(local.OSPFRouterID); routerID != "" { attrs[tagOSPFRouterID] = routerID } if serial := strings.TrimSpace(local.SerialNumber); serial != "" { diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_management_helpers.go b/src/go/plugin/go.d/collector/snmp_topology/topology_management_helpers.go index 42426efc4fbbb2..742deff918ef57 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_management_helpers.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_management_helpers.go @@ -60,10 +60,10 @@ func normalizeTopologyDevice(dev topologyDevice) topologyDevice { if value := topologyMetadataValue(dev.Labels, topologyMetadataAliasModel); value != "" && dev.Model == "" { dev.Model = value } - if value := normalizeOSPFRouterID(dev.Labels[tagOSPFRouterID]); value != "" && dev.OSPFRouterID == "" { + if value := normalizeTopologyRouterID(dev.Labels[tagOSPFRouterID]); value != "" && dev.OSPFRouterID == "" { dev.OSPFRouterID = value } - if value := normalizeOSPFRouterID(dev.OSPFRouterID); value != "" { + if value := normalizeTopologyRouterID(dev.OSPFRouterID); value != "" { dev.OSPFRouterID = value setTopologyMetadataLabelIfMissing(dev.Labels, tagOSPFRouterID, value) } diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_links.go b/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_links.go index facfcc61fcbd4b..5767b5223b5e79 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_links.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_links.go @@ -18,14 +18,12 @@ type topologyOSPFEnrichmentStats struct { suppressedUnresolvedNeighbor int suppressedSelfActor int suppressedDuplicateLink int - suppressedL3SubnetOverlap int } func applyTopologyOSPFAdjacencyEnrichment(data *topologyData, aggregate topologyObservationAggregate) topologyOSPFEnrichmentStats { var stats topologyOSPFEnrichmentStats if data == nil || len(aggregate.ospfNeighbors) == 0 { - recordTopologyOSPFEnrichmentStats(data, stats) - return stats + return finishTopologyOSPFAdjacencyEnrichment(data, stats) } resolver := newTopologyL3ActorResolver(data, aggregate.snapshots) @@ -35,7 +33,7 @@ func applyTopologyOSPFAdjacencyEnrichment(data *topologyData, aggregate topology for _, row := range aggregate.ospfNeighbors { stats.observedRows++ localRef, localOK := resolver.resolveDeviceID(row.DeviceID) - remoteRef, remoteOK := resolver.resolveOSPFNeighbor(row) + remoteRef, remoteOK := resolver.resolveRouterEndpoint(row.NeighborRouterID, row.NeighborIP) if localOK { modalRow := topologyOSPFNeighborActorRow(row) if remoteOK { @@ -70,15 +68,18 @@ func applyTopologyOSPFAdjacencyEnrichment(data *topologyData, aggregate topology continue } seen[key] = struct{}{} - stats.suppressedL3SubnetOverlap += suppressMatchingTopologyL3SubnetLinks(data, link) data.Links = append(data.Links, link) stats.emittedLinks++ } - attachTopologyOSPFNeighborRows(data, neighborRowsByActor) + attachTopologyActorTableRows(data, "ospf_neighbors", neighborRowsByActor, sortTopologyOSPFNeighborRows) sort.Slice(data.Links, func(i, j int) bool { return topologyLinkSortKey(data.Links[i]) < topologyLinkSortKey(data.Links[j]) }) + return finishTopologyOSPFAdjacencyEnrichment(data, stats) +} + +func finishTopologyOSPFAdjacencyEnrichment(data *topologyData, stats topologyOSPFEnrichmentStats) topologyOSPFEnrichmentStats { recordTopologyOSPFEnrichmentStats(data, stats) recomputeTopologyLinkStats(data) return stats @@ -109,7 +110,7 @@ func topologyOSPFEndpointAttributes(routerID, ip string, row topologyOSPFNeighbo attrs := map[string]any{ "source": "ospf_mib", } - if normalizedRouterID := normalizeOSPFRouterID(routerID); normalizedRouterID != "" { + if normalizedRouterID := normalizeTopologyRouterID(routerID); normalizedRouterID != "" { attrs["router_id"] = normalizedRouterID } if normalizedIP := normalizeNonUnspecifiedIPAddress(ip); normalizedIP != "" { @@ -129,10 +130,10 @@ func topologyOSPFLinkMetrics(row topologyOSPFNeighbor) map[string]any { "attachment_mode": "logical_l3_ospf", "state": "full", } - if routerID := normalizeOSPFRouterID(row.LocalRouterID); routerID != "" { + if routerID := normalizeTopologyRouterID(row.LocalRouterID); routerID != "" { metrics["local_router_id"] = routerID } - if routerID := normalizeOSPFRouterID(row.NeighborRouterID); routerID != "" { + if routerID := normalizeTopologyRouterID(row.NeighborRouterID); routerID != "" { metrics["neighbor_router_id"] = routerID } if ip := normalizeNonUnspecifiedIPAddress(row.LocalIP); ip != "" { @@ -158,10 +159,10 @@ func topologyOSPFNeighborActorRow(row topologyOSPFNeighbor) map[string]any { "state": normalizeOSPFNeighborState(row.State), "source": "ospf_mib", } - if routerID := normalizeOSPFRouterID(row.LocalRouterID); routerID != "" { + if routerID := normalizeTopologyRouterID(row.LocalRouterID); routerID != "" { out["local_router_id"] = routerID } - if routerID := normalizeOSPFRouterID(row.NeighborRouterID); routerID != "" { + if routerID := normalizeTopologyRouterID(row.NeighborRouterID); routerID != "" { out["neighbor_router_id"] = routerID } if ip := normalizeNonUnspecifiedIPAddress(row.NeighborIP); ip != "" { @@ -179,24 +180,10 @@ func topologyOSPFNeighborActorRow(row topologyOSPFNeighbor) map[string]any { return out } -func attachTopologyOSPFNeighborRows(data *topologyData, rowsByActor map[string][]map[string]any) { - if data == nil || len(rowsByActor) == 0 { - return - } - for i := range data.Actors { - actor := &data.Actors[i] - rows := rowsByActor[strings.TrimSpace(actor.ActorID)] - if len(rows) == 0 { - continue - } - sort.Slice(rows, func(i, j int) bool { - return topologyOSPFNeighborActorRowSortKey(rows[i]) < topologyOSPFNeighborActorRowSortKey(rows[j]) - }) - if actor.Tables == nil { - actor.Tables = make(map[string][]map[string]any) - } - actor.Tables["ospf_neighbors"] = rows - } +func sortTopologyOSPFNeighborRows(rows []map[string]any) { + sort.Slice(rows, func(i, j int) bool { + return topologyOSPFNeighborActorRowSortKey(rows[i]) < topologyOSPFNeighborActorRowSortKey(rows[j]) + }) } func topologyOSPFNeighborActorRowSortKey(row map[string]any) string { @@ -229,58 +216,6 @@ func existingTopologyOSPFLinkKeys(links []topologyLink) map[string]struct{} { return seen } -func suppressMatchingTopologyL3SubnetLinks(data *topologyData, ospfLink topologyLink) int { - if data == nil || len(data.Links) == 0 { - return 0 - } - - dst := data.Links[:0] - removed := 0 - for _, link := range data.Links { - if topologyLinkMatchesOSPFAdjacencyL3Subnet(link, ospfLink) { - removed++ - continue - } - dst = append(dst, link) - } - data.Links = dst - return removed -} - -func topologyLinkMatchesOSPFAdjacencyL3Subnet(link, ospfLink topologyLink) bool { - if !strings.EqualFold(strings.TrimSpace(firstNonEmptyString(link.LinkType, link.Protocol)), topologyL3SubnetLinkType) { - return false - } - if !sameTopologyActorPair(link, ospfLink) { - return false - } - ospfSubnet := topologyMetricValueString(ospfLink.Metrics, "subnet") - if ospfSubnet != "" && ospfSubnet == topologyMetricValueString(link.Metrics, "subnet") { - return true - } - return sameTopologyEndpointIPPair( - topologyV1EndpointString(link.Src, "ip"), - topologyV1EndpointString(link.Dst, "ip"), - topologyV1EndpointString(ospfLink.Src, "ip"), - topologyV1EndpointString(ospfLink.Dst, "ip"), - ) -} - -func sameTopologyActorPair(a, b topologyLink) bool { - aSrc, aDst := strings.TrimSpace(a.SrcActorID), strings.TrimSpace(a.DstActorID) - bSrc, bDst := strings.TrimSpace(b.SrcActorID), strings.TrimSpace(b.DstActorID) - return (aSrc == bSrc && aDst == bDst) || (aSrc == bDst && aDst == bSrc) -} - -func sameTopologyEndpointIPPair(aSrc, aDst, bSrc, bDst string) bool { - aSrc, aDst = normalizeNonUnspecifiedIPAddress(aSrc), normalizeNonUnspecifiedIPAddress(aDst) - bSrc, bDst = normalizeNonUnspecifiedIPAddress(bSrc), normalizeNonUnspecifiedIPAddress(bDst) - if aSrc == "" || aDst == "" || bSrc == "" || bDst == "" { - return false - } - return (aSrc == bSrc && aDst == bDst) || (aSrc == bDst && aDst == bSrc) -} - func recordTopologyOSPFEnrichmentStats(data *topologyData, stats topologyOSPFEnrichmentStats) { if data == nil { return @@ -296,22 +231,4 @@ func recordTopologyOSPFEnrichmentStats(data *topologyData, stats topologyOSPFEnr data.Stats["ospf_adjacency_suppressed_unresolved_neighbor"] = stats.suppressedUnresolvedNeighbor data.Stats["ospf_adjacency_suppressed_self_actor"] = stats.suppressedSelfActor data.Stats["ospf_adjacency_suppressed_duplicate_link"] = stats.suppressedDuplicateLink - data.Stats["ospf_adjacency_suppressed_l3_subnet_overlap"] = stats.suppressedL3SubnetOverlap - recomputeTopologyOSPFVisibleLinkStats(data) -} - -func recomputeTopologyOSPFVisibleLinkStats(data *topologyData) { - if data == nil || data.Stats == nil { - return - } - if _, ok := data.Stats["ospf_adjacency_emitted_links"]; !ok { - return - } - count := 0 - for _, link := range data.Links { - if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(link.LinkType, link.Protocol)), topologyOSPFAdjacencyLinkType) { - count++ - } - } - data.Stats["ospf_adjacency_visible_links"] = count } diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_links_test.go b/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_links_test.go index e78681b6ce33c7..580ebf27f59686 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_links_test.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_links_test.go @@ -162,7 +162,7 @@ func TestApplyTopologyOSPFAdjacencyEnrichmentDeduplicatesBidirectionalObservatio require.Len(t, data.Actors[1].Tables["ospf_neighbors"], 1) } -func TestApplyTopologyOSPFAdjacencyEnrichmentSuppressesMatchingL3SubnetEdge(t *testing.T) { +func TestApplyTopologyOSPFAdjacencyEnrichmentKeepsMatchingL3SubnetEdge(t *testing.T) { l3Link := topologyLink{ Protocol: topologyL3SubnetLinkType, LinkType: topologyL3SubnetLinkType, @@ -198,10 +198,10 @@ func TestApplyTopologyOSPFAdjacencyEnrichmentSuppressesMatchingL3SubnetEdge(t *t stats := applyTopologyOSPFAdjacencyEnrichment(&data, aggregate) require.Equal(t, 1, stats.emittedLinks) - require.Equal(t, 1, stats.suppressedL3SubnetOverlap) - require.Len(t, data.Links, 1) - require.Equal(t, topologyOSPFAdjacencyLinkType, data.Links[0].LinkType) - require.Equal(t, 0, data.Stats["l3_subnet_visible_links"]) + require.Len(t, data.Links, 2) + require.Equal(t, 1, countTopologyLinksByType(data.Links, topologyL3SubnetLinkType)) + require.Equal(t, 1, countTopologyLinksByType(data.Links, topologyOSPFAdjacencyLinkType)) + require.Equal(t, 1, data.Stats["l3_subnet_visible_links"]) require.Equal(t, 1, data.Stats["ospf_adjacency_visible_links"]) } @@ -244,7 +244,6 @@ func TestApplyTopologyOSPFAdjacencyEnrichmentKeepsUnrelatedL3SubnetEdge(t *testi stats := applyTopologyOSPFAdjacencyEnrichment(&data, aggregate) require.Equal(t, 1, stats.emittedLinks) - require.Zero(t, stats.suppressedL3SubnetOverlap) require.Len(t, data.Links, 2) require.Equal(t, 1, countTopologyLinksByType(data.Links, topologyL3SubnetLinkType)) require.Equal(t, 1, countTopologyLinksByType(data.Links, topologyOSPFAdjacencyLinkType)) diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_neighbors.go b/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_neighbors.go index 88d5250d56e22c..5f6202be298963 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_neighbors.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_neighbors.go @@ -16,14 +16,14 @@ func init() { } func (c *topologyCache) updateOSPFNeighbor(tags map[string]string) { - neighborRouterID := normalizeOSPFRouterID(tags[tagOSPFNeighborRouterID]) + neighborRouterID := normalizeTopologyRouterID(tags[tagOSPFNeighborRouterID]) neighborIP := normalizeNonUnspecifiedIPAddress(tags[tagOSPFNeighborIP]) if neighborRouterID == "" && neighborIP == "" { return } row := topologyOSPFNeighbor{ - LocalRouterID: normalizeOSPFRouterID(c.localDevice.OSPFRouterID), + LocalRouterID: normalizeTopologyRouterID(c.localDevice.OSPFRouterID), NeighborRouterID: neighborRouterID, NeighborIP: neighborIP, AddresslessIndex: strings.TrimSpace(tags[tagOSPFNeighborAddresslessIndex]), @@ -50,7 +50,7 @@ func (c *topologyCache) snapshotOSPFNeighbors(localDeviceID string) []topologyOS row := c.ospfNeighborsByKey[key] row.DeviceID = strings.TrimSpace(localDeviceID) if row.LocalRouterID == "" { - row.LocalRouterID = normalizeOSPFRouterID(c.localDevice.OSPFRouterID) + row.LocalRouterID = normalizeTopologyRouterID(c.localDevice.OSPFRouterID) } if iface, ok := c.matchOSPFNeighborLocalInterface(row.NeighborIP); ok { row.LocalIP = iface.IP @@ -121,23 +121,12 @@ func (c *topologyCache) matchOSPFNeighborLocalInterface(neighborIP string) (topo func topologyOSPFNeighborCacheKey(row topologyOSPFNeighbor) string { return topologyL3SubnetLinkKeyParts( - normalizeOSPFRouterID(row.NeighborRouterID), + normalizeTopologyRouterID(row.NeighborRouterID), normalizeNonUnspecifiedIPAddress(row.NeighborIP), strings.TrimSpace(row.AddresslessIndex), ) } -func normalizeOSPFRouterID(value string) string { - value = strings.TrimSpace(value) - if value == "" { - return "" - } - if ip := normalizeIPAddress(value); ip != "" { - return normalizeNonUnspecifiedIPAddress(ip) - } - return value -} - func normalizeOSPFNeighborState(value string) string { value = strings.TrimSpace(value) if value == "" { @@ -176,8 +165,8 @@ func topologyOSPFNeighborLinkKeyParts(row topologyOSPFNeighbor, srcActorID, dstA srcActorID, dstActorID = dstActorID, srcActorID } - localRouterID := normalizeOSPFRouterID(row.LocalRouterID) - neighborRouterID := normalizeOSPFRouterID(row.NeighborRouterID) + localRouterID := normalizeTopologyRouterID(row.LocalRouterID) + neighborRouterID := normalizeTopologyRouterID(row.NeighborRouterID) if localRouterID > neighborRouterID { localRouterID, neighborRouterID = neighborRouterID, localRouterID } diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_neighbors_test.go b/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_neighbors_test.go index 31f6b5d7319dd2..4d4978ee5a9741 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_neighbors_test.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_ospf_neighbors_test.go @@ -8,24 +8,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestNormalizeOSPFRouterIDDropsUnspecifiedAddresses(t *testing.T) { - tests := map[string]struct { - in string - want string - }{ - "ipv4-unspecified": {in: "0.0.0.0"}, - "ipv6-unspecified": {in: "::"}, - "hex-unspecified": {in: "00000000"}, - "valid-router-id": {in: " 1.2.3.4 ", want: "1.2.3.4"}, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - require.Equal(t, tc.want, normalizeOSPFRouterID(tc.in)) - }) - } -} - func TestTopologyOSPFNeighborLinkKeyIgnoresUnspecifiedIPs(t *testing.T) { base := topologyOSPFNeighbor{ LocalRouterID: "1.1.1.1", diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_registry_test.go b/src/go/plugin/go.d/collector/snmp_topology/topology_registry_test.go index 593f926edd926c..46d4680ac5dc4e 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_registry_test.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_registry_test.go @@ -249,16 +249,12 @@ func TestTopologyRegistry_OSPFSnapshotEnrichesSubnetAfterNeighborIngest(t *testi data, ok := snapshotTopologyRegistryForTest(registry) require.True(t, ok) - require.Len(t, data.Links, 1) - link := data.Links[0] - require.Equal(t, topologyOSPFAdjacencyLinkType, link.LinkType) - require.Equal(t, "198.51.100.0/30", link.Metrics["subnet"]) - require.Equal(t, "198.51.100.1", link.Src.Attributes["ip"]) - require.Equal(t, "198.51.100.2", link.Dst.Attributes["ip"]) + require.Len(t, data.Links, 2) + require.Equal(t, 1, countTopologyLinksByType(data.Links, topologyL3SubnetLinkType)) + require.Equal(t, 1, countTopologyLinksByType(data.Links, topologyOSPFAdjacencyLinkType)) require.Equal(t, 1, data.Stats["l3_subnet_emitted_links"]) - require.Equal(t, 0, data.Stats["l3_subnet_visible_links"]) + require.Equal(t, 1, data.Stats["l3_subnet_visible_links"]) require.Equal(t, 1, data.Stats["ospf_adjacency_emitted_links"]) - require.Equal(t, 1, data.Stats["ospf_adjacency_suppressed_l3_subnet_overlap"]) require.Equal(t, 1, data.Stats["ospf_adjacency_visible_links"]) } diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_router_id.go b/src/go/plugin/go.d/collector/snmp_topology/topology_router_id.go new file mode 100644 index 00000000000000..6688c73e580582 --- /dev/null +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_router_id.go @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package snmptopology + +import "strings" + +func normalizeTopologyRouterID(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + if ip := normalizeIPAddress(value); ip != "" { + return normalizeNonUnspecifiedIPAddress(ip) + } + return value +} + +func normalizeBGPRouterID(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + if ip := normalizeNonUnspecifiedIPAddress(value); ip != "" { + return ip + } + return value +} diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_router_id_test.go b/src/go/plugin/go.d/collector/snmp_topology/topology_router_id_test.go new file mode 100644 index 00000000000000..e821aaa9aca7a9 --- /dev/null +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_router_id_test.go @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package snmptopology + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNormalizeTopologyRouterIDDropsUnspecifiedAddresses(t *testing.T) { + tests := map[string]struct { + in string + want string + }{ + "ipv4-unspecified": {in: "0.0.0.0"}, + "ipv6-unspecified": {in: "::"}, + "hex-unspecified": {in: "00000000"}, + "valid-router-id": {in: " 1.2.3.4 ", want: "1.2.3.4"}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + require.Equal(t, tc.want, normalizeTopologyRouterID(tc.in)) + }) + } +} diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_shape_stats.go b/src/go/plugin/go.d/collector/snmp_topology/topology_shape_stats.go index a0549b995814a2..5075c0fbd36e24 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_shape_stats.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_shape_stats.go @@ -46,3 +46,35 @@ func recomputeTopologyL3VisibleLinkStats(data *topologyData) { } data.Stats["l3_subnet_visible_links"] = count } + +func recomputeTopologyOSPFVisibleLinkStats(data *topologyData) { + if data == nil || data.Stats == nil { + return + } + if _, ok := data.Stats["ospf_adjacency_emitted_links"]; !ok { + return + } + count := 0 + for _, link := range data.Links { + if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(link.LinkType, link.Protocol)), topologyOSPFAdjacencyLinkType) { + count++ + } + } + data.Stats["ospf_adjacency_visible_links"] = count +} + +func recomputeTopologyBGPVisibleLinkStats(data *topologyData) { + if data == nil || data.Stats == nil { + return + } + if _, ok := data.Stats["bgp_adjacency_emitted_links"]; !ok { + return + } + count := 0 + for _, link := range data.Links { + if strings.EqualFold(strings.TrimSpace(firstNonEmptyString(link.LinkType, link.Protocol)), topologyBGPAdjacencyLinkType) { + count++ + } + } + data.Stats["bgp_adjacency_visible_links"] = count +} diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_snapshot_builder.go b/src/go/plugin/go.d/collector/snmp_topology/topology_snapshot_builder.go index bec66e4bb26c2b..4edf97b1394cf1 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_snapshot_builder.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_snapshot_builder.go @@ -42,7 +42,7 @@ func buildLocalTopologyDevice(dev ddsnmp.DeviceConnectionInfo) topologyDevice { if value := topologyMetadataValue(device.Labels, topologyMetadataAliasModel); value != "" && device.Model == "" { device.Model = value } - if value := normalizeOSPFRouterID(device.Labels[tagOSPFRouterID]); value != "" { + if value := normalizeTopologyRouterID(device.Labels[tagOSPFRouterID]); value != "" { device.OSPFRouterID = value setTopologyMetadataLabelIfMissing(device.Labels, tagOSPFRouterID, value) } diff --git a/src/go/plugin/go.d/collector/snmp_topology/topology_types.go b/src/go/plugin/go.d/collector/snmp_topology/topology_types.go index 454e6638498d8c..b82c63a6dd4fdb 100644 --- a/src/go/plugin/go.d/collector/snmp_topology/topology_types.go +++ b/src/go/plugin/go.d/collector/snmp_topology/topology_types.go @@ -2,17 +2,17 @@ package snmptopology -import topologyengine "github.com/netdata/netdata/go/plugins/pkg/l2topology" +import "github.com/netdata/netdata/go/plugins/pkg/topology/graph" const topologySchemaVersion = "2.0" -type topologyData = topologyengine.Graph -type topologyActor = topologyengine.Actor -type topologyMatch = topologyengine.Match -type topologyLink = topologyengine.Link -type topologyLinkEndpoint = topologyengine.LinkEndpoint -type topologyFlow = topologyengine.Flow -type topologyIPPolicy = topologyengine.IPPolicy +type topologyData = graph.Graph +type topologyActor = graph.Actor +type topologyMatch = graph.Match +type topologyLink = graph.Link +type topologyLinkEndpoint = graph.LinkEndpoint +type topologyFlow = graph.Flow +type topologyIPPolicy = graph.IPPolicy type topologyManagementAddress struct { Address string `json:"address"`