github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/gnmi/client/client.go (about) 1 // Copyright (c) 2017 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 "errors" 10 "flag" 11 "fmt" 12 "os" 13 "runtime/debug" 14 "strconv" 15 "strings" 16 "sync/atomic" 17 "time" 18 19 aflag "github.com/aristanetworks/goarista/flag" 20 "github.com/aristanetworks/goarista/gnmi" 21 22 "github.com/aristanetworks/glog" 23 pb "github.com/openconfig/gnmi/proto/gnmi" 24 "github.com/openconfig/gnmi/proto/gnmi_ext" 25 "golang.org/x/sync/errgroup" 26 "google.golang.org/grpc" 27 "google.golang.org/grpc/keepalive" 28 "google.golang.org/protobuf/proto" 29 ) 30 31 // TODO: Make this more clear 32 var help = `Usage of gnmi: 33 gnmi -addr [<VRF-NAME>/]ADDRESS:PORT [options...] 34 capabilities 35 get ((encoding=ENCODING) (origin=ORIGIN) (target=TARGET) PATH+)+ 36 subscribe ((origin=ORIGIN) (target=TARGET) PATH+)+ 37 ((update|replace|union_replace (origin=ORIGIN) (target=TARGET) PATH JSON|FILE) | 38 (delete (origin=ORIGIN) (target=TARGET) PATH))+ 39 ` 40 41 type reqParams struct { 42 encoding string 43 origin string 44 target string 45 paths []string 46 } 47 48 func usageAndExit(s string) { 49 flag.Usage() 50 if s != "" { 51 fmt.Fprintln(os.Stderr, s) 52 } 53 os.Exit(1) 54 } 55 56 // Main initializes the gNMI client. 57 func Main() { 58 cfg := &gnmi.Config{} 59 flag.StringVar(&cfg.Addr, "addr", "", "Address of gNMI gRPC server with optional VRF name") 60 flag.StringVar(&cfg.CAFile, "cafile", "", "Path to server TLS certificate file") 61 flag.StringVar(&cfg.CertFile, "certfile", "", "Path to client TLS certificate file") 62 flag.StringVar(&cfg.KeyFile, "keyfile", "", "Path to client TLS private key file") 63 flag.StringVar(&cfg.Password, "password", "", "Password to authenticate with") 64 flag.StringVar(&cfg.Username, "username", "", "Username to authenticate with") 65 flag.StringVar(&cfg.Compression, "compression", "", "Compression method. "+ 66 `Supported options: "" and "gzip"`) 67 flag.BoolVar(&cfg.TLS, "tls", false, "Enable TLS") 68 flag.StringVar(&cfg.TLSMinVersion, "tls-min-version", "", 69 fmt.Sprintf("Set minimum TLS version for connection (%s)", gnmi.TLSVersions)) 70 flag.StringVar(&cfg.TLSMaxVersion, "tls-max-version", "", 71 fmt.Sprintf("Set maximum TLS version for connection (%s)", gnmi.TLSVersions)) 72 flag.BoolVar(&cfg.BDP, "bdp", true, 73 "Enable Bandwidth Delay Product (BDP) estimation and dynamic flow control window") 74 outputVersion := flag.Bool("version", false, "print version information") 75 76 subscribeOptions := &gnmi.SubscribeOptions{} 77 flag.StringVar(&subscribeOptions.Prefix, "prefix", "", "Subscribe prefix path") 78 flag.BoolVar(&subscribeOptions.UpdatesOnly, "updates_only", false, 79 "Subscribe to updates only (false | true)") 80 flag.StringVar(&subscribeOptions.Mode, "mode", "stream", 81 "Subscribe mode (stream | once | poll)") 82 flag.StringVar(&subscribeOptions.StreamMode, "stream_mode", "target_defined", 83 "Subscribe stream mode, only applies for stream subscriptions "+ 84 "(target_defined | on_change | sample)") 85 sampleIntervalStr := flag.String("sample_interval", "0", "Subscribe sample interval, "+ 86 "only applies for sample subscriptions (400ms, 2.5s, 1m, etc.)") 87 heartbeatIntervalStr := flag.String("heartbeat_interval", "0", "Subscribe heartbeat "+ 88 "interval, only applies for on-change subscriptions (400ms, 2.5s, 1m, etc.)") 89 arbitrationStr := flag.String("arbitration", "", "master arbitration identifier "+ 90 "([<role_id>:]<election_id>)") 91 historyStartStr := flag.String("history_start", "", "Historical data subscription "+ 92 "start time (nanoseconds since Unix epoch or RFC3339 format with nanosecond "+ 93 "precision, e.g., 2006-01-02T15:04:05.999999999+07:00)") 94 historyEndStr := flag.String("history_end", "", "Historical data subscription "+ 95 "end time (nanoseconds since Unix epoch or RFC3339 format with nanosecond "+ 96 "precision, e.g., 2006-01-02T15:04:05.999999999+07:00)") 97 historySnapshotStr := flag.String("history_snapshot", "", "Historical data subscription "+ 98 "snapshot time (nanoseconds since Unix epoch or RFC3339 format with nanosecond "+ 99 "precision, e.g., 2006-01-02T15:04:05.999999999+07:00)") 100 dataTypeStr := flag.String("data_type", "all", 101 "Get data type (all | config | state | operational)") 102 flag.StringVar(&cfg.Token, "token", "", "Authentication token") 103 grpcMetadata := aflag.Map{} 104 flag.Var(grpcMetadata, "grpcmetadata", 105 "key=value gRPC metadata fields, can be used repeatedly") 106 107 debugMode := flag.String("debug", "", "Enable a debug mode:\n"+ 108 " 'proto' : print SubscribeResponses in protobuf text format\n"+ 109 " 'latency' : print timing numbers to help debug latency\n"+ 110 " 'throughput' : print number of notifications sent in a second\n"+ 111 " 'clog' : start a subscribe and then don't read any of the responses") 112 113 keepaliveTimeStr := flag.String("keepalive_time", "", "Keepalive ping interval. "+ 114 "After inactivity of this duration, ping the server (30s, 2m, etc. Default 10s). "+ 115 "10s is the minimum value allowed. If a value less than 10s is supplied, 10s will be used") 116 117 flag.Usage = func() { 118 fmt.Fprintln(os.Stderr, help) 119 flag.PrintDefaults() 120 } 121 flag.Parse() 122 if *outputVersion { 123 if info, ok := debug.ReadBuildInfo(); ok { 124 for _, data := range info.Settings { 125 if data.Key == "vcs.revision" { 126 fmt.Printf("%s_%s", data.Value, info.GoVersion) 127 } 128 } 129 } else { 130 fmt.Printf("version information only available in go 1.18+") 131 } 132 return 133 } 134 135 if cfg.Addr == "" { 136 usageAndExit("error: address not specified") 137 } 138 cfg.GRPCMetadata = grpcMetadata 139 140 var sampleInterval, heartbeatInterval time.Duration 141 var err error 142 if sampleInterval, err = time.ParseDuration(*sampleIntervalStr); err != nil { 143 usageAndExit(fmt.Sprintf("error: sample interval (%s) invalid", *sampleIntervalStr)) 144 } 145 subscribeOptions.SampleInterval = uint64(sampleInterval) 146 if heartbeatInterval, err = time.ParseDuration(*heartbeatIntervalStr); err != nil { 147 usageAndExit(fmt.Sprintf("error: heartbeat interval (%s) invalid", *heartbeatIntervalStr)) 148 } 149 subscribeOptions.HeartbeatInterval = uint64(heartbeatInterval) 150 151 var histExt *gnmi_ext.Extension_History 152 if *historyStartStr != "" || *historyEndStr != "" || *historySnapshotStr != "" { 153 if *historySnapshotStr != "" { 154 if *historyStartStr != "" || *historyEndStr != "" { 155 usageAndExit("error: specified history start/end and snapshot time") 156 } 157 t, err := parseTime(*historySnapshotStr) 158 if err != nil { 159 usageAndExit(fmt.Sprintf("error: invalid snapshot time (%s): %s", 160 *historySnapshotStr, err)) 161 } 162 histExt = gnmi.HistorySnapshotExtension(t.UnixNano()) 163 } else { 164 var s, e int64 165 if *historyStartStr != "" { 166 st, err := parseTime(*historyStartStr) 167 if err != nil { 168 usageAndExit(fmt.Sprintf("error: invalid start time (%s): %s", 169 *historyStartStr, err)) 170 } 171 s = st.UnixNano() 172 } 173 if *historyEndStr != "" { 174 et, err := parseTime(*historyEndStr) 175 if err != nil { 176 usageAndExit(fmt.Sprintf("error: invalid end time (%s): %s", 177 *historyEndStr, err)) 178 } 179 e = et.UnixNano() 180 } 181 histExt = gnmi.HistoryRangeExtension(s, e) 182 } 183 } 184 185 if *keepaliveTimeStr != "" { 186 var keepaliveTime time.Duration 187 var err error 188 if keepaliveTime, err = time.ParseDuration(*keepaliveTimeStr); err != nil { 189 usageAndExit(fmt.Sprintf("error: keepalive time (%s) invalid", *keepaliveTimeStr)) 190 } 191 192 timeout := time.Duration(keepaliveTime * time.Second) 193 cfg.DialOptions = append(cfg.DialOptions, 194 grpc.WithKeepaliveParams(keepalive.ClientParameters{Time: timeout})) 195 } 196 197 args := flag.Args() 198 199 ctx := gnmi.NewContext(context.Background(), cfg) 200 client, err := gnmi.Dial(cfg) 201 if err != nil { 202 glog.Fatal(err) 203 } 204 205 var setOps []*gnmi.Operation 206 for i := 0; i < len(args); i++ { 207 op := args[i] 208 switch op { 209 case "capabilities": 210 if len(setOps) != 0 { 211 usageAndExit("error: 'capabilities' not allowed after" + 212 " 'update|replace|delete|union_replace'") 213 } 214 err := gnmi.Capabilities(ctx, client) 215 if err != nil { 216 glog.Fatal(err) 217 } 218 return 219 case "get": 220 if len(setOps) != 0 { 221 usageAndExit("error: 'get' not allowed after" + 222 " 'update|replace|delete|union_replace'") 223 } 224 pathParams, argsParsed := parsereqParams(args[1:], false) 225 if argsParsed == 0 { 226 usageAndExit("error: missing path") 227 } 228 for _, pathParam := range pathParams { 229 req, err := newGetRequest(pathParam, *dataTypeStr) 230 if err != nil { 231 usageAndExit("error: " + err.Error()) 232 } 233 234 err = gnmi.GetWithRequest(ctx, client, req) 235 if err != nil { 236 glog.Fatal(err) 237 } 238 } 239 240 return 241 case "subscribe": 242 if len(setOps) != 0 { 243 usageAndExit("error: 'subscribe' not allowed after" + 244 " 'update|replace|delete|union_replace'") 245 } 246 var g errgroup.Group 247 pathParams, argsParsed := parsereqParams(args[1:], false) 248 if argsParsed == 0 { 249 usageAndExit("error: missing path") 250 } 251 for _, pathParam := range pathParams { 252 subOptions, err := newSubscribeOptions(pathParam, histExt, subscribeOptions) 253 if err != nil { 254 usageAndExit("error: " + err.Error()) 255 } 256 257 respChan := make(chan *pb.SubscribeResponse) 258 g.Go(func() error { 259 return gnmi.SubscribeErr(ctx, client, subOptions, respChan) 260 }) 261 switch *debugMode { 262 case "proto": 263 for resp := range respChan { 264 fmt.Println(resp) 265 } 266 case "latency": 267 for resp := range respChan { 268 printLatencyStats(resp) 269 } 270 case "throughput": 271 handleThroughput(respChan) 272 case "clog": 273 // Don't read any subscription updates 274 g.Wait() 275 case "": 276 go processSubscribeResponses(pathParam.origin, respChan) 277 278 default: 279 usageAndExit(fmt.Sprintf("unknown debug option: %q", *debugMode)) 280 } 281 } 282 if err := g.Wait(); err != nil { 283 glog.Fatal(err) 284 } 285 return 286 case "update", "replace", "delete", "union_replace": 287 j, op, err := newSetOperation(i, args, *arbitrationStr) 288 if err != nil { 289 usageAndExit("error: " + err.Error()) 290 } 291 if op != nil { 292 setOps = append(setOps, op) 293 } 294 i = j 295 default: 296 usageAndExit(fmt.Sprintf("error: unknown operation %q", args[i])) 297 } 298 } 299 arb, err := gnmi.ArbitrationExt(*arbitrationStr) 300 if err != nil { 301 glog.Fatal(err) 302 } 303 var exts []*gnmi_ext.Extension 304 if arb != nil { 305 exts = append(exts, arb) 306 } 307 err = gnmi.Set(ctx, client, setOps, exts...) 308 if err != nil { 309 glog.Fatal(err) 310 } 311 312 } 313 314 func newSetOperation( 315 index int, 316 args []string, 317 arbitrationStr string) (int, *gnmi.Operation, error) { 318 319 // ok if no args, if arbitration was specified 320 if len(args) == index+1 && arbitrationStr == "" { 321 return 0, nil, errors.New("missing path") 322 } 323 324 op := &gnmi.Operation{ 325 Type: args[index], 326 } 327 index++ 328 if len(args) <= index { 329 return index, nil, nil 330 } 331 332 pathParams, argsParsed := parsereqParams(args[index:], true) 333 index += argsParsed 334 335 // process update | replace | delete | union_replace request one at a time 336 pathParam := pathParams[0] 337 338 // check that encoding is not set 339 if pathParam.encoding != "" { 340 return 0, nil, fmt.Errorf("encoding option is not supported for '%s'", op.Type) 341 } 342 343 if len(pathParam.paths) == 0 { 344 return 0, nil, fmt.Errorf("missing path for '%s'", op.Type) 345 } 346 347 op.Path = gnmi.SplitPath(pathParam.paths[0]) 348 op.Origin = pathParam.origin 349 op.Target = pathParam.target 350 if op.Type == "delete" { 351 // set index to be right before the next arg that needs to be processed 352 index-- 353 } else { 354 // no need for index-- since the value of update/replace/union_replace is right 355 // before the next arg 356 if len(args) == index { 357 return 0, nil, errors.New("missing JSON or FILEPATH to data") 358 } 359 op.Val = args[index] 360 } 361 362 return index, op, nil 363 } 364 365 func newSubscribeOptions( 366 pathParam reqParams, 367 histExt *gnmi_ext.Extension_History, 368 subscribeOptions *gnmi.SubscribeOptions) (*gnmi.SubscribeOptions, error) { 369 370 origin := pathParam.origin 371 target := pathParam.target 372 paths := pathParam.paths 373 encoding := pathParam.encoding 374 375 // check that encoding is not set 376 if encoding != "" { 377 return nil, errors.New("encoding option is not supported for 'subscribe'") 378 } 379 380 subOptions := new(gnmi.SubscribeOptions) 381 *subOptions = *subscribeOptions 382 subOptions.Origin = origin 383 subOptions.Target = target 384 subOptions.Paths = gnmi.SplitPaths(paths) 385 if histExt != nil { 386 subOptions.Extensions = []*gnmi_ext.Extension{{ 387 Ext: histExt, 388 }} 389 } 390 391 return subOptions, nil 392 } 393 394 func newGetRequest(pathParam reqParams, dataTypeStr string) (*pb.GetRequest, error) { 395 origin := pathParam.origin 396 target := pathParam.target 397 paths := pathParam.paths 398 encoding := pathParam.encoding 399 400 req, err := gnmi.NewGetRequest(gnmi.SplitPaths(paths), origin) 401 if err != nil { 402 glog.Fatal(err) 403 } 404 405 // set target 406 if target != "" { 407 if req.Prefix == nil { 408 req.Prefix = &pb.Path{} 409 } 410 req.Prefix.Target = target 411 } 412 413 // set type 414 switch strings.ToLower(dataTypeStr) { 415 case "", "all": 416 req.Type = pb.GetRequest_ALL 417 case "config": 418 req.Type = pb.GetRequest_CONFIG 419 case "state": 420 req.Type = pb.GetRequest_STATE 421 case "operational": 422 req.Type = pb.GetRequest_OPERATIONAL 423 default: 424 return nil, fmt.Errorf("invalid data type (%s)", dataTypeStr) 425 } 426 427 // set encoding 428 switch en := strings.ToLower(encoding); en { 429 case "ascii": 430 req.Encoding = pb.Encoding_ASCII 431 case "json": 432 req.Encoding = pb.Encoding_JSON 433 case "json_ietf": 434 req.Encoding = pb.Encoding_JSON_IETF 435 case "proto": 436 req.Encoding = pb.Encoding_PROTO 437 case "bytes": 438 req.Encoding = pb.Encoding_BYTES 439 case "": 440 default: 441 return nil, fmt.Errorf( 442 `invalid encoding '%s' 443 Supported encodings are (case insensitive): 444 - JSON 445 - Bytes 446 - Proto 447 - ASCII 448 - JSON_IETF`, en) 449 } 450 451 return req, nil 452 } 453 454 func processSubscribeResponses(origin string, respChan chan *pb.SubscribeResponse) { 455 for resp := range respChan { 456 if err := gnmi.LogSubscribeResponse(resp); err != nil { 457 glog.Fatal(err) 458 } 459 } 460 } 461 462 // Parse string timestamp, first trying for ns since epoch, and then 463 // for RFC3339. 464 func parseTime(ts string) (time.Time, error) { 465 if ti, err := strconv.ParseInt(ts, 10, 64); err == nil { 466 return time.Unix(0, ti), nil 467 } 468 return time.Parse(time.RFC3339Nano, ts) 469 } 470 471 func parseStringOpt(s, prefix string) (string, bool) { 472 if strings.HasPrefix(s, prefix+"=") { 473 return strings.TrimPrefix(s, prefix+"="), true 474 } 475 return "", false 476 } 477 478 func parseOrigin(s string) (string, bool) { 479 return parseStringOpt(s, "origin") 480 } 481 482 func parseTarget(s string) (string, bool) { 483 return parseStringOpt(s, "target") 484 } 485 486 func parseEncoding(s string) (string, bool) { 487 return parseStringOpt(s, "encoding") 488 } 489 490 func parsereqParams(args []string, maxOnePath bool) ([]reqParams, int) { 491 var pathParam *reqParams = new(reqParams) 492 var pathParams []reqParams 493 var argsParsed int 494 495 // [[ Some Considerations ]] 496 // - No need to follow encoding - origin - target - path as names are specified. 497 // - PATHS+ still mark the end of a pathParam 498 // - only path is required and everything else is optional 499 // - there can be one or more paths 500 // - there can be zero or one encoding, origin, and target. 501 502 var isOriginSet bool 503 var isTargetSet bool 504 var isEncodingSet bool 505 506 // check if the current config forms a pathParam 507 // If yes, reset all the trackers and add it to pathParams 508 // otherwise, don't do anything 509 var checkGroup = func(isItemSet bool) { 510 if isItemSet || len(pathParam.paths) > 0 { 511 isOriginSet = false 512 isTargetSet = false 513 isEncodingSet = false 514 pathParams = append(pathParams, *pathParam) 515 pathParam = new(reqParams) 516 } 517 } 518 519 for _, arg := range args { 520 argsParsed++ 521 if o, ok := parseOrigin(arg); ok { 522 checkGroup(isOriginSet) 523 pathParam.origin = o 524 isOriginSet = true 525 } else if t, ok := parseTarget(arg); ok { 526 checkGroup(isTargetSet) 527 pathParam.target = t 528 isTargetSet = true 529 } else if e, ok := parseEncoding(arg); ok { 530 checkGroup(isEncodingSet) 531 pathParam.encoding = e 532 isEncodingSet = true 533 } else { 534 pathParam.paths = append(pathParam.paths, arg) 535 if maxOnePath { 536 break 537 } 538 } 539 } 540 541 // The last pathParam wasn't added, add it now 542 pathParams = append(pathParams, *pathParam) 543 544 // validate that all reqParams have a valid path 545 for _, param := range pathParams { 546 if param.paths == nil { 547 return pathParams, 0 // no path provided 548 } 549 } 550 551 return pathParams, argsParsed 552 } 553 554 func printLatencyStats(s *pb.SubscribeResponse) { 555 switch resp := s.Response.(type) { 556 case *pb.SubscribeResponse_SyncResponse: 557 fmt.Printf("now=%d sync_response=%t\n", 558 time.Now().UnixNano(), resp.SyncResponse) 559 case *pb.SubscribeResponse_Update: 560 notif := resp.Update 561 now := time.Now().UnixNano() 562 fmt.Printf("now=%d timestamp=%d latency=%s prefix=%s size=%d updates=%d deletes=%d\n", 563 now, 564 notif.Timestamp, 565 time.Duration(now-notif.Timestamp), 566 gnmi.StrPath(notif.Prefix), 567 proto.Size(s), 568 len(notif.Update), 569 len(notif.Delete), 570 ) 571 } 572 } 573 574 func handleThroughput(respChan <-chan *pb.SubscribeResponse) { 575 var notifs uint64 576 var updates uint64 577 go func() { 578 var ( 579 lastNotifs uint64 580 lastUpdates uint64 581 lastTime = time.Now() 582 ) 583 ticker := time.NewTicker(10 * time.Second) 584 for t := range ticker.C { 585 newNotifs := atomic.LoadUint64(¬ifs) 586 newUpdates := atomic.LoadUint64(&updates) 587 dNotifs := newNotifs - lastNotifs 588 dUpdates := newUpdates - lastUpdates 589 since := t.Sub(lastTime).Seconds() 590 lastNotifs = newNotifs 591 lastUpdates = newUpdates 592 lastTime = t 593 fmt.Printf("%s: %f notifs/s %f updates/s\n", 594 t, float64(dNotifs)/since, float64(dUpdates)/since) 595 } 596 }() 597 598 for resp := range respChan { 599 r, ok := resp.Response.(*pb.SubscribeResponse_Update) 600 if !ok { 601 continue 602 } 603 notif := r.Update 604 atomic.AddUint64(¬ifs, 1) 605 atomic.AddUint64(&updates, uint64(len(notif.Update)+len(notif.Delete))) 606 } 607 return 608 }