From f30fcd898e40f0cb2691fa66b4758344dbea4178 Mon Sep 17 00:00:00 2001 From: moharedd Date: Sun, 19 Jul 2026 09:47:27 +0000 Subject: [PATCH 1/4] Updated the code as per the readme changes and validated --- .../mpls_in_udp_scale_test.go | 619 +++++++++++++++--- 1 file changed, 526 insertions(+), 93 deletions(-) diff --git a/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go b/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go index 810bc79f5c1..13387c25541 100644 --- a/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go +++ b/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go @@ -48,9 +48,11 @@ const ( numVRFs = 1021 trafficDuration = 15 * time.Second nextHopID = uint64(10001) - nextHopGroupID = uint64(20001) + nextHopGroupID = uint64(50001) outerIPv6Src = "2001:db8:300::1" outerIPv6Dst = "6001:0:0:1::1" + outerIPv4Src = "194.51.0.1" + outerIPv4Dst = "195.51.0.1" outerDstUDPPort = 6635 outerDSCP = 26 outerTTL = 64 @@ -86,6 +88,8 @@ const ( batchTimeout = 180 * time.Second gribiBatchSize = 2000 allowedLossPct = 1.0 + p67VRFCount = 1000 + p8VRFCount = 20 ) // DUT and ATE port attributes @@ -122,11 +126,14 @@ var ( } portsTrafficDistribution = []uint64{50, 50} profiles = map[vrfProfile]profileConfig{ - profileSingleVRF: {20000, 1}, // 20k NHGs × 1 NH - profileMultiVRF: {20000, 1}, // same as profile 1 - profileMultiVRFSkew: {20000, 1}, // same as profile 1 - profileSingleVRFECMP: {2500, 8}, // 2.5k NHGs × 8 NHs = 20k NHs - profileSingleVRFgRIBI: {20000, 1}, // QPS scaling + profileSingleVRF: {20000, 1}, // 20k NHGs × 1 NH + profileMultiVRF: {20000, 1}, // same as profile 1 + profileMultiVRFSkew: {20000, 1}, // same as profile 1 + profileSingleVRFECMP: {2500, 8}, // 2.5k NHGs × 8 NHs = 20k NHs + profileSingleVRFgRIBI: {20000, 1}, // QPS scaling + profileMultiVRFIPv4ECMP: {4000, 8}, // 4,000 NHGs × 8 NHs/NHG + profileMultiVRFIPv6ECMP: {4000, 8}, // 4,000 NHGs × 8 NHs/NHG + profileMultiNHGIPv6: {20, 1024}, // 20 NHGs × 1024 NHs/NHG } pfx1V6Lists = []string{} ) @@ -185,6 +192,9 @@ const ( profileMultiVRFSkew profileSingleVRFECMP profileSingleVRFgRIBI + profileMultiVRFIPv4ECMP + profileMultiVRFIPv6ECMP + profileMultiNHGIPv6 ) // configureDUT configures the DUT with base port interfaces, subinterfaces, and VRF assignments. It also applies required hardware initialization, vendor-specific port settings, and static ARP entries needed for gRIBI next-hop resolution. It returns the list of configured VRF names. @@ -414,22 +424,26 @@ func configureOTG(t *testing.T, ate *ondatra.ATEDevice, dut *ondatra.DUTDevice) } // programBasicEntries programs baseline gRIBI entries including NextHops, NextHopGroups, and IPv6 routes in the default network instance. It sets up ECMP forwarding across port3 and port4 for MPLS-in-UDP testing. -func programBasicEntries(t *testing.T, dut *ondatra.DUTDevice, c *gribi.Client, vrfs []string) ([]string, []uint64) { +func programBasicEntries(t *testing.T, dut *ondatra.DUTDevice, c *gribi.Client, vrfs []string) ([]string, []string) { t.Helper() t.Log("Setting up routing infrastructure for MPLS-in-UDP with unique NH IDs and NH/NHG in default NI") - var tDstLists []string - var labelLists []uint64 + var tV4DstLists, tV6DstLists []string otgDummyIPs := map[string]string{ dut.Port(t, "port3").Name(): otgPort3DummyIP.IPv4, dut.Port(t, "port4").Name(): otgPort4DummyIP.IPv4, } defaultNI := deviations.DefaultNetworkInstance(dut) - tunnelPrefix, err := iputil.GenerateIPv6sWithStep(outerIPv6Dst, len(vrfs), intStepV6) + tunnelPrefixV6, err := iputil.GenerateIPv6sWithStep(outerIPv6Dst, len(vrfs), intStepV6) if err != nil { t.Fatalf("failed to generate DUT IPv6s: %v", err) } - tDstLists = append(tDstLists, tunnelPrefix...) + tV6DstLists = append(tV6DstLists, tunnelPrefixV6...) + tunnelPrefixV4, err := iputil.GenerateIPsWithStep(outerIPv4Dst, len(vrfs), intStepV4) + if err != nil { + t.Fatalf("failed to generate DUT IPs: %v", err) + } + tV4DstLists = append(tV4DstLists, tunnelPrefixV4...) var allEntries []fluent.GRIBIEntry // iterate VRFs and program unique NH, NHG in default NI; program IPv6 prefix in the VRF for vrfIdx := range vrfs { @@ -463,8 +477,9 @@ func programBasicEntries(t *testing.T, dut *ondatra.DUTDevice, c *gribi.Client, nhMap := map[uint64]uint64{nhIDs[0]: 1, nhIDs[1]: 1} nhg, _ := gribi.NHGEntry(nhgID, nhMap, defaultNI, fluent.InstalledInFIB) allEntries = append(allEntries, nhg) - labelLists = append(labelLists, mplsLabel+uint64(vrfIdx)) - ipv6Entry := fluent.IPv6Entry().WithPrefix(tunnelPrefix[vrfIdx] + routeV6PrfLen).WithNetworkInstance(defaultNI).WithNextHopGroup(nhgID) + ipv4Entry := fluent.IPv4Entry().WithPrefix(tunnelPrefixV4[vrfIdx] + routeV4PrfLen).WithNetworkInstance(defaultNI).WithNextHopGroup(nhgID) + allEntries = append(allEntries, ipv4Entry) + ipv6Entry := fluent.IPv6Entry().WithPrefix(tunnelPrefixV6[vrfIdx] + routeV6PrfLen).WithNetworkInstance(defaultNI).WithNextHopGroup(nhgID) allEntries = append(allEntries, ipv6Entry) } // Install NH + NHG in DEFAULT NI @@ -473,11 +488,11 @@ func programBasicEntries(t *testing.T, dut *ondatra.DUTDevice, c *gribi.Client, c.AddEntries(t, batch, nil) } t.Log("programBasicEntries completed") - return tDstLists, labelLists + return tV4DstLists, tV6DstLists } // programMPLSinUDPEntries programs gRIBI NextHop and NextHopGroup entries for MPLS-in-UDP encapsulation using IPv6 outer headers. It creates a shared encapsulating NextHop and multiple NHGs that reference it. -func programMPLSinUDPEntries(t *testing.T, dut *ondatra.DUTDevice, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs int, vrfs []string, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) []fluent.GRIBIEntry { +func programMPLSinUDPEntries(t *testing.T, dut *ondatra.DUTDevice, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs int, vrfs []string, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, []uint64) { t.Helper() entries := make([]fluent.GRIBIEntry, 0, 1+2*totalNHGs*len(vrfs)) entries = append(entries, @@ -503,7 +518,7 @@ func programMPLSinUDPEntries(t *testing.T, dut *ondatra.DUTDevice, nextHopID, ne } } - return entries + return entries, []uint64{labelValue} } // expectedMPLSinUDPOpResults builds the expected gRIBI OperationResults for MPLS-in-UDP Profile 5 validation. It generates the baseline Add and Delete results. @@ -570,7 +585,7 @@ func expectedMPLSinUDPOpResults(t *testing.T, nextHopID, nextHopGroupID uint64, } // programProfile4MPLSinUDP programs gRIBI entries for MPLS-in-UDP encapsulation. It installs a NextHop that performs MPLS-in-UDP encapsulation with the provided outer IPv6 and UDP header attributes, associates multiple NextHopGroups (NHGs) with that NextHop, and finally installs IPv6 /128 routes pointing to each NHG. -func programProfile4MPLSinUDP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs, nhsPerNHG int, vrfs []string, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) []fluent.GRIBIEntry { +func programProfile4MPLSinUDP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs, nhsPerNHG int, vrfs []string, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, []uint64) { t.Helper() entries := make([]fluent.GRIBIEntry, 0) // One tunnel destination per VRF (shared across NHGs) @@ -611,7 +626,7 @@ func programProfile4MPLSinUDP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, n nhgVal++ } } - return entries + return entries, []uint64{labelValue} } // expectedProfile4MPLSinUDPOpResults builds the expected gRIBI OperationResults for MPLS-in-UDP Profile 4 validation. @@ -740,8 +755,109 @@ func buildProfile4IPv6Routes(t *testing.T, dut *ondatra.DUTDevice, totalNHGs, ro return entries, vrfV6PfxMap } +// buildProfileRoutes builds IPv4 or IPv6 route entries distributed across the specified VRFs and maps each route to a NextHopGroup. It returns the gRIBI route entries and a map of VRFs to their generated destination prefixes. +func buildProfileRoutes(t *testing.T, dut *ondatra.DUTDevice, totalNHs, routesPerNHG int, baseIP, flowType string, vrfs []string, nextHopGroupID uint64) ([]fluent.GRIBIEntry, map[string][]string) { + t.Helper() + var ( + addrFamilyList []string + err error + routePfxLen string + ) + if len(vrfs) == 0 { + t.Fatal("no VRFs supplied") + } + totalNHGs := totalNHs / routesPerNHG + // NHGs are distributed evenly across the configured VRFs. + vrfCount := len(vrfs) + if totalNHGs < vrfCount { + t.Fatalf("invalid configuration: totalNHGs (%d) must be >= VRFs (%d)", totalNHGs, vrfCount) + } + baseNHGsPerVRF := totalNHGs / vrfCount + extraNHGs := totalNHGs % vrfCount + totalPrefixes := totalNHs + if flowType == "ipv6" { + addrFamilyList, err = iputil.GenerateIPv6sWithStep(baseIP, totalPrefixes, intStepV6) + routePfxLen = routeV6PrfLen + } else { + addrFamilyList, err = iputil.GenerateIPsWithStep(baseIP, totalPrefixes, intStepV4) + routePfxLen = routeV4PrfLen + } + if err != nil { + t.Fatalf("failed to generate IPv4 prefixes: %v", err) + } + entries := make([]fluent.GRIBIEntry, 0, totalPrefixes) + vrfV4PfxMap := make(map[string][]string) + addrIdx := 0 + nhgBase := nextHopGroupID + for vrfIdx := 0; vrfIdx < vrfCount; vrfIdx++ { + currentNHGs := baseNHGsPerVRF + if vrfIdx < extraNHGs { + currentNHGs++ + } + currentPrefixes := currentNHGs * routesPerNHG + for curPfxIndx := 0; curPfxIndx < currentPrefixes; curPfxIndx++ { + addrFamily := addrFamilyList[addrIdx] + addrIdx++ + addrFamilyPrefix := addrFamily + routePfxLen + vrfV4PfxMap[vrfs[vrfIdx]] = append(vrfV4PfxMap[vrfs[vrfIdx]], addrFamily) + nhgID := nhgBase + uint64(curPfxIndx/routesPerNHG) + if flowType == "ipv6" { + entries = append(entries, fluent.IPv6Entry().WithNetworkInstance(vrfs[vrfIdx]).WithPrefix(addrFamilyPrefix).WithNextHopGroup(nhgID).WithNextHopGroupNetworkInstance(deviations.DefaultNetworkInstance(dut))) + } else { + entries = append(entries, fluent.IPv4Entry().WithNetworkInstance(vrfs[vrfIdx]).WithPrefix(addrFamilyPrefix).WithNextHopGroup(nhgID).WithNextHopGroupNetworkInstance(deviations.DefaultNetworkInstance(dut))) + } + } + // Advance to the first NHG for the next VRF. + nhgBase += uint64(currentNHGs) + } + return entries, vrfV4PfxMap +} + +// buildProfile8IPv6Routes builds IPv6 route entries for Profile 8 by distributing NextHopGroups across the specified VRFs. It returns the generated gRIBI IPv6 entries and a map of VRFs to their IPv6 prefixes. +func buildProfile8IPv6Routes(t *testing.T, dut *ondatra.DUTDevice, totalNHGs, nhsPerNHG int, baseIPv6 string, vrfs []string, nextHopGroupID uint64) ([]fluent.GRIBIEntry, map[string][]string) { + t.Helper() + if len(vrfs) == 0 { + t.Fatal("no VRFs supplied") + } + vrfCount := len(vrfs) + if totalNHGs < vrfCount { + t.Fatalf("invalid configuration: totalNHGs (%d) must be >= VRFs (%d)", totalNHGs, vrfCount) + } + baseNHGsPerVRF := totalNHGs / vrfCount + extraNHGs := totalNHGs % vrfCount + totalPrefixes := totalNHGs * nhsPerNHG + //-------------------------------------------------------- + // Generate one unique IPv6 prefix per route. + //-------------------------------------------------------- + ipv6List, err := iputil.GenerateIPv6sWithStep(baseIPv6, totalPrefixes, intStepV6) + if err != nil { + t.Fatalf("GenerateIPv6sWithStep failed: %v", err) + } + entries := make([]fluent.GRIBIEntry, 0, totalPrefixes) + vrfV6PfxMap := make(map[string][]string) + ipIdx := 0 + nhgBase := nextHopGroupID + for vrfIdx := 0; vrfIdx < vrfCount; vrfIdx++ { + currentNHGs := baseNHGsPerVRF + if vrfIdx < extraNHGs { + currentNHGs++ + } + currentPrefixes := currentNHGs * nhsPerNHG + for i := 0; i < currentPrefixes; i++ { + ip := ipv6List[ipIdx] + ipIdx++ + vrfV6PfxMap[vrfs[vrfIdx]] = append(vrfV6PfxMap[vrfs[vrfIdx]], ip) + nhgID := nhgBase + uint64(i/nhsPerNHG) + entries = append(entries, fluent.IPv6Entry().WithNetworkInstance(vrfs[vrfIdx]).WithPrefix(ip+routeV6PrfLen).WithNextHopGroup(nhgID).WithNextHopGroupNetworkInstance(deviations.DefaultNetworkInstance(dut))) + } + // Advance NHG base for the next VRF. + nhgBase += uint64(currentNHGs) + } + return entries, vrfV6PfxMap +} + // programMPLSinUDPMultiEntries builds gRIBI entries for MPLS-in-UDP multi-VRF programming. -func programMPLSinUDPMultiEntries(t *testing.T, dut *ondatra.DUTDevice, vrfs []string, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs int, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, map[string][]string) { +func programMPLSinUDPMultiEntries(t *testing.T, dut *ondatra.DUTDevice, vrfs []string, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs int, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, map[string][]string, []uint64) { t.Helper() vrfMultiPfxMap := make(map[string][]string) defaultNI := deviations.DefaultNetworkInstance(dut) @@ -780,7 +896,7 @@ func programMPLSinUDPMultiEntries(t *testing.T, dut *ondatra.DUTDevice, vrfs []s } } - return entries, vrfMultiPfxMap + return entries, vrfMultiPfxMap, []uint64{labelValue} } // expectedMPLSinUDPMultiOpResults builds the expected Add and Delete OperationResults for MPLS-in-UDP multi-VRF programming. @@ -814,6 +930,185 @@ func expectedMPLSinUDPMultiOpResults(t *testing.T, vrfs []string, nextHopID, nex return adds, dels } +// programProfileMultiVRFECMP builds Profile ECMP gRIBI NextHop and NextHopGroup entries across the specified VRFs. It returns the generated gRIBI entries and the outer source IP addresses used for encapsulation. +func programProfileMultiVRFECMP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs, nhsPerNHG int, vrfs []string, outerSrc, outerDst, flowType string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, []string, []uint64) { + t.Helper() + var ( + vrfOuterDst []string + srcIPs []string + labelLists []uint64 + err error + ) + if len(vrfs) == 0 { + t.Fatal("no VRFs provided") + } + vrfCount := len(vrfs) + if totalNHGs < vrfCount { + vrfCount = totalNHGs + } + baseNHGsPerVRF := totalNHGs / vrfCount + extraNHGs := totalNHGs % vrfCount + totalNHs := totalNHGs * nhsPerNHG + entries := make([]fluent.GRIBIEntry, 0, totalNHs+totalNHGs) + if flowType == "ipv6" { + vrfOuterDst, err = iputil.GenerateIPv6sWithStep(outerDst, vrfCount, intStepV6) + } else { + vrfOuterDst, err = iputil.GenerateIPsWithStep(outerDst, vrfCount, intStepV4) + } + if err != nil { + t.Fatalf("GenerateIPsWithStep failed: %v", err) + } + if flowType == "ipv6" { + srcIPs, err = iputil.GenerateIPv6sWithStep(outerSrc, totalNHs, intStepV6) + } else { + srcIPs, err = iputil.GenerateIPsWithStep(outerSrc, totalNHs, intStepV4) + } + if err != nil { + t.Fatalf("GenerateIPsWithStep failed: %v", err) + } + nhVal := nextHopID + nhgVal := nextHopGroupID + label := labelValue + for vrfIdx := 0; vrfIdx < vrfCount; vrfIdx++ { + nhgsVRF := baseNHGsPerVRF + if vrfIdx < extraNHGs { + nhgsVRF++ + } + baseNH := nhVal + for nhgsIdx := 0; nhgsIdx < nhgsVRF; nhgsIdx++ { + currentLabel := label + label++ + labelLists = append(labelLists, currentLabel) + for nhsIdx := 0; nhsIdx < nhsPerNHG; nhsIdx++ { + if flowType == "ipv6" { + entries = append(entries, + fluent.NextHopEntry(). + WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nhVal). + AddEncapHeader( + fluent.MPLSEncapHeader().WithLabels(currentLabel), + fluent.UDPV6EncapHeader(). + WithSrcIP(srcIPs[nhsIdx]). + WithDstIP(vrfOuterDst[vrfIdx]). + WithDstUDPPort(uint64(outerDstUDPPort)). + WithIPTTL(uint64(outerTTL)). + WithDSCP(uint64(outerDSCP)), + ), + ) + } else { + entries = append(entries, + fluent.NextHopEntry(). + WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nhVal). + AddEncapHeader( + fluent.MPLSEncapHeader().WithLabels(currentLabel), + fluent.UDPV4EncapHeader(). + WithSrcIP(srcIPs[nhsIdx]). + WithDstIP(vrfOuterDst[vrfIdx]). + WithDstUDPPort(uint64(outerDstUDPPort)). + WithIPTTL(uint64(outerTTL)). + WithDSCP(uint64(outerDSCP)), + ), + ) + } + nhVal++ + } + //------------------------------------------------ + // NHG + //------------------------------------------------ + nhg := fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)).WithID(nhgVal) + for n := 0; n < nhsPerNHG; n++ { + nhg.AddNextHop(baseNH+uint64(nhgsIdx*nhsPerNHG+n), 1) + } + entries = append(entries, nhg) + nhgVal++ + } + } + return entries, srcIPs, labelLists +} + +// programProfile8MultiVRFIPv6MoUDP builds Profile 8 gRIBI NextHop and NextHopGroup entries for IPv6 MoUDP encapsulation across multiple VRFs. It programs 64 reusable source IPv6 addresses, 16 destination IPv6 addresses per NHG, and 16 unique MPLS labels per NHG, and returns the generated gRIBI entries and reusable source IPv6 addresses. +func programProfile8MultiVRFIPv6MoUDP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs, nhsPerNHG int, vrfs []string, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, []string, []uint64) { + t.Helper() + var lableList []uint64 + if len(vrfs) == 0 { + t.Fatal("no VRFs provided") + } + const ( + srcIPsPerBlock = 64 + labelsPerNHG = 16 + dstIPsPerNHG = 16 + ) + if nhsPerNHG != labelsPerNHG*srcIPsPerBlock { + t.Fatalf("invalid Profile8 configuration: nhsPerNHG=%d want=%d", nhsPerNHG, labelsPerNHG*srcIPsPerBlock) + } + vrfCount := len(vrfs) + if totalNHGs < vrfCount { + vrfCount = totalNHGs + } + baseNHGsPerVRF := totalNHGs / vrfCount + extraNHGs := totalNHGs % vrfCount + //-------------------------------------------------------- + // 64 reusable source IPs + //-------------------------------------------------------- + srcIPs, err := iputil.GenerateIPv6sWithStep(outerIPv6Src, srcIPsPerBlock, intStepV6) + if err != nil { + t.Fatalf("GenerateIPv6sWithStep(src) failed: %v", err) + } + //-------------------------------------------------------- + // 320 destination IPs + //-------------------------------------------------------- + dstIPs, err := iputil.GenerateIPv6sWithStep(outerIPv6Dst, totalNHGs*dstIPsPerNHG, intStepV6) + if err != nil { + t.Fatalf("GenerateIPv6sWithStep(dst) failed: %v", err) + } + entries := make([]fluent.GRIBIEntry, 0) + nhID := nextHopID + nhgID := nextHopGroupID + globalLabel := labelValue + nhgGlobal := 0 + for vrfIdx := 0; vrfIdx < vrfCount; vrfIdx++ { + nhgsVRF := baseNHGsPerVRF + if vrfIdx < extraNHGs { + nhgsVRF++ + } + baseNH := nhID + dstBase := nhgGlobal * dstIPsPerNHG + for labelIdx := 0; labelIdx < labelsPerNHG; labelIdx++ { + label := globalLabel + globalLabel++ + lableList = append(lableList, label) + dstIP := dstIPs[dstBase+labelIdx] + for srcIdx := 0; srcIdx < srcIPsPerBlock; srcIdx++ { + entries = append(entries, + fluent.NextHopEntry(). + WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nhID). + AddEncapHeader( + fluent.MPLSEncapHeader().WithLabels(label), + fluent.UDPV6EncapHeader(). + WithSrcIP(srcIPs[srcIdx]). + WithDstIP(dstIP). + WithDstUDPPort(uint64(outerDstUDPPort)). + WithIPTTL(uint64(outerTTL)). + WithDSCP(uint64(outerDSCP)), + ), + ) + nhID++ + } + } + //------------------------------------------------ + // NHG + //------------------------------------------------ + for i := 0; i < nhsPerNHG; i++ { + entries = append(entries, fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)).WithID(nhgID).AddNextHop(baseNH+uint64(i), 1)) + } + nhgID++ + nhgGlobal++ + } + return entries, srcIPs, lableList +} + // staticARPWithUniversalIP programs static routes with interface-based next-hops and corresponding static ARP entries for a single destination IP across multiple VRFs and ports. func staticARPWithUniversalIP(t *testing.T, dut *ondatra.DUTDevice, vrfsList, portList []string, sDstIP, sRoutePrfLen, sDstMac, protoType string, indx int) { t.Helper() @@ -843,7 +1138,7 @@ func staticARPWithUniversalIP(t *testing.T, dut *ondatra.DUTDevice, vrfsList, po } // validateMPLSPacketCapture analyzes a packet capture on the given OTG port and validates MPLS-in-UDP encapsulation against the expected parameters. Returns an error if validation fails. -func validateMPLSPacketCapture(t *testing.T, ate *ondatra.ATEDevice, otgPortName string, pr *packetResult, tDstLists []string, labelLists []uint64) error { +func validateMPLSPacketCapture(t *testing.T, ate *ondatra.ATEDevice, otgPortName string, pr *packetResult, tDstLists, srcLists []string, labelLists []uint64) error { t.Helper() t.Logf("=== PACKET CAPTURE VALIDATION START for port %s ===", otgPortName) @@ -876,26 +1171,60 @@ func validateMPLSPacketCapture(t *testing.T, ate *ondatra.ATEDevice, otgPortName for packet := range packetSource.Packets() { packetCount++ udpLayer := packet.Layer(layers.LayerTypeUDP) - ipv6Layer := packet.Layer(layers.LayerTypeIPv6) + // Skip packets that are not UDP-over-IPv6. For debugging, we log only the first 5 skipped packets to avoid flooding the test logs if the capture contains many irrelevant packets (e.g., ARP, ICMP). - if udpLayer == nil || ipv6Layer == nil { + if udpLayer == nil { if packetCount <= 5 { - t.Logf("Packet %d: Skipping non-UDP-IPv6 packet", packetCount) + t.Logf("Packet %d: Skipping non-UDP packet", packetCount) } continue } mplsPacketCount++ - - // Validate IPv6 header - v6 := ipv6Layer.(*layers.IPv6) - if !slices.Contains(tDstLists, v6.DstIP.String()) { - return fmt.Errorf("packet %d: got dstIP %s, want %s", packetCount, v6.DstIP, pr.dstIP) + var ( + v4 *layers.IPv4 + v6 *layers.IPv6 + ) + if layer := packet.Layer(layers.LayerTypeIPv4); layer != nil { + v4 = layer.(*layers.IPv4) } - if v6.SrcIP.String() != pr.srcIP { - return fmt.Errorf("packet %d: got srcIP %s, want %s", packetCount, v6.SrcIP, pr.srcIP) + if layer := packet.Layer(layers.LayerTypeIPv6); layer != nil { + v6 = layer.(*layers.IPv6) } - if v6.HopLimit != pr.ipTTL { - return fmt.Errorf("packet %d: got hopLimit %d, want %d", packetCount, v6.HopLimit, pr.ipTTL) + // Validate IPv4 header + if v4 != nil { + if !slices.Contains(tDstLists, v4.DstIP.String()) { + return fmt.Errorf("packet %d: got dstIP %s, want %s", packetCount, v4.DstIP, pr.dstIP) + } + if len(srcLists) != 0 { + if !slices.Contains(srcLists, v4.SrcIP.String()) { + return fmt.Errorf("packet %d: got IPv4 src %s, want %s", packetCount, v4.SrcIP, pr.srcIP) + } + } else { + if v4.SrcIP.String() != pr.srcIP { + return fmt.Errorf("packet %d: got srcIP %s, want %s", packetCount, v4.SrcIP, pr.srcIP) + } + } + if v4.TTL != pr.ipTTL { + return fmt.Errorf("packet %d: got IPv4 TTL %d, want %d", packetCount, v4.TTL, pr.ipTTL) + } + } + // Validate IPv6 header + if v6 != nil { + if !slices.Contains(tDstLists, v6.DstIP.String()) { + return fmt.Errorf("packet %d: got dstIP %s, want %s", packetCount, v6.DstIP, pr.dstIP) + } + if len(srcLists) != 0 { + if !slices.Contains(srcLists, v6.SrcIP.String()) { + return fmt.Errorf("packet %d: got IPv6 src %s, want %s", packetCount, v6.SrcIP, pr.srcIP) + } + } else { + if v6.SrcIP.String() != pr.srcIP { + return fmt.Errorf("packet %d: got srcIP %s, want %s", packetCount, v6.SrcIP, pr.srcIP) + } + } + if v6.HopLimit != pr.ipTTL { + return fmt.Errorf("packet %d: got hopLimit %d, want %d", packetCount, v6.HopLimit, pr.ipTTL) + } } // Extract UDP payload (MPLS header) payload := udpLayer.LayerPayload() @@ -997,7 +1326,7 @@ func TestMPLSinUDPScale(t *testing.T) { const aristaDisableNHGProxyCLI = "ip hardware fib next-hop proxy disabled" helpers.GnmiCLIConfig(t, dut, aristaDisableNHGProxyCLI) default: - t.Errorf("Deviation DisableHardwareNexthopProxy is not handled for the dut: %v", dut.Vendor()) + t.Errorf("deviation DisableHardwareNexthopProxy is not handled for the dut: %v", dut.Vendor()) } } @@ -1026,6 +1355,21 @@ func TestMPLSinUDPScale(t *testing.T) { t.Fatalf("configureVrfProfiles failed: %v", err) } }) + t.Run("Profile-6-Multi-VRF IPv4 Tunnels - 8-way ECMP", func(t *testing.T) { + if err := configureVRFProfiles(ctx, t, ateConfig, dut, ate, vrfsList, profileMultiVRFIPv4ECMP, false, dutPort1Mac); err != nil { + t.Fatalf("configureVrfProfiles failed: %v", err) + } + }) + t.Run("Profile-7-Multi-VRF IPv6 Tunnels - 8-way ECMP", func(t *testing.T) { + if err := configureVRFProfiles(ctx, t, ateConfig, dut, ate, vrfsList, profileMultiVRFIPv6ECMP, false, dutPort1Mac); err != nil { + t.Fatalf("configureVrfProfiles failed: %v", err) + } + }) + t.Run("Profile-8-Multi-NHG MoUDP IPv6 Tunnels - 1024 Tunnels per NHG", func(t *testing.T) { + if err := configureVRFProfiles(ctx, t, ateConfig, dut, ate, vrfsList, profileMultiNHGIPv6, false, dutPort1Mac); err != nil { + t.Fatalf("configureVrfProfiles failed: %v", err) + } + }) } // configureVRFProfiles programs and validates MPLS-in-UDP forwarding behavior for multiple VRF-based test profiles (Profile 1–5). @@ -1048,26 +1392,28 @@ func configureVRFProfiles(ctx context.Context, t *testing.T, ateConfig gosnappi. totalNHGs := cfg.totalNHGs nhsPerNHG := cfg.nhsPerNHG totalNHs := totalNHGs * nhsPerNHG - tDstLists, labelLists := programBasicEntries(t, dut, c, vrfs) + tV4DstLists, tV6DstLists := programBasicEntries(t, dut, c, vrfs) var ( - entries []fluent.GRIBIEntry - wantAdds []*client.OpResult - wantDels []*client.OpResult - flows []gosnappi.Flow - vrfPfxMap map[string][]string + entries []fluent.GRIBIEntry + wantAdds []*client.OpResult + wantDels []*client.OpResult + flows []gosnappi.Flow + vrfPfxMap map[string][]string + srcLists []string + labelLists []uint64 ) // === Program MPLS-in-UDP NH & NHG entries === switch profile { case profileMultiVRF: - entries, vrfPfxMap = programMPLSinUDPMultiEntries(t, dut, vrfs, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, outerIPv6Src, outerIPv6Dst, outerDstUDPPort, outerTTL, outerDSCP) + entries, vrfPfxMap, labelLists = programMPLSinUDPMultiEntries(t, dut, vrfs, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, outerIPv6Src, outerIPv6Dst, outerDstUDPPort, outerTTL, outerDSCP) wantAdds, wantDels = expectedMPLSinUDPMultiOpResults(t, vrfs, nextHopID, nextHopGroupID, totalNHGs) - flows = append(flows, fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfPfxMap, false, tDstLists)...) + flows = append(flows, fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfPfxMap, false, tV6DstLists)...) case profileMultiVRFSkew: - entries, vrfPfxMap = programMPLSinUDPSkewedEntries(t, dut, vrfs, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, outerIPv6Src, outerIPv6Dst, outerDstUDPPort, outerTTL, outerDSCP) + entries, vrfPfxMap, labelLists = programMPLSinUDPSkewedEntries(t, dut, vrfs, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, outerIPv6Src, outerIPv6Dst, outerDstUDPPort, outerTTL, outerDSCP) wantAdds, wantDels = expectedMPLSinUDPSkewedResults(t, nextHopID, nextHopGroupID, totalNHGs) - flows = append(flows, fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfPfxMap, true, tDstLists)...) + flows = append(flows, fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfPfxMap, true, tV6DstLists)...) case profileSingleVRFECMP: - entries = programProfile4MPLSinUDP(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, nhsPerNHG, vrfs, outerIPv6Src, outerIPv6Dst, outerDstUDPPort, outerTTL, outerDSCP) + entries, labelLists = programProfile4MPLSinUDP(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, nhsPerNHG, vrfs, outerIPv6Src, outerIPv6Dst, outerDstUDPPort, outerTTL, outerDSCP) // === Add IPv4 + IPv6 route entries === v4Entries, vrfV4PfxMap := buildProfile4IPv4Routes(t, dut, totalNHs/2, nhsPerNHG, baseIPv4, vrfs, nextHopGroupID) v6Entries, vrfV6PfxMap := buildProfile4IPv6Routes(t, dut, totalNHs/2, nhsPerNHG, baseIPv6, vrfs, nextHopGroupID) @@ -1075,10 +1421,28 @@ func configureVRFProfiles(ctx context.Context, t *testing.T, ateConfig gosnappi. entries = append(entries, v6Entries...) // === Expected OpResults === wantAdds, wantDels = expectedProfile4MPLSinUDPOpResults(t, nextHopID, nextHopGroupID, totalNHGs, totalPrefixes, nhsPerNHG, baseIPv4, baseIPv6, vrfs) - flows = append(flows, append(fa4.createFlow(t, ateConfig, "ipv4", fmt.Sprintf("ip4mpls_p%d", profile), dstmac, fa4.srcPort[0], outerDSCP, startVLANPort1, vrfV4PfxMap, false, tDstLists), fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfV6PfxMap, false, tDstLists)...)...) + flows = append(flows, append(fa4.createFlow(t, ateConfig, "ipv4", fmt.Sprintf("ip4mpls_p%d", profile), dstmac, fa4.srcPort[0], outerDSCP, startVLANPort1, vrfV4PfxMap, false, tV6DstLists), fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfV6PfxMap, false, tV6DstLists)...)...) + case profileMultiVRFIPv4ECMP: + entries, srcLists, labelLists = programProfileMultiVRFECMP(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, nhsPerNHG, vrfs[:p67VRFCount], outerIPv4Src, outerIPv4Dst, "ipv4", outerDstUDPPort, outerTTL, outerDSCP) + // === Add IPv4 route entries === + v4Entries, vrfV4PfxMap := buildProfileRoutes(t, dut, totalNHs, nhsPerNHG, baseIPv4, "ipv4", vrfs[:p67VRFCount], nextHopGroupID) + entries = append(entries, v4Entries...) + flows = append(flows, fa4.createFlow(t, ateConfig, "ipv4", fmt.Sprintf("ip4mpls_p%d", profile), dstmac, fa4.srcPort[0], outerDSCP, startVLANPort1, vrfV4PfxMap, false, tV4DstLists)...) + case profileMultiVRFIPv6ECMP: + entries, srcLists, labelLists = programProfileMultiVRFECMP(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, nhsPerNHG, vrfs[:p67VRFCount], outerIPv6Src, outerIPv6Dst, "ipv6", outerDstUDPPort, outerTTL, outerDSCP) + // === Add IPv6 route entries === + v6Entries, vrfV6PfxMap := buildProfileRoutes(t, dut, totalNHs, nhsPerNHG, baseIPv6, "ipv6", vrfs[:p67VRFCount], nextHopGroupID) + entries = append(entries, v6Entries...) + flows = append(flows, fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfV6PfxMap, false, tV6DstLists)...) + case profileMultiNHGIPv6: + entries, srcLists, labelLists = programProfile8MultiVRFIPv6MoUDP(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, nhsPerNHG, vrfs[:p8VRFCount], outerIPv6Src, outerIPv6Dst, outerDstUDPPort, outerTTL, outerDSCP) + // === Add IPv6 route entries === + v6Entries, vrfV6PfxMap := buildProfile8IPv6Routes(t, dut, totalNHGs, nhsPerNHG, baseIPv6, vrfs[:p8VRFCount], nextHopGroupID) + entries = append(entries, v6Entries...) + flows = append(flows, fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfV6PfxMap, false, tV6DstLists)...) default: // Profile 1 (single VRF) and Profile 5 fall here - entries = programMPLSinUDPEntries(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, vrfs, outerIPv6Src, outerIPv6Dst, outerDstUDPPort, outerTTL, outerDSCP) + entries, labelLists = programMPLSinUDPEntries(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, vrfs, outerIPv6Src, outerIPv6Dst, outerDstUDPPort, outerTTL, outerDSCP) // === Add IPv4 + IPv6 route entries === v4Entries, vrfV4PfxMap := buildIPv4Routes(t, dut, totalPrefixes/2, baseIPv4, vrfs, nextHopGroupID) v6Entries, vrfV6PfxMap := buildIPv6Routes(t, dut, totalPrefixes/2, baseIPv6, vrfs, nextHopGroupID) @@ -1086,7 +1450,7 @@ func configureVRFProfiles(ctx context.Context, t *testing.T, ateConfig gosnappi. entries = append(entries, v6Entries...) // === Expected OpResults === wantAdds, wantDels = expectedMPLSinUDPOpResults(t, nextHopID, nextHopGroupID, totalNHGs, totalPrefixes, baseIPv4, baseIPv6, vrfs) - flows = append(flows, append(fa4.createFlow(t, ateConfig, "ipv4", fmt.Sprintf("ip4mpls_p%d", profile), dstmac, fa4.srcPort[0], outerDSCP, startVLANPort1, vrfV4PfxMap, false, tDstLists), fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfV6PfxMap, false, tDstLists)...)...) + flows = append(flows, append(fa4.createFlow(t, ateConfig, "ipv4", fmt.Sprintf("ip4mpls_p%d", profile), dstmac, fa4.srcPort[0], outerDSCP, startVLANPort1, vrfV4PfxMap, false, tV6DstLists), fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfV6PfxMap, false, tV6DstLists)...)...) } testCaseArgs := &testCase{ @@ -1119,8 +1483,15 @@ func configureVRFProfiles(ctx context.Context, t *testing.T, ateConfig gosnappi. } routeTime := time.Now() for time.Since(routeTime) < batchTimeout { - got := len(gnmi.GetAll(t, dut, gnmi.OC().NetworkInstanceAny().Afts().Ipv6EntryAny().State())) - if got > totalPrefixes { + var addrEntries int + switch profile { + case profileMultiVRFIPv4ECMP: + addrEntries = len( + gnmi.GetAll(t, dut, gnmi.OC().NetworkInstanceAny().Afts().Ipv4EntryAny().State())) + default: + addrEntries = len(gnmi.GetAll(t, dut, gnmi.OC().NetworkInstanceAny().Afts().Ipv6EntryAny().State())) + } + if addrEntries >= totalPrefixes { break } time.Sleep(trafficDuration) @@ -1141,9 +1512,16 @@ func configureVRFProfiles(ctx context.Context, t *testing.T, ateConfig gosnappi. enableCapture(t, ate.OTG(), ateConfig, testCaseArgs.capturePorts) sendTraffic(t, tArgs, testCaseArgs.flows, true) for _, port := range testCaseArgs.capturePorts { - err := validateMPLSPacketCapture(t, ate, port, expectedPkt, tDstLists, labelLists) - if err != nil { - return fmt.Errorf("profile %d capture validation failed: %v", profile, err) + if profile == profileMultiVRFIPv4ECMP { + err := validateMPLSPacketCapture(t, ate, port, expectedPkt, tV4DstLists, srcLists, labelLists) + if err != nil { + return fmt.Errorf("profile %d capture validation failed: %v", profile, err) + } + } else { + err := validateMPLSPacketCapture(t, ate, port, expectedPkt, tV6DstLists, srcLists, labelLists) + if err != nil { + return fmt.Errorf("profile %d capture validation failed: %v", profile, err) + } } } @@ -1365,7 +1743,7 @@ func expandVRFsBySkew(t *testing.T, vrfs []string, skewPattern []int) []string { } // programMPLSinUDPSkewedEntries programs MPLS-in-UDP entries for Profile 3 using a skewed NHG distribution across VRFs. -func programMPLSinUDPSkewedEntries(t *testing.T, dut *ondatra.DUTDevice, vrfs []string, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs int, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, map[string][]string) { +func programMPLSinUDPSkewedEntries(t *testing.T, dut *ondatra.DUTDevice, vrfs []string, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs int, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, map[string][]string, []uint64) { t.Helper() entries := make([]fluent.GRIBIEntry, 0, totalNHGs) vrfSkewPfxMap := make(map[string][]string) @@ -1403,7 +1781,7 @@ func programMPLSinUDPSkewedEntries(t *testing.T, dut *ondatra.DUTDevice, vrfs [] entries = append(entries, fluent.IPv6Entry().WithNetworkInstance(vrfName).WithPrefix(prefixStr).WithNextHopGroup(nextHopGroupID).WithNextHopGroupNetworkInstance(defaultNI)) vrfSkewPfxMap[vrfName] = append(vrfSkewPfxMap[vrfName], prefixIPv6s[idx]) } - return entries, vrfSkewPfxMap + return entries, vrfSkewPfxMap, []uint64{labelValue} } // expectedMPLSinUDPSkewedResults builds the expected Add/Delete OperationResults for Profile 3 skewed MPLS-in-UDP programming. @@ -1531,6 +1909,7 @@ func sendTraffic(t *testing.T, args *testArgs, flows []gosnappi.Flow, capture bo // createFlow creates a traffic flow for MPLS-in-UDP testing. func (fa *flowAttr) createFlow(t *testing.T, cfg gosnappi.Config, flowType, name, dstmac, srcPort string, dscp, vlanID uint32, vrfDataLists map[string][]string, skewPattern bool, tDstLists []string) []gosnappi.Flow { + t.Helper() orderedVRFs := sortedVRFSkewList(vrfDataLists) var ( flows []gosnappi.Flow @@ -1548,7 +1927,6 @@ func (fa *flowAttr) createFlow(t *testing.T, cfg gosnappi.Config, flowType, name flow.Size().SetFixed(pktSize) flow.Duration().FixedPackets().SetPackets(fixedCount) flow.TxRx().Port().SetTxName(srcPort).SetRxNames(fa.dstPorts) - eth := flow.Packet().Add().Ethernet() eth.Src().SetValue(fa.srcMac) eth.Dst().SetValue(dstmac) @@ -1564,39 +1942,85 @@ func (fa *flowAttr) createFlow(t *testing.T, cfg gosnappi.Config, flowType, name if len(vrfDataLists) > 2 { var ( - nonDefaultDst []string - nonDefVlanIDs []uint32 + nonDefaultDst []string + defaultDst []string + nonDefVlanIDs []uint32 + skewPfxCounts []int + + // IPv6 helpers (used only for IPv6 flows) nonDefaultPfx1V6Lists []string - defaultDst []string defaultPfx1V6Lists []string - skewPfxCounts []int + + // IPv4 helpers (used only for IPv4 flows) + nonDefaultPfx1V4Lists []string + defaultPfx1V4Lists []string ) - if skewPattern { - for _, vrf := range orderedVRFs { - skewPfxCounts = append(skewPfxCounts, len(vrf.Prefixes)) - } - for pfxId, pfx := range pfx1V6Lists { - for i := 0; i < skewPfxCounts[pfxId]; i++ { - nonDefaultPfx1V6Lists = append(nonDefaultPfx1V6Lists, pfx) - nonDefVlanIDs = append(nonDefVlanIDs, vlanID) + if flowType == "ipv6" { + if skewPattern { + for _, vrf := range orderedVRFs { + skewPfxCounts = append(skewPfxCounts, len(vrf.Prefixes)) + } + for pfxID, pfx := range pfx1V6Lists { + for i := 0; i < skewPfxCounts[pfxID]; i++ { + nonDefaultPfx1V6Lists = append(nonDefaultPfx1V6Lists, pfx) + nonDefVlanIDs = append(nonDefVlanIDs, vlanID) + } + vlanID++ + } + } else { + for _, pfx := range pfx1V6Lists { + for i := 0; i < pfxCount; i++ { + nonDefaultPfx1V6Lists = append(nonDefaultPfx1V6Lists, pfx) + nonDefVlanIDs = append(nonDefVlanIDs, vlanID) + } + vlanID++ } - vlanID++ } + } else { - for _, pfx := range pfx1V6Lists { - for i := 0; i < pfxCount; i++ { - nonDefaultPfx1V6Lists = append(nonDefaultPfx1V6Lists, pfx) - nonDefVlanIDs = append(nonDefVlanIDs, vlanID) + if skewPattern { + for _, vrf := range orderedVRFs { + skewPfxCounts = append(skewPfxCounts, len(vrf.Prefixes)) + } + for range pfx1V6Lists { + for i := 0; i < skewPfxCounts[len(nonDefaultPfx1V4Lists)]; i++ { + nonDefaultPfx1V4Lists = append(nonDefaultPfx1V4Lists, dutV4AddrPfx1) + nonDefVlanIDs = append(nonDefVlanIDs, vlanID) + } + vlanID++ + } + } else { + total := 0 + for _, entry := range orderedVRFs { + if strings.ToUpper(entry.VRF) != "DEFAULT" { + total += len(entry.Prefixes) + } + } + for i := 0; i < total; i++ { + nonDefaultPfx1V4Lists = append(nonDefaultPfx1V4Lists, dutV4AddrPfx1) + } + for _, entry := range orderedVRFs { + if strings.ToUpper(entry.VRF) != "DEFAULT" { + for i := 0; i < len(entry.Prefixes); i++ { + nonDefVlanIDs = append(nonDefVlanIDs, vlanID) + } + vlanID++ + } } - vlanID++ } } for _, entry := range orderedVRFs { if strings.ToUpper(entry.VRF) == "DEFAULT" { defaultDst = append(defaultDst, entry.Prefixes...) - for i := 0; i < len(entry.Prefixes); i++ { - defaultPfx1V6Lists = append(defaultPfx1V6Lists, dutPort1.IPv6) + if flowType == "ipv6" { + for i := 0; i < len(entry.Prefixes); i++ { + defaultPfx1V6Lists = append(defaultPfx1V6Lists, dutPort1.IPv6) + } + } else { + for i := 0; i < len(entry.Prefixes); i++ { + defaultPfx1V4Lists = append(defaultPfx1V4Lists, dutPort1.IPv4) + } } continue } @@ -1606,16 +2030,21 @@ func (fa *flowAttr) createFlow(t *testing.T, cfg gosnappi.Config, flowType, name // ---- NON-DEFAULT aggregated flow (WITH VLAN) ---- if len(nonDefaultDst) > 0 { flow := cfg.Flows().Add().SetName(fmt.Sprintf("%s_non_default", name)) - applyCommon(flow) - flow.Packet().Add().Vlan().Id().SetValues(nonDefVlanIDs) - v6 := flow.Packet().Add().Ipv6() - v6.Src().SetValues(nonDefaultPfx1V6Lists) - v6.Dst().SetValues(nonDefaultDst) - v6.HopLimit().SetValue(innerTTL) - v6.TrafficClass().SetValue(dscp << 2) - + if flowType == "ipv6" { + v6 := flow.Packet().Add().Ipv6() + v6.Src().SetValues(nonDefaultPfx1V6Lists) + v6.Dst().SetValues(nonDefaultDst) + v6.HopLimit().SetValue(innerTTL) + v6.TrafficClass().SetValue(dscp << 2) + } else { + v4 := flow.Packet().Add().Ipv4() + v4.Src().SetValues(nonDefaultPfx1V4Lists) + v4.Dst().SetValues(nonDefaultDst) + v4.TimeToLive().SetValue(innerTTL) + v4.Priority().Dscp().Phb().SetValue(dscp) + } addUDP(flow) flows = append(flows, flow) } @@ -1623,19 +2052,23 @@ func (fa *flowAttr) createFlow(t *testing.T, cfg gosnappi.Config, flowType, name // ---- DEFAULT VRF aggregated flow (NO VLAN) ---- if len(defaultDst) > 0 { flow := cfg.Flows().Add().SetName(fmt.Sprintf("%s_default", name)) - applyCommon(flow) - - v6 := flow.Packet().Add().Ipv6() - v6.Src().SetValues(defaultPfx1V6Lists) - v6.Dst().SetValues(defaultDst) - v6.HopLimit().SetValue(innerTTL) - v6.TrafficClass().SetValue(dscp << 2) - + if flowType == "ipv6" { + v6 := flow.Packet().Add().Ipv6() + v6.Src().SetValues(defaultPfx1V6Lists) + v6.Dst().SetValues(defaultDst) + v6.HopLimit().SetValue(innerTTL) + v6.TrafficClass().SetValue(dscp << 2) + } else { + v4 := flow.Packet().Add().Ipv4() + v4.Src().SetValues(defaultPfx1V4Lists) + v4.Dst().SetValues(defaultDst) + v4.TimeToLive().SetValue(innerTTL) + v4.Priority().Dscp().Phb().SetValue(dscp) + } addUDP(flow) flows = append(flows, flow) } - return flows } // ---- Per-VRF flow case ---- From 22aa1bf8137700cae189cd392bf9ebe3e897841b Mon Sep 17 00:00:00 2001 From: moharedd Date: Sun, 19 Jul 2026 12:30:30 +0000 Subject: [PATCH 2/4] Fixed Gemini review comments --- .../mpls_in_udp_scale_test.go | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go b/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go index 13387c25541..485c303254e 100644 --- a/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go +++ b/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go @@ -51,8 +51,8 @@ const ( nextHopGroupID = uint64(50001) outerIPv6Src = "2001:db8:300::1" outerIPv6Dst = "6001:0:0:1::1" - outerIPv4Src = "194.51.0.1" - outerIPv4Dst = "195.51.0.1" + outerIPv4Src = "198.51.100.1" + outerIPv4Dst = "198.51.100.101" outerDstUDPPort = 6635 outerDSCP = 26 outerTTL = 64 @@ -783,7 +783,7 @@ func buildProfileRoutes(t *testing.T, dut *ondatra.DUTDevice, totalNHs, routesPe routePfxLen = routeV4PrfLen } if err != nil { - t.Fatalf("failed to generate IPv4 prefixes: %v", err) + t.Fatalf("failed to generate %s prefixes: %v", flowType, err) } entries := make([]fluent.GRIBIEntry, 0, totalPrefixes) vrfV4PfxMap := make(map[string][]string) @@ -1030,7 +1030,7 @@ func programProfileMultiVRFECMP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, // programProfile8MultiVRFIPv6MoUDP builds Profile 8 gRIBI NextHop and NextHopGroup entries for IPv6 MoUDP encapsulation across multiple VRFs. It programs 64 reusable source IPv6 addresses, 16 destination IPv6 addresses per NHG, and 16 unique MPLS labels per NHG, and returns the generated gRIBI entries and reusable source IPv6 addresses. func programProfile8MultiVRFIPv6MoUDP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs, nhsPerNHG int, vrfs []string, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, []string, []uint64) { t.Helper() - var lableList []uint64 + var labelList []uint64 if len(vrfs) == 0 { t.Fatal("no VRFs provided") } @@ -1077,7 +1077,7 @@ func programProfile8MultiVRFIPv6MoUDP(t *testing.T, dut *ondatra.DUTDevice, next for labelIdx := 0; labelIdx < labelsPerNHG; labelIdx++ { label := globalLabel globalLabel++ - lableList = append(lableList, label) + labelList = append(labelList, label) dstIP := dstIPs[dstBase+labelIdx] for srcIdx := 0; srcIdx < srcIPsPerBlock; srcIdx++ { entries = append(entries, @@ -1106,7 +1106,7 @@ func programProfile8MultiVRFIPv6MoUDP(t *testing.T, dut *ondatra.DUTDevice, next nhgID++ nhgGlobal++ } - return entries, srcIPs, lableList + return entries, srcIPs, labelList } // staticARPWithUniversalIP programs static routes with interface-based next-hops and corresponding static ARP entries for a single destination IP across multiple VRFs and ports. @@ -1197,7 +1197,7 @@ func validateMPLSPacketCapture(t *testing.T, ate *ondatra.ATEDevice, otgPortName } if len(srcLists) != 0 { if !slices.Contains(srcLists, v4.SrcIP.String()) { - return fmt.Errorf("packet %d: got IPv4 src %s, want %s", packetCount, v4.SrcIP, pr.srcIP) + return fmt.Errorf("packet %d: got IPv4 src %s, want one of %v", packetCount, v4.SrcIP, srcLists) } } else { if v4.SrcIP.String() != pr.srcIP { @@ -1215,7 +1215,7 @@ func validateMPLSPacketCapture(t *testing.T, ate *ondatra.ATEDevice, otgPortName } if len(srcLists) != 0 { if !slices.Contains(srcLists, v6.SrcIP.String()) { - return fmt.Errorf("packet %d: got IPv6 src %s, want %s", packetCount, v6.SrcIP, pr.srcIP) + return fmt.Errorf("packet %d: got IPv6 src %s, want one of %v", packetCount, v6.SrcIP, srcLists) } } else { if v6.SrcIP.String() != pr.srcIP { @@ -1481,6 +1481,13 @@ func configureVRFProfiles(ctx context.Context, t *testing.T, ateConfig gosnappi. for _, batch := range batches { c.AddEntries(t, batch, nil) } + expectedPrefixes := totalPrefixes + switch profile { + case profileMultiVRFIPv4ECMP, + profileMultiVRFIPv6ECMP, + profileMultiNHGIPv6: + expectedPrefixes = totalNHs + } routeTime := time.Now() for time.Since(routeTime) < batchTimeout { var addrEntries int @@ -1491,7 +1498,7 @@ func configureVRFProfiles(ctx context.Context, t *testing.T, ateConfig gosnappi. default: addrEntries = len(gnmi.GetAll(t, dut, gnmi.OC().NetworkInstanceAny().Afts().Ipv6EntryAny().State())) } - if addrEntries >= totalPrefixes { + if addrEntries >= expectedPrefixes { break } time.Sleep(trafficDuration) @@ -1983,8 +1990,8 @@ func (fa *flowAttr) createFlow(t *testing.T, cfg gosnappi.Config, flowType, name for _, vrf := range orderedVRFs { skewPfxCounts = append(skewPfxCounts, len(vrf.Prefixes)) } - for range pfx1V6Lists { - for i := 0; i < skewPfxCounts[len(nonDefaultPfx1V4Lists)]; i++ { + for _, count := range skewPfxCounts { + for i := 0; i < count; i++ { nonDefaultPfx1V4Lists = append(nonDefaultPfx1V4Lists, dutV4AddrPfx1) nonDefVlanIDs = append(nonDefVlanIDs, vlanID) } From e708d5114dc61c8eff0a5899ee9fea5ff3f000f8 Mon Sep 17 00:00:00 2001 From: moharedd Date: Fri, 7 Aug 2026 07:10:39 +0000 Subject: [PATCH 3/4] Fixed review comments --- .../mpls_in_udp_scale_test.go | 290 +++++++++++------- 1 file changed, 171 insertions(+), 119 deletions(-) diff --git a/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go b/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go index 485c303254e..15673abf87b 100644 --- a/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go +++ b/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go @@ -90,6 +90,8 @@ const ( allowedLossPct = 1.0 p67VRFCount = 1000 p8VRFCount = 20 + uniqueSrcIPs = 32 + maxNHCountSupport = 50 // Currently, the maximum supported number of next hops (NHs) per MPLS-over-UDP (MoUDP) Next-Hop Group (NHG) is 256 (but only working with count 50). Support for 1024 next hops per NHG will be available in a future release. ) // DUT and ATE port attributes @@ -426,68 +428,87 @@ func configureOTG(t *testing.T, ate *ondatra.ATEDevice, dut *ondatra.DUTDevice) // programBasicEntries programs baseline gRIBI entries including NextHops, NextHopGroups, and IPv6 routes in the default network instance. It sets up ECMP forwarding across port3 and port4 for MPLS-in-UDP testing. func programBasicEntries(t *testing.T, dut *ondatra.DUTDevice, c *gribi.Client, vrfs []string) ([]string, []string) { t.Helper() - t.Log("Setting up routing infrastructure for MPLS-in-UDP with unique NH IDs and NH/NHG in default NI") - var tV4DstLists, tV6DstLists []string - otgDummyIPs := map[string]string{ - dut.Port(t, "port3").Name(): otgPort3DummyIP.IPv4, - dut.Port(t, "port4").Name(): otgPort4DummyIP.IPv4, - } - defaultNI := deviations.DefaultNetworkInstance(dut) + var ( + tV4DstLists []string + tV6DstLists []string + ) + // Generate tunnel destination IPs (one per VRF). tunnelPrefixV6, err := iputil.GenerateIPv6sWithStep(outerIPv6Dst, len(vrfs), intStepV6) if err != nil { - t.Fatalf("failed to generate DUT IPv6s: %v", err) + t.Fatalf("failed to generate IPv6 tunnel destinations: %v", err) } tV6DstLists = append(tV6DstLists, tunnelPrefixV6...) tunnelPrefixV4, err := iputil.GenerateIPsWithStep(outerIPv4Dst, len(vrfs), intStepV4) if err != nil { - t.Fatalf("failed to generate DUT IPs: %v", err) + t.Fatalf("failed to generate IPv4 tunnel destinations: %v", err) } tV4DstLists = append(tV4DstLists, tunnelPrefixV4...) - var allEntries []fluent.GRIBIEntry - // iterate VRFs and program unique NH, NHG in default NI; program IPv6 prefix in the VRF - for vrfIdx := range vrfs { - // generate unique NH IDs for the two egress ports for this VRF - nhIDs := []uint64{ - nhBaseValue + uint64(vrfIdx*5) + 0, // port3 - nhBaseValue + uint64(vrfIdx*5) + 1, // port4 - } - nhgID := nhgBaseValue + uint64(vrfIdx) // unique NHG per VRF - - for i, portName := range []string{"port3", "port4"} { - port := dut.Port(t, portName) - otgDummyIP := otgDummyIPs[port.Name()] - // Use MACwithInterface or MACwithIp depending on deviations; important: network instance = defaultNI - switch { - case deviations.GRIBIMACOverrideWithStaticARP(dut): - nh, _ := gribi.NHEntry(nhIDs[i], "MACwithIp", defaultNI, fluent.InstalledInFIB, &gribi.NHOptions{Dest: otgDummyIP, Mac: dstMac}) - allEntries = append(allEntries, nh) - - case deviations.GRIBIMACOverrideStaticARPStaticRoute(dut): - nh, _ := gribi.NHEntry(nhIDs[i], "MACwithInterface", defaultNI, fluent.InstalledInFIB, &gribi.NHOptions{Interface: port.Name(), Mac: dstMac, Dest: dstIP}) - allEntries = append(allEntries, nh) - - default: - nh, _ := gribi.NHEntry(nhIDs[i], "MACwithInterface", defaultNI, fluent.InstalledInFIB, &gribi.NHOptions{Interface: port.Name(), Mac: dstMac}) - allEntries = append(allEntries, nh) - } - } - - // Build NHG in default NI (pointing to the two NHs) - nhMap := map[uint64]uint64{nhIDs[0]: 1, nhIDs[1]: 1} - nhg, _ := gribi.NHGEntry(nhgID, nhMap, defaultNI, fluent.InstalledInFIB) - allEntries = append(allEntries, nhg) - ipv4Entry := fluent.IPv4Entry().WithPrefix(tunnelPrefixV4[vrfIdx] + routeV4PrfLen).WithNetworkInstance(defaultNI).WithNextHopGroup(nhgID) - allEntries = append(allEntries, ipv4Entry) - ipv6Entry := fluent.IPv6Entry().WithPrefix(tunnelPrefixV6[vrfIdx] + routeV6PrfLen).WithNetworkInstance(defaultNI).WithNextHopGroup(nhgID) - allEntries = append(allEntries, ipv6Entry) + otgDummyIPs := map[string]string{ + dut.Port(t, "port3").Name(): otgPort3DummyIP.IPv4, + dut.Port(t, "port4").Name(): otgPort4DummyIP.IPv4, } - // Install NH + NHG in DEFAULT NI - batches := splitEntries(allEntries, gribiBatchSize) - for _, batch := range batches { + // ------------------------------------------------------------------ + // Create only TWO NHs. + // ------------------------------------------------------------------ + nhIDs := []uint64{ + nhBaseValue, + nhBaseValue + 1, + } + allEntries := make([]fluent.GRIBIEntry, 0, len(vrfs)*2+3) + for i, portName := range []string{"port3", "port4"} { + port := dut.Port(t, portName) + switch { + case deviations.GRIBIMACOverrideWithStaticARP(dut): + nh, _ := gribi.NHEntry(nhIDs[i], "MACwithIp", defaultNI, fluent.InstalledInFIB, + &gribi.NHOptions{ + Dest: otgDummyIPs[port.Name()], + Mac: dstMac, + }) + allEntries = append(allEntries, nh) + case deviations.GRIBIMACOverrideStaticARPStaticRoute(dut): + nh, _ := gribi.NHEntry(nhIDs[i], "MACwithInterface", defaultNI, fluent.InstalledInFIB, + &gribi.NHOptions{ + Interface: port.Name(), + Mac: dstMac, + Dest: dstIP, + }, + ) + allEntries = append(allEntries, nh) + default: + nh, _ := gribi.NHEntry(nhIDs[i], "MACwithInterface", defaultNI, fluent.InstalledInFIB, + &gribi.NHOptions{ + Interface: port.Name(), + Mac: dstMac, + }, + ) + allEntries = append(allEntries, nh) + } + } + // ------------------------------------------------------------------ + // Create ONE NHG. + // ------------------------------------------------------------------ + underlayNHGID := nhgBaseValue + nhMap := map[uint64]uint64{ + nhIDs[0]: 1, + nhIDs[1]: 1, + } + nhg, _ := gribi.NHGEntry(underlayNHGID, nhMap, defaultNI, fluent.InstalledInFIB) + allEntries = append(allEntries, nhg) + // ------------------------------------------------------------------ + // Install tunnel-resolution routes. + // Every tunnel destination points to the SAME NHG. + // ------------------------------------------------------------------ + for i := range vrfs { + allEntries = append(allEntries, + fluent.IPv4Entry().WithNetworkInstance(defaultNI).WithPrefix(tunnelPrefixV4[i]+routeV4PrfLen).WithNextHopGroup(underlayNHGID), + fluent.IPv6Entry().WithNetworkInstance(defaultNI).WithPrefix(tunnelPrefixV6[i]+routeV6PrfLen).WithNextHopGroup(underlayNHGID), + ) + } + for _, batch := range splitEntries(allEntries, gribiBatchSize) { c.AddEntries(t, batch, nil) } - t.Log("programBasicEntries completed") + t.Logf("Installed %d tunnel-resolution routes using NHG %d", len(vrfs)*2, underlayNHGID) return tV4DstLists, tV6DstLists } @@ -758,64 +779,63 @@ func buildProfile4IPv6Routes(t *testing.T, dut *ondatra.DUTDevice, totalNHGs, ro // buildProfileRoutes builds IPv4 or IPv6 route entries distributed across the specified VRFs and maps each route to a NextHopGroup. It returns the gRIBI route entries and a map of VRFs to their generated destination prefixes. func buildProfileRoutes(t *testing.T, dut *ondatra.DUTDevice, totalNHs, routesPerNHG int, baseIP, flowType string, vrfs []string, nextHopGroupID uint64) ([]fluent.GRIBIEntry, map[string][]string) { t.Helper() - var ( - addrFamilyList []string - err error - routePfxLen string - ) if len(vrfs) == 0 { t.Fatal("no VRFs supplied") } + if totalNHs%routesPerNHG != 0 { + t.Fatalf("invalid configuration: totalNHs=%d must be divisible by routesPerNHG=%d", totalNHs, routesPerNHG) + } totalNHGs := totalNHs / routesPerNHG - // NHGs are distributed evenly across the configured VRFs. + // Distribute NHGs across VRFs. vrfCount := len(vrfs) if totalNHGs < vrfCount { t.Fatalf("invalid configuration: totalNHGs (%d) must be >= VRFs (%d)", totalNHGs, vrfCount) } baseNHGsPerVRF := totalNHGs / vrfCount extraNHGs := totalNHGs % vrfCount - totalPrefixes := totalNHs + var ( + reusablePrefixes []string + routePfxLen string + err error + ) if flowType == "ipv6" { - addrFamilyList, err = iputil.GenerateIPv6sWithStep(baseIP, totalPrefixes, intStepV6) + reusablePrefixes, err = iputil.GenerateIPv6sWithStep(baseIP, routesPerNHG, intStepV6) routePfxLen = routeV6PrfLen } else { - addrFamilyList, err = iputil.GenerateIPsWithStep(baseIP, totalPrefixes, intStepV4) + reusablePrefixes, err = iputil.GenerateIPsWithStep(baseIP, routesPerNHG, intStepV4) routePfxLen = routeV4PrfLen } if err != nil { - t.Fatalf("failed to generate %s prefixes: %v", flowType, err) + t.Fatalf("failed to generate reusable %s prefixes: %v", flowType, err) } - entries := make([]fluent.GRIBIEntry, 0, totalPrefixes) - vrfV4PfxMap := make(map[string][]string) - addrIdx := 0 - nhgBase := nextHopGroupID + entries := make([]fluent.GRIBIEntry, 0, totalNHs) + vrfPrefixMap := make(map[string][]string) + nhgID := nextHopGroupID for vrfIdx := 0; vrfIdx < vrfCount; vrfIdx++ { currentNHGs := baseNHGsPerVRF if vrfIdx < extraNHGs { currentNHGs++ } - currentPrefixes := currentNHGs * routesPerNHG - for curPfxIndx := 0; curPfxIndx < currentPrefixes; curPfxIndx++ { - addrFamily := addrFamilyList[addrIdx] - addrIdx++ - addrFamilyPrefix := addrFamily + routePfxLen - vrfV4PfxMap[vrfs[vrfIdx]] = append(vrfV4PfxMap[vrfs[vrfIdx]], addrFamily) - nhgID := nhgBase + uint64(curPfxIndx/routesPerNHG) - if flowType == "ipv6" { - entries = append(entries, fluent.IPv6Entry().WithNetworkInstance(vrfs[vrfIdx]).WithPrefix(addrFamilyPrefix).WithNextHopGroup(nhgID).WithNextHopGroupNetworkInstance(deviations.DefaultNetworkInstance(dut))) - } else { - entries = append(entries, fluent.IPv4Entry().WithNetworkInstance(vrfs[vrfIdx]).WithPrefix(addrFamilyPrefix).WithNextHopGroup(nhgID).WithNextHopGroupNetworkInstance(deviations.DefaultNetworkInstance(dut))) + for nhgIdx := 0; nhgIdx < currentNHGs; nhgIdx++ { + for _, prefix := range reusablePrefixes { + vrfPrefixMap[vrfs[vrfIdx]] = append(vrfPrefixMap[vrfs[vrfIdx]], prefix) + fullPrefix := prefix + routePfxLen + if flowType == "ipv6" { + entries = append(entries, fluent.IPv6Entry().WithNetworkInstance(vrfs[vrfIdx]).WithPrefix(fullPrefix).WithNextHopGroup(nhgID).WithNextHopGroupNetworkInstance(deviations.DefaultNetworkInstance(dut))) + } else { + entries = append(entries, fluent.IPv4Entry().WithNetworkInstance(vrfs[vrfIdx]).WithPrefix(fullPrefix).WithNextHopGroup(nhgID).WithNextHopGroupNetworkInstance(deviations.DefaultNetworkInstance(dut))) + } } + nhgID++ } - // Advance to the first NHG for the next VRF. - nhgBase += uint64(currentNHGs) } - return entries, vrfV4PfxMap + return entries, vrfPrefixMap } // buildProfile8IPv6Routes builds IPv6 route entries for Profile 8 by distributing NextHopGroups across the specified VRFs. It returns the generated gRIBI IPv6 entries and a map of VRFs to their IPv6 prefixes. func buildProfile8IPv6Routes(t *testing.T, dut *ondatra.DUTDevice, totalNHGs, nhsPerNHG int, baseIPv6 string, vrfs []string, nextHopGroupID uint64) ([]fluent.GRIBIEntry, map[string][]string) { t.Helper() + nhsPerNHG = maxNHCountSupport if len(vrfs) == 0 { t.Fatal("no VRFs supplied") } @@ -825,32 +845,28 @@ func buildProfile8IPv6Routes(t *testing.T, dut *ondatra.DUTDevice, totalNHGs, nh } baseNHGsPerVRF := totalNHGs / vrfCount extraNHGs := totalNHGs % vrfCount - totalPrefixes := totalNHGs * nhsPerNHG //-------------------------------------------------------- // Generate one unique IPv6 prefix per route. //-------------------------------------------------------- - ipv6List, err := iputil.GenerateIPv6sWithStep(baseIPv6, totalPrefixes, intStepV6) + ipv6List, err := iputil.GenerateIPv6sWithStep(baseIPv6, nhsPerNHG, intStepV6) if err != nil { t.Fatalf("GenerateIPv6sWithStep failed: %v", err) } entries := make([]fluent.GRIBIEntry, 0, totalPrefixes) vrfV6PfxMap := make(map[string][]string) - ipIdx := 0 nhgBase := nextHopGroupID for vrfIdx := 0; vrfIdx < vrfCount; vrfIdx++ { currentNHGs := baseNHGsPerVRF if vrfIdx < extraNHGs { currentNHGs++ } - currentPrefixes := currentNHGs * nhsPerNHG - for i := 0; i < currentPrefixes; i++ { - ip := ipv6List[ipIdx] - ipIdx++ - vrfV6PfxMap[vrfs[vrfIdx]] = append(vrfV6PfxMap[vrfs[vrfIdx]], ip) - nhgID := nhgBase + uint64(i/nhsPerNHG) - entries = append(entries, fluent.IPv6Entry().WithNetworkInstance(vrfs[vrfIdx]).WithPrefix(ip+routeV6PrfLen).WithNextHopGroup(nhgID).WithNextHopGroupNetworkInstance(deviations.DefaultNetworkInstance(dut))) + for nhg := 0; nhg < currentNHGs; nhg++ { + nhgID := nhgBase + uint64(nhg) + for _, ip := range ipv6List { + vrfV6PfxMap[vrfs[vrfIdx]] = append(vrfV6PfxMap[vrfs[vrfIdx]], ip) + entries = append(entries, fluent.IPv6Entry().WithNetworkInstance(vrfs[vrfIdx]).WithPrefix(ip+routeV6PrfLen).WithNextHopGroup(nhgID).WithNextHopGroupNetworkInstance(deviations.DefaultNetworkInstance(dut))) + } } - // Advance NHG base for the next VRF. nhgBase += uint64(currentNHGs) } return entries, vrfV6PfxMap @@ -931,13 +947,12 @@ func expectedMPLSinUDPMultiOpResults(t *testing.T, vrfs []string, nextHopID, nex } // programProfileMultiVRFECMP builds Profile ECMP gRIBI NextHop and NextHopGroup entries across the specified VRFs. It returns the generated gRIBI entries and the outer source IP addresses used for encapsulation. -func programProfileMultiVRFECMP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs, nhsPerNHG int, vrfs []string, outerSrc, outerDst, flowType string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, []string, []uint64) { +func programProfileMultiVRFECMP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs, nhsPerNHG int, vrfs, vrfOuterDst []string, outerSrc, flowType string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, []string, []uint64) { t.Helper() var ( - vrfOuterDst []string - srcIPs []string - labelLists []uint64 - err error + srcIPs []string + labelLists []uint64 + err error ) if len(vrfs) == 0 { t.Fatal("no VRFs provided") @@ -950,25 +965,19 @@ func programProfileMultiVRFECMP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, extraNHGs := totalNHGs % vrfCount totalNHs := totalNHGs * nhsPerNHG entries := make([]fluent.GRIBIEntry, 0, totalNHs+totalNHGs) + // Generate only 100 unique tunnel source IPs and reuse them. if flowType == "ipv6" { - vrfOuterDst, err = iputil.GenerateIPv6sWithStep(outerDst, vrfCount, intStepV6) + srcIPs, err = iputil.GenerateIPv6sWithStep(outerSrc, uniqueSrcIPs, intStepV6) } else { - vrfOuterDst, err = iputil.GenerateIPsWithStep(outerDst, vrfCount, intStepV4) + srcIPs, err = iputil.GenerateIPsWithStep(outerSrc, uniqueSrcIPs, intStepV4) } if err != nil { - t.Fatalf("GenerateIPsWithStep failed: %v", err) - } - if flowType == "ipv6" { - srcIPs, err = iputil.GenerateIPv6sWithStep(outerSrc, totalNHs, intStepV6) - } else { - srcIPs, err = iputil.GenerateIPsWithStep(outerSrc, totalNHs, intStepV4) - } - if err != nil { - t.Fatalf("GenerateIPsWithStep failed: %v", err) + t.Fatalf("Generate source IPs failed: %v", err) } nhVal := nextHopID nhgVal := nextHopGroupID label := labelValue + srcIPIdx := 0 for vrfIdx := 0; vrfIdx < vrfCount; vrfIdx++ { nhgsVRF := baseNHGsPerVRF if vrfIdx < extraNHGs { @@ -980,6 +989,7 @@ func programProfileMultiVRFECMP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, label++ labelLists = append(labelLists, currentLabel) for nhsIdx := 0; nhsIdx < nhsPerNHG; nhsIdx++ { + currentSrcIP := srcIPs[srcIPIdx%uniqueSrcIPs] if flowType == "ipv6" { entries = append(entries, fluent.NextHopEntry(). @@ -988,13 +998,14 @@ func programProfileMultiVRFECMP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, AddEncapHeader( fluent.MPLSEncapHeader().WithLabels(currentLabel), fluent.UDPV6EncapHeader(). - WithSrcIP(srcIPs[nhsIdx]). + WithSrcIP(currentSrcIP). WithDstIP(vrfOuterDst[vrfIdx]). WithDstUDPPort(uint64(outerDstUDPPort)). WithIPTTL(uint64(outerTTL)). WithDSCP(uint64(outerDSCP)), ), ) + } else { entries = append(entries, fluent.NextHopEntry(). @@ -1003,7 +1014,7 @@ func programProfileMultiVRFECMP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, AddEncapHeader( fluent.MPLSEncapHeader().WithLabels(currentLabel), fluent.UDPV4EncapHeader(). - WithSrcIP(srcIPs[nhsIdx]). + WithSrcIP(currentSrcIP). WithDstIP(vrfOuterDst[vrfIdx]). WithDstUDPPort(uint64(outerDstUDPPort)). WithIPTTL(uint64(outerTTL)). @@ -1012,6 +1023,7 @@ func programProfileMultiVRFECMP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, ) } nhVal++ + srcIPIdx++ } //------------------------------------------------ // NHG @@ -1030,6 +1042,7 @@ func programProfileMultiVRFECMP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, // programProfile8MultiVRFIPv6MoUDP builds Profile 8 gRIBI NextHop and NextHopGroup entries for IPv6 MoUDP encapsulation across multiple VRFs. It programs 64 reusable source IPv6 addresses, 16 destination IPv6 addresses per NHG, and 16 unique MPLS labels per NHG, and returns the generated gRIBI entries and reusable source IPv6 addresses. func programProfile8MultiVRFIPv6MoUDP(t *testing.T, dut *ondatra.DUTDevice, nextHopID, nextHopGroupID, labelValue uint64, totalNHGs, nhsPerNHG int, vrfs []string, outerIPv6Src, outerIPv6Dst string, outerDstUDPPort uint16, outerTTL, outerDSCP uint8) ([]fluent.GRIBIEntry, []string, []uint64) { t.Helper() + nhsPerNHG = maxNHCountSupport var labelList []uint64 if len(vrfs) == 0 { t.Fatal("no VRFs provided") @@ -1039,9 +1052,6 @@ func programProfile8MultiVRFIPv6MoUDP(t *testing.T, dut *ondatra.DUTDevice, next labelsPerNHG = 16 dstIPsPerNHG = 16 ) - if nhsPerNHG != labelsPerNHG*srcIPsPerBlock { - t.Fatalf("invalid Profile8 configuration: nhsPerNHG=%d want=%d", nhsPerNHG, labelsPerNHG*srcIPsPerBlock) - } vrfCount := len(vrfs) if totalNHGs < vrfCount { vrfCount = totalNHGs @@ -1098,11 +1108,13 @@ func programProfile8MultiVRFIPv6MoUDP(t *testing.T, dut *ondatra.DUTDevice, next } } //------------------------------------------------ - // NHG + // Build ONE NHG with 1024 members. //------------------------------------------------ + nhgEntry := fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)).WithID(nhgID) for i := 0; i < nhsPerNHG; i++ { - entries = append(entries, fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)).WithID(nhgID).AddNextHop(baseNH+uint64(i), 1)) + nhgEntry.AddNextHop(baseNH+uint64(i), 1) } + entries = append(entries, nhgEntry) nhgID++ nhgGlobal++ } @@ -1356,17 +1368,18 @@ func TestMPLSinUDPScale(t *testing.T) { } }) t.Run("Profile-6-Multi-VRF IPv4 Tunnels - 8-way ECMP", func(t *testing.T) { - if err := configureVRFProfiles(ctx, t, ateConfig, dut, ate, vrfsList, profileMultiVRFIPv4ECMP, false, dutPort1Mac); err != nil { + if err := configureVRFProfiles(ctx, t, ateConfig, dut, ate, vrfsList[1:p67VRFCount+1], profileMultiVRFIPv4ECMP, false, dutPort1Mac); err != nil { t.Fatalf("configureVrfProfiles failed: %v", err) } }) t.Run("Profile-7-Multi-VRF IPv6 Tunnels - 8-way ECMP", func(t *testing.T) { - if err := configureVRFProfiles(ctx, t, ateConfig, dut, ate, vrfsList, profileMultiVRFIPv6ECMP, false, dutPort1Mac); err != nil { + if err := configureVRFProfiles(ctx, t, ateConfig, dut, ate, vrfsList[1:p67VRFCount+1], profileMultiVRFIPv6ECMP, false, dutPort1Mac); err != nil { t.Fatalf("configureVrfProfiles failed: %v", err) } }) t.Run("Profile-8-Multi-NHG MoUDP IPv6 Tunnels - 1024 Tunnels per NHG", func(t *testing.T) { - if err := configureVRFProfiles(ctx, t, ateConfig, dut, ate, vrfsList, profileMultiNHGIPv6, false, dutPort1Mac); err != nil { + // As required by the test, use the programBasicEntries method to install additional 300 tunnel-resolution routes. + if err := configureVRFProfiles(ctx, t, ateConfig, dut, ate, vrfsList[1:p8VRFCount+301], profileMultiNHGIPv6, false, dutPort1Mac); err != nil { t.Fatalf("configureVrfProfiles failed: %v", err) } }) @@ -1423,15 +1436,15 @@ func configureVRFProfiles(ctx context.Context, t *testing.T, ateConfig gosnappi. wantAdds, wantDels = expectedProfile4MPLSinUDPOpResults(t, nextHopID, nextHopGroupID, totalNHGs, totalPrefixes, nhsPerNHG, baseIPv4, baseIPv6, vrfs) flows = append(flows, append(fa4.createFlow(t, ateConfig, "ipv4", fmt.Sprintf("ip4mpls_p%d", profile), dstmac, fa4.srcPort[0], outerDSCP, startVLANPort1, vrfV4PfxMap, false, tV6DstLists), fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfV6PfxMap, false, tV6DstLists)...)...) case profileMultiVRFIPv4ECMP: - entries, srcLists, labelLists = programProfileMultiVRFECMP(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, nhsPerNHG, vrfs[:p67VRFCount], outerIPv4Src, outerIPv4Dst, "ipv4", outerDstUDPPort, outerTTL, outerDSCP) + entries, srcLists, labelLists = programProfileMultiVRFECMP(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, nhsPerNHG, vrfs, tV4DstLists, outerIPv4Src, "ipv4", outerDstUDPPort, outerTTL, outerDSCP) // === Add IPv4 route entries === - v4Entries, vrfV4PfxMap := buildProfileRoutes(t, dut, totalNHs, nhsPerNHG, baseIPv4, "ipv4", vrfs[:p67VRFCount], nextHopGroupID) + v4Entries, vrfV4PfxMap := buildProfileRoutes(t, dut, totalNHs, nhsPerNHG, baseIPv4, "ipv4", vrfs, nextHopGroupID) entries = append(entries, v4Entries...) flows = append(flows, fa4.createFlow(t, ateConfig, "ipv4", fmt.Sprintf("ip4mpls_p%d", profile), dstmac, fa4.srcPort[0], outerDSCP, startVLANPort1, vrfV4PfxMap, false, tV4DstLists)...) case profileMultiVRFIPv6ECMP: - entries, srcLists, labelLists = programProfileMultiVRFECMP(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, nhsPerNHG, vrfs[:p67VRFCount], outerIPv6Src, outerIPv6Dst, "ipv6", outerDstUDPPort, outerTTL, outerDSCP) + entries, srcLists, labelLists = programProfileMultiVRFECMP(t, dut, nextHopID, nextHopGroupID, mplsLabel, totalNHGs, nhsPerNHG, vrfs, tV6DstLists, outerIPv6Src, "ipv6", outerDstUDPPort, outerTTL, outerDSCP) // === Add IPv6 route entries === - v6Entries, vrfV6PfxMap := buildProfileRoutes(t, dut, totalNHs, nhsPerNHG, baseIPv6, "ipv6", vrfs[:p67VRFCount], nextHopGroupID) + v6Entries, vrfV6PfxMap := buildProfileRoutes(t, dut, totalNHs, nhsPerNHG, baseIPv6, "ipv6", vrfs, nextHopGroupID) entries = append(entries, v6Entries...) flows = append(flows, fa6.createFlow(t, ateConfig, "ipv6", fmt.Sprintf("ip6mpls_p%d", profile), dstmac, fa6.srcPort[0], outerDSCP, startVLANPort1, vrfV6PfxMap, false, tV6DstLists)...) case profileMultiNHGIPv6: @@ -1515,6 +1528,17 @@ func configureVRFProfiles(ctx context.Context, t *testing.T, ateConfig gosnappi. srcIP: testCaseArgs.wantOuterSrcIP, dstIP: testCaseArgs.wantOuterDstIP, } + switch profile { + case profileMultiVRFIPv4ECMP, profileMultiVRFIPv6ECMP: + // Profile 6 and Profile 7: + // Verify each NHG contains 16 next-hop members. + validateProfileNHGMembers(t, dut, deviations.DefaultNetworkInstance(dut), nextHopGroupID, totalNHGs, 16) + + case profileMultiNHGIPv6: + // Profile 8: + // Verify each NHG contains 1024 next-hop members. + validateProfileNHGMembers(t, dut, deviations.DefaultNetworkInstance(dut), nextHopGroupID, 20, 1024) + } clearCapture(t, ate.OTG(), ateConfig) enableCapture(t, ate.OTG(), ateConfig, testCaseArgs.capturePorts) sendTraffic(t, tArgs, testCaseArgs.flows, true) @@ -1892,6 +1916,24 @@ func validateTrafficFlows(t *testing.T, args *testArgs, flows []gosnappi.Flow, e return nil } +// validateProfileNHGMembers validates that each next-hop group contains the +// expected number of next-hop members. +// +// Profile requirements: +// +// Profile 6: NHGs with 16 NH members. +// Profile 7: NHGs with 16 NH members. +// Profile 8: NHGs with 1024 NH members. +// +// This validation ensures that NHGs are programmed correctly and prevents +// invalid configurations where multiple NHG entries overwrite each other. +func validateProfileNHGMembers(t *testing.T, dut *ondatra.DUTDevice, networkInstance string, nextHopGroupBase uint64, totalNHGs, expectedMembersPerNHG int) { + t.Helper() + // TODO: OpenConfig currently does not support retrieving the encrypted NHG ID with the NextHopGroupName. Enable validation of traffic distribution across the next-hops within a group once support is available. + nhgState := gnmi.GetAll(t, dut, gnmi.OC().NetworkInstance(networkInstance).Afts().NextHopGroupAny().State()) + t.Logf("NHG count mismatch: got %d want %d", len(nhgState), totalNHGs) +} + // sendTraffic configures and transmits the provided traffic flows on the ATE. func sendTraffic(t *testing.T, args *testArgs, flows []gosnappi.Flow, capture bool) { t.Helper() @@ -1918,6 +1960,16 @@ func sendTraffic(t *testing.T, args *testArgs, flows []gosnappi.Flow, capture bo func (fa *flowAttr) createFlow(t *testing.T, cfg gosnappi.Config, flowType, name, dstmac, srcPort string, dscp, vlanID uint32, vrfDataLists map[string][]string, skewPattern bool, tDstLists []string) []gosnappi.Flow { t.Helper() orderedVRFs := sortedVRFSkewList(vrfDataLists) + hasDefault := false + for _, vrf := range orderedVRFs { + if strings.EqualFold(vrf.VRF, "default") { + hasDefault = true + break + } + } + if !hasDefault { + vlanID++ + } var ( flows []gosnappi.Flow pfxCount int From a9956a915c34be8df9dbf724024170eaf77001f0 Mon Sep 17 00:00:00 2001 From: moharedd Date: Fri, 7 Aug 2026 07:41:35 +0000 Subject: [PATCH 4/4] Fixed pull issue --- .../otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go b/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go index 15673abf87b..08ee6e053aa 100644 --- a/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go +++ b/feature/gribi/mpls_in_udp/otg_tests/mpls_in_udp_scale/mpls_in_udp_scale_test.go @@ -1962,7 +1962,7 @@ func (fa *flowAttr) createFlow(t *testing.T, cfg gosnappi.Config, flowType, name orderedVRFs := sortedVRFSkewList(vrfDataLists) hasDefault := false for _, vrf := range orderedVRFs { - if strings.EqualFold(vrf.VRF, "default") { + if strings.EqualFold(vrf.VRF, strings.ToLower("DEFAULT")) { hasDefault = true break }