Skip to content

Commit cbb7631

Browse files
authored
Debug grpc to fetch subscribe preferences of a path (#130)
Added a new 'GetSubscribePreferences' grpc which accepts a list of yang paths and returns following subscription preferences for each: - OnChangeSupported (if on_change subscription will be accepted) - TargetDefinedMode (mode to which target_defined will map to) - WildcardSupported (if wildcard keys will be accepted) - MinSampleInterval (minimum sample_interval accepted, in nanos) Accepts only translib managed yang paths. Uses the existing translib API IsSubscribeSupported() to learn the preferences. This rpc is defined in a new protobuf sonic_debug.proto. It defines only one rpc 'GetSubscribePreferences' now. More debuggability actions can be added in future.
1 parent 099ff7c commit cbb7631

File tree

7 files changed

+731
-10
lines changed

7 files changed

+731
-10
lines changed

Makefile

+18
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,24 @@ endif
9191
swsscommon_wrap:
9292
make -C swsscommon
9393

94+
.SECONDEXPANSION:
95+
96+
PROTOC_PATH := $(PATH):$(GOBIN)
97+
PROTOC_OPTS := -I$(CURDIR)/vendor -I/usr/local/include -I/usr/include
98+
99+
# Generate following go & grpc bindings using teh legacy protoc-gen-go
100+
PROTO_GO_BINDINGS += proto/sonic_internal.pb.go
101+
PROTO_GO_BINDINGS += proto/gnoi/sonic_debug.pb.go
102+
103+
$(PROTO_GO_BINDINGS): $$(patsubst %.pb.go,%.proto,$$@) | $(GOBIN)/protoc-gen-go
104+
PATH=$(PROTOC_PATH) protoc -I$(@D) $(PROTOC_OPTS) --go_out=plugins=grpc:$(@D) $<
105+
106+
$(GOBIN)/protoc-gen-go:
107+
cd $$(mktemp -d) && \
108+
$(GO) mod init protoc && \
109+
$(GO) install github.com/golang/protobuf/protoc-gen-go
110+
111+
94112
DBCONFG = $(DBDIR)/database_config.json
95113
ENVFILE = build/test/env.txt
96114
TESTENV = $(shell cat $(ENVFILE))

gnmi_server/debug.go

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
// //
3+
// Copyright 2023 Broadcom. The term Broadcom refers to Broadcom Inc. and/or //
4+
// its subsidiaries. //
5+
// //
6+
// Licensed under the Apache License, Version 2.0 (the "License"); //
7+
// you may not use this file except in compliance with the License. //
8+
// You may obtain a copy of the License at //
9+
// //
10+
// http://www.apache.org/licenses/LICENSE-2.0 //
11+
// //
12+
// Unless required by applicable law or agreed to in writing, software //
13+
// distributed under the License is distributed on an "AS IS" BASIS, //
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
15+
// See the License for the specific language governing permissions and //
16+
// limitations under the License. //
17+
// //
18+
////////////////////////////////////////////////////////////////////////////////
19+
20+
package gnmi
21+
22+
import (
23+
"sort"
24+
"time"
25+
26+
"github.com/Azure/sonic-mgmt-common/translib"
27+
"github.com/Azure/sonic-mgmt-common/translib/path"
28+
gnmipb "github.com/openconfig/gnmi/proto/gnmi"
29+
"github.com/sonic-net/sonic-gnmi/common_utils"
30+
spb_gnoi "github.com/sonic-net/sonic-gnmi/proto/gnoi"
31+
"github.com/sonic-net/sonic-gnmi/transl_utils"
32+
"google.golang.org/grpc/codes"
33+
"google.golang.org/grpc/status"
34+
)
35+
36+
func (srv *Server) GetSubscribePreferences(req *spb_gnoi.SubscribePreferencesReq, stream spb_gnoi.Debug_GetSubscribePreferencesServer) error {
37+
ctx := stream.Context()
38+
ctx, err := authenticate(srv.config.UserAuth, ctx)
39+
if err != nil {
40+
return err
41+
}
42+
43+
translPaths := make([]translib.IsSubscribePath, 0, len(req.GetPath()))
44+
for i, p := range req.GetPath() {
45+
reqPath, err := transl_utils.ConvertToURI(nil, p,
46+
&path.AppendModulePrefix{}, &path.AddWildcardKeys{})
47+
if err != nil {
48+
return status.Error(codes.InvalidArgument, "Unknown path: "+path.String(p))
49+
}
50+
51+
translPaths = append(translPaths, translib.IsSubscribePath{
52+
ID: uint32(i),
53+
Path: reqPath,
54+
Mode: translib.TargetDefined,
55+
})
56+
}
57+
58+
rc, _ := common_utils.GetContext(ctx)
59+
trResp, err := translib.IsSubscribeSupported(translib.IsSubscribeRequest{
60+
Paths: translPaths,
61+
User: translib.UserRoles{Name: rc.Auth.User, Roles: rc.Auth.Roles},
62+
})
63+
if err != nil {
64+
return status.Error(codes.InvalidArgument, err.Error())
65+
}
66+
67+
// When the path supports on_change but some of its subpaths do not, extra entries
68+
// gets appended with same request ID. Group such entries by ID.
69+
sort.Slice(trResp, func(i, j int) bool {
70+
return trResp[i].ID < trResp[j].ID ||
71+
(trResp[i].ID == trResp[j].ID && trResp[i].Path < trResp[j].Path)
72+
})
73+
74+
for _, r := range trResp {
75+
pathStr, err := path.New(r.Path)
76+
if err != nil {
77+
return status.Error(codes.Internal, err.Error())
78+
}
79+
pref := &spb_gnoi.SubscribePreference{
80+
Path: pathStr,
81+
WildcardSupported: r.IsWildcardSupported,
82+
OnChangeSupported: r.IsOnChangeSupported,
83+
TargetDefinedMode: gnmipb.SubscriptionMode_SAMPLE,
84+
MinSampleInterval: uint64(r.MinInterval) * uint64(time.Second),
85+
}
86+
if !r.IsSubPath && hasOnChangeDisabledSubpath(r.ID, trResp) {
87+
pref.OnChangeSupported = false
88+
}
89+
if r.IsOnChangeSupported && r.PreferredType == translib.OnChange {
90+
pref.TargetDefinedMode = gnmipb.SubscriptionMode_ON_CHANGE
91+
}
92+
93+
if err = stream.Send(pref); err != nil {
94+
return err
95+
}
96+
}
97+
98+
return nil
99+
}
100+
101+
func hasOnChangeDisabledSubpath(id uint32, allPrefs []*translib.IsSubscribeResponse) bool {
102+
for _, p := range allPrefs {
103+
if p.ID == id && p.IsSubPath && !p.IsOnChangeSupported {
104+
return true
105+
}
106+
}
107+
return false
108+
}

gnmi_server/server.go

+1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ func NewServer(config *Config, opts []grpc.ServerOption) (*Server, error) {
163163
if srv.config.EnableTranslibWrite {
164164
spb_gnoi.RegisterSonicServiceServer(srv.s, srv)
165165
}
166+
spb_gnoi.RegisterDebugServer(srv.s, srv)
166167
log.V(1).Infof("Created Server on %s, read-only: %t", srv.Address(), !srv.config.EnableTranslibWrite)
167168
return srv, nil
168169
}

gnmi_server/server_test.go

+14
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,20 @@ func loadDBNotStrict(t *testing.T, rclient *redis.Client, mpi map[string]interfa
9999
}
100100
}
101101

