package sdk import ( "fmt" "strconv" "time" mnepb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement" "code.fbi.h-da.de/danet/gosdn/controller/customerrs" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" gpb "github.com/openconfig/gnmi/proto/gnmi" gnmiv "github.com/openconfig/gnmi/value" "github.com/openconfig/goyang/pkg/yang" "github.com/openconfig/ygot/ygot" "github.com/openconfig/ygot/ytypes" log "github.com/sirupsen/logrus" ) type DeviceModel struct { model ygot.ValidatedGoStruct schema *ytypes.Schema generatedUnmarshalFn func([]byte, ygot.GoStruct, ...ytypes.UnmarshalOpt) error genereatedSchemaTreeGzipFn func() []byte } func NewDeviceModel(generatedSchemaFn func() (*ytypes.Schema, error), generatedUnmarshalFn func([]byte, ygot.GoStruct, ...ytypes.UnmarshalOpt) error, genereatedSchemaTreeGzipFn func() []byte) (*DeviceModel, error) { schema, err := generatedSchemaFn() if err != nil { return nil, err } validatedCopy, err := createValidatedCopy(schema.Root) if err != nil { return nil, err } return &DeviceModel{ model: validatedCopy, schema: schema, generatedUnmarshalFn: generatedUnmarshalFn, genereatedSchemaTreeGzipFn: genereatedSchemaTreeGzipFn, }, nil } // Unmarshal takes a JSON as []byte and parses it into the DeviceModels GoStruct. func (d *DeviceModel) Unmarshal(json []byte, path *gpb.Path) error { opts := []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}} return unmarshal(d.schema, d.generatedUnmarshalFn, json, path, d.model, opts...) } // unmarshal parses a gNMI response to a go struct. func unmarshal( schema *ytypes.Schema, generatedUnmarshalFn func([]byte, ygot.GoStruct, ...ytypes.UnmarshalOpt) error, bytes []byte, path *gpb.Path, goStruct ygot.GoStruct, opt ...ytypes.UnmarshalOpt) error { defer func() { if r := recover(); r != nil { log.Error(r.(error)) } }() validatedDeepCopy, err := createValidatedCopy(schema.Root) if err != nil { return err } // returns the node we want to fill with the data contained in 'bytes', // using the specified 'path'. createdNode, _, err := ytypes.GetOrCreateNode(schema.RootSchema(), validatedDeepCopy, path) if err != nil { return err } validatedCreatedNode, ok := createdNode.(ygot.ValidatedGoStruct) if !ok { return &customerrs.InvalidTypeAssertionError{ Value: createdNode, Type: (*ygot.ValidatedGoStruct)(nil), } } if err := generatedUnmarshalFn(bytes, validatedCreatedNode, opt...); err != nil { return err } opts := []ygot.MergeOpt{&ygot.MergeOverwriteExistingFields{}} return ygot.MergeStructInto(goStruct, validatedDeepCopy, opts...) } // SetNode sets the current Node within the given ygot.GoStruct with the value // provided at the given path. func (d *DeviceModel) SetNode(path *gpb.Path, value *gpb.TypedValue) error { opts := []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}, &ytypes.TolerateJSONInconsistencies{}} return ytypes.SetNode(d.schema.RootSchema(), d.model, path, value, opts...) } // DeleteNode deletes the current Node within the given ygot.GoStruct at the // given path. func (d *DeviceModel) DeleteNode(path *gpb.Path) error { opts := []ytypes.DelNodeOpt{} return ytypes.DeleteNode(d.schema.RootSchema(), d.model, path, opts...) } // GetNode gets the node for the given path. func (d *DeviceModel) GetNode(path *gpb.Path, requestForIntendedState bool) ([]*gpb.Notification, error) { opts := []ytypes.GetNodeOpt{&ytypes.GetHandleWildcards{}, &ytypes.GetPartialKeyMatch{}} nodes, err := ytypes.GetNode(d.schema.RootSchema(), d.model, path, opts...) if err != nil { return nil, err } notifications := make([]*gpb.Notification, len(nodes)) for i, node := range nodes { if requestForIntendedState && node.Schema.ReadOnly() { return nil, status.Errorf(codes.Aborted, "Error: request for intended state not allowed on read-only field") } var nodeStruct interface{} = node.Data if requestForIntendedState && !node.Schema.IsLeaf() { nodeStruct, ok := nodeStruct.(ygot.GoStruct) if !ok { return nil, status.Errorf(codes.Aborted, "Error: asserting ytypes.TreeNode to GoStruct") } err = ygot.PruneConfigFalse(node.Schema, nodeStruct) if err != nil { return nil, err } } generatedNotifictaions, err := genGnmiNotification(path, nodeStruct) if err != nil { return nil, status.Errorf(codes.Aborted, "%v", err) } notifications[i] = generatedNotifictaions } return notifications, nil } func genGnmiNotification(path *gpb.Path, val any) (*gpb.Notification, error) { typedVal, err := ygot.EncodeTypedValue(val, gpb.Encoding_JSON_IETF) if err != nil { return nil, err } return &gpb.Notification{ Timestamp: time.Now().UnixNano(), Update: []*gpb.Update{ { Path: &gpb.Path{ Elem: path.GetElem(), }, Val: typedVal, }, }, }, nil } // Model returns the current model as byte slice representing a JSON. func (d *DeviceModel) Model(filterReadOnly bool) ([]byte, error) { modelCopy, err := createValidatedCopy(d.model) if err != nil { return nil, err } if filterReadOnly { if err := ygot.PruneConfigFalse(d.schema.RootSchema(), modelCopy); err != nil { return nil, err } } return ygot.Marshal7951(modelCopy, getYgotMarshal7951Config(), ygot.JSONIndent("")) } // SchemaTree returns the gzipped version of the SchemaTree as byte slice. func (d *DeviceModel) SchemaTreeGzip() ([]byte, error) { return d.genereatedSchemaTreeGzipFn(), nil } // Diff returns the difference of two DeviceModels based on their JSON // representation. func (d *DeviceModel) Diff(original, modified []byte) (*gpb.Notification, error) { opts := []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}} originalAsValidatedCopy, err := createValidatedCopy(d.schema.Root) if err != nil { return nil, err } modifiedAsValidatedCopy, err := createValidatedCopy(d.schema.Root) if err != nil { return nil, err } if err := d.generatedUnmarshalFn(original, originalAsValidatedCopy, opts...); err != nil { return nil, err } if err := d.generatedUnmarshalFn(modified, modifiedAsValidatedCopy, opts...); err != nil { return nil, err } return ygot.Diff(originalAsValidatedCopy, modifiedAsValidatedCopy) } func (d *DeviceModel) ValidateChange(operation mnepb.ApiOperation, path *gpb.Path, value []byte) ([]byte, error) { modelCopy, err := createValidatedCopy(d.model) if err != nil { return nil, err } switch operation { case mnepb.ApiOperation_API_OPERATION_UPDATE, mnepb.ApiOperation_API_OPERATION_REPLACE: createdNode, entry, err := ytypes.GetOrCreateNode(d.schema.RootSchema(), modelCopy, path) if err != nil { return nil, err } validatedCreatedNode, ok := createdNode.(ygot.ValidatedGoStruct) if !ok { return nil, &customerrs.InvalidTypeAssertionError{ Value: createdNode, Type: (*ygot.ValidatedGoStruct)(nil), } } if entry.IsDir() { opts := []ytypes.UnmarshalOpt{ // NOTE: I think we should not ignore extra fields if we want // to validate a specific change. The input for a valid change // should be correct to be valid. // //&ytypes.IgnoreExtraFields{} } if err := d.generatedUnmarshalFn(value, validatedCreatedNode, opts...); err != nil { return nil, err } } else if entry.IsLeaf() { typedValue, err := convertStringToGnmiTypedValue(string(value), entry.Type) if err != nil { return nil, err } opts := []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}, &ytypes.TolerateJSONInconsistencies{}} if err := ytypes.SetNode(d.schema.RootSchema(), validatedCreatedNode, path, typedValue, opts...); err != nil { return nil, err } } case mnepb.ApiOperation_API_OPERATION_DELETE: if err := ytypes.DeleteNode(d.schema.RootSchema(), modelCopy, path); err != nil { return nil, err } default: return nil, &customerrs.OperationNotSupportedError{Op: operation} } ygot.PruneEmptyBranches(modelCopy) return ygot.Marshal7951(modelCopy, getYgotMarshal7951Config(), ygot.JSONIndent("")) } func (d *DeviceModel) PruneConfigFalse(value []byte) ([]byte, error) { validatedCopy, err := createValidatedCopy(d.schema.Root) if err != nil { return nil, err } opts := []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}} if err := d.generatedUnmarshalFn(value, validatedCopy, opts...); err != nil { return nil, err } if err := ygot.PruneConfigFalse(d.schema.RootSchema(), validatedCopy); err != nil { return nil, err } return ygot.Marshal7951(validatedCopy, getYgotMarshal7951Config(), ygot.JSONIndent("")) } // convertStringToGnmiTypedValue allows to convert a string into a // gnmi.TypedValue; this conversion is based on the provided YANG type. func convertStringToGnmiTypedValue(s string, t *yang.YangType) (*gpb.TypedValue, error) { // TODO: add more types switch t.Kind { case yang.Yint8, yang.Yint16, yang.Yint32, yang.Yint64: return convertStringToIntTypedValue(s) case yang.Yuint8, yang.Yuint16, yang.Yuint32, yang.Yuint64: return convertStringToUintTypedValue(s) case yang.Ybool: parsedBool, err := strconv.ParseBool(s) if err != nil { return nil, err } return gnmiv.FromScalar(parsedBool) case yang.Ystring: return gnmiv.FromScalar(s) default: return nil, fmt.Errorf("could not convert to TypedValue, unsupported type of: %v", t) } } func convertStringToIntTypedValue(s string) (*gpb.TypedValue, error) { parsedInt, err := strconv.ParseInt(s, 10, 64) if err != nil { return nil, err } return &gpb.TypedValue{ Value: &gpb.TypedValue_IntVal{ IntVal: int64(parsedInt), }, }, nil } func convertStringToUintTypedValue(s string) (*gpb.TypedValue, error) { parsedInt, err := strconv.ParseUint(s, 10, 64) if err != nil { return nil, err } return &gpb.TypedValue{ Value: &gpb.TypedValue_UintVal{ UintVal: uint64(parsedInt), }, }, nil } // createValidatedCopy is a helper function which returns a validated // copy of the `Device` struct. func createValidatedCopy(toCopy ygot.GoStruct) (ygot.ValidatedGoStruct, error) { // create a deep copy of the schema's root schemaRootCopy, err := ygot.DeepCopy(toCopy) if err != nil { return nil, err } //ygot.BuildEmptyTree(schemaRootCopy) validatedCopy, ok := schemaRootCopy.(ygot.ValidatedGoStruct) if !ok { return nil, customerrs.InvalidTypeAssertionError{ Value: validatedCopy, Type: (*ygot.ValidatedGoStruct)(nil), } } return validatedCopy, nil } // NOTE: can be used with: return ygot.EmitJSON(d.model, getYgotEmitJSONConfig()) // Kept in case we do not want to use ygot.Marshal7951 //func getYgotEmitJSONConfig() *ygot.EmitJSONConfig { // return &ygot.EmitJSONConfig{ // Format: ygot.RFC7951, // Indent: "", // SkipValidation: true, // RFC7951Config: &ygot.RFC7951JSONConfig{ // AppendModuleName: true, // }} //} func getYgotMarshal7951Config() *ygot.RFC7951JSONConfig { return &ygot.RFC7951JSONConfig{ AppendModuleName: true, } }