diff --git a/feature/gnmi/tests/union_replace_test/README.md b/feature/gnmi/tests/union_replace_test/README.md index a3b3d14acdd..c52ed081c1b 100644 --- a/feature/gnmi/tests/union_replace_test/README.md +++ b/feature/gnmi/tests/union_replace_test/README.md @@ -2,181 +2,290 @@ ## Summary -Perform a series of tests that validate the union_replace specification. +The tests defined here will cover the basic operation of `union_replace` to +add, remove and change configuration and the scenarios defined in [gNMI +union_replace section +5.3](https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-union_replace.md#53-union-behavior-options). +The goal is to catch bugs related to add/remove/change actions and avoid the +complexity of attempting to test all combinations of features and their +configuration items. + +In depth configuration scenarios for the full landscape of configuration data +will not be covered in tests of the union_replace feature. Instead, in depth +configuration will be covered within the tests for the given feature. For +example, detailed coverage of BGP configuration with OC and CLI will be covered +in the [featureprofiles BGP feature +folder](https://github.com/openconfig/featureprofiles/tree/main/feature/bgp). + +### Baseline DUT management configuration + +`union_replace` causes the entire device configuration to be overwritten. This +is a problem for featureprofiles because there is an assumption that a +“baseline configuration” is present on the DUT, allowing its management +interface(s) to be reachable to the ondatra test runner. This baseline +configuration varies per environment (eg: at vendors and at Google) and is not +specified by the featureprofiles code. A featureprpfiles test with +`union_replace` must have or gain knowledge of the “baseline configuration” so +it can be safely replaced without disrupting management connectivity. + +This test uses ssh to to obtain the baseline configuration in CLI format and +merge that with the union_replace configuration generated by the test. + +## gNMI-3.1 - Idempotent configuration + +Verify the same configuration as already on the device can be pushed and +accepted without changing the configuration. -In depth tests for use of union_replace should reside within the features being -tested. +Steps -## Testbed type +* Get baseline configuration +* Push configuration baseline + A to the DUT +* Get configuration A.1 +* Verify A.1 == baseline + A +* Push configuration baseline + A to the DUT +* Get configuration A.1 +* Verify A.1 == baseline + A -* [Single DUT testbed](https://github.com/openconfig/featureprofiles/blob/main/topologies/dut.testbed) +## gNMI-3.2 - union_replace add configuration -## Procedure +Generate a configuration for interface MTU and add that to the baseline +configuration using union_replace. -### Test environment setup +## gnmi-3.2.1 - Add an interface using OC to the baseline configuration -* Get the DUT configuriation in CLI format and store as `baseline_cfg`. +Steps -* gNMI-3 should be a single _test.go file which contains a series of test functions which may be run in any order or individually. +* Get the baseline configuration (A). +* Generate a configuration with a new interface using OC (A.1). +* Push configuration A + A.1 to the DUT. +* Get configuration as A.2 +* Verify A.2 == A + A.1 -### gNMI-3.1 - Idempotent configuration -Verify the same configuration as already on the device can be pushed and accepted without changing the configuration. +## gnmi-3.2.2 - add the interface configuration using CLI -Steps -Get baseline configuration -Push configuration baseline + A to the DUT -Get configuration A.1 -Verify A.1 == baseline + A -Push configuration baseline + A to the DUT -Get configuration A.1 -Verify A.1 == baseline + A - -### gNMI-3.2 - union_replace add configuration -Generate an interface configuration and add that to the baseline configuration using union_replace. The interface should include description, MTU, and IP address. -#### gNMI-3.2.1 - Add an interface configuration using OC to the baseline (CLI) configuration. - -1. Get the baseline configuration (A). -1. Generate a configuration with a new interface using OC (A.1). -1. Push configuration A + A.1 to the DUT. -1. Get configuration as A.2 -1. Verify A.2 == A + A.1 - -#### gNMI-3.2.2 - Add the interface configuration using CLI Repeat steps in gnmi-3.2.1 but use CLI for the added interface configuration. -### gNMI-3.3 - union_replace change configuration -#### gNMI-3.3.1 - Change the interface description using OC. -Steps -1. Get baseline configuration (B). -1. Change the description of an interface using OC. (B.1) -1. Push configuration baseline + B.1 to the DUT. -1. Get configuration as B.2 -1. Verify B.2 == B + B.1 +## gNMI-3.3 - union_replace change configuration -#### gNMI-3.3.2 - Repeat gNMI-3.3.1 but change only the interface description using CLI. -### gNMI-3.4 - union_replace delete configuration through omission -#### gNMI-3.4.1 - Remove an interface by omitting it in OC. +### gnmi-3.3.1 Change the interface description using OC Steps -1. Get baseline configuration (B). -1. Generate OC configuration adding interfaces 1 and 2 (B.1). -1. Push configuration baseline + B.1 to the DUT. -1. Get configuration as B.2 -1. Verify B.2 == B + B.1 -1. Generate OC configuration adding only interface 1 (B.3). -1. Push configuration baseline + B.3 to the DUT. -1. Get configuration as B.4 -1. Verify B.4 == B + B.3 - -#### gNMI-3.4.2 - Repeat gNMI-3.4.1 but remove the interface by omitting it in CLI. - -### gNMI-3.5 - union_replace move configuration -In some scenarios it is observed that moving a configuration from one interface to another can trigger bugs. Particularly if there is some conflicting element in the configuration such as an IP address. This test moves an IP address from interface 1 to interface 2 using union replace. -#### gNMI-3.5.1 - Move IP address between interfaces using OC -#### gNMI-3.5.2 - Move IP address between interfaces using CLI - -### gNMI-3.6 - union_replace accepted with hardware mismatch -Interface configurations containing a mismatch with hardware (for example, due to a missing or incompatible transceiver module) must be accepted by a device. The interface with the mismatched configuration is expected to be in a down operational state as the result of such a configuration commit. + +* Get baseline configuration (B). +* Change the description of an interface using OC. (B.1) +* Push configuration baseline + B.1 to the DUT. +* Get configuration as B.2 +* Verify B.2 == B + B.1 + +### gnmi-3.3.2 - repeat gnmi-3.3.1 but change only the interface description using CLI + +## gNMI-3.4 - union_replace delete configuration through omission + +### gnmi-3.4.1 - Remove the interface ip address by omitting it in OC. Steps -1. Get configuration D.1 from DUT -1. Generate a configuration D.2 with a port-speed mismatch in the OC which should be accepted by the device -1. Push the configuration to the DUT -1. Verify the gnmi.Set is accepted -1. Get configuration D.3 -1. Verify D.2 == D.3. That is, verify only the interface speed is changed between D.1 and D.3. The remaining CLI and all OC must be unchanged. -#### gNMI-3.6.1 - Verify configuration with OC hardware mismatch is accepted -Generate a configuration D.2 with a port-speed mismatch in the OC which should be accepted and applied by the DUT. +* Get baseline configuration (B). +* Generate OC configuration adding interfaces 1 and 2 (B.1). +* Push configuration baseline + B.1 to the DUT. +* Get configuration as B.2 +* Verify B.2 == B + B.1 +* Generate OC configuration adding only interface 1 (B.3). +* Push configuration baseline + B.3 to the DUT. +* Get configuration as B.4 +* Verify B.4 == B + B.3 -#### gNMI-3.6.2 - Verify configuration with CLI hardware mismatch is accepted +### gnmi-3.4.2 - repeat gnmi 3.4.1 but instead remove the interface ip address by omitting it in CLI. -Generate a configuration D.2 with a port-speed mismatch in the CLI which should be accepted and applied by the DUT. +## gNMI-3.5 - union_replace move configuration +In some scenarios it is observed that moving a configuration from one interface +to another can trigger bugs. Particularly if there is some conflicting element +in the configuration such as an IP address. This test moves a an IP address +from interface 1 to interface 2 using union replace. -### gNMI-3.7 - union_replace rejected with error in CLI with OC -Verify a DUT rejects and rolls back a gnmi.Set union_replace with an invalid configuration in origin CLI. Verify the original configuration is preserved. +### gnmi-3.5.1 move IP address between interfaces using OC -TODO: Decide what configuration error(s) to use. I think we need cases where there is OC that fails leafref validation, but even more importantly, a scenario where the OC will validate, but contains a semantic error. +### gnmi-3.5.2 move IP address between interfaces using CLI -Simple issues like a value out of range or referencing a policy that doesn’t exist in the OC case will be caught with a validation of the structs. Such an error is likely a different code path in a DUT vs. processing a configuration that validates but has some semantic error. +## gNMI-3.6 - union_replace accepted with hardware mismatch -For example of a config that fails validation: referencing a BGP policy that doesn’t exist is an example of data that won’t validate, since BGP policy references in OC are defined as leafrefs. We can write that test and send an unvalidated config to a DUT, but it seems unlikely to reveal bugs. +Interface configurations containing a mismatch with hardware (for example, due +to a missing or incompatible transceiver module) must be accepted by a device. +The interface with the mismatched configuration is expected to be in a down +operational state as the result of such a configuration commit. -For the case where the config passes validation but contains a semantic error, a test case could be configuring an interface to use a QoS queue that doesn’t exist. In this case, the queue name is a string, not a leafref. See qos/interfaces/interface/input/queues/queue/config/name. (This leaf is a string and not a leafref because a device may expose queues which are not explicitly configured) Steps -1. Get configuration E.1 from DUT. -1. Generate a configuration E.2 which includes invalid configuration (see sub tests). -1. Push the configuration E.1 + E.2 to the DUT. -1. Confirm the DUT rejects the gnmi.Set. -1. Get configuration E.3 from DUT. -1. Verify E.1 == E.3 (the configuration is unchanged). +* Get configuration D.1 from DUT +* Generate a configuration D.2 with a port-speed mismatch in the OC which + should be accepted by the device +* Push the configuration to the DUT +* Verify the gnmi.Set is accepted +* Get configuration D.3 +* Verify D.2 == D.3. That is, verify only the interface speed is changed + between D.1 and D.3. The remaining CLI and all OC must be unchanged. + +### gnmi3.6.1 verify configuration with OC hardware mismatch is accepted + +Generate a configuration D.2 with a port-speed mismatch in the OC which should +be accepted and applied by the DUT. + +### gnmi3.6.2 verify configuration with CLI hardware mismatch is accepted -#### gnmi-3.7.1 reference validation error in OC -The invalid configuration is OC which references a BGP neighbor import policy that does not exist. +Generate a configuration D.2 with a port-speed mismatch in the CLI which should +be accepted and applied by the DUT. -#### gnmi-3.7.2 reference which validates but is an error in OC -The invalid configuration is OC which references an qos queue on an interface which does not exist. +## gNMI-3.7 - union_replace rejected with error in CLI with OC -#### gnmi-3.7.3 reference error in CLI -The invalid configuration is CLI which references a BGP neighbor import policy that does not exist. +TODO: implement this test in a future pull request -### gNMI-3.8 - union_replace rejected with error due to configuration item overlap -This test verifies union_replace option 1 or 2 behavior for resolving overlapping configuration items between OC and CLI. Generate the following configuration item combinations which have overlaps between CLI and OC. For NOS which implement option 1, the DUT should return a gRPC error of `INVALID_ARGUMENT`. For NOS with implement option 2, the configuration should be accepted, with the CLI value taking effect and the OC configuration leaf being accepted, but not applied as “state”. +Verify a DUT rejects and rolls back a gnmi.Set union_replace with an invalid +configuration in origin CLI. Verify the original configuration is preserved. + +TODO: Decide what configuration error(s) to use. I think we need cases where +there is OC that fails leafref validation, but even more importantly, a +scenario where the OC will validate, but contains a semantic error. + +Simple issues like a value out of range or referencing a policy that doesn’t +exist in the OC case will be caught with a validation of the structs. Such an +error is likely a different code path in a DUT vs. processing a configuration +that validates but has some semantic error. + +For example of a config that fails validation: referencing a BGP policy that +doesn’t exist is an example of data that won’t validate, since BGP policy +references in OC are defined as leafrefs. We can write that test and send an +unvalidated config to a DUT, but it seems unlikely to reveal bugs. + +For the case where the config passes validation but contains a semantic error, +a test case could be configuring an interface to use a QoS queue that doesn’t +exist. In this case, the queue name is a string, not a leafref. See +qos/interfaces/interface/input/queues/queue/config/name. (I think this leaf +is a string and not a leafref because a device may expose queues which are not +explicitly configured) Steps -1. Get configuration E.1 from DUT. -1. Generate a configuration E.2 which includes the overlapping configuration. -1. Push the configuration E.1 + E.2 to the DUT. -1. Confirm the DUT rejects the gnmi.Set with a gRPC error code of INVALID_ARGUMENT. Log the contents of the optional gRPC error string. -1. Get configuration E.3 from DUT. -1. Verify the configuration is as expected -1. For option 1 NOS, verify E.1 == E.3 (the configuration is unchanged). -1. For option 2 NOS, verify E.2 == E.3 (the CLI config is updated, the OC state is updated to match the CLI value, the OC config is updated using the OC value. Note that the OC state leaves do not equal the OC config leaves) +* Get configuration E.1 from DUT. +* Generate a configuration E.2 which includes invalid configuration (see sub + tests). +* Push the configuration E.1 + E.2 to the DUT. +* Confirm the DUT rejects the gnmi.Set. +* Get configuration E.3 from DUT. +* Verify E.1 == E.3 (the configuration is unchanged). + +### gnmi-3.7.1 reference validation error in OC + +TODO: implement this test in a future pull request + +The invalid configuration is OC which references a BGP neighbor import policy +that does not exist. + +### gnmi-3.7.2 reference which validates but is an error in OC + +TODO: implement this test in a future pull request + +The invalid configuration is OC which references an qos queue on an interface +which does not exist. + +### gnmi-3.7.3 reference error in CLI -#### gnmi-3.8.1 interface CLI and OC overlap with different values -Test where the configuration overlap is the interface MTU with two different MTU values. +The invalid configuration is CLI which references a BGP neighbor import policy +that does not exist. -#### gnmi-3.8.2 interface CLI and OC overlap with same value -Test where the configuration overlap is the interface MTU with the same MTU values. +## gNMI-3.8 - union_replace rejected with error due to configuration item overlap -#### gnmi-3.8.3 BGP model overlap -Test where the configuration overlap is /network-instances/network-instance/protocols/protocol/bgp/global/config/as +TODO: implement this test in a future pull request -####gnmi-3.8.4 routing-policy model overlap -Test where the overlap is /routing-policy/policy-definitions/policy-definition/config/name. +This test verifies union_replace option 1 or 2 behavior for resolving +overlapping configuration items between OC and CLI. Generate the following +configuration item combinations which have overlaps between CLI and OC. For +NOS which implement option 1, the DUT should return a gRPC error of +`INVALID_ARGUMENT`. For NOS with implement option 2, the configuration should +be accepted, with the CLI value taking effect and the OC configuration leaf +being accepted, but not applied as “state”. + +* Get configuration E.1 from DUT. +* Generate a configuration E.2 which includes the overlapping configuration. +* Push the configuration E.1 + E.2 to the DUT. +* Confirm the DUT rejects the gnmi.Set with a gRPC error code of + INVALID_ARGUMENT. Log the contents of the optional gRPC error string. +* Get configuration E.3 from DUT. +* Verify the configuration is as expected + * For option 1 NOS, verify E.1 == E.3 (the configuration is unchanged). + * For option 2 NOS, verify E.2 == E.3 (the CLI config is updated, the OC + state is updated to match the CLI value, the OC config is updated using the + OC value. Note that the OC state leaves do not equal the OC config leaves) + +### gnmi-3.8.1 interface CLI and OC overlap with different values + +Test where the configuration overlap is the interface MTU with two different +MTU values. + +### gnmi-3.8.2 interface CLI and OC overlap with same value + + Test where the configuration overlap is the interface MTU with the same MTU + values. + +### gnmi-3.8.3 BGP model overlap + +Test where the configuration overlap is +/network-instances/network-instance/protocols/protocol/bgp/global/config/as + +### gnmi-3.8.4 routing-policy model overlap + +Test where the overlap is +/routing-policy/policy-definitions/policy-definition/config/name. + +## gNMI-3.9 CLI and OC non-overlap in same OC configuration tree + +TODO: implement this test in a future pull request -###gNMI-3.9 CLI and OC non-overlap in same OC configuration tree These configurations should be accepted and applied successfully by the DUT. -#### gnmi-3.9.1 interface and MTU in OC and interface description in CLI +### gnmi-3.9.1 interface and MTU in OC and interface description in CLI + Steps -1. Get configuration D.1 from DUT -Generate a configuration D.2 with one interface description set in CLI and a second set using OC. -1. Push the configuration to the DUT -1. Verify the gnmi.Set is accepted -1. Get configuration D.3 -1. Verify OC configuration for interface one and two match the descriptions provided by the CLI and OC respectively. -### gNMI-3.10 - union_replace accepted with missing hardware -Configure an interface with a missing transceiver module. The interface with the missing transceiver is expected to contain “config” leaves with the desired values. The “state” leaves should show a down operational state as the result of the configuration commit. +* Get configuration D.1 from DUT +* Generate a configuration D.2 with one interface description set in CLI and a + second set using OC. +* Push the configuration to the DUT +* Verify the gnmi.Set is accepted +* Get configuration D.3 +* Verify OC configuration for interface one and two match the descriptions + provided by the CLI and OC respectively. + +## gNMI-3.10 - union_replace accepted with missing hardware + +TODO: implement this test in a future pull request + +Configure an interface with a missing transceiver module. The interface with +the missing transceiver is expected to contain “config” leaves with the desired +values. The “state” leaves should show a down operational state as the result +of the configuration commit. Steps -1. Identify an interface without a transceiver module installed. -1. Get configuration D.1 from DUT -1. Generate a configuration D.2 , including a port-speed and breakout mode for the interface without a transceiver. -1. Push the configuration to the DUT -1. Verify the gnmi.Set is accepted -1. Get configuration D.3 -1. Verify D.2 == D.3 configuration. That is, verify the “config” leaves for breakout mode and port speed are set to the target values. Verify the state for the interface is oper-state DOWN. All other CLI and OC config leaves must be unchanged. -#### gnmi3.6.1 verify configuration with OC hardware missing is accepted -Perform the steps where a configuration D.2 where the port-speed and breakout set using OC. +* Identify an interface without a transceiver module installed. +* Get configuration D.1 from DUT +* Generate a configuration D.2 , including a port-speed and breakout mode for +the interface without a transceiver. +* Push the configuration to the DUT Verify the gnmi.Set is accepted +* Get configuration D.3 +* Verify D.2 == D.3 configuration. That is, verify the “config” leaves for +breakout mode and port speed are set to the target values. +* Verify the state for the interface is oper-state DOWN. +* Verify all other CLI and OC config leaves are unchanged. + +## gnmi3.10.1 verify configuration with OC hardware missing is accepted -####gnmi3.6.2 verify configuration with CLI hardware missing is accepted -Perform the steps where a configuration D.2 where the port-speed and breakout set using CLI. +Perform the steps where a configuration D.2 where the port-speed and breakout +set using OC. +### gnmi3.10.2 verify configuration with CLI hardware missing is accepted + +Perform the steps where a configuration D.2 where the port-speed and breakout +set using CLI. ## Canonical OC @@ -229,6 +338,7 @@ Perform the steps where a configuration D.2 where the port-speed and breakout se { "config": { "description": "First 50G breakout", + "mtu": 1500, "name": "eth0/0" }, "ethernet": { @@ -259,17 +369,18 @@ Perform the steps where a configuration D.2 where the port-speed and breakout se ```yaml paths: + ## Config Paths ## + /interfaces/interface/config/mtu: /interfaces/interface/ethernet/config/port-speed: + + ## State Paths ## + /interfaces/interface/state/mtu: + /interfaces/interface/state/oper-status: /interfaces/interface/ethernet/state/port-speed: rpcs: gnmi: - gNMI.Set: - union_replace: true - gNMI.Get: gNMI.Subscribe: + gNMI.Set: ``` -## Required DUT platform - -vRX diff --git a/feature/gnmi/tests/union_replace_test/metadata.textproto b/feature/gnmi/tests/union_replace_test/metadata.textproto index 8256585a882..9d0affcb7fc 100644 --- a/feature/gnmi/tests/union_replace_test/metadata.textproto +++ b/feature/gnmi/tests/union_replace_test/metadata.textproto @@ -1,7 +1,36 @@ # proto-file: github.com/openconfig/featureprofiles/proto/metadata.proto # proto-message: Metadata -uuid: "ac092ed2-0383-4eeb-b7ed-624b0c7f8477" -plan_id: "gNMI-3" -description: "union_replace" -testbed: TESTBED_DUT_ATE_2LINKS +uuid: "912d6fc8-5245-419a-9ae9-7da432a46cdf" +plan_id: "gNMI-3" +description: "union_replace" +testbed: TESTBED_DUT_ATE_2LINKS +platform_exceptions: { + platform: { + vendor: CISCO + } + deviations: { + ipv4_missing_enabled: true + } +} +platform_exceptions: { + platform: { + vendor: NOKIA + } + deviations: { + explicit_interface_in_default_vrf: true + aggregate_atomic_update: true + interface_enabled: true + } +} +platform_exceptions: { + platform: { + vendor: ARISTA + } + deviations: { + aggregate_atomic_update: true + interface_enabled: true + explicit_port_speed: true + default_network_instance: "default" + } +} diff --git a/feature/gnmi/tests/union_replace_test/union_replace_test.go b/feature/gnmi/tests/union_replace_test/union_replace_test.go new file mode 100644 index 00000000000..2b041cada73 --- /dev/null +++ b/feature/gnmi/tests/union_replace_test/union_replace_test.go @@ -0,0 +1,671 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package union_replace_test implements tests that cover the gnmi union_replace +// operations. +package union_replace_test + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/openconfig/featureprofiles/internal/attrs" + "github.com/openconfig/featureprofiles/internal/deviations" + "github.com/openconfig/featureprofiles/internal/fptest" + "github.com/openconfig/featureprofiles/internal/helpers" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygnmi/ygnmi" + + log "github.com/golang/glog" + gpb "github.com/openconfig/gnmi/proto/gnmi" +) + +func TestMain(m *testing.M) { + + fptest.RunTests(m) +} + +const ( + awaitTimeOut = 10 * time.Second +) + +var ( + dutIntf = attrs.Attributes{ + Desc: "unionreplacetest", + IPv4: "192.0.2.1", + IPv6: "2001:db8::1", + IPv4Len: 30, + IPv6Len: 64, + Duplex: "FULL", + } +) + +var portSpeed = map[ondatra.Speed]oc.E_IfEthernet_ETHERNET_SPEED{ + ondatra.Speed10Gb: oc.IfEthernet_ETHERNET_SPEED_SPEED_10GB, + ondatra.Speed100Gb: oc.IfEthernet_ETHERNET_SPEED_SPEED_100GB, + ondatra.Speed400Gb: oc.IfEthernet_ETHERNET_SPEED_SPEED_400GB, +} + +func configOCInterface(t *testing.T, sb *gnmi.SetBatch, dut *ondatra.DUTDevice) { + t.Helper() + dp1 := dut.Port(t, "port1") + i := dutIntf.NewOCInterface(dut.Port(t, "port1").Name(), dut) + if deviations.ExplicitPortSpeed(dut) { + i.GetOrCreateEthernet().SetPortSpeed(fptest.GetIfSpeed(t, dp1)) + } + inf := gnmi.OC().Interface(dp1.Name()) + // TODO: add handling for ExplicitPortSpeed deviation and ExplicitInterfaceInDefaultVRF deviation + + gnmi.BatchUnionReplace(sb, inf.Config(), i) +} + +func buildCliConfigRequest(config string) (*gpb.SetRequest, error) { + // Build config with Origin set to cli and Ascii encoded config. + gpbSetRequest := &gpb.SetRequest{ + Update: []*gpb.Update{{ + Path: &gpb.Path{ + Origin: "cli", + Elem: []*gpb.PathElem{}, + }, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_AsciiVal{ + AsciiVal: config, + }, + }, + }}, + } + return gpbSetRequest, nil +} + +func buildGNMICliSubscribe(cmd string) *gpb.SubscribeRequest { + return &gpb.SubscribeRequest{ + Request: &gpb.SubscribeRequest_Subscribe{ + Subscribe: &gpb.SubscriptionList{ + Subscription: []*gpb.Subscription{{ + Path: &gpb.Path{ + Origin: "cli", + Elem: []*gpb.PathElem{{Name: cmd}}, + }, + }}, + Mode: gpb.SubscriptionList_STREAM, + }, + }, + } +} + +// prettyPrintYgnmiResult formats a *ygnmi.Result as JSON for logging. +// Note: ygnmi.Result contains a protobuf (SetResponse) rather than YANG data, +// so it is formatted as standard JSON via protojson rather than RFC7951. +func prettyPrintYgnmiResult(setResult *ygnmi.Result) string { + if setResult == nil || setResult.RawResponse == nil { + return "" + } + opts := protojson.MarshalOptions{ + Multiline: true, + Indent: " ", + } + b, err := opts.Marshal(setResult.RawResponse) + if err != nil { + return err.Error() + } + return string(b) +} + +func setCLINoMTU(t *testing.T, dut *ondatra.DUTDevice, portName string) { + t.Helper() + cliConfig := "" + if dut.Vendor() == ondatra.ARISTA { + cliConfig = fmt.Sprintf("configure terminal\ninterface %s\nno mtu\n", portName) + } else { + t.Fatalf("Unsupported vendor: %v", dut.Vendor()) + } + helpers.GnmiCLIConfig(t, dut, cliConfig) + // Wait for the MTU to be removed (i.e., not equal to 1500). + gnmi.Watch(t, dut, gnmi.OC().Interface(portName).Mtu().State(), awaitTimeOut, func(val *ygnmi.Value[uint16]) bool { + m, present := val.Val() + if !present { + t.Logf("Got MTU not present, want 1500.") + return false + } + if m == 1500 { + return true + } + return false + }).Await(t) +} + +// setCLIunionReplace adds any necessary modifications to the base CLI configuration +// for union replace. +func setCLIunionReplace(t *testing.T, dut *ondatra.DUTDevice) { + t.Helper() + clicfg1 := "" + + if dut.Vendor() == ondatra.ARISTA { + // Add "operation set namespace" command from the CLI config as this is required for Arista + // when using union replace. + clicfg1 = "configure terminal\nmanagement api gnmi\n provider eos-native\n operation set persistence\n operation set namespace\n" + helpers.GnmiCLIConfig(t, dut, clicfg1) + // Poll for config to be applied. + start := time.Now() + for { + if time.Since(start) > 1*time.Minute { + t.Fatal("setCLIunionReplace config not applied in time") + } + clicfg2 := cliConfig(t, dut) + if strings.Contains(clicfg2, "operation set namespace") && strings.Contains(clicfg2, "operation set persistence") { + break + } + time.Sleep(1 * time.Second) + } + } + +} + +// cliConfig returns the CLI config of the DUT as a string +func cliConfig(t *testing.T, dut *ondatra.DUTDevice) string { + t.Helper() + + switch dut.Vendor() { + case ondatra.ARISTA: + return helpers.RunCliCommand(t, dut, "show running-config") + case ondatra.CISCO: + return helpers.RunCliCommand(t, dut, "show running-config") + case ondatra.JUNIPER: + return helpers.RunCliCommand(t, dut, "show | display set") + case ondatra.NOKIA: + return helpers.RunCliCommand(t, dut, "info | as-set") + default: + t.Errorf("Unsupported vendor: %v", dut.Vendor()) + } + + return "" +} + +// TestUnionReplace3_1_idempotentConfig verifies the gNMI UnionReplace operation with CLI config only. +// gNMI-3.1 - Idempotent configuration +func TestUnionReplace3_1_idempotentConfig(t *testing.T) { + dut := ondatra.DUT(t, "dut") + + // ensure union replace is enabled on the DUT and get the CLI config using union replace after setting + // the base CLI config. + setCLIunionReplace(t, dut) + clicfg1 := cliConfig(t, dut) + sb1 := &gnmi.SetBatch{} + gnmi.BatchUnionReplaceCLI(sb1, "cli", clicfg1) + sb1.Set(t, dut) + time.Sleep(5 * time.Second) + clicfg2 := helpers.RunCliCommand(t, dut, "show running-config") + + // second, set the same CLI config again. + sb2 := &gnmi.SetBatch{} + gnmi.BatchUnionReplaceCLI(sb2, "cli", clicfg2) + sb2.Set(t, dut) + time.Sleep(5 * time.Second) + + // verify the CLI config has not changed. + clicfg3 := helpers.RunCliCommand(t, dut, "show running-config") + if clicfg2 != clicfg3 { + t.Errorf("cliConfig before and after do not match!") + } +} + +// TestUnionReplace3_2_1_addOCMTU verifies the gNMI UnionReplace with CLI for a base config and +// adds MTU of an interface using OC only. This tests that the MTU leaf is updated even though the +// interface already exists in the CLI config. It assumes that the CLI config does not contain an MTU +// value for the interface. +// gNMI-3.2.1 - Add interface using OC +func TestUnionReplace3_2_1_addOCMTU(t *testing.T) { + dut := ondatra.DUT(t, "dut") + dp1 := dut.Port(t, "port1") + sb1 := &gnmi.SetBatch{} + + setCLIunionReplace(t, dut) + setCLINoMTU(t, dut, dp1.Name()) + + // Add MTU to the interface using OC config. + cliConfig2 := cliConfig(t, dut) + gnmi.BatchUnionReplaceCLI(sb1, "cli", cliConfig2) + gnmi.BatchUnionReplace(sb1, gnmi.OC().Interface(dp1.Name()).Mtu().Config(), 1400) + t.Logf("Generated BatchUnionReplace: %#v\n", sb1.String()) + + setResult := sb1.Set(t, dut) + t.Logf("\nSetResult: %#v\n", prettyPrintYgnmiResult(setResult)) + + want := uint16(1400) + gnmi.Watch(t, dut, gnmi.OC().Interface(dp1.Name()).Mtu().State(), awaitTimeOut, func(val *ygnmi.Value[uint16]) bool { + m, present := val.Val() + if !present { + t.Errorf("MTU not present") + return false + } + if m != want { + t.Errorf("MTU not correct, got: %v, want: %v", m, want) + return false + } + return true + }).Await(t) + +} + +// TestUnionReplace3_2_2_addCLIInterface verifies the gNMI UnionReplace with CLI for a base config +// and configures MTU of an interface using CLI and OC, therefore mixing interfaces with CLI and OC. +// gNMI-3.2.2 - Add interface using CLI +func TestUnionReplace3_2_2_addCLIMTU(t *testing.T) { + dut := ondatra.DUT(t, "dut") + dp2 := dut.Port(t, "port2") + sb1 := &gnmi.SetBatch{} + sb2 := &gnmi.SetBatch{} + + // ensure that the device has union_replace enabled and the base CLI config does not contain an MTU + // config for the interface being tested. + setCLIunionReplace(t, dut) + setCLINoMTU(t, dut, dp2.Name()) + + // Add MTU to the interface using OC config to a known value. + cliConfig1 := cliConfig(t, dut) + gnmi.BatchUnionReplaceCLI(sb1, "cli", cliConfig1) + gnmi.BatchUnionReplace(sb1, gnmi.OC().Interface(dp2.Name()).Mtu().Config(), 1400) + sb1.Set(t, dut) + gnmi.Watch(t, dut, gnmi.OC().Interface(dp2.Name()).Mtu().State(), awaitTimeOut, func(val *ygnmi.Value[uint16]) bool { + m, present := val.Val() + if !present { + t.Errorf("MTU not present") + return false + } + if m != 1400 { + t.Logf("MTU not yet 1400, got: %v", m) + return false + } + return true + }).Await(t) + + // Change the MTU to a different value using CLI config. + cliConfig2 := cliConfig(t, dut) + switch dut.Vendor() { + case ondatra.ARISTA: + cliConfig2 += fmt.Sprintf("interface %s\nmtu 1300\n", dp2.Name()) + case ondatra.CISCO: + cliConfig2 += fmt.Sprintf("interface %s\nmtu 1300\n", dp2.Name()) + case ondatra.JUNIPER: + cliConfig2 += fmt.Sprintf("set interfaces %s mtu 1300\n", dp2.Name()) + default: + t.Errorf("Unsupported vendor: %v", dut.Vendor()) + } + gnmi.BatchUnionReplaceCLI(sb2, "cli", cliConfig2) + setResult := sb2.Set(t, dut) + t.Logf("\nSetResult: %#v\n", prettyPrintYgnmiResult(setResult)) + + // If union_replace option for CLI overriding OC is the DUT behavior, verify the MTU is updated + // to the new, CLI configured value. If union_replace option for CLI and OC config error is the + // DUT behavior, verify the MTU is not updated to the new, CLI configured value. + switch dut.Vendor() { + case ondatra.ARISTA: + // CLI overrides OC + want := uint16(1300) + gnmi.Watch(t, dut, gnmi.OC().Interface(dp2.Name()).Mtu().State(), awaitTimeOut, func(val *ygnmi.Value[uint16]) bool { + m, present := val.Val() + if !present { + t.Logf("MTU not present yet") + return false + } + if m != want { + t.Logf("MTU not yet %d, got: %v", want, m) + return false + } + return true + }).Await(t) + case ondatra.CISCO: + // OC and CLI conflict generates an error, does not update MTU + want := uint16(1400) + gnmi.Watch(t, dut, gnmi.OC().Interface(dp2.Name()).Mtu().State(), awaitTimeOut, func(val *ygnmi.Value[uint16]) bool { + m, present := val.Val() + if !present { + t.Logf("MTU not present yet") + return false + } + if m != want { + t.Logf("MTU not yet %d, got: %v", want, m) + return false + } + return true + }).Await(t) + case ondatra.JUNIPER: + // OC and CLI conflict generates an error, does not update MTU + want := uint16(1400) + gnmi.Watch(t, dut, gnmi.OC().Interface(dp2.Name()).Mtu().State(), awaitTimeOut, func(val *ygnmi.Value[uint16]) bool { + m, present := val.Val() + if !present { + t.Logf("MTU not present yet") + return false + } + if m != want { + t.Logf("MTU not yet %d, got: %v", want, m) + return false + } + return true + }).Await(t) + case ondatra.NOKIA: + // OC and CLI conflict generates an error, does not update MTU + want := uint16(1400) + gnmi.Watch(t, dut, gnmi.OC().Interface(dp2.Name()).Mtu().State(), awaitTimeOut, func(val *ygnmi.Value[uint16]) bool { + m, present := val.Val() + if !present { + t.Logf("MTU not present yet") + return false + } + if m != want { + t.Logf("MTU not yet %d, got: %v", want, m) + return false + } + return true + }).Await(t) + default: + t.Errorf("Unsupported vendor: %v", dut.Vendor()) + } + +} + +// TestUnionReplace3_3_1_changeOCConfig verifies the gNMI UnionReplace with CLI for a base config and +// changes an OC config. +// gNMI-3.3.1 - Change OC config +func TestUnionReplace3_3_1_changeOCConfig(t *testing.T) { + dut := ondatra.DUT(t, "dut") + setCLIunionReplace(t, dut) + portName := dut.Port(t, "port2").Name() + sb := &gnmi.SetBatch{} + + // reset the CLI config for the port to remove any previous MTU config. + setCLINoMTU(t, dut, portName) + + // Add MTU to the interface using OC config. + cliConfig1 := cliConfig(t, dut) + gnmi.BatchUnionReplaceCLI(sb, "cli", cliConfig1) + gnmi.BatchUnionReplace(sb, gnmi.OC().Interface(portName).Mtu().Config(), 1450) + sb.Set(t, dut) + + want1 := uint16(1450) + gnmi.Watch(t, dut, gnmi.OC().Interface(portName).Mtu().State(), awaitTimeOut, func(val *ygnmi.Value[uint16]) bool { + m, present := val.Val() + if !present { + t.Logf("MTU not present yet") + return false + } + if m != want1 { + t.Logf("MTU not yet %v, got: %v", want1, m) + return false + } + return true + }).Await(t) + + // Change the MTU using OC config. + // reuse the same CLI config without any MTU config. + sb2 := &gnmi.SetBatch{} + gnmi.BatchUnionReplace(sb2, gnmi.OC().Interface(portName).Mtu().Config(), 1440) + gnmi.BatchUnionReplaceCLI(sb2, "cli", cliConfig1) + sb2.Set(t, dut) + + want2 := uint16(1440) + gnmi.Watch(t, dut, gnmi.OC().Interface(portName).Mtu().State(), awaitTimeOut, func(val *ygnmi.Value[uint16]) bool { + m, present := val.Val() + if !present { + t.Logf("MTU not present yet") + return false + } + if m != want2 { + t.Logf("MTU not yet %v, got: %v", want2, m) + return false + } + return true + }).Await(t) +} + +// TestUnionReplace3_3_2_changeCLIConfig verifies the gNMI UnionReplace with CLI for a base config and +// changes a CLI config. +// gNMI-3.3.2 - Change CLI config +func TestUnionReplace3_3_2_changeCLIConfig(t *testing.T) { + dut := ondatra.DUT(t, "dut") + setCLIunionReplace(t, dut) + port1Name := dut.Port(t, "port1").Name() + port1DescriptionOC := "unionreplacetest gnmi-3.3.2 OC" + port1DescriptionCLI := "unionreplacetest gnmi-3.3.2 CLI" + sb1 := &gnmi.SetBatch{} + + // Set the interface description to a known value using OC config. + // Add OC interface and set description on the interface. + gnmi.BatchUnionReplace(sb1, gnmi.OC().Interface(port1Name).Description().Config(), port1DescriptionOC) + cliConfig1 := cliConfig(t, dut) + gnmi.BatchUnionReplaceCLI(sb1, "cli", cliConfig1) + sb1.Set(t, dut) + gnmi.Watch(t, dut, gnmi.OC().Interface(port1Name).Description().State(), awaitTimeOut, func(val *ygnmi.Value[string]) bool { + desc, present := val.Val() + if !present { + t.Logf("Description not present. Want: %q, got: not present", port1DescriptionOC) + return false + } + if desc != port1DescriptionOC { + t.Logf("Description not set to OC configured value. Want: %q, got: %q", port1DescriptionOC, desc) + return false + } + return true + }).Await(t) + + // Change the interface description using CLI config. + // the OC configuration does not include an interface description. + sb2 := &gnmi.SetBatch{} + cliConfig2 := cliConfig(t, dut) + cliConfig2 += fmt.Sprintf("interface %s\ndescription "+port1DescriptionCLI+"\n", dut.Port(t, "port1").Name()) + gnmi.BatchUnionReplaceCLI(sb2, "cli", cliConfig2) + sb2.Set(t, dut) + + // Watch for the description to be updated to the CLI configured value. + gnmi.Watch(t, dut, gnmi.OC().Interface(port1Name).Description().State(), awaitTimeOut, func(val *ygnmi.Value[string]) bool { + desc, present := val.Val() + if !present { + t.Logf("Description not present. Want: %q, got: not present", port1DescriptionCLI) + return false + } + if desc != port1DescriptionCLI { + t.Logf("Description does not match the CLI configured value. want: %q, got: %q", port1DescriptionCLI, desc) + return false + } + return true // Description is now port1DescriptionCLI + }).Await(t) +} + +// TestUnionReplace3_6_1 tests the gNMI union_replace accepted with hardware mismatch. +// load the cli config from DUT +// generate OC config for 1 DUT 100Gbps port but set port speed to 10Gbps (intentionally mismatch) +// build the union replace request with the cli config and OC config +// send the request to the DUT +// verify the DUT OC config contains the port speed of 10Gbps +// verify the DUT OC /interfaces/interface/state/oper-status is DOWN +func TestUnionReplace3_6_1(t *testing.T) { + dut := ondatra.DUT(t, "dut") + setCLIunionReplace(t, dut) + sb := &gnmi.SetBatch{} + targetSpeed := oc.IfEthernet_ETHERNET_SPEED_SPEED_10GB + + // confirm the testbed defined and DUT reported port speed are not the target speed. + dp1 := dut.Port(t, "port1") + speedCurrent := portSpeed[dp1.Speed()] + t.Logf("DUT %v port speed defined in the testbed is %v", dp1.Name(), speedCurrent) + beforeSpeed := gnmi.Get(t, dut, gnmi.OC().Interface(dp1.Name()).Ethernet().PortSpeed().State()) + t.Logf("DUT reported PortSpeed state before any changes: %v", beforeSpeed) + + if speedCurrent == targetSpeed { + t.Fatalf("Need a different topology for this test. DUT port %q current port speed must not be %q", dp1.Name(), targetSpeed) + } + + t.Logf("Configuring DUT port %q to mismatched port-speed %q using gNMI union_replace.", dp1.Name(), targetSpeed) + // get the cli config from DUT and add it to the SetBatch. + clicfg1 := cliConfig(t, dut) + gnmi.BatchUnionReplaceCLI(sb, "cli", clicfg1) + /* + These Arista EOS CLI commands would allow EOS to accept the port speed mismatch but are not + included as they are not accepted as a deviation. + system l1 + unsupported speed action warn + */ + + // add configuration of the OC interface to the SetBatch + configOCInterface(t, sb, dut) + gnmi.BatchUnionReplace(sb, gnmi.OC().Interface(dp1.Name()).Ethernet().PortSpeed().Config(), targetSpeed) + gnmi.BatchUnionReplace(sb, gnmi.OC().Interface(dp1.Name()).Ethernet().DuplexMode().Config(), oc.Ethernet_DuplexMode_FULL) + t.Logf("Generated BatchUnionReplace: %#v\n", sb.String()) + + // send the request to the DUT. + setResult := sb.Set(t, dut) + t.Logf("SetResult:\n%s", prettyPrintYgnmiResult(setResult)) + + // Verify the port speed CONFIG leaf is the before speed. It is expected that the port speed config + // leaf is updated to the target speed. + gnmi.Watch(t, dut, gnmi.OC().Interface(dp1.Name()).Ethernet().PortSpeed().Config(), awaitTimeOut, func(val *ygnmi.Value[oc.E_IfEthernet_ETHERNET_SPEED]) bool { + speed, present := val.Val() + if !present { + t.Logf("PortSpeed config not present. Want: %v, got: not present", targetSpeed) + return false + } + if speed != targetSpeed { + t.Logf("PortSpeed config not set to target speed. Want: %v, got: %v", targetSpeed, speed) + return false + } + t.Logf("PortSpeed config is set to target speed: %v", speed) + return true + }).Await(t) + + // Verify the port speed state leaf is the beforeSpeed or UNKNOWN. It is expected that the + // PortSpeed state leaf was not affected by the new configuration and reflects the actual + // operating speed of the port. + foundSpeed := gnmi.Get(t, dut, gnmi.OC().Interface(dp1.Name()).Ethernet().PortSpeed().State()) + if foundSpeed != beforeSpeed && foundSpeed != oc.IfEthernet_ETHERNET_SPEED_SPEED_UNKNOWN { + t.Errorf("DUT port1 PortSpeed state: got %v, want %v or unknown", foundSpeed, beforeSpeed) + } + + want := oc.Interface_OperStatus_DOWN + gnmi.Watch(t, dut, gnmi.OC().Interface(dp1.Name()).OperStatus().State(), awaitTimeOut, func(val *ygnmi.Value[oc.E_Interface_OperStatus]) bool { + status, present := val.Val() + if !present { + t.Logf("OperStatus not present yet") + return false + } + if status != want { + t.Logf("OperStatus not in expected state. Want: %v, got: %v", want, status) + return false + } + t.Logf("OperStatus is in expected state: %v", status) + return true + }).Await(t) + +} + +// TestUnionReplace3_6_2 tests the gNMI union_replace accepted with hardware mismatch using CLI. +// load the cli config from DUT +// generate CLI config for 1 DUT 100Gbps port but set port speed to 10Gbps (intentionally mismatch) +// build the union replace request with the cli config and OC config +// send the request to the DUT +// verify the DUT OC config contains the port speed of 10Gbps +// verify the DUT OC /interfaces/interface/state/oper-status is DOWN +func TestUnionReplace3_6_2(t *testing.T) { + dut := ondatra.DUT(t, "dut") + setCLIunionReplace(t, dut) + sb := &gnmi.SetBatch{} + targetSpeed := oc.IfEthernet_ETHERNET_SPEED_SPEED_10GB + + // confirm the testbed defined and DUT reported port speed are not the target speed. + dp1 := dut.Port(t, "port1") + speedCurrent := portSpeed[dp1.Speed()] + t.Logf("DUT %v port speed defined in the testbed is %v", dp1.Name(), speedCurrent) + beforeSpeed := gnmi.Get(t, dut, gnmi.OC().Interface(dp1.Name()).Ethernet().PortSpeed().State()) + t.Logf("DUT reported PortSpeed state before any changes: %v", beforeSpeed) + + if speedCurrent == targetSpeed { + t.Fatalf("Need a different topology for this test. DUT port %q current port speed must not be %q", dp1.Name(), targetSpeed) + } + + t.Logf("Configuring DUT port %q to mismatched port-speed %q using gNMI union_replace CLI.", dp1.Name(), targetSpeed) + // get the cli config from DUT, modify it to introduce the port speed mismatch, and add it to the SetBatch. + clicfg1 := cliConfig(t, dut) + switch dut.Vendor() { + case ondatra.ARISTA: + clicfg1 += fmt.Sprintf("interface %s\nspeed 10g\n", dp1.Name()) + case ondatra.CISCO: + clicfg1 += fmt.Sprintf("interface %s\nspeed 10000\n", dp1.Name()) + case ondatra.JUNIPER: + clicfg1 += fmt.Sprintf("set interfaces %s speed 10g\n", dp1.Name()) + default: + t.Errorf("Unsupported vendor: %v", dut.Vendor()) + } + gnmi.BatchUnionReplaceCLI(sb, "cli", clicfg1) + /* + These Arista EOS CLI commands would allow EOS to accept the port speed mismatch but are not + included as they are not accepted as a deviation. + system l1 + unsupported speed action warn + */ + + // add configuration of the OC interface to the SetBatch + configOCInterface(t, sb, dut) + gnmi.BatchUnionReplace(sb, gnmi.OC().Interface(dp1.Name()).Ethernet().DuplexMode().Config(), oc.Ethernet_DuplexMode_FULL) + t.Logf("Generated BatchUnionReplace: %#v\n", sb.String()) + + // send the request to the DUT. + setResult := sb.Set(t, dut) + t.Logf("SetResult:\n%s", prettyPrintYgnmiResult(setResult)) + + // Verify the port speed CONFIG leaf is the before speed. It is expected that the port speed config + // leaf is updated to the target speed. + gnmi.Watch(t, dut, gnmi.OC().Interface(dp1.Name()).Ethernet().PortSpeed().Config(), awaitTimeOut, func(val *ygnmi.Value[oc.E_IfEthernet_ETHERNET_SPEED]) bool { + speed, present := val.Val() + if !present { + t.Logf("PortSpeed config not present. Want: %v, got: not present", targetSpeed) + return false + } + if speed != targetSpeed { + t.Logf("PortSpeed config not set to target speed. Want: %v, got: %v", targetSpeed, speed) + return false + } + t.Logf("PortSpeed config is set to target speed: %v", speed) + return true + }).Await(t) + + // Verify the port speed state leaf is the beforeSpeed or UNKNOWN. It is expected that the + // PortSpeed state leaf was not affected by the new configuration and reflects the actual + // operating speed of the port. + foundSpeed := gnmi.Get(t, dut, gnmi.OC().Interface(dp1.Name()).Ethernet().PortSpeed().State()) + if foundSpeed != beforeSpeed && foundSpeed != oc.IfEthernet_ETHERNET_SPEED_SPEED_UNKNOWN { + t.Errorf("DUT port1 PortSpeed state: got %v, want %v or unknown", foundSpeed, beforeSpeed) + } + + want := oc.Interface_OperStatus_DOWN + gnmi.Watch(t, dut, gnmi.OC().Interface(dp1.Name()).OperStatus().State(), awaitTimeOut, func(val *ygnmi.Value[oc.E_Interface_OperStatus]) bool { + status, present := val.Val() + if !present { + t.Logf("OperStatus not present yet") + return false + } + if status != want { + t.Logf("OperStatus not in expected state. Want: %v, got: %v", want, status) + return false + } + t.Logf("OperStatus is in expected state: %v", status) + return true + }).Await(t) + +} diff --git a/internal/attrs/attrs.go b/internal/attrs/attrs.go index cab9b372615..c7ae18e2a58 100644 --- a/internal/attrs/attrs.go +++ b/internal/attrs/attrs.go @@ -27,6 +27,8 @@ import ( "github.com/openconfig/ondatra" "github.com/openconfig/ondatra/gnmi/oc" "github.com/openconfig/ygot/ygot" + + log "github.com/golang/glog" ) // Attributes bundles some common attributes for devices and/or interfaces. @@ -47,6 +49,7 @@ type Attributes struct { IPv6Len uint8 // Prefix length for IPv6. MTU uint16 ID uint32 // /interfaces/interface/state/id p4rt interface id + Duplex string // interface Ethernet duplex mode: FULL or HALF } // IPv4CIDR constructs the IPv4 CIDR notation with the given prefix @@ -78,6 +81,16 @@ func (a *Attributes) ConfigOCInterface(intf *oc.Interface, dut *ondatra.DUTDevic e.MacAddress = ygot.String(a.MAC) } + if a.Duplex != "" { + if a.Duplex == "FULL" { + e.DuplexMode = oc.Ethernet_DuplexMode_FULL + } else if a.Duplex == "HALF" { + e.DuplexMode = oc.Ethernet_DuplexMode_HALF + } else { + log.Errorf("Unsupported duplex mode: %s", a.Duplex) + } + } + s := intf.GetOrCreateSubinterface(a.Subinterface) if a.IPv4 != "" { s4 := s.GetOrCreateIpv4() diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index ebe9a6e4cc8..25ddc6cc09c 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -175,3 +175,13 @@ func GetRouterTime(t *testing.T, dut *ondatra.DUTDevice) time.Time { t.Logf("Router current-datetime: %s (parsed UTC: %s)", routerTimeStr, startTime.UTC().Format(time.RFC3339Nano)) return startTime } + +// RunCliCommand runs a CLI command on the DUT and returns the output. +func RunCliCommand(t *testing.T, dut *ondatra.DUTDevice, cliCommand string) string { + cliClient := dut.RawAPIs().CLI(t) + output, err := cliClient.RunCommand(context.Background(), cliCommand) + if err != nil { + t.Fatalf("Failed to execute CLI command '%q': %v", cliCommand, err) + } + return output.Output() +}