Skip to content

Commit 6f9535f

Browse files
authored
CVL Changes #6: Customized Xpath Engine integration (sonic-net#27)
Adding support for evaluating xpath expression (leafref, must, when expression) using customized open source xpath, xmlquery and jsonquery.
1 parent 5e2466b commit 6f9535f

File tree

7 files changed

+1452
-2
lines changed

7 files changed

+1452
-2
lines changed

cvl/cvl.go

+18
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
log "github.com/golang/glog"
2828
"github.com/go-redis/redis"
2929
"github.com/antchfx/xmlquery"
30+
"github.com/antchfx/xpath"
3031
"github.com/antchfx/jsonquery"
3132
"github.com/Azure/sonic-mgmt-common/cvl/internal/yparser"
3233
. "github.com/Azure/sonic-mgmt-common/cvl/internal/util"
@@ -177,6 +178,15 @@ func init() {
177178
SetTrace(true)
178179
}
179180

181+
xpath.SetKeyGetClbk(func(listName string) []string {
182+
if modelInfo.tableInfo[listName] != nil {
183+
return modelInfo.tableInfo[listName].keys
184+
}
185+
186+
return nil
187+
})
188+
189+
180190
ConfigFileSyncHandler()
181191

182192
cvlCfgMap := ReadConfFile()
@@ -214,6 +224,14 @@ func init() {
214224
}
215225

216226
dbCacheSet(false, "PORT", 0)
227+
228+
xpath.SetLogCallback(func(fmt string, args ...interface{}) {
229+
if !IsTraceLevelSet(TRACE_SEMANTIC) {
230+
return
231+
}
232+
233+
TRACE_LOG(INFO_API, TRACE_SEMANTIC, "XPATH: " + fmt, args...)
234+
})
217235
}
218236

