github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/gnmireverse/client/client_test.go (about)

     1  // Copyright (c) 2020 Arista Networks, Inc.
     2  // Use of this source code is governed by the Apache License 2.0
     3  // that can be found in the COPYING file.
     4  
     5  package client
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"net"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/aristanetworks/glog"
    15  	"github.com/aristanetworks/goarista/gnmireverse"
    16  	"github.com/openconfig/gnmi/proto/gnmi"
    17  	"golang.org/x/sync/errgroup"
    18  	"google.golang.org/grpc"
    19  	"google.golang.org/grpc/codes"
    20  	"google.golang.org/grpc/status"
    21  	"google.golang.org/protobuf/proto"
    22  )
    23  
    24  func TestSampleList(t *testing.T) {
    25  	for name, tc := range map[string]struct {
    26  		arg string
    27  
    28  		error    bool
    29  		path     *gnmi.Path
    30  		interval time.Duration
    31  	}{
    32  		"working": {
    33  			arg: "/foos/foo[name=bar]/baz@30s",
    34  
    35  			path: &gnmi.Path{Elem: []*gnmi.PathElem{
    36  				&gnmi.PathElem{Name: "foos"},
    37  				&gnmi.PathElem{Name: "foo",
    38  					Key: map[string]string{"name": "bar"}},
    39  				&gnmi.PathElem{Name: "baz"},
    40  			}},
    41  			interval: 30 * time.Second,
    42  		},
    43  		"no_interval": {
    44  			arg:   "/foos/foo[name=bar]/baz",
    45  			error: true,
    46  		},
    47  		"empty_interval": {
    48  			arg:   "/foos/foo[name=bar]/baz@",
    49  			error: true,
    50  		},
    51  		"invalid_path": {
    52  			arg:   "/foos/foo[name=bar]]/baz@30s",
    53  			error: true,
    54  		},
    55  	} {
    56  		t.Run(name, func(t *testing.T) {
    57  			var l sampleList
    58  			err := l.Set(tc.arg)
    59  			if err != nil {
    60  				if !tc.error {
    61  					t.Fatalf("unexpected error: %s", err)
    62  				}
    63  				return
    64  			} else if tc.error {
    65  				t.Fatal("expected error and didn't get one")
    66  			}
    67  
    68  			sub := l.subs[0]
    69  			sub.p.Element = nil // Ignore the backward compatible path
    70  			if !proto.Equal(tc.path, sub.p) {
    71  				t.Errorf("Paths don't match. Expected: %s Got: %s",
    72  					tc.path, sub.p)
    73  			}
    74  			if tc.interval != sub.interval {
    75  				t.Errorf("Intervals don't match. Expected %s Got: %s",
    76  					tc.interval, sub.interval)
    77  			}
    78  			str := l.String()
    79  			if tc.arg != str {
    80  				t.Errorf("Unexpected String() result: Expected: %q Got: %q", tc.arg, str)
    81  			}
    82  		})
    83  	}
    84  }
    85  
    86  func TestSubscriptionList(t *testing.T) {
    87  	for name, tc := range map[string]struct {
    88  		arg string
    89  
    90  		error    bool
    91  		path     *gnmi.Path
    92  		interval time.Duration
    93  	}{
    94  		"working": {
    95  			arg: "/foos/foo[name=bar]/baz@30s",
    96  
    97  			path: &gnmi.Path{Elem: []*gnmi.PathElem{
    98  				&gnmi.PathElem{Name: "foos"},
    99  				&gnmi.PathElem{Name: "foo",
   100  					Key: map[string]string{"name": "bar"}},
   101  				&gnmi.PathElem{Name: "baz"},
   102  			}},
   103  			interval: 30 * time.Second,
   104  		},
   105  		"no_interval": {
   106  			arg: "/foos/foo[name=bar]/baz",
   107  			path: &gnmi.Path{Elem: []*gnmi.PathElem{
   108  				&gnmi.PathElem{Name: "foos"},
   109  				&gnmi.PathElem{Name: "foo",
   110  					Key: map[string]string{"name": "bar"}},
   111  				&gnmi.PathElem{Name: "baz"},
   112  			}},
   113  		},
   114  		"empty_interval": {
   115  			arg:   "/foos/foo[name=bar]/baz@",
   116  			error: true,
   117  		},
   118  		"invalid_path": {
   119  			arg:   "/foos/foo[name=bar]]/baz@30s",
   120  			error: true,
   121  		},
   122  	} {
   123  		t.Run(name, func(t *testing.T) {
   124  			var l subscriptionList
   125  			err := l.Set(tc.arg)
   126  			if err != nil {
   127  				if !tc.error {
   128  					t.Fatalf("unexpected error: %s", err)
   129  				}
   130  				return
   131  			} else if tc.error {
   132  				t.Fatal("expected error and didn't get one")
   133  			}
   134  
   135  			sub := l.subs[0]
   136  			sub.p.Element = nil // Ignore the backward compatible path
   137  			if !proto.Equal(tc.path, sub.p) {
   138  				t.Errorf("Paths don't match. Expected: %s Got: %s",
   139  					tc.path, sub.p)
   140  			}
   141  			if tc.interval != sub.interval {
   142  				t.Errorf("Intervals don't match. Expected %s Got: %s",
   143  					tc.interval, sub.interval)
   144  			}
   145  			str := l.String()
   146  			if tc.arg != str {
   147  				t.Errorf("Unexpected String() result: Expected: %q Got: %q", tc.arg, str)
   148  			}
   149  		})
   150  	}
   151  }
   152  
   153  func TestParseCredentialsFile(t *testing.T) {
   154  	for _, tc := range []struct {
   155  		name            string
   156  		config          *config
   157  		credentialsFile []byte
   158  		username        string
   159  		password        string
   160  		err             bool
   161  	}{{
   162  		name:   "empty config, username/password in credentials file",
   163  		config: &config{},
   164  		credentialsFile: []byte(`
   165  username: admin
   166  password: pass123
   167  `),
   168  		username: "admin",
   169  		password: "pass123",
   170  	}, {
   171  		name:   "empty config, only username in credentials file",
   172  		config: &config{},
   173  		credentialsFile: []byte(`
   174  username: admin
   175  `),
   176  		username: "admin",
   177  	}, {
   178  		name: "username in config, only password in credentials file",
   179  		config: &config{
   180  			username: "admin",
   181  		},
   182  		credentialsFile: []byte(`
   183  password: pass123
   184  `),
   185  		username: "admin",
   186  		password: "pass123",
   187  	}, {
   188  		name: "username/password in config takes precedence over credentials file",
   189  		config: &config{
   190  			username: "admin",
   191  			password: "pass123",
   192  		},
   193  		credentialsFile: []byte(`
   194  username: bob
   195  password: secret123
   196  `),
   197  		username: "admin",
   198  		password: "pass123",
   199  	}, {
   200  		name: "username/password in config, empty credentials file",
   201  		config: &config{
   202  			username: "admin",
   203  			password: "pass123",
   204  		},
   205  		username: "admin",
   206  		password: "pass123",
   207  	}, {
   208  		name:   "username/password in credentials file with newlines, quotes and spaces",
   209  		config: &config{},
   210  		credentialsFile: []byte(`
   211  username: "ad min"
   212  
   213  password: pass 123
   214  `),
   215  		username: "ad min",
   216  		password: "pass 123",
   217  	}, {
   218  		name:   "unknown field in credentials file",
   219  		config: &config{},
   220  		credentialsFile: []byte(`
   221  username: admin
   222  password: pass123
   223  unknown: unknown
   224  `),
   225  		err: true,
   226  	}} {
   227  		t.Run(tc.name, func(t *testing.T) {
   228  			if err := tc.config.parseCredentialsFile(tc.credentialsFile); tc.err != (err != nil) {
   229  				t.Errorf("want error %t, got error: %s", tc.err, err)
   230  			}
   231  			if tc.config.username != tc.username {
   232  				t.Errorf("want username: %q, got: %q", tc.username, tc.config.username)
   233  			}
   234  			if tc.config.password != tc.password {
   235  				t.Errorf("want password: %q, got: %q", tc.password, tc.config.password)
   236  			}
   237  		})
   238  	}
   239  }
   240  
   241  func TestStreamGetResponses(t *testing.T) {
   242  	// Set the Get paths list.
   243  	var cfgGetList getList
   244  	cfgGetList.Set("/foo/bar")
   245  
   246  	cfg := &config{
   247  		targetVal:         "baz",
   248  		targetAddr:        getTestAddress(t),
   249  		collectorAddr:     getTestAddress(t),
   250  		getPaths:          cfgGetList,
   251  		getSampleInterval: time.Second,
   252  	}
   253  
   254  	collectorErrChan := make(chan error, 1)
   255  	gnmiServer := &gnmiServer{}
   256  	gnmireverseServer := &gnmireverseServer{
   257  		errChan: collectorErrChan,
   258  	}
   259  	runStreamGetResponsesTest(t, cfg, collectorErrChan,
   260  		gnmiServer, gnmireverseServer, streamGetResponses)
   261  }
   262  
   263  // getTestAddress gets a localhost address with a random unused port.
   264  func getTestAddress(t *testing.T) string {
   265  	listener, err := net.Listen("tcp", "127.0.0.1:0")
   266  	if err != nil {
   267  		t.Fatalf("failed to find an available port: %s", err)
   268  	}
   269  	defer listener.Close()
   270  	return listener.Addr().String()
   271  }
   272  
   273  // runStreamGetResponsesTest runs the gNMIReverse Get client with the mock gNMI server and
   274  // mock gNMIReverse server and checks if the collectorErrChan receives an error.
   275  func runStreamGetResponsesTest(t *testing.T, cfg *config, collectorErrChan chan error,
   276  	gnmiServer gnmi.GNMIServer, gnmireverseServer gnmireverse.GNMIReverseServer,
   277  	streamResponsesFunc func(*config, *grpc.ClientConn, *grpc.ClientConn) func(
   278  		context.Context, *errgroup.Group)) {
   279  	// Start the mock gNMI target server.
   280  	targetGRPCServer := grpc.NewServer()
   281  	gnmi.RegisterGNMIServer(targetGRPCServer, gnmiServer)
   282  	targetListener, err := net.Listen("tcp", cfg.targetAddr)
   283  	if err != nil {
   284  		t.Fatal(err)
   285  	}
   286  	glog.V(1).Infof("gNMI target server listening on %s", cfg.targetAddr)
   287  	go func() {
   288  		if err := targetGRPCServer.Serve(targetListener); err != nil {
   289  			t.Error(err)
   290  		}
   291  	}()
   292  
   293  	// Start the mock gNMIReverse collector server.
   294  	collectorGRPCServer := grpc.NewServer()
   295  	gnmireverse.RegisterGNMIReverseServer(collectorGRPCServer, gnmireverseServer)
   296  	collectorListener, err := net.Listen("tcp", cfg.collectorAddr)
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  	glog.V(1).Infof("gNMIReverse collector server listening on %s", cfg.collectorAddr)
   301  	go func() {
   302  		if err := collectorGRPCServer.Serve(collectorListener); err != nil {
   303  			t.Error(err)
   304  		}
   305  	}()
   306  
   307  	// Start the gNMIReverse client to stream GetResponses from target to collector.
   308  	destConn, err := dialCollector(cfg)
   309  	if err != nil {
   310  		glog.Fatalf("error dialing destination %q: %s", cfg.collectorAddr, err)
   311  	}
   312  	targetConn, err := dialTarget(cfg)
   313  	if err != nil {
   314  		glog.Fatalf("error dialing target %q: %s", cfg.targetAddr, err)
   315  	}
   316  	glog.V(1).Infof("gNMIReverse client publish Get response from %s to %s",
   317  		targetConn.Target(), destConn.Target())
   318  	go func() {
   319  		streamResponses(streamResponsesFunc(cfg, destConn, targetConn))
   320  	}()
   321  
   322  	// Check that the gNMIReverse collector server receives the expected Get response.
   323  	if err := <-collectorErrChan; err != nil {
   324  		t.Error(err)
   325  	}
   326  }
   327  
   328  var testNotification = &gnmi.Notification{
   329  	Prefix: &gnmi.Path{
   330  		Target: "baz",
   331  	},
   332  	Update: []*gnmi.Update{{
   333  		Path: &gnmi.Path{
   334  			Elem: []*gnmi.PathElem{
   335  				{Name: "foo"},
   336  				{Name: "bar"},
   337  			},
   338  		},
   339  		Val: &gnmi.TypedValue{
   340  			Value: &gnmi.TypedValue_IntVal{
   341  				IntVal: 1,
   342  			},
   343  		},
   344  	}},
   345  }
   346  
   347  var testGetResponse = &gnmi.GetResponse{
   348  	Notification: []*gnmi.Notification{testNotification},
   349  }
   350  
   351  // Mock gNMIReverse server checks if the published Get response matches the testGetResponse.
   352  type gnmireverseServer struct {
   353  	errChan chan error
   354  	gnmireverse.UnimplementedGNMIReverseServer
   355  }
   356  
   357  func (s *gnmireverseServer) PublishGet(stream gnmireverse.GNMIReverse_PublishGetServer) error {
   358  	for {
   359  		res, err := stream.Recv()
   360  		if err != nil {
   361  			return err
   362  		}
   363  		// Overwrite the timestamp so notification can be compared.
   364  		res.Notification[0].Timestamp = 0
   365  		if !proto.Equal(testGetResponse, res) {
   366  			s.errChan <- fmt.Errorf(
   367  				"Get response not equal: want %v, got %v", testGetResponse, res)
   368  		} else {
   369  			s.errChan <- nil
   370  		}
   371  	}
   372  }
   373  
   374  // Mock gNMI server returns testGetResponse for Get.
   375  type gnmiServer struct {
   376  	gnmi.UnimplementedGNMIServer
   377  }
   378  
   379  func (*gnmiServer) Get(ctx context.Context, req *gnmi.GetRequest) (*gnmi.GetResponse, error) {
   380  	return testGetResponse, nil
   381  }
   382  
   383  func TestCombineGetResponses(t *testing.T) {
   384  	for name, tc := range map[string]struct {
   385  		getResponses        []*gnmi.GetResponse
   386  		combinedGetResponse *gnmi.GetResponse
   387  	}{
   388  		"0_notifs_0_notifs_total_0_notifs": {
   389  			getResponses: []*gnmi.GetResponse{
   390  				{},
   391  				{},
   392  			},
   393  			combinedGetResponse: &gnmi.GetResponse{},
   394  		},
   395  		"1_notif_0_notif_total_1_notif": {
   396  			getResponses: []*gnmi.GetResponse{
   397  				testGetResponse,
   398  				{},
   399  			},
   400  			combinedGetResponse: testGetResponse,
   401  		},
   402  		"0_notif_1_notif_total_1_notif": {
   403  			getResponses: []*gnmi.GetResponse{
   404  				{},
   405  				testEOSNativeGetResponse,
   406  			},
   407  			combinedGetResponse: testEOSNativeGetResponse,
   408  		},
   409  		"1_notif_1_notif_total_2_notifs": {
   410  			getResponses: []*gnmi.GetResponse{
   411  				testGetResponse,
   412  				testEOSNativeGetResponse,
   413  			},
   414  			combinedGetResponse: testCombinedGetResponse,
   415  		},
   416  	} {
   417  		t.Run(name, func(t *testing.T) {
   418  			timestamp := int64(123)
   419  			target := "baz"
   420  			combinedGetResponse := combineGetResponses(timestamp, target, tc.getResponses...)
   421  			if !proto.Equal(tc.combinedGetResponse, combinedGetResponse) {
   422  				t.Errorf("combined Get responses do not match, expected: %v, got: %v",
   423  					tc.combinedGetResponse, combinedGetResponse)
   424  			}
   425  		})
   426  	}
   427  }
   428  
   429  func TestStreamMixedOriginGetResponses(t *testing.T) {
   430  	// Set the Get paths list with one OpenConfig and one EOS native path.
   431  	var cfgGetList getList
   432  	cfgGetList.Set("openconfig:/foo/bar")
   433  	cfgGetList.Set("eos_native:/a/b")
   434  
   435  	cfg := &config{
   436  		targetVal:         "baz",
   437  		targetAddr:        getTestAddress(t),
   438  		collectorAddr:     getTestAddress(t),
   439  		getPaths:          cfgGetList,
   440  		getSampleInterval: time.Second,
   441  	}
   442  
   443  	collectorErrChan := make(chan error, 1)
   444  	gnmiServer := &mixedOriginGNMIServer{}
   445  	gnmireverseServer := &mixedOriginGNMIReverseServer{
   446  		errChan: collectorErrChan,
   447  	}
   448  	runStreamGetResponsesTest(t, cfg, collectorErrChan,
   449  		gnmiServer, gnmireverseServer, streamGetResponses)
   450  }
   451  
   452  var testEOSNativeNotification = &gnmi.Notification{
   453  	Timestamp: 123,
   454  	Prefix: &gnmi.Path{
   455  		Target: "baz",
   456  		Origin: "eos_native",
   457  	},
   458  	Update: []*gnmi.Update{{
   459  		Path: &gnmi.Path{
   460  			Elem: []*gnmi.PathElem{
   461  				{Name: "a"},
   462  				{Name: "b"},
   463  			},
   464  		},
   465  		Val: &gnmi.TypedValue{
   466  			Value: &gnmi.TypedValue_StringVal{
   467  				StringVal: "c",
   468  			},
   469  		},
   470  	}},
   471  }
   472  
   473  var testEOSNativeGetResponse = &gnmi.GetResponse{
   474  	Notification: []*gnmi.Notification{testEOSNativeNotification},
   475  }
   476  
   477  var testCombinedGetResponse = &gnmi.GetResponse{
   478  	Notification: []*gnmi.Notification{testNotification, testEOSNativeNotification},
   479  }
   480  
   481  // Mock gNMIReverse server checks if the published Get response matches the
   482  // testCombinedGetResponse.
   483  type mixedOriginGNMIReverseServer struct {
   484  	errChan chan error
   485  	gnmireverse.UnimplementedGNMIReverseServer
   486  }
   487  
   488  func (s *mixedOriginGNMIReverseServer) PublishGet(
   489  	stream gnmireverse.GNMIReverse_PublishGetServer) error {
   490  	for {
   491  		res, err := stream.Recv()
   492  		if err != nil {
   493  			return err
   494  		}
   495  		// Overwrite the OpenConfig and EOS native notification timestamps so notification
   496  		// can be compared.
   497  		res.Notification[0].Timestamp = 123
   498  		res.Notification[1].Timestamp = 123
   499  		if !proto.Equal(testCombinedGetResponse, res) {
   500  			s.errChan <- fmt.Errorf(
   501  				"Get response not equal: want %v, got %v", testGetResponse, res)
   502  		} else {
   503  			s.errChan <- nil
   504  		}
   505  	}
   506  }
   507  
   508  // Mock gNMI server returns for Get the testGetResponse for OpenConfig origins and
   509  // testEOSNativeGetResponse for EOS native origins.
   510  type mixedOriginGNMIServer struct {
   511  	gnmi.UnimplementedGNMIServer
   512  }
   513  
   514  func (*mixedOriginGNMIServer) Get(
   515  	ctx context.Context, req *gnmi.GetRequest) (*gnmi.GetResponse, error) {
   516  	if req.GetPath()[0].GetOrigin() == "eos_native" {
   517  		return testEOSNativeGetResponse, nil
   518  	}
   519  	return testGetResponse, nil
   520  }
   521  
   522  func TestStreamGetResponsesModeSubscribe(t *testing.T) {
   523  	// Set the Get paths list with one OpenConfig and one EOS native path.
   524  	var cfgGetList getList
   525  	cfgGetList.Set("/foo/bar")
   526  	cfgGetList.Set("eos_native:/a/b")
   527  
   528  	cfg := &config{
   529  		targetVal:         "baz",
   530  		targetAddr:        getTestAddress(t),
   531  		collectorAddr:     getTestAddress(t),
   532  		getPaths:          cfgGetList,
   533  		getSampleInterval: time.Second,
   534  	}
   535  
   536  	collectorErrChan := make(chan error, 1)
   537  	gnmiServer := &gnmiServerGetModeSubscribe{}
   538  	gnmireverseServer := &gnmireverseServerGetModeSubscribe{
   539  		errChan: collectorErrChan,
   540  	}
   541  	runStreamGetResponsesTest(t, cfg, collectorErrChan,
   542  		gnmiServer, gnmireverseServer, streamGetResponsesModeSubscribe)
   543  }
   544  
   545  func TestStreamGetResponsesModeSubscribeOnceNotSupported(t *testing.T) {
   546  	// Set the Get paths list with one OpenConfig and one EOS native path.
   547  	var cfgGetList getList
   548  	cfgGetList.Set("/foo/bar")
   549  	cfgGetList.Set("eos_native:/a/b")
   550  
   551  	cfg := &config{
   552  		targetVal:         "baz",
   553  		targetAddr:        getTestAddress(t),
   554  		collectorAddr:     getTestAddress(t),
   555  		getPaths:          cfgGetList,
   556  		getSampleInterval: time.Second,
   557  	}
   558  
   559  	collectorErrChan := make(chan error, 1)
   560  	gnmiServer := &gnmiServerGetModeSubscribe{
   561  		subscribeOnceNotSupported: true,
   562  	}
   563  	gnmireverseServer := &gnmireverseServerGetModeSubscribe{
   564  		errChan: collectorErrChan,
   565  	}
   566  	runStreamGetResponsesTest(t, cfg, collectorErrChan,
   567  		gnmiServer, gnmireverseServer, streamGetResponsesModeSubscribe)
   568  }
   569  
   570  var testSubscribeResponse = &gnmi.SubscribeResponse{
   571  	Response: &gnmi.SubscribeResponse_Update{
   572  		Update: testNotification,
   573  	},
   574  }
   575  
   576  var testEOSNativeSubscribeResponse = &gnmi.SubscribeResponse{
   577  	Response: &gnmi.SubscribeResponse_Update{
   578  		Update: testEOSNativeNotification,
   579  	},
   580  }
   581  
   582  // Mock gNMIReverse server checks if the published Get response matches the
   583  // testCombinedGetResponse.
   584  type gnmireverseServerGetModeSubscribe struct {
   585  	errChan chan error
   586  	gnmireverse.UnimplementedGNMIReverseServer
   587  }
   588  
   589  func (s *gnmireverseServerGetModeSubscribe) PublishGet(
   590  	stream gnmireverse.GNMIReverse_PublishGetServer) error {
   591  	for {
   592  		res, err := stream.Recv()
   593  		if err != nil {
   594  			return err
   595  		}
   596  		if !proto.Equal(testCombinedGetResponse, res) {
   597  			s.errChan <- fmt.Errorf(
   598  				"Get response not equal: want %v, got %v", testCombinedGetResponse, res)
   599  		} else {
   600  			s.errChan <- nil
   601  		}
   602  	}
   603  }
   604  
   605  // gNMI server which mocks the Subscribe behaviour and sends expected
   606  // responses for OpenConfig and EOS native path subscriptions.
   607  type gnmiServerGetModeSubscribe struct {
   608  	subscribeOnceNotSupported bool
   609  	gnmi.UnimplementedGNMIServer
   610  }
   611  
   612  func (s *gnmiServerGetModeSubscribe) Subscribe(stream gnmi.GNMI_SubscribeServer) error {
   613  	req, err := stream.Recv()
   614  	if err != nil {
   615  		return err
   616  	}
   617  	origin := req.GetSubscribe().GetSubscription()[0].GetPath().GetOrigin()
   618  	switch origin {
   619  	case "":
   620  		return openconfigSubscribePoll(stream, req)
   621  	case "eos_native":
   622  		mode := req.GetSubscribe().GetMode()
   623  		switch mode {
   624  		case gnmi.SubscriptionList_ONCE:
   625  			if s.subscribeOnceNotSupported {
   626  				return status.Errorf(codes.Unimplemented, "Subscribe ONCE mode not supported")
   627  			}
   628  			return eosNativeSubscribeOnce(stream, req)
   629  		case gnmi.SubscriptionList_STREAM:
   630  			return eosNativeSubscribeStream(stream)
   631  		default:
   632  			return fmt.Errorf("unexpected Subscribe mode: %s", mode.String())
   633  		}
   634  	default:
   635  		return fmt.Errorf("unexpected Subscribe origin: %s", origin)
   636  	}
   637  }
   638  
   639  var syncResponse = &gnmi.SubscribeResponse{
   640  	Response: &gnmi.SubscribeResponse_SyncResponse{
   641  		SyncResponse: true,
   642  	},
   643  }
   644  
   645  // openconfigSubscribePoll mocks the server Subscribe POLL behavior.
   646  func openconfigSubscribePoll(stream gnmi.GNMI_SubscribeServer, req *gnmi.SubscribeRequest) error {
   647  	if !(req.GetSubscribe().GetMode() == gnmi.SubscriptionList_POLL &&
   648  		req.GetSubscribe().GetUpdatesOnly()) {
   649  		return fmt.Errorf("expect SubscribeRequest mode=POLL and updates_only=true,"+
   650  			" got mode=%s and updates_only=%t", req.GetSubscribe().GetMode(),
   651  			req.GetSubscribe().GetUpdatesOnly())
   652  	}
   653  	// Send initial sync response.
   654  	if err := stream.Send(syncResponse); err != nil {
   655  		return err
   656  	}
   657  
   658  	for {
   659  		// Await for poll trigger request.
   660  		req, err := stream.Recv()
   661  		if err != nil {
   662  			return err
   663  		}
   664  		if req.GetPoll() == nil {
   665  			return fmt.Errorf("expect Subscribe POLL trigger request, got %s", req)
   666  		}
   667  		// Send one notification.
   668  		if err := stream.Send(testSubscribeResponse); err != nil {
   669  			return err
   670  		}
   671  		// Mark the end of the poll updates with a sync response.
   672  		if err := stream.Send(syncResponse); err != nil {
   673  			return err
   674  		}
   675  	}
   676  }
   677  
   678  // eosNativeSubscribeOnce mocks the server Subscribe ONCE behavior.
   679  func eosNativeSubscribeOnce(stream gnmi.GNMI_SubscribeServer, req *gnmi.SubscribeRequest) error {
   680  	if !req.GetSubscribe().GetUpdatesOnly() {
   681  		// Send one notification.
   682  		if err := stream.Send(testEOSNativeSubscribeResponse); err != nil {
   683  			return err
   684  		}
   685  	}
   686  	// Mark the end of updates with a sync response.
   687  	return stream.Send(syncResponse)
   688  }
   689  
   690  // eosNativeSubscribeStream mocks the server Subscribe STREAM behavior.
   691  func eosNativeSubscribeStream(stream gnmi.GNMI_SubscribeServer) error {
   692  	// Send one notification.
   693  	if err := stream.Send(testEOSNativeSubscribeResponse); err != nil {
   694  		return err
   695  	}
   696  	// Mark the end of updates with a sync response.
   697  	if err := stream.Send(syncResponse); err != nil {
   698  		return err
   699  	}
   700  	// Wait for the gNMIReverse client to close the stream.
   701  	<-stream.Context().Done()
   702  	return nil
   703  }