Skip to content

Commit

Permalink
CVL Changes #7: 'leafref' evaluation (sonic-net#28)
Browse files Browse the repository at this point in the history
Adding support for evaluating 'leafref' expression based on customized xpath engine.
  • Loading branch information
dutta-partha authored Oct 20, 2020
1 parent 6f9535f commit dabf231
Show file tree
Hide file tree
Showing 2 changed files with 259 additions and 8 deletions.
43 changes: 40 additions & 3 deletions cvl/cvl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
224 changes: 219 additions & 5 deletions cvl/cvl_semantics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -201,16 +204,16 @@ 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
// container ACL_RULE { list ACL_RULE_LIST {} }
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"
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down

0 comments on commit dabf231

Please sign in to comment.