Skip to content
Snippets Groups Projects
deviceModel.go 10.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • 	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)
    
    		validatedCreatedNode, ok := createdNode.(ygot.ValidatedGoStruct)
    		if !ok {
    			return nil, &customerrs.InvalidTypeAssertionError{
    				Value: createdNode,
    				Type:  (*ygot.ValidatedGoStruct)(nil),
    			}
    		}
    
    
    			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 {
    
    	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,
    	}
    }