github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/gnmi/operation.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 gnmi 6 7 import ( 8 "bufio" 9 "bytes" 10 "context" 11 "encoding/base64" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "io" 16 "io/ioutil" 17 "math" 18 "os" 19 "path" 20 "strconv" 21 "strings" 22 "time" 23 24 pb "github.com/openconfig/gnmi/proto/gnmi" 25 "github.com/openconfig/gnmi/proto/gnmi_ext" 26 "google.golang.org/grpc/codes" 27 ) 28 29 // GetWithRequest takes a fully formed GetRequest, performs the Get, 30 // and displays any response. 31 func GetWithRequest(ctx context.Context, client pb.GNMIClient, 32 req *pb.GetRequest) error { 33 resp, err := client.Get(ctx, req) 34 if err != nil { 35 return err 36 } 37 for _, notif := range resp.Notification { 38 prefix := StrPath(notif.Prefix) 39 for _, update := range notif.Update { 40 fmt.Printf("%s:\n", path.Join(prefix, StrPath(update.Path))) 41 fmt.Println(StrUpdateVal(update)) 42 } 43 } 44 return nil 45 } 46 47 // Get sends a GetRequest to the given client. 48 func Get(ctx context.Context, client pb.GNMIClient, paths [][]string, 49 origin string) error { 50 req, err := NewGetRequest(paths, origin) 51 if err != nil { 52 return err 53 } 54 return GetWithRequest(ctx, client, req) 55 } 56 57 // Capabilities retuns the capabilities of the client. 58 func Capabilities(ctx context.Context, client pb.GNMIClient) error { 59 resp, err := client.Capabilities(ctx, &pb.CapabilityRequest{}) 60 if err != nil { 61 return err 62 } 63 fmt.Printf("Version: %s\n", resp.GNMIVersion) 64 for _, mod := range resp.SupportedModels { 65 fmt.Printf("SupportedModel: %s\n", mod) 66 } 67 for _, enc := range resp.SupportedEncodings { 68 fmt.Printf("SupportedEncoding: %s\n", enc) 69 } 70 return nil 71 } 72 73 // val may be a path to a file or it may be json. First see if it is a 74 // file, if so return its contents, otherwise return val 75 func extractContent(val string, origin string) []byte { 76 if jsonBytes, err := ioutil.ReadFile(val); err == nil { 77 return jsonBytes 78 } 79 // for CLI commands we don't need to add the outer quotes 80 if origin == "cli" { 81 return []byte(val) 82 } 83 // Best effort check if the value might a string literal, in which 84 // case wrap it in quotes. This is to allow a user to do: 85 // gnmi update ../hostname host1234 86 // gnmi update ../description 'This is a description' 87 // instead of forcing them to quote the string: 88 // gnmi update ../hostname '"host1234"' 89 // gnmi update ../description '"This is a description"' 90 maybeUnquotedStringLiteral := func(s string) bool { 91 if s == "true" || s == "false" || s == "null" || // JSON reserved words 92 strings.ContainsAny(s, `"'{}[]`) { // Already quoted or is a JSON object or array 93 return false 94 } else if _, err := strconv.ParseInt(s, 0, 32); err == nil { 95 // Integer. Using byte size of 32 because larger integer 96 // types are supposed to be sent as strings in JSON. 97 return false 98 } else if _, err := strconv.ParseFloat(s, 64); err == nil { 99 // Float 100 return false 101 } 102 103 return true 104 } 105 if maybeUnquotedStringLiteral(val) { 106 out := make([]byte, len(val)+2) 107 out[0] = '"' 108 copy(out[1:], val) 109 out[len(out)-1] = '"' 110 return out 111 } 112 return []byte(val) 113 } 114 115 // StrUpdateVal will return a string representing the value within the supplied update 116 func StrUpdateVal(u *pb.Update) string { 117 return strUpdateVal(u, false) 118 } 119 120 // StrUpdateValCompactJSON will return a string representing the value within the supplied 121 // update. If the value is a JSON value, a non-indented JSON string will be returned. 122 func StrUpdateValCompactJSON(u *pb.Update) string { 123 return strUpdateVal(u, true) 124 } 125 126 func strUpdateVal(u *pb.Update, alwaysCompactJSON bool) string { 127 if u.Value != nil { 128 // Backwards compatibility with pre-v0.4 gnmi 129 switch u.Value.Type { 130 case pb.Encoding_JSON, pb.Encoding_JSON_IETF: 131 return strJSON(u.Value.Value, alwaysCompactJSON) 132 case pb.Encoding_BYTES, pb.Encoding_PROTO: 133 return base64.StdEncoding.EncodeToString(u.Value.Value) 134 case pb.Encoding_ASCII: 135 return string(u.Value.Value) 136 default: 137 return string(u.Value.Value) 138 } 139 } 140 return strVal(u.Val, alwaysCompactJSON) 141 } 142 143 // StrVal will return a string representing the supplied value 144 func StrVal(val *pb.TypedValue) string { 145 return strVal(val, false) 146 } 147 148 // StrValCompactJSON will return a string representing the supplied value. If the value 149 // is a JSON value, a non-indented JSON string will be returned. 150 func StrValCompactJSON(val *pb.TypedValue) string { 151 return strVal(val, true) 152 } 153 154 func strVal(val *pb.TypedValue, alwaysCompactJSON bool) string { 155 switch v := val.GetValue().(type) { 156 case *pb.TypedValue_StringVal: 157 return v.StringVal 158 case *pb.TypedValue_JsonIetfVal: 159 return strJSON(v.JsonIetfVal, alwaysCompactJSON) 160 case *pb.TypedValue_JsonVal: 161 return strJSON(v.JsonVal, alwaysCompactJSON) 162 case *pb.TypedValue_IntVal: 163 return strconv.FormatInt(v.IntVal, 10) 164 case *pb.TypedValue_UintVal: 165 return strconv.FormatUint(v.UintVal, 10) 166 case *pb.TypedValue_BoolVal: 167 return strconv.FormatBool(v.BoolVal) 168 case *pb.TypedValue_BytesVal: 169 return base64.StdEncoding.EncodeToString(v.BytesVal) 170 case *pb.TypedValue_DecimalVal: 171 return strDecimal64(v.DecimalVal) 172 case *pb.TypedValue_FloatVal: 173 return strconv.FormatFloat(float64(v.FloatVal), 'g', -1, 32) 174 case *pb.TypedValue_DoubleVal: 175 return strconv.FormatFloat(float64(v.DoubleVal), 'g', -1, 64) 176 case *pb.TypedValue_LeaflistVal: 177 return strLeaflist(v.LeaflistVal) 178 case *pb.TypedValue_AsciiVal: 179 return v.AsciiVal 180 case *pb.TypedValue_AnyVal: 181 return v.AnyVal.String() 182 case *pb.TypedValue_ProtoBytes: 183 return base64.StdEncoding.EncodeToString(v.ProtoBytes) 184 case nil: 185 return "" 186 default: 187 panic(v) 188 } 189 } 190 191 func strJSON(inJSON []byte, alwaysCompactJSON bool) string { 192 var ( 193 out bytes.Buffer 194 err error 195 ) 196 // Check for ',' as simple heuristic on whether to expand JSON 197 // onto multiple lines, or compact it to a single line. 198 if !alwaysCompactJSON && bytes.Contains(inJSON, []byte{','}) { 199 err = json.Indent(&out, inJSON, "", " ") 200 } else { 201 err = json.Compact(&out, inJSON) 202 } 203 if err != nil { 204 return fmt.Sprintf("(error unmarshalling json: %s)\n", err) + string(inJSON) 205 } 206 return out.String() 207 } 208 209 func strDecimal64(d *pb.Decimal64) string { 210 var i, frac int64 211 if d.Precision > 0 { 212 div := int64(10) 213 it := d.Precision - 1 214 for it > 0 { 215 div *= 10 216 it-- 217 } 218 i = d.Digits / div 219 frac = d.Digits % div 220 } else { 221 i = d.Digits 222 } 223 format := "%d.%0*d" 224 if frac < 0 { 225 if i == 0 { 226 // The integer part doesn't provide the necessary minus sign. 227 format = "-" + format 228 } 229 frac = -frac 230 } 231 return fmt.Sprintf(format, i, int(d.Precision), frac) 232 } 233 234 // strLeafList builds a human-readable form of a leaf-list. e.g. [1, 2, 3] or [a, b, c] 235 func strLeaflist(v *pb.ScalarArray) string { 236 var b strings.Builder 237 b.WriteByte('[') 238 239 for i, elm := range v.Element { 240 b.WriteString(StrVal(elm)) 241 if i < len(v.Element)-1 { 242 b.WriteString(", ") 243 } 244 } 245 246 b.WriteByte(']') 247 return b.String() 248 } 249 250 // TypedValue marshals an interface into a gNMI TypedValue value 251 func TypedValue(val interface{}) *pb.TypedValue { 252 // TODO: handle more types: 253 // maps 254 // key.Key 255 // key.Map 256 // ... etc 257 switch v := val.(type) { 258 case *pb.TypedValue: 259 return v 260 case string: 261 return &pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: v}} 262 case int: 263 return &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: int64(v)}} 264 case int8: 265 return &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: int64(v)}} 266 case int16: 267 return &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: int64(v)}} 268 case int32: 269 return &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: int64(v)}} 270 case int64: 271 return &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: v}} 272 case uint: 273 return &pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: uint64(v)}} 274 case uint8: 275 return &pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: uint64(v)}} 276 case uint16: 277 return &pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: uint64(v)}} 278 case uint32: 279 return &pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: uint64(v)}} 280 case uint64: 281 return &pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: v}} 282 case bool: 283 return &pb.TypedValue{Value: &pb.TypedValue_BoolVal{BoolVal: v}} 284 case float32: 285 return &pb.TypedValue{Value: &pb.TypedValue_FloatVal{FloatVal: v}} 286 case float64: 287 return &pb.TypedValue{Value: &pb.TypedValue_DoubleVal{DoubleVal: v}} 288 case []byte: 289 return &pb.TypedValue{Value: &pb.TypedValue_BytesVal{BytesVal: v}} 290 case []interface{}: 291 gnmiElems := make([]*pb.TypedValue, len(v)) 292 for i, elem := range v { 293 gnmiElems[i] = TypedValue(elem) 294 } 295 return &pb.TypedValue{ 296 Value: &pb.TypedValue_LeaflistVal{ 297 LeaflistVal: &pb.ScalarArray{ 298 Element: gnmiElems, 299 }}} 300 default: 301 panic(fmt.Sprintf("unexpected type %T for value %v", val, val)) 302 } 303 } 304 305 // ExtractValue pulls a value out of a gNMI Update, parsing JSON if present. 306 // Possible return types: 307 // 308 // string 309 // int64 310 // uint64 311 // bool 312 // []byte 313 // float32 314 // *gnmi.Decimal64 315 // json.Number 316 // *any.Any 317 // []interface{} 318 // map[string]interface{} 319 func ExtractValue(update *pb.Update) (interface{}, error) { 320 var i interface{} 321 var err error 322 if update == nil { 323 return nil, fmt.Errorf("empty update") 324 } 325 if update.Val != nil { 326 i, err = extractValueV04(update.Val) 327 } else if update.Value != nil { 328 i, err = extractValueV03(update.Value) 329 } 330 return i, err 331 } 332 333 func extractValueV04(val *pb.TypedValue) (interface{}, error) { 334 switch v := val.Value.(type) { 335 case *pb.TypedValue_StringVal: 336 return v.StringVal, nil 337 case *pb.TypedValue_IntVal: 338 return v.IntVal, nil 339 case *pb.TypedValue_UintVal: 340 return v.UintVal, nil 341 case *pb.TypedValue_BoolVal: 342 return v.BoolVal, nil 343 case *pb.TypedValue_BytesVal: 344 return v.BytesVal, nil 345 case *pb.TypedValue_FloatVal: 346 return v.FloatVal, nil 347 case *pb.TypedValue_DoubleVal: 348 return v.DoubleVal, nil 349 case *pb.TypedValue_DecimalVal: 350 return v.DecimalVal, nil 351 case *pb.TypedValue_LeaflistVal: 352 elementList := v.LeaflistVal.Element 353 l := make([]interface{}, len(elementList)) 354 for i, element := range elementList { 355 el, err := extractValueV04(element) 356 if err != nil { 357 return nil, err 358 } 359 l[i] = el 360 } 361 return l, nil 362 case *pb.TypedValue_AnyVal: 363 return v.AnyVal, nil 364 case *pb.TypedValue_JsonVal: 365 return decode(v.JsonVal) 366 case *pb.TypedValue_JsonIetfVal: 367 return decode(v.JsonIetfVal) 368 case *pb.TypedValue_AsciiVal: 369 return v.AsciiVal, nil 370 case *pb.TypedValue_ProtoBytes: 371 return v.ProtoBytes, nil 372 } 373 return nil, fmt.Errorf("unhandled type of value %v", val.GetValue()) 374 } 375 376 func extractValueV03(val *pb.Value) (interface{}, error) { 377 switch val.Type { 378 case pb.Encoding_JSON, pb.Encoding_JSON_IETF: 379 return decode(val.Value) 380 case pb.Encoding_BYTES, pb.Encoding_PROTO: 381 return val.Value, nil 382 case pb.Encoding_ASCII: 383 return string(val.Value), nil 384 } 385 return nil, fmt.Errorf("unhandled type of value %v", val.GetValue()) 386 } 387 388 func decode(byteArr []byte) (interface{}, error) { 389 decoder := json.NewDecoder(bytes.NewReader(byteArr)) 390 decoder.UseNumber() 391 var value interface{} 392 err := decoder.Decode(&value) 393 return value, err 394 } 395 396 // DecimalToFloat converts a gNMI Decimal64 to a float64 397 func DecimalToFloat(dec *pb.Decimal64) float64 { 398 return float64(dec.Digits) / math.Pow10(int(dec.Precision)) 399 } 400 401 func update(p *pb.Path, val string) (*pb.Update, error) { 402 var v *pb.TypedValue 403 switch p.Origin { 404 case "", "openconfig": 405 v = &pb.TypedValue{ 406 Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: extractContent(val, p.Origin)}} 407 case "eos_native": 408 v = &pb.TypedValue{ 409 Value: &pb.TypedValue_JsonVal{JsonVal: extractContent(val, p.Origin)}} 410 case "cli", "test-regen-cli": 411 v = &pb.TypedValue{ 412 Value: &pb.TypedValue_AsciiVal{AsciiVal: string(extractContent(val, p.Origin))}} 413 case "p4_config": 414 b, err := ioutil.ReadFile(val) 415 if err != nil { 416 return nil, err 417 } 418 v = &pb.TypedValue{ 419 Value: &pb.TypedValue_ProtoBytes{ProtoBytes: b}} 420 default: 421 return nil, fmt.Errorf("unexpected origin: %q", p.Origin) 422 } 423 424 return &pb.Update{Path: p, Val: v}, nil 425 } 426 427 // Operation describes an gNMI operation. 428 type Operation struct { 429 Type string 430 Origin string 431 Target string 432 Path []string 433 Val string 434 } 435 436 func newSetRequest(setOps []*Operation, exts ...*gnmi_ext.Extension) (*pb.SetRequest, error) { 437 req := &pb.SetRequest{} 438 for _, op := range setOps { 439 p, err := ParseGNMIElements(op.Path) 440 if err != nil { 441 return nil, err 442 } 443 p.Origin = op.Origin 444 445 // Target must apply to the entire SetRequest. 446 if op.Target != "" { 447 req.Prefix = &pb.Path{ 448 Target: op.Target, 449 } 450 } 451 452 switch op.Type { 453 case "delete": 454 req.Delete = append(req.Delete, p) 455 case "update": 456 u, err := update(p, op.Val) 457 if err != nil { 458 return nil, err 459 } 460 req.Update = append(req.Update, u) 461 case "replace": 462 u, err := update(p, op.Val) 463 if err != nil { 464 return nil, err 465 } 466 req.Replace = append(req.Replace, u) 467 case "union_replace": 468 u, err := update(p, op.Val) 469 if err != nil { 470 return nil, err 471 } 472 req.UnionReplace = append(req.UnionReplace, u) 473 } 474 } 475 for _, ext := range exts { 476 req.Extension = append(req.Extension, ext) 477 } 478 return req, nil 479 } 480 481 // Set sends a SetRequest to the given client. 482 func Set(ctx context.Context, client pb.GNMIClient, setOps []*Operation, 483 exts ...*gnmi_ext.Extension) error { 484 req, err := newSetRequest(setOps, exts...) 485 if err != nil { 486 return err 487 } 488 resp, err := client.Set(ctx, req) 489 if err != nil { 490 return err 491 } 492 if resp.Message != nil && codes.Code(resp.Message.Code) != codes.OK { 493 return errors.New(resp.Message.Message) 494 } 495 return nil 496 } 497 498 // Subscribe sends a SubscribeRequest to the given client. 499 // Deprecated: Use SubscribeErr instead. 500 func Subscribe(ctx context.Context, client pb.GNMIClient, subscribeOptions *SubscribeOptions, 501 respChan chan<- *pb.SubscribeResponse, errChan chan<- error) { 502 defer close(errChan) 503 if err := SubscribeErr(ctx, client, subscribeOptions, respChan); err != nil { 504 errChan <- err 505 } 506 } 507 508 // SubscribeErr makes a gNMI.Subscribe call and writes the responses 509 // to the respChan. Before returning respChan will be closed. 510 func SubscribeErr(ctx context.Context, client pb.GNMIClient, subscribeOptions *SubscribeOptions, 511 respChan chan<- *pb.SubscribeResponse) error { 512 ctx, cancel := context.WithCancel(ctx) 513 defer cancel() 514 defer close(respChan) 515 516 stream, err := client.Subscribe(ctx) 517 if err != nil { 518 return err 519 } 520 req, err := NewSubscribeRequest(subscribeOptions) 521 if err != nil { 522 return err 523 } 524 if err := stream.Send(req); err != nil { 525 return err 526 } 527 528 for { 529 resp, err := stream.Recv() 530 if err != nil { 531 if err == io.EOF { 532 return nil 533 } 534 return err 535 } 536 537 select { 538 case respChan <- resp: 539 case <-ctx.Done(): 540 return ctx.Err() 541 } 542 543 // For POLL subscriptions, initiate a poll request by pressing ENTER 544 if subscribeOptions.Mode == "poll" { 545 switch resp.Response.(type) { 546 case *pb.SubscribeResponse_SyncResponse: 547 fmt.Print("Press ENTER to send a poll request: ") 548 reader := bufio.NewReader(os.Stdin) 549 reader.ReadString('\n') 550 551 pollReq := &pb.SubscribeRequest{ 552 Request: &pb.SubscribeRequest_Poll{ 553 Poll: &pb.Poll{}, 554 }, 555 } 556 if err := stream.Send(pollReq); err != nil { 557 return err 558 } 559 } 560 } 561 } 562 } 563 564 // LogSubscribeResponse logs update responses to stderr. 565 func LogSubscribeResponse(response *pb.SubscribeResponse) error { 566 switch resp := response.Response.(type) { 567 case *pb.SubscribeResponse_Error: 568 return errors.New(resp.Error.Message) 569 case *pb.SubscribeResponse_SyncResponse: 570 if !resp.SyncResponse { 571 return errors.New("initial sync failed") 572 } 573 case *pb.SubscribeResponse_Update: 574 t := time.Unix(0, resp.Update.Timestamp).UTC() 575 prefix := StrPath(resp.Update.Prefix) 576 var target string 577 if t := resp.Update.Prefix.GetTarget(); t != "" { 578 target = "(" + t + ") " 579 } 580 for _, del := range resp.Update.Delete { 581 fmt.Printf("[%s] %sDeleted %s\n", t.Format(time.RFC3339Nano), 582 target, 583 path.Join(prefix, StrPath(del))) 584 } 585 for _, update := range resp.Update.Update { 586 fmt.Printf("[%s] %s%s = %s\n", t.Format(time.RFC3339Nano), 587 target, 588 path.Join(prefix, StrPath(update.Path)), 589 StrUpdateVal(update)) 590 } 591 } 592 return nil 593 }