Skip to content

Commit

Permalink
Debug grpc to fetch subscribe preferences of a path (sonic-net#130)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sachinholla authored Sep 27, 2023
1 parent 099ff7c commit cbb7631
Show file tree
Hide file tree
Showing 7 changed files with 731 additions and 10 deletions.
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@ endif
swsscommon_wrap:
make -C swsscommon

.SECONDEXPANSION:

PROTOC_PATH := $(PATH):$(GOBIN)
PROTOC_OPTS := -I$(CURDIR)/vendor -I/usr/local/include -I/usr/include

# Generate following go & grpc bindings using teh legacy protoc-gen-go
PROTO_GO_BINDINGS += proto/sonic_internal.pb.go
PROTO_GO_BINDINGS += proto/gnoi/sonic_debug.pb.go

$(PROTO_GO_BINDINGS): $$(patsubst %.pb.go,%.proto,$$@) | $(GOBIN)/protoc-gen-go
PATH=$(PROTOC_PATH) protoc -I$(@D) $(PROTOC_OPTS) --go_out=plugins=grpc:$(@D) $<

$(GOBIN)/protoc-gen-go:
cd $$(mktemp -d) && \
$(GO) mod init protoc && \
$(GO) install github.com/golang/protobuf/protoc-gen-go


DBCONFG = $(DBDIR)/database_config.json
ENVFILE = build/test/env.txt
TESTENV = $(shell cat $(ENVFILE))
Expand Down
108 changes: 108 additions & 0 deletions gnmi_server/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
////////////////////////////////////////////////////////////////////////////////
// //
// Copyright 2023 Broadcom. The term Broadcom refers to Broadcom Inc. and/or //
// its subsidiaries. //
// //
// Licensed under the Apache License, Version 2.0 (the "License"); //
// you may not use this file except in compliance with the License. //
// You may obtain a copy of the License at //
// //
// http://www.apache.org/licenses/LICENSE-2.0 //
// //
// Unless required by applicable law or agreed to in writing, software //
// distributed under the License is distributed on an "AS IS" BASIS, //
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
// See the License for the specific language governing permissions and //
// limitations under the License. //
// //
////////////////////////////////////////////////////////////////////////////////

package gnmi

import (
"sort"
"time"

"github.com/Azure/sonic-mgmt-common/translib"
"github.com/Azure/sonic-mgmt-common/translib/path"
gnmipb "github.com/openconfig/gnmi/proto/gnmi"
"github.com/sonic-net/sonic-gnmi/common_utils"
spb_gnoi "github.com/sonic-net/sonic-gnmi/proto/gnoi"
"github.com/sonic-net/sonic-gnmi/transl_utils"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func (srv *Server) GetSubscribePreferences(req *spb_gnoi.SubscribePreferencesReq, stream spb_gnoi.Debug_GetSubscribePreferencesServer) error {
ctx := stream.Context()
ctx, err := authenticate(srv.config.UserAuth, ctx)
if err != nil {
return err
}

translPaths := make([]translib.IsSubscribePath, 0, len(req.GetPath()))
for i, p := range req.GetPath() {
reqPath, err := transl_utils.ConvertToURI(nil, p,
&path.AppendModulePrefix{}, &path.AddWildcardKeys{})
if err != nil {
return status.Error(codes.InvalidArgument, "Unknown path: "+path.String(p))
}

translPaths = append(translPaths, translib.IsSubscribePath{
ID: uint32(i),
Path: reqPath,
Mode: translib.TargetDefined,
})
}

rc, _ := common_utils.GetContext(ctx)
trResp, err := translib.IsSubscribeSupported(translib.IsSubscribeRequest{
Paths: translPaths,
User: translib.UserRoles{Name: rc.Auth.User, Roles: rc.Auth.Roles},
})
if err != nil {
return status.Error(codes.InvalidArgument, err.Error())
}

// When the path supports on_change but some of its subpaths do not, extra entries
// gets appended with same request ID. Group such entries by ID.
sort.Slice(trResp, func(i, j int) bool {
return trResp[i].ID < trResp[j].ID ||
(trResp[i].ID == trResp[j].ID && trResp[i].Path < trResp[j].Path)
})

for _, r := range trResp {
pathStr, err := path.New(r.Path)
if err != nil {
return status.Error(codes.Internal, err.Error())
}
pref := &spb_gnoi.SubscribePreference{
Path: pathStr,
WildcardSupported: r.IsWildcardSupported,
OnChangeSupported: r.IsOnChangeSupported,
TargetDefinedMode: gnmipb.SubscriptionMode_SAMPLE,
MinSampleInterval: uint64(r.MinInterval) * uint64(time.Second),
}
if !r.IsSubPath && hasOnChangeDisabledSubpath(r.ID, trResp) {
pref.OnChangeSupported = false
}
if r.IsOnChangeSupported && r.PreferredType == translib.OnChange {
pref.TargetDefinedMode = gnmipb.SubscriptionMode_ON_CHANGE
}

if err = stream.Send(pref); err != nil {
return err
}
}

return nil
}

func hasOnChangeDisabledSubpath(id uint32, allPrefs []*translib.IsSubscribeResponse) bool {
for _, p := range allPrefs {
if p.ID == id && p.IsSubPath && !p.IsOnChangeSupported {
return true
}
}
return false
}
1 change: 1 addition & 0 deletions gnmi_server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ func NewServer(config *Config, opts []grpc.ServerOption) (*Server, error) {
if srv.config.EnableTranslibWrite {
spb_gnoi.RegisterSonicServiceServer(srv.s, srv)
}
spb_gnoi.RegisterDebugServer(srv.s, srv)
log.V(1).Infof("Created Server on %s, read-only: %t", srv.Address(), !srv.config.EnableTranslibWrite)
return srv, nil
}
Expand Down
14 changes: 14 additions & 0 deletions gnmi_server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ func loadDBNotStrict(t *testing.T, rclient *redis.Client, mpi map[string]interfa
}
}

func createClient(t *testing.T, port int) *grpc.ClientConn {
t.Helper()
cred := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
conn, err := grpc.Dial(
fmt.Sprintf("127.0.0.1:%d", port),
grpc.WithTransportCredentials(cred),
)
if err != nil {
t.Fatalf("Dialing to :%d failed: %v", port, err)
}
t.Cleanup(func() { conn.Close() })
return conn
}

func createServer(t *testing.T, port int64) *Server {
t.Helper()
certificate, err := testcert.NewCert()
Expand Down
166 changes: 156 additions & 10 deletions gnmi_server/transl_sub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gnmi
import (
"crypto/tls"
"fmt"
"io"
"path/filepath"
"reflect"
"strings"
Expand All @@ -18,11 +19,10 @@ import (
extnpb "github.com/openconfig/gnmi/proto/gnmi_ext"
"github.com/openconfig/ygot/ygot"
spb "github.com/sonic-net/sonic-gnmi/proto"
spb_gnoi "github.com/sonic-net/sonic-gnmi/proto/gnoi"
dbconfig "github.com/sonic-net/sonic-gnmi/sonic_db_config"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
)

Expand Down Expand Up @@ -875,17 +875,11 @@ func doSet(t *testing.T, data ...interface{}) {
}
}

cred := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
conn, err := grpc.Dial("127.0.0.1:8081", grpc.WithTransportCredentials(cred))
if err != nil {
t.Fatalf("Could not create client: %v", err)
}

client := gnmipb.NewGNMIClient(createClient(t, 8081))
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
defer conn.Close()

_, err = gnmipb.NewGNMIClient(conn).Set(ctx, req)
_, err := client.Set(ctx, req)
if err != nil {
t.Fatalf("Set failed: %v", err)
}
Expand Down Expand Up @@ -935,3 +929,155 @@ func newBundleVersion(t *testing.T, version string) *extnpb.Extension {
ext := &extnpb.RegisteredExtension{Id: spb.BUNDLE_VERSION_EXT, Msg: v}
return &extnpb.Extension{Ext: &extnpb.Extension_RegisteredExt{RegisteredExt: ext}}
}

func TestDebugSubscribePreferences(t *testing.T) {
s := createServer(t, 8081)
go runServer(t, s)
defer s.s.Stop()

ifTop := &spb_gnoi.SubscribePreference{
Path: strToPath("/openconfig-interfaces:interfaces/interface[name=*]"),
OnChangeSupported: false,
TargetDefinedMode: ON_CHANGE,
WildcardSupported: true,
}
ifMtu := &spb_gnoi.SubscribePreference{
Path: strToPath("/openconfig-interfaces:interfaces/interface[name=*]/config/mtu"),
OnChangeSupported: true,
TargetDefinedMode: ON_CHANGE,
WildcardSupported: true,
}
ifStat := &spb_gnoi.SubscribePreference{
Path: strToPath("/openconfig-interfaces:interfaces/interface[name=*]/state/counters"),
OnChangeSupported: false,
TargetDefinedMode: SAMPLE,
WildcardSupported: true,
}
aclConfig := &spb_gnoi.SubscribePreference{
Path: strToPath("/openconfig-acl:acl/acl-sets/acl-set[name=*][type=*]/config"),
OnChangeSupported: true,
TargetDefinedMode: ON_CHANGE,
WildcardSupported: true,
}
yanglib := &spb_gnoi.SubscribePreference{
Path: strToPath("/ietf-yang-library:modules-state/module-set-id"),
OnChangeSupported: false,
TargetDefinedMode: SAMPLE,
WildcardSupported: false,
}

t.Run("invalid_path", func(t *testing.T) {
_, err := getSubscribePreferences(t, nil)
if res, _ := status.FromError(err); res.Code() != codes.InvalidArgument {
t.Fatalf("Expecting InvalidArgument error; got %v", err)
}
})

t.Run("unknown_path", func(t *testing.T) {
_, err := getSubscribePreferences(t, strToPath("/unknown"))
if res, _ := status.FromError(err); res.Code() != codes.InvalidArgument {
t.Fatalf("Expecting InvalidArgument error; got %v", err)
}
})

t.Run("onchange_supported", func(t *testing.T) {
verifySubscribePreferences(t,
[]*gnmipb.Path{ifMtu.Path},
[]*spb_gnoi.SubscribePreference{ifMtu})
})

t.Run("onchange_unsupported", func(t *testing.T) {
verifySubscribePreferences(t,
[]*gnmipb.Path{ifStat.Path},
[]*spb_gnoi.SubscribePreference{ifStat})
})

t.Run("onchange_mixed", func(t *testing.T) {
verifySubscribePreferences(t,
[]*gnmipb.Path{ifTop.Path},
[]*spb_gnoi.SubscribePreference{ifTop, ifStat})
})

t.Run("nondb_path", func(t *testing.T) {
verifySubscribePreferences(t,
[]*gnmipb.Path{yanglib.Path},
[]*spb_gnoi.SubscribePreference{yanglib})
})

t.Run("unprefixed_path", func(t *testing.T) {
verifySubscribePreferences(t,
[]*gnmipb.Path{strToPath("/acl/acl-sets/acl-set/config")},
[]*spb_gnoi.SubscribePreference{aclConfig})
})

t.Run("multiple_paths", func(t *testing.T) {
verifySubscribePreferences(t,
[]*gnmipb.Path{yanglib.Path, ifTop.Path, aclConfig.Path},
[]*spb_gnoi.SubscribePreference{yanglib, ifTop, ifStat, aclConfig})
})
}

func TestDebugSubscribePreferences_dummy(t *testing.T) {
// Dummy testcase to increase code coverage !!!
f := func(_ ...interface{}) {}
for _, m := range []*spb_gnoi.SubscribePreferencesReq{nil, {}} {
f(m.String(), m.GetPath())
f(m.Descriptor())
}
for _, p := range []*spb_gnoi.SubscribePreference{nil, {}} {
f(p.String(), p.GetPath(), p.GetOnChangeSupported(), p.GetTargetDefinedMode(), p.GetWildcardSupported(), p.GetMinSampleInterval())
f(p.Descriptor())
}
}

func getSubscribePreferences(t *testing.T, paths ...*gnmipb.Path) ([]*spb_gnoi.SubscribePreference, error) {
t.Helper()
client := spb_gnoi.NewDebugClient(createClient(t, 8081))
stream, err := client.GetSubscribePreferences(
context.Background(),
&spb_gnoi.SubscribePreferencesReq{Path: paths},
)
if err != nil {
t.Fatalf("Could not invoke GetSubscribePreferences: %v", err)
}

var prefs []*spb_gnoi.SubscribePreference
for {
if p, err := stream.Recv(); err == nil {
prefs = append(prefs, p)
} else if err == io.EOF {
break
} else {
return prefs, err
}
}

return prefs, nil
}

func verifySubscribePreferences(t *testing.T, paths []*gnmipb.Path, exp []*spb_gnoi.SubscribePreference) {
t.Helper()
resp, err := getSubscribePreferences(t, paths...)
if err != nil {
t.Fatalf("GetSubscribePreferences returned error: %v", err)
}
if len(resp) != len(exp) {
t.Fatalf("Expected: %s\nReceived: %s", prefsText(exp), prefsText(resp))
}
for i, ex := range exp {
if ex.MinSampleInterval == 0 {
resp[i].MinSampleInterval = 0 // ignore MinSampleInterval for comparison
}
if !proto.Equal(ex, resp[i]) {
t.Fatalf("Expected: %s\nReceived: %s", prefsText(exp), prefsText(resp))
}
}
}

func prefsText(prefs []*spb_gnoi.SubscribePreference) string {
var s []string
for _, p := range prefs {
s = append(s, proto.MarshalTextString(p))
}
return "[\n" + strings.Join(s, "\n") + "]"
}
Loading

0 comments on commit cbb7631

Please sign in to comment.