From ac85cec3cc28bef7d02616838243f9097f82191b Mon Sep 17 00:00:00 2001 From: Varun Jarial Date: Thu, 22 Oct 2026 09:50:15 +0000 Subject: [PATCH] qos_ecn_config_test: ECN config test with ecn_min_greater_max_threshold_unsupported deviation --- .../qos_ecn_config_test/metadata.textproto | 9 +- .../qos_ecn_config_test.go | 801 +++++++++--------- internal/deviations/deviations.go | 6 + internal/gnoi/gnoi.go | 53 ++ internal/qoscfg/qoscfg.go | 330 +++++++- proto/metadata.proto | 9 + proto/metadata_go_proto/metadata.pb.go | 25 +- 7 files changed, 807 insertions(+), 426 deletions(-) diff --git a/feature/qos/tests/qos_ecn_config_test/metadata.textproto b/feature/qos/tests/qos_ecn_config_test/metadata.textproto index 8e0c3fe8ded..209e24b1f6b 100644 --- a/feature/qos/tests/qos_ecn_config_test/metadata.textproto +++ b/feature/qos/tests/qos_ecn_config_test/metadata.textproto @@ -11,6 +11,8 @@ platform_exceptions: { } deviations: { interface_ref_interface_id_format: true + ecn_same_min_max_threshold_unsupported: true + state_path_unsupported: true } } platform_exceptions: { @@ -20,6 +22,7 @@ platform_exceptions: { deviations: { qos_queue_requires_id: true qos_buffer_allocation_config_required: true + ecn_min_greater_max_threshold_unsupported: true } } platform_exceptions: { @@ -27,8 +30,11 @@ platform_exceptions: { vendor: ARISTA } deviations: { + qos_get_state_path_unsupported: true state_path_unsupported: true interface_ref_config_unsupported: true + ecn_min_greater_max_threshold_unsupported: true + gnoi_subcomponent_path: true } } platform_exceptions: { @@ -36,12 +42,13 @@ platform_exceptions: { vendor: CISCO } deviations: { + qos_get_state_path_unsupported: true state_path_unsupported: true drop_weight_leaves_unsupported: true qos_queue_requires_id: true ecn_same_min_max_threshold_unsupported: true + ecn_min_greater_max_threshold_unsupported: true qos_set_weight_config_unsupported: true - qos_get_state_path_unsupported: true qos_scheduler_config_required: true } } diff --git a/feature/qos/tests/qos_ecn_config_test/qos_ecn_config_test.go b/feature/qos/tests/qos_ecn_config_test/qos_ecn_config_test.go index ae94ee7fb04..dbec2df1144 100644 --- a/feature/qos/tests/qos_ecn_config_test/qos_ecn_config_test.go +++ b/feature/qos/tests/qos_ecn_config_test/qos_ecn_config_test.go @@ -16,471 +16,444 @@ package qos_ecn_config_test import ( "testing" + "time" + "github.com/openconfig/featureprofiles/internal/components" "github.com/openconfig/featureprofiles/internal/deviations" "github.com/openconfig/featureprofiles/internal/fptest" + "github.com/openconfig/featureprofiles/internal/gnoi" "github.com/openconfig/featureprofiles/internal/qoscfg" "github.com/openconfig/ondatra" "github.com/openconfig/ondatra/gnmi" "github.com/openconfig/ondatra/gnmi/oc" "github.com/openconfig/ondatra/netutil" - "github.com/openconfig/ygot/ygot" + "github.com/openconfig/testt" ) func TestMain(m *testing.M) { fptest.RunTests(m) } -type Testcase struct { - name string - fn func(t *testing.T, q *oc.Qos) -} - -// For Cisco devices, minimum and maximum threshold values can't be the same, -// as well as it should be a multiple of 6,144 bytes +// Closest supported min/max threshold pair used by devices with +// deviations.EcnSameMinMaxThresholdUnsupported, which cannot set equal values. const ( - CiscoMinThreshold = (uint64(8005632)) - CiscoMaxThreshold = (uint64(8011776)) + DeviatedMinThreshold = (uint64(8005632)) + DeviatedMaxThreshold = (uint64(8011776)) ) -var ( - QoSEcnConfigTestcases = []Testcase{ - { - - name: "testECNConfig", - fn: testECNConfig, - }, - { - name: "testQoSOutputIntfConfig", - fn: testQoSOutputIntfConfig, - }, - } -) +// wantWeight is the WRED weight set on every ECN profile. The README gives no +// value, so 0 (instantaneous queue depth) is used, matching the DP-1.2 test. +const wantWeight = uint32(0) -// QoS ecn OC config: -// - ECN queue-management-profiles: -// - /qos/queue-management-profiles/queue-management-profile/wred/uniform/config/min-threshold -// - /qos/queue-management-profiles/queue-management-profile/wred/uniform/config/max-threshold -// - /qos/queue-management-profiles/queue-management-profile/wred/uniform/config/enable-ecn -// - /qos/queue-management-profiles/queue-management-profile/wred/uniform/config/weight -// - /qos/queue-management-profiles/queue-management-profile/wred/uniform/config/drop -// - /qos/queue-management-profiles/queue-management-profile/wred/uniform/config/max-drop-probability-percent -// - Output interface queue and scheduler-policy: -// - /qos/interfaces/interface/output/queues/queue/config/queue-management-profile -// - /qos/interfaces/interface/output/queues/queue/config/name -// - /qos/interfaces/interface/output/scheduler-policy/config/name -// -// -// Topology: -// ate:port1 <--> port1:dut:port2 <--> ate:port2 -// -// Test notes: -// -// Sample CLI command to get telemetry using gmic: -// - gnmic -a ipaddr:10162 -u username -p password --skip-verify get \ -// --path /components/component --format flat -// - gnmic tool info: -// - https://github.com/karimra/gnmic/blob/main/README.md -// +// ecnProfiles lists every queue management profile this test may create. +var ecnProfiles = []string{"ECN_PROFILE_1", "ECN_PROFILE_2", "ECN_PROFILE_3", "ECN_PROFILE_NEG"} -func TestQosEcnConfigTests(t *testing.T) { - d := &oc.Root{} - q := d.GetOrCreateQos() - for _, tt := range QoSEcnConfigTestcases { - t.Run(tt.name, func(t *testing.T) { - tt.fn(t, q) +// removeECNConfig detaches the ECN profile from queueName on dp and deletes all +// ecnProfiles, logging and ignoring errors. Used by t.Cleanup if DP-1.3.5 did not complete. +func removeECNConfig(t *testing.T, dut *ondatra.DUTDevice, dp *ondatra.Port, queueName string) { + t.Helper() + bestEffort := func(desc string, fn func(t testing.TB)) { + if msg := testt.CaptureFatal(t, fn); msg != nil { + t.Logf("Cleanup: %s failed (ignored): %s", desc, *msg) + } + } + bestEffort("detach queue management profile", func(t testing.TB) { + qoscfg.DetachOutputQueueManagementProfile(t, dut, dp.Name(), queueName) + }) + for _, p := range ecnProfiles { + bestEffort("delete queue management profile "+p, func(t testing.TB) { + gnmi.Delete(t, dut, gnmi.OC().Qos().QueueManagementProfile(p).Config()) }) } } -func testECNConfig(t *testing.T, q *oc.Qos) { - dut := ondatra.DUT(t, "dut") - ecnConfig := struct { - ecnEnabled bool - dropEnabled bool - minThreshold uint64 - maxThreshold uint64 - maxDropProbabilityPercent uint8 - weight uint32 - }{ - ecnEnabled: true, - dropEnabled: false, - minThreshold: uint64(80000), - maxThreshold: uint64(80000), - maxDropProbabilityPercent: uint8(100), - weight: uint32(0), +// newValidECNProfile adds a complete ECN profile called name to q, using the +// DP-1.3.2 values that all vendors accept, and returns its wred/uniform +// settings. It does not send anything to the device. +// +// DP-1.3.4 Negative Tests 1 and 2 change one leaf to an invalid value, so a +// rejection is caused by that leaf and not by missing leaves (Junos rejects a +// profile without enable-ecn and max-drop-probability-percent). +// Negative Test 4 uses it unchanged: attaches it to queue "0", then deletes it. +func newValidECNProfile(dut *ondatra.DUTDevice, q *oc.Qos, name string) *oc.Qos_QueueManagementProfile_Wred_Uniform { + profile := q.GetOrCreateQueueManagementProfile(name) + profile.SetName(name) + uniform := profile.GetOrCreateWred().GetOrCreateUniform() + uniform.SetEnableEcn(true) + uniform.SetDrop(false) + uniform.SetMaxDropProbabilityPercent(100) + uniform.SetMinThreshold(3276800) + uniform.SetMaxThreshold(6553600) + if !deviations.QosSetWeightConfigUnsupported(dut) { + uniform.SetWeight(wantWeight) } + return uniform +} - queueMgmtProfile := q.GetOrCreateQueueManagementProfile("DropProfile") - queueMgmtProfile.SetName("DropProfile") - wred := queueMgmtProfile.GetOrCreateWred() - uniform := wred.GetOrCreateUniform() - uniform.SetEnableEcn(ecnConfig.ecnEnabled) - uniform.SetDrop(ecnConfig.dropEnabled) - wantMinThreshold := ecnConfig.minThreshold - wantMaxThreshold := ecnConfig.maxThreshold - if deviations.EcnSameMinMaxThresholdUnsupported(dut) { - wantMinThreshold = CiscoMinThreshold - wantMaxThreshold = CiscoMaxThreshold - } - uniform.SetMinThreshold(wantMinThreshold) - uniform.SetMaxThreshold(wantMaxThreshold) - uniform.SetMaxDropProbabilityPercent(ecnConfig.maxDropProbabilityPercent) - if !deviations.QosSetWeightConfigUnsupported(dut) { - uniform.SetWeight(ecnConfig.weight) +func TestQosEcnConfig(t *testing.T) { + dut := ondatra.DUT(t, "dut") + dp1 := dut.Port(t, "port1") + + queueName := "0" + if !deviations.QOSQueueRequiresID(dut) { + queueName = netutil.CommonTrafficQueues(t, dut).BE0 } - if deviations.QosSchedulerConfigRequired(dut) { - schedulerPolicy := q.GetOrCreateSchedulerPolicy("scheduler") - schedulerPolicy.SetName("scheduler") - dp := dut.Port(t, "port2") - i := q.GetOrCreateInterface(dp.Name()) - i.SetInterfaceId(dp.Name()) - i.GetOrCreateInterfaceRef().Interface = ygot.String(dp.Name()) - - queueName := []string{"NC1", "AF4", "AF3", "AF2", "AF1", "BE0", "BE1"} - - for i, queue := range queueName { - q1 := q.GetOrCreateQueue(queue) - q1.Name = ygot.String(queue) - queueid := len(queueName) - i - q1.QueueId = ygot.Uint8(uint8(queueid)) + // teardownVerified is set when DP-1.3.5 removes and verifies all ECN config. + teardownVerified := false + // Safety net for DP-1.3.5: registered before any configuration is pushed so + // the ECN config is removed even if a subtest panics before DP-1.3.5 runs. + // It does nothing when DP-1.3.5 already completed successfully. + t.Cleanup(func() { + if teardownVerified { + return } - cases := []struct { - desc string - sequence uint32 - priority oc.E_Scheduler_Priority - inputID string - inputType oc.E_Input_InputType - weight uint64 - queueName string - targetGrpoup string - ecnProfile string - scheduler string - }{{ - desc: "scheduler-policy-BE1", - sequence: uint32(1), - priority: oc.Scheduler_Priority_UNSET, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(1), - queueName: "BE1", - targetGrpoup: "target-group-BE1", - ecnProfile: "DropProfile", - scheduler: "scheduler", - }, { - desc: "scheduler-policy-BE0", - sequence: uint32(1), - priority: oc.Scheduler_Priority_UNSET, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(4), - queueName: "BE0", - targetGrpoup: "target-group-BE0", - ecnProfile: "DropProfile", - scheduler: "scheduler", - }, { - desc: "scheduler-policy-AF1", - sequence: uint32(1), - priority: oc.Scheduler_Priority_UNSET, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(8), - queueName: "AF1", - targetGrpoup: "target-group-AF1", - ecnProfile: "DropProfile", - scheduler: "scheduler", - }, { - desc: "scheduler-policy-AF2", - sequence: uint32(1), - priority: oc.Scheduler_Priority_UNSET, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(16), - queueName: "AF2", - ecnProfile: "DropProfile", - scheduler: "scheduler", - targetGrpoup: "target-group-AF2", - }, { - desc: "scheduler-policy-AF3", - sequence: uint32(1), - priority: oc.Scheduler_Priority_UNSET, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(32), - queueName: "AF3", - targetGrpoup: "target-group-AF3", - ecnProfile: "DropProfile", - scheduler: "scheduler", - }, { - desc: "scheduler-policy-AF4", - sequence: uint32(0), - priority: oc.Scheduler_Priority_STRICT, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(6), - queueName: "AF4", - targetGrpoup: "target-group-AF4", - ecnProfile: "DropProfile", - scheduler: "scheduler", - }, { - desc: "scheduler-policy-NC1", - sequence: uint32(0), - priority: oc.Scheduler_Priority_STRICT, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(7), - queueName: "NC1", - targetGrpoup: "target-group-NC1", - ecnProfile: "DropProfile", - scheduler: "scheduler", - }} - - t.Logf("qos scheduler policies config cases: %v", cases) - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - s := schedulerPolicy.GetOrCreateScheduler(tc.sequence) - s.SetSequence(tc.sequence) - s.SetPriority(tc.priority) - input := s.GetOrCreateInput(tc.queueName) - input.SetId(tc.queueName) - input.SetInputType(tc.inputType) - input.SetQueue(tc.queueName) - input.SetWeight(tc.weight) - - output := i.GetOrCreateOutput() - schedulerPolicy := output.GetOrCreateSchedulerPolicy() - schedulerPolicy.SetName(tc.scheduler) - queue := output.GetOrCreateQueue(tc.queueName) - queue.SetQueueManagementProfile(tc.ecnProfile) - queue.SetName(tc.queueName) + t.Log("DP-1.3.5 did not complete; removing ECN configuration (best effort)") + removeECNConfig(t, dut, dp1, queueName) + }) - }) + // DP-1.3 Test environment setup + t.Run("Test environment setup", func(t *testing.T) { + // DP-1.3 Test environment setup: DUT port-1 connects to ATE port-1 + // (testbed). Configure the DUT port-1 interface used by all subtests. + qoscfg.ConfigureInterfaceSetup(t, dut, dp1) + + // DP-1.3 Test environment setup: Build the base QoS config (classifier, + // forwarding group, queue). + qos := &oc.Root{} + q := qos.GetOrCreateQos() + + // DP-1.3 Test environment setup: Create an input IPv4 classifier to match traffic intended for the QoS queue being tested + className := "ipv4_dscp_classifier" + targetGroupName := "target-group-" + queueName + qoscfg.ConfigureIPv4DSCPClassifier(t, dut, q, className, targetGroupName, 10 /* dscp 10 for example */) + q.GetOrCreateForwardingGroup(targetGroupName).SetOutputQueue(queueName) + qQueue := q.GetOrCreateQueue(queueName) + qQueue.SetName(queueName) + if deviations.QOSQueueRequiresID(dut) { + qQueue.SetQueueId(0) } - } - t.Logf("qos ECN QueueManagementProfile config cases: %v", ecnConfig) - gnmi.Replace(t, dut, gnmi.OC().Qos().Config(), q) + gnmi.Replace(t, dut, gnmi.OC().Qos().Config(), q) - // Verify the QueueManagementProfile is applied by checking the telemetry path state values. - wredUniform := gnmi.OC().Qos().QueueManagementProfile("DropProfile").Wred().Uniform() - if deviations.QosGetStatePathUnsupported(dut) { - if got, want := gnmi.Get(t, dut, wredUniform.EnableEcn().Config()), ecnConfig.ecnEnabled; got != want { - t.Errorf("wredUniform.EnableEcn().Config(): got %v, want %v", got, want) + // DP-1.3 Test environment setup: Apply the classifier to the input of DUT port-1 + // Use a separate QoS struct so the classifier definitions are not pushed + // again together with the interface input binding. + d2 := &oc.Root{} + q2 := d2.GetOrCreateQos() + qoscfg.SetInputClassifier(t, dut, q2, dp1.Name(), oc.Input_Classifier_Type_IPV4, className) + + // DP-1.3 Test environment setup: Validate the classifier is applied to the input of DUT port-1. + inClassifierPath := gnmi.OC().Qos().Interface(qoscfg.QosClassifierInterfaceID(dut, dp1.Name())).Input().Classifier(oc.Input_Classifier_Type_IPV4) + qoscfg.VerifyLeaf(t, dut, inClassifierPath.Name().State(), inClassifierPath.Name().Config(), className, time.Minute) + }) + + t.Run("DP-1.3.1 - 80KB min-threshold equal max-threshold", func(t *testing.T) { + // DP-1.3.1 Step 1 - Generate DUT configuration + qos := &oc.Root{} + q := qos.GetOrCreateQos() + + profileName := "ECN_PROFILE_1" + queueMgmtProfile := q.GetOrCreateQueueManagementProfile(profileName) + queueMgmtProfile.SetName(profileName) + uniform := queueMgmtProfile.GetOrCreateWred().GetOrCreateUniform() + uniform.SetEnableEcn(true) + uniform.SetDrop(false) + uniform.SetMaxDropProbabilityPercent(100) + if !deviations.QosSetWeightConfigUnsupported(dut) { + uniform.SetWeight(wantWeight) } - if got, want := gnmi.Get(t, dut, wredUniform.MaxDropProbabilityPercent().Config()), ecnConfig.maxDropProbabilityPercent; got != want { - t.Errorf("wredUniform.MaxDropProbabilityPercent().Config(): got %v, want %v", got, want) + + // DP-1.3.1 Step 1: min-threshold = max-threshold = 81920 (80KB). Devices with + // deviations.EcnSameMinMaxThresholdUnsupported cannot set equal values, so the + // closest supported pair (DeviatedMinThreshold/DeviatedMaxThreshold) is used. + var wantMinThreshold uint64 = 81920 + var wantMaxThreshold uint64 = 81920 + if deviations.EcnSameMinMaxThresholdUnsupported(dut) { + wantMinThreshold = DeviatedMinThreshold + wantMaxThreshold = DeviatedMaxThreshold } - if got, want := gnmi.Get(t, dut, wredUniform.MinThreshold().Config()), wantMinThreshold; got != want { - t.Errorf("wredUniform.MinThreshold().Config(): got %v, want %v", got, want) + uniform.SetMinThreshold(wantMinThreshold) + uniform.SetMaxThreshold(wantMaxThreshold) + + qoscfg.BuildOutputQueueManagementProfile(dut, q, dp1.Name(), queueName, profileName) + + // DP-1.3.1 Step 2 - Push configuration to DUT using gNMI Set with REPLACE option. + batch := &gnmi.SetBatch{} + qoscfg.ReplaceQueueManagementProfile(t, dut, batch, q, dp1.Name(), queueName, profileName) + batch.Set(t, dut) + + // DP-1.3.1 Step 3 - Validation with pass/fail criteria: verify each + // wred/uniform state leaf (config leaf on devices without QoS state + // support) using gnmi.Watch/Await via qoscfg.VerifyLeaf. + uniformPath := gnmi.OC().Qos().QueueManagementProfile(profileName).Wred().Uniform() + qoscfg.VerifyLeaf(t, dut, uniformPath.EnableEcn().State(), uniformPath.EnableEcn().Config(), true, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MaxDropProbabilityPercent().State(), uniformPath.MaxDropProbabilityPercent().Config(), 100, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MinThreshold().State(), uniformPath.MinThreshold().Config(), wantMinThreshold, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MaxThreshold().State(), uniformPath.MaxThreshold().Config(), wantMaxThreshold, time.Minute) + if !deviations.DropWeightLeavesUnsupported(dut) { + qoscfg.VerifyLeaf(t, dut, uniformPath.Drop().State(), uniformPath.Drop().Config(), false, time.Minute) } - if got, want := gnmi.Get(t, dut, wredUniform.MaxThreshold().Config()), wantMaxThreshold; got != want { - t.Errorf("wredUniform.MaxThreshold().Config(): got %v, want %v", got, want) + if !deviations.QosSetWeightConfigUnsupported(dut) { + qoscfg.VerifyLeaf(t, dut, uniformPath.Weight().State(), uniformPath.Weight().Config(), wantWeight, time.Minute) } - } else { - if got, want := gnmi.Get(t, dut, wredUniform.EnableEcn().State()), ecnConfig.ecnEnabled; got != want { - t.Errorf("wredUniform.EnableEcn().State(): got %v, want %v", got, want) + + // DP-1.3.1 Step 4 - Validate ECN profile application + outQueuePath := gnmi.OC().Qos().Interface(qoscfg.QosInterfaceID(dut, dp1.Name())).Output().Queue(queueName) + qoscfg.VerifyLeaf(t, dut, outQueuePath.Name().State(), outQueuePath.Name().Config(), queueName, time.Minute) + qoscfg.VerifyLeaf(t, dut, outQueuePath.QueueManagementProfile().State(), outQueuePath.QueueManagementProfile().Config(), profileName, time.Minute) + + // DP-1.3.1 Step 5 - Trigger a supervisor switchover via gNOI SwitchControlProcessor. + // Mandatory on multi form factor DUTs (2+ controller cards); a switchover + // failure fails the test. Only fixed form factor DUTs skip Steps 5-6. + t.Log("Triggering supervisor switchover") + switched, _, prevStandby := gnoi.SwitchControlProcessor(t, dut) + if !switched { + t.Log("Skipping DP-1.3.1 Steps 5-6: fixed form factor DUT has fewer than two controller cards") + return } - if got, want := gnmi.Get(t, dut, wredUniform.MaxDropProbabilityPercent().State()), ecnConfig.maxDropProbabilityPercent; got != want { - t.Errorf("wredUniform.MaxDropProbabilityPercent().State(): got %v, want %v", got, want) + + t.Log("Waiting for device to reconnect...") + startT := time.Now() + for { + // DP-1.3.1 Step 5: Wait for the new active supervisor. gNMI is unreachable + // during the switchover, so gnmi.Watch/Await fail on dial and cannot wait + // across the outage. A 30s back-off between reconnect attempts is therefore + // required (same pattern as feature/gnoi/system/tests/supervisor_switchover_test). + time.Sleep(30 * time.Second) + errMsg := testt.CaptureFatal(t, func(t testing.TB) { + gnmi.Get(t, dut, gnmi.OC().System().CurrentDatetime().State()) + }) + if errMsg == nil { + t.Log("Device successfully reconnected") + break + } + if time.Since(startT) > 10*time.Minute { + t.Fatalf("Device failed to reconnect within 10 minutes") + } } - } - if !deviations.StatePathsUnsupported(dut) { - if got, want := gnmi.Get(t, dut, wredUniform.MinThreshold().State()), ecnConfig.minThreshold; got != want { - t.Errorf("wredUniform.MinThreshold().State(): got %v, want %v", got, want) + // DP-1.3.1 Step 6 - "Once the new supervisor is active": wait until the new + // active card reports switchover-ready before repeating Steps 3 and 4. + gnoi.AwaitSwitchoverReady(t, dut, prevStandby, 30*time.Minute) + + // DP-1.3.1 Step 6 - Repeat checks from Steps 3 and 4. Also verify the + // previous standby card is now active. + cards := components.FindComponentsByType(t, dut, oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_CONTROLLER_CARD) + if _, newActive := components.FindStandbyControllerCard(t, dut, cards); newActive != prevStandby { + t.Errorf("Active controller card after switchover: got %q, want %q", newActive, prevStandby) } - if got, want := gnmi.Get(t, dut, wredUniform.MaxThreshold().State()), ecnConfig.maxThreshold; got != want { - t.Errorf("wredUniform.MaxThreshold().State(): got %v, want %v", got, want) + qoscfg.VerifyLeaf(t, dut, uniformPath.EnableEcn().State(), uniformPath.EnableEcn().Config(), true, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MaxDropProbabilityPercent().State(), uniformPath.MaxDropProbabilityPercent().Config(), 100, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MinThreshold().State(), uniformPath.MinThreshold().Config(), wantMinThreshold, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MaxThreshold().State(), uniformPath.MaxThreshold().Config(), wantMaxThreshold, time.Minute) + if !deviations.DropWeightLeavesUnsupported(dut) { + qoscfg.VerifyLeaf(t, dut, uniformPath.Drop().State(), uniformPath.Drop().Config(), false, time.Minute) } - } - if !deviations.DropWeightLeavesUnsupported(dut) { - if got, want := gnmi.Get(t, dut, wredUniform.Drop().State()), ecnConfig.dropEnabled; got != want { - t.Errorf("wredUniform.Drop().State(): got %v, want %v", got, want) + if !deviations.QosSetWeightConfigUnsupported(dut) { + qoscfg.VerifyLeaf(t, dut, uniformPath.Weight().State(), uniformPath.Weight().Config(), wantWeight, time.Minute) } - if got, want := gnmi.Get(t, dut, wredUniform.Weight().State()), ecnConfig.weight; got != want { - t.Errorf("wredUniform.Weight().State(): got %v, want %v", got, want) + qoscfg.VerifyLeaf(t, dut, outQueuePath.Name().State(), outQueuePath.Name().Config(), queueName, time.Minute) + qoscfg.VerifyLeaf(t, dut, outQueuePath.QueueManagementProfile().State(), outQueuePath.QueueManagementProfile().Config(), profileName, time.Minute) + + qoscfg.ResyncOutputQueueManagementProfile(t, dut, q, dp1.Name(), queueName, profileName) + }) + + t.Run("DP-1.3.2 - Threshold in MB, min-threshold not-equal max-threshold", func(t *testing.T) { + // DP-1.3.2 Step 1 - Generate DUT configuration + qos := &oc.Root{} + q := qos.GetOrCreateQos() + + profileName := "ECN_PROFILE_2" + queueMgmtProfile := q.GetOrCreateQueueManagementProfile(profileName) + queueMgmtProfile.SetName(profileName) + uniform := queueMgmtProfile.GetOrCreateWred().GetOrCreateUniform() + uniform.SetEnableEcn(true) + uniform.SetDrop(false) + uniform.SetMaxDropProbabilityPercent(100) + uniform.SetMinThreshold(3276800) + uniform.SetMaxThreshold(6553600) + if !deviations.QosSetWeightConfigUnsupported(dut) { + uniform.SetWeight(wantWeight) } - } -} -func testQoSOutputIntfConfig(t *testing.T, q *oc.Qos) { - dut := ondatra.DUT(t, "dut") - dp := dut.Port(t, "port2") - queues := netutil.CommonTrafficQueues(t, dut) - - ecnConfig := struct { - ecnEnabled bool - dropEnabled bool - minThreshold uint64 - maxThreshold uint64 - maxDropProbabilityPercent uint8 - weight uint32 - }{ - ecnEnabled: true, - dropEnabled: false, - minThreshold: uint64(80000), - maxThreshold: uint64(80000), - maxDropProbabilityPercent: uint8(100), - weight: uint32(0), - } + qoscfg.BuildOutputQueueManagementProfile(dut, q, dp1.Name(), queueName, profileName) - queueMgmtProfile := q.GetOrCreateQueueManagementProfile("DropProfile") - queueMgmtProfile.SetName("DropProfile") - wred := queueMgmtProfile.GetOrCreateWred() - uniform := wred.GetOrCreateUniform() - uniform.SetEnableEcn(ecnConfig.ecnEnabled) - uniform.SetDrop(ecnConfig.dropEnabled) - wantMinThreshold := ecnConfig.minThreshold - wantMaxThreshold := ecnConfig.maxThreshold - if deviations.EcnSameMinMaxThresholdUnsupported(dut) { - wantMinThreshold = CiscoMinThreshold - wantMaxThreshold = CiscoMaxThreshold - } - uniform.SetMinThreshold(wantMinThreshold) - uniform.SetMaxThreshold(wantMaxThreshold) - uniform.SetMaxDropProbabilityPercent(ecnConfig.maxDropProbabilityPercent) - if !deviations.QosSetWeightConfigUnsupported(dut) { - uniform.SetWeight(ecnConfig.weight) - } + // DP-1.3.2 Step 2 - Push configuration to DUT using gNMI Set with REPLACE option. + batch := &gnmi.SetBatch{} + qoscfg.ReplaceQueueManagementProfile(t, dut, batch, q, dp1.Name(), queueName, profileName) + batch.Set(t, dut) - cases := []struct { - desc string - queueName string - ecnProfile string - scheduler string - sequence uint32 - priority oc.E_Scheduler_Priority - inputID string - inputType oc.E_Input_InputType - weight uint64 - }{{ - desc: "output-interface-BE1", - queueName: queues.BE1, - ecnProfile: "DropProfile", - scheduler: "scheduler", - sequence: uint32(1), - priority: oc.Scheduler_Priority_UNSET, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(1), - }, { - desc: "output-interface-BE0", - queueName: queues.BE0, - ecnProfile: "DropProfile", - scheduler: "scheduler", - sequence: uint32(1), - priority: oc.Scheduler_Priority_UNSET, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(4), - }, { - desc: "output-interface-AF1", - queueName: queues.AF1, - ecnProfile: "DropProfile", - scheduler: "scheduler", - sequence: uint32(1), - priority: oc.Scheduler_Priority_UNSET, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(8), - }, { - desc: "output-interface-AF2", - queueName: queues.AF2, - ecnProfile: "DropProfile", - scheduler: "scheduler", - sequence: uint32(1), - priority: oc.Scheduler_Priority_UNSET, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(16), - }, { - desc: "output-interface-AF3", - queueName: queues.AF3, - ecnProfile: "DropProfile", - scheduler: "scheduler", - sequence: uint32(1), - priority: oc.Scheduler_Priority_UNSET, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(32), - }, { - desc: "output-interface-AF4", - queueName: queues.AF4, - ecnProfile: "DropProfile", - scheduler: "scheduler", - sequence: uint32(0), - priority: oc.Scheduler_Priority_STRICT, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(6), - }, { - desc: "output-interface-NC1", - queueName: queues.NC1, - ecnProfile: "DropProfile", - scheduler: "scheduler", - sequence: uint32(0), - priority: oc.Scheduler_Priority_STRICT, - inputType: oc.Input_InputType_QUEUE, - weight: uint64(7), - }} - - i := q.GetOrCreateInterface(dp.Name()) - i.SetInterfaceId(dp.Name()) - i.GetOrCreateInterfaceRef().Interface = ygot.String(dp.Name()) - if deviations.InterfaceRefConfigUnsupported(dut) { - i.InterfaceRef = nil - } - if deviations.QOSQueueRequiresID(dut) { - queueNames := []string{queues.NC1, queues.AF4, queues.AF3, queues.AF2, queues.AF1, queues.BE0, queues.BE1} - for i, queue := range queueNames { - q1 := q.GetOrCreateQueue(queue) - q1.Name = ygot.String(queue) - queueid := len(queueNames) - i - q1.QueueId = ygot.Uint8(uint8(queueid)) + // DP-1.3.2 Step 3 - Validation with pass/fail criteria: verify each + // wred/uniform state leaf (config leaf on devices without QoS state + // support) using gnmi.Watch/Await via qoscfg.VerifyLeaf. + uniformPath := gnmi.OC().Qos().QueueManagementProfile(profileName).Wred().Uniform() + qoscfg.VerifyLeaf(t, dut, uniformPath.EnableEcn().State(), uniformPath.EnableEcn().Config(), true, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MinThreshold().State(), uniformPath.MinThreshold().Config(), 3276800, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MaxThreshold().State(), uniformPath.MaxThreshold().Config(), 6553600, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MaxDropProbabilityPercent().State(), uniformPath.MaxDropProbabilityPercent().Config(), 100, time.Minute) + if !deviations.DropWeightLeavesUnsupported(dut) { + qoscfg.VerifyLeaf(t, dut, uniformPath.Drop().State(), uniformPath.Drop().Config(), false, time.Minute) } - } + if !deviations.QosSetWeightConfigUnsupported(dut) { + qoscfg.VerifyLeaf(t, dut, uniformPath.Weight().State(), uniformPath.Weight().Config(), wantWeight, time.Minute) + } + + // DP-1.3.2 Step 4 - Validate ECN profile application + outQueuePath := gnmi.OC().Qos().Interface(qoscfg.QosInterfaceID(dut, dp1.Name())).Output().Queue(queueName) + qoscfg.VerifyLeaf(t, dut, outQueuePath.Name().State(), outQueuePath.Name().Config(), queueName, time.Minute) + qoscfg.VerifyLeaf(t, dut, outQueuePath.QueueManagementProfile().State(), outQueuePath.QueueManagementProfile().Config(), profileName, time.Minute) + }) + + t.Run("DP-1.3.3 - Threshold in percentage, min-threshold not-equal max-threshold", func(t *testing.T) { + // DP-1.3.3 Step 1 - Generate DUT configuration + qos := &oc.Root{} + q := qos.GetOrCreateQos() + + profileName := "ECN_PROFILE_3" + queueMgmtProfile := q.GetOrCreateQueueManagementProfile(profileName) + queueMgmtProfile.SetName(profileName) + uniform := queueMgmtProfile.GetOrCreateWred().GetOrCreateUniform() + uniform.SetEnableEcn(true) + uniform.SetDrop(false) + uniform.SetMaxDropProbabilityPercent(100) + uniform.SetMinThresholdPercent(1) + uniform.SetMaxThresholdPercent(2) + if !deviations.QosSetWeightConfigUnsupported(dut) { + uniform.SetWeight(wantWeight) + } + + qoscfg.BuildOutputQueueManagementProfile(dut, q, dp1.Name(), queueName, profileName) - schedulerPolicy := q.GetOrCreateSchedulerPolicy("scheduler") - schedulerPolicy.SetName("scheduler") - t.Logf("qos output interface config cases: %v", cases) - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - qoscfg.SetForwardingGroup(t, dut, q, tc.queueName, tc.queueName) - s := schedulerPolicy.GetOrCreateScheduler(tc.sequence) - s.SetSequence(tc.sequence) - s.SetPriority(tc.priority) - input := s.GetOrCreateInput(tc.queueName) - input.SetId(tc.queueName) - input.SetInputType(tc.inputType) - input.SetQueue(tc.queueName) - input.SetWeight(tc.weight) - output := i.GetOrCreateOutput() - schedulerPolicy := output.GetOrCreateSchedulerPolicy() - schedulerPolicy.SetName(tc.scheduler) - queue := output.GetOrCreateQueue(tc.queueName) - queue.SetQueueManagementProfile(tc.ecnProfile) - queue.SetName(tc.queueName) - if deviations.QOSBufferAllocationConfigRequired(dut) { - bufferAllocation := q.GetOrCreateBufferAllocationProfile("ballocprofile") - bq := bufferAllocation.GetOrCreateQueue(tc.queueName) - bq.SetStaticSharedBufferLimit(uint32(268435456)) - output.SetBufferAllocationProfile("ballocprofile") + // DP-1.3.3 Step 2 - Push configuration to DUT using gNMI Set with REPLACE option. + batch := &gnmi.SetBatch{} + qoscfg.ReplaceQueueManagementProfile(t, dut, batch, q, dp1.Name(), queueName, profileName) + batch.Set(t, dut) + + // DP-1.3.3 Step 3 - Validation with pass/fail criteria: verify each + // wred/uniform state leaf (config leaf on devices without QoS state + // support) using gnmi.Watch/Await via qoscfg.VerifyLeaf. + uniformPath := gnmi.OC().Qos().QueueManagementProfile(profileName).Wred().Uniform() + qoscfg.VerifyLeaf(t, dut, uniformPath.EnableEcn().State(), uniformPath.EnableEcn().Config(), true, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MinThresholdPercent().State(), uniformPath.MinThresholdPercent().Config(), 1, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MaxThresholdPercent().State(), uniformPath.MaxThresholdPercent().Config(), 2, time.Minute) + qoscfg.VerifyLeaf(t, dut, uniformPath.MaxDropProbabilityPercent().State(), uniformPath.MaxDropProbabilityPercent().Config(), 100, time.Minute) + if !deviations.DropWeightLeavesUnsupported(dut) { + qoscfg.VerifyLeaf(t, dut, uniformPath.Drop().State(), uniformPath.Drop().Config(), false, time.Minute) + } + if !deviations.QosSetWeightConfigUnsupported(dut) { + qoscfg.VerifyLeaf(t, dut, uniformPath.Weight().State(), uniformPath.Weight().Config(), wantWeight, time.Minute) + } + + // DP-1.3.3 Step 4 - Validate ECN profile application + outQueuePath := gnmi.OC().Qos().Interface(qoscfg.QosInterfaceID(dut, dp1.Name())).Output().Queue(queueName) + qoscfg.VerifyLeaf(t, dut, outQueuePath.Name().State(), outQueuePath.Name().Config(), queueName, time.Minute) + qoscfg.VerifyLeaf(t, dut, outQueuePath.QueueManagementProfile().State(), outQueuePath.QueueManagementProfile().Config(), profileName, time.Minute) + }) + + t.Run("DP-1.3.4 - Negative Test Cases", func(t *testing.T) { + // DP-1.3.4: each negative test builds its own new QoS config (q), so nothing + // carries over between tests. Tests that need a profile get a complete, valid + // one from newValidECNProfile and change only the leaf under test. + profileName := "ECN_PROFILE_NEG" + outQueuePath := gnmi.OC().Qos().Interface(qoscfg.QosInterfaceID(dut, dp1.Name())).Output().Queue(queueName) + + // DP-1.3.4 Negative Test 1: min-threshold (81920) > max-threshold (40960). + // Verify the gNMI Set is rejected. + t.Run("Negative Test 1 (min > max threshold)", func(t *testing.T) { + if deviations.EcnMinGreaterMaxThresholdUnsupported(dut) { + t.Skip("Device does not support validating min-threshold > max-threshold") } - gnmi.Replace(t, dut, gnmi.OC().Qos().Config(), q) + q := &oc.Qos{} + uniform := newValidECNProfile(dut, q, profileName) + uniform.SetMinThreshold(81920) + uniform.SetMaxThreshold(40960) + // Some devices validate cross-leaf constraints only when the profile is + // attached to a queue, so attach it to queue "0". + qoscfg.BuildOutputQueueManagementProfile(dut, q, dp1.Name(), queueName, profileName) + + // testt.ExpectFatal fails the test if the Set is accepted. + errStr := testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Update(t, dut, gnmi.OC().Qos().Config(), q) + }) + t.Logf("min-threshold > max-threshold rejected as expected: %s", errStr) }) - // Verify the policy is applied by checking the telemetry path state values. - policy := gnmi.OC().Qos().Interface(dp.Name()).Output().SchedulerPolicy() - outQueue := gnmi.OC().Qos().Interface(dp.Name()).Output().Queue(tc.queueName) - if !deviations.StatePathsUnsupported(dut) { - if got, want := gnmi.Get(t, dut, policy.Name().State()), tc.scheduler; got != want { - t.Errorf("policy.Name().State(): got %v, want %v", got, want) - } - if got, want := gnmi.Get(t, dut, outQueue.Name().State()), tc.queueName; got != want { - t.Errorf("outQueue.Name().State(): got %v, want %v", got, want) - } - if got, want := gnmi.Get(t, dut, outQueue.QueueManagementProfile().State()), tc.ecnProfile; got != want { - t.Errorf("outQueue.QueueManagementProfile().State(): got %v, want %v", got, want) - } + // DP-1.3.4 Negative Test 2: max-drop-probability-percent = 101 (out of + // range). Verify the gNMI Set is rejected. + t.Run("Negative Test 2 (invalid max-drop-prob)", func(t *testing.T) { + q := &oc.Qos{} + newValidECNProfile(dut, q, profileName).SetMaxDropProbabilityPercent(101) + + errStr := testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Update(t, dut, gnmi.OC().Qos().Config(), q) + }) + t.Logf("max-drop-probability-percent 101 rejected as expected: %s", errStr) + }) + + // DP-1.3.4 Negative Test 3: assign non-existent profile "BOGUS_PROFILE" to + // queue "0". Verify the gNMI Set is rejected. + t.Run("Negative Test 3 (non-existent profile)", func(t *testing.T) { + q := &oc.Qos{} + qoscfg.BuildOutputQueueManagementProfile(dut, q, dp1.Name(), queueName, "BOGUS_PROFILE") + + errStr := testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Update(t, dut, gnmi.OC().Qos().Config(), q) + }) + t.Logf("Non-existent profile assignment rejected as expected: %s", errStr) + }) + + // DP-1.3.4 Negative Test 4: apply a valid profile to queue "0", then attempt + // to delete it while it is still applied. Verify the deletion is rejected. + t.Run("Negative Test 4 (delete active profile)", func(t *testing.T) { + // The profile is created here (same values as DP-1.3.2) so this test still + // works if DP-1.3.2 failed. + activeProfile := "ECN_PROFILE_2" + + // DP-1.3.4 Negative Test 4 - "Apply a valid queue-management-profile to + // an interface's queue". + q := &oc.Qos{} + newValidECNProfile(dut, q, activeProfile) + qoscfg.BuildOutputQueueManagementProfile(dut, q, dp1.Name(), queueName, activeProfile) + batch := &gnmi.SetBatch{} + qoscfg.ReplaceQueueManagementProfile(t, dut, batch, q, dp1.Name(), queueName, activeProfile) + batch.Set(t, dut) + qoscfg.VerifyLeaf(t, dut, outQueuePath.QueueManagementProfile().State(), outQueuePath.QueueManagementProfile().Config(), activeProfile, time.Minute) + + // DP-1.3.4 Negative Test 4 - "attempt to delete that queue-management-profile + // while it is still actively applied ... Verify the deletion is rejected." + errStr := testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Delete(t, dut, gnmi.OC().Qos().QueueManagementProfile(activeProfile).Config()) + }) + t.Logf("Deletion of active profile %s rejected as expected: %s", activeProfile, errStr) + + // DP-1.3.4 Negative Test 4: the rejected delete must leave the profile applied. + qoscfg.VerifyLeaf(t, dut, outQueuePath.QueueManagementProfile().State(), outQueuePath.QueueManagementProfile().Config(), activeProfile, time.Minute) + }) + }) + + t.Run("DP-1.3.5 - Teardown and Cleanup Verification", func(t *testing.T) { + // DP-1.3.5 Step 1: Detach profile from interface + qosIntfID := qoscfg.QosInterfaceID(dut, dp1.Name()) + qoscfg.DetachOutputQueueManagementProfile(t, dut, dp1.Name(), queueName) + + // DP-1.3.5 Step 2: Validate profile is detached + outQueuePath := gnmi.OC().Qos().Interface(qosIntfID).Output().Queue(queueName) + allProfiles := []string{"ECN_PROFILE_1", "ECN_PROFILE_2", "ECN_PROFILE_3"} + qoscfg.VerifyLeafDetached(t, dut, outQueuePath.QueueManagementProfile().State(), outQueuePath.QueueManagementProfile().Config(), allProfiles, time.Minute) + + // DP-1.3.5 Step 3: Delete profile globally + for _, profileName := range allProfiles { + gnmi.Delete(t, dut, gnmi.OC().Qos().QueueManagementProfile(profileName).Config()) } - if deviations.QosGetStatePathUnsupported(dut) { - if got, want := gnmi.Get(t, dut, policy.Name().Config()), tc.scheduler; got != want { - t.Errorf("policy.Name().Config(): got %v, want %v", got, want) - } - if got, want := gnmi.Get(t, dut, outQueue.Name().Config()), tc.queueName; got != want { - t.Errorf("outQueue.Name().Config(): got %v, want %v", got, want) - } - if got, want := gnmi.Get(t, dut, outQueue.QueueManagementProfile().Config()), tc.ecnProfile; got != want { - t.Errorf("outQueue.QueueManagementProfile().Config(): got %v, want %v", got, want) - } + + // DP-1.3.5 Step 4: Validate profile is removed + for _, profileName := range allProfiles { + profilePath := gnmi.OC().Qos().QueueManagementProfile(profileName) + qoscfg.VerifyLeafRemoved(t, dut, profilePath.State(), profilePath.Config(), time.Minute) } - } + + // DP-1.3.5 complete: the t.Cleanup safety net is not needed. + teardownVerified = !t.Failed() + }) } diff --git a/internal/deviations/deviations.go b/internal/deviations/deviations.go index d10a206696a..06760f74fa7 100644 --- a/internal/deviations/deviations.go +++ b/internal/deviations/deviations.go @@ -2502,3 +2502,9 @@ func CfmPmFt(dut *ondatra.DUTDevice) string { func QosRedUnsupported(dut *ondatra.DUTDevice) bool { return lookupDUTDeviations(dut).GetDropWeightLeavesUnsupported() } + +// EcnMinGreaterMaxThresholdUnsupported returns true if the device does not +// validate that max-threshold is strictly greater than min-threshold. +func EcnMinGreaterMaxThresholdUnsupported(dut *ondatra.DUTDevice) bool { + return lookupDUTDeviations(dut).GetEcnMinGreaterMaxThresholdUnsupported() +} diff --git a/internal/gnoi/gnoi.go b/internal/gnoi/gnoi.go index 12c1433df96..464f5c694d8 100644 --- a/internal/gnoi/gnoi.go +++ b/internal/gnoi/gnoi.go @@ -21,6 +21,7 @@ import ( "testing" "time" + "github.com/openconfig/featureprofiles/internal/components" "github.com/openconfig/featureprofiles/internal/deviations" "github.com/openconfig/featureprofiles/internal/system" gpb "github.com/openconfig/gnmi/proto/gnmi" @@ -28,6 +29,7 @@ import ( "github.com/openconfig/ondatra" "github.com/openconfig/ondatra/gnmi" "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/testt" "github.com/openconfig/ygnmi/ygnmi" ) @@ -168,3 +170,54 @@ func RestartRoutingProcess(t *testing.T, dut *ondatra.DUTDevice) { } KillProcess(t, dut, ROUTING, SigTerm, true, true) } + +// SwitchControlProcessor triggers a switchover to the standby controller card +// following the gNOI-3.3 supervisor switchover pattern. It returns false +// without sending any RPC when the DUT reports fewer than two controller cards, +// since a switchover is not possible. On success it returns true together with +// the names of the previously active and previously standby controller cards. +func SwitchControlProcessor(t *testing.T, dut *ondatra.DUTDevice) (switched bool, prevActive, prevStandby string) { + t.Helper() + cards := components.FindComponentsByType(t, dut, oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_CONTROLLER_CARD) + if len(cards) < 2 { + t.Logf("DUT reports %d controller card(s) %v; supervisor switchover is not applicable", len(cards), cards) + return false, "", "" + } + standby, active := components.FindStandbyControllerCard(t, dut, cards) + gnmi.Await(t, dut, gnmi.OC().Component(active).SwitchoverReady().State(), 30*time.Minute, true) + req := &spb.SwitchControlProcessorRequest{ + ControlProcessor: components.GetSubcomponentPath(standby, deviations.GNOISubcomponentPath(dut)), + } + t.Logf("SwitchControlProcessorRequest: %v", req) + if _, err := dut.RawAPIs().GNOI(t).System().SwitchControlProcessor(context.Background(), req); err != nil { + t.Fatalf("SwitchControlProcessor(%v) failed: %v", req, err) + } + t.Logf("Successfully triggered SwitchControlProcessor from %q to %q", active, standby) + return true, active, standby +} + +// switchoverRetryBackoff is the delay before re-subscribing after the gNMI +// stream is reset while control processors converge after a switchover. +const switchoverRetryBackoff = 30 * time.Second + +// AwaitSwitchoverReady waits up to timeout for component's switchover-ready +// state to be true. The gNMI stream may be reset while control processors +// converge after a switchover, so a failed Await is retried after +// switchoverRetryBackoff until timeout expires. +func AwaitSwitchoverReady(t *testing.T, dut *ondatra.DUTDevice, component string, timeout time.Duration) { + t.Helper() + deadline := time.Now().Add(timeout) + for { + errMsg := testt.CaptureFatal(t, func(t testing.TB) { + gnmi.Await(t, dut, gnmi.OC().Component(component).SwitchoverReady().State(), time.Until(deadline), true) + }) + if errMsg == nil { + return + } + if time.Until(deadline) <= switchoverRetryBackoff { + t.Fatalf("Component %s not switchover-ready within %v: %s", component, timeout, *errMsg) + } + t.Logf("Waiting for %s switchover-ready interrupted, retrying: %s", component, *errMsg) + time.Sleep(switchoverRetryBackoff) + } +} diff --git a/internal/qoscfg/qoscfg.go b/internal/qoscfg/qoscfg.go index 9bdede89d1e..4873d9ebcd2 100644 --- a/internal/qoscfg/qoscfg.go +++ b/internal/qoscfg/qoscfg.go @@ -16,12 +16,16 @@ package qoscfg import ( + "fmt" "testing" + "time" "github.com/openconfig/featureprofiles/internal/deviations" "github.com/openconfig/ondatra" "github.com/openconfig/ondatra/gnmi" "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygnmi/ygnmi" + "github.com/openconfig/ygot/ygot" ) // SetForwardingGroup sets a forwarding group in the specified QoS config. @@ -29,24 +33,338 @@ func SetForwardingGroup(t *testing.T, dut *ondatra.DUTDevice, qos *oc.Qos, group t.Helper() qos.GetOrCreateForwardingGroup(groupName).SetOutputQueue(queueName) qos.GetOrCreateQueue(queueName) - gnmi.Replace(t, dut, gnmi.OC().Qos().Config(), qos) + gnmi.Update(t, dut, gnmi.OC().Qos().Config(), qos) } // SetInputClassifier sets an input classifier in the specified QoS config. func SetInputClassifier(t *testing.T, dut *ondatra.DUTDevice, qos *oc.Qos, intfID string, classType oc.E_Input_Classifier_Type, className string) { t.Helper() - qosIntfID := intfID + qosIntfID := QosClassifierInterfaceID(dut, intfID) + intf := qos.GetOrCreateInterface(qosIntfID) + intf.SetInterfaceId(qosIntfID) + intf.GetOrCreateInterfaceRef().SetInterface(intfID) + if !deviations.QosSchedulerConfigRequired(dut) { + intf.GetOrCreateInterfaceRef().SetSubinterface(0) + } + if deviations.InterfaceRefConfigUnsupported(dut) { + intf.InterfaceRef = nil + } + intf.GetOrCreateInput().GetOrCreateClassifier(classType).SetName(className) + gnmi.Update(t, dut, gnmi.OC().Qos().Config(), qos) +} + +// ConfigureIPv4DSCPClassifier adds IPv4 classifier className to qos with one +// term that matches dscp and, if targetGroup is non-empty, sends matching +// packets to that forwarding group. It only edits qos; nothing is pushed to +// the DUT. On DUTs with deviations.QOSQueueRequiresID the term ID is "0" and +// the match uses dscp-set instead of dscp. +func ConfigureIPv4DSCPClassifier(t *testing.T, dut *ondatra.DUTDevice, qos *oc.Qos, className string, targetGroup string, dscp uint8) { + t.Helper() + classifier := qos.GetOrCreateClassifier(className) + classifier.SetName(className) + classifier.SetType(oc.Qos_Classifier_Type_IPV4) + termID := "term1" + if deviations.QOSQueueRequiresID(dut) { + termID = "0" + } + term := classifier.GetOrCreateTerm(termID) + term.SetId(termID) + if deviations.QOSQueueRequiresID(dut) { + term.GetOrCreateConditions().GetOrCreateIpv4().SetDscpSet([]uint8{dscp}) + } else { + term.GetOrCreateConditions().GetOrCreateIpv4().SetDscp(dscp) + } + if targetGroup != "" { + term.GetOrCreateActions().SetTargetGroup(targetGroup) + } +} + +// QosInterfaceID returns the qos/interfaces list key for intfID: intfID +// itself, or ".0" on DUTs with deviations.InterfaceRefInterfaceIDFormat. +func QosInterfaceID(dut *ondatra.DUTDevice, intfID string) string { if deviations.InterfaceRefInterfaceIDFormat(dut) { - qosIntfID += ".0" + return intfID + ".0" } + return intfID +} + +// QosClassifierInterfaceID returns the qos/interfaces list key that +// SetInputClassifier uses for intfID. It matches QosInterfaceID, except that +// DUTs with deviations.QOSBufferAllocationConfigRequired also use ".0", +// because they accept input classifiers only on the subinterface. +func QosClassifierInterfaceID(dut *ondatra.DUTDevice, intfID string) string { + if deviations.InterfaceRefInterfaceIDFormat(dut) || deviations.QOSBufferAllocationConfigRequired(dut) { + return intfID + ".0" + } + return intfID +} + +// BuildOutputQueueManagementProfile edits qos so that output queue queueName +// on interface intfID uses queue management profile profileName. If +// profileName is empty, the queue is created without a profile. It only edits +// qos; add it to a SetBatch with ReplaceQueueManagementProfile. +// +// It also adds the config some DUTs require before a profile can be attached: +// - deviations.QOSQueueRequiresID: adds queueName to the top-level queue list +// with queue ID 0. +// - deviations.QOSBufferAllocationConfigRequired: creates buffer allocation +// profile "ballocprofile" and attaches it to the interface output. +// - deviations.QosSchedulerConfigRequired: adds queue "7" and scheduler +// policy "scheduler" (queue "7" strict priority, queueName weight 1) and +// attaches the policy to the interface output. +func BuildOutputQueueManagementProfile(dut *ondatra.DUTDevice, qos *oc.Qos, intfID, queueName, profileName string) { + qosIntfID := QosInterfaceID(dut, intfID) intf := qos.GetOrCreateInterface(qosIntfID) + intf.SetInterfaceId(qosIntfID) intf.GetOrCreateInterfaceRef().SetInterface(intfID) - if dut.Vendor() != ondatra.CISCO { + if deviations.InterfaceRefInterfaceIDFormat(dut) { intf.GetOrCreateInterfaceRef().SetSubinterface(0) } if deviations.InterfaceRefConfigUnsupported(dut) { intf.InterfaceRef = nil } - intf.GetOrCreateInput().GetOrCreateClassifier(classType).SetName(className) - gnmi.Replace(t, dut, gnmi.OC().Qos().Config(), qos) + + if deviations.QOSQueueRequiresID(dut) { + q1 := qos.GetOrCreateQueue(queueName) + q1.Name = ygot.String(queueName) + q1.QueueId = ygot.Uint8(0) + } + + queue := intf.GetOrCreateOutput().GetOrCreateQueue(queueName) + queue.SetName(queueName) + queue.Name = ygot.String(queueName) + if profileName != "" { + queue.SetQueueManagementProfile(profileName) + } + if deviations.QOSBufferAllocationConfigRequired(dut) { + bufferAllocationProfile := qos.GetOrCreateBufferAllocationProfile("ballocprofile") + bufferAllocationQueue := bufferAllocationProfile.GetOrCreateQueue(queueName) + bufferAllocationQueue.SetStaticSharedBufferLimit(uint32(268435456)) + intf.GetOrCreateOutput().SetBufferAllocationProfile("ballocprofile") + } + if deviations.QosSchedulerConfigRequired(dut) { + q7 := qos.GetOrCreateQueue("7") + q7.Name = ygot.String("7") + q7.QueueId = ygot.Uint8(7) + + outQ7 := intf.GetOrCreateOutput().GetOrCreateQueue("7") + outQ7.SetName("7") + + schedulerPolicy := qos.GetOrCreateSchedulerPolicy("scheduler") + schedulerPolicy.SetName("scheduler") + + s0 := schedulerPolicy.GetOrCreateScheduler(0) + s0.SetSequence(0) + s0.SetPriority(oc.Scheduler_Priority_STRICT) + in0 := s0.GetOrCreateInput("7") + in0.SetId("7") + in0.SetInputType(oc.Input_InputType_QUEUE) + in0.SetQueue("7") + in0.SetWeight(7) + + s1 := schedulerPolicy.GetOrCreateScheduler(1) + s1.SetSequence(1) + s1.SetPriority(oc.Scheduler_Priority_UNSET) + in1 := s1.GetOrCreateInput(queueName) + in1.SetId(queueName) + in1.SetInputType(oc.Input_InputType_QUEUE) + in1.SetQueue(queueName) + in1.SetWeight(1) + + intf.GetOrCreateOutput().GetOrCreateSchedulerPolicy().SetName("scheduler") + } +} + +// ReplaceQueueManagementProfile appends to batch the operations that push qos +// to the DUT in one gNMI SetRequest. The caller sends it with batch.Set: +// - REPLACE queue management profile profileName. +// - UPDATE (merge) the rest of qos, including output queue queueName on +// interface intfID. +// +// Only the profile subtree is replaced, so each test case gets a clean profile +// without deleting the classifier, forwarding group and queues created during +// test setup, and without deleting pre-existing system queues on the interface. +// The UPDATE is built from a copy of qos without the replaced profile subtree, +// so no path is sent twice in the SetRequest. qos itself is not modified; call +// batch.Set before editing qos again. +// +// Call BuildOutputQueueManagementProfile with the same arguments first. The +// test fails if qos does not contain the profile or the output queue. +func ReplaceQueueManagementProfile(t *testing.T, dut *ondatra.DUTDevice, batch *gnmi.SetBatch, qos *oc.Qos, intfID, queueName, profileName string) { + t.Helper() + qosIntfID := QosInterfaceID(dut, intfID) + + // Fail on missing subtrees: replacing a path with an empty value would + // delete the existing device config at that path. + profile := qos.QueueManagementProfile[profileName] + if profile == nil { + t.Fatalf("ReplaceQueueManagementProfile: qos does not describe queue management profile %q", profileName) + } + intf := qos.Interface[qosIntfID] + if intf == nil || intf.Output == nil || intf.Output.Queue[queueName] == nil { + t.Fatalf("ReplaceQueueManagementProfile: qos does not describe output queue %q on interface %q", queueName, qosIntfID) + } + + // Remove the replaced subtree from a copy rather than from qos: SetBatch + // marshals values only when Set is called, so a temporary edit of qos that + // is restored before then would have no effect. + c, err := ygot.DeepCopy(qos) + if err != nil { + t.Fatalf("ReplaceQueueManagementProfile: cannot copy qos: %v", err) + } + rest := c.(*oc.Qos) + delete(rest.QueueManagementProfile, profileName) + // Drop lists and containers left empty by the removal: some devices reject + // empty JSON lists (e.g. "queue-management-profile": []) in a SetRequest. + if len(rest.QueueManagementProfile) == 0 { + rest.QueueManagementProfile = nil + } + ygot.PruneEmptyBranches(rest) + + // On DUTs with QosSchedulerConfigRequired, swapping one queue-management-profile + // for another while the output scheduler-policy remains attached causes the + // device to synthesize a second egress service-policy in the same commit + // ("Invalid multi policy type in egress"). Remove the existing output and + // scheduler-policy first so the subsequent batch attaches a single egress policy. + if deviations.QosSchedulerConfigRequired(dut) { + schedPath := gnmi.OC().Qos().Interface(qosIntfID).Output().SchedulerPolicy().Name().Config() + if gnmi.LookupConfig(t, dut.GNMIOpts().WithYGNMIOpts(ygnmi.WithUseGet()), schedPath).IsPresent() { + clearBatch := &gnmi.SetBatch{} + gnmi.BatchDelete(clearBatch, gnmi.OC().Qos().Interface(qosIntfID).Output().Config()) + gnmi.BatchDelete(clearBatch, gnmi.OC().Qos().SchedulerPolicy("scheduler").Config()) + clearBatch.Set(t, dut) + } + } + + gnmi.BatchReplace(batch, gnmi.OC().Qos().QueueManagementProfile(profileName).Config(), profile) + gnmi.BatchUpdate(batch, gnmi.OC().Qos().Config(), rest) +} + +// DetachOutputQueueManagementProfile detaches the queue management profile from +// output queue queueName on interface intfID. On DUTs with +// deviations.QosSchedulerConfigRequired it also removes the interface output +// and scheduler policy added by BuildOutputQueueManagementProfile. +func DetachOutputQueueManagementProfile(t testing.TB, dut *ondatra.DUTDevice, intfID, queueName string) { + t.Helper() + qosIntfID := QosInterfaceID(dut, intfID) + if deviations.QosSchedulerConfigRequired(dut) { + batch := &gnmi.SetBatch{} + gnmi.BatchDelete(batch, gnmi.OC().Qos().Interface(qosIntfID).Output().Config()) + gnmi.BatchDelete(batch, gnmi.OC().Qos().SchedulerPolicy("scheduler").Config()) + batch.Set(t, dut) + return + } + gnmi.Delete(t, dut, gnmi.OC().Qos().Interface(qosIntfID).Output().Queue(queueName).QueueManagementProfile().Config()) +} + +// ResyncOutputQueueManagementProfile re-establishes the DUT's internal +// OpenConfig-to-native QoS policy mapping after a control processor switchover +// on DUTs with deviations.QosSchedulerConfigRequired, and then detaches the +// output policy so subsequent test cases can attach a new profile cleanly. +func ResyncOutputQueueManagementProfile(t *testing.T, dut *ondatra.DUTDevice, qos *oc.Qos, intfID, queueName, profileName string) { + t.Helper() + if !deviations.QosSchedulerConfigRequired(dut) { + return + } + DetachOutputQueueManagementProfile(t, dut, intfID, queueName) + gnmi.Delete(t, dut, gnmi.OC().Qos().QueueManagementProfile(profileName).Config()) + + batch := &gnmi.SetBatch{} + ReplaceQueueManagementProfile(t, dut, batch, qos, intfID, queueName, profileName) + batch.Set(t, dut) + + DetachOutputQueueManagementProfile(t, dut, intfID, queueName) +} + +// ConfigureInterfaceSetup merges (gNMI UPDATE) an ethernetCsmacd interface +// config for port dp onto the DUT. It also creates subinterface 0, except on +// DUTs with deviations.QosSchedulerConfigRequired. +func ConfigureInterfaceSetup(t *testing.T, dut *ondatra.DUTDevice, dp *ondatra.Port) { + t.Helper() + intfConfig := &oc.Interface{Name: ygot.String(dp.Name())} + intfConfig.SetType(oc.IETFInterfaces_InterfaceType_ethernetCsmacd) + if !deviations.QosSchedulerConfigRequired(dut) { + intfConfig.GetOrCreateSubinterface(0).SetIndex(0) + } + gnmi.Update(t, dut, gnmi.OC().Interface(dp.Name()).Config(), intfConfig) +} + +// qosStateUnsupported reports whether the DUT lacks QoS state paths, in which +// case the matching config paths are read instead. +func qosStateUnsupported(dut *ondatra.DUTDevice) bool { + return deviations.QosGetStatePathUnsupported(dut) || deviations.StatePathsUnsupported(dut) +} + +// configPollInterval is the delay between config path reads in verifyLeaf. +const configPollInterval = 2 * time.Second + +// verifyLeaf waits up to timeout for a QoS leaf to satisfy pred, and reports a +// test error showing want as the expected value if it does not. +// +// By default it watches the state path. On DUTs without QoS state support (see +// qosStateUnsupported) it instead polls the config path with gNMI Get every +// configPollInterval. Get is used because some devices do not serve +// config-only paths over Subscribe, and polling allows for slow convergence, +// such as after a control processor switchover. +func verifyLeaf[T any](t testing.TB, dut *ondatra.DUTDevice, state ygnmi.SingletonQuery[T], config ygnmi.ConfigQuery[T], timeout time.Duration, want string, pred func(*ygnmi.Value[T]) bool) { + t.Helper() + if qosStateUnsupported(dut) { + deadline := time.Now().Add(timeout) + var got *ygnmi.Value[T] + for { + got = gnmi.LookupConfig(t, dut.GNMIOpts().WithYGNMIOpts(ygnmi.WithUseGet()), config) + if pred(got) { + return + } + if !time.Now().Before(deadline) { + break + } + time.Sleep(configPollInterval) + } + t.Errorf("verifyLeaf(%v): got %v, want %s", config, got, want) + return + } + got, ok := gnmi.Watch(t, dut, state, timeout, pred).Await(t) + if !ok { + t.Errorf("verifyLeaf(%v): got %v, want %s", state, got, want) + } +} + +// VerifyLeaf checks that a QoS leaf equals want within timeout. It reads the +// state path, or the config path on DUTs without QoS state support. +func VerifyLeaf[T comparable](t testing.TB, dut *ondatra.DUTDevice, state ygnmi.SingletonQuery[T], config ygnmi.ConfigQuery[T], want T, timeout time.Duration) { + t.Helper() + verifyLeaf(t, dut, state, config, timeout, fmt.Sprintf("%v", want), func(v *ygnmi.Value[T]) bool { + got, present := v.Val() + return present && got == want + }) +} + +// VerifyLeafRemoved checks that a QoS leaf has no value within timeout. It +// reads the same state or config path as VerifyLeaf. +func VerifyLeafRemoved[T comparable](t testing.TB, dut *ondatra.DUTDevice, state ygnmi.SingletonQuery[T], config ygnmi.ConfigQuery[T], timeout time.Duration) { + t.Helper() + verifyLeaf(t, dut, state, config, timeout, "no value", func(v *ygnmi.Value[T]) bool { + return !v.IsPresent() + }) +} + +// VerifyLeafDetached checks that a QoS leaf, within timeout, is either unset or +// set to a value not in unwanted. It reads the same state or config path as +// VerifyLeaf. Use it instead of VerifyLeafRemoved when a removed leaf may fall +// back to a default value rather than disappear. +func VerifyLeafDetached[T comparable](t testing.TB, dut *ondatra.DUTDevice, state ygnmi.SingletonQuery[T], config ygnmi.ConfigQuery[T], unwanted []T, timeout time.Duration) { + t.Helper() + verifyLeaf(t, dut, state, config, timeout, fmt.Sprintf("no value or a value other than %v", unwanted), func(v *ygnmi.Value[T]) bool { + got, present := v.Val() + if !present { + return true + } + for _, u := range unwanted { + if got == u { + return false + } + } + return true + }) } diff --git a/proto/metadata.proto b/proto/metadata.proto index b4aea364469..c81c80080a8 100644 --- a/proto/metadata.proto +++ b/proto/metadata.proto @@ -1563,6 +1563,15 @@ message Metadata { // qos_red_unsupported specifies if device does not support RED under // queue-management-profiles. bool qos_red_unsupported = 475; + + // Devices that do not validate that max-threshold is strictly greater than + // min-threshold, so a min > max configuration is accepted instead of + // rejected. + // Arista: https://partnerissuetracker.corp.google.com/issues/573059610 + // Nokia: https://partnerissuetracker.corp.google.com/issues/573067365 + // Cisco: https://partnerissuetracker.corp.google.com/issues/573074330 + bool ecn_min_greater_max_threshold_unsupported = 476; + // Reserved field numbers and identifiers. reserved 84, 9, 28, 20, 38, 43, 90, 97, 55, 89, 19, 36, 35, 40, 113, 131, 141, 173, 234, 254, 231, 300, 241, 49; } diff --git a/proto/metadata_go_proto/metadata.pb.go b/proto/metadata_go_proto/metadata.pb.go index 3ccd4fa2cfa..1a87eeb5d42 100644 --- a/proto/metadata_go_proto/metadata.pb.go +++ b/proto/metadata_go_proto/metadata.pb.go @@ -1638,8 +1638,15 @@ type Metadata_Deviations struct { // qos_red_unsupported specifies if device does not support RED under // queue-management-profiles. QosRedUnsupported bool `protobuf:"varint,475,opt,name=qos_red_unsupported,json=qosRedUnsupported,proto3" json:"qos_red_unsupported,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Devices that do not validate that max-threshold is strictly greater than + // min-threshold, so a min > max configuration is accepted instead of + // rejected. + // Arista: https://partnerissuetracker.corp.google.com/issues/573059610 + // Nokia: https://partnerissuetracker.corp.google.com/issues/573067365 + // Cisco: https://partnerissuetracker.corp.google.com/issues/573074330 + EcnMinGreaterMaxThresholdUnsupported bool `protobuf:"varint,476,opt,name=ecn_min_greater_max_threshold_unsupported,json=ecnMinGreaterMaxThresholdUnsupported,proto3" json:"ecn_min_greater_max_threshold_unsupported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Metadata_Deviations) Reset() { @@ -4745,6 +4752,13 @@ func (x *Metadata_Deviations) GetQosRedUnsupported() bool { return false } +func (x *Metadata_Deviations) GetEcnMinGreaterMaxThresholdUnsupported() bool { + if x != nil { + return x.EcnMinGreaterMaxThresholdUnsupported + } + return false +} + type Metadata_PlatformExceptions struct { state protoimpl.MessageState `protogen:"open.v1"` Platform *Metadata_Platform `protobuf:"bytes,1,opt,name=platform,proto3" json:"platform,omitempty"` @@ -4801,7 +4815,7 @@ var File_metadata_proto protoreflect.FileDescriptor const file_metadata_proto_rawDesc = "" + "\n" + - "\x0emetadata.proto\x12\x12openconfig.testing\x1a1github.com/openconfig/ondatra/proto/testbed.proto\"\x9e\x8c\x02\n" + + "\x0emetadata.proto\x12\x12openconfig.testing\x1a1github.com/openconfig/ondatra/proto/testbed.proto\"\xf8\x8c\x02\n" + "\bMetadata\x12\x12\n" + "\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x17\n" + "\aplan_id\x18\x02 \x01(\tR\x06planId\x12 \n" + @@ -4813,7 +4827,7 @@ const file_metadata_proto_rawDesc = "" + "\bPlatform\x12.\n" + "\x06vendor\x18\x01 \x01(\x0e2\x16.ondatra.Device.VendorR\x06vendor\x120\n" + "\x14hardware_model_regex\x18\x03 \x01(\tR\x12hardwareModelRegex\x124\n" + - "\x16software_version_regex\x18\x04 \x01(\tR\x14softwareVersionRegexJ\x04\b\x02\x10\x03R\x0ehardware_model\x1a\x86\x81\x02\n" + + "\x16software_version_regex\x18\x04 \x01(\tR\x14softwareVersionRegexJ\x04\b\x02\x10\x03R\x0ehardware_model\x1a\xe0\x81\x02\n" + "\n" + "Deviations\x120\n" + "\x14ipv4_missing_enabled\x18\x01 \x01(\bR\x12ipv4MissingEnabled\x129\n" + @@ -5259,7 +5273,8 @@ const file_metadata_proto_rawDesc = "" + "\tcfm_pm_ft\x18\xd8\x03 \x01(\tR\acfmPmFt\x128\n" + "\x18qos_queue_index_required\x18\xd9\x03 \x01(\bR\x15qosQueueIndexRequired\x12=\n" + "\x1bqos_max_burst_size_required\x18\xda\x03 \x01(\bR\x17qosMaxBurstSizeRequired\x12/\n" + - "\x13qos_red_unsupported\x18\xdb\x03 \x01(\bR\x11qosRedUnsupportedJ\x06\b\x85\x02\x10\x86\x02J\x04\bT\x10UJ\x04\b\t\x10\n" + + "\x13qos_red_unsupported\x18\xdb\x03 \x01(\bR\x11qosRedUnsupported\x12X\n" + + ")ecn_min_greater_max_threshold_unsupported\x18\xdc\x03 \x01(\bR$ecnMinGreaterMaxThresholdUnsupportedJ\x06\b\x85\x02\x10\x86\x02J\x04\bT\x10UJ\x04\b\t\x10\n" + "J\x04\b\x1c\x10\x1dJ\x04\b\x14\x10\x15J\x04\b&\x10'J\x04\b+\x10,J\x04\bZ\x10[J\x04\ba\x10bJ\x04\b7\x108J\x04\bY\x10ZJ\x04\b\x13\x10\x14J\x04\b$\x10%J\x04\b#\x10$J\x04\b(\x10)J\x04\bq\x10rJ\x06\b\x83\x01\x10\x84\x01J\x06\b\x8d\x01\x10\x8e\x01J\x06\b\xad\x01\x10\xae\x01J\x06\b\xea\x01\x10\xeb\x01J\x06\b\xfe\x01\x10\xff\x01J\x06\b\xe7\x01\x10\xe8\x01J\x06\b\xac\x02\x10\xad\x02J\x06\b\xf1\x01\x10\xf2\x01J\x04\b1\x102\x1a\xa0\x01\n" + "\x12PlatformExceptions\x12A\n" + "\bplatform\x18\x01 \x01(\v2%.openconfig.testing.Metadata.PlatformR\bplatform\x12G\n" +