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 }