| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | //go:build snmp_topology_fixtures |
| 4 | |
| 5 | package parity |
| 6 | |
| 7 | import ( |
| 8 | "encoding/json" |
| 9 | "net/netip" |
| 10 | "os" |
| 11 | "path/filepath" |
| 12 | "sort" |
| 13 | "strings" |
| 14 | "testing" |
| 15 | |
| 16 | "github.com/netdata/netdata/go/plugins/pkg/l2topology" |
| 17 | "github.com/stretchr/testify/require" |
| 18 | ) |
| 19 | |
| 20 | type nodeTopologyFixtureDocument struct { |
| 21 | SchemaVersion int `json:"schema_version"` |
| 22 | Scenarios map[string]nodeTopologyScenario `json:"scenarios"` |
| 23 | } |
| 24 | |
| 25 | type nodeTopologyScenario struct { |
| 26 | BuilderClass string `json:"builder_class"` |
| 27 | Getters []string `json:"getters"` |
| 28 | Nodes []nodeTopologyFixtureNode `json:"nodes"` |
| 29 | IPs []nodeTopologyFixtureIP `json:"ips"` |
| 30 | SNMP []nodeTopologyFixtureSNMP `json:"snmp"` |
| 31 | Expect nodeTopologyFixtureExpectations `json:"expect"` |
| 32 | } |
| 33 | |
| 34 | type nodeTopologyFixtureNode struct { |
| 35 | ID int `json:"id"` |
| 36 | Label string `json:"label"` |
| 37 | SysObject string `json:"sys_object"` |
| 38 | SysName string `json:"sys_name"` |
| 39 | Address string `json:"address"` |
| 40 | } |
| 41 | |
| 42 | type nodeTopologyFixtureIP struct { |
| 43 | ID int `json:"id"` |
| 44 | NodeID int `json:"node_id"` |
| 45 | IPAddress string `json:"ip_address"` |
| 46 | Netmask string `json:"netmask"` |
| 47 | IsManaged bool `json:"is_managed"` |
| 48 | IsSnmpPrimary bool `json:"is_snmp_primary"` |
| 49 | IfIndex int `json:"if_index"` |
| 50 | SnmpInterfaceID int `json:"snmp_interface_id"` |
| 51 | } |
| 52 | |
| 53 | type nodeTopologyFixtureSNMP struct { |
| 54 | ID int `json:"id"` |
| 55 | NodeID int `json:"node_id"` |
| 56 | IfIndex int `json:"if_index"` |
| 57 | IfName string `json:"if_name"` |
| 58 | IfDescr string `json:"if_descr"` |
| 59 | } |
| 60 | |
| 61 | type nodeTopologyFixtureExpectations struct { |
| 62 | Nodes int `json:"nodes"` |
| 63 | IPs *int `json:"ips"` |
| 64 | Subnets *int `json:"subnets"` |
| 65 | LegalSubnets *int `json:"legal_subnets"` |
| 66 | PTPSubnets *int `json:"ptp_subnets"` |
| 67 | LegalPTPSubnets *int `json:"legal_ptp_subnets"` |
| 68 | Loopbacks *int `json:"loopbacks"` |
| 69 | LegalLoopbacks *int `json:"legal_loopbacks"` |
| 70 | MultiBet *int `json:"multibet"` |
| 71 | PrioritySize *int `json:"priority_size"` |
| 72 | PriorityRule nodeTopologyPriorityRule `json:"priority_rule"` |
| 73 | NLinksFinal *int `json:"nlinks_final"` |
| 74 | TopologyVertices *int `json:"topology_vertices"` |
| 75 | TopologyEdges *int `json:"topology_edges"` |
| 76 | CIDRNodeSizes []nodeTopologyCIDRExpectation `json:"cidr_node_sizes"` |
| 77 | } |
| 78 | |
| 79 | type nodeTopologyPriorityRule struct { |
| 80 | Kind string `json:"kind"` |
| 81 | Value *int `json:"value"` |
| 82 | } |
| 83 | |
| 84 | type nodeTopologyCIDRExpectation struct { |
| 85 | CIDR string `json:"cidr"` |
| 86 | NodeIDs int `json:"node_ids"` |
| 87 | } |
| 88 | |
| 89 | func loadNodeTopologyFixtureDocument(t *testing.T) nodeTopologyFixtureDocument { |
| 90 | t.Helper() |
| 91 | path := filepath.Join("../../../testdata/snmp/enlinkd", "node_topology", "scenarios.json") |
| 92 | data, err := os.ReadFile(path) |
| 93 | require.NoError(t, err) |
| 94 | |
| 95 | var doc nodeTopologyFixtureDocument |
| 96 | require.NoError(t, json.Unmarshal(data, &doc)) |
| 97 | require.Equal(t, 1, doc.SchemaVersion) |
| 98 | require.NotEmpty(t, doc.Scenarios) |
| 99 | return doc |
| 100 | } |
| 101 | |
| 102 | func scenarioToService(t *testing.T, scenario nodeTopologyScenario) *l2topology.NodeTopologyService { |
| 103 | t.Helper() |
| 104 | |
| 105 | nodes := make([]l2topology.NodeTopologyEntity, 0, len(scenario.Nodes)) |
| 106 | for _, node := range scenario.Nodes { |
| 107 | entry := l2topology.NodeTopologyEntity{ |
| 108 | ID: node.ID, |
| 109 | Label: node.Label, |
| 110 | SysObject: node.SysObject, |
| 111 | SysName: node.SysName, |
| 112 | } |
| 113 | if ip, ok := parseOptionalAddr(node.Address); ok { |
| 114 | entry.Address = ip |
| 115 | } |
| 116 | nodes = append(nodes, entry) |
| 117 | } |
| 118 | |
| 119 | ips := make([]l2topology.IPInterfaceTopologyEntity, 0, len(scenario.IPs)) |
| 120 | for _, ip := range scenario.IPs { |
| 121 | entry := l2topology.IPInterfaceTopologyEntity{ |
| 122 | ID: ip.ID, |
| 123 | NodeID: ip.NodeID, |
| 124 | IsManaged: ip.IsManaged, |
| 125 | IsSnmpPrimary: ip.IsSnmpPrimary, |
| 126 | IfIndex: ip.IfIndex, |
| 127 | SnmpInterfaceID: ip.SnmpInterfaceID, |
| 128 | } |
| 129 | parsedIP, ok := parseOptionalAddr(ip.IPAddress) |
| 130 | require.Truef(t, ok, "invalid fixture IP address %q", ip.IPAddress) |
| 131 | entry.IPAddress = parsedIP |
| 132 | if mask, ok := parseOptionalAddr(ip.Netmask); ok { |
| 133 | entry.NetMask = mask |
| 134 | } |
| 135 | ips = append(ips, entry) |
| 136 | } |
| 137 | |
| 138 | snmp := make([]l2topology.SnmpInterfaceTopologyEntity, 0, len(scenario.SNMP)) |
| 139 | for _, iface := range scenario.SNMP { |
| 140 | snmp = append(snmp, l2topology.SnmpInterfaceTopologyEntity{ |
| 141 | ID: iface.ID, |
| 142 | NodeID: iface.NodeID, |
| 143 | IfIndex: iface.IfIndex, |
| 144 | IfName: iface.IfName, |
| 145 | IfDescr: iface.IfDescr, |
| 146 | }) |
| 147 | } |
| 148 | |
| 149 | return l2topology.NewNodeTopologyService(nodes, ips, snmp) |
| 150 | } |
| 151 | |
| 152 | func parseOptionalAddr(value string) (netip.Addr, bool) { |
| 153 | value = stringsTrimSpace(value) |
| 154 | if value == "" { |
| 155 | return netip.Addr{}, false |
| 156 | } |
| 157 | addr, err := netip.ParseAddr(value) |
| 158 | if err != nil { |
| 159 | return netip.Addr{}, false |
| 160 | } |
| 161 | return addr.Unmap(), true |
| 162 | } |
| 163 | |
| 164 | func runNodeTopologyScenarioParity(t *testing.T, scenarioName string) { |
| 165 | t.Helper() |
| 166 | doc := loadNodeTopologyFixtureDocument(t) |
| 167 | scenario, ok := doc.Scenarios[scenarioName] |
| 168 | require.Truef(t, ok, "missing scenario %q", scenarioName) |
| 169 | |
| 170 | service := scenarioToService(t, scenario) |
| 171 | expect := scenario.Expect |
| 172 | |
| 173 | require.Len(t, service.FindAllNode(), expect.Nodes) |
| 174 | if expect.IPs != nil { |
| 175 | require.Len(t, service.FindAllIP(), *expect.IPs) |
| 176 | } |
| 177 | |
| 178 | subnets := service.FindAllSubNetwork() |
| 179 | if expect.Subnets != nil { |
| 180 | require.Len(t, subnets, *expect.Subnets) |
| 181 | } |
| 182 | |
| 183 | legalSubnets := service.FindAllLegalSubNetwork() |
| 184 | if expect.LegalSubnets != nil { |
| 185 | require.Len(t, legalSubnets, *expect.LegalSubnets) |
| 186 | } |
| 187 | |
| 188 | ptpSubnets := service.FindAllPointToPointSubNetwork() |
| 189 | if expect.PTPSubnets != nil { |
| 190 | require.Len(t, ptpSubnets, *expect.PTPSubnets) |
| 191 | } |
| 192 | |
| 193 | legalPTPSubnets := service.FindAllLegalPointToPointSubNetwork() |
| 194 | if expect.LegalPTPSubnets != nil { |
| 195 | require.Len(t, legalPTPSubnets, *expect.LegalPTPSubnets) |
| 196 | } |
| 197 | |
| 198 | loopbacks := service.FindAllLoopbacks() |
| 199 | if expect.Loopbacks != nil { |
| 200 | require.Len(t, loopbacks, *expect.Loopbacks) |
| 201 | } |
| 202 | |
| 203 | legalLoopbacks := service.FindAllLegalLoopbacks() |
| 204 | if expect.LegalLoopbacks != nil { |
| 205 | require.Len(t, legalLoopbacks, *expect.LegalLoopbacks) |
| 206 | } |
| 207 | |
| 208 | multibet := service.FindSubNetworkByNetworkPrefixLessThen(30, 126) |
| 209 | if expect.MultiBet != nil { |
| 210 | require.Len(t, multibet, *expect.MultiBet) |
| 211 | } |
| 212 | |
| 213 | priority := service.GetNodeIDPriorityMap() |
| 214 | if expect.PrioritySize != nil { |
| 215 | require.Len(t, priority, *expect.PrioritySize) |
| 216 | } |
| 217 | switch expect.PriorityRule.Kind { |
| 218 | case "all_zero": |
| 219 | for _, value := range priority { |
| 220 | require.Equal(t, 0, value) |
| 221 | } |
| 222 | case "lt": |
| 223 | require.NotNil(t, expect.PriorityRule.Value) |
| 224 | for _, value := range priority { |
| 225 | require.Less(t, value, *expect.PriorityRule.Value) |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | if len(expect.CIDRNodeSizes) > 0 { |
| 230 | actual := make(map[string]int) |
| 231 | for _, subnet := range legalSubnets { |
| 232 | actual[subnet.CIDR()] = len(subnet.NodeIDs()) |
| 233 | } |
| 234 | for _, expectedCIDR := range expect.CIDRNodeSizes { |
| 235 | require.Equalf(t, expectedCIDR.NodeIDs, actual[expectedCIDR.CIDR], "cidr %s", expectedCIDR.CIDR) |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | topology := l2topology.BuildNetworkRouterTopology(service, 30, 126) |
| 240 | if expect.TopologyVertices != nil { |
| 241 | require.Len(t, topology.Vertices, *expect.TopologyVertices) |
| 242 | } else if expect.MultiBet != nil { |
| 243 | require.Len(t, topology.Vertices, expect.Nodes+*expect.MultiBet) |
| 244 | } |
| 245 | |
| 246 | if expect.TopologyEdges != nil { |
| 247 | require.Len(t, topology.Edges, *expect.TopologyEdges) |
| 248 | } else if expect.NLinksFinal != nil { |
| 249 | require.Len(t, topology.Edges, *expect.NLinksFinal) |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | func TestNodeTopologyServiceIT_nms0001SubnetworksTest(t *testing.T) { |
| 254 | runNodeTopologyScenarioParity(t, "nms0001SubnetworksTest") |
| 255 | } |
| 256 | func TestNodeTopologyServiceIT_nms0002SubnetworksTest(t *testing.T) { |
| 257 | runNodeTopologyScenarioParity(t, "nms0002SubnetworksTest") |
| 258 | } |
| 259 | func TestNodeTopologyServiceIT_nms003SubnetworkTests(t *testing.T) { |
| 260 | runNodeTopologyScenarioParity(t, "nms003SubnetworkTests") |
| 261 | } |
| 262 | func TestNodeTopologyServiceIT_nms007SubnetworkTest(t *testing.T) { |
| 263 | runNodeTopologyScenarioParity(t, "nms007SubnetworkTest") |
| 264 | } |
| 265 | func TestNodeTopologyServiceIT_nms101SubnetworksTest(t *testing.T) { |
| 266 | runNodeTopologyScenarioParity(t, "nms101SubnetworksTest") |
| 267 | } |
| 268 | func TestNodeTopologyServiceIT_nms102SubnetworksTest(t *testing.T) { |
| 269 | runNodeTopologyScenarioParity(t, "nms102SubnetworksTest") |
| 270 | } |
| 271 | func TestNodeTopologyServiceIT_nms0123SubnetworksTest(t *testing.T) { |
| 272 | runNodeTopologyScenarioParity(t, "nms0123SubnetworksTest") |
| 273 | } |
| 274 | func TestNodeTopologyServiceIT_nms1055SubnetworksTest(t *testing.T) { |
| 275 | runNodeTopologyScenarioParity(t, "nms1055SubnetworksTest") |
| 276 | } |
| 277 | func TestNodeTopologyServiceIT_nms4005SubnetworksTest(t *testing.T) { |
| 278 | runNodeTopologyScenarioParity(t, "nms4005SubnetworksTest") |
| 279 | } |
| 280 | func TestNodeTopologyServiceIT_nms4930SubnetworksTest(t *testing.T) { |
| 281 | runNodeTopologyScenarioParity(t, "nms4930SubnetworksTest") |
| 282 | } |
| 283 | func TestNodeTopologyServiceIT_nms6802SubnetworksTest(t *testing.T) { |
| 284 | runNodeTopologyScenarioParity(t, "nms6802SubnetworksTest") |
| 285 | } |
| 286 | func TestNodeTopologyServiceIT_nms7467SubnetworksTest(t *testing.T) { |
| 287 | runNodeTopologyScenarioParity(t, "nms7467SubnetworksTest") |
| 288 | } |
| 289 | func TestNodeTopologyServiceIT_nms7563SubnetworksTest(t *testing.T) { |
| 290 | runNodeTopologyScenarioParity(t, "nms7563SubnetworksTest") |
| 291 | } |
| 292 | func TestNodeTopologyServiceIT_nms7777DWSubnetworksTest(t *testing.T) { |
| 293 | runNodeTopologyScenarioParity(t, "nms7777DWSubnetworksTest") |
| 294 | } |
| 295 | func TestNodeTopologyServiceIT_nms7918SubnetworksTest(t *testing.T) { |
| 296 | runNodeTopologyScenarioParity(t, "nms7918SubnetworksTest") |
| 297 | } |
| 298 | func TestNodeTopologyServiceIT_nms8000SubnetworksTest(t *testing.T) { |
| 299 | runNodeTopologyScenarioParity(t, "nms8000SubnetworksTest") |
| 300 | } |
| 301 | func TestNodeTopologyServiceIT_nms10205aSubnetworksTest(t *testing.T) { |
| 302 | runNodeTopologyScenarioParity(t, "nms10205aSubnetworksTest") |
| 303 | } |
| 304 | func TestNodeTopologyServiceIT_nms10205bSubnetworksTest(t *testing.T) { |
| 305 | runNodeTopologyScenarioParity(t, "nms10205bSubnetworksTest") |
| 306 | } |
| 307 | func TestNodeTopologyServiceIT_nms13593SubnetworksTest(t *testing.T) { |
| 308 | runNodeTopologyScenarioParity(t, "nms13593SubnetworksTest") |
| 309 | } |
| 310 | func TestNodeTopologyServiceIT_nms13637SubnetworksTest(t *testing.T) { |
| 311 | runNodeTopologyScenarioParity(t, "nms13637SubnetworksTest") |
| 312 | } |
| 313 | func TestNodeTopologyServiceIT_nms13923SubnetworksTest(t *testing.T) { |
| 314 | runNodeTopologyScenarioParity(t, "nms13923SubnetworksTest") |
| 315 | } |
| 316 | func TestNodeTopologyServiceIT_nms17216SubnetworksTest(t *testing.T) { |
| 317 | runNodeTopologyScenarioParity(t, "nms17216SubnetworksTest") |
| 318 | } |
| 319 | |
| 320 | func TestNms0001EnIT_testLinkdNetworkTopologyUpdater(t *testing.T) { |
| 321 | doc := loadNodeTopologyFixtureDocument(t) |
| 322 | scenario, ok := doc.Scenarios["nms0001EnIT_testLinkdNetworkTopologyUpdater"] |
| 323 | require.True(t, ok) |
| 324 | |
| 325 | service := scenarioToService(t, scenario) |
| 326 | topology := l2topology.BuildNetworkRouterTopology(service, 30, 126) |
| 327 | require.NotEmpty(t, topology.Vertices) |
| 328 | require.Len(t, topology.Vertices, *scenario.Expect.TopologyVertices) |
| 329 | require.Len(t, topology.Edges, *scenario.Expect.TopologyEdges) |
| 330 | } |
| 331 | |
| 332 | func TestNodeTopologyFixtureCoverage(t *testing.T) { |
| 333 | doc := loadNodeTopologyFixtureDocument(t) |
| 334 | scenarios := make([]string, 0, len(doc.Scenarios)) |
| 335 | for name := range doc.Scenarios { |
| 336 | scenarios = append(scenarios, name) |
| 337 | } |
| 338 | sort.Strings(scenarios) |
| 339 | |
| 340 | require.Contains(t, scenarios, "nms0001SubnetworksTest") |
| 341 | require.Contains(t, scenarios, "nms17216SubnetworksTest") |
| 342 | require.Contains(t, scenarios, "nms0001EnIT_testLinkdNetworkTopologyUpdater") |
| 343 | } |
| 344 | |
| 345 | func stringsTrimSpace(value string) string { return strings.TrimSpace(value) } |