diff --git a/feature/gribi/otg_tests/ipv6_entry_test/README.md b/feature/gribi/otg_tests/ipv6_entry_test/README.md new file mode 100644 index 00000000000..2f92b6dd6e2 --- /dev/null +++ b/feature/gribi/otg_tests/ipv6_entry_test/README.md @@ -0,0 +1,130 @@ +# TE-2.3: gRIBI IPv6 Entry with Prefix Length > 64 + +## Summary + +Validate IPv6 route entry support in gRIBI, specifically focusing on IPv6 prefixes with prefix length strictly greater than 64 bits (such as /65, /96, /126, /127, and /128). This verifies that the device agent (DA) and underlying switch ASIC (e.g. TCAM / ALPM paired memory tables) correctly program and forward traffic for longer IPv6 prefixes without prefix bitmask truncation, incorrect table allocation, or route lookup failures. + +## Testbed type + +* [TESTBED_DUT_ATE_4LINKS](https://github.com/openconfig/featureprofiles/blob/main/topologies/atedut_4ports.testbed) + +## Procedure + +* Connect ATE port-1 to DUT port-1, ATE port-2 to DUT port-2, and ATE port-3 to DUT port-3. +* Establish gRIBI client connection with DUT, negotiating `RIB_AND_FIB_ACK` as the requested `ack_type` and persistence mode `PRESERVE`. Make it become leader. Flush all entries after each case. + +### Test Cases + +* Using gRIBI Modify RPC install the following IPv6Entry sets, and validate the specified behaviours: + * **Single next-hop with prefix length > 64**: + * Install `2001:db8:100::/65` to NextHopGroup containing one NextHop specified to ATE port-2. + * Forward packets from ATE port-1 destined to `2001:db8:100::/65` and verify 100% traffic arrives on ATE port-2 with 0% traffic loss. + * **Multiple next-hops (ECMP) with prefix length > 64**: + * Install `2001:db8:100::/65` to NextHopGroup containing two NextHop entries specified to ATE ports 2 and 3. + * Forward packets destined to `2001:db8:100::/65` and verify traffic is balanced across ATE ports 2 and 3. + * **Multiple next-hops with MAC override with prefix length > 64**: + * Install `2001:db8:100::/65` to NextHopGroup containing NextHops with destination MAC override, and verify traffic forwards successfully without packet loss. + * **Non-existent next-hop with prefix length > 64**: + * Send Modify() installing `2001:db8:100::/65` referencing next-hops that do not exist. Validate that FAILED error is received and traffic is dropped. + * **Downed next-hop interface with prefix length > 64**: + * Install `2001:db8:100::/65` to NextHopGroup with NextHops on port 2 and port 3. + * Set link state down on port 2. + * Forward packets and verify 100% traffic reroutes to port 3 without packet loss. + * **Longest Prefix Match (LPM) Discrimination (/64 vs /65)**: + * Install `2001:db8:200::/64` -> NextHop(ATE port-2). + * Install `2001:db8:200::/65` -> NextHop(ATE port-3). + * Send Flow 1 destined to `2001:db8:200::1/128` (matches `/64` and `/65`) -> verify it arrives on ATE port-3 due to LPM. + * Send Flow 2 destined to `2001:db8:200:0:8000::1/128` (matches `/64` but has bit 65 = 1, so outside `/65`) -> verify it arrives on ATE port-2. + * **Boundary Prefix Length Sweep (/65, /96, /126, /127, /128)**: + * Install and verify forwarding for boundary prefixes: + * `/65` (`2001:db8:101::/65`) + * `/96` (`2001:db8:102::/96`) + * `/126` (`2001:db8:103::/126`) + * `/127` (`2001:db8:104::/127`) + * `/128` (`2001:db8:105::1/128`) + * Verify zero packet loss for each prefix length to ensure hardware table allocation succeeds across all bit boundaries. + * **Route Deletion and Flush**: + * Verify route removal and FlushAll cleanly clears all IPv6 entries from FIB. + +## Config Parameter coverage + +N/A + +## Telemetry Parameter coverage + +N/A + +## Protocol/RPC Parameter coverage + +* gRIBI + * Modify() + * ModifyRequest: + * AFTOperation: + * id + * network_instance + * op + * Ipv6 + * Ipv6EntryKey: prefix + * Ipv6Entry: next_hop_group + * next_hop_group + * NextHopGroupKey: id + * NextHopGroup: next_hop + * next_hop + * NextHopKey: id + * NextHop: + * ip_address + * ModifyResponse: + * AFTResult: + * id + * status + +## OpenConfig Path and RPC Coverage +```yaml +paths: + /interfaces/interface/config/description: + /interfaces/interface/config/enabled: + /interfaces/interface/config/name: + /interfaces/interface/config/type: + /interfaces/interface/ethernet/config/port-speed: + /interfaces/interface/subinterfaces/subinterface/ipv4/addresses/address/config/ip: + /interfaces/interface/subinterfaces/subinterface/ipv4/addresses/address/config/prefix-length: + /interfaces/interface/subinterfaces/subinterface/ipv4/neighbors/neighbor/config/link-layer-address: + /interfaces/interface/subinterfaces/subinterface/ipv6/addresses/address/config/ip: + /interfaces/interface/subinterfaces/subinterface/ipv6/addresses/address/config/prefix-length: + /interfaces/interface/subinterfaces/subinterface/ipv6/neighbors/neighbor/config/link-layer-address: + /interfaces/interface/subinterfaces/subinterface/ipv6/config/enabled: + /network-instances/network-instance/interfaces/interface/config/id: + /network-instances/network-instance/interfaces/interface/config/interface: + /network-instances/network-instance/interfaces/interface/config/subinterface: + /network-instances/network-instance/protocols/protocol/config/identifier: + /network-instances/network-instance/protocols/protocol/config/name: + /network-instances/network-instance/protocols/protocol/static-routes/static/config/prefix: + /network-instances/network-instance/protocols/protocol/static-routes/static/next-hops/next-hop/config/index: + /network-instances/network-instance/protocols/protocol/static-routes/static/next-hops/next-hop/interface-ref/config/interface: +rpcs: + gnmi: + gNMI.Get: + gNMI.Set: + gNMI.Subscribe: + gribi: + gRIBI.Flush: + gRIBI.Get: + gRIBI.Modify: +``` + +## Canonical OC + +```json +{ + "network-instances": { + "network-instance": [ + { + "name": "DEFAULT", + "config": { + "name": "DEFAULT" + } + } + ] + } +} +``` diff --git a/feature/gribi/otg_tests/ipv6_entry_test/ipv6_entry_test.go b/feature/gribi/otg_tests/ipv6_entry_test/ipv6_entry_test.go new file mode 100644 index 00000000000..1a81a2b7527 --- /dev/null +++ b/feature/gribi/otg_tests/ipv6_entry_test/ipv6_entry_test.go @@ -0,0 +1,815 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipv6_entry_test + +import ( + "context" + "fmt" + "strconv" + "testing" + "time" + + "github.com/open-traffic-generator/snappi/gosnappi" + "github.com/openconfig/featureprofiles/internal/attrs" + "github.com/openconfig/featureprofiles/internal/deviations" + "github.com/openconfig/featureprofiles/internal/fptest" + "github.com/openconfig/featureprofiles/internal/gribi" + "github.com/openconfig/featureprofiles/internal/otgutils" + "github.com/openconfig/gribigo/chk" + "github.com/openconfig/gribigo/client" + "github.com/openconfig/gribigo/constants" + "github.com/openconfig/gribigo/fluent" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygot/ygot" +) + +const ( + // Next-hop group ID for dstPfx + nhgID = 42 + // Next-hop 1 ID for dutPort2 + nh1ID = 43 + // Next-hop 2 ID for dutPort3 + nh2ID = 44 + // Unconfigured next-hop ID + badNH = 45 + // A destination MAC address set by gRIBI. + staticDstMAC = "02:00:00:00:00:01" + ethernetCsmacd = oc.IETFInterfaces_InterfaceType_ethernetCsmacd + nh1IpAddr = "2001:db8:2::22" + nh2IpAddr = "2001:db8:3::42" +) + +const ( + // Destination prefix for DUT to ATE traffic (> 64 bits). + dstPfx = "2001:db8:100::/65" + dstPfxMin = "2001:db8:100::" + dstPfxMax = "2001:db8:100::ff" + dstPfxCount = 256 +) + +const ( + // Destination prefixes for LPM discrimination test. + dstIPv6Pfx64 = "2001:db8:200::/64" + dstIPv6Pfx65More = "2001:db8:200::/65" +) + +var ( + dutPort1 = attrs.Attributes{ + Desc: "DUT Port 1", + IPv4: "192.0.2.1", + IPv4Len: 30, + IPv6: "2001:db8:1::1", + IPv6Len: 64, + } + dutPort2 = attrs.Attributes{ + Desc: "DUT Port 2", + IPv4: "192.0.2.5", + IPv4Len: 30, + IPv6: "2001:db8:2::1", + IPv6Len: 64, + } + dutPort3 = attrs.Attributes{ + Desc: "DUT Port 3", + IPv4: "192.0.2.9", + IPv4Len: 30, + IPv6: "2001:db8:3::1", + IPv6Len: 64, + } + dutPort2DummyIP = attrs.Attributes{ + Desc: "DUT Port 2", + IPv6: "2001:db8:2::21", + IPv6Len: 64, + } + dutPort3DummyIP = attrs.Attributes{ + Desc: "DUT Port 3", + IPv6: "2001:db8:3::41", + IPv6Len: 64, + } + + atePort1 = attrs.Attributes{ + Name: "port1", + MAC: "02:00:01:01:01:01", + Desc: "ATE Port 1", + IPv4: "192.0.2.2", + IPv4Len: 30, + IPv6: "2001:db8:1::2", + IPv6Len: 64, + } + atePort2 = attrs.Attributes{ + Name: "port2", + MAC: "02:00:02:01:01:01", + Desc: "ATE Port 2", + IPv4: "192.0.2.6", + IPv4Len: 30, + IPv6: "2001:db8:2::2", + IPv6Len: 64, + } + atePort3 = attrs.Attributes{ + Name: "port3", + MAC: "02:00:03:01:01:01", + Desc: "ATE Port 3", + IPv4: "192.0.2.10", + IPv4Len: 30, + IPv6: "2001:db8:3::2", + IPv6Len: 64, + } +) + +func TestMain(m *testing.M) { + fptest.RunTests(m) +} + +func staticARPWithMagicUniversalIP(t *testing.T, dut *ondatra.DUTDevice) { + t.Helper() + dummyIPCIDR1 := nh1IpAddr + "/128" + dummyIPCIDR2 := nh2IpAddr + "/128" + p2 := dut.Port(t, "port2") + p3 := dut.Port(t, "port3") + s2 := &oc.NetworkInstance_Protocol_Static{ + Prefix: ygot.String(dummyIPCIDR1), + NextHop: map[string]*oc.NetworkInstance_Protocol_Static_NextHop{ + strconv.Itoa(nh1ID): { + Index: ygot.String(strconv.Itoa(nh1ID)), + InterfaceRef: &oc.NetworkInstance_Protocol_Static_NextHop_InterfaceRef{ + Interface: ygot.String(p2.Name()), + }, + }, + }, + } + s3 := &oc.NetworkInstance_Protocol_Static{ + Prefix: ygot.String(dummyIPCIDR2), + NextHop: map[string]*oc.NetworkInstance_Protocol_Static_NextHop{ + strconv.Itoa(nh2ID): { + Index: ygot.String(strconv.Itoa(nh2ID)), + InterfaceRef: &oc.NetworkInstance_Protocol_Static_NextHop_InterfaceRef{ + Interface: ygot.String(p3.Name()), + }, + }, + }, + } + static1 := &oc.NetworkInstance_Protocol{ + Identifier: oc.PolicyTypes_INSTALL_PROTOCOL_TYPE_STATIC, + Name: ygot.String(deviations.StaticProtocolName(dut)), + Static: map[string]*oc.NetworkInstance_Protocol_Static{ + dummyIPCIDR1: s2, + }, + } + static2 := &oc.NetworkInstance_Protocol{ + Identifier: oc.PolicyTypes_INSTALL_PROTOCOL_TYPE_STATIC, + Name: ygot.String(deviations.StaticProtocolName(dut)), + Static: map[string]*oc.NetworkInstance_Protocol_Static{ + dummyIPCIDR2: s3, + }, + } + fptest.ConfigureDefaultNetworkInstance(t, dut) + sp := gnmi.OC().NetworkInstance(deviations.DefaultNetworkInstance(dut)).Protocol(oc.PolicyTypes_INSTALL_PROTOCOL_TYPE_STATIC, deviations.StaticProtocolName(dut)) + gnmi.Update(t, dut, sp.Config(), static1) + gnmi.Update(t, dut, sp.Config(), static2) + gnmi.Update(t, dut, gnmi.OC().Interface(p2.Name()).Config(), configStaticArp(p2, nh1IpAddr, staticDstMAC)) + gnmi.Update(t, dut, gnmi.OC().Interface(p3.Name()).Config(), configStaticArp(p3, nh2IpAddr, staticDstMAC)) + + t.Cleanup(func() { + gnmi.Delete(t, dut, sp.Static(dummyIPCIDR1).Config()) + gnmi.Delete(t, dut, sp.Static(dummyIPCIDR2).Config()) + gnmi.Delete(t, dut, gnmi.OC().Interface(p2.Name()).Subinterface(0).Ipv6().Neighbor(nh1IpAddr).Config()) + gnmi.Delete(t, dut, gnmi.OC().Interface(p3.Name()).Subinterface(0).Ipv6().Neighbor(nh2IpAddr).Config()) + }) +} + +// TestIPv6Entry tests IPv6Entry forwarding with prefix length > 64. +func TestIPv6Entry(t *testing.T) { + ctx := context.Background() + // Configure ATE + ate := ondatra.ATE(t, "ate") + configureATE(t, ate) + + dut := ondatra.DUT(t, "dut") + configureDUT(t, dut) + + gribic := dut.RawAPIs().GRIBI(t) + + cases := []struct { + desc string + entries []fluent.GRIBIEntry + downPort *ondatra.Port + wantGoodFlows []string + wantBadFlows []string + wantOperationResults []*client.OpResult + gribiMACOverrideWithStaticARP bool + gribiMACOverrideWithStaticARPStaticRoute bool + }{ + { + desc: "Single next-hop", + entries: []fluent.GRIBIEntry{ + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh1ID).WithIPAddress(atePort2.IPv6), + fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithID(nhgID).AddNextHop(nh1ID, 1), + fluent.IPv6Entry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithPrefix(dstPfx).WithNextHopGroup(nhgID), + }, + wantGoodFlows: []string{"port2Flow"}, + wantBadFlows: []string{"port3Flow"}, + wantOperationResults: []*client.OpResult{ + fluent.OperationResult(). + WithNextHopOperation(nh1ID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithNextHopGroupOperation(nhgID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithIPv6Operation(dstPfx). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + }, + }, + { + desc: "Multiple next-hops", + entries: []fluent.GRIBIEntry{ + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh1ID).WithIPAddress(atePort2.IPv6), + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh2ID).WithIPAddress(atePort3.IPv6), + fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithID(nhgID). + AddNextHop(nh1ID, 1). + AddNextHop(nh2ID, 1), + fluent.IPv6Entry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithPrefix(dstPfx).WithNextHopGroup(nhgID), + }, + wantGoodFlows: []string{"ecmpFlow"}, + wantOperationResults: []*client.OpResult{ + fluent.OperationResult(). + WithNextHopOperation(nh1ID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithNextHopOperation(nh2ID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithNextHopGroupOperation(nhgID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithIPv6Operation(dstPfx). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + }, + }, + { + desc: "Multiple next-hops with MAC override", + entries: []fluent.GRIBIEntry{ + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh1ID).WithInterfaceRef(dut.Port(t, "port2").Name()).WithMacAddress(staticDstMAC), + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh2ID).WithInterfaceRef(dut.Port(t, "port3").Name()).WithMacAddress(staticDstMAC), + fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithID(nhgID). + AddNextHop(nh1ID, 1). + AddNextHop(nh2ID, 1), + fluent.IPv6Entry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithPrefix(dstPfx).WithNextHopGroup(nhgID), + }, + wantGoodFlows: []string{"ecmpFlow"}, + wantOperationResults: []*client.OpResult{ + fluent.OperationResult(). + WithNextHopOperation(nh1ID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithNextHopOperation(nh2ID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithNextHopGroupOperation(nhgID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithIPv6Operation(dstPfx). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + }, + gribiMACOverrideWithStaticARP: deviations.GRIBIMACOverrideWithStaticARP(dut), + gribiMACOverrideWithStaticARPStaticRoute: deviations.GRIBIMACOverrideStaticARPStaticRoute(dut), + }, + { + desc: "Nonexistant next-hop", + entries: []fluent.GRIBIEntry{ + fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithID(nhgID). + AddNextHop(badNH, 1), + fluent.IPv6Entry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithPrefix(dstPfx).WithNextHopGroup(nhgID), + }, + wantBadFlows: []string{"port2Flow", "port3Flow"}, + wantOperationResults: []*client.OpResult{ + fluent.OperationResult(). + WithNextHopGroupOperation(nhgID). + WithProgrammingResult(fluent.ProgrammingFailed). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithIPv6Operation(dstPfx). + WithProgrammingResult(fluent.ProgrammingFailed). + WithOperationType(constants.Add). + AsResult(), + }, + }, + { + desc: "Downed next-hop interface", + entries: []fluent.GRIBIEntry{ + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh1ID).WithIPAddress(atePort2.IPv6), + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh2ID).WithIPAddress(atePort3.IPv6), + fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithID(nhgID). + AddNextHop(nh1ID, 1). + AddNextHop(nh2ID, 1), + fluent.IPv6Entry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithPrefix(dstPfx).WithNextHopGroup(nhgID), + }, + downPort: dut.Port(t, "port2"), + wantGoodFlows: []string{"port3Flow"}, + wantBadFlows: []string{"port2Flow"}, + wantOperationResults: []*client.OpResult{ + fluent.OperationResult(). + WithNextHopOperation(nh1ID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithNextHopOperation(nh2ID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithNextHopGroupOperation(nhgID). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + fluent.OperationResult(). + WithIPv6Operation(dstPfx). + WithProgrammingResult(fluent.InstalledInFIB). + WithOperationType(constants.Add). + AsResult(), + }, + }, + } + + const ( + usePreserve = "PRESERVE" + ) + + // Each case will run with its own gRIBI fluent client. + for _, persist := range []string{usePreserve} { + t.Run(fmt.Sprintf("Persistence=%s", persist), func(t *testing.T) { + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + newGoodFlows, newBadFlows := createTrafficFlows(t, ate, tc.wantGoodFlows, tc.wantBadFlows) + if tc.gribiMACOverrideWithStaticARPStaticRoute { + staticARPWithMagicUniversalIP(t, dut) + } else if tc.gribiMACOverrideWithStaticARP { + // Creating a Static ARP entry for staticDstMAC + d := gnmi.OC() + p2 := dut.Port(t, "port2") + p3 := dut.Port(t, "port3") + gnmi.Update(t, dut, d.Interface(p2.Name()).Config(), dutPort2DummyIP.NewOCInterface(p2.Name(), dut)) + gnmi.Update(t, dut, d.Interface(p3.Name()).Config(), dutPort3DummyIP.NewOCInterface(p3.Name(), dut)) + gnmi.Update(t, dut, d.Interface(p2.Name()).Config(), configStaticArp(p2, nh1IpAddr, staticDstMAC)) + gnmi.Update(t, dut, d.Interface(p3.Name()).Config(), configStaticArp(p3, nh2IpAddr, staticDstMAC)) + } + if tc.gribiMACOverrideWithStaticARP || tc.gribiMACOverrideWithStaticARPStaticRoute { + // Programming a gRIBI flow with above IP/mac-address as the next-hop entry + tc.entries = []fluent.GRIBIEntry{ + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh1ID).WithInterfaceRef(dut.Port(t, "port2").Name()).WithIPAddress(nh1IpAddr).WithMacAddress(staticDstMAC), + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh2ID).WithInterfaceRef(dut.Port(t, "port3").Name()).WithIPAddress(nh2IpAddr).WithMacAddress(staticDstMAC), + fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithID(nhgID).AddNextHop(nh1ID, 1).AddNextHop(nh2ID, 1), + fluent.IPv6Entry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithPrefix(dstPfx).WithNextHopGroup(nhgID), + } + } + // Configure the gRIBI client. + c := fluent.NewClient() + conn := c.Connection(). + WithStub(gribic). + WithRedundancyMode(fluent.ElectedPrimaryClient). + WithInitialElectionID(1 /* low */, 0 /* hi */) // ID must be > 0. + if persist == usePreserve { + conn.WithPersistence() + } + + if !deviations.GRIBIRIBAckOnly(dut) { + // The main difference WithFIBACK() made was that we are now expecting + // fluent.InstalledInFIB in []*client.OpResult, as opposed to + // fluent.InstalledInRIB. + conn.WithFIBACK() + } + + c.Start(ctx, t) + defer c.Stop(t) + c.StartSending(ctx, t) + if err := awaitTimeout(ctx, c, t, time.Minute); err != nil { + t.Fatalf("Await got error during session negotiation: %v", err) + } + gribi.BecomeLeader(t, c) + + if persist == usePreserve { + defer func() { + if err := gribi.FlushAll(c); err != nil { + t.Errorf("Cannot flush: %v", err) + } + }() + } + + if tc.downPort != nil { + if deviations.ATEPortLinkStateOperationsUnsupported(ate) { + // Setting admin state down on the DUT interface. + // Setting the OTG interface down has no effect in KNE environments. + setDUTInterfaceWithState(t, dut, &dutPort2, tc.downPort, false) + defer setDUTInterfaceWithState(t, dut, &dutPort2, tc.downPort, true) + } else { + portStateAction := gosnappi.NewControlState() + linkState := portStateAction.Port().Link().SetPortNames([]string{tc.downPort.ID()}).SetState(gosnappi.StatePortLinkState.DOWN) + ate.OTG().SetControlState(t, portStateAction) + // Restore port state at end of test case. + linkState.SetState(gosnappi.StatePortLinkState.UP) + defer ate.OTG().SetControlState(t, portStateAction) + } + } + + c.Modify().AddEntry(t, tc.entries...) + if err := awaitTimeout(ctx, c, t, time.Minute); err != nil { + t.Fatalf("Await got error for entries: %v", err) + } + defer func() { + // Delete should reverse the order of entries, i.e. IPv6Entry must be removed + // before NextHopGroupEntry, which must be removed before NextHopEntry. + var revEntries []fluent.GRIBIEntry + for i := len(tc.entries) - 1; i >= 0; i-- { + revEntries = append(revEntries, tc.entries[i]) + } + c.Modify().DeleteEntry(t, revEntries...) + if err := awaitTimeout(ctx, c, t, time.Minute); err != nil { + t.Fatalf("Await got error for entries: %v", err) + } + p2 := dut.Port(t, "port2") + p3 := dut.Port(t, "port3") + if tc.gribiMACOverrideWithStaticARP { + gnmi.Delete(t, dut, gnmi.OC().Interface(p2.Name()).Subinterface(0).Ipv6().Neighbor(nh1IpAddr).Config()) + gnmi.Delete(t, dut, gnmi.OC().Interface(p3.Name()).Subinterface(0).Ipv6().Neighbor(nh2IpAddr).Config()) + gnmi.Replace(t, dut, gnmi.OC().Interface(p2.Name()).Config(), dutPort2.NewOCInterface(p2.Name(), dut)) + gnmi.Replace(t, dut, gnmi.OC().Interface(p3.Name()).Config(), dutPort3.NewOCInterface(p3.Name(), dut)) + } + }() + + for _, wantResult := range tc.wantOperationResults { + chk.HasResult(t, c.Results(t), wantResult, chk.IgnoreOperationID()) + } + validateTrafficFlows(t, ate, newGoodFlows, newBadFlows) + if tc.desc == "Multiple next-hops" { + p2In := gnmi.Get(t, ate.OTG(), gnmi.OTG().Port(atePort2.Name).Counters().InFrames().State()) + p3In := gnmi.Get(t, ate.OTG(), gnmi.OTG().Port(atePort3.Name).Counters().InFrames().State()) + if p2In == 0 || p3In == 0 { + t.Errorf("ECMP balancing failed: traffic not received on both ports: port2=%d, port3=%d", p2In, p3In) + } + } + }) + } + }) + } + + // Subtest: LPM discrimination (/64 vs /65) + t.Run("LPM_Discrimination_64_vs_65", func(t *testing.T) { + c := fluent.NewClient() + conn := c.Connection(). + WithStub(gribic). + WithPersistence(). + WithInitialElectionID(1, 0). + WithRedundancyMode(fluent.ElectedPrimaryClient) + if !deviations.GRIBIRIBAckOnly(dut) { + conn.WithFIBACK() + } + c.Start(ctx, t) + defer c.Stop(t) + c.StartSending(ctx, t) + if err := awaitTimeout(ctx, c, t, time.Minute); err != nil { + t.Fatalf("Await got error during session negotiation: %v", err) + } + gribi.BecomeLeader(t, c) + defer func() { + if err := gribi.FlushAll(c); err != nil { + t.Errorf("Cannot flush: %v", err) + } + }() + if err := gribi.FlushAll(c); err != nil { + t.Fatalf("Cannot flush: %v", err) + } + + c.Modify().AddEntry(t, + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh1ID).WithIPAddress(atePort2.IPv6), + fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithID(nhgID).AddNextHop(nh1ID, 1), + fluent.IPv6Entry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithPrefix(dstIPv6Pfx64).WithNextHopGroup(nhgID), + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh2ID).WithIPAddress(atePort3.IPv6), + fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithID(nhgID+1).AddNextHop(nh2ID, 1), + fluent.IPv6Entry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithPrefix(dstIPv6Pfx65More).WithNextHopGroup(nhgID+1), + ) + if err := awaitTimeout(ctx, c, t, time.Minute); err != nil { + t.Fatalf("Could not program LPM entries: %v", err) + } + + otg := ate.OTG() + ateTop := otg.GetConfig(t) + ateTop.Flows().Clear().Items() + // Flow 1: destined to more specific /65 -> should arrive on Port 3, not Port 2 + f1Good := createFlowWithPrefix(t, "LPM_MoreSpecific_Port3", ate, ateTop, "2001:db8:200::1", 256, &atePort3) + f1Bad := createFlowWithPrefix(t, "LPM_MoreSpecific_Drop_Port2", ate, ateTop, "2001:db8:200::1", 256, &atePort2) + // Flow 2: destined to /64 outside /65 -> should arrive on Port 2, not Port 3 + f2Good := createFlowWithPrefix(t, "LPM_Coarser_Port2", ate, ateTop, "2001:db8:200:0:8000::1", 256, &atePort2) + f2Bad := createFlowWithPrefix(t, "LPM_Coarser_Drop_Port3", ate, ateTop, "2001:db8:200:0:8000::1", 256, &atePort3) + otg.PushConfig(t, ateTop) + otg.StartProtocols(t) + + validateTrafficFlows(t, ate, []string{f1Good, f2Good}, []string{f1Bad, f2Bad}) + }) + + // Subtest: Boundary prefix length sweep (/65, /96, /126, /127, /128) + t.Run("Boundary_Prefix_Length_Sweep", func(t *testing.T) { + sweepPrefixes := []struct { + name string + prefix string + flowIP string + }{ + {"Prefix_65", "2001:db8:101::/65", "2001:db8:101::1"}, + {"Prefix_96", "2001:db8:102::/96", "2001:db8:102::1"}, + {"Prefix_126", "2001:db8:103::/126", "2001:db8:103::1"}, + {"Prefix_127", "2001:db8:104::/127", "2001:db8:104::1"}, + {"Prefix_128", "2001:db8:105::1/128", "2001:db8:105::1"}, + } + + c := fluent.NewClient() + conn := c.Connection(). + WithStub(gribic). + WithPersistence(). + WithInitialElectionID(1, 0). + WithRedundancyMode(fluent.ElectedPrimaryClient) + if !deviations.GRIBIRIBAckOnly(dut) { + conn.WithFIBACK() + } + c.Start(ctx, t) + defer c.Stop(t) + c.StartSending(ctx, t) + if err := awaitTimeout(ctx, c, t, time.Minute); err != nil { + t.Fatalf("Await got error during session negotiation: %v", err) + } + gribi.BecomeLeader(t, c) + defer func() { + if err := gribi.FlushAll(c); err != nil { + t.Errorf("Cannot flush: %v", err) + } + }() + + for _, sp := range sweepPrefixes { + t.Run(sp.name, func(t *testing.T) { + if err := gribi.FlushAll(c); err != nil { + t.Fatalf("Cannot flush: %v", err) + } + c.Modify().AddEntry(t, + fluent.NextHopEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithIndex(nh1ID).WithIPAddress(atePort2.IPv6), + fluent.NextHopGroupEntry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithID(nhgID).AddNextHop(nh1ID, 1), + fluent.IPv6Entry().WithNetworkInstance(deviations.DefaultNetworkInstance(dut)). + WithPrefix(sp.prefix).WithNextHopGroup(nhgID), + ) + if err := awaitTimeout(ctx, c, t, time.Minute); err != nil { + t.Fatalf("Could not program prefix %s: %v", sp.prefix, err) + } + + otg := ate.OTG() + ateTop := otg.GetConfig(t) + ateTop.Flows().Clear().Items() + flowName := createFlowWithPrefix(t, "Flow_"+sp.name, ate, ateTop, sp.flowIP, 1, &atePort2) + otg.PushConfig(t, ateTop) + otg.StartProtocols(t) + + validateTrafficFlows(t, ate, []string{flowName}, nil) + }) + } + }) +} + +// configureDUT configures port1-3 on the DUT. +func configureDUT(t *testing.T, dut *ondatra.DUTDevice) { + d := gnmi.OC() + + p1 := dut.Port(t, "port1") + p2 := dut.Port(t, "port2") + p3 := dut.Port(t, "port3") + + gnmi.Replace(t, dut, d.Interface(p1.Name()).Config(), dutPort1.NewOCInterface(p1.Name(), dut)) + gnmi.Replace(t, dut, d.Interface(p2.Name()).Config(), dutPort2.NewOCInterface(p2.Name(), dut)) + gnmi.Replace(t, dut, d.Interface(p3.Name()).Config(), dutPort3.NewOCInterface(p3.Name(), dut)) + if deviations.ExplicitIPv6EnableForGRIBI(dut) { + gnmi.Update(t, dut, d.Interface(p2.Name()).Subinterface(0).Ipv6().Enabled().Config(), bool(true)) + gnmi.Update(t, dut, d.Interface(p3.Name()).Subinterface(0).Ipv6().Enabled().Config(), bool(true)) + } + + if deviations.ExplicitPortSpeed(dut) { + fptest.SetPortSpeed(t, p1) + fptest.SetPortSpeed(t, p2) + fptest.SetPortSpeed(t, p3) + } + if deviations.ExplicitInterfaceInDefaultVRF(dut) { + fptest.AssignToNetworkInstance(t, dut, p1.Name(), deviations.DefaultNetworkInstance(dut), 0) + fptest.AssignToNetworkInstance(t, dut, p2.Name(), deviations.DefaultNetworkInstance(dut), 0) + fptest.AssignToNetworkInstance(t, dut, p3.Name(), deviations.DefaultNetworkInstance(dut), 0) + } +} + +// configureATE configures port1-3 on the ATE. +func configureATE(t *testing.T, ate *ondatra.ATEDevice) gosnappi.Config { + top := gosnappi.NewConfig() + + p1 := ate.Port(t, "port1") + p2 := ate.Port(t, "port2") + p3 := ate.Port(t, "port3") + + atePort1.AddToOTG(top, p1, &dutPort1) + atePort2.AddToOTG(top, p2, &dutPort2) + atePort3.AddToOTG(top, p3, &dutPort3) + + ate.OTG().PushConfig(t, top) + ate.OTG().StartProtocols(t) + + return top +} + +// createFlow returns a flow from atePort1 to the dstPfx, expected to arrive on ATE interface dsts. +func createFlow(t *testing.T, name string, ate *ondatra.ATEDevice, ateTop gosnappi.Config, dsts ...*attrs.Attributes) string { + return createFlowWithPrefix(t, name, ate, ateTop, dstPfxMin, dstPfxCount, dsts...) +} + +func createFlowWithPrefix(t *testing.T, name string, ate *ondatra.ATEDevice, ateTop gosnappi.Config, startIP string, dstIPCount uint32, dsts ...*attrs.Attributes) string { + var rxEndpoints []string + for _, dst := range dsts { + rxEndpoints = append(rxEndpoints, dst.Name+".IPv6") + } + flowipv6 := ateTop.Flows().Add().SetName(name) + flowipv6.Metrics().SetEnable(true) + e1 := flowipv6.Packet().Add().Ethernet() + e1.Src().SetValue(atePort1.MAC) + flowipv6.TxRx().Device().SetTxNames([]string{atePort1.Name + ".IPv6"}).SetRxNames(rxEndpoints) + v6 := flowipv6.Packet().Add().Ipv6() + v6.Src().SetValue(atePort1.IPv6) + if dstIPCount > 1 { + v6.Dst().Increment().SetStart(startIP).SetCount(dstIPCount) + } else { + v6.Dst().SetValue(startIP) + } + return name +} + +func createTrafficFlows(t *testing.T, ate *ondatra.ATEDevice, good, bad []string) (newGood, newBad []string) { + var newGoodFlows, newBadFlows []string + allFlows := append(good, bad...) + otg := ate.OTG() + ateTop := otg.GetConfig(t) + if len(good) == 0 && len(bad) == 0 { + otg.PushConfig(t, ateTop) + otg.StartProtocols(t) + return newGoodFlows, newBadFlows + } + ateTop.Flows().Clear().Items() + for _, flow := range allFlows { + if flow == "port2Flow" { + if elementInSlice(flow, good) { + newGoodFlows = append(newGoodFlows, createFlow(t, "Port1_to_Port2", ate, ateTop, &atePort2)) + } + if elementInSlice(flow, bad) { + newBadFlows = append(newBadFlows, createFlow(t, "Port1_to_Port2", ate, ateTop, &atePort2)) + } + } + if flow == "port3Flow" { + if elementInSlice(flow, good) { + newGoodFlows = append(newGoodFlows, createFlow(t, "Port1_to_Port3", ate, ateTop, &atePort3)) + } + if elementInSlice(flow, bad) { + newBadFlows = append(newBadFlows, createFlow(t, "Port1_to_Port3", ate, ateTop, &atePort3)) + } + + } + if flow == "ecmpFlow" { + if elementInSlice(flow, good) { + newGoodFlows = append(newGoodFlows, createFlow(t, "ecmpFlow", ate, ateTop, &atePort2, &atePort3)) + } + if elementInSlice(flow, bad) { + newBadFlows = append(newBadFlows, createFlow(t, "ecmpFlow", ate, ateTop, &atePort2, &atePort3)) + } + + } + } + otg.PushConfig(t, ateTop) + otg.StartProtocols(t) + return newGoodFlows, newBadFlows +} + +func validateTrafficFlows(t *testing.T, ate *ondatra.ATEDevice, good, bad []string) { + if len(good) == 0 && len(bad) == 0 { + return + } + + newGoodFlows := good + newBadFlows := bad + + ateTop := ate.OTG().GetConfig(t) + + ate.OTG().StartTraffic(t) + time.Sleep(15 * time.Second) + ate.OTG().StopTraffic(t) + + otgutils.LogFlowMetrics(t, ate.OTG(), ateTop) + otgutils.LogPortMetrics(t, ate.OTG(), ateTop) + + for _, flow := range newGoodFlows { + otgutils.ExpectedTrafficLoss(t, ate.OTG(), flow, 0, 0) + } + + for _, flow := range newBadFlows { + otgutils.ExpectedTrafficLoss(t, ate.OTG(), flow, 100, 100) + } +} + +// awaitTimeout calls a fluent client Await, adding a timeout to the context. +func awaitTimeout(ctx context.Context, c *fluent.GRIBIClient, t testing.TB, timeout time.Duration) error { + subctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + return c.Await(subctx, t) +} + +func configStaticArp(p *ondatra.Port, ipv6addr string, macAddr string) *oc.Interface { + i := &oc.Interface{Name: ygot.String(p.Name())} + i.Type = oc.IETFInterfaces_InterfaceType_ethernetCsmacd + s := i.GetOrCreateSubinterface(0) + s6 := s.GetOrCreateIpv6() + n6 := s6.GetOrCreateNeighbor(ipv6addr) + n6.LinkLayerAddress = ygot.String(macAddr) + return i +} + +// setDUTInterfaceWithState sets the admin state on the dut interface +func setDUTInterfaceWithState(t testing.TB, dut *ondatra.DUTDevice, dutPort *attrs.Attributes, p *ondatra.Port, state bool) { + dc := gnmi.OC() + i := &oc.Interface{} + i.Enabled = ygot.Bool(state) + i.Type = ethernetCsmacd + i.Name = ygot.String(p.Name()) + gnmi.Update(t, dut, dc.Interface(p.Name()).Config(), i) +} + +func elementInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} diff --git a/feature/gribi/otg_tests/ipv6_entry_test/metadata.textproto b/feature/gribi/otg_tests/ipv6_entry_test/metadata.textproto new file mode 100644 index 00000000000..ec5691c0b81 --- /dev/null +++ b/feature/gribi/otg_tests/ipv6_entry_test/metadata.textproto @@ -0,0 +1,42 @@ +# proto-file: github.com/openconfig/featureprofiles/proto/metadata.proto +# proto-message: Metadata + +uuid: "b2184e4f-2114-48f1-9c8e-5b62e49c7161" +plan_id: "TE-2.3" +description: "gRIBI IPv6 Entry with Prefix Length > 64" +testbed: TESTBED_DUT_ATE_4LINKS +platform_exceptions: { + platform: { + vendor: CISCO + } + deviations: { + gribi_mac_override_static_arp_static_route: true + } +} +platform_exceptions: { + platform: { + vendor: JUNIPER + } + deviations: { + ipv6_enable_for_gribi_nh_dmac: true + } +} +platform_exceptions: { + platform: { + vendor: NOKIA + } + deviations: { + explicit_interface_in_default_vrf: true + static_protocol_name: "static" + } +} +platform_exceptions: { + platform: { + vendor: ARISTA + } + deviations: { + static_protocol_name: "STATIC" + gribi_mac_override_static_arp_static_route: true + default_network_instance: "default" + } +} diff --git a/testregistry.textproto b/testregistry.textproto index c2bf1c7b27d..a9c2eee7f26 100644 --- a/testregistry.textproto +++ b/testregistry.textproto @@ -1649,6 +1649,12 @@ test: { readme: "https://github.com/openconfig/featureprofiles/blob/main/feature/gribi/otg_tests/ipv4_entry_with_aggregate_ports_test/README.md" exec: "https://github.com/openconfig/featureprofiles/blob/main/feature/gribi/otg_tests/ipv4_entry_with_aggregate_ports_test/ipv4_entry_with_aggregate_ports_test.go" } +test: { + id: "TE-2.3" + description: "gRIBI IPv6 Entry with Prefix Length > 64" + readme: "https://github.com/openconfig/featureprofiles/blob/main/feature/gribi/otg_tests/ipv6_entry_test/README.md" + exec: "https://github.com/openconfig/featureprofiles/blob/main/feature/gribi/otg_tests/ipv6_entry_test/ipv6_entry_test.go" +} test: { id: "TE-3.1" description: "Base Hierarchical Route Installation"