From 1a0059cb36c25221c3319daa8f31610e416560bd Mon Sep 17 00:00:00 2001 From: Malte Bauch <malte.bauch@stud.h-da.de> Date: Tue, 29 Mar 2022 10:04:01 +0000 Subject: [PATCH] Resolve "Requesting a specific config path of an OND returns always the whole config" See merge request danet/gosdn!266 --- controller/northbound/server/pnd.go | 102 ++++++++++++++++- controller/northbound/server/pnd_test.go | 134 ++++++++++++++++++++++- 2 files changed, 231 insertions(+), 5 deletions(-) diff --git a/controller/northbound/server/pnd.go b/controller/northbound/server/pnd.go index 10dad37f4..df50fd617 100644 --- a/controller/northbound/server/pnd.go +++ b/controller/northbound/server/pnd.go @@ -12,7 +12,9 @@ import ( "code.fbi.h-da.de/danet/gosdn/controller/nucleus" "code.fbi.h-da.de/danet/gosdn/controller/nucleus/errors" "github.com/google/uuid" + "github.com/openconfig/gnmi/proto/gnmi" "github.com/openconfig/ygot/ygot" + "github.com/openconfig/ygot/ytypes" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" @@ -139,6 +141,100 @@ func fillOnds(pnd networkdomain.NetworkDomain, all bool, did ...string) ([]*ppb. return onds, nil } +func fillOndBySpecificPath(pnd networkdomain.NetworkDomain, did string, path string) (*ppb.OrchestratedNetworkingDevice, error) { + d, err := pnd.GetDevice(did) + if err != nil { + log.Error(err) + return nil, status.Errorf(codes.Aborted, "%v", err) + } + + gnmiPath, err := ygot.StringToPath(path, ygot.StructuredPath) + if err != nil { + log.Error(err) + return nil, status.Errorf(codes.Aborted, "%v", err) + } + + opts := []ytypes.GetNodeOpt{&ytypes.GetHandleWildcards{}} + nodes, err := ytypes.GetNode(d.SBI().Schema().RootSchema(), d.Model(), gnmiPath, opts...) + if err != nil { + log.Error(err) + return nil, status.Errorf(codes.Aborted, "%v", err) + } + + // we should be fine to access nodes[0] here, since ytypes.GetNode() throws + // an error if there are no matches for the path. + nodeEntry := nodes[0].Schema + node := nodes[0].Data + + var dev []*gnmi.Notification + if nodeEntry.IsDir() { + validatedNode, ok := nodes[0].Data.(ygot.ValidatedGoStruct) + if !ok { + return nil, status.Errorf(codes.Aborted, "%v", errors.ErrInvalidTypeAssertion{ + Value: node, + Type: (ygot.ValidatedGoStruct)(nil), + }) + } + cfg := ygot.GNMINotificationsConfig{} + dev, err = ygot.TogNMINotifications(validatedNode, time.Now().UnixNano(), cfg) + if err != nil { + log.Error(err) + return nil, status.Errorf(codes.Aborted, "%v", err) + } + } else if nodeEntry.IsLeaf() { + dev, err = genGnmiNotificationForLeaf(gnmiPath, node) + if err != nil { + log.Error(err) + return nil, status.Errorf(codes.Aborted, "%v", err) + } + } + + sbi := spb.SouthboundInterface{} + if d.SBI() != nil { + sbi.Id = d.SBI().ID().String() + sbi.Type = d.SBI().Type() + } + + ond := &ppb.OrchestratedNetworkingDevice{ + Id: d.ID().String(), + Name: d.Name(), + Device: dev, + Sbi: &sbi, + } + + return ond, nil +} + +func genGnmiNotificationForLeaf(path *gnmi.Path, val any) ([]*gnmi.Notification, error) { + typedVal, err := ygot.EncodeTypedValue(val, gnmi.Encoding_PROTO) + if err != nil { + return nil, err + } + pathElem := path.GetElem()[len(path.GetElem())-1].GetName() + ppath := []string{pathElem} + return []*gnmi.Notification{ + { + Timestamp: time.Now().UnixNano(), + Update: []*gnmi.Update{ + { + Path: &gnmi.Path{ + // NOTE: This is dirty and should be changed as soon as + // ygot.TogNMINotifications does not return + // notifications with `Element`! + // `Element` is marked as deprecated, but since + // ygot.TogNMINotifications returns `Element`, we will + // keep using it for now and to maintain consistency. + // Only returning the last element here, since we + // request a path node. + Element: ppath, + }, + Val: typedVal, + }, + }, + }, + }, nil +} + func (p pndServer) GetSbi(ctx context.Context, request *ppb.GetSbiRequest) (*ppb.GetSbiResponse, error) { labels := prometheus.Labels{"service": "pnd", "rpc": "get"} start := metrics.StartHook(labels, grpcRequestsTotal) @@ -276,11 +372,13 @@ func (p pndServer) GetPath(ctx context.Context, request *ppb.GetPathRequest) (*p log.Error(err) return nil, status.Errorf(codes.Aborted, "%v", err) } - ond, err := fillOnds(pnd, false, request.Did) + + ond, err := fillOndBySpecificPath(pnd, request.Did, path) if err != nil { log.Error(err) return nil, status.Errorf(codes.Aborted, "%v", err) } + return &ppb.GetPathResponse{ Timestamp: time.Now().UnixNano(), Pnd: &ppb.PrincipalNetworkDomain{ @@ -288,7 +386,7 @@ func (p pndServer) GetPath(ctx context.Context, request *ppb.GetPathRequest) (*p Name: pnd.GetName(), Description: pnd.GetDescription(), }, - Device: ond[0].Device, + Device: ond.Device, }, nil } diff --git a/controller/northbound/server/pnd_test.go b/controller/northbound/server/pnd_test.go index 11e8e1259..3bd0af7e1 100644 --- a/controller/northbound/server/pnd_test.go +++ b/controller/northbound/server/pnd_test.go @@ -17,7 +17,10 @@ import ( "code.fbi.h-da.de/danet/gosdn/controller/nucleus" "code.fbi.h-da.de/danet/gosdn/controller/store" "code.fbi.h-da.de/danet/yang-models/generated/openconfig" + "github.com/golang/protobuf/proto" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" + "github.com/openconfig/gnmi/proto/gnmi" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/mock" "google.golang.org/grpc" @@ -33,6 +36,7 @@ const sbiID = "f6fd4b35-f039-4111-9156-5e4501bb8a5a" const ondID = "7e0ed8cc-ebf5-46fa-9794-741494914883" var hostname = "manfred" +var domainname = "uwe" var pndUUID uuid.UUID var sbiUUID uuid.UUID var pendingChangeUUID uuid.UUID @@ -122,7 +126,8 @@ func TestMain(m *testing.M) { GoStruct: &openconfig.Device{ System: &openconfig.OpenconfigSystem_System{ Config: &openconfig.OpenconfigSystem_System_Config{ - Hostname: &hostname, + Hostname: &hostname, + DomainName: &domainname, }, }, }, @@ -159,11 +164,16 @@ func TestMain(m *testing.M) { mockPnd.On("Commit", mock.Anything).Return(nil) mockPnd.On("Confirm", mock.Anything).Return(nil) mockPnd.On("ChangeOND", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(uuid.Nil, nil) + mockPnd.On("Request", mock.Anything, mock.Anything).Return(nil, nil) - newPnd := getMockPND() + //newPnd := getMockPND() pndc = store.NewPndStore() - if err := pndc.Add(newPnd); err != nil { + //if err := pndc.Add(newPnd); err != nil { + // log.Fatal(err) + //} + + if err := pndc.Add(mockPnd); err != nil { log.Fatal(err) } @@ -224,6 +234,124 @@ func Test_pnd_Get(t *testing.T) { } } +func Test_pnd_GetPath(t *testing.T) { + removeExistingStores() + // options to apply to cmp.Equal + type args struct { + ctx context.Context + request *ppb.GetPathRequest + } + tests := []struct { + name string + args args + want []*gnmi.Notification + wantErr bool + }{ + { + name: "get path: system/config/hostname", + args: args{ + ctx: context.Background(), + request: &ppb.GetPathRequest{ + Timestamp: 1648488947324854250, + Did: mockDevice.ID().String(), + Path: "system/config/hostname", + Pid: mockPnd.ID().String(), + }, + }, + want: []*gnmi.Notification{ + { + Update: []*gnmi.Update{ + { + Path: &gnmi.Path{Element: []string{"hostname"}}, + Val: &gnmi.TypedValue{ + Value: &gnmi.TypedValue_StringVal{ + StringVal: "manfred", + }, + }, + }, + }}, + }, + wantErr: false, + }, + { + name: "get path: system", + args: args{ + ctx: context.Background(), + request: &ppb.GetPathRequest{ + Timestamp: 1648488947324854250, + Did: mockDevice.ID().String(), + Path: "system", + Pid: mockPnd.ID().String(), + }, + }, + want: []*gnmi.Notification{ + { + Update: []*gnmi.Update{ + { + Path: &gnmi.Path{Element: []string{"config", "domain-name"}}, + Val: &gnmi.TypedValue{ + Value: &gnmi.TypedValue_StringVal{ + StringVal: "uwe", + }, + }, + }, + { + Path: &gnmi.Path{Element: []string{"config", "hostname"}}, + Val: &gnmi.TypedValue{ + Value: &gnmi.TypedValue_StringVal{ + StringVal: "manfred", + }, + }, + }, + }}, + }, + wantErr: false, + }, + { + name: "get path: this/path/is/not/valid", + args: args{ + ctx: context.Background(), + request: &ppb.GetPathRequest{ + Timestamp: 1648488947324854250, + Did: mockDevice.ID().String(), + Path: "this/path/is/not/valid", + Pid: mockPnd.ID().String(), + }, + }, + want: []*gnmi.Notification{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := pndServer{ + UnimplementedPndServiceServer: ppb.UnimplementedPndServiceServer{}, + } + resp, err := s.GetPath(tt.args.ctx, tt.args.request) + if (err != nil) != tt.wantErr { + t.Errorf("GetPath() error = %v, wantErr %v", err, tt.wantErr) + return + } + + got := resp.GetDevice() + + // with the current implementation we should only get a + // notification set with length one. For the case that we do not + // get anything this makes sure we do not trigger a panic. + if len(got) >= 1 { + // set timestamp for equality; not the prettiest solution + tt.want[0].Timestamp = got[0].Timestamp + } + + for i, n := range got { + if diff := cmp.Diff(n, tt.want[i], cmp.Comparer(proto.Equal)); diff != "" { + t.Errorf("GetPath() diff in the received notification %d: \n%s", i+1, diff) + } + } + }) + } +} + func Test_pnd_Set(t *testing.T) { removeExistingStores() -- GitLab