219237
func Debug(on bool) {

cvl/cvl_semantics.go

+92
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package cvl
2222
import (
2323
"strings"
2424
"encoding/xml"
25+
"encoding/json"
2526
"github.com/antchfx/xmlquery"
2627
"github.com/antchfx/jsonquery"
2728
"github.com/Azure/sonic-mgmt-common/cvl/internal/yparser"
@@ -691,6 +692,97 @@ func (c *CVL) setOperation(op CVLOperation) {
691692
}
692693
}
693694

695+
//Add given YANG data buffer to Yang Validator
696+
//redisKeys - Set of redis keys
697+
//redisKeyFilter - Redis key filter in glob style pattern
698+
//keyNames - Names of all keys separated by "|"
699+
//predicate - Condition on keys/fields
700+
//fields - Fields to retrieve, separated by "|"
701+
//Return "," separated list of leaf nodes if only one leaf is requested
702+
//One leaf is used as xpath query result in other nested xpath
703+
func (c *CVL) addDepYangData(redisKeys []string, redisKeyFilter,
704+
keyNames, predicate, fields, count string) string {
705+
706+
var v interface{}
707+
tmpPredicate := ""
708+
709+
//Get filtered Redis data based on lua script
710+
//filter derived from Xpath predicate
711+
if (predicate != "") {
712+
tmpPredicate = "return (" + predicate + ")"
713+
}
714+
715+
cfgData, err := luaScripts["filter_entries"].Run(redisClient, []string{},
716+
redisKeyFilter, keyNames, tmpPredicate, fields, count).Result()
717+
718+
singleLeaf := "" //leaf data for single leaf
719+
720+
TRACE_LOG(INFO_API, TRACE_SEMANTIC, "addDepYangData() with redisKeyFilter=%s, " +
721+
"predicate=%s, fields=%s, returned cfgData = %s, err=%v",
722+
redisKeyFilter, predicate, fields, cfgData, err)
723+
724+
if (cfgData == nil) {
725+
return ""
726+
}
727+
728+
//Parse the JSON map received from lua script
729+
b := []byte(cfgData.(string))
730+
if err := json.Unmarshal(b, &v); err != nil {
731+
return ""
732+
}
733+
734+
var dataMap map[string]interface{} = v.(map[string]interface{})
735+
736+
dataTop, _ := jsonquery.ParseJsonMap(&dataMap)
737+
738+
for jsonNode := dataTop.FirstChild; jsonNode != nil; jsonNode=jsonNode.NextSibling {
739+
//Generate YANG data for Yang Validator from Redis JSON
740+
topYangNode, _ := c.generateYangListData(jsonNode, false)
741+
742+
if topYangNode == nil {
743+
continue
744+
}
745+
746+
if (topYangNode.FirstChild != nil) &&
747+
(topYangNode.FirstChild.FirstChild != nil) {
748+
//Add attribute mentioning that data is from db
749+
addAttrNode(topYangNode.FirstChild.FirstChild, "db", "")
750+
}
751+
752+
//Build single leaf data requested
753+
singleLeaf = ""
754+
for redisKey := topYangNode.FirstChild.FirstChild;
755+
redisKey != nil; redisKey = redisKey.NextSibling {
756+
757+
for field := redisKey.FirstChild; field != nil;
758+
field = field.NextSibling {
759+
if (field.Data == fields) {
760+
//Single field requested
761+
singleLeaf = singleLeaf + field.FirstChild.Data + ","
762+
break
763+
}
764+
}
765+
}
766+
767+
//Merge with main YANG data cache
768+
doc := &xmlquery.Node{Type: xmlquery.DocumentNode}
769+
doc.FirstChild = topYangNode
770+
doc.LastChild = topYangNode
771+
topYangNode.Parent = doc
772+
if c.mergeYangData(c.yv.root, doc) != CVL_SUCCESS {
773+
continue
774+
}
775+
}
776+
777+
778+
//remove last comma in case mulitple values returned
779+
if (singleLeaf != "") {
780+
return singleLeaf[:len(singleLeaf) - 1]
781+
}
782+
783+
return ""
784+
}
785+
694786
//Check delete constraint for leafref if key/field is deleted
695787
func (c *CVL) checkDeleteConstraint(cfgData []CVLEditConfigData,
696788
tableName, keyVal, field string) CVLRetCode {

cvl/internal/util/util.go

+4
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ func IsTraceSet() bool {
133133
}
134134
}
135135

136+
func IsTraceLevelSet(tracelevel CVLTraceLevel) bool {
137+
return (cvlTraceFlags & (uint32)(tracelevel)) != 0
138+
}
139+
136140
func TRACE_LEVEL_LOG(level log.Level, tracelevel CVLTraceLevel, fmtStr string, args ...interface{}) {
137141

138142
if (IsTraceSet() == false) {

patches/apply.sh

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ patch -d ${DEST_DIR}/github.com/openconfig -p1 < ${PATCH_DIR}/ygot/ygot.patch
4343
patch -d ${DEST_DIR}/github.com/openconfig/goyang -p1 < ${PATCH_DIR}/goyang/goyang.patch
4444

4545
patch -d ${DEST_DIR}/github.com/antchfx/jsonquery -p1 < ${PATCH_DIR}/jsonquery.patch
46+
patch -d ${DEST_DIR}/github.com/antchfx/xmlquery -p1 < ${PATCH_DIR}/xmlquery.patch
47+
patch -d ${DEST_DIR}/github.com/antchfx/xpath -p1 < ${PATCH_DIR}/xpath.patch
4648

4749
patch -d ${DEST_DIR}/github.com/golang/glog -p1 < ${PATCH_DIR}/glog.patch
4850

patches/jsonquery.patch

+83-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,70 @@
11
diff --git a/node.go b/node.go
2-
index 76032bb..db73a1e 100644
2+
index 76032bb..f6103d9 100644
33
--- a/node.go
44
+++ b/node.go
5-
@@ -155,3 +155,9 @@ func Parse(r io.Reader) (*Node, error) {
5+
@@ -8,6 +8,7 @@ import (
6+
"net/http"
7+
"sort"
8+
"strconv"
9+
+ "strings"
10+
)
11+
12+
// A NodeType is the type of a Node.
13+
@@ -110,6 +111,29 @@ func parseValue(x interface{}, top *Node, level int) {
14+
addNode(n)
15+
parseValue(vv, n, level+1)
16+
}
17+
+ case map[string]string:
18+
+ var keys []string
19+
+ for key := range v {
20+
+ keys = append(keys, key)
21+
+ }
22+
+ sort.Strings(keys)
23+
+ for _, key := range keys {
24+
+ tmpKey := key
25+
+ var tmpVal interface{}
26+
+ tmpVal = v[key]
27+
+ if (strings.HasSuffix(key, "@")) {
28+
+ tmpKey = key[:len(key) - 1]
29+
+ tmpValArr := []interface{}{}
30+
+ for _, val := range strings.Split(v[key], ",") {
31+
+ tmpValArr = append(tmpValArr, val)
32+
+ }
33+
+ tmpVal = tmpValArr
34+
+ }
35+
+
36+
+ n := &Node{Data: tmpKey, Type: ElementNode, level: level}
37+
+ addNode(n)
38+
+ parseValue(tmpVal, n, level+1)
39+
+ }
40+
case map[string]interface{}:
41+
// The Go’s map iteration order is random.
42+
// (https://blog.golang.org/go-maps-in-action#Iteration-order)
43+
@@ -119,9 +143,21 @@ func parseValue(x interface{}, top *Node, level int) {
44+
}
45+
sort.Strings(keys)
46+
for _, key := range keys {
47+
- n := &Node{Data: key, Type: ElementNode, level: level}
48+
+ tmpKey := key
49+
+ var tmpVal interface{}
50+
+ tmpVal = v[key]
51+
+ if (strings.HasSuffix(key, "@")) {
52+
+ tmpKey = key[:len(key) - 1]
53+
+ tmpValArr := []interface{}{}
54+
+ for _, val := range strings.Split(v[key].(string), ",") {
55+
+ tmpValArr = append(tmpValArr, val)
56+
+ }
57+
+ tmpVal = tmpValArr
58+
+ }
59+
+
60+
+ n := &Node{Data: tmpKey, Type: ElementNode, level: level}
61+
addNode(n)
62+
- parseValue(v[key], n, level+1)
63+
+ parseValue(tmpVal, n, level+1)
64+
}
65+
case string:
66+
n := &Node{Data: v, Type: TextNode, level: level}
67+
@@ -155,3 +191,9 @@ func Parse(r io.Reader) (*Node, error) {
668
}
769
return parse(b)
870
}
@@ -12,3 +74,22 @@ index 76032bb..db73a1e 100644
1274
+ parseValue(*jsonMap, doc, 1)
1375
+ return doc, nil
1476
+}
77+
diff --git a/query.go b/query.go
78+
index d105962..e8db1d6 100644
79+
--- a/query.go
80+
+++ b/query.go
81+
@@ -120,6 +120,14 @@ func (a *NodeNavigator) MoveToRoot() {
82+
a.cur = a.root
83+
}
84+
85+
+func (a *NodeNavigator) MoveToContext() {
86+
+ return
87+
+}
88+
+
89+
+func (a *NodeNavigator) CurrentPrefix() string {
90+
+ return ""
91+
+}
92+
+
93+
func (a *NodeNavigator) MoveToParent() bool {
94+
if n := a.cur.Parent; n != nil {
95+
a.cur = n

patches/xmlquery.patch

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
diff --git a/node.go b/node.go
2+
index e86c0c3..028867c 100644
3+
--- a/node.go
4+
+++ b/node.go
5+
@@ -48,7 +48,7 @@ type Node struct {
6+
7+
// InnerText returns the text between the start and end tags of the object.
8+
func (n *Node) InnerText() string {
9+
- var output func(*bytes.Buffer, *Node)
10+
+ /*var output func(*bytes.Buffer, *Node)
11+
output = func(buf *bytes.Buffer, n *Node) {
12+
switch n.Type {
13+
case TextNode:
14+
@@ -64,7 +64,18 @@ func (n *Node) InnerText() string {
15+
16+
var buf bytes.Buffer
17+
output(&buf, n)
18+
- return buf.String()
19+
+ return buf.String()*/
20+
+
21+
+ if (n.Type == TextNode) {
22+
+ return n.Data
23+
+ } else if (n.Type == ElementNode) &&
24+
+ (n.FirstChild != nil) &&
25+
+ (n.FirstChild.Type == TextNode) {
26+
+ return n.FirstChild.Data
27+
+ }
28+
+
29+
+
30+
+ return ""
31+
}
32+
33+
func (n *Node) sanitizedData(preserveSpaces bool) string {
34+
diff --git a/query.go b/query.go
35+
index 146c2a4..f21b61b 100644
36+
--- a/query.go
37+
+++ b/query.go
38+
@@ -49,6 +49,29 @@ func CreateXPathNavigator(top *Node) *NodeNavigator {
39+
return &NodeNavigator{curr: top, root: top, attr: -1}
40+
}
41+
42+
+//Evaluate XPath expression, the expression should evaluate to true or false
43+
+func Eval(top, ctx *Node, exp *xpath.Expr) bool {
44+
+ if exp == nil {
45+
+ return false
46+
+ }
47+
+
48+
+ v := exp.Evaluate(&NodeNavigator{curr: ctx, ctxt: ctx, root: top, attr: -1})
49+
+
50+
+ switch val := v.(type) {
51+
+ case bool:
52+
+ return val
53+
+ case string:
54+
+ return (val != "")
55+
+ case float64:
56+
+ return (val != 0)
57+
+ case *xpath.NodeIterator:
58+
+ return (val != nil)
59+
+ }
60+
+
61+
+ //return v.(bool)
62+
+ return false
63+
+}
64+
+
65+
func getCurrentNode(it *xpath.NodeIterator) *Node {
66+
n := it.Current().(*NodeNavigator)
67+
if n.NodeType() == xpath.AttributeNode {
68+
@@ -145,7 +168,7 @@ func FindEachWithBreak(top *Node, expr string, cb func(int, *Node) bool) {
69+
}
70+
71+
type NodeNavigator struct {
72+
- root, curr *Node
73+
+ root, curr, ctxt *Node
74+
attr int
75+
}
76+
77+
@@ -212,6 +235,17 @@ func (x *NodeNavigator) MoveToRoot() {
78+
x.curr = x.root
79+
}
80+
81+
+func (x *NodeNavigator) MoveToContext() {
82+
+ x.curr = x.ctxt
83+
+}
84+
+
85+
+func (x *NodeNavigator) CurrentPrefix() string {
86+
+ if (x.ctxt != nil) {
87+
+ return x.ctxt.Prefix
88+
+ }
89+
+ return ""
90+
+}
91+
+
92+
func (x *NodeNavigator) MoveToParent() bool {
93+
if x.attr != -1 {
94+
x.attr = -1

0 commit comments

Comments
 (0)