From dabf2310cf441af6e2edfdbc62ab221f1caf658f Mon Sep 17 00:00:00 2001 From: Partha Dutta <51353699+dutta-partha@users.noreply.github.com> Date: Tue, 20 Oct 2020 21:12:14 +0530 Subject: [PATCH] CVL Changes #7: 'leafref' evaluation (#28) Adding support for evaluating 'leafref' expression based on customized xpath engine. --- cvl/cvl.go | 43 ++++++++- cvl/cvl_semantics.go | 224 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 259 insertions(+), 8 deletions(-) diff --git a/cvl/cvl.go b/cvl/cvl.go index 30d1ee18534d..56697ac1a06f 100644 --- a/cvl/cvl.go +++ b/cvl/cvl.go @@ -65,12 +65,11 @@ var luaScripts map[string]*redis.Script type leafRefInfo struct { path string //leafref path + exprTree *xpath.Expr //compiled expression tree yangListNames []string //all yang list in path targetNodeName string //target node name } -//var tmpDbCache map[string]interface{} //map of table storing map of key-value pair - //m["PORT_TABLE] = {"key" : {"f1": "v1"}} //Important schema information to be loaded at bootup time type modelTableInfo struct { dbNum uint8 @@ -262,6 +261,43 @@ func getNodeName(node *xmlquery.Node) string { return node.Data } +//Get list of YANG list names used in xpath expression +func getYangListNamesInExpr(expr string) []string { + tbl := []string{} + + //Check with all table names + for tblName := range modelInfo.tableInfo { + + //Match 1 - Prefix is used in path + //Match 2 - Prefix is not used in path, it is in same YANG model + if strings.Contains(expr, ":" + tblName + "_LIST") || strings.Contains(expr, "/" + tblName + "_LIST") { + tbl = append(tbl, tblName) + } + } + + return tbl +} + +//Get all YANG lists referred and the target node for leafref +//Ex: leafref { path "../../../ACL_TABLE/ACL_TABLE_LIST[aclname=current()]/aclname";} +//will return [ACL_TABLE] and aclname +func getLeafRefTargetInfo(path string) ([]string, string) { + target := "" + + //Get list of all YANG list used in the path + tbl := getYangListNamesInExpr(path) + + //Get the target node name from end of the path + idx := strings.LastIndex(path, ":") //check with prefix first + if idx > 0 { + target = path[idx+1:] + } else if idx = strings.LastIndex(path, "/"); idx > 0{ //no prefix there + target = path[idx+1:] + } + + return tbl, target +} + //Store useful schema data during initialization func storeModelInfo(modelFile string, module *yparser.YParserModule) { //such model info can be maintained in C code and fetched from there f, err := os.Open(CVL_SCHEMA + modelFile) @@ -384,7 +420,8 @@ func storeModelInfo(modelFile string, module *yparser.YParserModule) { //such mo continue } - tableInfo.leafRef = make(map[string][]*leafRefInfo) + tableInfo.leafRef = make(map[string][]*leafRefInfo) + for _, leafRefNode := range leafRefNodes { if (leafRefNode.Parent == nil || leafRefNode.FirstChild == nil) { continue diff --git a/cvl/cvl_semantics.go b/cvl/cvl_semantics.go index 345be7f75278..30176057cbed 100644 --- a/cvl/cvl_semantics.go +++ b/cvl/cvl_semantics.go @@ -20,9 +20,12 @@ package cvl import ( - "strings" + "fmt" "encoding/xml" "encoding/json" + "strings" + "regexp" + "github.com/antchfx/xpath" "github.com/antchfx/xmlquery" "github.com/antchfx/jsonquery" "github.com/Azure/sonic-mgmt-common/cvl/internal/yparser" @@ -201,8 +204,8 @@ func (c *CVL) generateYangListData(jsonNode *jsonquery.Node, var cvlErrObj CVLErrorInfo tableName := jsonNode.Data - //c.batchLeaf = nil - //c.batchLeaf = make([]*yparser.YParserLeafValue, 0) + c.batchLeaf = nil + c.batchLeaf = make([]*yparser.YParserLeafValue, 0) //Every Redis table is mapped as list within a container, //E.g. ACL_RULE is mapped as @@ -210,7 +213,7 @@ func (c *CVL) generateYangListData(jsonNode *jsonquery.Node, var topNode *xmlquery.Node if _, exists := modelInfo.tableInfo[tableName]; !exists { - CVL_LOG(ERROR, "Failed to find schema details for table %s", tableName) + CVL_LOG(WARNING, "Failed to find schema details for table %s", tableName) cvlErrObj.ErrCode = CVL_SYNTAX_ERROR cvlErrObj.TableName = tableName cvlErrObj.Msg ="Schema details not found" @@ -756,7 +759,7 @@ func (c *CVL) addDepYangData(redisKeys []string, redisKeyFilter, for field := redisKey.FirstChild; field != nil; field = field.NextSibling { - if (field.Data == fields) { + if (field.Data == fields && field.FirstChild != nil) { //Single field requested singleLeaf = singleLeaf + field.FirstChild.Data + "," break @@ -783,6 +786,217 @@ func (c *CVL) addDepYangData(redisKeys []string, redisKeyFilter, return "" } +func compileLeafRefPath() { + reMultiPred := regexp.MustCompile(`\][ ]*\[`) + + for _, tInfo := range modelInfo.tableInfo { + if (len(tInfo.leafRef) == 0) { //no leafref + continue + } + + //for nodeName, leafRefArr := range tInfo.leafRef { + for _, leafRefArr := range tInfo.leafRef { + for _, leafRefArrItem := range leafRefArr { + if (leafRefArrItem.path == "non-leafref") { + //Leaf type has at-least one non-learef data type + continue + } + + //first store the referred table and target node + leafRefArrItem.yangListNames, leafRefArrItem.targetNodeName = + getLeafRefTargetInfo(leafRefArrItem.path) + //check if predicate is used in path + //for complex expression, xpath engine is + //used for evaluation, + //else don't build expression tree, + //it is handled by just checking redis entry + if strings.Contains(leafRefArrItem.path, "[") && + strings.Contains(leafRefArrItem.path, "]") { + //Compile the xpath in leafref + tmpExp := reMultiPred.ReplaceAllString(leafRefArrItem.path, " and ") + //tmpExp = nodeName + " = " + tmpExp + tmpExp = "current() = " + tmpExp + leafRefArrItem.exprTree = xpath.MustCompile(tmpExp) + } + } + } + } +} + +//Validate leafref +//Convert leafref to must expression +//type leafref { path "../../../ACL_TABLE/ACL_TABLE_LIST/aclname";} converts to +// "current() = ../../../ACL_TABLE/ACL_TABLE_LIST[aclname=current()]/aclname" +func (c *CVL) validateLeafRef(node *xmlquery.Node, + tableName, key string, op CVLOperation) (r CVLErrorInfo) { + defer func() { + ret := &r + CVL_LOG(INFO_API, "validateLeafRef(): table name = %s, " + + "return value = %v", tableName, *ret) + }() + + if (op == OP_DELETE) { + //No new node getting added so skip leafref validation + return CVLErrorInfo{ErrCode:CVL_SUCCESS} + } + + //Set xpath callback for retreiving dependent data + xpath.SetDepDataClbk(c, func(ctxt interface{}, redisKeys []string, + redisKeyFilter, keyNames, pred, fields, count string) string { + c := ctxt.(*CVL) + TRACE_LOG(INFO_API, TRACE_SEMANTIC, "validateLeafRef(): calling addDepYangData()") + return c.addDepYangData(redisKeys, redisKeyFilter, keyNames, pred, fields, "") + }) + + listNode := node + if (listNode == nil || listNode.FirstChild == nil) { + return CVLErrorInfo{ + TableName: tableName, + Keys: strings.Split(key, modelInfo.tableInfo[tableName].redisKeyDelim), + ErrCode: CVL_SEMANTIC_ERROR, + CVLErrDetails: cvlErrorMap[CVL_SEMANTIC_ERROR], + Msg: "Failed to find YANG data for leafref expression validation", + } + } + + tblInfo := modelInfo.tableInfo[tableName] + + for nodeName, leafRefs := range tblInfo.leafRef { //for each leafref node + + //Reach to the node where leafref is present + ctxNode := listNode.FirstChild + for ;(ctxNode !=nil) && (ctxNode.Data != nodeName); + ctxNode = ctxNode.NextSibling { + } + + if (ctxNode == nil) { + //No leafref instance present, proceed to next leafref + continue + } + + //Check leafref for each leaf-list node + for ;(ctxNode != nil) && (ctxNode.Data == nodeName); + ctxNode = ctxNode.NextSibling { + //Load first data for each referred table. + //c.yv.root has all requested data merged and any depdendent + //data needed for leafref validation should be available from this. + + leafRefSuccess := false + nonLeafRefPresent := false //If leaf has non-leafref data type due to union + nodeValMatchedWithLeafref := false + + ctxtVal := "" + //Get the leaf value + if (ctxNode.FirstChild != nil) { + ctxtVal = ctxNode.FirstChild.Data + } + + //Excute all leafref checks, multiple leafref for unions + leafRefLoop: + for _, leafRefPath := range leafRefs { + if (leafRefPath.path == "non-leafref") { + //Leaf has at-least one non-leaferf data type in union + nonLeafRefPresent = true + continue + } + + //Add dependent data for all referred tables + for _, refListName := range leafRefPath.yangListNames { + refRedisTableName := getYangListToRedisTbl(refListName) + + filter := "" + var err error + var tableKeys []string + if (leafRefPath.exprTree == nil) { //no predicate, single key case + //Context node used for leafref + //Keys -> ACL_TABLE|TestACL1 + filter = refRedisTableName + + modelInfo.tableInfo[refListName].redisKeyDelim + ctxtVal + tableKeys, err = redisClient.Keys(filter).Result() + } else { + //Keys -> ACL_TABLE|* + filter = refRedisTableName + + modelInfo.tableInfo[refListName].redisKeyDelim + "*" + //tableKeys, _, err = redisClient.Scan(0, filter, 1).Result() + tableKeys, err = redisClient.Keys(filter).Result() + } + + if (err != nil) || (len(tableKeys) == 0) { + //There must be at least one entry in the ref table + TRACE_LOG(INFO_API, TRACE_SEMANTIC, "Leafref dependent data " + + "table %s, key %s not found in Redis", refRedisTableName, + ctxtVal) + + if (leafRefPath.exprTree == nil) { + //Check the key in request cache also + if _, exists := c.requestCache[refRedisTableName][ctxtVal]; exists { + //no predicate and single key is referred + leafRefSuccess = true + break leafRefLoop + } else if node := c.findYangList(refListName, ctxtVal); + node != nil { + //Found in the request tree + leafRefSuccess = true + break leafRefLoop + } + } + continue + } else { + if (leafRefPath.exprTree == nil) { + //no predicate and single key is referred + leafRefSuccess = true + break leafRefLoop + } + } + + //Now add the first data + c.addDepYangData([]string{}, tableKeys[0], + strings.Join(modelInfo.tableInfo[refListName].keys, "|"), + "true", "", "") + } + + //Excute xpath expression for complex leafref path + if xmlquery.Eval(c.yv.root, ctxNode, leafRefPath.exprTree) { + leafRefSuccess = true + break leafRefLoop + } + } //for loop for all leafref check for a leaf - union case + + if !leafRefSuccess && nonLeafRefPresent && (len(leafRefs) > 1) { + //If union has mixed type with base and leafref type, + //check if node value matched with any leafref. + //If so non-existence of leafref in DB will be treated as failure. + if (ctxtVal != "") { + nodeValMatchedWithLeafref = c.yp.IsLeafrefMatchedInUnion(tblInfo.module, + fmt.Sprintf("/%s:%s/%s/%s_LIST/%s", tblInfo.modelName, + tblInfo.modelName, tblInfo.redisTableName, + tableName, nodeName), + ctxtVal) + } + } + + if !leafRefSuccess && (!nonLeafRefPresent || nodeValMatchedWithLeafref) { + //Return failure if none of the leafref exists + return CVLErrorInfo{ + TableName: tableName, + Keys: strings.Split(key, + modelInfo.tableInfo[tableName].redisKeyDelim), + ErrCode: CVL_SEMANTIC_DEPENDENT_DATA_MISSING, + CVLErrDetails: cvlErrorMap[CVL_SEMANTIC_DEPENDENT_DATA_MISSING], + ErrAppTag: "instance-required", + ConstraintErrMsg: "No instance found for '" + ctxtVal + "'", + } + } else if !leafRefSuccess { + TRACE_LOG(INFO_API, TRACE_SEMANTIC, "validateLeafRef(): " + + "Leafref dependent data not found but leaf has " + + "other data type in union, returning success.") + } + } //for each leaf-list node + } + + return CVLErrorInfo{ErrCode:CVL_SUCCESS} +} + //Check delete constraint for leafref if key/field is deleted func (c *CVL) checkDeleteConstraint(cfgData []CVLEditConfigData, tableName, keyVal, field string) CVLRetCode {