diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b785b246ef0bc9a208c56fff416ec7c350ae5869..d12d224a27aa8b9ba2850dae75bf64de55869155 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -30,6 +30,9 @@ RUN apt-get update \ && rm -Rf /usr/share/doc && rm -Rf /usr/share/man \ && apt-get clean +# Install gnmic +RUN bash -c "$(curl -sL https://get-gnmic.openconfig.net)" + # Install oh-my-zsh for more terminal features and set it as primary shell ENV SHELL /bin/zsh RUN wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh || true diff --git a/.gitlab/ci/.test.yml b/.gitlab/ci/.test.yml index 2b291b8f4d32e65f9701ce46d4e051df14622357..4d952f5bac1cfa3ce07148610b12f8563805b012 100644 --- a/.gitlab/ci/.test.yml +++ b/.gitlab/ci/.test.yml @@ -11,6 +11,7 @@ integration-test-gosdn: MONGO_INITDB_ROOT_PASSWORD: example INTEGRATION_TEST_TARGET_A: gnmi-target_A:7030 INTEGRATION_TEST_TARGET_B: gnmi-target_B:7030 + RABBITMQ_HOSTNAME: rabbitmq services: - name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/mongo:5 alias: mongo @@ -39,6 +40,7 @@ integration-test-gosdn: # Remove start of goSDN later when fixed. See: https://code.fbi.h-da.de/danet/gosdn/-/issues/335 - cp -r artifacts/ssl/gosdn/certs artifacts/ssl/ - cp -r artifacts/ssl/gosdn/private artifacts/ssl/ + - mkdir artifacts/configs && cp controller/configs/gNMISubscriptions.txt.example artifacts/configs/gNMISubscriptions.txt - make build-gosdn - cd artifacts && GOSDN_ADMIN_PASSWORD=TestPassword ./gosdn --config ../controller/configs/integration-test-gosdn.toml --security secure & - INTEGRATION_TEST_CONTROLLER_URL="localhost:55055" go test -p 1 ./integration-tests/* diff --git a/application-framework/registration/registration.go b/application-framework/registration/registration.go index 3cb1827ce5a513719c37aa68aa5ffb9de32d3d32..10305ba5c517a06329157240be9534d96fb3d74a 100644 --- a/application-framework/registration/registration.go +++ b/application-framework/registration/registration.go @@ -10,13 +10,12 @@ import ( ) // Register registers a new app at the control plane. -func Register(gosdnAddress, name, token string) (string, error) { +func Register(ctx context.Context, gosdnAddress, name, token string) (string, error) { conn, err := grpc.Dial(gosdnAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return "", err } - ctx := context.Background() appService := app.NewAppServiceClient(conn) request := &app.AppRegisterRequest{ diff --git a/applications/arista-routing-engine/main.go b/applications/arista-routing-engine/main.go index 7963c24ebbf47f475742c360b90036d66f08ea57..0ab37ca61d9964b846439c6bd7723fd2eb5537e6 100644 --- a/applications/arista-routing-engine/main.go +++ b/applications/arista-routing-engine/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "code.fbi.h-da.de/danet/gosdn/application-framework/event" @@ -9,7 +10,7 @@ import ( ) func main() { - queueCredentials, err := registration.Register("localhost:55055", "arista-routing-engine", "SecurePresharedToken") + queueCredentials, err := registration.Register(context.Background(), "localhost:55055", "arista-routing-engine", "SecurePresharedToken") if err != nil { logrus.Errorf("failed to register application on control plane. %v", err) os.Exit(1) diff --git a/applications/basic-interface-monitoring/main.go b/applications/basic-interface-monitoring/main.go index 5f451c99a2287e7be7a3e3cd46b42c6dc39d6f5e..676ee5f36aac4a245cf256f419a89dd620c68310 100644 --- a/applications/basic-interface-monitoring/main.go +++ b/applications/basic-interface-monitoring/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "os" @@ -18,7 +19,7 @@ func main() { flag.Parse() - queueCredentials, err := registration.Register(controllerAddress, "basic-interface-monitoring", "SecurePresharedToken") + queueCredentials, err := registration.Register(context.Background(), controllerAddress, "basic-interface-monitoring", "SecurePresharedToken") if err != nil { logrus.Errorf("failed to register application on control plane. %v", err) os.Exit(1) diff --git a/applications/hostname-checker/main.go b/applications/hostname-checker/main.go index 53c3e462e2012a3361148b41525cde678fa97292..0dbcaabd5659d4821017f3b295c2e1c27eeb3a95 100644 --- a/applications/hostname-checker/main.go +++ b/applications/hostname-checker/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "code.fbi.h-da.de/danet/gosdn/application-framework/event" @@ -9,7 +10,7 @@ import ( ) func main() { - queueCredentials, err := registration.Register("localhost:55055", "hostname-checker", "SecurePresharedToken") + queueCredentials, err := registration.Register(context.Background(), "localhost:55055", "hostname-checker", "SecurePresharedToken") if err != nil { logrus.Errorf("failed to register application on control plane. %v", err) os.Exit(1) diff --git a/applications/ws-events/main.go b/applications/ws-events/main.go index 74131c99192356f6090a4555325ae1b949597b52..54565d568a06d36bb97c0c3f5ed8b1a3eb81d728 100644 --- a/applications/ws-events/main.go +++ b/applications/ws-events/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "os" @@ -14,7 +15,7 @@ func main() { flag.Parse() // Register the application at the controller - queueCredentials, err := registration.Register(*controllerAddress, "ws-events", "SecurePresharedToken") + queueCredentials, err := registration.Register(context.Background(), *controllerAddress, "ws-events", "SecurePresharedToken") if err != nil { logrus.Errorf("failed to register application on control plane. %v", err) os.Exit(1) diff --git a/dev_env_data/docker-compose/integration-test_docker-compose.yml b/dev_env_data/docker-compose/integration-test_docker-compose.yml index 9d5ba0cb0ba554b36eee75c90f0196f9daab3e1c..8a8b6dd70c2f88064e73eae5a7184ec88e5858fb 100644 --- a/dev_env_data/docker-compose/integration-test_docker-compose.yml +++ b/dev_env_data/docker-compose/integration-test_docker-compose.yml @@ -19,6 +19,9 @@ services: rabbitmq: image: rabbitmq:3-management + ports: + - 127.0.0.1:5672:5672 + - 127.0.0.1:15672:15672 healthcheck: test: rabbitmq-diagnostics -q ping interval: 30s diff --git a/integration-tests/application_tests/appUtility_test.go b/integration-tests/application_tests/appUtility_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4539072576150b1742635ed42307cfa17cfa0da2 --- /dev/null +++ b/integration-tests/application_tests/appUtility_test.go @@ -0,0 +1,102 @@ +package integration_test_application + +import ( + "context" + "os" + "os/signal" + "strings" + "syscall" + + "code.fbi.h-da.de/danet/gosdn/application-framework/event" + "code.fbi.h-da.de/danet/gosdn/application-framework/registration" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" +) + +const localhost = "127.0.0.1" + +// Application is an example for a sdn application. +type Application struct { + eventService event.ServiceInterface + stopChannel chan os.Signal + grpcClientConn *grpc.ClientConn + addEventChannel chan event.Event + updateEventChannel chan event.Event + deleteEventChannel chan event.Event + subscribeEventChannel chan event.Event +} + +func NewApplication(ctx context.Context, grpcClientConn *grpc.ClientConn, controllerAddress string, topics []event.Topic, rabbitMQAddress string) *Application { + queueCredentials, err := registration.Register(ctx, controllerAddress, "integration-test-application", "SecurePresharedToken") + if err != nil { + logrus.Errorf("failed to register application on control plane. %v", err) + os.Exit(1) + } + + if rabbitMQAddress != "" { + queueCredentials = strings.ReplaceAll(queueCredentials, localhost, rabbitMQAddress) + } + + eventService, err := event.NewEventService( + queueCredentials, + topics, + ) + if err != nil { + logrus.Errorf("failed to create event service. %v", err) + os.Exit(1) + } + + return &Application{ + eventService: eventService, + stopChannel: make(chan os.Signal, 1), + grpcClientConn: grpcClientConn, + addEventChannel: make(chan event.Event, 1), + updateEventChannel: make(chan event.Event, 1), + deleteEventChannel: make(chan event.Event, 1), + subscribeEventChannel: make(chan event.Event, 1), + } +} + +// Run runs the application. +func (a *Application) Run(eventTypeCallbackTuples []event.TypeToCallbackTuple) { + signal.Notify(a.stopChannel, os.Interrupt, syscall.SIGTERM) + + a.eventService.SubscribeToEventType(eventTypeCallbackTuples) + a.eventService.SetupEventReciever(a.stopChannel) + + var forever chan struct{} + + go func() { + for { + select { + case <-a.stopChannel: + close(forever) + _ = a.grpcClientConn.Close() + + return + } + } + }() + + <-forever +} + +func (a *Application) addCallback(event *event.Event) { + logrus.Infof("Incoming Event: EntityID: %v, ID: %v, PathsAndValues: %v, Type: %v", event.EntityID, event.ID, event.PathsAndValuesMap, event.Type) + a.addEventChannel <- *event +} + +func (a *Application) updateCallback(event *event.Event) { + logrus.Infof("Incoming Event: EntityID: %v, ID: %v, PathsAndValues: %v, Type: %v", event.EntityID, event.ID, event.PathsAndValuesMap, event.Type) + a.updateEventChannel <- *event +} + +func (a *Application) deleteCallback(event *event.Event) { + logrus.Infof("Incoming Event: EntityID: %v, ID: %v, PathsAndValues: %v, Type: %v", event.EntityID, event.ID, event.PathsAndValuesMap, event.Type) + a.deleteEventChannel <- *event +} + +func (a *Application) subscribeCallback(event *event.Event) { + logrus.Infof("Incoming Event: EntityID: %v, ID: %v, PathsAndValues: %v, Type: %v", event.EntityID, event.ID, event.PathsAndValuesMap, event.Type) + a.subscribeEventChannel <- *event +} diff --git a/integration-tests/application_tests/application_test.go b/integration-tests/application_tests/application_test.go new file mode 100644 index 0000000000000000000000000000000000000000..721bd2cfa770f6a30826e75e49ddeb907a849e0f --- /dev/null +++ b/integration-tests/application_tests/application_test.go @@ -0,0 +1,305 @@ +package integration_test_application + +import ( + "context" + "fmt" + "os" + "testing" + + "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/conflict" + mnepb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement" + apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac" + tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport" + "code.fbi.h-da.de/danet/gosdn/application-framework/event" + integration_test_utils "code.fbi.h-da.de/danet/gosdn/integration-tests/integrationTestUtils" + + "google.golang.org/grpc" +) + +// Notes: +// When adding tests take into account all the messages that get added to the message +// queues during test setups and actual test cases. So make sure to read from the according eventChannels +// of the application. +// +// Do not reset test setup in this because it causes weird interactions between test cases and RabbitMQ message queues. +// Reason for that is that cleanup creates messages to be put into the queues of RabbitMQ. + +const targetAAdress = "gnmi-target_A:7030" +const targetUserAndPW = "admin" +const pndID = "5f20f34b-cbd0-4511-9ddc-c50cf6a3b49d" +const pluginID = "d1c269a2-6482-4010-b0d8-679dff73153b" +const mneID = "9ee7bf15-15ff-44b8-aafa-035487d1e97f" + +const userID = "23224223-aeb6-433b-a797-2d671d7424f0" +const roleID = "679f7982-d740-452e-b78d-c1d133233694" + +// The connection to the controller to use in each test. +var conn *grpc.ClientConn + +// The context containing the credentials when authenticated. +var ctx context.Context + +// A defaultSDN config with default/empty values. +var defaultSDNConfig string + +var application *Application + +func TestMain(m *testing.M) { + localConn, localCtx, err := integration_test_utils.CreateSecureConnection() + if err != nil { + fmt.Println(err.Error()) + } + conn = localConn + ctx = localCtx + + sndConfig, err := integration_test_utils.ExportCurrentSDNConfig(conn, ctx) + defaultSDNConfig = sndConfig + if err != nil { + fmt.Println(err.Error()) + } + + topics := []event.Topic{event.ManagedNetworkElement, event.User, event.Role} + + rabbitMQAddress := "" + envVarRabbitmq := os.Getenv("RABBITMQ_HOSTNAME") + if envVarRabbitmq != "" { + rabbitMQAddress = envVarRabbitmq + } + + application = NewApplication(ctx, conn, ":55055", topics, rabbitMQAddress) + + eventTypeCallbackTuples := []event.TypeToCallbackTuple{ + { + Type: event.Add, + Callback: application.addCallback, + }, + { + Type: event.Update, + Callback: application.updateCallback, + }, + { + Type: event.Delete, + Callback: application.deleteCallback, + }, + { + Type: event.Subscribe, + Callback: application.subscribeCallback, + }, + } + go application.Run(eventTypeCallbackTuples) + + // This is needed to clear the go channel of the messages sent by RabbitMQ when creating + // and logging in with the admin user. + // Important note: only works once after starting the setup, because first time use creates + // a user and role and update the user because of the login. After then only logins are done, no user and role creations. + // This means that this will block after trying once, because of the three attempts to read from eventChannels. + + _ = <-application.addEventChannel + _ = <-application.addEventChannel + _ = <-application.updateEventChannel + + m.Run() +} + +func TestNetworkElementAddAndSubscribeEvent(t *testing.T) { + // setup required parameters + opt := &tpb.TransportOption{ + Address: targetAAdress, + Username: targetUserAndPW, + Password: targetUserAndPW, + TransportOption: &tpb.TransportOption_GnmiTransportOption{ + GnmiTransportOption: &tpb.GnmiTransportOption{}, + }, + Tls: true, + } + + addListRequest := &mnepb.AddListRequest{ + Timestamp: integration_test_utils.GetTimestamp(), + Mne: []*mnepb.SetMne{ + { + MneId: mneID, + Address: "gnmi-target_A:7030", + Pid: pndID, + PluginId: pluginID, + MneName: "Horst", + TransportOption: opt, + }, + }, + Pid: pndID, + } + + deleteMneRequest := &mnepb.DeleteRequest{ + Timestamp: integration_test_utils.GetTimestamp(), + Pid: pndID, + Mneid: mneID, + } + + // setup gRPC services + mneService := mnepb.NewNetworkElementServiceClient(conn) + + // add one device to the controller + _, err := mneService.AddList(ctx, addListRequest) + if err != nil { + t.Error(err) + t.FailNow() + } + + // check if events are available and correct type and content + addEvent := <-application.addEventChannel + assertAddEvent(t, addEvent, mneID) + + subscribeEvent := <-application.subscribeEventChannel + assertSubscribeEvent(t, subscribeEvent, mneID) + + subscribeEvent = <-application.subscribeEventChannel + assertSubscribeEvent(t, subscribeEvent, mneID) + + // TODO: add tests fot rhis event type when update function is implemented properly + // update mne and check events + + // delete mne and check for events + _, err = mneService.Delete(ctx, deleteMneRequest) + if err != nil { + t.Error(err) + t.FailNow() + } + + deleteEvent := <-application.deleteEventChannel + assertDeleteEvent(t, deleteEvent, mneID) +} + +func TestUserEvents(t *testing.T) { + // setup required parameters + const newUserName = "new name" + + addUserRequest := &apb.CreateUsersRequest{ + Timestamp: integration_test_utils.GetTimestamp(), + User: []*apb.User{ + { + Id: userID, + Name: "user", + Roles: map[string]string{pndID: "admin"}, + Password: targetUserAndPW, + Metadata: &conflict.Metadata{ + ResourceVersion: 0, + }, + }, + }, + } + + updateUserRequest := &apb.UpdateUsersRequest{ + Timestamp: integration_test_utils.GetTimestamp(), + User: []*apb.UpdateUser{ + { + Id: userID, + Name: newUserName, + Metadata: &conflict.Metadata{}, + }, + }, + } + + deleteUserRequest := &apb.DeleteUsersRequest{ + Timestamp: integration_test_utils.GetTimestamp(), + Username: []string{newUserName}, + } + + // setup gRPC services + userService := apb.NewUserServiceClient(conn) + + // add one device to the controller + _, err := userService.CreateUsers(ctx, addUserRequest) + if err != nil { + t.Error(err) + t.FailNow() + } + + // check if event is available and correct type + addEvent := <-application.addEventChannel + assertAddEvent(t, addEvent, userID) + + // update user and check for event + _, err = userService.UpdateUsers(ctx, updateUserRequest) + if err != nil { + t.Error(err) + t.FailNow() + } + + updateEvent := <-application.updateEventChannel + assertUpdateEvent(t, updateEvent, userID) + + // delete user and check for event + _, err = userService.DeleteUsers(ctx, deleteUserRequest) + if err != nil { + t.Error(err) + t.FailNow() + } + + deleteEvent := <-application.deleteEventChannel + assertDeleteEvent(t, deleteEvent, userID) +} + +func TestRoleEvents(t *testing.T) { + // setup required parameters + const roleName = "new role" + + addRoleRequest := &apb.CreateRolesRequest{ + Timestamp: integration_test_utils.GetTimestamp(), + Roles: []*apb.Role{ + { + Id: roleID, + Name: roleName, + Description: "well, not much to see here", + Permissions: []string{"some permission"}, + }, + }, + } + + updateRolesRequest := &apb.UpdateRolesRequest{ + Timestamp: integration_test_utils.GetTimestamp(), + Roles: []*apb.Role{ + { + Id: roleID, + Name: roleName, + Description: "even less now", + }, + }, + } + + deleteRoleRequest := &apb.DeleteRolesRequest{ + Timestamp: integration_test_utils.GetTimestamp(), + RoleName: []string{roleName}, + } + + // setup gRPC services + roleService := apb.NewRoleServiceClient(conn) + + // add role and check add event + _, err := roleService.CreateRoles(ctx, addRoleRequest) + if err != nil { + t.Error(err) + t.FailNow() + } + + addEvent := <-application.addEventChannel + assertAddEvent(t, addEvent, roleID) + + // update role and check for event + _, err = roleService.UpdateRoles(ctx, updateRolesRequest) + if err != nil { + t.Error(err) + t.FailNow() + } + + updateEvent := <-application.updateEventChannel + assertUpdateEvent(t, updateEvent, roleID) + + // delete new role and check for event + _, err = roleService.DeleteRoles(ctx, deleteRoleRequest) + if err != nil { + t.Error(err) + t.FailNow() + } + + deleteEvent := <-application.deleteEventChannel + assertDeleteEvent(t, deleteEvent, roleID) +} diff --git a/integration-tests/application_tests/eventAssertion_test.go b/integration-tests/application_tests/eventAssertion_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6bf363b248f609d391fadaaeb0458353ee0fb4dd --- /dev/null +++ b/integration-tests/application_tests/eventAssertion_test.go @@ -0,0 +1,26 @@ +package integration_test_application + +import ( + "code.fbi.h-da.de/danet/gosdn/application-framework/event" + "github.com/stretchr/testify/assert" +) + +func assertAddEvent(t assert.TestingT, addEvent event.Event, ID string) { + assert.Equal(t, event.Add.String(), addEvent.Type) + assert.Equal(t, ID, addEvent.EntityID.String()) +} + +func assertUpdateEvent(t assert.TestingT, updateEvent event.Event, ID string) { + assert.Equal(t, event.Update.String(), updateEvent.Type) + assert.Equal(t, ID, updateEvent.EntityID.String()) +} + +func assertDeleteEvent(t assert.TestingT, deleteEvent event.Event, ID string) { + assert.Equal(t, event.Delete.String(), deleteEvent.Type) + assert.Equal(t, ID, deleteEvent.EntityID.String()) +} + +func assertSubscribeEvent(t assert.TestingT, subscribeEvent event.Event, ID string) { + assert.Equal(t, event.Subscribe.String(), subscribeEvent.Type) + assert.Equal(t, ID, subscribeEvent.EntityID.String()) +} diff --git a/integration-tests/example_tests/example_test.go b/integration-tests/example_tests/example_test.go index b97e86fa85c09004d7dd6167e880a8e9a5c9b51e..03f52081b1fd0c72be24cb0ae74cc4411a899537 100644 --- a/integration-tests/example_tests/example_test.go +++ b/integration-tests/example_tests/example_test.go @@ -31,6 +31,9 @@ func TestMain(m *testing.M) { if err != nil { fmt.Println(err.Error()) } + + integration_test_utils.ApplySDNConfig(conn, ctx, defaultSDNConfig) + m.Run() }