102+
func createClient(t *testing.T, port int) *grpc.ClientConn {
103+
t.Helper()
104+
cred := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
105+
conn, err := grpc.Dial(
106+
fmt.Sprintf("127.0.0.1:%d", port),
107+
grpc.WithTransportCredentials(cred),
108+
)
109+
if err != nil {
110+
t.Fatalf("Dialing to :%d failed: %v", port, err)
111+
}
112+
t.Cleanup(func() { conn.Close() })
113+
return conn
114+
}
115+
102116
func createServer(t *testing.T, port int64) *Server {
103117
t.Helper()
104118
certificate, err := testcert.NewCert()

gnmi_server/transl_sub_test.go

+156-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gnmi
33
import (
44
"crypto/tls"
55
"fmt"
6+
"io"
67
"path/filepath"
78
"reflect"
89
"strings"
@@ -18,11 +19,10 @@ import (
1819
extnpb "github.com/openconfig/gnmi/proto/gnmi_ext"
1920
"github.com/openconfig/ygot/ygot"
2021
spb "github.com/sonic-net/sonic-gnmi/proto"
22+
spb_gnoi "github.com/sonic-net/sonic-gnmi/proto/gnoi"
2123
dbconfig "github.com/sonic-net/sonic-gnmi/sonic_db_config"
2224
"golang.org/x/net/context"
23-
"google.golang.org/grpc"
2425
"google.golang.org/grpc/codes"
25-
"google.golang.org/grpc/credentials"
2626
"google.golang.org/grpc/status"
2727
)
2828

@@ -875,17 +875,11 @@ func doSet(t *testing.T, data ...interface{}) {
875875
}
876876
}
877877

878-
cred := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
879-
conn, err := grpc.Dial("127.0.0.1:8081", grpc.WithTransportCredentials(cred))
880-
if err != nil {
881-
t.Fatalf("Could not create client: %v", err)
882-
}
883-
878+
client := gnmipb.NewGNMIClient(createClient(t, 8081))
884879
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
885880
defer cancel()
886-
defer conn.Close()
887881

888-
_, err = gnmipb.NewGNMIClient(conn).Set(ctx, req)
882+
_, err := client.Set(ctx, req)
889883
if err != nil {
890884
t.Fatalf("Set failed: %v", err)
891885
}
@@ -935,3 +929,155 @@ func newBundleVersion(t *testing.T, version string) *extnpb.Extension {
935929
ext := &extnpb.RegisteredExtension{Id: spb.BUNDLE_VERSION_EXT, Msg: v}
936930
return &extnpb.Extension{Ext: &extnpb.Extension_RegisteredExt{RegisteredExt: ext}}
937931
}
932+
933+
func TestDebugSubscribePreferences(t *testing.T) {
934+
s := createServer(t, 8081)
935+
go runServer(t, s)
936+
defer s.s.Stop()
937+
938+
ifTop := &spb_gnoi.SubscribePreference{
939+
Path: strToPath("/openconfig-interfaces:interfaces/interface[name=*]"),
940+
OnChangeSupported: false,
941+
TargetDefinedMode: ON_CHANGE,
942+
WildcardSupported: true,
943+
}
944+
ifMtu := &spb_gnoi.SubscribePreference{
945+
Path: strToPath("/openconfig-interfaces:interfaces/interface[name=*]/config/mtu"),
946+
OnChangeSupported: true,
947+
TargetDefinedMode: ON_CHANGE,
948+
WildcardSupported: true,
949+
}
950+
ifStat := &spb_gnoi.SubscribePreference{
951+
Path: strToPath("/openconfig-interfaces:interfaces/interface[name=*]/state/counters"),
952+
OnChangeSupported: false,
953+
TargetDefinedMode: SAMPLE,
954+
WildcardSupported: true,
955+
}
956+
aclConfig := &spb_gnoi.SubscribePreference{
957+
Path: strToPath("/openconfig-acl:acl/acl-sets/acl-set[name=*][type=*]/config"),
958+
OnChangeSupported: true,
959+
TargetDefinedMode: ON_CHANGE,
960+
WildcardSupported: true,
961+
}
962+
yanglib := &spb_gnoi.SubscribePreference{
963+
Path: strToPath("/ietf-yang-library:modules-state/module-set-id"),
964+
OnChangeSupported: false,
965+
TargetDefinedMode: SAMPLE,
966+
WildcardSupported: false,
967+
}
968+
969+
t.Run("invalid_path", func(t *testing.T) {
970+
_, err := getSubscribePreferences(t, nil)
971+
if res, _ := status.FromError(err); res.Code() != codes.InvalidArgument {
972+
t.Fatalf("Expecting InvalidArgument error; got %v", err)
973+
}
974+
})
975+
976+
t.Run("unknown_path", func(t *testing.T) {
977+
_, err := getSubscribePreferences(t, strToPath("/unknown"))
978+
if res, _ := status.FromError(err); res.Code() != codes.InvalidArgument {
979+
t.Fatalf("Expecting InvalidArgument error; got %v", err)
980+
}
981+
})
982+
983+
t.Run("onchange_supported", func(t *testing.T) {
984+
verifySubscribePreferences(t,
985+
[]*gnmipb.Path{ifMtu.Path},
986+
[]*spb_gnoi.SubscribePreference{ifMtu})
987+
})
988+
989+
t.Run("onchange_unsupported", func(t *testing.T) {
990+
verifySubscribePreferences(t,
991+
[]*gnmipb.Path{ifStat.Path},
992+
[]*spb_gnoi.SubscribePreference{ifStat})
993+
})
994+
995+
t.Run("onchange_mixed", func(t *testing.T) {
996+
verifySubscribePreferences(t,
997+
[]*gnmipb.Path{ifTop.Path},
998+
[]*spb_gnoi.SubscribePreference{ifTop, ifStat})
999+
})
1000+
1001+
t.Run("nondb_path", func(t *testing.T) {
1002+
verifySubscribePreferences(t,
1003+
[]*gnmipb.Path{yanglib.Path},
1004+
[]*spb_gnoi.SubscribePreference{yanglib})
1005+
})
1006+
1007+
t.Run("unprefixed_path", func(t *testing.T) {
1008+
verifySubscribePreferences(t,
1009+
[]*gnmipb.Path{strToPath("/acl/acl-sets/acl-set/config")},
1010+
[]*spb_gnoi.SubscribePreference{aclConfig})
1011+
})
1012+
1013+
t.Run("multiple_paths", func(t *testing.T) {
1014+
verifySubscribePreferences(t,
1015+
[]*gnmipb.Path{yanglib.Path, ifTop.Path, aclConfig.Path},
1016+
[]*spb_gnoi.SubscribePreference{yanglib, ifTop, ifStat, aclConfig})
1017+
})
1018+
}
1019+
1020+
func TestDebugSubscribePreferences_dummy(t *testing.T) {
1021+
// Dummy testcase to increase code coverage !!!
1022+
f := func(_ ...interface{}) {}
1023+
for _, m := range []*spb_gnoi.SubscribePreferencesReq{nil, {}} {
1024+
f(m.String(), m.GetPath())
1025+
f(m.Descriptor())
1026+
}
1027+
for _, p := range []*spb_gnoi.SubscribePreference{nil, {}} {
1028+
f(p.String(), p.GetPath(), p.GetOnChangeSupported(), p.GetTargetDefinedMode(), p.GetWildcardSupported(), p.GetMinSampleInterval())
1029+
f(p.Descriptor())
1030+
}
1031+
}
1032+
1033+
func getSubscribePreferences(t *testing.T, paths ...*gnmipb.Path) ([]*spb_gnoi.SubscribePreference, error) {
1034+
t.Helper()
1035+
client := spb_gnoi.NewDebugClient(createClient(t, 8081))
1036+
stream, err := client.GetSubscribePreferences(
1037+
context.Background(),
1038+
&spb_gnoi.SubscribePreferencesReq{Path: paths},
1039+
)
1040+
if err != nil {
1041+
t.Fatalf("Could not invoke GetSubscribePreferences: %v", err)
1042+
}
1043+
1044+
var prefs []*spb_gnoi.SubscribePreference
1045+
for {
1046+
if p, err := stream.Recv(); err == nil {
1047+
prefs = append(prefs, p)
1048+
} else if err == io.EOF {
1049+
break
1050+
} else {
1051+
return prefs, err
1052+
}
1053+
}
1054+
1055+
return prefs, nil
1056+
}
1057+
1058+
func verifySubscribePreferences(t *testing.T, paths []*gnmipb.Path, exp []*spb_gnoi.SubscribePreference) {
1059+
t.Helper()
1060+
resp, err := getSubscribePreferences(t, paths...)
1061+
if err != nil {
1062+
t.Fatalf("GetSubscribePreferences returned error: %v", err)
1063+
}
1064+
if len(resp) != len(exp) {
1065+
t.Fatalf("Expected: %s\nReceived: %s", prefsText(exp), prefsText(resp))
1066+
}
1067+
for i, ex := range exp {
1068+
if ex.MinSampleInterval == 0 {
1069+
resp[i].MinSampleInterval = 0 // ignore MinSampleInterval for comparison
1070+
}
1071+
if !proto.Equal(ex, resp[i]) {
1072+
t.Fatalf("Expected: %s\nReceived: %s", prefsText(exp), prefsText(resp))
1073+
}
1074+
}
1075+
}
1076+
1077+
func prefsText(prefs []*spb_gnoi.SubscribePreference) string {
1078+
var s []string
1079+
for _, p := range prefs {
1080+
s = append(s, proto.MarshalTextString(p))
1081+
}
1082+
return "[\n" + strings.Join(s, "\n") + "]"
1083+
}

0 commit comments

Comments
 (0)