Skip to content

Add New Feature : Allow UTCP Client to support GET,SET and CAPABILITIES (subscribe already supported) #127

@gwoodwa1

Description

@gwoodwa1

The subscribe method is already implemented with the UTCPClient and there is some minor changes needed to support the other operations for GET,SET and CAPABILITIES which will mean the full set are supported.

I have already applied changes locally and they are all within the transports/grpc/grpc_transport.go

First Change is to update CallTool to implement support for these new methods callGNMICapabilities,callGNMIGet,callGNMISet:

func (t *GRPCClientTransport) CallTool(
	ctx context.Context,
	toolName string,
	args map[string]any,
	prov Provider,
	l *string,
) (any, error) {
	gp, ok := prov.(*GRPCProvider)
	if !ok {
		return nil, errors.New("GRPCClientTransport can only be used with GRPCProvider")
	}

	if gp.ServiceName == "gnmi.gNMI" {
		switch gp.MethodName {
		case "Capabilities":
			return t.callGNMICapabilities(ctx, args, gp)
		case "Get":
			return t.callGNMIGet(ctx, args, gp)
		case "Set":
			return t.callGNMISet(ctx, args, gp)
		}
	}

	// ---- Fallback: UTCP server path ----
	// Add target to context if specified
	ctx = t.addTargetToContext(ctx, gp)

	conn, err := t.dial(ctx, gp)
	if err != nil {
		return nil, err
	}
	defer conn.Close()

	client := grpcpb.NewUTCPServiceClient(conn)

	payload, err := json.Marshal(args)
	if err != nil {
		return nil, err
	}

	resp, err := client.CallTool(ctx, &grpcpb.ToolCallRequest{
		Tool:     toolName,
		ArgsJson: string(payload),
	})
	if err != nil {
		return nil, err
	}

	var result any
	if resp.ResultJson != "" {
		_ = json.Unmarshal([]byte(resp.ResultJson), &result)
	}
	return result, nil
}

Next up we need to implement these new methods:

func (t *GRPCClientTransport) callGNMIGet(
	ctx context.Context,
	args map[string]any,
	gp *GRPCProvider,
) (any, error) {
	ctx = t.addTargetToContext(ctx, gp)

	conn, err := t.dial(ctx, gp)
	if err != nil { return nil, err }
	defer conn.Close()

	client := gnmi.NewGNMIClient(conn)

	// paths: []string
	var pathStrs []string
	if v, ok := args["paths"].([]any); ok {
		for _, p := range v { pathStrs = append(pathStrs, fmt.Sprint(p)) }
	} else if v, ok := args["paths"].([]string); ok {
		pathStrs = v
	} else {
		return nil, fmt.Errorf("gnmi_get: missing or invalid 'paths'")
	}
	var paths []*gnmi.Path
	for _, s := range pathStrs { paths = append(paths, parseGNMIPath(s)) }

	// encoding
	enc := gnmi.Encoding_JSON_IETF
	if s, ok := args["encoding"].(string); ok {
		switch strings.ToUpper(s) {
		case "JSON": enc = gnmi.Encoding_JSON
		case "ASCII": enc = gnmi.Encoding_ASCII
		case "BYTES": enc = gnmi.Encoding_BYTES
		case "PROTO": enc = gnmi.Encoding_PROTO
		}
	}

	req := &gnmi.GetRequest{
		Path:     paths,
		Encoding: enc,
	}
	// optional use_models: []string "name@version"
	if ums, ok := args["use_models"].([]any); ok {
		for _, x := range ums {
			if s, ok := x.(string); ok && s != "" {
				name, ver := s, ""
				if i := strings.IndexByte(s, '@'); i > 0 {
					name, ver = s[:i], s[i+1:]
				}
				req.UseModels = append(req.UseModels, &gnmi.ModelData{Name: name, Version: ver})
			}
		}
	}

	resp, err := client.Get(ctx, req)
	if err != nil { return nil, err }

	b, err := protojson.Marshal(resp)
	if err != nil { return nil, err }
	var obj any
	if err := json.Unmarshal(b, &obj); err != nil { return nil, err }
	return obj, nil
}
func (t *GRPCClientTransport) callGNMISet(
	ctx context.Context,
	args map[string]any,
	gp *GRPCProvider,
) (any, error) {
	ctx = t.addTargetToContext(ctx, gp)

	conn, err := t.dial(ctx, gp)
	if err != nil { return nil, err }
	defer conn.Close()

	client := gnmi.NewGNMIClient(conn)

	mkTV := func(v any) *gnmi.TypedValue {
		// Accept GNMI JSON typed form: {"stringVal": "..."} etc.
		if m, ok := v.(map[string]any); ok {
			b, _ := json.Marshal(m)
			tv := &gnmi.TypedValue{}
			if err := protojson.Unmarshal(b, tv); err == nil && tv.Value != nil {
				return tv
			}
		}
		// Fallback: stringify
		return &gnmi.TypedValue{Value: &gnmi.TypedValue_StringVal{StringVal: fmt.Sprint(v)}}
	}

	req := &gnmi.SetRequest{}

	if ups, ok := args["update"].([]any); ok {
		for _, u := range ups {
			if m, ok := u.(map[string]any); ok {
				p := parseGNMIPath(fmt.Sprint(m["path"]))
				req.Update = append(req.Update, &gnmi.Update{Path: p, Val: mkTV(m["val"])})
			}
		}
	}
	if reps, ok := args["replace"].([]any); ok {
		for _, r := range reps {
			if m, ok := r.(map[string]any); ok {
				p := parseGNMIPath(fmt.Sprint(m["path"]))
				req.Replace = append(req.Replace, &gnmi.Update{Path: p, Val: mkTV(m["val"])})
			}
		}
	}
	if dels, ok := args["delete"].([]any); ok {
		for _, d := range dels {
			req.Delete = append(req.Delete, parseGNMIPath(fmt.Sprint(d)))
		}
	}

	resp, err := client.Set(ctx, req)
	if err != nil { return nil, err }

	b, err := protojson.Marshal(resp)
	if err != nil { return nil, err }
	var obj any
	if err := json.Unmarshal(b, &obj); err != nil { return nil, err }
	return obj, nil
}
func (t *GRPCClientTransport) callGNMICapabilities(
	ctx context.Context,
	_ map[string]any,
	gp *GRPCProvider,
) (any, error) {
	// Attach target header (if any)
	ctx = t.addTargetToContext(ctx, gp)

	conn, err := t.dial(ctx, gp)
	if err != nil { return nil, err }
	defer conn.Close()

	client := gnmi.NewGNMIClient(conn)
	resp, err := client.Capabilities(ctx, &gnmi.CapabilityRequest{})
	if err != nil { return nil, err }

	b, err := protojson.Marshal(resp)
	if err != nil { return nil, err }
	var obj any
	if err := json.Unmarshal(b, &obj); err != nil { return nil, err }
	return obj, nil
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions