From 2aa93d64f1a5a5888c8a998084574c9042a2074e Mon Sep 17 00:00:00 2001
From: Andre Sterba <andre.sterba@stud.h-da.de>
Date: Tue, 11 Oct 2022 09:25:47 +0000
Subject: [PATCH] Add additional example application hostname-checker

See merge request danet/gosdn!376

Co-authored-by: Fabian Seidl <fabian.b.seidl@stud.h-da.de>
Co-authored-by: Malte Bauch <malte.bauch@tbnet.works>
---
 Makefile                                |   8 +-
 applications/hostname-checker/app.go    | 127 ++++++++++++++++++++++++
 applications/hostname-checker/device.go |  28 ++++++
 applications/hostname-checker/main.go   |  33 ++++++
 4 files changed, 195 insertions(+), 1 deletion(-)
 create mode 100644 applications/hostname-checker/app.go
 create mode 100644 applications/hostname-checker/device.go
 create mode 100644 applications/hostname-checker/main.go

diff --git a/Makefile b/Makefile
index 1be95526c..63557f708 100644
--- a/Makefile
+++ b/Makefile
@@ -56,7 +56,7 @@ generate-csbi-yang-models: install-tools
 	../../$(TOOLS_DIR)/go-ygot-generator-generator config.yaml gostructs.go &&\
 	go generate
 
-build: pre build-gosdn build-gosdnc build-orchestrator build-venv-manager build-arista-routing-engine-app
+build: pre build-gosdn build-gosdnc build-orchestrator build-venv-manager build-arista-routing-engine-app build-hostname-checker-app
 
 build-gosdn: pre
 	$(GOBUILD) -trimpath -o $(BUILD_ARTIFACTS_PATH)/gosdn ./controller/cmd/gosdn
@@ -73,6 +73,9 @@ build-venv-manager: pre
 build-arista-routing-engine-app: pre
 	$(GOBUILD) -trimpath -o $(BUILD_ARTIFACTS_PATH)/arista-routing-engine ./applications/arista-routing-engine
 
+build-hostname-checker-app: pre
+	$(GOBUILD) -trimpath -o $(BUILD_ARTIFACTS_PATH)/hostname-checker ./applications/hostname-checker
+
 containerize-all: containerize-gosdn containerize-gosdnc containerize-orchestrator containerize-target
 
 containerize-gosdn:
@@ -93,6 +96,9 @@ containerize-target:
 containerize-arista-routing-engine-app:
 	docker buildx build --rm -t arista-routing-engine-app -f applications/arista-routing-engine/arista-routing-engine.Dockerfile .
 
+containerize-hostname-checker-app:
+	docker buildx build --rm -t hostname-checker-app -f applications/hostname-checker/hostname-checker.Dockerfile .
+
 containerlab-start: containerize-all
 	sudo containerlab deploy --topo gosdn.clab.yaml
 
