package integration import ( "context" "os" "reflect" "sort" "testing" "time" "github.com/google/uuid" ppb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/pnd" spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound" tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport" "code.fbi.h-da.de/danet/gosdn/controller/interfaces/change" "code.fbi.h-da.de/danet/forks/goarista/gnmi" "code.fbi.h-da.de/danet/gosdn/controller/nucleus" "code.fbi.h-da.de/danet/gosdn/controller/nucleus/errors" "code.fbi.h-da.de/danet/gosdn/controller/nucleus/types" "code.fbi.h-da.de/danet/gosdn/controller/nucleus/util/proto" gpb "github.com/openconfig/gnmi/proto/gnmi" log "github.com/sirupsen/logrus" pb "google.golang.org/protobuf/proto" ) const unreachable = "203.0.113.10:6030" const testPath = "/system/config/hostname" var modifiedHostname = "ceos3000" var testAddress = "10.254.254.105:6030" var testUsername = "admin" var testPassword = "arista" var opt *tpb.TransportOption var gnmiMessages map[string]pb.Message func TestMain(m *testing.M) { testSetupIntegration() os.Exit(m.Run()) } func testSetupIntegration() { if os.Getenv("GOSDN_LOG") == "nolog" { log.SetLevel(log.PanicLevel) } addr := os.Getenv("CEOS_TEST_ENDPOINT") if addr != "" { testAddress = addr log.Infof("CEOS_TEST_ENDPOINT set to %v", testAddress) } u := os.Getenv("GOSDN_TEST_USER") if u != "" { testUsername = u log.Infof("GOSDN_TEST_USER set to %v", testUsername) } p := os.Getenv("GOSDN_TEST_PASSWORD") if p != "" { testPassword = p log.Infof("GOSDN_TEST_PASSWORD set to %v", testPassword) } gnmiMessages = map[string]pb.Message{ "../proto/cap-resp-arista-ceos": &gpb.CapabilityResponse{}, "../proto/req-full-node": &gpb.GetRequest{}, "../proto/req-full-node-arista-ceos": &gpb.GetRequest{}, "../proto/req-interfaces-arista-ceos": &gpb.GetRequest{}, "../proto/req-interfaces-interface-arista-ceos": &gpb.GetRequest{}, "../proto/req-interfaces-wildcard": &gpb.GetRequest{}, "../proto/resp-full-node": &gpb.GetResponse{}, "../proto/resp-full-node-arista-ceos": &gpb.GetResponse{}, "../proto/resp-interfaces-arista-ceos": &gpb.GetResponse{}, "../proto/resp-interfaces-interface-arista-ceos": &gpb.GetResponse{}, "../proto/resp-interfaces-wildcard": &gpb.GetResponse{}, "../proto/resp-set-system-config-hostname": &gpb.SetResponse{}, } for k, v := range gnmiMessages { if err := proto.Read(k, v); err != nil { log.Fatalf("error parsing %v: %v", k, err) } } opt = &tpb.TransportOption{ Address: testAddress, Username: testUsername, Password: testPassword, TransportOption: &tpb.TransportOption_GnmiTransportOption{ GnmiTransportOption: &tpb.GnmiTransportOption{}, }, } } func TestGnmi_SetInvalidIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } type fields struct { opt *tpb.TransportOption } type args struct { ctx context.Context payload change.Payload } tests := []struct { name string fields fields args args wantErr bool }{ { name: "destination unreachable", fields: fields{ opt: &tpb.TransportOption{ Address: unreachable, TransportOption: &tpb.TransportOption_GnmiTransportOption{ GnmiTransportOption: &tpb.GnmiTransportOption{}}, }, }, args: args{ ctx: context.Background(), payload: change.Payload{}, }, wantErr: true, }, { name: "invalid update", fields: fields{opt: opt}, args: args{ ctx: context.Background(), payload: change.Payload{}, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("SetInvalidIntegration() error = %v", err) return } g, err := nucleus.NewTransport(tt.fields.opt, sbi) if (err != nil) != tt.wantErr { t.Errorf("SetInvalidIntegration() error = %v, wantErr %v", err, tt.wantErr) return } err = g.Set(tt.args.ctx, tt.args.payload) if (err != nil) != tt.wantErr { t.Errorf("SetInvalidIntegration() error = %v, wantErr %v", err, tt.wantErr) return } }) } } func TestGnmi_SetValidIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("SetValidIntegration() err = %v", err) return } opt := &tpb.TransportOption{ Address: testAddress, Username: testUsername, Password: testPassword, TransportOption: &tpb.TransportOption_GnmiTransportOption{ GnmiTransportOption: &tpb.GnmiTransportOption{}, }, } pnd, err := nucleus.NewPND("test", "test", uuid.New(), sbi, nil, nil) if err != nil { t.Error(err) return } if err := pnd.AddDevice("test", opt, sbi.ID()); err != nil { t.Error(err) return } device, err := pnd.GetDevice("test") if err != nil { t.Error(err) return } tests := []struct { name string apiOp ppb.ApiOperation path string value string want string }{ { name: "update", apiOp: ppb.ApiOperation_API_OPERATION_UPDATE, path: testPath, value: modifiedHostname, want: modifiedHostname, }, { name: "replace", apiOp: ppb.ApiOperation_API_OPERATION_REPLACE, path: "/system/config/domain-name", value: modifiedHostname, want: modifiedHostname, }, { name: "delete", apiOp: ppb.ApiOperation_API_OPERATION_DELETE, path: testPath, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cuid, err := pnd.ChangeOND(device.ID(), tt.apiOp, tt.path, tt.value) if err != nil { t.Error(err) return } if err := pnd.Commit(cuid); err != nil { t.Error(err) return } if err := pnd.Confirm(cuid); err != nil { t.Error(err) return } if tt.name != "delete" { resp, err := pnd.Request(device.ID(), tt.path) if err != nil { t.Error(err) return } r, ok := resp.(*gpb.GetResponse) if !ok { t.Error(&errors.ErrInvalidTypeAssertion{ Value: resp, Type: &gpb.GetResponse{}, }) return } got := r.Notification[0].Update[0].Val.GetStringVal() if !reflect.DeepEqual(got, tt.want) { t.Errorf("GetDevice() got = %v, want %v", got, tt.want) } } }) } } func TestGnmi_GetIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } paths := []string{ "/interfaces/interface", "system/config/hostname", } type fields struct { opt *tpb.TransportOption } type args struct { ctx context.Context params []string } tests := []struct { name string fields fields args args want interface{} wantErr bool }{ { name: "default", fields: fields{opt: opt}, args: args{ ctx: context.Background(), params: paths[:1], }, want: gnmiMessages["../proto/resp-interfaces-arista-ceos"], wantErr: false, }, { name: "destination unreachable", fields: fields{ opt: &tpb.TransportOption{ Address: unreachable, TransportOption: &tpb.TransportOption_GnmiTransportOption{ GnmiTransportOption: &tpb.GnmiTransportOption{}}, }, }, args: args{ ctx: context.Background(), params: paths, }, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("Get() error = %v", err) return } g, err := nucleus.NewTransport(tt.fields.opt, sbi) if err != nil { t.Error(err) return } got, err := g.Get(tt.args.ctx, tt.args.params...) if (err != nil) != tt.wantErr { t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) return } if reflect.TypeOf(got) != reflect.TypeOf(tt.want) { t.Errorf("Get() got = %v, want %v", got, tt.want) } }) } } func TestGnmi_SubscribeIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } type fields struct { opt *tpb.TransportOption } type args struct { ctx context.Context opts *gnmi.SubscribeOptions } tests := []struct { name string fields fields args args wantErr bool }{ { name: "default", fields: fields{ opt: &tpb.TransportOption{ Address: testAddress, Username: testUsername, Password: testPassword, Tls: false, TransportOption: &tpb.TransportOption_GnmiTransportOption{ GnmiTransportOption: &tpb.GnmiTransportOption{ Compression: "", GrpcDialOptions: nil, Token: "", Encoding: 0, }, }, }, }, args: args{ ctx: context.Background(), opts: &gnmi.SubscribeOptions{ Mode: "stream", StreamMode: "sample", SampleInterval: uint64(1 * time.Second), HeartbeatInterval: uint64(100 * time.Millisecond), Paths: gnmi.SplitPaths([]string{ "/interfaces/interface/name", "/system/config/hostname", }), Target: testAddress, }, }, wantErr: false, }, { name: "wrong path", fields: fields{ opt: &tpb.TransportOption{ TransportOption: &tpb.TransportOption_GnmiTransportOption{ GnmiTransportOption: &tpb.GnmiTransportOption{}}, }, }, args: args{ opts: &gnmi.SubscribeOptions{ Mode: "stream", StreamMode: "sample", SampleInterval: uint64(1 * time.Second), HeartbeatInterval: uint64(100 * time.Millisecond), Paths: gnmi.SplitPaths([]string{ "interfaces/interface/name", "ystem/config/hostname", }), Target: testAddress, }, }, wantErr: true, }, { name: "destination unreachable", fields: fields{ opt: &tpb.TransportOption{ Address: "203.0.113.10:6030", TransportOption: &tpb.TransportOption_GnmiTransportOption{ GnmiTransportOption: &tpb.GnmiTransportOption{}}, }, }, args: args{ opts: &gnmi.SubscribeOptions{}, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var wantErr = tt.wantErr sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("Subscribe() error = %v", err) return } g, err := nucleus.NewTransport(tt.fields.opt, sbi) if err != nil { t.Error(err) return } ctx := context.WithValue(context.Background(), types.CtxKeyOpts, tt.args.opts) //nolint ctx, cancel := context.WithCancel(ctx) go func() { subErr := g.Subscribe(ctx) if (subErr != nil) != wantErr { if !wantErr && subErr != nil { if subErr.Error() != "rpc error: code = Canceled desc = context canceled" { t.Errorf("Subscribe() error = %v, wantErr %v", subErr, tt.wantErr) } } } }() time.Sleep(time.Second * 3) cancel() time.Sleep(time.Second * 1) }) } } func TestGnmi_CapabilitiesIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } type fields struct { opt *tpb.TransportOption } type args struct { ctx context.Context } tests := []struct { name string fields fields args args want interface{} wantErr bool }{ { name: "supported models", fields: fields{opt: opt}, args: args{ctx: context.Background()}, want: gnmiMessages["../proto/cap-resp-arista-ceos"].(*gpb.CapabilityResponse).SupportedModels, wantErr: false, }, { name: "supported encodings", fields: fields{opt: opt}, args: args{ctx: context.Background()}, want: gnmiMessages["../proto/cap-resp-arista-ceos"].(*gpb.CapabilityResponse).SupportedEncodings, wantErr: false, }, { name: "gnmi version", fields: fields{opt: opt}, args: args{ctx: context.Background()}, want: gnmiMessages["../proto/cap-resp-arista-ceos"].(*gpb.CapabilityResponse).GNMIVersion, wantErr: false, }, { name: "destination unreachable", fields: fields{opt: &tpb.TransportOption{ Address: "203.0.113.10:6030", TransportOption: &tpb.TransportOption_GnmiTransportOption{ GnmiTransportOption: &tpb.GnmiTransportOption{}}, }, }, args: args{ctx: context.Background()}, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("Capabilities() error = %v", err) return } tr, err := nucleus.NewTransport(tt.fields.opt, sbi) if err != nil { t.Error(err) return } g, ok := tr.(*nucleus.Gnmi) if !ok { t.Error(&errors.ErrInvalidTypeAssertion{ Value: tr, Type: &nucleus.Gnmi{}, }) } resp, err := g.Capabilities(tt.args.ctx) if (err != nil) != tt.wantErr { t.Errorf("Capabilities() error = %v, wantErr %v", err, tt.wantErr) return } var got interface{} switch tt.name { case "supported encodings": got = resp.(*gpb.CapabilityResponse).SupportedEncodings case "supported models": t.Skip("test causes false negative") got = resp.(*gpb.CapabilityResponse).SupportedModels sort.Slice(got.([]*gpb.ModelData), func(i, j int) bool { return got.([]*gpb.ModelData)[i].Name < got.([]*gpb.ModelData)[j].Name }) sort.Slice(tt.want.([]*gpb.ModelData), func(i, j int) bool { return tt.want.([]*gpb.ModelData)[i].Name < tt.want.([]*gpb.ModelData)[j].Name }) case "gnmi version": got = resp.(*gpb.CapabilityResponse).GNMIVersion default: } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Type() = %v, want %v", got, tt.want) } }) } }