Skip to content

Commit

Permalink
Unmarshal SetRequest and Notification to a root GoStruct. (#726)
Browse files Browse the repository at this point in the history
* Unmarshal SetRequest and Notification to a root GoStruct.

Possible use cases:
* Maintaining a schema-aware view of the configuration at a gNMI server.
* Simulating the effects of a SetRequest to compare against results from a server.

* add some test cases

* Add parameter to skip validation

* improve comment
  • Loading branch information
wenovus authored Sep 14, 2022
1 parent 77d1f74 commit 076ea7e
Show file tree
Hide file tree
Showing 3 changed files with 784 additions and 0 deletions.
180 changes: 180 additions & 0 deletions ytypes/gnmi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package ytypes

import (
"fmt"
"reflect"

"github.com/openconfig/goyang/pkg/yang"
"github.com/openconfig/ygot/ygot"

gpb "github.com/openconfig/gnmi/proto/gnmi"
)

// UnmarshalNotifications unmarshals a Notification on a root GoStruct specified by
// "schema", and returns a reference to it.
//
// It does not make a copy and overwrites this value, so make a copy using
// ygot.DeepCopy() if you wish to retain the value at schema.Root prior to
// calling this function.
//
// - If preferShadowPath is specified, then the shadow path values are
// unmarshalled instead of non-shadow path values when GoStructs are generated
// with shadow paths.
// - If skipValidation is specified, then schema validation won't be performed
// after all the notifications have been unmarshalled.
func UnmarshalNotifications(schema *Schema, ns []*gpb.Notification, preferShadowPath, skipValidation bool) (ygot.GoStruct, error) {
for _, n := range ns {
_, err := UnmarshalSetRequest(schema, &gpb.SetRequest{
Prefix: n.Prefix,
Delete: n.Delete,
Update: n.Update,
}, preferShadowPath, true)
if err != nil {
return nil, err
}
}

root := schema.Root
if !skipValidation {
if err := validateGoStruct(root); err != nil {
return nil, fmt.Errorf("sum of notifications is not schema-compliant: %v", err)
}
}
return root, nil
}

// UnmarshalSetRequest applies a SetRequest on a root GoStruct specified by
// "schema", and returns a reference to the resulting schema.Root.
//
// It does not make a copy and overwrites this value, so make a copy using
// ygot.DeepCopy() if you wish to retain the value at schema.Root prior to
// calling this function.
//
// - If preferShadowPath is specified, then the shadow path values are
// unmarshalled instead of non-shadow path values when GoStructs are generated
// with shadow paths.
// - If skipValidation is specified, then schema validation won't be performed
// after the set request has been unmarshalled.
func UnmarshalSetRequest(schema *Schema, req *gpb.SetRequest, preferShadowPath, skipValidation bool) (ygot.GoStruct, error) {
root := schema.Root
node, nodeName, err := getOrCreateNode(schema.RootSchema(), root, req.Prefix, preferShadowPath)
if err != nil {
return nil, err
}

// Process deletes, then replace, then updates.
if err := deletePaths(schema.SchemaTree[nodeName], node, req.Delete, preferShadowPath); err != nil {
return nil, err
}
if err := replacePaths(schema.SchemaTree[nodeName], node, req.Replace, preferShadowPath); err != nil {
return nil, err
}
if err := updatePaths(schema.SchemaTree[nodeName], node, req.Update, preferShadowPath); err != nil {
return nil, err
}

if !skipValidation {
if err := validateGoStruct(root); err != nil {
return nil, fmt.Errorf("SetRequest is not schema-compliant: %v", err)
}
}
return root, nil
}

func validateGoStruct(goStruct ygot.GoStruct) error {
vroot, ok := goStruct.(validatedGoStruct)
if !ok {
return fmt.Errorf("schema root cannot be validated: (%T, %v)", goStruct, goStruct)
}
return vroot.ΛValidate()
}

// validatedGoStruct is an interface used for validating GoStructs.
// This interface is implemented by all Go structs (YANG container or lists),
// regardless of generation flag.
type validatedGoStruct interface {
// GoStruct ensures that the interface for a standard GoStruct
// is embedded.
ygot.GoStruct
// ΛValidate compares the contents of the implementing struct against
// the YANG schema, and returns an error if the struct's contents
// are not valid, or nil if the struct complies with the schema.
ΛValidate(...ygot.ValidationOption) error
}

// getOrCreateNode instantiates the node at the given path, and returns that
// node along with its name.
func getOrCreateNode(schema *yang.Entry, goStruct ygot.GoStruct, path *gpb.Path, preferShadowPath bool) (ygot.GoStruct, string, error) {
var gcopts []GetOrCreateNodeOpt
if preferShadowPath {
gcopts = append(gcopts, &PreferShadowPath{})
}
// Operate at the prefix level.
nodeI, _, err := GetOrCreateNode(schema, goStruct, path, gcopts...)
if err != nil {
return nil, "", fmt.Errorf("failed to GetOrCreate the prefix node: %v", err)
}
node, ok := nodeI.(ygot.GoStruct)
if !ok {
return nil, "", fmt.Errorf("prefix path points to a non-GoStruct, this is not allowed: %T, %v", nodeI, nodeI)
}

return node, reflect.TypeOf(nodeI).Elem().Name(), nil
}

// deletePaths deletes a slice of paths from the given GoStruct.
func deletePaths(schema *yang.Entry, goStruct ygot.GoStruct, paths []*gpb.Path, preferShadowPath bool) error {
var dopts []DelNodeOpt
if preferShadowPath {
dopts = append(dopts, &PreferShadowPath{})
}

for _, path := range paths {
if err := DeleteNode(schema, goStruct, path, dopts...); err != nil {
return err
}
}
return nil
}

// replacePaths unmarshals a slice of updates into the given GoStruct. It
// deletes the values at these paths before unmarshalling them. These updates
// can either by JSON-encoded or gNMI-encoded values (scalars).
func replacePaths(schema *yang.Entry, goStruct ygot.GoStruct, updates []*gpb.Update, preferShadowPath bool) error {
var dopts []DelNodeOpt
if preferShadowPath {
dopts = append(dopts, &PreferShadowPath{})
}

for _, update := range updates {
if err := DeleteNode(schema, goStruct, update.Path, dopts...); err != nil {
return err
}
if err := setNode(schema, goStruct, update, preferShadowPath); err != nil {
return err
}
}
return nil
}

// updatePaths unmarshals a slice of updates into the given GoStruct. These
// updates can either by JSON-encoded or gNMI-encoded values (scalars).
func updatePaths(schema *yang.Entry, goStruct ygot.GoStruct, updates []*gpb.Update, preferShadowPath bool) error {
for _, update := range updates {
if err := setNode(schema, goStruct, update, preferShadowPath); err != nil {
return err
}
}
return nil
}

// setNode unmarshals either a JSON-encoded value or a gNMI-encoded (scalar)
// value into the given GoStruct.
func setNode(schema *yang.Entry, goStruct ygot.GoStruct, update *gpb.Update, preferShadowPath bool) error {
sopts := []SetNodeOpt{&InitMissingElements{}}
if preferShadowPath {
sopts = append(sopts, &PreferShadowPath{})
}

return SetNode(schema, goStruct, update.Path, update.Val, sopts...)
}
Loading

0 comments on commit 076ea7e

Please sign in to comment.