From 81b9d0837acd8a61c556b2d277f0e88ed84d24de Mon Sep 17 00:00:00 2001 From: "Dipesh Chauhan (Nokia)" Date: Tue, 21 Jan 2025 20:26:56 -0500 Subject: [PATCH] DSCP Transparency with ECN "This code is a Contribution to the OpenConfig Feature Profiles project ("Work") made under the Google Software Grant and Corporate Contributor License Agreement ("CLA") and governed by the Apache License 2.0. No other rights or licenses in or to any of Nokia's intellectual property are granted for any other purpose. This code is provided on an "as is" basis without any warranties of any kind." --- .../README.md | 103 ++- .../dscp_transparency_test.go | 860 ++++++++++++++++++ .../dscp_transparency_test/metadata.textproto | 18 + testregistry.textproto | 10 +- 4 files changed, 941 insertions(+), 50 deletions(-) rename feature/qos/ecn/otg_tests/{DSCP_transparency => dscp_transparency_test}/README.md (51%) create mode 100644 feature/qos/ecn/otg_tests/dscp_transparency_test/dscp_transparency_test.go create mode 100644 feature/qos/ecn/otg_tests/dscp_transparency_test/metadata.textproto diff --git a/feature/qos/ecn/otg_tests/DSCP_transparency/README.md b/feature/qos/ecn/otg_tests/dscp_transparency_test/README.md similarity index 51% rename from feature/qos/ecn/otg_tests/DSCP_transparency/README.md rename to feature/qos/ecn/otg_tests/dscp_transparency_test/README.md index 6eec6f86e7a..4c12cf018f9 100644 --- a/feature/qos/ecn/otg_tests/DSCP_transparency/README.md +++ b/feature/qos/ecn/otg_tests/dscp_transparency_test/README.md @@ -1,5 +1,4 @@ - -# DP-1.14: DSCP transperency with ECN +# DP-1.17: DSCP Transparency with ECN ## Summary @@ -28,10 +27,10 @@ This test evaluates if all 64 combination of DSCP bits are transparently handled * 7 queues and 7 corresponding forwarding group * Scheduler policy with * one scheduler of STRICT priority type serving NC1 queue - * one scheduler of WRR type serving 6 queues AF4, AF3, AF2, AF1, BE0, BE1 with equal weights 10:10:10:10:10:10 respectivly + * one scheduler of WRR type serving 6 queues AF4, AF3, AF2, AF1, BE0, BE1 with equal weights 10:10:10:10:10:10 respectively * queue-management profile of WRED type with: - * min-treshold: 80KB - * max-treshold: 3MB + * min-threshold: 80KB + * max-threshold: 3MB * max-drop-percentage: 100 * ecn: enabled * attach queue-management profile to queues NC1, AF4, AF3, AF2, AF1, BE0, BE1; @@ -48,8 +47,9 @@ This test evaluates if all 64 combination of DSCP bits are transparently handled * Verify using DUTPort3 telemetry that: * no drops are seen in any of queues on DUTPort3 * all queues reports non-zero transmit packets, octets. -* Verify on ATEPort3 that all flows are recived w/o DSCP modification -all 64 values are observed -* verify on ATEPort3 that all recived packet has ECT(0) ECN value +* Verify on ATEPort3 that all flows are received w/o DSCP modification -all 64 values are observed +* verify on ATEPort3 that all received packet has ECT(0) ECN value + ### Sub Test #2 - Congestion * Generate 64 flows of traffic form ATEPort1 and 64 flows of traffic form ATEPort2 toward ATEPort3 * each flow form ATEPort1 has distinct DSCP value @@ -64,10 +64,11 @@ This test evaluates if all 64 combination of DSCP bits are transparently handled * Verify using DUTPort3 telemetry that: * Drops are seen in all queues except NC1 on DUTPort3 * all queues reports non-zero transmit packets, octets. -* Verify on ATEPort3 that all flows are recived w/o DSCP modification - all 64 values are observed +* Verify on ATEPort3 that all flows are received w/o DSCP modification - all 64 values are observed * verify on ATEPort3 that: - * all recived packets with DSCP 48-63 has ECT(0) value + * all received packets with DSCP 48-63 has ECT(0) value * vast majority (almost all) packets with DSCP 0-47 has CE ECN value. + ### Sub Test #3 - NC1 congestion * Generate 16 flows of traffic form ATEPort1 and 16 flows of traffic form ATEPort2 toward ATEPort3 * each flow form ATEPort1 has distinct DSCP value from 48-63 range @@ -81,47 +82,53 @@ This test evaluates if all 64 combination of DSCP bits are transparently handled * wait 1 minutes; stop traffic generation. * Verify using DUTPort3 telemetry that: * Drops are seen in NC1 queue on DUTPort3 - * all queues but NC1 reports nzero transmit packets, octets. + * all queues but NC1 reports zero transmit packets, octets. * NC1 queue reports non-zero transmit packets, octets. -* Verify on ATEPort3 that all flows are recived w/o DSCP modification - all 16 values are observed. +* Verify on ATEPort3 that all flows are received w/o DSCP modification - all 16 values are observed. * verify on ATEPort3 that: - * all recived packets with DSCP has CE value - -## Config Parameter Coverage - - * qos/classifiers/classifier/terms/term/conditions/ipv4/config/dscp-set - * qos/classifiers/classifier/terms/term/conditions/ipv6/config/dscp-set - * qos/classifiers/classifier/terms/term/actions/config/target-group - * qos/queues/queue/config/name - * qos/forwarding-groups/forwarding-group/config/name - * qos/forwarding-groups/forwarding-group/config/output-queue - * qos/scheduler-policies/scheduler-policy/schedulers/scheduler/config/priority - * qos/scheduler-policies/scheduler-policy/schedulers/scheduler/config/sequence - * qos/scheduler-policies/scheduler-policy/schedulers/scheduler/inputs/input/config/id - * qos/scheduler-policies/scheduler-policy/schedulers/scheduler/inputs/input/config/input-type - * qos/scheduler-policies/scheduler-policy/schedulers/scheduler/inputs/input/config/queue - * qos/scheduler-policies/scheduler-policy/schedulers/scheduler/inputs/input/config/weight - * qos/queue-management-profiles/queue-management-profile/wred/uniform/config/enable-ecn - * qos/queue-management-profiles/queue-management-profile/wred/uniform/config/max-drop-probability-percent - * qos/queue-management-profiles/queue-management-profile/wred/uniform/config/max-threshold - * qos/queue-management-profiles/queue-management-profile/wred/uniform/config/min-threshold - * qos/interfaces/interface/output/queues/queue/config/name - * qos/interfaces/interface/output/queues/queue/config/queue-management-profile - * qos/interfaces/interface/output/scheduler-policy/config/name - * qos/interfaces/interface/input/classifiers/classifier/config/name - * qos/interfaces/interface/input/classifiers/classifier/config/type - -## Telemetry Parameter Coverage - - * qos/interfaces/interface/output/queues/queue/state/dropped-octets - * qos/interfaces/interface/output/queues/queue/state/dropped-pkts - * qos/interfaces/interface/output/queues/queue/state/name - * qos/interfaces/interface/output/queues/queue/state/transmit-octets - * qos/interfaces/interface/output/queues/queue/state/transmit-pkts - -## Protocol/RPC Parameter Coverage - - * NONE. + * all received packets with DSCP has CE value + +## OpenConfig Path and RPC Coverage + +The below yaml defines the OC paths intended to be covered by this test. OC paths used for test setup are not listed here. + +```yaml +paths: + ## Config Paths ## + /qos/classifiers/classifier/terms/term/conditions/ipv4/config/dscp-set: + /qos/classifiers/classifier/terms/term/conditions/ipv6/config/dscp-set: + /qos/classifiers/classifier/terms/term/actions/config/target-group: + /qos/queues/queue/config/name: + /qos/forwarding-groups/forwarding-group/config/name: + /qos/forwarding-groups/forwarding-group/config/output-queue: + /qos/scheduler-policies/scheduler-policy/schedulers/scheduler/config/priority: + /qos/scheduler-policies/scheduler-policy/schedulers/scheduler/config/sequence: + /qos/scheduler-policies/scheduler-policy/schedulers/scheduler/inputs/input/config/id: + /qos/scheduler-policies/scheduler-policy/schedulers/scheduler/inputs/input/config/input-type: + /qos/scheduler-policies/scheduler-policy/schedulers/scheduler/inputs/input/config/queue: + /qos/scheduler-policies/scheduler-policy/schedulers/scheduler/inputs/input/config/weight: + /qos/queue-management-profiles/queue-management-profile/wred/uniform/config/enable-ecn: + /qos/queue-management-profiles/queue-management-profile/wred/uniform/config/max-drop-probability-percent: + /qos/queue-management-profiles/queue-management-profile/wred/uniform/config/max-threshold: + /qos/queue-management-profiles/queue-management-profile/wred/uniform/config/min-threshold: + /qos/interfaces/interface/output/queues/queue/config/name: + /qos/interfaces/interface/output/queues/queue/config/queue-management-profile: + /qos/interfaces/interface/output/scheduler-policy/config/name: + /qos/interfaces/interface/input/classifiers/classifier/config/name: + /qos/interfaces/interface/input/classifiers/classifier/config/type: + + ## State Paths ## + /qos/interfaces/interface/output/queues/queue/state/dropped-octets: + /qos/interfaces/interface/output/queues/queue/state/dropped-pkts: + /qos/interfaces/interface/output/queues/queue/state/name: + /qos/interfaces/interface/output/queues/queue/state/transmit-octets: + /qos/interfaces/interface/output/queues/queue/state/transmit-pkts: + +rpcs: + gnmi: + gNMI.Set: + gNMI.Subscribe: +``` ## Required DUT platform diff --git a/feature/qos/ecn/otg_tests/dscp_transparency_test/dscp_transparency_test.go b/feature/qos/ecn/otg_tests/dscp_transparency_test/dscp_transparency_test.go new file mode 100644 index 00000000000..921451ad2bb --- /dev/null +++ b/feature/qos/ecn/otg_tests/dscp_transparency_test/dscp_transparency_test.go @@ -0,0 +1,860 @@ +// Copyright 2025 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 dscp_transparency_test + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "testing" + "time" + + "github.com/open-traffic-generator/snappi/gosnappi" + "github.com/openconfig/entity-naming/entname" + "github.com/openconfig/featureprofiles/internal/attrs" + "github.com/openconfig/featureprofiles/internal/deviations" + "github.com/openconfig/featureprofiles/internal/fptest" + "github.com/openconfig/featureprofiles/internal/otgutils" + "github.com/openconfig/featureprofiles/internal/qoscfg" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygot/ygot" +) + +func TestMain(m *testing.M) { + fptest.RunTests(m) +} + +const ( + ipv4 = "IPv4" + ipv6 = "IPv6" + ipv4PrefixLen = 30 + ipv6PrefixLen = 126 + subInterfaceIndex = 0 + flowFrameSize uint32 = 1_000 + trafficRunDuration = 1 * time.Minute + trafficStopWaitDuration = 30 * time.Second + dutEgressPort = "port1" +) + +var ( + dutPort1 = &attrs.Attributes{ + Name: "dutPort1", + IPv4: "192.0.2.1", + IPv6: "2001:db8::1", + IPv4Len: ipv4PrefixLen, + IPv6Len: ipv6PrefixLen, + } + + dutPort2 = &attrs.Attributes{ + Name: "dutPort2", + IPv4: "192.0.2.5", + IPv6: "2001:db8::5", + IPv4Len: ipv4PrefixLen, + IPv6Len: ipv6PrefixLen, + } + + dutPort3 = &attrs.Attributes{ + Name: "dutPort3", + IPv4: "192.0.2.9", + IPv6: "2001:db8::9", + IPv4Len: ipv4PrefixLen, + IPv6Len: ipv6PrefixLen, + } + + atePort1 = &attrs.Attributes{ + Name: "atePort1", + MAC: "02:00:01:01:01:01", + IPv4: "192.0.2.2", + IPv6: "2001:db8::2", + IPv4Len: ipv4PrefixLen, + IPv6Len: ipv6PrefixLen, + } + + atePort2 = &attrs.Attributes{ + Name: "atePort2", + MAC: "02:00:02:01:01:01", + IPv4: "192.0.2.6", + IPv6: "2001:db8::6", + IPv4Len: ipv4PrefixLen, + IPv6Len: ipv6PrefixLen, + } + + atePort3 = &attrs.Attributes{ + Name: "atePort3", + MAC: "02:00:03:01:01:01", + IPv4: "192.0.2.10", + IPv6: "2001:db8::a", + IPv4Len: ipv4PrefixLen, + IPv6Len: ipv6PrefixLen, + } + + dutPorts = map[string]*attrs.Attributes{ + "port1": dutPort1, + "port2": dutPort2, + "port3": dutPort3, + } + + atePorts = map[string]*attrs.Attributes{ + "port1": atePort1, + "port2": atePort2, + "port3": atePort3, + } + + allQueueNames = []entname.QoSQueue{ + entname.QoSNC1, + entname.QoSAF4, + entname.QoSAF3, + entname.QoSAF2, + entname.QoSAF1, + entname.QoSBE0, + entname.QoSBE1, + } + + testCases = []struct { + name string + createFlowsF func(otgConfig gosnappi.Config, protocol string, atePortSpeed int) + validateFlowsF func(t *testing.T, dut *ondatra.DUTDevice, ate *ondatra.ATEDevice, atePortSpeed int, startingCounters map[entname.QoSQueue]*queueCounters) + }{ + { + name: "TestNoCongestion", + createFlowsF: testNoCongestionCreateFlows, + validateFlowsF: testNoCongestionValidateFlows, + }, + { + name: "TestCongestion", + createFlowsF: testCongestionCreateFlows, + validateFlowsF: testCongestionValidateFlows, + }, + { + name: "TestNC1Congestion", + createFlowsF: testNC1CongestionCreateFlows, + validateFlowsF: testNC1CongestionValidateFlows, + }, + } +) + +type queueCounters struct { + droppedPackets uint64 + transmitPackets uint64 + transmitOctets uint64 +} + +func prettyPrint(i any) string { + s, _ := json.MarshalIndent(i, "", "\t") + return string(s) +} + +func getZeroIshThresholds(dutPortSpeed int) (uint64, uint64) { + // Max allowed "zero" counters -- counters that are supposed to be zero per the test but + // can have a few packets trickling about for random things; basically: a fudge factor, + // proportional to the port speed. + maxAllowedZeroPackets := uint64(5 * dutPortSpeed) + maxAllowedZeroOctets := uint64(40 * dutPortSpeed) + + return maxAllowedZeroPackets, maxAllowedZeroOctets +} + +func configureDUTQoS( + t *testing.T, + dut *ondatra.DUTDevice, +) { + dp1 := dut.Port(t, "port1") + dp2 := dut.Port(t, "port2") + dp3 := dut.Port(t, "port3") + + qosConfig := &oc.Qos{} + + if deviations.QOSQueueRequiresID(dut) { + for i, queueName := range allQueueNames { + q1 := qosConfig.GetOrCreateQueue(string(queueName)) + q1.Name = ygot.String(string(queueName)) + queueID := len(allQueueNames) - i + q1.QueueId = ygot.Uint8(uint8(queueID)) + } + } + + // Forwarding group :: queue config. + for _, queueName := range allQueueNames { + qoscfg.SetForwardingGroup( + t, + dut, + qosConfig, + fmt.Sprintf("target-group-%s", string(queueName)), + string(queueName), + ) + } + + // Queue management profile. + queueManagementProfile := qosConfig.GetOrCreateQueueManagementProfile("queueManagementProfile") + wredUniformProfile := queueManagementProfile.GetOrCreateWred().GetOrCreateUniform() + wredUniformProfile.SetEnableEcn(true) + wredUniformProfile.SetMinThreshold(uint64(80_000)) + wredUniformProfile.SetMaxThreshold(uint64(3_000_000)) + wredUniformProfile.SetMaxDropProbabilityPercent(uint8(100)) + + // Classifier config. + classifiers := []struct { + name string + termID string + targetGroup string + dscpSet []uint8 + }{ + { + name: "dscp_based_classifier_", + termID: "0", + targetGroup: "target-group-BE1", + dscpSet: []uint8{0, 1, 2, 3}, + }, + { + name: "dscp_based_classifier_", + termID: "1", + targetGroup: "target-group-BE0", + dscpSet: []uint8{4, 5, 6, 7}, + }, + { + name: "dscp_based_classifier_", + termID: "2", + targetGroup: "target-group-AF1", + dscpSet: []uint8{8, 9, 10, 11, 12, 13, 14, 15}, + }, + { + name: "dscp_based_classifier_", + termID: "3", + targetGroup: "target-group-AF2", + dscpSet: []uint8{16, 17, 18, 19, 20, 21, 22, 23}, + }, + { + name: "dscp_based_classifier_", + termID: "4", + targetGroup: "target-group-AF3", + dscpSet: []uint8{24, 25, 26, 27, 28, 29, 30, 31}, + }, + { + name: "dscp_based_classifier_", + termID: "5", + targetGroup: "target-group-AF4", + dscpSet: []uint8{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}, + }, + { + name: "dscp_based_classifier_", + termID: "6", + targetGroup: "target-group-NC1", + dscpSet: []uint8{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63}, + }, + } + + for _, tc := range classifiers { + for _, protocol := range []oc.E_Qos_Classifier_Type{ + oc.Qos_Classifier_Type_IPV4, + oc.Qos_Classifier_Type_IPV6, + } { + protocolString := "ipv4" + if protocol == oc.Qos_Classifier_Type_IPV6 { + protocolString = "ipv6" + } + + name := fmt.Sprintf("%s%s", tc.name, protocolString) + classifier := qosConfig.GetOrCreateClassifier(name) + classifier.SetName(name) + classifier.SetType(protocol) + + term, err := classifier.NewTerm(tc.termID) + if err != nil { + t.Fatalf("Failed to create classifier.NewTerm(): %v", err) + } + term.SetId(tc.termID) + action := term.GetOrCreateActions() + action.SetTargetGroup(tc.targetGroup) + condition := term.GetOrCreateConditions() + + switch protocol { + case oc.Qos_Classifier_Type_IPV4: + condition.GetOrCreateIpv4().SetDscpSet(tc.dscpSet) + case oc.Qos_Classifier_Type_IPV6: + condition.GetOrCreateIpv6().SetDscpSet(tc.dscpSet) + } + } + } + + // Ingress classifier config. + for _, inputInterfaceName := range []string{dp2.Name(), dp3.Name()} { + for _, protocol := range []oc.E_Input_Classifier_Type{ + oc.Input_Classifier_Type_IPV4, + oc.Input_Classifier_Type_IPV6, + } { + protocolString := "ipv4" + if protocol == oc.Input_Classifier_Type_IPV6 { + protocolString = "ipv6" + } + + qoscfg.SetInputClassifier( + t, + dut, + qosConfig, + inputInterfaceName, + protocol, + fmt.Sprintf("dscp_based_classifier_%s", protocolString), + ) + } + } + + // Egress scheduler config. + schedulerPolicy := qosConfig.GetOrCreateSchedulerPolicy("schedulerPolicy") + strictScheduler := schedulerPolicy.GetOrCreateScheduler(uint32(0)) + strictScheduler.SetPriority(oc.Scheduler_Priority_STRICT) + strictInput := strictScheduler.GetOrCreateInput(string(entname.QoSNC1)) + strictInput.SetInputType(oc.Input_InputType_QUEUE) + strictInput.SetQueue(string(entname.QoSNC1)) + + wrrScheduler := schedulerPolicy.GetOrCreateScheduler(uint32(1)) + + // WRR queues, equally weighted. + for _, queueName := range allQueueNames { + if queueName == entname.QoSNC1 { + // Skipping NC1 since it's in its own strict scheduler. + continue + } + input := wrrScheduler.GetOrCreateInput(string(queueName)) + input.SetInputType(oc.Input_InputType_QUEUE) + input.SetQueue(string(queueName)) + input.SetWeight(uint64(10)) + } + + // Egress policy config. + for _, queueName := range allQueueNames { + qosInterface := qosConfig.GetOrCreateInterface(dp1.Name()) + qosInterface.GetOrCreateInterfaceRef().Interface = ygot.String(dp1.Name()) + output := qosInterface.GetOrCreateOutput() + outputSchedulerPolicy := output.GetOrCreateSchedulerPolicy() + outputSchedulerPolicy.SetName("schedulerPolicy") + queue := output.GetOrCreateQueue(string(queueName)) + queue.SetQueueManagementProfile("queueManagementProfile") + if deviations.QOSBufferAllocationConfigRequired(dut) { + bufferAllocationProfile := qosConfig.GetOrCreateBufferAllocationProfile("bufferAllocationProfile") + bufferAllocationQueue := bufferAllocationProfile.GetOrCreateQueue(string(queueName)) + bufferAllocationQueue.SetStaticSharedBufferLimit(uint32(268435456)) + output.SetBufferAllocationProfile("bufferAllocationProfile") + } + } + + gnmi.Replace(t, dut, gnmi.OC().Qos().Config(), qosConfig) +} + +func configureDUTPort( + t *testing.T, + dut *ondatra.DUTDevice, + port *ondatra.Port, + portAttrs *attrs.Attributes, +) { + gnmiOCRoot := gnmi.OC() + + gnmi.Replace( + t, + dut, + gnmiOCRoot.Interface(port.Name()).Config(), + portAttrs.NewOCInterface(port.Name(), dut), + ) + + if deviations.ExplicitInterfaceInDefaultVRF(dut) { + fptest.AssignToNetworkInstance( + t, dut, port.Name(), deviations.DefaultNetworkInstance(dut), subInterfaceIndex, + ) + } +} + +func configureDUT(t *testing.T, dut *ondatra.DUTDevice) { + for portName, portAttrs := range dutPorts { + port := dut.Port(t, portName) + configureDUTPort(t, dut, port, portAttrs) + } + configureDUTQoS(t, dut) +} + +func configureATE(t *testing.T, ate *ondatra.ATEDevice) gosnappi.Config { + otgConfig := gosnappi.NewConfig() + for portName, portAttrs := range atePorts { + port := ate.Port(t, portName) + dutPort := dutPorts[portName] + portAttrs.AddToOTG(otgConfig, port, dutPort) + } + return otgConfig +} + +func trafficClassFieldsToDecimal(dscpValue, ecnValue int) uint32 { + dscpByte := byte(dscpValue) + ecnByte := byte(ecnValue) + tosStr := fmt.Sprintf("%06b%02b", dscpByte, ecnByte) + tosDec, _ := strconv.ParseInt(tosStr, 2, 64) + return uint32(tosDec) +} + +func createFlow(otgConfig gosnappi.Config, protocol string, targetTotalFlowRate uint64, dscpValue int, sourceAtePort *attrs.Attributes) gosnappi.Flow { + flow := otgConfig.Flows().Add().SetName(fmt.Sprintf("dscp-%d-%s", dscpValue, sourceAtePort.Name)) + flow.Metrics().SetEnable(true) + + // Flows go from ate port 2 -> dut -> ate port 1 and + // from ate port 3 -> dut -> ate port 1 to be consistent with the previous test which + // can be run with only two ports instead of three. + flow.TxRx().Device(). + SetTxNames([]string{fmt.Sprintf("%s.%s", sourceAtePort.Name, protocol)}). + SetRxNames([]string{fmt.Sprintf("%s.%s", atePort1.Name, protocol)}) + flow.EgressPacket().Add().Ethernet() + + ethHeader := flow.Packet().Add().Ethernet() + ethHeader.Src().SetValue(sourceAtePort.MAC) + + switch protocol { + case ipv4: + v4 := flow.Packet().Add().Ipv4() + v4.Src().SetValue(sourceAtePort.IPv4) + v4.Dst().SetValue(atePort1.IPv4) + v4.Priority().Raw().SetValue(trafficClassFieldsToDecimal(dscpValue, 2)) + + tracking := flow.EgressPacket().Add().Ipv4() + tracking.Priority().Raw().MetricTags().Add().SetName(fmt.Sprintf("dst-dscp-%d-%s", dscpValue, sourceAtePort.Name)).SetOffset(0).SetLength(6) + tracking.Priority().Raw().MetricTags().Add().SetName(fmt.Sprintf("dst-ecn-%d-%s", dscpValue, sourceAtePort.Name)).SetOffset(6).SetLength(2) + case ipv6: + v6 := flow.Packet().Add().Ipv6() + v6.Src().SetValue(sourceAtePort.IPv6) + v6.Dst().SetValue(atePort1.IPv6) + v6.TrafficClass().SetValue(trafficClassFieldsToDecimal(dscpValue, 2)) + + tracking := flow.EgressPacket().Add().Ipv6() + tracking.TrafficClass().MetricTags().Add().SetName(fmt.Sprintf("dst-dscp-%d-%s", dscpValue, sourceAtePort.Name)).SetOffset(0).SetLength(6) + tracking.TrafficClass().MetricTags().Add().SetName(fmt.Sprintf("dst-ecn-%d-%s", dscpValue, sourceAtePort.Name)).SetOffset(6).SetLength(2) + } + + flow.Size().SetFixed(flowFrameSize) + flow.Rate().SetKbps(targetTotalFlowRate) + return flow +} + +func getQueueCounters(t *testing.T, dut *ondatra.DUTDevice) map[entname.QoSQueue]*queueCounters { + t.Helper() + ep := dut.Port(t, dutEgressPort) + qc := map[entname.QoSQueue]*queueCounters{} + + for _, egressQueueName := range allQueueNames { + qc[egressQueueName] = &queueCounters{ + droppedPackets: gnmi.Get(t, dut, gnmi.OC().Qos().Interface(ep.Name()).Output().Queue(string(egressQueueName)).DroppedPkts().State()), + transmitPackets: gnmi.Get(t, dut, gnmi.OC().Qos().Interface(ep.Name()).Output().Queue(string(egressQueueName)).TransmitPkts().State()), + transmitOctets: gnmi.Get(t, dut, gnmi.OC().Qos().Interface(ep.Name()).Output().Queue(string(egressQueueName)).TransmitOctets().State()), + } + } + + return qc +} + +func logAndGetResolvedQueueCounters(t *testing.T, egressQueueName entname.QoSQueue, egressQueueStartingCounters, egressQueueEndingCounters *queueCounters) (uint64, uint64, uint64) { + queueDroppedPackets := egressQueueEndingCounters.droppedPackets - egressQueueStartingCounters.droppedPackets + queueTransmitPackets := egressQueueEndingCounters.transmitPackets - egressQueueStartingCounters.transmitPackets + queueTransmitOctets := egressQueueEndingCounters.transmitOctets - egressQueueStartingCounters.transmitOctets + + t.Logf( + "\nqueue %q pre-test telemetry data:\n\tdropped %d packets\n\ttransmit %d packets\n\ttransmit %d octets\n", + egressQueueName, + egressQueueStartingCounters.droppedPackets, + egressQueueStartingCounters.transmitPackets, + egressQueueStartingCounters.transmitOctets, + ) + + t.Logf( + "\nqueue %q post-test telemetry data:\n\tdropped %d packets\n\ttransmit %d packets\n\ttransmit %d octets\n", + egressQueueName, + egressQueueEndingCounters.droppedPackets, + egressQueueEndingCounters.transmitPackets, + egressQueueEndingCounters.transmitOctets, + ) + + t.Logf( + "\nqueue %q resolved telemetry data:\n\tdropped %d packets\n\ttransmit %d packets\n\ttransmit %d octets\n", + egressQueueName, + queueDroppedPackets, + queueTransmitPackets, + queueTransmitOctets, + ) + + return queueDroppedPackets, queueTransmitPackets, queueTransmitOctets +} + +func testNoCongestionCreateFlows(otgConfig gosnappi.Config, protocol string, dutPortSpeed int) { + // Target flow rate is 60% of the ate port speed spread across 64 flows (do this in kbps so we + // still work w/ round numbers on 1g interfaces). + portSpeedInKbps := dutPortSpeed * 1_000_000 + portSpeedSixtyPercent := float32(portSpeedInKbps) * float32(0.6) + targetTotalFlowRate := uint64(portSpeedSixtyPercent / 64) + + for dscpValue := 0; dscpValue < 64; dscpValue++ { + finalTargetFlowRate := targetTotalFlowRate + if dscpValue <= 7 { + // There are fewer flows in the BE0/BE1 queues so increase those flows to have + // a similar amount of traffic so wred handles things consistently. + finalTargetFlowRate = targetTotalFlowRate * 2 + } + + createFlow( + otgConfig, + protocol, + finalTargetFlowRate, + dscpValue, + atePort2, + ) + } +} + +func testNoCongestionValidateFlows(t *testing.T, dut *ondatra.DUTDevice, ate *ondatra.ATEDevice, dutPortSpeed int, startingCounters map[entname.QoSQueue]*queueCounters) { + maxAllowedZeroPackets, _ := getZeroIshThresholds(dutPortSpeed) + endingCounters := getQueueCounters(t, dut) + + for egressQueueName, egressQueueEndingCounters := range endingCounters { + egressQueueStartingCounters := startingCounters[egressQueueName] + + queueDroppedPackets, queueTransmitPackets, queueTransmitOctets := logAndGetResolvedQueueCounters( + t, + egressQueueName, + egressQueueStartingCounters, + egressQueueEndingCounters, + ) + + if queueDroppedPackets > maxAllowedZeroPackets { + t.Errorf("queue %s indicates %d dropped packets but should show zero or near-zero", egressQueueName, queueDroppedPackets) + } + + if queueTransmitPackets == 0 { + t.Errorf("queue %s indicates 0 transmit packets but should be non-zero", egressQueueName) + } + + if queueTransmitOctets == 0 { + t.Errorf("queue %s indicates 0 transmit octets but should be non-zero", egressQueueName) + } + } + + for dscpValue := 0; dscpValue < 64; dscpValue++ { + etPath := gnmi.OTG().Flow(fmt.Sprintf("dscp-%d-%s", dscpValue, atePort2.Name)).TaggedMetricAny() + ets := gnmi.GetAll(t, ate.OTG(), etPath.State()) + + dscpAsHex := fmt.Sprintf("0x%02x", dscpValue) + + if len(ets) != 1 { + t.Logf("got %d flows, but expected one, this probably indicates that the flow has"+ + " some packets tagged 01 and some tagged 11 (congestion experienced) -- "+ + "this should not happen in this test case, will continue validation...", len(ets)) + } + + for _, et := range ets { + if len(et.Tags) != 2 { + t.Errorf("expected two metric tags (dscp/ecn) but got %d", len(et.Tags)) + } + + for _, tag := range et.Tags { + tagName := tag.GetTagName() + valueAsHex := tag.GetTagValue().GetValueAsHex() + t.Logf("flow with dscp value %d, tag name %q, got value %s", dscpValue, tagName, valueAsHex) + if strings.Contains(tagName, "dscp") { + if valueAsHex != dscpAsHex { + t.Errorf("expected dscp bit to be %x, but got %s", dscpAsHex, valueAsHex) + } + } else { + // ECN should be 10 -- ecn capable but no congestion experienced. + if valueAsHex != "0x2" { + t.Errorf("expected ecn bit to be 0x2, but got %s", valueAsHex) + } + } + } + } + } +} + +func testCongestionCreateFlows(otgConfig gosnappi.Config, protocol string, dutPortSpeed int) { + // Target flow rate is 60% of the ate port speed spread across 64 flows (do this in kbps so we + // still work w/ round numbers on 1g interfaces). + portSpeedInKbps := dutPortSpeed * 1_000_000 + portSpeedSixtyPercent := float32(portSpeedInKbps) * float32(0.6) + targetTotalFlowRate := uint64(portSpeedSixtyPercent / 64) + + for _, sourceAtePort := range []*attrs.Attributes{atePort2, atePort3} { + for dscpValue := 0; dscpValue < 64; dscpValue++ { + finalTargetFlowRate := targetTotalFlowRate + if dscpValue <= 7 { + // There are fewer flows in the be0/be1 queues so increase those flows to have + // a similar amount of traffic so wred handles things consistently. + finalTargetFlowRate = targetTotalFlowRate * 2 + } + + createFlow( + otgConfig, + protocol, + finalTargetFlowRate, + dscpValue, + sourceAtePort, + ) + } + } +} + +func testCongestionValidateFlows(t *testing.T, dut *ondatra.DUTDevice, ate *ondatra.ATEDevice, dutPortSpeed int, startingCounters map[entname.QoSQueue]*queueCounters) { + maxAllowedZeroPackets, _ := getZeroIshThresholds(dutPortSpeed) + endingCounters := getQueueCounters(t, dut) + + for egressQueueName, egressQueueEndingCounters := range endingCounters { + egressQueueStartingCounters := startingCounters[egressQueueName] + + queueDroppedPackets, queueTransmitPackets, queueTransmitOctets := logAndGetResolvedQueueCounters( + t, + egressQueueName, + egressQueueStartingCounters, + egressQueueEndingCounters, + ) + + if queueTransmitPackets == 0 { + t.Errorf("queue %s indicates 0 transmit packets but should be non-zero", egressQueueName) + } + + if queueTransmitOctets == 0 { + t.Errorf("queue %s indicates 0 transmit octets but should be non-zero", egressQueueName) + } + + if egressQueueName == entname.QoSNC1 { + // NC1 should have no drops + if queueDroppedPackets > maxAllowedZeroPackets { + t.Errorf("queue %s indicates %d dropped packets but should show zero or near-zero", egressQueueName, queueDroppedPackets) + } + } else { + // Any other queue should have at least some drops. + if queueDroppedPackets == 0 { + t.Errorf( + "queue %s indicates %d dropped packets but should show some non-zero value as there is congestion in this case", + egressQueueName, queueDroppedPackets) + } + } + } + + var congestedFlowCount int + + // These should have the majority of flows have ecn set. + for _, sourceAtePort := range []*attrs.Attributes{atePort2, atePort3} { + for dscpValue := 0; dscpValue < 48; dscpValue++ { + etPath := gnmi.OTG().Flow(fmt.Sprintf("dscp-%d-%s", dscpValue, sourceAtePort.Name)).TaggedMetricAny() + ets := gnmi.GetAll(t, ate.OTG(), etPath.State()) + + dscpAsHex := fmt.Sprintf("0x%02x", dscpValue) + + if len(ets) != 2 { + // We should always have two sets of metric tags for flows in this test case -- the + // initial packets will not be marked as congestion experienced of course, but all + // the flows should eventually be marked as such. if we get a flow w/ only 1 path + // we know this flow had no congestion. + t.Logf("expected two sets of tags for flow but got %d\n\t%s", len(ets), prettyPrint(ets)) + continue + } + + // We only care about checking the second set of tags as these are the ones that should + // have been marked w/ congestion experienced. + if len(ets[1].Tags) != 2 { + t.Errorf("expected two metric tags (dscp/ecn) but got %d", len(ets[1].Tags)) + } + + for _, tag := range ets[1].Tags { + tagName := tag.GetTagName() + valueAsHex := tag.GetTagValue().GetValueAsHex() + t.Logf("flow with dscp value %d, tag name %q, got value %s", dscpValue, tagName, valueAsHex) + if strings.Contains(tagName, "dscp") { + if valueAsHex != dscpAsHex { + t.Errorf("expected dscp bit to be %x, but got %s", dscpAsHex, valueAsHex) + } + } else if valueAsHex != "0x2" { + // Not dscp tag, and not 0x2, meaning ecn tag and congestion experienced. + congestedFlowCount++ + } + } + } + } + + if float32(congestedFlowCount/96) < 0.9 { + t.Errorf("less than 90 percent of flows (not in nc1 queue) had congestion experienced") + } + + // These flows should all have no ecn set. + for _, sourceAtePort := range []*attrs.Attributes{atePort2, atePort3} { + for dscpValue := 48; dscpValue < 64; dscpValue++ { + etPath := gnmi.OTG().Flow(fmt.Sprintf("dscp-%d-%s", dscpValue, sourceAtePort.Name)).TaggedMetricAny() + ets := gnmi.GetAll(t, ate.OTG(), etPath.State()) + + dscpAsHex := fmt.Sprintf("0x%02x", dscpValue) + + for _, et := range ets { + if len(et.Tags) != 2 { + t.Errorf("expected two metric tags (dscp/ecn) but got %d", len(et.Tags)) + } + + for _, tag := range et.Tags { + tagName := tag.GetTagName() + valueAsHex := tag.GetTagValue().GetValueAsHex() + t.Logf("flow with dscp value %d, tag name %q, got value %s", dscpValue, tagName, valueAsHex) + if strings.Contains(tagName, "dscp") { + if valueAsHex != dscpAsHex { + t.Errorf("expected dscp bit to be %x, but got %s", dscpAsHex, valueAsHex) + } + } else { + if valueAsHex != "0x2" { + t.Errorf("expected ecn bit for dscp value %d to be 0x2, but got %s", dscpValue, valueAsHex) + } + } + } + } + } + } +} + +func testNC1CongestionCreateFlows(otgConfig gosnappi.Config, protocol string, dutPortSpeed int) { + // Target flow rate is 60% of the ate port speed spread across 16 flows (do this in kbps so we + // still work w/ round numbers on 1g interfaces). + portSpeedInKbps := dutPortSpeed * 1_000_000 + portSpeedSixtyPercent := float32(portSpeedInKbps) * float32(0.6) + targetTotalFlowRate := uint64(portSpeedSixtyPercent / 16) + + for _, sourceAtePort := range []*attrs.Attributes{atePort2, atePort3} { + for dscpValue := 48; dscpValue < 64; dscpValue++ { + createFlow( + otgConfig, + protocol, + targetTotalFlowRate, + dscpValue, + sourceAtePort, + ) + } + } +} + +func testNC1CongestionValidateFlows(t *testing.T, dut *ondatra.DUTDevice, ate *ondatra.ATEDevice, dutPortSpeed int, startingCounters map[entname.QoSQueue]*queueCounters) { + maxAllowedZeroPackets, maxAllowedZeroOctets := getZeroIshThresholds(dutPortSpeed) + endingCounters := getQueueCounters(t, dut) + + for egressQueueName, egressQueueEndingCounters := range endingCounters { + egressQueueStartingCounters := startingCounters[egressQueueName] + + queueDroppedPackets, queueTransmitPackets, queueTransmitOctets := logAndGetResolvedQueueCounters( + t, + egressQueueName, + egressQueueStartingCounters, + egressQueueEndingCounters, + ) + + if egressQueueName == entname.QoSNC1 { + if queueTransmitPackets == 0 { + t.Errorf("queue %s indicates 0 transmit packets but should be non-zero", egressQueueName) + } + + if queueTransmitOctets == 0 { + t.Errorf("queue %s indicates 0 transmit octets but should be non-zero", egressQueueName) + } + + if queueDroppedPackets == 0 { + t.Errorf("queue %s indicates %d dropped packets but should show non-zero", egressQueueName, queueDroppedPackets) + } + } else { + if queueTransmitPackets > maxAllowedZeroPackets { + t.Errorf("queue %s indicates non zero transmit packets but should be zero or near zero", egressQueueName) + } + + if queueTransmitOctets > maxAllowedZeroOctets { + t.Errorf("queue %s indicates non zero transmit octets but should be zero or near zero", egressQueueName) + } + } + } + + var congestedFlowCount int + + for _, sourceAtePort := range []*attrs.Attributes{atePort2, atePort3} { + for dscpValue := 48; dscpValue < 64; dscpValue++ { + etPath := gnmi.OTG().Flow(fmt.Sprintf("dscp-%d-%s", dscpValue, sourceAtePort.Name)).TaggedMetricAny() + ets := gnmi.GetAll(t, ate.OTG(), etPath.State()) + + dscpAsHex := fmt.Sprintf("0x%02x", dscpValue) + + if len(ets) != 2 { + // Similar to the congestion (non NC1) test, we expect two sets of metrics -- one for + // the start of the flow where ecn is not yet set, and the second for when it is. + t.Logf("expected two sets of tags for flow but got %d\n\t%s", len(ets), prettyPrint(ets)) + continue + } + + if len(ets[1].Tags) != 2 { + t.Errorf("expected two metric tags (dscp/ecn) but got %d", len(ets[1].Tags)) + } + + for _, tag := range ets[1].Tags { + tagName := tag.GetTagName() + valueAsHex := tag.GetTagValue().GetValueAsHex() + t.Logf("flow with dscp value %d, tag name %q, got value %s", dscpValue, tagName, valueAsHex) + if strings.Contains(tagName, "dscp") { + if valueAsHex != dscpAsHex { + t.Errorf("expected dscp bit to be %x, but got %s", dscpAsHex, valueAsHex) + } + } else if valueAsHex != "0x2" { + // Not dscp tag, and not 0x2, meaning ecn tag and congestion experienced. + congestedFlowCount++ + } + } + } + } + + if float32(congestedFlowCount/32) < 0.9 { + t.Errorf("less than 90 percent of flows (in nc1 queue) had congestion experienced") + } +} + +func TestDSCPTransparency(t *testing.T) { + dut := ondatra.DUT(t, "dut") + ate := ondatra.ATE(t, "ate") + otg := ate.OTG() + + configureDUT(t, dut) + + otgConfig := configureATE(t, ate) + + dutPortSpeed := dut.Ports()[0].Speed() + if dutPortSpeed == 0 { + t.Log("dut port speed was unset, assuming 100G.") + dutPortSpeed = 100 + } + + for _, testCase := range testCases { + for _, flowProto := range []string{ipv4, ipv6} { + t.Run(fmt.Sprintf("%s-%s", testCase.name, flowProto), func(t *testing.T) { + otgConfig.Flows().Clear() + testCase.createFlowsF(otgConfig, flowProto, int(dutPortSpeed)) + + otg.PushConfig(t, otgConfig) + otg.StartProtocols(t) + otgutils.WaitForARP(t, otg, otgConfig, flowProto) + + // Get QoS egress packet counters before the traffic. + startingCounters := getQueueCounters(t, dut) + + otg.StartTraffic(t) + time.Sleep(trafficRunDuration) + otg.StopTraffic(t) + time.Sleep(trafficStopWaitDuration) + + testCase.validateFlowsF(t, dut, ate, int(dutPortSpeed), startingCounters) + otg.StopProtocols(t) + }) + } + } +} diff --git a/feature/qos/ecn/otg_tests/dscp_transparency_test/metadata.textproto b/feature/qos/ecn/otg_tests/dscp_transparency_test/metadata.textproto new file mode 100644 index 00000000000..508963f8fb2 --- /dev/null +++ b/feature/qos/ecn/otg_tests/dscp_transparency_test/metadata.textproto @@ -0,0 +1,18 @@ +# proto-file: github.com/openconfig/featureprofiles/proto/metadata.proto +# proto-message: Metadata + +uuid: "61af8f04-c537-499e-9ac9-dbadccb677a3" +plan_id: "DP-1.17" +description: "DSCP Transparency with ECN" +testbed: TESTBED_DUT_ATE_4LINKS +platform_exceptions: { + platform: { + vendor: NOKIA + } + deviations: { + explicit_interface_in_default_vrf: true + interface_enabled: true + qos_queue_requires_id: true + qos_buffer_allocation_config_required: true + } +} diff --git a/testregistry.textproto b/testregistry.textproto index 5364f42a0b2..6fb77e57dad 100644 --- a/testregistry.textproto +++ b/testregistry.textproto @@ -219,8 +219,8 @@ test: { } test: { id: "DP-1.14" - description: "DSCP transperency with ECN" - readme: "https://github.com/openconfig/featureprofiles/blob/main/feature/qos/ecn/otg_tests/DSCP-transparency/README.md" + description: "QoS basic test" + readme: "https://github.com/openconfig/featureprofiles/blob/main/feature/qos/otg_tests/qos_basic_test/README.md" exec: " " } test: { @@ -235,6 +235,12 @@ test: { readme: "https://github.com/openconfig/featureprofiles/blob/main/feature/qos/otg_tests/ingress_traffic_classification_and_rewrite_test/README.md" exec: " " } +test: { + id: "DP-1.17" + description: "DSCP transparency with ECN" + readme: "https://github.com/openconfig/featureprofiles/blob/main/feature/qos/ecn/otg_tests/dscp_transparency_test/README.md" + exec: " " +} test: { id: "DP-1.18" description: "Flow matching using ACL and to Port Mirror/Redirect"