github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/gnmireverse/client/client.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  	"bufio"
     9  	"context"
    10  	"crypto/tls"
    11  	"crypto/x509"
    12  	"flag"
    13  	"fmt"
    14  	"io/ioutil"
    15  	"math"
    16  	"net"
    17  	"os"
    18  	"strconv"
    19  	"strings"
    20  	"syscall"
    21  	"time"
    22  
    23  	"github.com/aristanetworks/glog"
    24  	"github.com/aristanetworks/goarista/dscp"
    25  	gnmilib "github.com/aristanetworks/goarista/gnmi"
    26  	"github.com/aristanetworks/goarista/gnmireverse"
    27  	"github.com/aristanetworks/goarista/netns"
    28  	"github.com/cenkalti/backoff/v4"
    29  	"github.com/openconfig/gnmi/proto/gnmi"
    30  	"golang.org/x/sync/errgroup"
    31  	"google.golang.org/grpc"
    32  	"google.golang.org/grpc/codes"
    33  	"google.golang.org/grpc/credentials"
    34  	"google.golang.org/grpc/encoding/gzip"
    35  	"google.golang.org/grpc/grpclog"
    36  	"google.golang.org/grpc/metadata"
    37  	"google.golang.org/grpc/status"
    38  	"google.golang.org/protobuf/proto"
    39  	"gopkg.in/yaml.v2"
    40  )
    41  
    42  const (
    43  	// errorLoopRetryMaxInterval caps the time between error loop retries.
    44  	errorLoopRetryMaxInterval = time.Minute
    45  )
    46  
    47  type subscriptionList struct {
    48  	subs []subscription
    49  }
    50  
    51  type sampleList struct {
    52  	subs []subscription
    53  }
    54  
    55  type subscription struct {
    56  	p        *gnmi.Path
    57  	interval time.Duration
    58  }
    59  
    60  type getList struct {
    61  	openconfigPaths []*gnmi.Path
    62  	eosNativePaths  []*gnmi.Path
    63  }
    64  
    65  func str(subs []subscription) string {
    66  	s := make([]string, len(subs))
    67  	for i, sub := range subs {
    68  		s[i] = gnmilib.StrPath(sub.p)
    69  		if sub.interval > 0 {
    70  			s[i] += "@" + sub.interval.String()
    71  		}
    72  	}
    73  	return strings.Join(s, ", ")
    74  }
    75  
    76  func (l *subscriptionList) String() string {
    77  	if l == nil {
    78  		return ""
    79  	}
    80  	return str(l.subs)
    81  }
    82  
    83  func (l *sampleList) String() string {
    84  	if l == nil {
    85  		return ""
    86  	}
    87  	return str(l.subs)
    88  }
    89  
    90  func (l *getList) String() string {
    91  	if l == nil {
    92  		return ""
    93  	}
    94  	var pathStrs []string
    95  	for _, path := range l.openconfigPaths {
    96  		pathStrs = append(pathStrs, gnmilib.StrPath(path))
    97  	}
    98  	for _, path := range l.eosNativePaths {
    99  		pathStrs = append(pathStrs, gnmilib.StrPath(path))
   100  	}
   101  	return strings.Join(pathStrs, ", ")
   102  }
   103  
   104  func parseInterval(s string) (time.Duration, int, error) {
   105  	i := strings.LastIndexByte(s, '@')
   106  	if i == -1 {
   107  		return -1, -1, fmt.Errorf("SAMPLE subscription is missing interval: %q", s)
   108  	}
   109  	interval, err := time.ParseDuration(s[i+1:])
   110  	if err != nil {
   111  		return -1, i, fmt.Errorf("error parsing interval in %q: %s", s, err)
   112  	}
   113  	if interval < 0 {
   114  		return -1, i, fmt.Errorf("negative interval not allowed: %q", s)
   115  	}
   116  	return interval, i, nil
   117  }
   118  
   119  func setSubscriptions(subs *[]subscription, s string, interval time.Duration) error {
   120  	gnmiPath, err := gnmilib.ParseGNMIElements(gnmilib.SplitPath(s))
   121  	if err != nil {
   122  		return err
   123  	}
   124  	sub := subscription{p: gnmiPath, interval: interval}
   125  	*subs = append(*subs, sub)
   126  	return nil
   127  }
   128  
   129  // Set implements flag.Value interface
   130  func (l *subscriptionList) Set(s string) error {
   131  	interval, i, err := parseInterval(s)
   132  	if err != nil {
   133  		if i == -1 {
   134  			// for subscription list, if there is no intervals, it's ok
   135  			interval = 0
   136  			i = len(s)
   137  		} else {
   138  			// invalid interval is found
   139  			return err
   140  		}
   141  	}
   142  	return setSubscriptions(&l.subs, s[:i], interval)
   143  }
   144  
   145  // Set implements flag.Value interface
   146  func (l *sampleList) Set(s string) error {
   147  	interval, i, err := parseInterval(s)
   148  	if err != nil {
   149  		// sample list must come with intervals
   150  		return err
   151  	}
   152  	return setSubscriptions(&l.subs, s[:i], interval)
   153  }
   154  
   155  func (l *getList) Set(gnmiPathStr string) error {
   156  	switch {
   157  	case strings.HasPrefix(gnmiPathStr, "eos_native:"):
   158  		gnmiPathStr = strings.TrimPrefix(gnmiPathStr, "eos_native:")
   159  		eosNativePath, err := gnmilib.ParseGNMIElements(gnmilib.SplitPath(gnmiPathStr))
   160  		if err != nil {
   161  			return err
   162  		}
   163  		eosNativePath.Origin = "eos_native"
   164  		l.eosNativePaths = append(l.eosNativePaths, eosNativePath)
   165  	default:
   166  		gnmiPathStr = strings.TrimPrefix(gnmiPathStr, "openconfig:")
   167  		openconfigPath, err := gnmilib.ParseGNMIElements(gnmilib.SplitPath(gnmiPathStr))
   168  		if err != nil {
   169  			return err
   170  		}
   171  		l.openconfigPaths = append(l.openconfigPaths, openconfigPath)
   172  	}
   173  	return nil
   174  }
   175  
   176  func (l *getList) readGetPathsFile(filePath string) {
   177  	file, err := os.Open(filePath)
   178  	if err != nil {
   179  		glog.Fatalf("failed to read Get paths file %q: %s", filePath, err)
   180  	}
   181  	defer file.Close()
   182  
   183  	scanner := bufio.NewScanner(file)
   184  	for scanner.Scan() {
   185  		if path := strings.TrimSpace(scanner.Text()); path != "" {
   186  			l.Set(path)
   187  		}
   188  	}
   189  
   190  	if err := scanner.Err(); err != nil {
   191  		glog.Fatalf("failed to read Get paths file %q: %s", filePath, err)
   192  	}
   193  }
   194  
   195  func (c *config) parseCredentialsFile(data []byte) error {
   196  	creds := struct {
   197  		Username string
   198  		Password string
   199  	}{}
   200  	if err := yaml.UnmarshalStrict(data, &creds); err != nil {
   201  		return err
   202  	}
   203  	// Do not overwrite username from -username flag.
   204  	if c.username == "" {
   205  		c.username = creds.Username
   206  	}
   207  	// Do not overwrite password from -password flag.
   208  	if c.password == "" {
   209  		c.password = creds.Password
   210  	}
   211  	return nil
   212  }
   213  
   214  func (c *config) readCredentialsFile(filePath string) {
   215  	data, err := os.ReadFile(filePath)
   216  	if err != nil {
   217  		glog.Fatalf("failed to read credentials file %q: %s", filePath, err)
   218  	}
   219  	if err := c.parseCredentialsFile(data); err != nil {
   220  		glog.Fatalf("failed to parse credentials file %q: %s", filePath, err)
   221  	}
   222  }
   223  
   224  type config struct {
   225  	// target config
   226  	targetAddr        string
   227  	username          string
   228  	password          string
   229  	targetTLSInsecure bool
   230  	targetCert        string
   231  	targetKey         string
   232  	targetCA          string
   233  
   234  	targetVal        string
   235  	subTargetDefined subscriptionList
   236  	subSample        sampleList
   237  	origin           string
   238  
   239  	getSampleInterval time.Duration
   240  	getPaths          getList
   241  
   242  	// collector config
   243  	collectorAddr        string
   244  	sourceAddr           string
   245  	dscp                 int
   246  	collectorTLS         bool
   247  	collectorSkipVerify  bool
   248  	collectorCert        string
   249  	collectorKey         string
   250  	collectorCA          string
   251  	collectorCompression string
   252  }
   253  
   254  // Main initializes the gNMIReverse client.
   255  func Main() {
   256  	var cfg config
   257  	flag.StringVar(&cfg.targetAddr, "target_addr", "127.0.0.1:6030",
   258  		"address of the gNMI target in the form of [<vrf-name>/]address:port")
   259  	flag.StringVar(&cfg.username, "username", "", "username to authenticate with target")
   260  	flag.StringVar(&cfg.password, "password", "", "password to authenticate with target")
   261  	credentialsFileUsage := `Path to file containing username and/or password to` +
   262  		` authenticate with target, in YAML form of:
   263    username: admin
   264    password: pass123
   265  Credentials specified with -username or -password take precedence.`
   266  	credentialsFile := flag.String("credentials_file", "", credentialsFileUsage)
   267  
   268  	flag.StringVar(&cfg.targetVal, "target_value", "",
   269  		"value to use in the target field of the Subscribe")
   270  	flag.Var(&cfg.subTargetDefined, "subscribe",
   271  		"Path to subscribe with TARGET_DEFINED subscription mode.\n"+
   272  			"To set a heartbeat interval include a suffix of @<heartbeat interval>.\n"+
   273  			"The interval should include a unit, such as 's' for seconds or 'm' for minutes.\n"+
   274  			"This option can be repeated multiple times.")
   275  	flag.Var(&cfg.subSample, "sample",
   276  		"Path to subscribe with SAMPLE subscription mode.\n"+
   277  			"Paths must have suffix of @<sample interval>.\n"+
   278  			"The interval should include a unit, such as 's' for seconds or 'm' for minutes.\n"+
   279  			"For example to subscribe to interface counters with a 30 second sample interval:\n"+
   280  			"  -sample /interfaces/interface/state/counters@30s\n"+
   281  			"This option can be repeated multiple times.")
   282  	flag.StringVar(&cfg.origin, "origin", "", "value for the origin field of the Subscribe")
   283  	flag.BoolVar(&cfg.targetTLSInsecure, "target_tls_insecure", false,
   284  		"use TLS connection with target and do not verify target certificate")
   285  	flag.StringVar(&cfg.targetCert, "target_certfile", "",
   286  		"path to TLS certificate file to authenticate with target")
   287  	flag.StringVar(&cfg.targetKey, "target_keyfile", "",
   288  		"path to TLS key file to authenticate with target")
   289  	flag.StringVar(&cfg.targetCA, "target_cafile", "",
   290  		"path to TLS CA file to verify target (leave empty to use host's root CA set)")
   291  
   292  	flag.Var(&cfg.getPaths, "get", "Path to retrieve periodically using Get.\n"+
   293  		"Arista EOS native origin paths can be specified with the prefix \"eos_native:\".\n"+
   294  		"For example, eos_native:/Sysdb/hardware\n"+
   295  		"This option can be repeated multiple times.")
   296  	getPathsFile := flag.String("get_file", "", "Path to file containing a list of paths"+
   297  		" separated by newlines to retrieve periodically using Get.")
   298  	getSampleIntervalStr := flag.String("get_sample_interval", "",
   299  		"Interval between periodic Get requests (400ms, 2.5s, 1m, etc.)\n"+
   300  			"Must be specified for Get and applies to all Get paths.")
   301  	getModeUsage :=
   302  		`Operation mode to gather notifications for the GetResponse message.
   303    get        Gather notifications using Get.
   304    subscribe  Gather notifications using Subscribe.
   305               Notifications from the Subscribe sync are bundled into one GetResponse.
   306               With Subscribe, individual leaf updates are gathered (instead of
   307               a subtree with Get) and timestamps for each leaf are preserved.
   308  `
   309  	getMode := flag.String("get_mode", "get", getModeUsage)
   310  
   311  	flag.StringVar(&cfg.collectorAddr, "collector_addr", "",
   312  		"Address of collector in the form of [<vrf-name>/]host:port.\n"+
   313  			"The host portion must be enclosed in square brackets "+
   314  			"if it is a literal IPv6 address.\n"+
   315  			"For example, -collector_addr mgmt/[::1]:1234")
   316  	flag.StringVar(&cfg.sourceAddr, "source_addr", "",
   317  		"Address to use as source in connection to collector in the form of ip[:port], or :port.\n"+
   318  			"An IPv6 address must be enclosed in square brackets when specified with a port.\n"+
   319  			"For example, [::1]:1234")
   320  	flag.IntVar(&cfg.dscp, "collector_dscp", 0,
   321  		"DSCP used on connection to collector, valid values 0-63")
   322  	flag.StringVar(&cfg.collectorCompression, "collector_compression", "none",
   323  		"compression method used when streaming to collector (none | gzip)")
   324  
   325  	flag.BoolVar(&cfg.collectorTLS, "collector_tls", true, "use TLS in connection with collector")
   326  	flag.BoolVar(&cfg.collectorSkipVerify, "collector_tls_skipverify", false,
   327  		"don't verify collector's certificate (insecure)")
   328  	flag.StringVar(&cfg.collectorCert, "collector_certfile", "",
   329  		"path to TLS certificate file to authenticate with collector")
   330  	flag.StringVar(&cfg.collectorKey, "collector_keyfile", "",
   331  		"path to TLS key file to authenticate with collector")
   332  	flag.StringVar(&cfg.collectorCA, "collector_cafile", "",
   333  		"path to TLS CA file to verify collector (leave empty to use host's root CA set)")
   334  
   335  	flag.Parse()
   336  
   337  	// No arguments are expected.
   338  	if len(flag.Args()) > 0 {
   339  		glog.Fatalf("unexpected arguments: %s", flag.Args())
   340  	}
   341  
   342  	// If -v is specified, enables gRPC logging at level corresponding to verbosity evel.
   343  	if glog.V(1) {
   344  		glogVStr := flag.Lookup("v").Value.String()
   345  		logLevel, err := strconv.Atoi(glogVStr)
   346  		if err != nil {
   347  			glog.Infof("cannot parse %q", glogVStr)
   348  		} else {
   349  			grpclog.SetLoggerV2(
   350  				grpclog.NewLoggerV2WithVerbosity(os.Stdout, os.Stdout, os.Stdout, logLevel))
   351  		}
   352  	}
   353  
   354  	if cfg.collectorAddr == "" {
   355  		glog.Fatal("collector address must be specified")
   356  	}
   357  
   358  	if *credentialsFile != "" {
   359  		cfg.readCredentialsFile(*credentialsFile)
   360  	}
   361  
   362  	if *getPathsFile != "" {
   363  		cfg.getPaths.readGetPathsFile(*getPathsFile)
   364  	}
   365  
   366  	if *getSampleIntervalStr != "" {
   367  		getSampleInterval, err := time.ParseDuration(*getSampleIntervalStr)
   368  		if err != nil {
   369  			glog.Fatalf("Get sample interval %q invalid", *getSampleIntervalStr)
   370  		}
   371  		cfg.getSampleInterval = getSampleInterval
   372  	}
   373  
   374  	if !(*getMode == "get" || *getMode == "subscribe") {
   375  		glog.Fatalf("Get mode %q invalid", *getMode)
   376  	}
   377  
   378  	isSubscribe := len(cfg.subTargetDefined.subs) != 0 || len(cfg.subSample.subs) != 0
   379  	isGet := len(cfg.getPaths.openconfigPaths) != 0 || len(cfg.getPaths.eosNativePaths) != 0
   380  
   381  	if !isSubscribe && !isGet {
   382  		glog.Fatal("Subscribe paths or Get paths must be specifed")
   383  	}
   384  	if !isGet && cfg.getSampleInterval != 0 {
   385  		glog.Fatal("Get path must be specified with Get sample interval")
   386  	}
   387  	if isGet && cfg.getSampleInterval == 0 {
   388  		glog.Fatal("Get sample interval must be specified with Get path")
   389  	}
   390  
   391  	if cfg.origin != "" {
   392  		// Workaround for EOS BUG479731: set origin on paths, rather
   393  		// than on the prefix.
   394  		for _, sub := range cfg.subTargetDefined.subs {
   395  			sub.p.Origin = cfg.origin
   396  		}
   397  		for _, sub := range cfg.subSample.subs {
   398  			sub.p.Origin = cfg.origin
   399  		}
   400  		for _, get := range cfg.getPaths.openconfigPaths {
   401  			get.Origin = cfg.origin
   402  		}
   403  		// If "eos_native" was specified by the global origin flag,
   404  		// point Get paths to EOS native Get paths instead.
   405  		if strings.ToLower(cfg.origin) == "eos_native" {
   406  			cfg.getPaths.eosNativePaths = cfg.getPaths.openconfigPaths
   407  			cfg.getPaths.openconfigPaths = nil
   408  		}
   409  	}
   410  
   411  	destConn, err := dialCollector(&cfg)
   412  	if err != nil {
   413  		glog.Fatalf("error dialing destination %q: %s", cfg.collectorAddr, err)
   414  	}
   415  	targetConn, err := dialTarget(&cfg)
   416  	if err != nil {
   417  		glog.Fatalf("error dialing target %q: %s", cfg.targetAddr, err)
   418  	}
   419  
   420  	if isSubscribe {
   421  		go streamResponses(streamSubscribeResponses(&cfg, destConn, targetConn))
   422  	}
   423  	if isGet {
   424  		switch *getMode {
   425  		case "get":
   426  			go streamResponses(streamGetResponses(&cfg, destConn, targetConn))
   427  		case "subscribe":
   428  			go streamResponses(streamGetResponsesModeSubscribe(&cfg, destConn, targetConn))
   429  		}
   430  	}
   431  	select {} // Wait forever
   432  }
   433  
   434  func streamResponses(streamResponsesFunc func(context.Context, *errgroup.Group)) {
   435  	// Used for error loop detection and backoff retries.
   436  	var lastErrorTime time.Time
   437  	bo := backoff.NewExponentialBackOff()
   438  	bo.MaxElapsedTime = 0 // Never stop
   439  	bo.MaxInterval = errorLoopRetryMaxInterval
   440  	bo.Reset()
   441  
   442  	for {
   443  		// Start publisher and client in a loop, each running in
   444  		// their own goroutine. If either of them encounters an error,
   445  		// retry.
   446  		var eg *errgroup.Group
   447  		eg, ctx := errgroup.WithContext(context.Background())
   448  		streamResponsesFunc(ctx, eg)
   449  		if err := eg.Wait(); err != nil {
   450  			nowTime := time.Now()
   451  			// If the last error was from a while ago, reset the backoff interval because
   452  			// this error is not from an error loop.
   453  			if lastErrorTime.Add(errorLoopRetryMaxInterval * 2).Before(nowTime) {
   454  				bo.Reset()
   455  			}
   456  			lastErrorTime = nowTime
   457  			glog.Infof("encountered error, retrying: %s", err)
   458  			time.Sleep(bo.NextBackOff())
   459  		}
   460  	}
   461  }
   462  
   463  func streamSubscribeResponses(cfg *config, destConn, targetConn *grpc.ClientConn) func(
   464  	context.Context, *errgroup.Group) {
   465  	return func(ctx context.Context, eg *errgroup.Group) {
   466  		c := make(chan *gnmi.SubscribeResponse)
   467  		eg.Go(func() error {
   468  			return publish(ctx, destConn, c)
   469  		})
   470  		eg.Go(func() error {
   471  			return subscribe(ctx, cfg, targetConn, c)
   472  		})
   473  	}
   474  }
   475  
   476  func streamGetResponses(cfg *config, destConn, targetConn *grpc.ClientConn) func(
   477  	context.Context, *errgroup.Group) {
   478  	return func(ctx context.Context, eg *errgroup.Group) {
   479  		c := make(chan *gnmi.GetResponse)
   480  		eg.Go(func() error {
   481  			return publishGet(ctx, destConn, c)
   482  		})
   483  		eg.Go(func() error {
   484  			return sampleGet(ctx, cfg, targetConn, c)
   485  		})
   486  	}
   487  }
   488  
   489  func streamGetResponsesModeSubscribe(cfg *config, destConn, targetConn *grpc.ClientConn) func(
   490  	context.Context, *errgroup.Group) {
   491  	return func(ctx context.Context, eg *errgroup.Group) {
   492  		c := make(chan *gnmi.GetResponse)
   493  		eg.Go(func() error {
   494  			return publishGet(ctx, destConn, c)
   495  		})
   496  		eg.Go(func() error {
   497  			return sampleGetModeSubscribe(ctx, cfg, targetConn, c)
   498  		})
   499  	}
   500  }
   501  
   502  func dialCollector(cfg *config) (*grpc.ClientConn, error) {
   503  	var dialOptions []grpc.DialOption
   504  
   505  	if cfg.collectorTLS {
   506  		tlsConfig, err := newTLSConfig(cfg.collectorSkipVerify,
   507  			cfg.collectorCert, cfg.collectorKey, cfg.collectorCA)
   508  		if err != nil {
   509  			return nil, fmt.Errorf("error creating TLS config for collector: %s", err)
   510  		}
   511  		dialOptions = append(dialOptions,
   512  			grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
   513  	} else {
   514  		dialOptions = append(dialOptions, grpc.WithInsecure())
   515  	}
   516  
   517  	switch cfg.collectorCompression {
   518  	case "", "none":
   519  	case "gzip":
   520  		dialOptions = append(dialOptions,
   521  			grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)))
   522  	default:
   523  		return nil, fmt.Errorf("unknown compression method %q", cfg.collectorCompression)
   524  	}
   525  
   526  	nsName, addr, err := netns.ParseAddress(cfg.collectorAddr)
   527  	if err != nil {
   528  		return nil, fmt.Errorf("error parsing address: %s", err)
   529  	}
   530  
   531  	dialer, err := newDialer(cfg)
   532  	if err != nil {
   533  		return nil, err
   534  	}
   535  
   536  	dialOptions = append(dialOptions, grpc.WithContextDialer(newVRFDialer(dialer, nsName)))
   537  	return grpc.Dial(addr, dialOptions...)
   538  }
   539  
   540  func newVRFDialer(d *net.Dialer, nsName string) func(context.Context, string) (net.Conn, error) {
   541  	return func(ctx context.Context, addr string) (net.Conn, error) {
   542  		var conn net.Conn
   543  		err := netns.Do(nsName, func() error {
   544  			c, err := d.DialContext(ctx, "tcp", addr)
   545  			if err != nil {
   546  				return err
   547  			}
   548  			conn = c
   549  			return nil
   550  		})
   551  
   552  		return conn, err
   553  	}
   554  }
   555  
   556  func newTLSConfig(skipVerify bool, certFile, keyFile, caFile string) (*tls.Config,
   557  	error) {
   558  	var tlsConfig tls.Config
   559  	if skipVerify {
   560  		tlsConfig.InsecureSkipVerify = true
   561  	} else if caFile != "" {
   562  		b, err := ioutil.ReadFile(caFile)
   563  		if err != nil {
   564  			return nil, err
   565  		}
   566  		cp := x509.NewCertPool()
   567  		if !cp.AppendCertsFromPEM(b) {
   568  			return nil, fmt.Errorf("credentials: failed to append certificates")
   569  		}
   570  		tlsConfig.RootCAs = cp
   571  	}
   572  	if certFile != "" {
   573  		if keyFile == "" {
   574  			return nil, fmt.Errorf("please provide both certfile and keyfile")
   575  		}
   576  		cert, err := tls.LoadX509KeyPair(certFile, keyFile)
   577  		if err != nil {
   578  			return nil, err
   579  		}
   580  		tlsConfig.Certificates = []tls.Certificate{cert}
   581  	}
   582  	return &tlsConfig, nil
   583  }
   584  
   585  func newDialer(cfg *config) (*net.Dialer, error) {
   586  	var d net.Dialer
   587  	if cfg.sourceAddr != "" {
   588  		var localAddr net.TCPAddr
   589  		sourceIP, sourcePort, _ := net.SplitHostPort(cfg.sourceAddr)
   590  		if sourceIP == "" {
   591  			// This can happend if cfg.sourceAddr doesn't have a port
   592  			sourceIP = cfg.sourceAddr
   593  		}
   594  		ip := net.ParseIP(sourceIP)
   595  		if ip == nil {
   596  			return nil, fmt.Errorf("failed to parse IP in source address: %q", sourceIP)
   597  		}
   598  		localAddr.IP = ip
   599  
   600  		if sourcePort != "" {
   601  			port, err := strconv.Atoi(sourcePort)
   602  			if err != nil {
   603  				return nil, fmt.Errorf("failed to parse port in source address: %q", sourcePort)
   604  			}
   605  			localAddr.Port = port
   606  		}
   607  
   608  		d.LocalAddr = &localAddr
   609  	}
   610  
   611  	if cfg.dscp != 0 {
   612  		if cfg.dscp < 0 || cfg.dscp >= 64 {
   613  			return nil, fmt.Errorf("DSCP value must be a value in the range 0-63, got %d", cfg.dscp)
   614  		}
   615  		// DSCP is the top 6 bits of the TOS byte
   616  		tos := byte(cfg.dscp << 2)
   617  		d.Control = func(network, address string, c syscall.RawConn) error {
   618  			return dscp.SetTOS(network, c, tos)
   619  		}
   620  	}
   621  
   622  	return &d, nil
   623  }
   624  
   625  func dialTarget(cfg *config) (*grpc.ClientConn, error) {
   626  	nsName, addr, err := netns.ParseAddress(cfg.targetAddr)
   627  	if err != nil {
   628  		return nil, fmt.Errorf("error parsing address: %s", err)
   629  	}
   630  
   631  	var dialOptions []grpc.DialOption
   632  	if cfg.targetTLSInsecure || cfg.targetCert != "" || cfg.targetKey != "" || cfg.targetCA != "" {
   633  		tlsConfig, err := newTLSConfig(cfg.targetTLSInsecure,
   634  			cfg.targetCert, cfg.targetKey, cfg.targetCA)
   635  		if err != nil {
   636  			return nil, fmt.Errorf("error creating TLS config for target: %s", err)
   637  		}
   638  		dialOptions = append(dialOptions,
   639  			grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
   640  	} else {
   641  		dialOptions = append(dialOptions, grpc.WithInsecure())
   642  	}
   643  
   644  	var d net.Dialer
   645  	dialOptions = append(dialOptions,
   646  		grpc.WithContextDialer(newVRFDialer(&d, nsName)),
   647  		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(math.MaxInt32)),
   648  	)
   649  
   650  	return grpc.Dial(addr, dialOptions...)
   651  }
   652  
   653  func publish(ctx context.Context, destConn *grpc.ClientConn,
   654  	c <-chan *gnmi.SubscribeResponse) error {
   655  	client := gnmireverse.NewGNMIReverseClient(destConn)
   656  	stream, err := client.Publish(ctx, grpc.WaitForReady(true))
   657  	if err != nil {
   658  		return fmt.Errorf("error from Publish: %s", err)
   659  	}
   660  	for {
   661  		select {
   662  		case <-ctx.Done():
   663  			return ctx.Err()
   664  		case response := <-c:
   665  			if err := stream.Send(response); err != nil {
   666  				return fmt.Errorf("error from Publish.Send: %s", err)
   667  			}
   668  		}
   669  	}
   670  }
   671  
   672  func publishGet(ctx context.Context, destConn *grpc.ClientConn, c <-chan *gnmi.GetResponse) error {
   673  	client := gnmireverse.NewGNMIReverseClient(destConn)
   674  	stream, err := client.PublishGet(ctx, grpc.WaitForReady(true))
   675  	if err != nil {
   676  		return fmt.Errorf("error from PublishGet: %s", err)
   677  	}
   678  	for {
   679  		select {
   680  		case <-ctx.Done():
   681  			return ctx.Err()
   682  		case response := <-c:
   683  			if glog.V(3) {
   684  				glog.Infof("send Get response: size_bytes=%d num_notifs=%d",
   685  					proto.Size(response), len(response.GetNotification()))
   686  			}
   687  			if glog.V(7) {
   688  				glog.Infof("send Get response to collector: %v", response)
   689  			}
   690  			if err := stream.Send(response); err != nil {
   691  				return fmt.Errorf("error from PublishGet.Send: %s", err)
   692  			}
   693  		}
   694  	}
   695  }
   696  
   697  func subscribe(ctx context.Context, cfg *config, targetConn *grpc.ClientConn,
   698  	c chan<- *gnmi.SubscribeResponse) error {
   699  	client := gnmi.NewGNMIClient(targetConn)
   700  	subList := &gnmi.SubscriptionList{
   701  		Prefix: &gnmi.Path{Target: cfg.targetVal},
   702  	}
   703  
   704  	for _, sub := range cfg.subTargetDefined.subs {
   705  		subList.Subscription = append(subList.Subscription,
   706  			&gnmi.Subscription{
   707  				Path:              sub.p,
   708  				Mode:              gnmi.SubscriptionMode_TARGET_DEFINED,
   709  				HeartbeatInterval: uint64(sub.interval),
   710  			},
   711  		)
   712  	}
   713  	for _, sub := range cfg.subSample.subs {
   714  		subList.Subscription = append(subList.Subscription,
   715  			&gnmi.Subscription{
   716  				Path:           sub.p,
   717  				Mode:           gnmi.SubscriptionMode_SAMPLE,
   718  				SampleInterval: uint64(sub.interval),
   719  			},
   720  		)
   721  	}
   722  	request := &gnmi.SubscribeRequest{
   723  		Request: &gnmi.SubscribeRequest_Subscribe{
   724  			Subscribe: subList,
   725  		},
   726  	}
   727  
   728  	if cfg.username != "" {
   729  		ctx = metadata.NewOutgoingContext(ctx,
   730  			metadata.Pairs(
   731  				"username", cfg.username,
   732  				"password", cfg.password),
   733  		)
   734  	}
   735  	stream, err := client.Subscribe(ctx, grpc.WaitForReady(true))
   736  	if err != nil {
   737  		return fmt.Errorf("error from Subscribe: %s", err)
   738  	}
   739  	if err := stream.Send(request); err != nil {
   740  		return fmt.Errorf("error sending SubscribeRequest: %s", err)
   741  	}
   742  
   743  	for {
   744  		resp, err := stream.Recv()
   745  		if err != nil {
   746  			return fmt.Errorf("error from Subscribe.Recv: %s", err)
   747  		}
   748  		select {
   749  		case <-ctx.Done():
   750  			return ctx.Err()
   751  		case c <- resp:
   752  		}
   753  	}
   754  }
   755  
   756  func sampleGet(ctx context.Context, cfg *config, targetConn *grpc.ClientConn,
   757  	c chan<- *gnmi.GetResponse) error {
   758  	client := gnmi.NewGNMIClient(targetConn)
   759  
   760  	openconfigGetReq := &gnmi.GetRequest{
   761  		Path: cfg.getPaths.openconfigPaths,
   762  	}
   763  
   764  	eosNativeGetReq := &gnmi.GetRequest{
   765  		Path: cfg.getPaths.eosNativePaths,
   766  	}
   767  
   768  	if cfg.username != "" {
   769  		ctx = metadata.NewOutgoingContext(ctx,
   770  			metadata.Pairs(
   771  				"username", cfg.username,
   772  				"password", cfg.password),
   773  		)
   774  	}
   775  
   776  	// Set up a ticker for a consistent interval to exclude the additional time taken
   777  	// for issuing the Get request(s) and processing the response(s).
   778  	ticker := time.NewTicker(cfg.getSampleInterval)
   779  	defer ticker.Stop()
   780  
   781  	for {
   782  		var openConfigGetResponse *gnmi.GetResponse
   783  		if len(cfg.getPaths.openconfigPaths) > 0 {
   784  			if glog.V(5) {
   785  				glog.Infof("send OpenConfig Get request to target: %v", openconfigGetReq)
   786  			}
   787  			var err error
   788  			openConfigGetResponse, err = client.Get(ctx, openconfigGetReq, grpc.WaitForReady(true))
   789  			if err != nil {
   790  				return fmt.Errorf("error from OpenConfig Get: %s", err)
   791  			}
   792  			if glog.V(7) {
   793  				glog.Infof("receive OpenConfig Get response: %v", openConfigGetResponse)
   794  			}
   795  		}
   796  
   797  		// Issue separate Get request for EOS native paths because target may not support mixed
   798  		// origin paths in the same Get request.
   799  		var eosNativeGetResponse *gnmi.GetResponse
   800  		if len(cfg.getPaths.eosNativePaths) > 0 {
   801  			if glog.V(5) {
   802  				glog.Infof("send EOS native Get request to target: %v", eosNativeGetReq)
   803  			}
   804  			var err error
   805  			eosNativeGetResponse, err = client.Get(ctx, eosNativeGetReq, grpc.WaitForReady(true))
   806  			if err != nil {
   807  				return fmt.Errorf("error from EOS native Get: %s", err)
   808  			}
   809  			if glog.V(7) {
   810  				glog.Infof("receive EOS native Get response: %v", eosNativeGetResponse)
   811  			}
   812  		}
   813  
   814  		// Combine the Get responses.
   815  		currentTime := time.Now().UnixNano()
   816  		combinedGetResponse := combineGetResponses(
   817  			currentTime, cfg.targetVal, openConfigGetResponse, eosNativeGetResponse)
   818  
   819  		select {
   820  		case <-ctx.Done():
   821  			return ctx.Err()
   822  		case c <- combinedGetResponse:
   823  		}
   824  
   825  		glog.V(5).Infof("wait for %s", cfg.getSampleInterval)
   826  		select {
   827  		case <-ctx.Done():
   828  			return ctx.Err()
   829  		case <-ticker.C:
   830  		}
   831  	}
   832  }
   833  
   834  // combineGetResponses combines the notifications of GetResponses to one GetResponse
   835  // with the same timestamp and target prefix for all notifications.
   836  func combineGetResponses(timestamp int64, target string,
   837  	getResponses ...*gnmi.GetResponse) *gnmi.GetResponse {
   838  	var totalNotifications int
   839  	for _, res := range getResponses {
   840  		totalNotifications += len(res.GetNotification())
   841  	}
   842  	combinedGetResponse := &gnmi.GetResponse{
   843  		Notification: make([]*gnmi.Notification, 0, totalNotifications),
   844  	}
   845  	for _, res := range getResponses {
   846  		for _, notif := range res.GetNotification() {
   847  			// Workaround for EOS BUG568084: set timestamp on GetResponse notification.
   848  			notif.Timestamp = timestamp
   849  			if notif.GetPrefix() == nil {
   850  				notif.Prefix = &gnmi.Path{}
   851  			}
   852  			notif.Prefix.Target = target
   853  			combinedGetResponse.Notification = append(combinedGetResponse.Notification, notif)
   854  		}
   855  	}
   856  	return combinedGetResponse
   857  }
   858  
   859  // sampleGetModeSubscribe performs a Subscribe sync at each sample interval and builds
   860  // one GetResponse containing all sync notifications to send to the gNMIReverse server.
   861  func sampleGetModeSubscribe(ctx context.Context, cfg *config, targetConn *grpc.ClientConn,
   862  	c chan<- *gnmi.GetResponse) error {
   863  	client := gnmi.NewGNMIClient(targetConn)
   864  
   865  	if cfg.username != "" {
   866  		ctx = metadata.NewOutgoingContext(ctx,
   867  			metadata.Pairs(
   868  				"username", cfg.username,
   869  				"password", cfg.password),
   870  		)
   871  	}
   872  
   873  	// For OpenConfig paths, keep a Subscribe POLL stream to perform a sync at
   874  	// each sample interval. Avoids having to initialize a new Subscribe stream
   875  	// at each sample interval.
   876  	var openconfigPollStream gnmi.GNMI_SubscribeClient
   877  	var err error
   878  	if len(cfg.getPaths.openconfigPaths) > 0 {
   879  		ctx, cancel := context.WithCancel(ctx)
   880  		defer cancel()
   881  		openconfigPollStream, err = initializeSubscribePollStream(
   882  			ctx, client, cfg.getPaths.openconfigPaths)
   883  		if err != nil {
   884  			return err
   885  		}
   886  		glog.V(3).Infof("OpenConfig paths: initialized Subscribe POLL stream")
   887  	}
   888  
   889  	// For EOS native paths, Subscribe POLL is not supported.
   890  	// Subscribe ONCE is supported only on newer EOS releases.
   891  	// Determine if Subscribe ONCE is supported and
   892  	// 1. if it is supported, perform a Subscribe ONCE.
   893  	// 2. if it is not supported, perform a Subscribe STREAM and close
   894  	//    the stream after a sync response is received, which is sent
   895  	//    after all initial updates are received.
   896  	var eosNativeSubscribeNotifsFunc func(context.Context,
   897  		gnmi.GNMIClient, *gnmi.SubscribeRequest) ([]*gnmi.Notification, error)
   898  	var eosNativeSubscribeRequest *gnmi.SubscribeRequest
   899  	if len(cfg.getPaths.eosNativePaths) > 0 {
   900  		isEOSNativeSubscribeOnceSupported, err := isSubscribeOnceSupported(ctx, client)
   901  		if err != nil {
   902  			return err
   903  		}
   904  		if isEOSNativeSubscribeOnceSupported {
   905  			eosNativeSubscribeNotifsFunc = subscribeOnceNotifs
   906  			eosNativeSubscribeRequest = buildSubscribeOnceRequest(cfg.getPaths.eosNativePaths)
   907  		} else {
   908  			eosNativeSubscribeNotifsFunc = subscribeStreamNotifs
   909  			eosNativeSubscribeRequest = buildSubscribeStreamRequest(cfg.getPaths.eosNativePaths)
   910  		}
   911  		glog.V(3).Infof("EOS native paths: subscribe_once_supported=%t subscribe_request=%s",
   912  			isEOSNativeSubscribeOnceSupported, eosNativeSubscribeRequest)
   913  	}
   914  
   915  	// Set up a ticker for a consistent interval to exclude the additional time taken
   916  	// for issuing the Subscribe requests and processing the responses.
   917  	ticker := time.NewTicker(cfg.getSampleInterval)
   918  	defer ticker.Stop()
   919  
   920  	for {
   921  		// Measure the time taken to process Subscribe notifications.
   922  		var processingStartTime time.Time
   923  		if glog.V(5) {
   924  			processingStartTime = time.Now()
   925  		}
   926  
   927  		// Gather notifications for OpenConfig paths.
   928  		var openconfigNotifs []*gnmi.Notification
   929  		if openconfigPollStream != nil {
   930  			openconfigNotifs, err = subscribePollNotifs(openconfigPollStream)
   931  			if err != nil {
   932  				return err
   933  			}
   934  		}
   935  
   936  		// Gather notifications for EOS native paths.
   937  		var eosNativeNotifs []*gnmi.Notification
   938  		if eosNativeSubscribeNotifsFunc != nil {
   939  			eosNativeNotifs, err = eosNativeSubscribeNotifsFunc(
   940  				ctx, client, eosNativeSubscribeRequest)
   941  			if err != nil {
   942  				return err
   943  			}
   944  		}
   945  
   946  		// Combine OpenConfig and EOS native notifications into one GetResponse.
   947  		getResponse := combineNotifs(cfg.targetVal, openconfigNotifs, eosNativeNotifs)
   948  		select {
   949  		case <-ctx.Done():
   950  			return ctx.Err()
   951  		case c <- getResponse:
   952  		}
   953  
   954  		// Wait for the next sample interval.
   955  		if glog.V(5) {
   956  			// If the processing time exceeds the sample interval, then
   957  			// the sample interval is too low.
   958  			processingTime := time.Since(processingStartTime)
   959  			glog.Infof("wait: get_sample_interval=%s processing_time=%s ",
   960  				cfg.getSampleInterval, processingTime)
   961  		}
   962  		select {
   963  		case <-ctx.Done():
   964  			return ctx.Err()
   965  		case <-ticker.C:
   966  		}
   967  	}
   968  }
   969  
   970  // initializeSubscribePollStream initializes a Subscribe stream and issues a Subscribe
   971  // POLL request and returns the stream.
   972  func initializeSubscribePollStream(ctx context.Context,
   973  	client gnmi.GNMIClient, paths []*gnmi.Path) (gnmi.GNMI_SubscribeClient, error) {
   974  	stream, err := client.Subscribe(ctx, grpc.WaitForReady(true))
   975  	if err != nil {
   976  		return nil, err
   977  	}
   978  	req := buildSubscribePollRequest(paths)
   979  	glog.V(3).Infof("initialize Subscribe POLL stream: subscribe_request=%s", req)
   980  	if err := stream.Send(req); err != nil {
   981  		return nil, err
   982  	}
   983  	res, err := stream.Recv()
   984  	if err != nil {
   985  		return nil, err
   986  	}
   987  	// We expect only a sync response because updates_only=true in the request.
   988  	if !res.GetSyncResponse() {
   989  		return nil, fmt.Errorf("failed to initialize Subscribe POLL stream:"+
   990  			" expected sync response but received %s", res)
   991  	}
   992  	return stream, nil
   993  }
   994  
   995  // isSubscribeOnceSupported returns true if a Subscribe ONCE is supported by issuing a
   996  // Subscribe ONCE request and checking the error code.
   997  func isSubscribeOnceSupported(ctx context.Context, client gnmi.GNMIClient) (bool, error) {
   998  	ctx, cancel := context.WithCancel(ctx)
   999  	defer cancel()
  1000  
  1001  	failedError := func(err error) error {
  1002  		return fmt.Errorf("failed to determine if EOS native Subscribe ONCE is supported: %s", err)
  1003  	}
  1004  	stream, err := client.Subscribe(ctx, grpc.WaitForReady(true))
  1005  	if err != nil {
  1006  		return false, failedError(err)
  1007  	}
  1008  
  1009  	req := &gnmi.SubscribeRequest{
  1010  		Request: &gnmi.SubscribeRequest_Subscribe{
  1011  			Subscribe: &gnmi.SubscriptionList{
  1012  				Mode: gnmi.SubscriptionList_ONCE,
  1013  				Subscription: []*gnmi.Subscription{{
  1014  					Path: &gnmi.Path{
  1015  						Origin: "eos_native",
  1016  						// Subscribe to a path that is not too large.
  1017  						Elem: []*gnmi.PathElem{
  1018  							{Name: "Kernel"},
  1019  							{Name: "sysinfo"},
  1020  						},
  1021  					},
  1022  				}},
  1023  				UpdatesOnly: true,
  1024  			},
  1025  		},
  1026  	}
  1027  	glog.V(3).Infof("determine if Subscribe ONCE supported: subscribe_request=%s", req)
  1028  	if err := stream.Send(req); err != nil {
  1029  		return false, failedError(err)
  1030  	}
  1031  	if _, err := stream.Recv(); err != nil {
  1032  		// Error code received is unimplemented, so Subscribe ONCE is not supported.
  1033  		if e, ok := status.FromError(err); ok && e.Code() == codes.Unimplemented {
  1034  			return false, nil
  1035  		}
  1036  		return false, failedError(err)
  1037  	}
  1038  	// Received a SubscribeResponse, so Subscribe ONCE is supported.
  1039  	return true, nil
  1040  }
  1041  
  1042  // subscribePollNotifs sends a poll trigger request to the long-lived Subscribe POLL
  1043  // stream and returns list of notifications gathered from the poll trigger sync.
  1044  func subscribePollNotifs(stream gnmi.GNMI_SubscribeClient) ([]*gnmi.Notification, error) {
  1045  	req := &gnmi.SubscribeRequest{
  1046  		Request: &gnmi.SubscribeRequest_Poll{
  1047  			Poll: &gnmi.Poll{},
  1048  		},
  1049  	}
  1050  	return subscribeSyncNotifs(stream, req, gnmi.SubscriptionList_POLL)
  1051  }
  1052  
  1053  // subscribeOnceNotifs performs a Subscribe ONCE and returns a list of notifications
  1054  // gathered from the sync.
  1055  func subscribeOnceNotifs(ctx context.Context,
  1056  	client gnmi.GNMIClient, req *gnmi.SubscribeRequest) ([]*gnmi.Notification, error) {
  1057  	ctx, cancel := context.WithCancel(ctx)
  1058  	defer cancel()
  1059  
  1060  	stream, err := client.Subscribe(ctx, grpc.WaitForReady(true))
  1061  	if err != nil {
  1062  		return nil, err
  1063  	}
  1064  	return subscribeSyncNotifs(stream, req, gnmi.SubscriptionList_ONCE)
  1065  }
  1066  
  1067  // subscribeStreamNotifs performs a Subscribe STREAM and returns a list of notifications
  1068  // gathered from the sync. When the sync response is received, indicating that all data
  1069  // paths have been sent at least once, the Subscribe stream is closed.
  1070  func subscribeStreamNotifs(ctx context.Context,
  1071  	client gnmi.GNMIClient, req *gnmi.SubscribeRequest) ([]*gnmi.Notification, error) {
  1072  	ctx, cancel := context.WithCancel(ctx)
  1073  	defer cancel()
  1074  
  1075  	stream, err := client.Subscribe(ctx, grpc.WaitForReady(true))
  1076  	if err != nil {
  1077  		return nil, err
  1078  	}
  1079  	return subscribeSyncNotifs(stream, req, gnmi.SubscriptionList_STREAM)
  1080  }
  1081  
  1082  // subscribeSyncNotifs returns a list of notifications gathered from the Subscribe sync.
  1083  func subscribeSyncNotifs(stream gnmi.GNMI_SubscribeClient, req *gnmi.SubscribeRequest,
  1084  	mode gnmi.SubscriptionList_Mode) ([]*gnmi.Notification, error) {
  1085  	if glog.V(9) {
  1086  		glog.Infof("subscribe_mode=%s subscribe_request=%s", mode.String(), req)
  1087  	}
  1088  	if err := stream.Send(req); err != nil {
  1089  		return nil, err
  1090  	}
  1091  
  1092  	var notifs []*gnmi.Notification
  1093  	for {
  1094  		res, err := stream.Recv()
  1095  		if err != nil {
  1096  			return nil, err
  1097  		}
  1098  		if glog.V(9) {
  1099  			glog.Infof("subscribe_mode=%s subscribe_response=%s", mode.String(), res)
  1100  		}
  1101  		if res.GetSyncResponse() {
  1102  			break
  1103  		}
  1104  		notifs = append(notifs, res.GetUpdate())
  1105  	}
  1106  	return notifs, nil
  1107  }
  1108  
  1109  func buildSubscribePollRequest(paths []*gnmi.Path) *gnmi.SubscribeRequest {
  1110  	return &gnmi.SubscribeRequest{
  1111  		Request: &gnmi.SubscribeRequest_Subscribe{
  1112  			Subscribe: &gnmi.SubscriptionList{
  1113  				Mode:         gnmi.SubscriptionList_POLL,
  1114  				Subscription: buildSubscriptions(paths),
  1115  				UpdatesOnly:  true,
  1116  			},
  1117  		},
  1118  	}
  1119  }
  1120  
  1121  func buildSubscribeOnceRequest(paths []*gnmi.Path) *gnmi.SubscribeRequest {
  1122  	return &gnmi.SubscribeRequest{
  1123  		Request: &gnmi.SubscribeRequest_Subscribe{
  1124  			Subscribe: &gnmi.SubscriptionList{
  1125  				Mode:         gnmi.SubscriptionList_ONCE,
  1126  				Subscription: buildSubscriptions(paths),
  1127  			},
  1128  		},
  1129  	}
  1130  }
  1131  
  1132  func buildSubscribeStreamRequest(paths []*gnmi.Path) *gnmi.SubscribeRequest {
  1133  	return &gnmi.SubscribeRequest{
  1134  		Request: &gnmi.SubscribeRequest_Subscribe{
  1135  			Subscribe: &gnmi.SubscriptionList{
  1136  				Mode:         gnmi.SubscriptionList_STREAM,
  1137  				Subscription: buildSubscriptions(paths),
  1138  			},
  1139  		},
  1140  	}
  1141  }
  1142  
  1143  func buildSubscriptions(paths []*gnmi.Path) []*gnmi.Subscription {
  1144  	subscriptions := make([]*gnmi.Subscription, 0, len(paths))
  1145  	for _, path := range paths {
  1146  		subscriptions = append(subscriptions, &gnmi.Subscription{
  1147  			Path: path,
  1148  		})
  1149  	}
  1150  	return subscriptions
  1151  }
  1152  
  1153  // combineNotifs combines the OpenConfig and EOS native notifications into a GetResponse.
  1154  // The target prefix is set for all notifications. For EOS native notifications, the origin
  1155  // prefix is set to "eos_native".
  1156  func combineNotifs(target string, openconfigNotifs []*gnmi.Notification,
  1157  	eosNativeNotifs []*gnmi.Notification) *gnmi.GetResponse {
  1158  	for _, notif := range openconfigNotifs {
  1159  		if notif.Prefix == nil {
  1160  			notif.Prefix = &gnmi.Path{}
  1161  		}
  1162  		notif.Prefix.Target = target
  1163  	}
  1164  	for _, notif := range eosNativeNotifs {
  1165  		if notif.Prefix == nil {
  1166  			notif.Prefix = &gnmi.Path{}
  1167  		}
  1168  		notif.Prefix.Target = target
  1169  		notif.Prefix.Origin = "eos_native"
  1170  	}
  1171  	return &gnmi.GetResponse{
  1172  		Notification: append(openconfigNotifs, eosNativeNotifs...),
  1173  	}
  1174  }