From 0df89244294dbdc7941939edf18804ac91102a56 Mon Sep 17 00:00:00 2001 From: Shivashankar Paulchamy Date: Tue, 19 May 2026 17:13:28 -0700 Subject: [PATCH 1/4] Add Cisco Deviation for TransportSecurity flag --- .../tests/container_connectivity/cntr_test.go | 28 ++++++++++++++++--- .../tests/gnmi_ni_test/metadata.textproto | 1 + internal/cfgplugins/system.go | 3 ++ internal/deviations/deviations.go | 6 ++++ proto/metadata.proto | 4 +++ proto/metadata_go_proto/metadata.pb.go | 23 +++++++++++---- 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/feature/container/networking/tests/container_connectivity/cntr_test.go b/feature/container/networking/tests/container_connectivity/cntr_test.go index 04c71b9d2d6..87dab0e67d0 100644 --- a/feature/container/networking/tests/container_connectivity/cntr_test.go +++ b/feature/container/networking/tests/container_connectivity/cntr_test.go @@ -27,6 +27,8 @@ import ( "time" "github.com/kr/pretty" + clnt "github.com/openconfig/containerz/client" + cpb "github.com/openconfig/featureprofiles/internal/cntrsrv/proto/cntr" "github.com/openconfig/featureprofiles/internal/containerztest" "github.com/openconfig/featureprofiles/internal/deviations" "github.com/openconfig/featureprofiles/internal/fptest" @@ -39,8 +41,6 @@ import ( "github.com/openconfig/ygot/ygot" "google.golang.org/grpc" "google.golang.org/protobuf/encoding/prototext" - - cpb "github.com/openconfig/featureprofiles/internal/cntrsrv/proto/cntr" ) func TestMain(m *testing.M) { @@ -48,7 +48,7 @@ func TestMain(m *testing.M) { } var ( - containerTar = flag.String("container_tar", "/tmp/cntrsrv.tar", "The container tarball to deploy.") + containerTar = flag.String("container_tar", "cntrsrv.tar", "The container tarball to deploy.") // containerTarPath returns the path to the container tarball. // This can be overridden for internal testing behavior using init(). containerTarPath = func(t *testing.T) string { @@ -75,7 +75,9 @@ func setupContainer(t *testing.T, dut *ondatra.DUTDevice) func() { Network: "host", PollForRunningState: true, } + t.Logf("Starting container %q with host networking on port %d using tar %q", instanceName, cntrPort, containerTarPath(t)) _, cleanup := containerztest.Setup(ctx, t, dut, opts) + t.Logf("Container %q setup completed", instanceName) return cleanup } @@ -115,6 +117,9 @@ func TestDial(t *testing.T) { // ready to accept connections. We retry the Ping RPC to handle this race // condition. var lastErr error + var retry int + containerz := dut.RawAPIs().GNOI(t).Containerz() + cli := clnt.NewClientFromStub(containerz) for { select { case <-ctx.Done(): @@ -128,8 +133,23 @@ func TestDial(t *testing.T) { return // Success } lastErr = err - t.Logf("Ping failed, retrying in 2 seconds... (error: %v)", err) + retry++ + t.Logf("Ping failed, retrying in 2 seconds... (attempt=%d, error=%v)", retry, err) time.Sleep(2 * time.Second) + if retry%3 == 0 { + listCh, listErr := cli.ListContainer(ctx, true, 0, map[string][]string{"name": {instanceName}}) + if listErr != nil { + t.Logf("ListContainer during ping retry failed: %v", listErr) + } else { + for info := range listCh { + if info.Error != nil { + t.Logf("ListContainer stream error during retry: %v", info.Error) + continue + } + t.Logf("Retry container state: name=%s state=%s image=%s labels=%v", info.Name, info.State, info.ImageName, info.ID) + } + } + } } } diff --git a/feature/gnmi/tests/gnmi_ni_test/metadata.textproto b/feature/gnmi/tests/gnmi_ni_test/metadata.textproto index 10d8e7acd0d..68b2b63109b 100644 --- a/feature/gnmi/tests/gnmi_ni_test/metadata.textproto +++ b/feature/gnmi/tests/gnmi_ni_test/metadata.textproto @@ -21,6 +21,7 @@ platform_exceptions: { } deviations: { default_ni_gnmi_server_name: "gnxi-default" + grpc_transport_security_false_unsupported: true } } platform_exceptions: { diff --git a/internal/cfgplugins/system.go b/internal/cfgplugins/system.go index 76aab8ad75e..15f36f49a40 100644 --- a/internal/cfgplugins/system.go +++ b/internal/cfgplugins/system.go @@ -68,6 +68,9 @@ func CreateGNMIServer(t testing.TB, d *ondatra.DUTDevice, batch *gnmi.SetBatch, if !deviations.GrpcServerServicesUnsupported(d) { gnmiServer.Services = []oc.E_SystemGrpc_GRPC_SERVICE{oc.SystemGrpc_GRPC_SERVICE_GNMI} } + if deviations.GrpcTransportSecurityFalseUnsupported(d) { + gnmiServer.TransportSecurity = ygot.Bool(true) + } gnmi.BatchUpdate(batch, gnmiServerPath.Config(), gnmiServer) } diff --git a/internal/deviations/deviations.go b/internal/deviations/deviations.go index a63c98cd746..25d52d16e0c 100644 --- a/internal/deviations/deviations.go +++ b/internal/deviations/deviations.go @@ -2196,3 +2196,9 @@ func BgpMultipathPathsUnderPeerGroupUnsupported(dut *ondatra.DUTDevice) bool { func LACPInterfaceMemberStateInterfaceUnsupported(dut *ondatra.DUTDevice) bool { return lookupDUTDeviations(dut).GetLacpInterfaceMemberStateInterfaceUnsupported() } + +// GrpcTransportSecurityFalseUnsupported returns true if the device does not +// support setting transport-security to false under grpc-server config. +func GrpcTransportSecurityFalseUnsupported(dut *ondatra.DUTDevice) bool { + return lookupDUTDeviations(dut).GetGrpcTransportSecurityFalseUnsupported() +} diff --git a/proto/metadata.proto b/proto/metadata.proto index 32bfb4ab1d1..1a1ac459df7 100644 --- a/proto/metadata.proto +++ b/proto/metadata.proto @@ -1342,6 +1342,10 @@ message Metadata { // Juniper: https://partnerissuetracker.corp.google.com/issues/510547636 bool containerz_retrieve_logs_unsupported = 426; + // Device does not support setting transport-security to false under + // grpc-server config. + bool grpc_transport_security_false_unsupported = 427; + // 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 e715e64e42a..3c799fafef9 100644 --- a/proto/metadata_go_proto/metadata.pb.go +++ b/proto/metadata_go_proto/metadata.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v3.21.12 +// protoc v6.33.4 // source: metadata.proto package metadata_go_proto @@ -1459,8 +1459,11 @@ type Metadata_Deviations struct { // Device does not support retrieving Containerz logs. // Juniper: https://partnerissuetracker.corp.google.com/issues/510547636 ContainerzRetrieveLogsUnsupported bool `protobuf:"varint,426,opt,name=containerz_retrieve_logs_unsupported,json=containerzRetrieveLogsUnsupported,proto3" json:"containerz_retrieve_logs_unsupported,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Device does not support setting transport-security to false under + // grpc-server config. + GrpcTransportSecurityFalseUnsupported bool `protobuf:"varint,427,opt,name=grpc_transport_security_false_unsupported,json=grpcTransportSecurityFalseUnsupported,proto3" json:"grpc_transport_security_false_unsupported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Metadata_Deviations) Reset() { @@ -4230,6 +4233,13 @@ func (x *Metadata_Deviations) GetContainerzRetrieveLogsUnsupported() bool { return false } +func (x *Metadata_Deviations) GetGrpcTransportSecurityFalseUnsupported() bool { + if x != nil { + return x.GrpcTransportSecurityFalseUnsupported + } + 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"` @@ -4286,7 +4296,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\"\xcd\xee\x01\n" + + "\x0emetadata.proto\x12\x12openconfig.testing\x1a1github.com/openconfig/ondatra/proto/testbed.proto\"\xa8\xef\x01\n" + "\bMetadata\x12\x12\n" + "\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x17\n" + "\aplan_id\x18\x02 \x01(\tR\x06planId\x12 \n" + @@ -4298,7 +4308,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\x99\xe4\x01\n" + + "\x16software_version_regex\x18\x04 \x01(\tR\x14softwareVersionRegexJ\x04\b\x02\x10\x03R\x0ehardware_model\x1a\xf4\xe4\x01\n" + "\n" + "Deviations\x120\n" + "\x14ipv4_missing_enabled\x18\x01 \x01(\bR\x12ipv4MissingEnabled\x129\n" + @@ -4695,7 +4705,8 @@ const file_metadata_proto_rawDesc = "" + "0bgp_multipath_paths_under_peer_group_unsupported\x18\xa7\x03 \x01(\bR*bgpMultipathPathsUnderPeerGroupUnsupported\x123\n" + "\x15ciscoxr_vendordrop_ft\x18\xa8\x03 \x01(\tR\x13ciscoxrVendordropFt\x12h\n" + "1lacp_interface_member_state_interface_unsupported\x18\xa9\x03 \x01(\bR,lacpInterfaceMemberStateInterfaceUnsupported\x12P\n" + - "$containerz_retrieve_logs_unsupported\x18\xaa\x03 \x01(\bR!containerzRetrieveLogsUnsupportedJ\x04\bT\x10UJ\x04\b\t\x10\n" + + "$containerz_retrieve_logs_unsupported\x18\xaa\x03 \x01(\bR!containerzRetrieveLogsUnsupported\x12Y\n" + + ")grpc_transport_security_false_unsupported\x18\xab\x03 \x01(\bR%grpcTransportSecurityFalseUnsupportedJ\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" + From c5237094ad19adeee9edcc8ceaffa8b44f594600 Mon Sep 17 00:00:00 2001 From: shpaulch Date: Wed, 20 May 2026 06:52:42 -0700 Subject: [PATCH 2/4] Delete feature/container/networking/tests/container_connectivity/cntr_test.go Remove CNTR file --- .../tests/container_connectivity/cntr_test.go | 398 ------------------ 1 file changed, 398 deletions(-) delete mode 100644 feature/container/networking/tests/container_connectivity/cntr_test.go diff --git a/feature/container/networking/tests/container_connectivity/cntr_test.go b/feature/container/networking/tests/container_connectivity/cntr_test.go deleted file mode 100644 index 87dab0e67d0..00000000000 --- a/feature/container/networking/tests/container_connectivity/cntr_test.go +++ /dev/null @@ -1,398 +0,0 @@ -/* - Copyright 2022 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 - - https://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 cntr_test implements an ONDATRA test for container functionalities -// as described in the CNTR-[234] tests in README.md. -package cntr_test - -import ( - "context" - "flag" - "fmt" - "strings" - "testing" - "time" - - "github.com/kr/pretty" - clnt "github.com/openconfig/containerz/client" - cpb "github.com/openconfig/featureprofiles/internal/cntrsrv/proto/cntr" - "github.com/openconfig/featureprofiles/internal/containerztest" - "github.com/openconfig/featureprofiles/internal/deviations" - "github.com/openconfig/featureprofiles/internal/fptest" - "github.com/openconfig/featureprofiles/internal/gribi" - "github.com/openconfig/gribigo/fluent" - "github.com/openconfig/ondatra" - "github.com/openconfig/ondatra/binding" - "github.com/openconfig/ondatra/gnmi" - "github.com/openconfig/ondatra/gnmi/oc" - "github.com/openconfig/ygot/ygot" - "google.golang.org/grpc" - "google.golang.org/protobuf/encoding/prototext" -) - -func TestMain(m *testing.M) { - fptest.RunTests(m) -} - -var ( - containerTar = flag.String("container_tar", "cntrsrv.tar", "The container tarball to deploy.") - // containerTarPath returns the path to the container tarball. - // This can be overridden for internal testing behavior using init(). - containerTarPath = func(t *testing.T) string { - return *containerTar - } -) - -const ( - imageName = "cntrsrv_image" - instanceName = "cntr-test-conn" - cntrPort = 60061 -) - -// setupContainer deploys and starts the cntrsrv container on the DUT. -// It returns a function to clean up the container. -func setupContainer(t *testing.T, dut *ondatra.DUTDevice) func() { - t.Helper() - ctx := context.Background() - opts := containerztest.StartContainerOptions{ - ImageName: imageName, - InstanceName: instanceName, - Command: fmt.Sprintf("./cntrsrv --port=%d", cntrPort), - TarPath: containerTarPath(t), - Network: "host", - PollForRunningState: true, - } - t.Logf("Starting container %q with host networking on port %d using tar %q", instanceName, cntrPort, containerTarPath(t)) - _, cleanup := containerztest.Setup(ctx, t, dut, opts) - t.Logf("Container %q setup completed", instanceName) - return cleanup -} - -// dialContainer dials a gRPC service running on a container on a device at the specified port. -func dialContainer(t *testing.T, ctx context.Context, dut *ondatra.DUTDevice, port int) *grpc.ClientConn { - t.Helper() - var dialer interface { - DialGRPCWithPort(context.Context, int, ...grpc.DialOption) (*grpc.ClientConn, error) - } - bindingDUT := dut.RawAPIs().BindingDUT() - if err := binding.DUTAs(bindingDUT, &dialer); err != nil { - t.Skipf("BindingDUT %T does not implement DialGRPCWithPort, which is required for this test: %v", bindingDUT, err) - } - - conn, err := dialer.DialGRPCWithPort(ctx, port) - if err != nil { - t.Fatalf("DialGRPCWithPort failed: %v", err) - } - return conn -} - -// TestDial implements CNTR-2, validating that it is possible for an external caller to dial into a service -// running in a container on a DUT. The service used is the cntr service defined by cntr.proto. -func TestDial(t *testing.T) { - dut := ondatra.DUT(t, "dut1") - cleanup := setupContainer(t, dut) - defer cleanup() - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - conn := dialContainer(t, ctx, dut, cntrPort) - defer conn.Close() - - client := cpb.NewCntrClient(conn) - - // The container can be in a RUNNING state before the gRPC server inside is - // ready to accept connections. We retry the Ping RPC to handle this race - // condition. - var lastErr error - var retry int - containerz := dut.RawAPIs().GNOI(t).Containerz() - cli := clnt.NewClientFromStub(containerz) - for { - select { - case <-ctx.Done(): - t.Fatalf("Ping failed after timeout, last error: %v", lastErr) - default: - } - - _, err := client.Ping(ctx, &cpb.PingRequest{}) - if err == nil { - t.Log("Successfully pinged cntrsrv.") - return // Success - } - lastErr = err - retry++ - t.Logf("Ping failed, retrying in 2 seconds... (attempt=%d, error=%v)", retry, err) - time.Sleep(2 * time.Second) - if retry%3 == 0 { - listCh, listErr := cli.ListContainer(ctx, true, 0, map[string][]string{"name": {instanceName}}) - if listErr != nil { - t.Logf("ListContainer during ping retry failed: %v", listErr) - } else { - for info := range listCh { - if info.Error != nil { - t.Logf("ListContainer stream error during retry: %v", info.Error) - continue - } - t.Logf("Retry container state: name=%s state=%s image=%s labels=%v", info.Name, info.State, info.ImageName, info.ID) - } - } - } - } -} - -// DUTCredentialer is an interface for getting credentials from a DUT binding. -type DUTCredentialer interface { - RPCUsername() string - RPCPassword() string -} - -// TestDialLocal implements CNTR-3, validating that it is possible for a -// container running on the device to connect to local gRPC services that are -// running on the DUT. -func TestDialLocal(t *testing.T) { - dut := ondatra.DUT(t, "dut1") - cleanup := setupContainer(t, dut) - defer cleanup() - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - conn := dialContainer(t, ctx, dut, cntrPort) - defer conn.Close() - client := cpb.NewCntrClient(conn) - - var lastErr error - for { - if ctx.Err() != nil { - t.Fatalf("Timed out waiting for container gRPC server to be ready, last error: %v", lastErr) - } - _, lastErr = client.Ping(ctx, &cpb.PingRequest{}) - if lastErr == nil { - t.Log("Successfully pinged cntrsrv.") - break // Success - } - t.Logf("Ping failed, retrying in 2 seconds... (error: %v)", lastErr) - time.Sleep(2 * time.Second) - } - - var creds DUTCredentialer - if err := binding.DUTAs(dut.RawAPIs().BindingDUT(), &creds); err != nil { - t.Fatalf("Failed to get DUT credentials using binding.DUTAs: %v. The binding for %s must implement the DUTCredentialer interface.", err, dut.Name()) - } - username := creds.RPCUsername() - password := creds.RPCPassword() - - var dialAddr string - dialAddr = "[::]" - if deviations.LocalhostForContainerz(dut) { - dialAddr = "[fd01::1]" - } - - // Establish a gRIBI client to program gRIBI entries. - gribiClient := gribi.Client{ - DUT: dut, - FIBACK: true, - Persistence: true, - } - if err := gribiClient.Start(t); err != nil { - t.Fatalf("gRIBI Connection can not be established") - } - - gribiClient.BecomeLeader(t) - gribiClient.FlushAll(t) - defer gribiClient.Close(t) - defer gribiClient.FlushAll(t) - - //Program a sample gRIBI Entry on DUT for gRIBI Get query response. - gribiClient.AddNH(t, 2001, "Decap", deviations.DefaultNetworkInstance(dut), fluent.InstalledInFIB) - gribiClient.AddNHG(t, 201, map[uint64]uint64{2001: 1}, deviations.DefaultNetworkInstance(dut), fluent.InstalledInFIB) - - tests := []struct { - desc string - inMsg *cpb.DialRequest - wantResp bool - wantErr bool - }{{ - desc: "dial gNMI", - inMsg: &cpb.DialRequest{ - Addr: dialAddr + ":9339", - Username: username, - Password: password, - Request: &cpb.DialRequest_Srv{ - Srv: cpb.Service_ST_GNMI, - }, - }, - wantResp: true, - }, { - desc: "dial gRIBI", - inMsg: &cpb.DialRequest{ - Addr: dialAddr + ":9340", - Username: username, - Password: password, - Request: &cpb.DialRequest_Srv{ - Srv: cpb.Service_ST_GRIBI, - }, - }, - wantResp: true, - }, { - desc: "dial something not listening", - inMsg: &cpb.DialRequest{ - Addr: dialAddr + ":4242", - Username: username, - Password: password, - Request: &cpb.DialRequest_Srv{ - Srv: cpb.Service_ST_GRIBI, - }, - }, - wantErr: true, - }} - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - tctx, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() - // Use the client established before the sub-tests. - got, err := client.Dial(tctx, tt.inMsg) - // For the gRIBI Get RPC, an EOF error is returned if the server has no entries to - // stream back. This is the expected behavior for a successful connection to a - // gRIBI service on a device with an empty RIB, so we treat it as a non-error. - if tt.inMsg.GetSrv() == cpb.Service_ST_GRIBI && err != nil && strings.Contains(err.Error(), "EOF") { - err = nil - } - if (err != nil) != tt.wantErr { - t.Fatalf("Dial(): got unexpected error, err: %v, wantErr? %v", err, tt.wantErr) - } - - t.Logf("got response: %s", prototext.Format(got)) - if (got != nil) != tt.wantResp { - t.Fatalf("Dial: did not get correct response, got: %s, wantResponse? %v", prototext.Format(got), tt.wantResp) - } - }) - } -} - -// TestConnectRemote implements CNTR-4, validating that it is possible for a container to connect to a container -// on an adjacent node via gRPC using IPv6 link local addresses. r0 and r1 in the topology are configured with -// IPv6 link-local addresses via gNMI, and the CNTR service is used to trigger a connection between the two addresses. -// -// The test is repeated for r0 --> r1 and r1 --> r0. -func TestConnectRemote(t *testing.T) { - t.Skipf("TODO(abhinavkmr): Testing pending on device. Skipping for now!") - configureIPv6Addr := func(dut *ondatra.DUTDevice, name, addr string) { - t.Helper() - pn := dut.Port(t, name).Name() - - d := &oc.Interface{ - Name: ygot.String(pn), - Type: oc.IETFInterfaces_InterfaceType_ethernetCsmacd, - Enabled: ygot.Bool(true), - } - s := d.GetOrCreateSubinterface(0) - s.GetOrCreateIpv4().Enabled = ygot.Bool(true) - v6 := s.GetOrCreateIpv6() - // TODO(robjs): Clarify whether IPv4 enabled is required here for multiple - // targets, otherwise add a deviation. - v6.Enabled = ygot.Bool(true) - a := v6.GetOrCreateAddress(addr) - a.PrefixLength = ygot.Uint8(64) - a.Type = oc.IfIp_Ipv6AddressType_LINK_LOCAL_UNICAST - gnmi.Replace(t, dut, gnmi.OC().Interface(pn).Config(), d) - - time.Sleep(1 * time.Second) - } - - r0 := ondatra.DUT(t, "dut1") - r1 := ondatra.DUT(t, "dut2") - - cleanup0 := setupContainer(t, r0) - defer cleanup0() - cleanup1 := setupContainer(t, r1) - defer cleanup1() - - configureIPv6Addr(r0, "port1", "fe80::cafe:1") - configureIPv6Addr(r1, "port2", "fe80::cafe:2") - - validateIPv6Present := func(dut *ondatra.DUTDevice, name string) { - // Check that there is a configured IPv6 address on the interface. - t.Helper() - // TODO(robjs): Validate expectations as to whether autoconf link-local is returned - // here. - v6addr := gnmi.GetAll(t, dut, gnmi.OC().Interface(dut.Port(t, name).Name()).SubinterfaceAny().Ipv6().AddressAny().State()) - if len(v6addr) < 1 { - t.Fatalf("%s: did not get a configured IPv6 address, got: %d (%s), want: 1", dut.Name(), len(v6addr), pretty.Sprint(v6addr)) - } - } - - validateIPv6Present(r0, "port1") - validateIPv6Present(r1, "port2") - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - containerInterfaceName := func(t *testing.T, d *ondatra.DUTDevice, port *ondatra.Port) string { - portName := port.Name() - if parts := strings.Split(port.Name(), ":"); len(parts) == 2 { - portName = parts[1] - } - switch d.Vendor() { - case ondatra.ARISTA: - switch { - case strings.HasPrefix(portName, "Ethernet"): - num, _ := strings.CutPrefix(portName, "Ethernet") - return fmt.Sprintf("eth%s", num) - } - } - t.Fatalf("cannot resolve interface name into Linux interface name, %s -> %s", d.Vendor(), portName) - return "" - } - - tests := []struct { - desc string - inRemoteAddr string - inDialer *ondatra.DUTDevice - dialerPort string - }{{ - desc: "r1->r0", - inRemoteAddr: "fe80::cafe:1", - inDialer: r1, - dialerPort: "port2", - }, { - desc: "r0->r1", - inRemoteAddr: "fe80::cafe:2", - inDialer: r0, - dialerPort: "port1", - }} - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - conn := dialContainer(t, ctx, tt.inDialer, cntrPort) - dialAddr := fmt.Sprintf("[%s%%25%s]:%d", tt.inRemoteAddr, containerInterfaceName(t, tt.inDialer, tt.inDialer.Port(t, tt.dialerPort)), cntrPort) - t.Logf("dialing remote address %s", dialAddr) - - client := cpb.NewCntrClient(conn) - got, err := client.Dial(ctx, &cpb.DialRequest{ - Addr: dialAddr, - Request: &cpb.DialRequest_Ping{ - Ping: &cpb.PingRequest{}, - }, - }) - if err != nil { - t.Fatalf("could not make request to remote device, got err: %v", err) - } - t.Logf("got response, %s", prototext.Format(got)) - }) - } -} From cf64b97ba6bd77d905ea59f3461821553880ce6c Mon Sep 17 00:00:00 2001 From: Shivashankar Paulchamy Date: Wed, 20 May 2026 07:33:48 -0700 Subject: [PATCH 3/4] Restore cntr_test.go (undo accidental deletion) --- .../tests/container_connectivity/cntr_test.go | 398 ++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 feature/container/networking/tests/container_connectivity/cntr_test.go diff --git a/feature/container/networking/tests/container_connectivity/cntr_test.go b/feature/container/networking/tests/container_connectivity/cntr_test.go new file mode 100644 index 00000000000..87dab0e67d0 --- /dev/null +++ b/feature/container/networking/tests/container_connectivity/cntr_test.go @@ -0,0 +1,398 @@ +/* + Copyright 2022 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 + + https://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 cntr_test implements an ONDATRA test for container functionalities +// as described in the CNTR-[234] tests in README.md. +package cntr_test + +import ( + "context" + "flag" + "fmt" + "strings" + "testing" + "time" + + "github.com/kr/pretty" + clnt "github.com/openconfig/containerz/client" + cpb "github.com/openconfig/featureprofiles/internal/cntrsrv/proto/cntr" + "github.com/openconfig/featureprofiles/internal/containerztest" + "github.com/openconfig/featureprofiles/internal/deviations" + "github.com/openconfig/featureprofiles/internal/fptest" + "github.com/openconfig/featureprofiles/internal/gribi" + "github.com/openconfig/gribigo/fluent" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/binding" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygot/ygot" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/prototext" +) + +func TestMain(m *testing.M) { + fptest.RunTests(m) +} + +var ( + containerTar = flag.String("container_tar", "cntrsrv.tar", "The container tarball to deploy.") + // containerTarPath returns the path to the container tarball. + // This can be overridden for internal testing behavior using init(). + containerTarPath = func(t *testing.T) string { + return *containerTar + } +) + +const ( + imageName = "cntrsrv_image" + instanceName = "cntr-test-conn" + cntrPort = 60061 +) + +// setupContainer deploys and starts the cntrsrv container on the DUT. +// It returns a function to clean up the container. +func setupContainer(t *testing.T, dut *ondatra.DUTDevice) func() { + t.Helper() + ctx := context.Background() + opts := containerztest.StartContainerOptions{ + ImageName: imageName, + InstanceName: instanceName, + Command: fmt.Sprintf("./cntrsrv --port=%d", cntrPort), + TarPath: containerTarPath(t), + Network: "host", + PollForRunningState: true, + } + t.Logf("Starting container %q with host networking on port %d using tar %q", instanceName, cntrPort, containerTarPath(t)) + _, cleanup := containerztest.Setup(ctx, t, dut, opts) + t.Logf("Container %q setup completed", instanceName) + return cleanup +} + +// dialContainer dials a gRPC service running on a container on a device at the specified port. +func dialContainer(t *testing.T, ctx context.Context, dut *ondatra.DUTDevice, port int) *grpc.ClientConn { + t.Helper() + var dialer interface { + DialGRPCWithPort(context.Context, int, ...grpc.DialOption) (*grpc.ClientConn, error) + } + bindingDUT := dut.RawAPIs().BindingDUT() + if err := binding.DUTAs(bindingDUT, &dialer); err != nil { + t.Skipf("BindingDUT %T does not implement DialGRPCWithPort, which is required for this test: %v", bindingDUT, err) + } + + conn, err := dialer.DialGRPCWithPort(ctx, port) + if err != nil { + t.Fatalf("DialGRPCWithPort failed: %v", err) + } + return conn +} + +// TestDial implements CNTR-2, validating that it is possible for an external caller to dial into a service +// running in a container on a DUT. The service used is the cntr service defined by cntr.proto. +func TestDial(t *testing.T) { + dut := ondatra.DUT(t, "dut1") + cleanup := setupContainer(t, dut) + defer cleanup() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + conn := dialContainer(t, ctx, dut, cntrPort) + defer conn.Close() + + client := cpb.NewCntrClient(conn) + + // The container can be in a RUNNING state before the gRPC server inside is + // ready to accept connections. We retry the Ping RPC to handle this race + // condition. + var lastErr error + var retry int + containerz := dut.RawAPIs().GNOI(t).Containerz() + cli := clnt.NewClientFromStub(containerz) + for { + select { + case <-ctx.Done(): + t.Fatalf("Ping failed after timeout, last error: %v", lastErr) + default: + } + + _, err := client.Ping(ctx, &cpb.PingRequest{}) + if err == nil { + t.Log("Successfully pinged cntrsrv.") + return // Success + } + lastErr = err + retry++ + t.Logf("Ping failed, retrying in 2 seconds... (attempt=%d, error=%v)", retry, err) + time.Sleep(2 * time.Second) + if retry%3 == 0 { + listCh, listErr := cli.ListContainer(ctx, true, 0, map[string][]string{"name": {instanceName}}) + if listErr != nil { + t.Logf("ListContainer during ping retry failed: %v", listErr) + } else { + for info := range listCh { + if info.Error != nil { + t.Logf("ListContainer stream error during retry: %v", info.Error) + continue + } + t.Logf("Retry container state: name=%s state=%s image=%s labels=%v", info.Name, info.State, info.ImageName, info.ID) + } + } + } + } +} + +// DUTCredentialer is an interface for getting credentials from a DUT binding. +type DUTCredentialer interface { + RPCUsername() string + RPCPassword() string +} + +// TestDialLocal implements CNTR-3, validating that it is possible for a +// container running on the device to connect to local gRPC services that are +// running on the DUT. +func TestDialLocal(t *testing.T) { + dut := ondatra.DUT(t, "dut1") + cleanup := setupContainer(t, dut) + defer cleanup() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + conn := dialContainer(t, ctx, dut, cntrPort) + defer conn.Close() + client := cpb.NewCntrClient(conn) + + var lastErr error + for { + if ctx.Err() != nil { + t.Fatalf("Timed out waiting for container gRPC server to be ready, last error: %v", lastErr) + } + _, lastErr = client.Ping(ctx, &cpb.PingRequest{}) + if lastErr == nil { + t.Log("Successfully pinged cntrsrv.") + break // Success + } + t.Logf("Ping failed, retrying in 2 seconds... (error: %v)", lastErr) + time.Sleep(2 * time.Second) + } + + var creds DUTCredentialer + if err := binding.DUTAs(dut.RawAPIs().BindingDUT(), &creds); err != nil { + t.Fatalf("Failed to get DUT credentials using binding.DUTAs: %v. The binding for %s must implement the DUTCredentialer interface.", err, dut.Name()) + } + username := creds.RPCUsername() + password := creds.RPCPassword() + + var dialAddr string + dialAddr = "[::]" + if deviations.LocalhostForContainerz(dut) { + dialAddr = "[fd01::1]" + } + + // Establish a gRIBI client to program gRIBI entries. + gribiClient := gribi.Client{ + DUT: dut, + FIBACK: true, + Persistence: true, + } + if err := gribiClient.Start(t); err != nil { + t.Fatalf("gRIBI Connection can not be established") + } + + gribiClient.BecomeLeader(t) + gribiClient.FlushAll(t) + defer gribiClient.Close(t) + defer gribiClient.FlushAll(t) + + //Program a sample gRIBI Entry on DUT for gRIBI Get query response. + gribiClient.AddNH(t, 2001, "Decap", deviations.DefaultNetworkInstance(dut), fluent.InstalledInFIB) + gribiClient.AddNHG(t, 201, map[uint64]uint64{2001: 1}, deviations.DefaultNetworkInstance(dut), fluent.InstalledInFIB) + + tests := []struct { + desc string + inMsg *cpb.DialRequest + wantResp bool + wantErr bool + }{{ + desc: "dial gNMI", + inMsg: &cpb.DialRequest{ + Addr: dialAddr + ":9339", + Username: username, + Password: password, + Request: &cpb.DialRequest_Srv{ + Srv: cpb.Service_ST_GNMI, + }, + }, + wantResp: true, + }, { + desc: "dial gRIBI", + inMsg: &cpb.DialRequest{ + Addr: dialAddr + ":9340", + Username: username, + Password: password, + Request: &cpb.DialRequest_Srv{ + Srv: cpb.Service_ST_GRIBI, + }, + }, + wantResp: true, + }, { + desc: "dial something not listening", + inMsg: &cpb.DialRequest{ + Addr: dialAddr + ":4242", + Username: username, + Password: password, + Request: &cpb.DialRequest_Srv{ + Srv: cpb.Service_ST_GRIBI, + }, + }, + wantErr: true, + }} + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + tctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + // Use the client established before the sub-tests. + got, err := client.Dial(tctx, tt.inMsg) + // For the gRIBI Get RPC, an EOF error is returned if the server has no entries to + // stream back. This is the expected behavior for a successful connection to a + // gRIBI service on a device with an empty RIB, so we treat it as a non-error. + if tt.inMsg.GetSrv() == cpb.Service_ST_GRIBI && err != nil && strings.Contains(err.Error(), "EOF") { + err = nil + } + if (err != nil) != tt.wantErr { + t.Fatalf("Dial(): got unexpected error, err: %v, wantErr? %v", err, tt.wantErr) + } + + t.Logf("got response: %s", prototext.Format(got)) + if (got != nil) != tt.wantResp { + t.Fatalf("Dial: did not get correct response, got: %s, wantResponse? %v", prototext.Format(got), tt.wantResp) + } + }) + } +} + +// TestConnectRemote implements CNTR-4, validating that it is possible for a container to connect to a container +// on an adjacent node via gRPC using IPv6 link local addresses. r0 and r1 in the topology are configured with +// IPv6 link-local addresses via gNMI, and the CNTR service is used to trigger a connection between the two addresses. +// +// The test is repeated for r0 --> r1 and r1 --> r0. +func TestConnectRemote(t *testing.T) { + t.Skipf("TODO(abhinavkmr): Testing pending on device. Skipping for now!") + configureIPv6Addr := func(dut *ondatra.DUTDevice, name, addr string) { + t.Helper() + pn := dut.Port(t, name).Name() + + d := &oc.Interface{ + Name: ygot.String(pn), + Type: oc.IETFInterfaces_InterfaceType_ethernetCsmacd, + Enabled: ygot.Bool(true), + } + s := d.GetOrCreateSubinterface(0) + s.GetOrCreateIpv4().Enabled = ygot.Bool(true) + v6 := s.GetOrCreateIpv6() + // TODO(robjs): Clarify whether IPv4 enabled is required here for multiple + // targets, otherwise add a deviation. + v6.Enabled = ygot.Bool(true) + a := v6.GetOrCreateAddress(addr) + a.PrefixLength = ygot.Uint8(64) + a.Type = oc.IfIp_Ipv6AddressType_LINK_LOCAL_UNICAST + gnmi.Replace(t, dut, gnmi.OC().Interface(pn).Config(), d) + + time.Sleep(1 * time.Second) + } + + r0 := ondatra.DUT(t, "dut1") + r1 := ondatra.DUT(t, "dut2") + + cleanup0 := setupContainer(t, r0) + defer cleanup0() + cleanup1 := setupContainer(t, r1) + defer cleanup1() + + configureIPv6Addr(r0, "port1", "fe80::cafe:1") + configureIPv6Addr(r1, "port2", "fe80::cafe:2") + + validateIPv6Present := func(dut *ondatra.DUTDevice, name string) { + // Check that there is a configured IPv6 address on the interface. + t.Helper() + // TODO(robjs): Validate expectations as to whether autoconf link-local is returned + // here. + v6addr := gnmi.GetAll(t, dut, gnmi.OC().Interface(dut.Port(t, name).Name()).SubinterfaceAny().Ipv6().AddressAny().State()) + if len(v6addr) < 1 { + t.Fatalf("%s: did not get a configured IPv6 address, got: %d (%s), want: 1", dut.Name(), len(v6addr), pretty.Sprint(v6addr)) + } + } + + validateIPv6Present(r0, "port1") + validateIPv6Present(r1, "port2") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + containerInterfaceName := func(t *testing.T, d *ondatra.DUTDevice, port *ondatra.Port) string { + portName := port.Name() + if parts := strings.Split(port.Name(), ":"); len(parts) == 2 { + portName = parts[1] + } + switch d.Vendor() { + case ondatra.ARISTA: + switch { + case strings.HasPrefix(portName, "Ethernet"): + num, _ := strings.CutPrefix(portName, "Ethernet") + return fmt.Sprintf("eth%s", num) + } + } + t.Fatalf("cannot resolve interface name into Linux interface name, %s -> %s", d.Vendor(), portName) + return "" + } + + tests := []struct { + desc string + inRemoteAddr string + inDialer *ondatra.DUTDevice + dialerPort string + }{{ + desc: "r1->r0", + inRemoteAddr: "fe80::cafe:1", + inDialer: r1, + dialerPort: "port2", + }, { + desc: "r0->r1", + inRemoteAddr: "fe80::cafe:2", + inDialer: r0, + dialerPort: "port1", + }} + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + conn := dialContainer(t, ctx, tt.inDialer, cntrPort) + dialAddr := fmt.Sprintf("[%s%%25%s]:%d", tt.inRemoteAddr, containerInterfaceName(t, tt.inDialer, tt.inDialer.Port(t, tt.dialerPort)), cntrPort) + t.Logf("dialing remote address %s", dialAddr) + + client := cpb.NewCntrClient(conn) + got, err := client.Dial(ctx, &cpb.DialRequest{ + Addr: dialAddr, + Request: &cpb.DialRequest_Ping{ + Ping: &cpb.PingRequest{}, + }, + }) + if err != nil { + t.Fatalf("could not make request to remote device, got err: %v", err) + } + t.Logf("got response, %s", prototext.Format(got)) + }) + } +} From 0d00ac59965cbc2568b07a127bad1a47e8986d47 Mon Sep 17 00:00:00 2001 From: Shivashankar Paulchamy Date: Wed, 20 May 2026 07:38:50 -0700 Subject: [PATCH 4/4] Revert unintended cntr_test.go changes --- .../tests/container_connectivity/cntr_test.go | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/feature/container/networking/tests/container_connectivity/cntr_test.go b/feature/container/networking/tests/container_connectivity/cntr_test.go index 87dab0e67d0..04c71b9d2d6 100644 --- a/feature/container/networking/tests/container_connectivity/cntr_test.go +++ b/feature/container/networking/tests/container_connectivity/cntr_test.go @@ -27,8 +27,6 @@ import ( "time" "github.com/kr/pretty" - clnt "github.com/openconfig/containerz/client" - cpb "github.com/openconfig/featureprofiles/internal/cntrsrv/proto/cntr" "github.com/openconfig/featureprofiles/internal/containerztest" "github.com/openconfig/featureprofiles/internal/deviations" "github.com/openconfig/featureprofiles/internal/fptest" @@ -41,6 +39,8 @@ import ( "github.com/openconfig/ygot/ygot" "google.golang.org/grpc" "google.golang.org/protobuf/encoding/prototext" + + cpb "github.com/openconfig/featureprofiles/internal/cntrsrv/proto/cntr" ) func TestMain(m *testing.M) { @@ -48,7 +48,7 @@ func TestMain(m *testing.M) { } var ( - containerTar = flag.String("container_tar", "cntrsrv.tar", "The container tarball to deploy.") + containerTar = flag.String("container_tar", "/tmp/cntrsrv.tar", "The container tarball to deploy.") // containerTarPath returns the path to the container tarball. // This can be overridden for internal testing behavior using init(). containerTarPath = func(t *testing.T) string { @@ -75,9 +75,7 @@ func setupContainer(t *testing.T, dut *ondatra.DUTDevice) func() { Network: "host", PollForRunningState: true, } - t.Logf("Starting container %q with host networking on port %d using tar %q", instanceName, cntrPort, containerTarPath(t)) _, cleanup := containerztest.Setup(ctx, t, dut, opts) - t.Logf("Container %q setup completed", instanceName) return cleanup } @@ -117,9 +115,6 @@ func TestDial(t *testing.T) { // ready to accept connections. We retry the Ping RPC to handle this race // condition. var lastErr error - var retry int - containerz := dut.RawAPIs().GNOI(t).Containerz() - cli := clnt.NewClientFromStub(containerz) for { select { case <-ctx.Done(): @@ -133,23 +128,8 @@ func TestDial(t *testing.T) { return // Success } lastErr = err - retry++ - t.Logf("Ping failed, retrying in 2 seconds... (attempt=%d, error=%v)", retry, err) + t.Logf("Ping failed, retrying in 2 seconds... (error: %v)", err) time.Sleep(2 * time.Second) - if retry%3 == 0 { - listCh, listErr := cli.ListContainer(ctx, true, 0, map[string][]string{"name": {instanceName}}) - if listErr != nil { - t.Logf("ListContainer during ping retry failed: %v", listErr) - } else { - for info := range listCh { - if info.Error != nil { - t.Logf("ListContainer stream error during retry: %v", info.Error) - continue - } - t.Logf("Retry container state: name=%s state=%s image=%s labels=%v", info.Name, info.State, info.ImageName, info.ID) - } - } - } } }