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  }