diff --git a/util/gnmi.go b/util/gnmi.go index abde7619b..546fcfea3 100644 --- a/util/gnmi.go +++ b/util/gnmi.go @@ -43,6 +43,7 @@ func PathMatchesPrefix(path *gpb.Path, prefix []string) bool { } // PathElemsEqual replaces the proto.Equal() check for PathElems. +// If a.Key["foo"] == "*" and b.Key["foo"] == "bar" func returns false. // This significantly improves comparison speed. func PathElemsEqual(a, b *gpb.PathElem) bool { // This check allows avoiding to deal with any null PathElems later on. @@ -53,12 +54,13 @@ func PathElemsEqual(a, b *gpb.PathElem) bool { if a.Name != b.Name { return false } + if len(a.Key) != len(b.Key) { return false } for k, v := range a.Key { - if vo, ok := b.Key[k]; !ok || vo != v { + if vo, ok := b.Key[k]; !ok || v != vo { return false } } @@ -80,6 +82,9 @@ func PathElemSlicesEqual(a, b []*gpb.PathElem) bool { // PathMatchesPathElemPrefix checks whether prefix is a prefix of path. Both paths // must use the gNMI >=0.4.0 PathElem path format. +// Note: Paths must match exactly, that is if path has a wildcard key, +// then the same key must also be a wildcard in the prefix. +// See PathMatchesQuery for comparing paths with wildcards. func PathMatchesPathElemPrefix(path, prefix *gpb.Path) bool { if len(path.GetElem()) < len(prefix.GetElem()) || path.Origin != prefix.Origin { return false @@ -92,6 +97,32 @@ func PathMatchesPathElemPrefix(path, prefix *gpb.Path) bool { return true } +// PathMatchesQuery returns whether query is prefix of path. +// Only the query may contain wildcard name or keys. +// TODO: Multilevel wildcards ("...") not supported. +// If either path and query contain nil elements func returns false. +// Both paths must use the gNMI >=0.4.0 PathElem path format. +func PathMatchesQuery(path, query *gpb.Path) bool { + if len(path.GetElem()) < len(query.GetElem()) || path.Origin != query.Origin { + return false + } + for i, queryElem := range query.Elem { + pathElem := path.Elem[i] + if queryElem == nil || pathElem == nil { + return false + } + if queryElem.Name != "*" && queryElem.Name != pathElem.Name { + return false + } + for qk, qv := range queryElem.Key { + if pv, ok := pathElem.Key[qk]; !ok || (qv != "*" && qv != pv) { + return false + } + } + } + return true +} + // TrimGNMIPathPrefix returns path with the prefix trimmed. It returns the // original path if the prefix does not fully match. func TrimGNMIPathPrefix(path *gpb.Path, prefix []string) *gpb.Path { diff --git a/util/gnmi_test.go b/util/gnmi_test.go index 871c9b4e6..9df882a0a 100644 --- a/util/gnmi_test.go +++ b/util/gnmi_test.go @@ -526,6 +526,200 @@ func TestPathMatchesPathElemPrefix(t *testing.T) { } } +func TestPathMatchesQuery(t *testing.T) { + tests := []struct { + desc string + inPath *gpb.Path + inQuery *gpb.Path + want bool + }{{ + desc: "valid query with no keys", + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + }, { + Name: "two", + }}, + }, + inQuery: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + }}, + }, + want: true, + }, { + desc: "valid query with wildcard name", + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + }, { + Name: "two", + }}, + }, + inQuery: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "*", + }, { + Name: "two", + }}, + }, + want: true, + }, { + desc: "valid query with exact key match", + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + Key: map[string]string{"two": "three"}, + }, { + Name: "four", + }}, + }, + inQuery: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + Key: map[string]string{"two": "three"}, + }}, + }, + want: true, + }, { + desc: "valid query with wildcard keys", + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + Key: map[string]string{"two": "three"}, + }, { + Name: "four", + }}, + }, + inQuery: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + Key: map[string]string{"two": "*"}, + }}, + }, + want: true, + }, { + desc: "valid query with no keys and path with keys", + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + Key: map[string]string{"two": "three"}, + }, { + Name: "four", + }}, + }, + inQuery: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + }}, + }, + want: true, + }, { + desc: "valid query with both missing and wildcard keys", + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + Key: map[string]string{ + "two": "three", + "four": "five", + }, + }, { + Name: "four", + }}, + }, + inQuery: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "one", + Key: map[string]string{"four": "*"}, + }}, + }, + want: true, + }, { + desc: "invalid nil elements", + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{ + nil, + { + Name: "twelve", + }}, + }, + inQuery: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "three", + }}, + }, + }, { + desc: "invalid names not equal", + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "fourteen", + }, { + Name: "twelve", + }}, + }, + inQuery: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "three", + }}, + }, + }, { + desc: "invalid origin", + inPath: &gpb.Path{ + Origin: "openconfig", + Elem: []*gpb.PathElem{{ + Name: "one", + }, { + Name: "two", + }}, + }, + inQuery: &gpb.Path{ + Origin: "google", + Elem: []*gpb.PathElem{{ + Name: "one", + }}, + }, + }, { + desc: "invalid keys", + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "three", + Key: map[string]string{"four": "five"}, + }, { + Name: "six", + }}, + }, + inQuery: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "three", + Key: map[string]string{"seven": "eight"}, + }}, + }, + }, { + desc: "invalid missing wildcard keys", + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "three", + Key: map[string]string{"four": "five"}, + }, { + Name: "six", + }}, + }, + inQuery: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "three", + Key: map[string]string{"seven": "*"}, + }}, + }, + }} + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + if got := PathMatchesQuery(tt.inPath, tt.inQuery); got != tt.want { + t.Fatalf("did not get expected result, got: %v, want: %v", got, tt.want) + } + }) + } +} + func TestTrimGNMIPathElemPrefix(t *testing.T) { tests := []struct { desc string