diff --git a/applications/hostname-checker/app.go b/applications/hostname-checker/app.go
new file mode 100644
index 000000000..01d6867c7
--- /dev/null
+++ b/applications/hostname-checker/app.go
@@ -0,0 +1,127 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"code.fbi.h-da.de/danet/gosdn/api/go/gosdn/device"
+
+	"github.com/google/uuid"
+	"github.com/openconfig/ygot/ygot"
+
+	"code.fbi.h-da.de/danet/gosdn/application-framework/event"
+	"code.fbi.h-da.de/danet/gosdn/application-framework/models"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+)
+
+// Application is an example for a sdn application.
+type Application struct {
+	eventService   event.ServiceInterface
+	stopChannel    chan os.Signal
+	grpcClientConn *grpc.ClientConn
+}
+
+// Run runs the application.
+func (a *Application) Run() {
+	signal.Notify(a.stopChannel, os.Interrupt, syscall.SIGTERM)
+
+	a.eventService.SubscribeToEventType([]event.TypeToCallbackTuple{
+		{Type: event.Update, Callback: a.callback},
+	})
+	a.eventService.SetupEventReciever(a.stopChannel)
+
+	conn, err := grpc.Dial("localhost:55055", grpc.WithTransportCredentials(insecure.NewCredentials()))
+	if err != nil {
+		panic(err)
+	}
+
+	a.grpcClientConn = conn
+
+	var forever chan struct{}
+
+	go func() {
+		for {
+			select {
+			case <-a.stopChannel:
+				close(forever)
+				_ = a.grpcClientConn.Close()
+
+				return
+			}
+		}
+	}()
+
+	<-forever
+}
+
+func (a *Application) callback(event *event.Event) {
+	ctx := context.Background()
+	deviceServer := device.NewDeviceServiceClient(a.grpcClientConn)
+
+	request := &device.GetDeviceRequest{
+		Timestamp: time.Now().UnixNano(),
+		DeviceID:  event.EntityID.String(),
+	}
+
+	response, err := deviceServer.Get(ctx, request)
+	if err != nil {
+		fmt.Printf("Error %+v\n ", err)
+		return
+	}
+
+	fmt.Printf("\n[APP] Device-ID: %v, Device-Name: %+v \n", response.Device.Id, response.Device.Name)
+
+	d := NewDevice(uuid.MustParse(response.Device.Id), response.Device.Name)
+
+	// Create 'root' path to be able to load the whole model from the store.
+	path, err := ygot.StringToPath("/", ygot.StructuredPath)
+	if err != nil {
+		panic(err)
+	}
+
+	// Use unmarshall from the devices SBI to unmarshall ygot json in go struct.
+	err = models.Unmarshal([]byte(response.Device.Model), path, &d.Model)
+	if err != nil {
+		panic(err)
+	}
+
+	if *d.Model.System.Config.Hostname != d.Name {
+		fmt.Printf("[APP] Device.Name (%s) doesn't match Device.Hostname (%s) in model! Updating...\n",
+			d.Name,
+			*d.Model.System.Config.Hostname,
+		)
+
+		*d.Model.System.Config.Hostname = d.Name
+
+		modelAsString, err := models.GetModelAsString(&d.Model)
+		if err != nil {
+			panic(err)
+		}
+
+		requestUpdate := &device.UpdateDeviceRequest{
+			Timestamp: time.Now().UnixNano(),
+			Device: &device.Device{
+				Id:    d.UUID.String(),
+				Name:  d.Name,
+				Model: modelAsString,
+			},
+		}
+
+		updateResponse, err := deviceServer.Update(ctx, requestUpdate)
+		if err != nil {
+			panic(err)
+		}
+
+		fmt.Printf("[APP] Update response: %+v", updateResponse)
+	} else {
+		fmt.Printf("[APP] Device.Name (%s) does match Device.Hostname (%s) in model! Nothing to do for me...\n",
+			d.Name,
+			*d.Model.System.Config.Hostname,
+		)
+	}
+}
diff --git a/applications/hostname-checker/device.go b/applications/hostname-checker/device.go
new file mode 100644
index 000000000..3c2a1af84
--- /dev/null
+++ b/applications/hostname-checker/device.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+	"code.fbi.h-da.de/danet/gosdn/models/generated/arista"
+
+	"github.com/google/uuid"
+)
+
+// Device is a device.
+type Device struct {
+	// UUID represents the Devices UUID
+	UUID uuid.UUID
+
+	// Name is the device's human readable Name
+	Name string
+
+	// Device embeds a ygot.GoStruct containing the device details
+	Model arista.Device
+}
+
+// NewDevice creates a new device.
+func NewDevice(id uuid.UUID, name string) *Device {
+	return &Device{
+		UUID:  id,
+		Model: arista.Device{},
+		Name:  name,
+	}
+}
diff --git a/applications/hostname-checker/main.go b/applications/hostname-checker/main.go
new file mode 100644
index 000000000..d68ebc980
--- /dev/null
+++ b/applications/hostname-checker/main.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+	"os"
+
+	"code.fbi.h-da.de/danet/gosdn/application-framework/event"
+	"code.fbi.h-da.de/danet/gosdn/application-framework/registration"
+	"github.com/sirupsen/logrus"
+)
+
+func main() {
+	queueCredentials, err := registration.Register("localhost:55055", "hostname-checker", "SecurePresharedToken")
+	if err != nil {
+		logrus.Errorf("failed to register application on control plane. %v", err)
+		os.Exit(1)
+	}
+
+	eventService, err := event.NewEventService(
+		queueCredentials,
+		[]event.Topic{event.Device},
+	)
+	if err != nil {
+		logrus.Errorf("failed to create event service. %v", err)
+		os.Exit(1)
+	}
+
+	app := &Application{
+		eventService: eventService,
+		stopChannel:  make(chan os.Signal, 1),
+	}
+
+	app.Run()
+}
-- 
GitLab